millora script budget
This commit is contained in:
26
AGENTS.md
Normal file
26
AGENTS.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# AGENTS.md - Coding Agent Guidelines
|
||||||
|
|
||||||
|
## Build/Test Commands
|
||||||
|
- **Check ledger**: `./commands/check` (validates beancount syntax)
|
||||||
|
- **Serve web UI**: `./commands/serve` (starts fava on port 8080)
|
||||||
|
- **Python tests**: `pytest` (single test: `pytest path/to/test.py::test_name`)
|
||||||
|
- **Install dependencies**: `uv sync` or `pip install -r requirements.txt`
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
- **Main ledger**: `ledger/main.beancount` (entry point)
|
||||||
|
- **Accounts**: `ledger/accounts.beancount`
|
||||||
|
- **Transactions**: `ledger/transactions/YYYY/MM.beancount`
|
||||||
|
- **Python plugins**: `ledger/plugins/`
|
||||||
|
- **Commands**: `commands/` (bash scripts)
|
||||||
|
|
||||||
|
## Code Style & Conventions
|
||||||
|
- **Python**: Follow PEP 8, use type hints, snake_case naming
|
||||||
|
- **Beancount**: Use consistent account naming (Assets:Bank:Checking)
|
||||||
|
- **Imports**: Standard library first, third-party second, local imports last
|
||||||
|
- **Error handling**: Use namedtuples for custom errors in plugins
|
||||||
|
- **Documentation**: Clear docstrings for plugin functions
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
- beancount 2.3.6 (double-entry accounting)
|
||||||
|
- fava 1.26.2 (web interface)
|
||||||
|
- pytest 7.4.3 (testing)
|
||||||
125
commands/budget
125
commands/budget
@@ -11,6 +11,7 @@ from tabulate import tabulate
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
|
||||||
class bcolors:
|
class bcolors:
|
||||||
HEADER = '\033[95m'
|
HEADER = '\033[95m'
|
||||||
OKBLUE = '\033[94m'
|
OKBLUE = '\033[94m'
|
||||||
@@ -22,17 +23,49 @@ class bcolors:
|
|||||||
BOLD = '\033[1m'
|
BOLD = '\033[1m'
|
||||||
UNDERLINE = '\033[4m'
|
UNDERLINE = '\033[4m'
|
||||||
|
|
||||||
|
|
||||||
def get_budget_entries(entries, period, start_date):
|
def get_budget_entries(entries, period, start_date):
|
||||||
budgets = []
|
budgets = []
|
||||||
|
seen_accounts = set()
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
if isinstance(entry, Custom) and entry.values[1].value == period and entry.date <= date.fromisoformat(start_date):
|
if isinstance(entry, Custom) and entry.values[1].value == period \
|
||||||
budgets.append({ "date": entry.date, "account": entry.values[0].value, "period": entry.values[1].value, "budget": entry.values[2].value })
|
and entry.date <= date.fromisoformat(start_date):
|
||||||
|
account = entry.values[0].value
|
||||||
|
if account not in seen_accounts:
|
||||||
|
seen_accounts.add(account)
|
||||||
|
budgets.append({
|
||||||
|
"date": entry.date,
|
||||||
|
"account": account,
|
||||||
|
"period": entry.values[1].value,
|
||||||
|
"budget": entry.values[2].value
|
||||||
|
})
|
||||||
return budgets
|
return budgets
|
||||||
|
|
||||||
def get_expenses(entries, options, period, start_date):
|
|
||||||
period_delta = relativedelta(months=1) if period == "monthly" else relativedelta(years=1)
|
def get_equity_amounts(entries, options, period, start_date):
|
||||||
|
period_delta = relativedelta(
|
||||||
|
months=1) if period == "monthly" else relativedelta(years=1)
|
||||||
end_date = date.fromisoformat(start_date) + period_delta
|
end_date = date.fromisoformat(start_date) + period_delta
|
||||||
expenses_query = f"SELECT account, sum(position) FROM OPEN ON {start_date} CLOSE ON {end_date.isoformat()} WHERE account ~ \"Expenses\""
|
equity_query = f"""SELECT account, sum(position) FROM
|
||||||
|
date >= {start_date} AND
|
||||||
|
date < {end_date.isoformat()}
|
||||||
|
WHERE account ~ \"Equity:(LloguerMiquel|FacturesUtilitatsMiquel)\""""
|
||||||
|
rtypes, rrows = query.run_query(
|
||||||
|
entries, options, equity_query)
|
||||||
|
equity = {}
|
||||||
|
for row in rrows:
|
||||||
|
equity[row.account] = row.sum_position
|
||||||
|
return equity
|
||||||
|
|
||||||
|
|
||||||
|
def get_expenses(entries, options, period, start_date):
|
||||||
|
period_delta = relativedelta(
|
||||||
|
months=1) if period == "monthly" else relativedelta(years=1)
|
||||||
|
end_date = date.fromisoformat(start_date) + period_delta
|
||||||
|
expenses_query = f"""SELECT account, sum(position) FROM
|
||||||
|
date >= {start_date} AND
|
||||||
|
date < {end_date.isoformat()}
|
||||||
|
WHERE account ~ \"Expenses\" OR account ~ \"Liabilities\""""
|
||||||
rtypes, rrows = query.run_query(
|
rtypes, rrows = query.run_query(
|
||||||
entries, options, expenses_query)
|
entries, options, expenses_query)
|
||||||
expenses = {}
|
expenses = {}
|
||||||
@@ -40,37 +73,63 @@ def get_expenses(entries, options, period, start_date):
|
|||||||
expenses[row.account] = row.sum_position
|
expenses[row.account] = row.sum_position
|
||||||
return expenses
|
return expenses
|
||||||
|
|
||||||
def build_budget(budget_entries, expenses):
|
|
||||||
|
def build_budget(budget_entries, expenses, equity_amounts, total_positive_expenses):
|
||||||
result = []
|
result = []
|
||||||
for entry in budget_entries:
|
for entry in budget_entries:
|
||||||
expense = Amount(Decimal(0), entry["budget"].currency)
|
expense = Amount(Decimal(0), entry["budget"].currency)
|
||||||
expense_perc = 0
|
expense_perc = 0
|
||||||
|
total_perc = 0
|
||||||
remaining = entry["budget"]
|
remaining = entry["budget"]
|
||||||
if entry["account"] in expenses:
|
if entry["account"] in expenses:
|
||||||
expense = expenses[entry["account"]].get_only_position()
|
expense = expenses[entry["account"]].get_only_position().units
|
||||||
expense_perc = (expense.units.number / entry["budget"].number) * 100
|
|
||||||
remaining = sub(remaining, expense.units)
|
# Apply equity deductions for specific accounts
|
||||||
|
if entry["account"] == "Expenses:Lloguer" and "Equity:LloguerMiquel" in equity_amounts:
|
||||||
|
equity_amount = equity_amounts["Equity:LloguerMiquel"].get_only_position(
|
||||||
|
)
|
||||||
|
expense = sub(expense, equity_amount.units)
|
||||||
|
elif entry["account"] == "Expenses:FacturesUtilitats" and "Equity:FacturesUtilitatsMiquel" in equity_amounts:
|
||||||
|
equity_amount = equity_amounts["Equity:FacturesUtilitatsMiquel"].get_only_position(
|
||||||
|
)
|
||||||
|
expense = sub(expense, equity_amount.units)
|
||||||
|
|
||||||
|
expense_perc = (expense.number /
|
||||||
|
entry["budget"].number) * 100
|
||||||
|
# Calculate percentage of total positive expenses only
|
||||||
|
if total_positive_expenses.number > 0 and expense.number > 0:
|
||||||
|
total_perc = (expense.number /
|
||||||
|
total_positive_expenses.number) * 100
|
||||||
|
remaining = sub(remaining, expense)
|
||||||
result.append({
|
result.append({
|
||||||
"Account": entry["account"],
|
"Account": entry["account"],
|
||||||
"Budget": entry["budget"].to_string(),
|
"Budget": entry["budget"].to_string(),
|
||||||
"Expense": expense,
|
"Expense": expense,
|
||||||
"Expense (%)": "{}{:,.2f}%{}".format(bcolors.FAIL if expense_perc >= 100 else '', expense_perc, bcolors.ENDC),
|
"Expense (%)": "{}{:,.2f}%{}".format(
|
||||||
|
bcolors.FAIL if expense_perc >= 100 else '',
|
||||||
|
expense_perc, bcolors.ENDC),
|
||||||
|
"Total (%)": "{:,.2f}%".format(total_perc),
|
||||||
"Remaining": remaining
|
"Remaining": remaining
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def print_report(budget_report, period, start_date, budget_sum, expenses_sum):
|
def print_report(budget_report, period, start_date, budget_sum, expenses_sum):
|
||||||
print(f"Budget Report (period={period}, start_date={start_date})")
|
print(f"Budget Report (period={period}, start_date={start_date})")
|
||||||
print(f"Budget: {budget_sum}")
|
print(f"Budget: {budget_sum}")
|
||||||
print(f"{bcolors.FAIL if expenses_sum >= budget_sum else ''}Expenses: {expenses_sum}{bcolors.ENDC}")
|
print(f"{bcolors.FAIL if expenses_sum >= budget_sum else ''}Expenses: {
|
||||||
headings = ['Account', 'Budget', 'Expense', '(%)', 'Remaining',]
|
expenses_sum}{bcolors.ENDC}")
|
||||||
print(tabulate(budget_report, headers="keys", numalign="right", floatfmt=".2f"))
|
print(tabulate(budget_report, headers="keys", numalign="right", floatfmt=".2f"))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='Generate budget report')
|
parser = argparse.ArgumentParser(description='Generate budget 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 '
|
||||||
parser.add_argument('-p', metavar='period', type=str, choices=["monthly", "yearly"], default="monthly", required=False,
|
'monthly report or one year after if yearly report)')
|
||||||
|
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()
|
||||||
@@ -85,16 +144,42 @@ def main():
|
|||||||
|
|
||||||
budget_entries = get_budget_entries(entries, period, start_date)
|
budget_entries = get_budget_entries(entries, period, start_date)
|
||||||
# TODO: Multiple currencies
|
# TODO: Multiple currencies
|
||||||
budget_sum = reduce(lambda a, b: add(a, b["budget"]), budget_entries, Amount(Decimal(0), budget_entries[0]["budget"].currency))
|
budget_sum = reduce(
|
||||||
|
lambda a, b: add(a, b["budget"]),
|
||||||
|
budget_entries,
|
||||||
|
Amount(Decimal(0), budget_entries[0]["budget"].currency)
|
||||||
|
)
|
||||||
expenses = get_expenses(entries, options, period, start_date)
|
expenses = get_expenses(entries, options, period, start_date)
|
||||||
|
equity_amounts = get_equity_amounts(entries, options, period, start_date)
|
||||||
filtered_expenses = {}
|
filtered_expenses = {}
|
||||||
for entry in budget_entries:
|
for entry in budget_entries:
|
||||||
if entry["account"] in expenses:
|
if entry["account"] in expenses:
|
||||||
filtered_expenses[entry["account"]] = expenses[entry["account"]]
|
filtered_expenses[entry["account"]] = expenses[entry["account"]]
|
||||||
expenses_sum = reduce(lambda a, b: add(a, b.get_only_position().units),
|
|
||||||
filtered_expenses.values(),
|
# Calculate total of positive expenses only for percentage calculation
|
||||||
Amount(Decimal(0), budget_entries[0]["budget"].currency))
|
positive_expenses_sum = Amount(
|
||||||
budget_report = build_budget(budget_entries, expenses)
|
Decimal(0), budget_entries[0]["budget"].currency)
|
||||||
print_report(budget_report, period, start_date, budget_sum, expenses_sum)
|
for entry in budget_entries:
|
||||||
|
if entry["account"] in expenses:
|
||||||
|
expense = expenses[entry["account"]].get_only_position().units
|
||||||
|
|
||||||
|
# Apply equity deductions for specific accounts (same logic as in build_budget)
|
||||||
|
if entry["account"] == "Expenses:Lloguer" and "Equity:LloguerMiquel" in equity_amounts:
|
||||||
|
equity_amount = equity_amounts["Equity:LloguerMiquel"].get_only_position(
|
||||||
|
)
|
||||||
|
expense = sub(expense, equity_amount.units)
|
||||||
|
elif entry["account"] == "Expenses:FacturesUtilitats" and "Equity:FacturesUtilitatsMiquel" in equity_amounts:
|
||||||
|
equity_amount = equity_amounts["Equity:FacturesUtilitatsMiquel"].get_only_position(
|
||||||
|
)
|
||||||
|
expense = sub(expense, equity_amount.units)
|
||||||
|
|
||||||
|
if expense.number > 0:
|
||||||
|
positive_expenses_sum = add(positive_expenses_sum, expense)
|
||||||
|
|
||||||
|
budget_report = build_budget(
|
||||||
|
budget_entries, expenses, equity_amounts, positive_expenses_sum)
|
||||||
|
print_report(budget_report, period, start_date,
|
||||||
|
budget_sum, positive_expenses_sum)
|
||||||
|
|
||||||
|
|
||||||
main()
|
main()
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
;; BUDGET 2025
|
;; BUDGET 2025
|
||||||
;; TOTAL:
|
;; TOTAL:
|
||||||
;; Monthly: 1807.00 EUR
|
;; Monthly: 3027.00 EUR
|
||||||
;; Yearly: 6782.65 EUR
|
;; Yearly: 6849.65 EUR
|
||||||
;;;; 565.22 EUR/month
|
2025-01-01 custom "budget" Expenses:Lloguer "monthly" 620.00 EUR
|
||||||
;; Total monthly: 2372.22 EUR
|
|
||||||
2025-01-01 custom "budget" Expenses:Lloguer "monthly" 600.00 EUR
|
|
||||||
2025-01-01 custom "budget" Expenses:FacturesUtilitats "monthly" 80.00 EUR
|
2025-01-01 custom "budget" Expenses:FacturesUtilitats "monthly" 80.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:Internet "monthly" 50.00 EUR
|
2025-01-01 custom "budget" Expenses:Internet "monthly" 50.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:Gasolina "monthly" 50.00 EUR
|
2025-01-01 custom "budget" Expenses:Gasolina "monthly" 50.00 EUR
|
||||||
@@ -15,43 +13,18 @@
|
|||||||
2025-01-01 custom "budget" Expenses:Entreteniment "monthly" 50.00 EUR
|
2025-01-01 custom "budget" Expenses:Entreteniment "monthly" 50.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:MenjarFora "monthly" 290.00 EUR
|
2025-01-01 custom "budget" Expenses:MenjarFora "monthly" 290.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:Altres "monthly" 200.00 EUR
|
2025-01-01 custom "budget" Expenses:Altres "monthly" 200.00 EUR
|
||||||
|
2025-01-01 custom "budget" Liabilities:Credit:Renta4:PolissaCredit "monthly" 1200 EUR
|
||||||
|
|
||||||
2025-01-01 custom "budget" Expenses:Roba "yearly" 1000 EUR
|
2025-01-01 custom "budget" Expenses:Roba "yearly" 1000 EUR
|
||||||
2025-01-01 custom "budget" Expenses:MantenimentCotxe "yearly" 800.00 EUR
|
2025-01-01 custom "budget" Expenses:MantenimentCotxe "yearly" 800.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:Educació "yearly" 150.00 EUR
|
2025-01-01 custom "budget" Expenses:Educació "yearly" 150.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:Medic "yearly" 650.00 EUR
|
2025-01-01 custom "budget" Expenses:Medic "yearly" 650.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:Vacances "yearly" 2500.00 EUR
|
2025-01-01 custom "budget" Expenses:Vacances "yearly" 2500.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:Taxes:ImpostCirculacio "yearly" 54.00 EUR
|
2025-01-01 custom "budget" Expenses:Taxes:ImpostCirculacio "yearly" 66.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:NintendoOnline "yearly" 35.00 EUR
|
2025-01-01 custom "budget" Expenses:NintendoOnline "yearly" 35.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:AmazonPrime "yearly" 50.00 EUR
|
2025-01-01 custom "budget" Expenses:AmazonPrime "yearly" 50.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:Insurance:Cotxe "yearly" 585.00 EUR
|
2025-01-01 custom "budget" Expenses:Insurance:Cotxe "yearly" 640.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:CarnetJove "yearly" 8.65 EUR
|
2025-01-01 custom "budget" Expenses:CarnetJove "yearly" 8.65 EUR
|
||||||
2025-01-01 custom "budget" Expenses:Gimnàs "yearly" 800.00 EUR
|
2025-01-01 custom "budget" Expenses:Gimnàs "yearly" 800.00 EUR
|
||||||
2025-01-01 custom "budget" Expenses:MarcaPersonal "yearly" 150.00 EUR
|
2025-01-01 custom "budget" Expenses:MarcaPersonal "yearly" 150.00 EUR
|
||||||
|
|
||||||
;; BUDGET 2024
|
|
||||||
|
|
||||||
2024-01-01 custom "budget" Expenses:Lloguer "monthly" 600.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:FacturesUtilitats "monthly" 80.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Internet "monthly" 50.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Gasolina "monthly" 50.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Perruqueria "monthly" 16.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Supermercat "monthly" 180.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Parking "monthly" 122.09 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Mobilitat "monthly" 125.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Entreteniment "monthly" 60.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:MenjarFora "monthly" 280.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Altres "monthly" 150.00 EUR
|
|
||||||
|
|
||||||
2024-01-01 custom "budget" Expenses:Roba "yearly" 1000 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:MantenimentCotxe "yearly" 180.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Educació "yearly" 200.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Medic "yearly" 800.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Vacances "yearly" 2000.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Taxes:ImpostCirculacio "yearly" 55.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:NintendoOnline "yearly" 35.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:AmazonPrime "yearly" 50.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Insurance:Cotxe "yearly" 520.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:CarnetJove "yearly" 8.65 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:Gimnàs "yearly" 800.00 EUR
|
|
||||||
2024-01-01 custom "budget" Expenses:MarcaPersonal "yearly" 200.00 EUR
|
|
||||||
|
|||||||
Reference in New Issue
Block a user