From 9b9929987aaffdb7c9ac0d6e15d1ef85baa6a603 Mon Sep 17 00:00:00 2001 From: Roger Oriol Date: Sun, 17 Dec 2023 21:54:59 +0100 Subject: [PATCH] new budget report --- .gitignore | 4 +- commands/balance-sheet | 4 +- commands/budget | 100 +++++++ commands/income-statement | 4 +- commands/net-worth | 2 + commands/objectives | 0 ledger/budget.md | 9 + .../__pycache__/amortize_over.cpython-311.pyc | Bin 5072 -> 0 bytes reports/balsheet/20231217/balsheet.html | 259 ++++++++++++++++++ .../{2023/12.html => 20231217/income.html} | 0 requirements.txt | 2 + 11 files changed, 381 insertions(+), 3 deletions(-) create mode 100755 commands/budget create mode 100755 commands/net-worth create mode 100644 commands/objectives create mode 100644 ledger/budget.md delete mode 100644 ledger/plugins/__pycache__/amortize_over.cpython-311.pyc create mode 100644 reports/balsheet/20231217/balsheet.html rename reports/income/{2023/12.html => 20231217/income.html} (100%) diff --git a/.gitignore b/.gitignore index bfd854c..f08a12f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ venv/ -.DS_Store \ No newline at end of file +env/ +.DS_Store +**/__pycache__ \ No newline at end of file diff --git a/commands/balance-sheet b/commands/balance-sheet index 7410ab3..7a31fcf 100755 --- a/commands/balance-sheet +++ b/commands/balance-sheet @@ -1,2 +1,4 @@ #!/usr/bin/env bash -echo "TO DO" \ No newline at end of file +now="$(date +'%Y%m%d')" +mkdir -p reports/balsheet/$now +bean-report ledger/main.beancount balsheet > reports/balsheet/$now/balsheet.html \ No newline at end of file diff --git a/commands/budget b/commands/budget new file mode 100755 index 0000000..4a23991 --- /dev/null +++ b/commands/budget @@ -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() \ No newline at end of file diff --git a/commands/income-statement b/commands/income-statement index 7410ab3..085782c 100755 --- a/commands/income-statement +++ b/commands/income-statement @@ -1,2 +1,4 @@ #!/usr/bin/env bash -echo "TO DO" \ No newline at end of file +now="$(date +'%Y%m%d')" +mkdir -p reports/income/$now +bean-report ledger/main.beancount income > reports/income/$now/income.html \ No newline at end of file diff --git a/commands/net-worth b/commands/net-worth new file mode 100755 index 0000000..176987a --- /dev/null +++ b/commands/net-worth @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +bean-report ledger/main.beancount networth \ No newline at end of file diff --git a/commands/objectives b/commands/objectives new file mode 100644 index 0000000..e69de29 diff --git a/ledger/budget.md b/ledger/budget.md new file mode 100644 index 0000000..dc7fdc1 --- /dev/null +++ b/ledger/budget.md @@ -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 \ No newline at end of file diff --git a/ledger/plugins/__pycache__/amortize_over.cpython-311.pyc b/ledger/plugins/__pycache__/amortize_over.cpython-311.pyc deleted file mode 100644 index 79aff2462d307e619ed05f29753e1ddeeac519cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5072 zcmdT|O>7j&6|SEDpB|459>W-7x5qJN44&~CFe?L#oPb$yaCU*+B@U}ed%Cd4p6Q-c z_W-tfHdd5_wX(EUgoLAjEXr|H97pSp ztRv@~a&m&O+^p`(dUD<=FRQz=zMOx`4|UH}z=1F)c_rUN2SShGt(K{v&`$`?iy3L~{GB|ORlKVrwNr!b+6eSW32Nq!kW*xBR<&!_IC#~Mqr$QG(V$uV z-IT9uJ9@Ehm(j6;rc|v6?xXw)zf>dZeRl|THUs644?7@Qq~ zaZ@OUcm-ipRy2qk>1;uQK$;gJ+G>gCWOZC`W_FTI7&w|K~1_aKB>mV%nHG=S+!~ew!+(@MciWrGHN}r6Po`l z7tJhIdxNuS8q0(4XjqmkkC=x@CdIv$w-&suJv+036_oMqRGq<74d|AeWq!*q(l3yf zRLcid7VK4;rfdHG_T%JdkVn)cQD!hno>S6=91KvC5a%^>HY1$q64jKtZ$o%s8xk+=-ci`dB1LMe8p96M9uyO%Gn-ZA-o&-tzmzH4ov#K+5g z+~DITzvr9FUteB3T;hAne6PWtiVubM*H{ZBzOT&p8SL2%cC2}fV3*XZwP>+8SX05p>Ht=FvbCJ19)~89oeA?<(%O(3b$$nWH=T9r&8(k z*AA){fr|o@$yXFN#T}OK*98%_ZxLI(nuonefz>HSbb@cNn&r)7l2Ihp+EeXFX^^o~ zwN5%QY&6*qzvM@C@++vQ*MKY`)7!Gz^ILsMH(NWF?<|MSX!m-5DH=Ci9cJ{%^XT!7 z=<%nYm!hZ3(bKe#?=phje>(c;mhs`GQu5M2E*PJG@ob_LyIzi6F9oj~uInTUg3}Gy z2Ms^GRFOCBKpEiVDCo2SO*GdSVYeXc0ka-SYE1x7M{}$fqyl;et3&H?qX)q3ZXnZX zmkuD4(;XBH+9W#Z8ma{VG3rLz5np4|aJkojH^Tch}@X0oiUJU7hUEqAku025XE1+?xKGz?SfcZnp^4;wC;KhZ0UOvw9iN7KH;}{P*@K)ji)E4R_0pkwTDo7TyLOx z<`*QQbGxiEY@ZcY`OmuV#Y2;?XrNgf8LM6IBzwln6(Hfwijc@4#icd4MrgMcA+M+! zU}QBx=nfGo0V0M+F65@+ZslqTtd%@}5*4BQM!G1WvaK*vH@i>^f~w_} zQ>@Esseh?0o~IeHz77bHRZCfWX!kZ&mOSeWAlyCVvioUwd6`f$tHJ zWp^cj{NZmxUx!w4$#<~qJ81Y0n&Hm1aU&cve6h`zy(@D@bfDaFWa+}oQ0r>sZr3_j zZi|)M4wu`KrO;40G_*8shTB)ZS`*7*p&UL`4kwpBe%TRQA9-}L)N!obact@5FZ&XY zepBi@TkbnsL2d2nWo(W_I>G?;P0Z9QaFzarRSl>|({q4Nq{d5vAWVYIF`j^T5w&6X^29 zCJ!mPdxP&@50?0&W&Wtap3MWo{j3xk{5o*{-VA&6bJK;W-`EJo zjUa%Boq*!s<`za^xok2Xwt{K6>C&4$T)+vO3Q;#{}Ti@xN zA{DauBQge`Xe?6y3Iw)+<0=D)>oriU`PoEA4EtlE!-oCYL{Y>3m}tnbKbtMWQn2Dd uk?z$?M)$CB{8FiXqTD`Vptg! + + + + + + + + + + +
+ +
+

Assets

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AccountEUROther
Assets
Assets:Benefits
Assets:Benefits:Edenred
Assets:Benefits:Edenred:TarjetaTransport40
Assets:Benefits:Edenred:TicketsRestaurant209
Assets:Debt
Assets:Debt:DeutesPerCobrar
Assets:Invest
Assets:Invest:R4
Assets:Invest:R4:Amundi
Assets:Invest:R4:Amundi:MSCIWRLD20436.50810
Assets:Invest:R4:Amundi:SUSTINC709.77780
Assets:Invest:R4:BNP
Assets:Invest:R4:BNP:DISTECH788.20245 USD
Assets:Invest:R4:Fidelity
Assets:Invest:R4:Fidelity:GLTECH14143.9382
Assets:Invest:R4:MSFT1367.2 USD
Assets:Invest:R4:PLTR160.250 USD
Assets:Invest:R4:Vanguard
Assets:Invest:R4:Vanguard:EMMK2664.81727
Assets:Liquid
Assets:Liquid:Caixabank
Assets:Liquid:Caixabank:Corrent18903.80
Assets:Liquid:Caixabank:Estalvi12666.49
Assets:Liquid:R4
Assets:Liquid:R4:EUR44.04
Assets:PersonalProperty
Assets:PersonalProperty:AltresPropietats
Assets:PersonalProperty:Cotxe10000
Assets:PersonalProperty:JoiesArtCollecionables1250
Assets:PersonalProperty:MetallsPreciosos
Assets:PersonalProperty:VivendaPrincipal
81068.371372315.65245 USD
+ +
+ +
+
+ +
+

Liabilities

+ + + + + + + + + + + + + + + + + + +
AccountEUROther
Liabilities
+ +
+
+
+
+

Equity

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AccountEUROther
Equity
Equity:Opening-Balances-80819.37
Equity:Opening-Balances:USD-2315.652 USD
-80819.37-2315.652 USD
+ +
+ +
+ + + + diff --git a/reports/income/2023/12.html b/reports/income/20231217/income.html similarity index 100% rename from reports/income/2023/12.html rename to reports/income/20231217/income.html diff --git a/requirements.txt b/requirements.txt index 750fb45..973d349 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ beancount==2.3.6 beautifulsoup4==4.12.2 blinker==1.7.0 bottle==0.12.25 +budget-report==0.4 cachetools==5.3.2 certifi==2023.11.17 chardet==5.2.0 @@ -45,6 +46,7 @@ rsa==4.9 simplejson==3.19.2 six==1.16.0 soupsieve==2.5 +tabulate==0.9.0 uritemplate==4.1.1 urllib3==2.1.0 Werkzeug==3.0.1