improve balance sheet script
This commit is contained in:
@@ -5,8 +5,9 @@ from beancount.parser import printer
|
||||
import argparse
|
||||
from tabulate import tabulate
|
||||
from decimal import Decimal
|
||||
from beancount.core.amount import Amount, sub, mul
|
||||
from beancount.core.amount import Amount, add, sub, mul
|
||||
from math import floor
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class bcolors:
|
||||
HEADER = '\033[95m'
|
||||
@@ -22,213 +23,130 @@ class bcolors:
|
||||
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_last_month_timestamps(date):
|
||||
month = int(date.split("-")[1])
|
||||
year = int(date.split("-")[0])
|
||||
d = datetime(year, month, 1)
|
||||
end_date = d - timedelta(days=1)
|
||||
start_date = f"{end_date.year}-{end_date.month}-01"
|
||||
return start_date, end_date.strftime("%Y-%m-%d")
|
||||
|
||||
def get_sum_stocks(balances):
|
||||
def get_sum_balances(balances, account_prefix):
|
||||
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:"):
|
||||
if account.startswith(account_prefix):
|
||||
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:
|
||||
if sum == 0 or sum.get_only_position() == None:
|
||||
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")
|
||||
result = sum.get_only_position().units
|
||||
return Amount(Decimal(round(result.number, 2)), result.currency).to_string()
|
||||
|
||||
def get_net_worth(balances):
|
||||
return sub(get_total_assets(balances), get_total_liabilites(balances))
|
||||
total_assets = Amount.from_string(get_sum_balances(balances, "Assets:"))
|
||||
total_liabilities = Amount.from_string(get_sum_balances(balances, "Liabilities:"))
|
||||
return add(total_assets, total_liabilities)
|
||||
|
||||
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
|
||||
total_assets = Amount.from_string(get_sum_balances(balances, "Assets:"))
|
||||
total_liabilities = Amount.from_string(get_sum_balances(balances, "Liabilities:"))
|
||||
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):
|
||||
def get_emergency_fund_ratio(balances, expenses, low, mid):
|
||||
liquid = 0
|
||||
living_expenses = 2000 # TODO: Hardcoded
|
||||
living_expenses = expenses[0].position.get_only_position().units.number
|
||||
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}"
|
||||
color = bcolors.FAIL if result < low else bcolors.OKGREEN if result > mid else bcolors.WARNING
|
||||
return f"{color}{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)
|
||||
total_investment = Amount.from_string(get_sum_balances(balances, "Assets:Invest:"))
|
||||
result = round((total_investment.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"):
|
||||
if account.startswith("Assets:Liquid") or account.startswith("Assets:Invest"):
|
||||
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)
|
||||
def get_savings_ratio(balances, gross_monthly_income, monthly_savings, min):
|
||||
result = round((monthly_savings.number / 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)
|
||||
def get_debt_service_ratio(balances, gross_monthly_income, debt_payments, max):
|
||||
result = round((debt_payments.number / 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)
|
||||
def get_non_mortgage_debt_service_ratio(balances, gross_monthly_income, mortgage_payments, max):
|
||||
result = round((mortgage_payments.number / 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)
|
||||
total_assets = Amount.from_string(get_sum_balances(balances, "Assets:"))
|
||||
result = round((get_net_worth(balances).number / total_assets.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
|
||||
total_assets = Amount.from_string(get_sum_balances(balances, "Assets:"))
|
||||
return Amount(round(total_assets.number * Decimal(0.9), 2), total_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()
|
||||
return Amount(Decimal(round(position.units.number, 2)), position.units.currency).to_string()
|
||||
|
||||
def print_report(date, balances):
|
||||
def print_report(date, balances, expenses, income, debt_payments, mortgage_payments, savings):
|
||||
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))],
|
||||
["Corrent", get_sum_balances(balances, "Assets:Liquid:Caixabank:Corrent")],
|
||||
["Estalvi", get_sum_balances(balances, "Assets:Liquid:Estalvi")],
|
||||
["Compte d'inversió", get_sum_balances(balances, "Assets:Liquid:R4:EUR")],
|
||||
["Total líquids", get_sum_balances(balances, "Assets:Liquid:")],
|
||||
]))
|
||||
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()],
|
||||
["Fons d'inversió", get_sum_balances(balances, "Assets:Invest:Fund:")],
|
||||
["ETFs", get_sum_balances(balances, "Assets:Invest:ETF:")],
|
||||
["Accions", get_sum_balances(balances, "Assets:Invest:Stock:")],
|
||||
["Renta fixa", get_sum_balances(balances, "Assets:Invest:Fixed:")],
|
||||
["Total inversions", get_sum_balances(balances, "Assets:Invest:")],
|
||||
]))
|
||||
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)],
|
||||
["Vivenda principal", get_sum_balances(balances, "Assets:PersonalProperty:VivendaPrincipal")],
|
||||
["Cotxes", get_sum_balances(balances, "Assets:PersonalProperty:Cotxe")],
|
||||
["Joies, Art, Col·leccionables", get_sum_balances(balances, "Assets:PersonalProperty:JoiesArtCollecionables")],
|
||||
["Metalls preciosos", get_sum_balances(balances, "Assets:PersonalProperty:MetallsPreciosos")],
|
||||
["Altres propietats", get_sum_balances(balances, "Assets:PersonalProperty:AltresPropietats")],
|
||||
["Total propietats", get_sum_balances(balances, "Assets:PersonalProperty:")],
|
||||
]))
|
||||
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)],
|
||||
["Total deutes", get_sum_balances(balances, "Assets:Debt:")],
|
||||
]))
|
||||
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)],
|
||||
["Total beneficis", get_sum_balances(balances, "Assets:Benefits:")],
|
||||
]))
|
||||
print(tabulate([
|
||||
[f"\t{bcolors.BOLD}Total Assets", get_total_assets(balances).to_string()]
|
||||
[f"\t{bcolors.BOLD}Total Assets", get_sum_balances(balances, "Assets:")]
|
||||
]))
|
||||
|
||||
draw_line()
|
||||
@@ -243,7 +161,7 @@ def print_report(date, balances):
|
||||
["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}"],
|
||||
[f"{bcolors.BOLD}Total passius{bcolors.ENDC}", f"{bcolors.BOLD}{get_sum_balances(balances, "Liabilities:")}{bcolors.ENDC}"],
|
||||
]))
|
||||
|
||||
draw_line()
|
||||
@@ -253,12 +171,12 @@ def print_report(date, balances):
|
||||
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, 6), "6"],
|
||||
["Emergency Fund", get_emergency_fund_ratio(balances, expenses, 3, 6), "3-6"],
|
||||
["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 %"],
|
||||
["Savings Ratio", get_savings_ratio(balances, income, savings, 20), "20 %"],
|
||||
["Debt-Service Ratio", get_debt_service_ratio(balances, income, debt_payments, 35), "35 %"],
|
||||
["Non-Mortgage Debt-Service Ratio", get_non_mortgage_debt_service_ratio(balances, income, mortgage_payments, 15), "15 %"],
|
||||
["Solvency Ratio", get_solvency_ratio(balances, 50), "50 %"]
|
||||
]))
|
||||
|
||||
@@ -271,6 +189,40 @@ def get_balances(entries, options, date):
|
||||
balances[row.account] = row.position
|
||||
return balances
|
||||
|
||||
def get_expenses(entries, options, date):
|
||||
start_date, end_date = get_last_month_timestamps(date)
|
||||
expenses_query = f"SELECT convert(sum(position), \"EUR\") as position FROM date <= {end_date} WHERE account ~ 'Expenses:' AND date >= {start_date}"
|
||||
rtypes, rrows = query.run_query(
|
||||
entries, options, expenses_query)
|
||||
return rrows
|
||||
|
||||
def get_income(entries, options, date):
|
||||
start_date, end_date = get_last_month_timestamps(date)
|
||||
income_query = f"SELECT convert(sum(position), \"EUR\") as position FROM date <= {end_date} WHERE account ~ '^(Income:Work|Income:Savings|Income:Invest)' AND date >= {start_date}"
|
||||
rtypes, rrows = query.run_query(
|
||||
entries, options, income_query)
|
||||
return rrows
|
||||
|
||||
def get_debt_payments(entries, options, date):
|
||||
start_date, end_date = get_last_month_timestamps(date)
|
||||
debt_payments_query = f"SELECT convert(sum(position), \"EUR\") as position FROM date <= {end_date} WHERE account ~ 'Liabilities:' AND date >= {start_date}"
|
||||
mortgage_payments_query = f"SELECT convert(sum(position), \"EUR\") as position FROM date <= {end_date} WHERE account ~ 'Liabilities:Hipoteca:' AND date >= {start_date}"
|
||||
rtypes, rrows_debt = query.run_query(
|
||||
entries, options, debt_payments_query)
|
||||
rtypes, rrows_mortgage = query.run_query(
|
||||
entries, options, mortgage_payments_query)
|
||||
debt_payments = rrows_debt[0].position.get_only_position().units if len(rrows_debt) > 0 else Amount(Decimal(0), "EUR")
|
||||
mortgage_payments = rrows_mortgage[0].position.get_only_position().units if len(rrows_mortgage) > 0 else Amount(Decimal(0), "EUR")
|
||||
return debt_payments, mortgage_payments
|
||||
|
||||
def get_savings(entries, options, date):
|
||||
start_date, end_date = get_last_month_timestamps(date)
|
||||
savings_query = f"SELECT convert(sum(position), \"EUR\") as position FROM date <= {end_date} WHERE account ~ '^Assets:Invest:' AND date >= {start_date}"
|
||||
rtypes, rrows = query.run_query(
|
||||
entries, options, savings_query)
|
||||
result = rrows[0].position.get_only_position().units if len(rrows) > 0 else Amount(Decimal(0), "EUR")
|
||||
return result
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate balance sheet report')
|
||||
parser.add_argument('date', metavar='date', type=str, nargs=1,
|
||||
@@ -286,6 +238,11 @@ def main():
|
||||
printer.print_errors(errors)
|
||||
|
||||
balances = get_balances(entries, options, date)
|
||||
print_report(date, balances)
|
||||
expenses = get_expenses(entries, options, date)
|
||||
income = get_income(entries, options, date)
|
||||
gross_monthly_income = income[0].position.get_only_position().units.number * -1
|
||||
debt_payments, mortgage_payments = get_debt_payments(entries, options, date)
|
||||
savings = get_savings(entries, options, date)
|
||||
print_report(date, balances, expenses, gross_monthly_income, debt_payments, mortgage_payments, savings)
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user