upgrade to beancount v3

This commit is contained in:
Roger Oriol
2025-12-29 17:49:19 +01:00
parent b83e80466c
commit 4c149cebb5
5 changed files with 967 additions and 589 deletions

View File

@@ -1,12 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from beancount import loader from beancount import loader
from beancount.query import query from beanquery import query
from beancount.parser import printer from beancount.parser import printer
import argparse import argparse
from tabulate import tabulate from tabulate import tabulate
from decimal import Decimal from decimal import Decimal
from beancount.core.amount import Amount, add, sub, mul from beancount.core.amount import Amount, add, sub
from math import floor
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -72,7 +71,7 @@ def get_debt_to_assets_ratio(balances, max):
def get_emergency_fund_ratio(balances, expenses, low, mid): def get_emergency_fund_ratio(balances, expenses, low, mid):
liquid = 0 liquid = 0
living_expenses = expenses[0].position.get_only_position( living_expenses = expenses[0][0].get_only_position(
).units.number / 12 ).units.number / 12
for account, balance in balances.items(): for account, balance in balances.items():
if account.startswith("Assets:Liquid"): if account.startswith("Assets:Liquid"):
@@ -129,7 +128,7 @@ def get_solvency_ratio(balances, min):
def get_interest_coverage_ratio(net_monthly_income, expenses, debt_payments, mortgage_payments, min): def get_interest_coverage_ratio(net_monthly_income, expenses, debt_payments, mortgage_payments, min):
living_expenses = expenses[0].position.get_only_position().units.number living_expenses = expenses[0][0].get_only_position().units.number
interest = debt_payments.number + mortgage_payments.number interest = debt_payments.number + mortgage_payments.number
interest = interest if interest > 0 else 1 interest = interest if interest > 0 else 1
result = round((net_monthly_income - living_expenses) / interest, 2) result = round((net_monthly_income - living_expenses) / interest, 2)
@@ -265,7 +264,7 @@ def get_balances(entries, options, date):
entries, options, balance_query) entries, options, balance_query)
balances = {} balances = {}
for row in rrows: for row in rrows:
balances[row.account] = row.position balances[row[0]] = row[1]
return balances return balances
@@ -284,7 +283,7 @@ def get_income(entries, options, date):
end_date} WHERE account ~ '^(Income:Work|Income:Savings|Income:Invest)' AND date >= {start_date}" end_date} WHERE account ~ '^(Income:Work|Income:Savings|Income:Invest)' AND date >= {start_date}"
rtypes, rrows = query.run_query( rtypes, rrows = query.run_query(
entries, options, income_query) entries, options, income_query)
net_monthly_income = rrows[0].position.get_only_position( net_monthly_income = rrows[0][0].get_only_position(
).units.number * -1 ).units.number * -1
start_date, end_date = get_last_year_timestamps(date) start_date, end_date = get_last_year_timestamps(date)
@@ -292,7 +291,7 @@ def get_income(entries, options, date):
end_date} WHERE account ~ '^(Income:Work|Income:Savings|Income:Invest)' AND date >= {start_date}" end_date} WHERE account ~ '^(Income:Work|Income:Savings|Income:Invest)' AND date >= {start_date}"
rtypes, rrows = query.run_query( rtypes, rrows = query.run_query(
entries, options, income_query) entries, options, income_query)
net_yearly_income = rrows[0].position.get_only_position( net_yearly_income = rrows[0][0].get_only_position(
).units.number * -1 ).units.number * -1
return net_monthly_income, net_yearly_income return net_monthly_income, net_yearly_income
@@ -308,7 +307,7 @@ def get_debt_payments(entries, options, date):
entries, options, debt_payments_query) entries, options, debt_payments_query)
rtypes, rrows_mortgage = query.run_query( rtypes, rrows_mortgage = query.run_query(
entries, options, mortgage_payments_query) entries, options, mortgage_payments_query)
debt_payments = rrows_debt[0].position.get_only_position().units if len( debt_payments = rrows_debt[0][0].get_only_position().units if len(
rrows_debt) > 0 else Amount(Decimal(0), "EUR") rrows_debt) > 0 else Amount(Decimal(0), "EUR")
mortgage_payments = rrows_mortgage[0].position.get_only_position( mortgage_payments = rrows_mortgage[0].position.get_only_position(
).units if len(rrows_mortgage) > 0 else Amount(Decimal(0), "EUR") ).units if len(rrows_mortgage) > 0 else Amount(Decimal(0), "EUR")
@@ -321,13 +320,13 @@ def get_savings(entries, options, date):
end_date} WHERE account ~ '^Assets:Invest:' AND date >= {start_date}" end_date} WHERE account ~ '^Assets:Invest:' AND date >= {start_date}"
rtypes, rrows = query.run_query( rtypes, rrows = query.run_query(
entries, options, investments_query) entries, options, investments_query)
result = rrows[0].position.get_only_position().units if len( result = rrows[0][0].get_only_position().units if len(
rrows) > 0 else Amount(Decimal(0), "EUR") rrows) > 0 else Amount(Decimal(0), "EUR")
liabilities_query = f"SELECT convert(sum(position), \"EUR\") as position FROM date <= { liabilities_query = f"SELECT convert(sum(position), \"EUR\") as position FROM date <= {
end_date} WHERE account ~ '^Liabilities:Credit:Renta4:' AND date >= {start_date}" end_date} WHERE account ~ '^Liabilities:Credit:Renta4:' AND date >= {start_date}"
rtypes, rrows = query.run_query( rtypes, rrows = query.run_query(
entries, options, liabilities_query) entries, options, liabilities_query)
liabilities = rrows[0].position.get_only_position().units if len( liabilities = rrows[0][0].get_only_position().units if len(
rrows) > 0 else Amount(Decimal(0), "EUR") rrows) > 0 else Amount(Decimal(0), "EUR")
result = add(result, liabilities) result = add(result, liabilities)
return result return result
@@ -345,7 +344,7 @@ def get_assets_pignorats(entries, options, date):
date}) as price" date}) as price"
rtypes, rrows = query.run_query( rtypes, rrows = query.run_query(
entries, options, pignorat_query) entries, options, pignorat_query)
result = result + rrows[0].price * assets_pignorats[curr] result = result + rrows[0][0] * assets_pignorats[curr]
return result return result

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from beancount import loader from beancount import loader
from beancount.query import query from beanquery import query
from beancount.core.data import Custom from beancount.core.data import Custom
from beancount.core.amount import Amount, add, sub from beancount.core.amount import Amount, add, sub
from beancount.parser import printer from beancount.parser import printer
@@ -61,7 +61,7 @@ WHERE account ~ \"Equity:(LloguerMiquel|FacturesUtilitatsMiquel)\""""
entries, options, equity_query) entries, options, equity_query)
equity = {} equity = {}
for row in rrows: for row in rrows:
equity[row.account] = row.sum_position equity[row[0]] = row[1]
return equity return equity
@@ -77,7 +77,7 @@ WHERE account ~ \"Expenses\" OR account ~ \"Liabilities\""""
entries, options, expenses_query) entries, options, expenses_query)
expenses = {} expenses = {}
for row in rrows: for row in rrows:
expenses[row.account] = row.sum_position expenses[row[0]] = row[1]
return expenses return expenses

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from beancount import loader from beancount import loader
from beancount.query import query from beanquery import query
from beancount.parser import printer from beancount.parser import printer
import argparse import argparse
from tabulate import tabulate from tabulate import tabulate
@@ -9,6 +9,7 @@ from beancount.core.amount import Amount, add, sub, mul
from datetime import date from datetime import date
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
class bcolors: class bcolors:
HEADER = '\033[95m' HEADER = '\033[95m'
OKBLUE = '\033[94m' OKBLUE = '\033[94m'
@@ -20,156 +21,189 @@ class bcolors:
BOLD = '\033[1m' BOLD = '\033[1m'
UNDERLINE = '\033[4m' UNDERLINE = '\033[4m'
def draw_line(): def draw_line():
print('─' * 30) print('─' * 30)
def get_income_val(obj, key): def get_income_val(obj, key):
if key in obj: if key in obj:
amount = obj[key].get_only_position().units amount = obj[key].get_only_position().units
amount = mul(amount, Decimal(-1.0)) amount = mul(amount, Decimal(-1.0))
return amount.to_string() return amount.to_string()
else: else:
return None return None
def get_expense_val(obj, key): def get_expense_val(obj, key):
if key in obj: if key in obj:
amount = obj[key].get_only_position().units amount = obj[key].get_only_position().units
return amount.to_string() return amount.to_string()
else: else:
return None return None
def print_expenses_table(expenses): def print_expenses_table(expenses):
table = [] table = []
for key, expense in expenses.items(): for key, expense in expenses.items():
parts = key.split(":", 1) parts = key.split(":", 1)
table.append([parts[1], get_expense_val(expenses, key)]) table.append([parts[1], get_expense_val(expenses, key)])
print(tabulate(table)) print(tabulate(table))
def get_total_inflows(income): def get_total_inflows(income):
sum = 0 sum = 0
for account, balance in income.items(): for account, balance in income.items():
sum = balance if sum == 0 else sum + balance sum = balance if sum == 0 else sum + balance
if sum != 0 and sum.get_only_position() != None: if sum != 0 and sum.get_only_position() != None:
result = sum.get_only_position().units result = sum.get_only_position().units
return Amount(Decimal(round(result.number, 2) * Decimal(-1.0)), result.currency) return Amount(Decimal(round(result.number, 2) * Decimal(-1.0)), result.currency)
else: else:
return Amount(Decimal(0), "EUR") return Amount(Decimal(0), "EUR")
def get_total_outflows(expenses, total_investments): def get_total_outflows(expenses, total_investments):
sum = 0 sum = 0
for account, balance in expenses.items(): for account, balance in expenses.items():
sum = balance if sum == 0 else sum + balance sum = balance if sum == 0 else sum + balance
if sum != 0 and sum.get_only_position() != None: if sum != 0 and sum.get_only_position() != None:
result = sum.get_only_position().units result = sum.get_only_position().units
return add(Amount(Decimal(round(result.number, 2)), result.currency), total_investments) return add(Amount(Decimal(round(result.number, 2)), result.currency), total_investments)
else: else:
return total_investments return total_investments
def get_total_investments(investments): def get_total_investments(investments):
sum = Amount(Decimal(0), "EUR") sum = Amount(Decimal(0), "EUR")
for inv in investments: for inv in investments:
sum = inv.cost_position if sum == Amount(Decimal(0), "EUR") else add(sum, inv.cost_position) sum = inv.cost_position if sum == Amount(
if sum != 0 and sum != None: Decimal(0), "EUR") else add(sum, inv.cost_position)
return Amount(Decimal(round(sum.number, 2)), sum.currency) if sum != 0 and sum != None:
else: return Amount(Decimal(round(sum.number, 2)), sum.currency)
return Amount(Decimal(0), "EUR") else:
return Amount(Decimal(0), "EUR")
def print_report(start_date, period, income, expenses, investments): def print_report(start_date, period, income, expenses, investments):
print(f"{bcolors.BOLD}Cash Flow Statement (period={period}, start_date={start_date}){bcolors.ENDC}") print(f"{bcolors.BOLD}Cash Flow Statement (period={
draw_line() period}, start_date={start_date}){bcolors.ENDC}")
print(f"{bcolors.BOLD}Inflows{bcolors.ENDC}") draw_line()
print(f"\t{bcolors.BOLD}Income{bcolors.ENDC}") print(f"{bcolors.BOLD}Inflows{bcolors.ENDC}")
print(tabulate([ print(f"\t{bcolors.BOLD}Income{bcolors.ENDC}")
["Salari", get_income_val(income,"Income:Work:Zurich:Salari")], print(tabulate([
["Tickets Restaurant", get_income_val(income,"Income:Work:Zurich:TicketsRestaurant")], ["Salari", get_income_val(income, "Income:Work:Zurich:Salari")],
["Targeta Transport", get_income_val(income,"Income:Work:Zurich:TargetaTransport")], ["Tickets Restaurant", get_income_val(
["Pla pensions Empleats Zurich", get_income_val(income,"Income:Work:Zurich:DZP")], income, "Income:Work:Zurich:TicketsRestaurant")],
["Seguro Mèdic", get_income_val(income,"Income:Work:Zurich:SeguroMedic")], ["Targeta Transport", get_income_val(
["Gimnàs", get_income_val(income,"Income:Work:Zurich:Gimnas")] income, "Income:Work:Zurich:TargetaTransport")],
])) ["Pla pensions Empleats Zurich", get_income_val(
print(f"\t{bcolors.BOLD}Income from Investment{bcolors.ENDC}") income, "Income:Work:Zurich:DZP")],
print(tabulate([ ["Seguro Mèdic", get_income_val(
["Capital Gains", get_income_val(income,"Income:Invest:R4:CapitalGains")], income, "Income:Work:Zurich:SeguroMedic")],
["Untaxable Capital Gains", get_income_val(income,"Income:Invest:R4:CapitalGains:Untaxable")], ["Gimnàs", get_income_val(income, "Income:Work:Zurich:Gimnas")]
["Dividends", get_income_val(income,"Income:Invest:R4:Dividends")], ]))
["Rentabilitat Estalvis", get_income_val(income,"Income:Savings:Caixabank:RentabilitatEstalvis")] print(f"\t{bcolors.BOLD}Income from Investment{bcolors.ENDC}")
])) print(tabulate([
print(f"\t{bcolors.BOLD}Other Inflows{bcolors.ENDC}") ["Capital Gains", get_income_val(
print(tabulate([ income, "Income:Invest:R4:CapitalGains")],
["Transferències", get_income_val(income,"Income:Other:Caixabank:Transferencia")], ["Untaxable Capital Gains", get_income_val(
["Bizum", get_income_val(income,"Income:Other:Caixabank:Bizum")], income, "Income:Invest:R4:CapitalGains:Untaxable")],
["Devolucions", get_income_val(income,"Income:Other:Devolucions")] ["Dividends", get_income_val(income, "Income:Invest:R4:Dividends")],
])) ["Rentabilitat Estalvis", get_income_val(
print(tabulate([ income, "Income:Savings:Caixabank:RentabilitatEstalvis")]
["Total Inflows", f"{bcolors.BOLD}{get_total_inflows(income).to_string()}{bcolors.ENDC}"] ]))
])) print(f"\t{bcolors.BOLD}Other Inflows{bcolors.ENDC}")
draw_line() print(tabulate([
print(f"{bcolors.BOLD}Outflows{bcolors.ENDC}") ["Transferències", get_income_val(
print_expenses_table(expenses) income, "Income:Other:Caixabank:Transferencia")],
print(f"{bcolors.BOLD}Outflows from Investment{bcolors.ENDC}") ["Bizum", get_income_val(income, "Income:Other:Caixabank:Bizum")],
total_investments = get_total_investments(investments) ["Devolucions", get_income_val(income, "Income:Other:Devolucions")]
print(tabulate([ ]))
["Compra de fons i accions", total_investments.to_string()] print(tabulate([
])) ["Total Inflows", f"{bcolors.BOLD}{get_total_inflows(income).to_string()}{
print(tabulate([ bcolors.ENDC}"]
["Total Outflows", f"{bcolors.BOLD}{get_total_outflows(expenses, total_investments).to_string()}{bcolors.ENDC}"] ]))
])) draw_line()
draw_line() print(f"{bcolors.BOLD}Outflows{bcolors.ENDC}")
net_cash_flow = sub(get_total_inflows(income), get_total_outflows(expenses, total_investments)) print_expenses_table(expenses)
print(tabulate([ print(f"{bcolors.BOLD}Outflows from Investment{bcolors.ENDC}")
["NET CASH FLOW", f"{bcolors.BOLD}{bcolors.OKGREEN if net_cash_flow.number >= 0 else bcolors.FAIL}{net_cash_flow.to_string()}{bcolors.ENDC}"] total_investments = get_total_investments(investments)
])) print(tabulate([
["Compra de fons i accions", total_investments.to_string()]
]))
print(tabulate([
["Total Outflows", f"{bcolors.BOLD}{get_total_outflows(
expenses, total_investments).to_string()}{bcolors.ENDC}"]
]))
draw_line()
net_cash_flow = sub(get_total_inflows(income),
get_total_outflows(expenses, total_investments))
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): def get_income(entries, options, period, start_date):
period_delta = relativedelta(months=1) if period == "monthly" else relativedelta(years=1) period_delta = relativedelta(
end_date = date.fromisoformat(start_date) + period_delta months=1) if period == "monthly" else relativedelta(years=1)
income_query = f"SELECT account, convert(sum(position), \"EUR\") as sum_position FROM OPEN ON {start_date} CLOSE ON {end_date.isoformat()} WHERE account ~ \"Income\"" end_date = date.fromisoformat(start_date) + period_delta
rtypes, rrows = query.run_query( income_query = f"SELECT account, convert(sum(position), \"EUR\") as sum_position FROM OPEN ON {
entries, options, income_query) start_date} CLOSE ON {end_date.isoformat()} WHERE account ~ \"Income\""
income = {} rtypes, rrows = query.run_query(
for row in rrows: entries, options, income_query)
income[row.account] = row.sum_position income = {}
return income for row in rrows:
income[row[0]] = row[1]
return income
def get_expenses(entries, options, period, start_date): def get_expenses(entries, options, period, start_date):
period_delta = relativedelta(months=1) if period == "monthly" else relativedelta(years=1) period_delta = relativedelta(
end_date = date.fromisoformat(start_date) + period_delta months=1) if period == "monthly" else relativedelta(years=1)
expenses_query = f"SELECT account, convert(sum(position), \"EUR\") as sum_position FROM OPEN ON {start_date} CLOSE ON {end_date.isoformat()} WHERE account ~ \"Expenses\"" end_date = date.fromisoformat(start_date) + period_delta
rtypes, rrows = query.run_query( expenses_query = f"SELECT account, convert(sum(position), \"EUR\") as sum_position FROM OPEN ON {
entries, options, expenses_query) start_date} CLOSE ON {end_date.isoformat()} WHERE account ~ \"Expenses\""
expenses = {} rtypes, rrows = query.run_query(
for row in rrows: entries, options, expenses_query)
expenses[row.account] = row.sum_position expenses = {}
return expenses for row in rrows:
expenses[row[0]] = row[1]
return expenses
def get_investments(entries, options, period, start_date): def get_investments(entries, options, period, start_date):
period_delta = relativedelta(months=1) if period == "monthly" else relativedelta(years=1) period_delta = relativedelta(
end_date = date.fromisoformat(start_date) + period_delta months=1) if period == "monthly" else relativedelta(years=1)
expenses_query = f"SELECT account, convert(cost(position), \"EUR\") as cost_position, currency, date WHERE account ~ \"Assets:Invest:R4:\" AND NOT currency ~ '^(EUR|USD)' AND date >= {start_date} AND date < {end_date.isoformat()}" end_date = date.fromisoformat(start_date) + period_delta
rtypes, rrows = query.run_query( expenses_query = f"SELECT account, convert(cost(position), \"EUR\") as cost_position, currency, date WHERE account ~ \"Assets:Invest:R4:\" AND NOT currency ~ '^(EUR|USD)' AND date >= {
entries, options, expenses_query) start_date} AND date < {end_date.isoformat()}"
return rrows rtypes, rrows = query.run_query(
entries, options, expenses_query)
return rrows
def main(): def main():
parser = argparse.ArgumentParser(description='Generate cash flow report') parser = argparse.ArgumentParser(description='Generate cash flow report')
parser.add_argument('start_date', metavar='start_date', type=str, nargs=1, 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)') 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, parser.add_argument('-p', metavar='period', type=str, choices=["monthly", "yearly"], default="monthly", required=False,
help='Period (monthly or yearly)') help='Period (monthly or yearly)')
args = parser.parse_args() args = parser.parse_args()
start_date = args.start_date[0] start_date = args.start_date[0]
period = args.p period = args.p
filename = "ledger/main.beancount" filename = "ledger/main.beancount"
entries, errors, options = loader.load_file(filename) entries, errors, options = loader.load_file(filename)
if errors: if errors:
printer.print_errors(errors) printer.print_errors(errors)
income = get_income(entries, options, period, start_date) income = get_income(entries, options, period, start_date)
expenses = get_expenses(entries, options, period, start_date) expenses = get_expenses(entries, options, period, start_date)
investments = get_investments(entries, options, period, start_date) investments = get_investments(entries, options, period, start_date)
print_report(start_date, period, income, expenses, investments) print_report(start_date, period, income, expenses, investments)
main()
main()

View File

@@ -6,7 +6,6 @@ readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"babel==2.13.1", "babel==2.13.1",
"beancount==2.3.6",
"beautifulsoup4==4.12.3", "beautifulsoup4==4.12.3",
"blinker==1.7.0", "blinker==1.7.0",
"bottle==0.12.25", "bottle==0.12.25",

1240
uv.lock generated

File diff suppressed because it is too large Load Diff