#!/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) * -1), 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 * -1) / 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_estalvi(balances): accounts = [ "Assets:Liquid:Caixabank:Estalvi", "Assets:Liquid:TradeRepublic:EUR" ] total = 0 for account in accounts: if account in balances: if total == 0: total = balances[account] else: total = total + balances[account] return total 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(get_estalvi(balances))], ["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:Corrent"] + get_estalvi(balances))], ])) 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"])], ["Pla Pensions Empleados Zurich", get_position_as_str(balances["Assets:Benefits:DZP:PPEZurich"]) if "Assets:Benefits:DZP:PPEZurich" in balances else "-"], ["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"] * Decimal(-1))], ["Hipoteques en vivenda d'inversió", Amount(Decimal(0), "EUR").to_string()], ["Crèdit", get_position_as_str(balances["Liabilities:Credit:Caixabank:TargetaCredit"] * Decimal(-1))], ["Factures impagades", get_position_as_str(balances["Liabilities:Factures:FacturesPendents"] * Decimal(-1))], ["Préstecs personals", Amount(Decimal(0), "EUR").to_string()], ["Impostos no pagats", get_position_as_str(balances["Liabilities:Taxes:IRPF"] * Decimal(-1))], ["Altres passius", Amount(Decimal(0), "EUR").to_string()] ])) print(tabulate([ [f"{bcolors.BOLD}Total passius{bcolors.ENDC}", f"{bcolors.BOLD}{get_total_liabilites(balances).to_string()}{bcolors.ENDC}"], ])) 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()