278 lines
12 KiB
Python
Executable File
278 lines
12 KiB
Python
Executable 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) * -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_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"] + balances["Assets:Liquid:TradeRepublic:EUR"])],
|
|
["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"] + balances["Assets:Liquid:TradeRepublic:EUR"])],
|
|
]))
|
|
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"])],
|
|
["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() |