new budget report
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
venv/
|
venv/
|
||||||
|
env/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
**/__pycache__
|
||||||
@@ -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
100
commands/budget
Executable 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()
|
||||||
@@ -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
2
commands/net-worth
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
bean-report ledger/main.beancount networth
|
||||||
0
commands/objectives
Normal file
0
commands/objectives
Normal file
9
ledger/budget.md
Normal file
9
ledger/budget.md
Normal 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
|
||||||
Binary file not shown.
259
reports/balsheet/20231217/balsheet.html
Normal file
259
reports/balsheet/20231217/balsheet.html
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user