new budget report

This commit is contained in:
Roger Oriol
2023-12-17 21:54:59 +01:00
parent ae8061b7e9
commit 9b9929987a
11 changed files with 381 additions and 3 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
venv/ venv/
env/
.DS_Store .DS_Store
**/__pycache__

View File

@@ -1,2 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
echo "TO DO" now="$(date +'%Y%m%d')"
mkdir -p reports/balsheet/$now
bean-report ledger/main.beancount balsheet > reports/balsheet/$now/balsheet.html

100
commands/budget Executable file
View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
from beancount import loader
from beancount.query import query
from beancount.core.data import Custom
from beancount.core.amount import Amount, add, sub
from beancount.parser import printer
import argparse
from datetime import date
from dateutil.relativedelta import relativedelta
from tabulate import tabulate
from decimal import Decimal
from functools import reduce
class bcolors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
def get_budget_entries(entries, period, start_date):
budgets = []
for entry in entries:
if isinstance(entry, Custom) and entry.values[1].value == period and entry.date <= date.fromisoformat(start_date):
budgets.append({ "date": entry.date, "account": entry.values[0].value, "period": entry.values[1].value, "budget": entry.values[2].value })
return budgets
def get_expenses(entries, options, period, start_date):
period_delta = relativedelta(months=1) if period == "monthly" else relativedelta(years=1)
end_date = date.fromisoformat(start_date) + period_delta
expenses_query = f"SELECT account, sum(position) FROM OPEN ON {start_date} CLOSE ON {end_date.isoformat()} WHERE account ~ \"Expenses\""
rtypes, rrows = query.run_query(
entries, options, expenses_query)
expenses = {}
for row in rrows:
expenses[row.account] = row.sum_position
return expenses
def build_budget(budget_entries, expenses):
result = []
for entry in budget_entries:
expense = Amount(Decimal(0), entry["budget"].currency)
expense_perc = 0
remaining = entry["budget"]
if entry["account"] in expenses:
expense = expenses[entry["account"]].get_only_position()
expense_perc = (expense.units.number / entry["budget"].number) * 100
remaining = sub(remaining, expense.units)
result.append({
"Account": entry["account"],
"Budget": entry["budget"].to_string(),
"Expense": expense,
"Expense (%)": "{}{:,.2f}%{}".format(bcolors.FAIL if expense_perc >= 100 else '', expense_perc, bcolors.ENDC),
"Remaining": remaining
})
return result
def print_report(budget_report, period, start_date, budget_sum, expenses_sum):
print(f"Budget Report (period={period}, start_date={start_date})")
print(f"Budget: {budget_sum}")
print(f"{bcolors.FAIL if expenses_sum >= budget_sum else ''}Expenses: {expenses_sum}{bcolors.ENDC}")
headings = ['Account', 'Budget', 'Expense', '(%)', 'Remaining',]
print(tabulate(budget_report, headers="keys", numalign="right", floatfmt=".2f"))
def main():
parser = argparse.ArgumentParser(description='Generate budget report')
parser.add_argument('start_date', metavar='start_date', type=str, nargs=1,
help='Start date (end date will be one month after if monthly report or one year after if yearly report)')
parser.add_argument('-p', metavar='period', type=str, choices=["monthly", "yearly"], default="monthly", required=False,
help='Period (monthly or yearly)')
args = parser.parse_args()
start_date = args.start_date[0]
period = args.p
filename = "ledger/main.beancount"
entries, errors, options = loader.load_file(filename)
if errors:
printer.print_errors(errors)
budget_entries = get_budget_entries(entries, period, start_date)
# TODO: Multiple currencies
budget_sum = reduce(lambda a, b: add(a, b["budget"]), budget_entries, Amount(Decimal(0), budget_entries[0]["budget"].currency))
expenses = get_expenses(entries, options, period, start_date)
filtered_expenses = {}
for entry in budget_entries:
if entry["account"] in expenses:
filtered_expenses[entry["account"]] = expenses[entry["account"]]
expenses_sum = reduce(lambda a, b: add(a, b.get_only_position().units),
filtered_expenses.values(),
Amount(Decimal(0), budget_entries[0]["budget"].currency))
budget_report = build_budget(budget_entries, expenses)
print_report(budget_report, period, start_date, budget_sum, expenses_sum)
main()

View File

@@ -1,2 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
echo "TO DO" now="$(date +'%Y%m%d')"
mkdir -p reports/income/$now
bean-report ledger/main.beancount income > reports/income/$now/income.html

2
commands/net-worth Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
bean-report ledger/main.beancount networth

0
commands/objectives Normal file
View File

9
ledger/budget.md Normal file
View File

@@ -0,0 +1,9 @@
# Budget
## Menjar fora
4.5 x 5.45
4.5 x 16.5
4.5 x 50
= 323.78
- 209 (Targ. Restaurant)
= 114.78

View File

@@ -0,0 +1,259 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<title></title>
</head>
<body>
<div class="halfleft">
<div id="assets">
<h3>Assets</h3>
<table class="fullwidth tree-table">
<thead>
<tr>
<th class="first">Account</th>
<th>EUR</th>
<th>Other</th>
</tr>
</thead>
<tr class="">
<td class="tree-node-name">Assets</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Benefits</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Benefits:Edenred</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Benefits:Edenred:TarjetaTransport</td>
<td class="num">40</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Benefits:Edenred:TicketsRestaurant</td>
<td class="num">209</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Debt</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Debt:DeutesPerCobrar</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:Amundi</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:Amundi:MSCIWRLD</td>
<td class="num">20436.50810</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:Amundi:SUSTINC</td>
<td class="num">709.77780</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:BNP</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:BNP:DISTECH</td>
<td class="num"></td>
<td class="num">788.20245 USD</td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:Fidelity</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:Fidelity:GLTECH</td>
<td class="num">14143.9382</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:MSFT</td>
<td class="num"></td>
<td class="num">1367.2 USD</td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:PLTR</td>
<td class="num"></td>
<td class="num">160.250 USD</td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:Vanguard</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Invest:R4:Vanguard:EMMK</td>
<td class="num">2664.81727</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Liquid</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Liquid:Caixabank</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Liquid:Caixabank:Corrent</td>
<td class="num">18903.80</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Liquid:Caixabank:Estalvi</td>
<td class="num">12666.49</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Liquid:R4</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:Liquid:R4:EUR</td>
<td class="num">44.04</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:PersonalProperty</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:PersonalProperty:AltresPropietats</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:PersonalProperty:Cotxe</td>
<td class="num">10000</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:PersonalProperty:JoiesArtCollecionables</td>
<td class="num">1250</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:PersonalProperty:MetallsPreciosos</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Assets:PersonalProperty:VivendaPrincipal</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="totals">
<td class="tree-node-name"><span class="totals-label"></span></td>
<td class="num">81068.37137</td>
<td class="num">2315.65245 USD</td>
</tr>
</table>
</div>
</div>
<div class="halfright">
<div id="liabilities">
<h3>Liabilities</h3>
<table class="fullwidth tree-table">
<thead>
<tr>
<th class="first">Account</th>
<th>EUR</th>
<th>Other</th>
</tr>
</thead>
<tr class="">
<td class="tree-node-name">Liabilities</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="totals">
<td class="tree-node-name"><span class="totals-label"></span></td>
<td class="num"></td>
<td class="num"></td>
</tr>
</table>
</div>
<div class="spacer">
</div>
<div id="equity">
<h3>Equity</h3>
<table class="fullwidth tree-table">
<thead>
<tr>
<th class="first">Account</th>
<th>EUR</th>
<th>Other</th>
</tr>
</thead>
<tr class="">
<td class="tree-node-name">Equity</td>
<td class="num"></td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Equity:Opening-Balances</td>
<td class="num">-80819.37</td>
<td class="num"></td>
</tr>
<tr class="">
<td class="tree-node-name">Equity:Opening-Balances:USD</td>
<td class="num"></td>
<td class="num">-2315.652 USD</td>
</tr>
<tr class="totals">
<td class="tree-node-name"><span class="totals-label"></span></td>
<td class="num">-80819.37</td>
<td class="num">-2315.652 USD</td>
</tr>
</table>
</div>
</div>
</body>
</html>

View File

@@ -3,6 +3,7 @@ beancount==2.3.6
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
blinker==1.7.0 blinker==1.7.0
bottle==0.12.25 bottle==0.12.25
budget-report==0.4
cachetools==5.3.2 cachetools==5.3.2
certifi==2023.11.17 certifi==2023.11.17
chardet==5.2.0 chardet==5.2.0
@@ -45,6 +46,7 @@ rsa==4.9
simplejson==3.19.2 simplejson==3.19.2
six==1.16.0 six==1.16.0
soupsieve==2.5 soupsieve==2.5
tabulate==0.9.0
uritemplate==4.1.1 uritemplate==4.1.1
urllib3==2.1.0 urllib3==2.1.0
Werkzeug==3.0.1 Werkzeug==3.0.1