From c1e103010dccf5dc1518ef5f15b2b1562120059a Mon Sep 17 00:00:00 2001 From: Roger Oriol Date: Wed, 27 Dec 2023 00:03:54 +0100 Subject: [PATCH] cash flow statement report --- commands/cash-flow-statement | 152 +++++++++++++++++++++++++- ledger/accounts.beancount | 1 + ledger/transactions/2024/01.beancount | 57 +--------- 3 files changed, 156 insertions(+), 54 deletions(-) diff --git a/commands/cash-flow-statement b/commands/cash-flow-statement index 7410ab3..2fd8644 100755 --- a/commands/cash-flow-statement +++ b/commands/cash-flow-statement @@ -1,2 +1,150 @@ -#!/usr/bin/env bash -echo "TO DO" \ No newline at end of file +#!/usr/bin/env python3 +from beancount import loader +from beancount.query import query +from beancount.parser import printer +import argparse +from tabulate import tabulate +from decimal import Decimal +from beancount.core.amount import Amount, sub, mul +from datetime import date +from dateutil.relativedelta import relativedelta + +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 draw_line(): + print('─' * 30) + +def get_income_val(obj, key): + if key in obj: + amount = obj[key].get_only_position().units + amount = mul(amount, Decimal(-1.0)) + return amount.to_string() + else: + return None + +def get_expense_val(obj, key): + if key in obj: + amount = obj[key].get_only_position().units + return amount.to_string() + else: + return None + +def print_expenses_table(expenses): + table = [] + for key, expense in expenses.items(): + parts = key.split(":", 1) + table.append([parts[1], get_expense_val(expenses, key)]) + print(tabulate(table)) + +def get_total_inflows(income): + sum = 0 + for account, balance in income.items(): + sum = balance if sum == 0 else sum + balance + if sum != 0 and sum.get_only_position() != None: + result = sum.get_only_position().units + return Amount(Decimal(round(result.number, 2) * Decimal(-1.0)), result.currency) + else: + return Amount(Decimal(0), "EUR") + +def get_total_outflows(expenses): + sum = 0 + for account, balance in expenses.items(): + sum = balance if sum == 0 else sum + balance + if sum != 0 and sum.get_only_position() != None: + result = sum.get_only_position().units + return Amount(Decimal(round(result.number, 2)), result.currency) + else: + return Amount(Decimal(0), "EUR") + +def print_report(start_date, period, income, expenses): + print(f"{bcolors.BOLD}Cash Flow Statement (period={period}, start_date={start_date}){bcolors.ENDC}") + draw_line() + print(f"{bcolors.BOLD}Inflows{bcolors.ENDC}") + print(f"\t{bcolors.BOLD}Income{bcolors.ENDC}") + print(tabulate([ + ["Salari", get_income_val(income,"Income:Work:Zurich:Salari")], + ["Tickets Restaurant", get_income_val(income,"Income:Work:Zurich:TicketsRestaurant")], + ["Targeta Transport", get_income_val(income,"Income:Work:Zurich:TargetaTransport")], + ["Seguro Mèdic", get_income_val(income,"Income:Work:Zurich:SeguroMedic")], + ["Gimnàs", get_income_val(income,"Income:Work:Zurich:Gimnas")] + ])) + print(f"\t{bcolors.BOLD}Income from Investment{bcolors.ENDC}") + print(tabulate([ + ["Capital Gains", get_income_val(income,"Income:Invest:R4:CapitalGains")], + ["Dividends", get_income_val(income,"Income:Invest:R4:Dividends")], + ["Rentabilitat Estalvis", get_income_val(income,"Income:Savings:Caixabank:RentabilitatEstalvis")] + ])) + print(f"\t{bcolors.BOLD}Other Inflows{bcolors.ENDC}") + print(tabulate([ + ["Transferències", get_income_val(income,"Income:Other:Caixabank:Transferencia")], + ["Bizum", get_income_val(income,"Income:Other:Caixabank:Bizum")], + ["Devolucions", get_income_val(income,"Income:Other:Devolucions")] + ])) + print(tabulate([ + ["Total Inflows", f"{bcolors.BOLD}{get_total_inflows(income).to_string()}{bcolors.ENDC}"] + ])) + draw_line() + print(f"{bcolors.BOLD}Outflows{bcolors.ENDC}") + print_expenses_table(expenses) + print(tabulate([ + ["Total Outflows", f"{bcolors.BOLD}{get_total_outflows(expenses).to_string()}{bcolors.ENDC}"] + ])) + draw_line() + net_cash_flow = sub(get_total_inflows(income), get_total_outflows(expenses)) + print(tabulate([ + ["NET CASH FLOW", f"{bcolors.BOLD}{bcolors.OKGREEN if net_cash_flow.number >= 0 else bcolors.FAIL}{net_cash_flow.to_string()}{bcolors.ENDC}"] + ])) + +def get_income(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 + income_query = f"SELECT account, sum(position) FROM OPEN ON {start_date} CLOSE ON {end_date.isoformat()} WHERE account ~ \"Income\"" + rtypes, rrows = query.run_query( + entries, options, income_query) + income = {} + for row in rrows: + income[row.account] = row.sum_position + return income + +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 main(): + parser = argparse.ArgumentParser(description='Generate cash flow 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) + + income = get_income(entries, options, period, start_date) + expenses = get_expenses(entries, options, period, start_date) + print_report(start_date, period, income, expenses) + +main() \ No newline at end of file diff --git a/ledger/accounts.beancount b/ledger/accounts.beancount index b6f7d81..4fa1d3f 100644 --- a/ledger/accounts.beancount +++ b/ledger/accounts.beancount @@ -32,6 +32,7 @@ 1970-01-01 open Income:Savings:Caixabank:RentabilitatEstalvis EUR 1970-01-01 open Income:Invest:R4:Dividends EUR 1970-01-01 open Income:Invest:R4:CapitalGains EUR +1970-01-01 open Income:Other:Devolucions EUR 1970-01-01 open Expenses:R4:Comissions EUR 1970-01-01 open Expenses:Taxes:IRPF EUR diff --git a/ledger/transactions/2024/01.beancount b/ledger/transactions/2024/01.beancount index 228bc3c..3787274 100644 --- a/ledger/transactions/2024/01.beancount +++ b/ledger/transactions/2024/01.beancount @@ -26,7 +26,6 @@ Assets:Invest:R4:MSFT 4 MSFT {341.8 USD} Equity:Opening-Balances:USD -<<<<<<< HEAD 2024-01-01 balance Assets:Liquid:Caixabank:Corrent 18903.80 EUR 2024-01-01 balance Assets:Liquid:Caixabank:Estalvi 12666.49 EUR 2024-01-01 balance Assets:Liquid:R4:EUR 44.04 EUR @@ -38,55 +37,29 @@ 2024-01-01 balance Assets:Invest:R4:PLTR 10 PLTR 2024-01-01 balance Assets:Invest:R4:MSFT 4 MSFT 2024-01-01 balance Assets:Benefits:Edenred:TicketsRestaurant 0 EUR -2024-01-01 balance Assets:Benefits:Edenred:TarjetaTransport 0 EUR +2024-01-01 balance Assets:Benefits:Edenred:TargetaTransport 0 EUR 2024-01-01 balance Assets:PersonalProperty:VivendaPrincipal 0 EUR 2024-01-01 balance Assets:PersonalProperty:Cotxe 10000 EUR 2024-01-01 balance Assets:PersonalProperty:JoiesArtCollecionables 1250 EUR 2024-01-01 balance Assets:PersonalProperty:MetallsPreciosos 0 EUR 2024-01-01 balance Assets:PersonalProperty:AltresPropietats 0 EUR 2024-01-01 balance Assets:Debt:DeutesPerCobrar 0 EUR -======= -2024-01-01 balance Assets:Liquid:Caixabank:Corrent 18903.80 EUR -2024-01-01 balance Assets:Liquid:Caixabank:Estalvi 12666.49 EUR -2024-01-01 balance Assets:Liquid:R4:EUR 44.04 EUR -2024-01-01 balance Assets:Invest:R4:Amundi:MSCIWRLD 86.005 AMNDMSCIWRLD -2024-01-01 balance Assets:Invest:R4:Vanguard:EMMK 14.99 VANEMMK -2024-01-01 balance Assets:Invest:R4:Fidelity:GLTECH 344.47 FIGLTECH -2024-01-01 balance Assets:Invest:R4:Amundi:SUSTINC 11.295 AMNDSUSINC -2024-01-01 balance Assets:Invest:R4:BNP:DISTECH 0.359 BNPDISTECH -2024-01-01 balance Assets:Invest:R4:PLTR 10 PLTR -2024-01-01 balance Assets:Invest:R4:MSFT 4 MSFT -2024-01-01 balance Assets:Benefits:Edenred:TicketsRestaurant 0 EUR -2024-01-01 balance Assets:Benefits:Edenred:TargetaTransport 0 EUR -2024-01-01 balance Assets:PersonalProperty:VivendaPrincipal 0 EUR -2024-01-01 balance Assets:PersonalProperty:Cotxe 10000 EUR -2024-01-01 balance Assets:PersonalProperty:JoiesArtCollecionables 1250 EUR -2024-01-01 balance Assets:PersonalProperty:MetallsPreciosos 0 EUR -2024-01-01 balance Assets:PersonalProperty:AltresPropietats 0 EUR -2024-01-01 balance Assets:Debt:DeutesPerCobrar 0 EUR ->>>>>>> ac34f2f (balance sheet report) 2024-01-01 * "Zurich" "Cuota gimnàs Andjoy" - amortize_months: 12 + ;amortize_months: 12 Expenses:Gimnàs 508.80 EUR Income:Work:Zurich:Gimnas 2024-01-01 * "Zurich" "Cuota seguro salut Cigna Salud OPT (solo cobertura extra hospitalaria)" - amortize_months: 12 + ;amortize_months: 12 Expenses:Medic 414.60 EUR Income:Work:Zurich:SeguroMedic 2024-01-01 * "Zurich" "Targeta Transport" -<<<<<<< HEAD - Assets:Benefits:Edenred:TarjetaTransport 40 EUR - Income:Work:Zurich:TarjetaTransport -======= - Assets:Benefits:Edenred:TargetaTransport 40 EUR + Assets:Benefits:Edenred:TargetaTransport 40 EUR Income:Work:Zurich:TargetaTransport ->>>>>>> ac34f2f (balance sheet report) 2024-01-01 * "Zurich" "Targeta Restaurant" Assets:Benefits:Edenred:TicketsRestaurant 209 EUR Income:Work:Zurich:TicketsRestaurant -<<<<<<< HEAD 2024-02-01 balance Assets:Liquid:Caixabank:Corrent 18903.80 EUR 2024-02-01 balance Assets:Liquid:Caixabank:Estalvi 12666.49 EUR ;2024-02-01 balance Assets:Liquid:R4:EUR 44.04 EUR @@ -98,30 +71,10 @@ 2024-02-01 balance Assets:Invest:R4:PLTR 10 PLTR ;2024-02-01 balance Assets:Invest:R4:MSFT 4 MSFT 2024-02-01 balance Assets:Benefits:Edenred:TicketsRestaurant 209 EUR -2024-02-01 balance Assets:Benefits:Edenred:TarjetaTransport 40 EUR +2024-02-01 balance Assets:Benefits:Edenred:TargetaTransport 40 EUR 2024-02-01 balance Assets:PersonalProperty:VivendaPrincipal 0 EUR 2024-02-01 balance Assets:PersonalProperty:Cotxe 10000 EUR 2024-02-01 balance Assets:PersonalProperty:JoiesArtCollecionables 1250 EUR 2024-02-01 balance Assets:PersonalProperty:MetallsPreciosos 0 EUR 2024-02-01 balance Assets:PersonalProperty:AltresPropietats 0 EUR 2024-02-01 balance Assets:Debt:DeutesPerCobrar 0 EUR -======= -2024-02-01 balance Assets:Liquid:Caixabank:Corrent 18903.80 EUR -2024-02-01 balance Assets:Liquid:Caixabank:Estalvi 12666.49 EUR -2024-02-01 balance Assets:Liquid:R4:EUR 44.04 EUR -2024-02-01 balance Assets:Invest:R4:Amundi:MSCIWRLD 86.005 AMNDMSCIWRLD -2024-02-01 balance Assets:Invest:R4:Vanguard:EMMK 14.99 VANEMMK -2024-02-01 balance Assets:Invest:R4:Fidelity:GLTECH 344.47 FIGLTECH -2024-02-01 balance Assets:Invest:R4:Amundi:SUSTINC 11.295 AMNDSUSINC -2024-02-01 balance Assets:Invest:R4:BNP:DISTECH 0.359 BNPDISTECH -2024-02-01 balance Assets:Invest:R4:PLTR 10 PLTR -2024-02-01 balance Assets:Invest:R4:MSFT 4 MSFT -2024-02-01 balance Assets:Benefits:Edenred:TicketsRestaurant 209 EUR -2024-02-01 balance Assets:Benefits:Edenred:TargetaTransport 40 EUR -2024-02-01 balance Assets:PersonalProperty:VivendaPrincipal 0 EUR -2024-02-01 balance Assets:PersonalProperty:Cotxe 10000 EUR -2024-02-01 balance Assets:PersonalProperty:JoiesArtCollecionables 1250 EUR -2024-02-01 balance Assets:PersonalProperty:MetallsPreciosos 0 EUR -2024-02-01 balance Assets:PersonalProperty:AltresPropietats 0 EUR -2024-02-01 balance Assets:Debt:DeutesPerCobrar 0 EUR ->>>>>>> ac34f2f (balance sheet report)