diff --git a/commands/balance-sheet b/commands/balance-sheet index 7a31fcf..6df6c6e 100755 --- a/commands/balance-sheet +++ b/commands/balance-sheet @@ -1,4 +1,275 @@ -#!/usr/bin/env bash -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 +#!/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 math import floor + +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_sum_invest_funds(balances): + sum = 0 + for account, balance in balances.items(): + if account.startswith("Assets:Invest:R4:"): + parts = account.split(":") + if len(parts) == 5: + sum = balance if sum == 0 else sum + balance + result = sum.get_only_position().units + return Amount(Decimal(round(result.number, 2)), result.currency).to_string() + +def get_sum_stocks(balances): + sum = 0 + for account, balance in balances.items(): + if account.startswith("Assets:Invest:R4:"): + parts = account.split(":") + if len(parts) == 4: + sum = balance if sum == 0 else sum + balance + result = sum.get_only_position().units + return Amount(Decimal(round(result.number, 2)), result.currency).to_string() + +def get_total_inversions(balances): + sum = 0 + for account, balance in balances.items(): + if account.startswith("Assets:Invest:R4:"): + sum = balance if sum == 0 else sum + balance + result = sum.get_only_position().units + return Amount(Decimal(round(result.number, 2)), result.currency) + +def get_total_propietats(balances): + sum = 0 + for account, balance in balances.items(): + if account.startswith("Assets:PersonalProperty:"): + sum = balance if sum == 0 else sum + balance + result = sum.get_only_position().units + return Amount(Decimal(round(result.number, 2)), result.currency).to_string() + +def get_total_debt_assets(balances): + sum = 0 + for account, balance in balances.items(): + if account.startswith("Assets:Debt:"): + 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).to_string() + else: + return Amount(Decimal(0), "EUR").to_string() + +def get_total_benefits(balances): + sum = 0 + for account, balance in balances.items(): + if account.startswith("Assets:Benefits:"): + 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).to_string() + else: + return Amount(Decimal(0), "EUR").to_string() + +def get_total_assets(balances): + sum = 0 + for account, balance in balances.items(): + if account.startswith("Assets:"): + 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 get_total_liabilites(balances): + sum = 0 + for account, balance in balances.items(): + if account.startswith("Liabilities:"): + 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 get_net_worth(balances): + return sub(get_total_assets(balances), get_total_liabilites(balances)) + +def get_debt_to_assets_ratio(balances, max): + assets = 0 + liabilities = 0 + for account, balance in balances.items(): + if account.startswith("Assets:"): + assets = balance if assets == 0 else assets + balance + elif account.startswith("Liabilities:"): + liabilities = balance if liabilities == 0 else liabilities + balance + total_liabilities = Amount(Decimal(0), "EUR") if liabilities.get_only_position() == None else liabilities.get_only_position().units + total_assets = Amount(Decimal(0), "EUR") if assets.get_only_position() == None else assets.get_only_position().units + result = round((total_liabilities.number / total_assets.number) * 100, 2) + return f"{bcolors.FAIL if result >= max else bcolors.OKGREEN}{result} %{bcolors.ENDC}" + +def get_basic_liquidity_ratio(balances, min): + liquid = 0 + living_expenses = 2000 # TODO: Hardcoded + for account, balance in balances.items(): + if account.startswith("Assets:Liquid"): + liquid = balance if liquid == 0 else liquid + balance + total_liquid = Amount(Decimal(0), "EUR") if liquid.get_only_position() == None else liquid.get_only_position().units + result = round(total_liquid.number / living_expenses, 2) + return f"{bcolors.FAIL if result < min else bcolors.OKGREEN}{result}{bcolors.ENDC}" + +def get_investment_assets_to_net_worth_ratio(balances, min): + result = round((get_total_inversions(balances).number / get_net_worth(balances).number) * 100, 2) + return f"{bcolors.FAIL if result < min else bcolors.OKGREEN}{result} %{bcolors.ENDC}" + +def get_liquid_assets_to_net_worth_ratio(balances, min): + liquid = 0 + for account, balance in balances.items(): + if account.startswith("Assets:Liquid"): + liquid = balance if liquid == 0 else liquid + balance + total_liquid = Amount(Decimal(0), "EUR") if liquid.get_only_position() == None else liquid.get_only_position().units + result = round((total_liquid.number / get_net_worth(balances).number) * 100, 2) + return f"{bcolors.FAIL if result < min else bcolors.OKGREEN}{result} %{bcolors.ENDC}" + +def get_savings_ratio(balances, min): + monthly_savings_for_investment = 1200 # TODO: Hardcoded + gross_monthly_income = 3300 # TODO: Hardcoded + result = round((monthly_savings_for_investment / gross_monthly_income) * 100, 2) + return f"{bcolors.FAIL if result < min else bcolors.OKGREEN}{result} %{bcolors.ENDC}" + +def get_debt_service_ratio(balances, max): + monthly_debt_repayment = 0 # TODO: Hardcoded + gross_monthly_income = 3300 # TODO: Hardcoded + result = round((monthly_debt_repayment / gross_monthly_income) * 100, 2) + return f"{bcolors.FAIL if result >= max else bcolors.OKGREEN}{result} %{bcolors.ENDC}" + +def get_non_mortgage_debt_service_ratio(balances, max): + monthly_mortgage_debt_repayment = 0 # TODO: Hardcoded + gross_monthly_income = 3300 # TODO: Hardcoded + result = round((monthly_mortgage_debt_repayment / gross_monthly_income) * 100, 2) + return f"{bcolors.FAIL if result >= max else bcolors.OKGREEN}{result} %{bcolors.ENDC}" + +def get_solvency_ratio(balances, min): + result = round((get_net_worth(balances).number / get_total_assets(balances).number) * 100, 2) + return f"{bcolors.FAIL if result < min else bcolors.OKGREEN}{result} %{bcolors.ENDC}" + +def get_max_leveraged_investment(balances): + assets = get_total_assets(balances) + return Amount(round(assets.number * Decimal(0.9), 2), assets.currency).to_string() + +def get_position_as_str(inventory): + position = inventory.get_only_position() + if position is None: + return position + else: + return position.to_string() + +def print_report(date, balances): + print(f"{bcolors.BOLD}Balance Sheet (date={date}){bcolors.ENDC}") + draw_line() + print(f"{bcolors.BOLD}Assets{bcolors.ENDC}") + print(f"\t{bcolors.BOLD}Liquids{bcolors.ENDC}") + print(tabulate([ + ["Corrent", get_position_as_str(balances["Assets:Liquid:Caixabank:Corrent"])], + ["Estalvi", get_position_as_str(balances["Assets:Liquid:Caixabank:Estalvi"])], + ["Compte d'inversió", get_position_as_str(balances["Assets:Liquid:R4:EUR"])], + ["Total líquids", get_position_as_str(balances["Assets:Liquid:R4:EUR"] + balances["Assets:Liquid:Caixabank:Estalvi"] + balances["Assets:Liquid:Caixabank:Corrent"])], + ])) + print(f"\t{bcolors.BOLD}Inversions{bcolors.ENDC}") + print(tabulate([ + ["Fons d'inversió", get_sum_invest_funds(balances)], + ["Accions", get_sum_stocks(balances)], + ["Renta fixa", Amount(Decimal(0), "EUR").to_string()], + ["Total inversions", get_total_inversions(balances).to_string()], + ])) + print(f"\t{bcolors.BOLD}Propietat personal{bcolors.ENDC}") + print(tabulate([ + ["Vivenda principal", get_position_as_str(balances["Assets:PersonalProperty:VivendaPrincipal"])], + ["Cotxes", get_position_as_str(balances["Assets:PersonalProperty:Cotxe"])], + ["Joies, Art, Col·leccionables", get_position_as_str(balances["Assets:PersonalProperty:JoiesArtCollecionables"])], + ["Metalls preciosos", get_position_as_str(balances["Assets:PersonalProperty:MetallsPreciosos"])], + ["Altres propietats", get_position_as_str(balances["Assets:PersonalProperty:AltresPropietats"])], + ["Total propietats", get_total_propietats(balances)], + ])) + print(f"\t{bcolors.BOLD}Deutes{bcolors.ENDC}") + print(tabulate([ + ["Deutes per cobrar", get_position_as_str(balances["Assets:Debt:DeutesPerCobrar"])], + ["Total deutes", get_total_debt_assets(balances)], + ])) + print(f"\t{bcolors.BOLD}Beneficis laborals{bcolors.ENDC}") + print(tabulate([ + ["Tickets Restaurant", get_position_as_str(balances["Assets:Benefits:Edenred:TicketsRestaurant"])], + ["Targeta Transport", get_position_as_str(balances["Assets:Benefits:Edenred:TargetaTransport"])], + ["Total beneficis", get_total_benefits(balances)], + ])) + print(tabulate([ + [f"\t{bcolors.BOLD}Total Assets", get_total_assets(balances).to_string()] + ])) + + draw_line() + print(f"{bcolors.BOLD}Liabilites{bcolors.ENDC}") + print(tabulate([ + ["Hipoteques en vivenda principal", get_position_as_str(balances["Liabilities:Hipoteca:VivendaPrincipal"])], + ["Hipoteques en vivenda d'inversió", Amount(Decimal(0), "EUR").to_string()], + ["Crèdit", get_position_as_str(balances["Liabilities:Credit:Caixabank:TargetaCredit"])], + ["Factures impagades", get_position_as_str(balances["Liabilities:Factures:FacturesPendents"])], + ["Préstecs personals", Amount(Decimal(0), "EUR").to_string()], + ["Impostos no pagats", get_position_as_str(balances["Liabilities:Taxes:IRPF"])], + ["Altres passius", Amount(Decimal(0), "EUR").to_string()], + ["Total passius", get_total_liabilites(balances).to_string()], + ])) + + draw_line() + print(f"{bcolors.BOLD}Net Worth\t{get_net_worth(balances)}{bcolors.ENDC}") + + draw_line() + print(f"{bcolors.BOLD}Financial Ratios{bcolors.ENDC}") + print(tabulate([ + ["Debt-to-Assets Ratio", get_debt_to_assets_ratio(balances, 50), "50 %"], + ["Basic Liquidity Ratio", get_basic_liquidity_ratio(balances, 12), "12"], + ["Investment Assets to Net Worth Ratio", get_investment_assets_to_net_worth_ratio(balances, 50), "50 %"], + ["Liquid Assets to Net Worth Ratio", get_liquid_assets_to_net_worth_ratio(balances, 15), "15 %"], + ["Savings Ratio", get_savings_ratio(balances, 20), "20 %"], + ["Debt-Service Ratio", get_debt_service_ratio(balances, 35), "35 %"], + ["Non-Mortgage Debt-Service Ratio", get_non_mortgage_debt_service_ratio(balances, 15), "15 %"], + ["Solvency Ratio", get_solvency_ratio(balances, 50), "50 %"], + ["Max Leveraged Investment", get_max_leveraged_investment(balances), ""] + ])) + +def get_balances(entries, options, date): + balance_query = f"SELECT account, convert(sum(position), \"EUR\") as position FROM date <= {date} WHERE account ~ '^(Liabilities|Assets)'" + rtypes, rrows = query.run_query( + entries, options, balance_query) + balances = {} + for row in rrows: + balances[row.account] = row.position + return balances + +def main(): + parser = argparse.ArgumentParser(description='Generate balance sheet report') + parser.add_argument('date', metavar='date', type=str, nargs=1, + help='Report date in ISO format (e.g. 1970-01-01)') + + args = parser.parse_args() + date = args.date[0] + + filename = "ledger/main.beancount" + entries, errors, options = loader.load_file(filename) + + if errors: + printer.print_errors(errors) + + balances = get_balances(entries, options, date) + print_report(date, balances) + +main() \ No newline at end of file diff --git a/ledger/accounts.beancount b/ledger/accounts.beancount index e9c6d90..b6f7d81 100644 --- a/ledger/accounts.beancount +++ b/ledger/accounts.beancount @@ -9,7 +9,7 @@ 1970-01-01 open Assets:Invest:R4:PLTR PLTR 1970-01-01 open Assets:Invest:R4:MSFT MSFT 1970-01-01 open Assets:Benefits:Edenred:TicketsRestaurant EUR -1970-01-01 open Assets:Benefits:Edenred:TarjetaTransport EUR +1970-01-01 open Assets:Benefits:Edenred:TargetaTransport EUR 1970-01-01 open Assets:PersonalProperty:VivendaPrincipal EUR 1970-01-01 open Assets:PersonalProperty:Cotxe EUR 1970-01-01 open Assets:PersonalProperty:JoiesArtCollecionables EUR @@ -20,11 +20,11 @@ 1970-01-01 open Liabilities:Credit:Caixabank:TargetaCredit EUR 1970-01-01 open Liabilities:Factures:FacturesPendents EUR 1970-01-01 open Liabilities:Taxes:IRPF EUR -;1970-01-01 open Liabilities:Hipoteca:VivendaPrincipal EUR +1970-01-01 open Liabilities:Hipoteca:VivendaPrincipal EUR 1970-01-01 open Income:Work:Zurich:Salari EUR 1970-01-01 open Income:Work:Zurich:TicketsRestaurant EUR -1970-01-01 open Income:Work:Zurich:TarjetaTransport EUR +1970-01-01 open Income:Work:Zurich:TargetaTransport EUR 1970-01-01 open Income:Work:Zurich:SeguroMedic EUR 1970-01-01 open Income:Work:Zurich:Gimnas EUR 1970-01-01 open Income:Other:Caixabank:Transferencia EUR diff --git a/ledger/transactions/2024/01.beancount b/ledger/transactions/2024/01.beancount index 66dcdc5..228bc3c 100644 --- a/ledger/transactions/2024/01.beancount +++ b/ledger/transactions/2024/01.beancount @@ -1,19 +1,24 @@ 2023-12-31 * "Balanç inicial EUR" - Assets:Liquid:Caixabank:Corrent 18903.80 EUR - Assets:Liquid:Caixabank:Estalvi 12666.49 EUR - Assets:Liquid:R4:EUR 44.04 EUR - Assets:Invest:R4:Amundi:MSCIWRLD 86.005 AMNDMSCIWRLD {237.62 EUR} - Assets:Invest:R4:Vanguard:EMMK 14.99 VANEMMK {177.773 EUR} - Assets:Invest:R4:Fidelity:GLTECH 344.47 FIGLTECH {41.06 EUR} - Assets:Invest:R4:Amundi:SUSTINC 11.295 AMNDSUSINC {62.84 EUR} - Assets:Benefits:Edenred:TicketsRestaurant 0 EUR - Assets:Benefits:Edenred:TarjetaTransport 0 EUR - Assets:PersonalProperty:VivendaPrincipal 0 EUR - Assets:PersonalProperty:Cotxe 10000 EUR - Assets:PersonalProperty:JoiesArtCollecionables 1250 EUR - Assets:PersonalProperty:MetallsPreciosos 0 EUR - Assets:PersonalProperty:AltresPropietats 0 EUR - Assets:Debt:DeutesPerCobrar 0 EUR + Assets:Liquid:Caixabank:Corrent 18903.80 EUR + Assets:Liquid:Caixabank:Estalvi 12666.49 EUR + Assets:Liquid:R4:EUR 44.04 EUR + Assets:Invest:R4:Amundi:MSCIWRLD 86.005 AMNDMSCIWRLD {237.62 EUR} + Assets:Invest:R4:Vanguard:EMMK 14.99 VANEMMK {177.773 EUR} + Assets:Invest:R4:Fidelity:GLTECH 344.47 FIGLTECH {41.06 EUR} + Assets:Invest:R4:Amundi:SUSTINC 11.295 AMNDSUSINC {62.84 EUR} + Assets:Benefits:Edenred:TicketsRestaurant 0 EUR + Assets:Benefits:Edenred:TargetaTransport 0 EUR + Assets:PersonalProperty:VivendaPrincipal 0 EUR + Assets:PersonalProperty:Cotxe 10000 EUR + Assets:PersonalProperty:JoiesArtCollecionables 1250 EUR + Assets:PersonalProperty:MetallsPreciosos 0 EUR + Assets:PersonalProperty:AltresPropietats 0 EUR + Assets:Debt:DeutesPerCobrar 0 EUR + Liabilities:Hipoteca:VivendaPrincipal 0 EUR + Liabilities:Credit:Caixabank:TargetaCredit 0 EUR + Liabilities:Credit:Caixabank:TargetaCredit 0 EUR + Liabilities:Factures:FacturesPendents 0 EUR + Liabilities:Taxes:IRPF 0 EUR Equity:Opening-Balances 2023-12-31 * "Balanç inicial USD" Assets:Invest:R4:BNP:DISTECH 0.359 BNPDISTECH {2195.55 USD} @@ -21,6 +26,7 @@ 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 @@ -39,6 +45,26 @@ 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 @@ -49,12 +75,18 @@ 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 + 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 @@ -73,3 +105,23 @@ 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)