diff --git a/drcr/models.py b/drcr/models.py index df47ce2..0206d2e 100644 --- a/drcr/models.py +++ b/drcr/models.py @@ -111,6 +111,9 @@ class Amount: def __sub__(self, other): return self + (-other) + def clone(self): + return Amount(self.quantity, self.commodity) + def format(self, commodity='non_reporting'): if commodity not in ('non_reporting', 'force', 'hide'): raise ValueError('Invalid commodity reporting option') @@ -168,6 +171,11 @@ class Balance: def __init__(self): self.amounts = [] + def clone(self): + balance = Balance() + balance.amounts = [a.clone() for a in self.amounts] + return balance + def add(self, rhs): amount = next((a for a in self.amounts if a.commodity == rhs.commodity), None) if amount is None: diff --git a/drcr/templates/transactions.html b/drcr/templates/transactions.html index f817a59..82bd7c2 100644 --- a/drcr/templates/transactions.html +++ b/drcr/templates/transactions.html @@ -1,5 +1,5 @@ {# DrCr: Web-based double-entry bookkeeping framework - Copyright (C) 2022 Lee Yingtong Li (RunasSudo) + Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -42,7 +42,6 @@ {% for transaction in transactions %} {% if transaction.postings|length == 2 %} {% for posting in transaction.postings if posting.account == account %} - {% set _ = running_total.__setattr__('quantity', running_total.quantity + posting.amount().as_cost().quantity) %} {{ transaction.dt.strftime('%Y-%m-%d') }} @@ -52,8 +51,8 @@ {% for p in transaction.postings if p.account != account %}{{ p.account }}{% endfor %} {{ posting.amount().as_cost().format() if posting.quantity >= 0 else '' }} {{ (posting.amount()|abs).as_cost().format() if posting.quantity < 0 else '' }} - {{ (running_total|abs).format() }} - {{ 'Dr' if running_total.quantity >= 0 else 'Cr' }} + {{ (running_totals[posting]|abs).format() }} + {{ 'Dr' if running_totals[posting].quantity >= 0 else 'Cr' }} {% endfor %} {% else %} @@ -69,15 +68,14 @@ {% for posting in transaction.postings if posting.account == account %} - {% set _ = running_total.__setattr__('quantity', running_total.quantity + posting.amount().as_cost().quantity) %} {{ 'Dr' if posting.quantity >= 0 else 'Cr' }} {{ account }} {{ posting.amount().as_cost().format() if posting.quantity >= 0 else '' }} {{ (posting.amount()|abs).as_cost().format() if posting.quantity < 0 else '' }} - {{ (running_total|abs).format() }} - {{ 'Dr' if running_total.quantity >= 0 else 'Cr' }} + {{ (running_totals[posting]|abs).format() }} + {{ 'Dr' if running_totals[posting].quantity >= 0 else 'Cr' }} {% endfor %} {% for posting in transaction.postings if posting.account != account %} diff --git a/drcr/templates/transactions_commodity_detail.html b/drcr/templates/transactions_commodity_detail.html index 424954f..65eeb12 100644 --- a/drcr/templates/transactions_commodity_detail.html +++ b/drcr/templates/transactions_commodity_detail.html @@ -1,5 +1,5 @@ {# DrCr: Web-based double-entry bookkeeping framework - Copyright (C) 2022–2023 Lee Yingtong Li (RunasSudo) + Copyright (C) 2022–2024 Lee Yingtong Li (RunasSudo) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -49,10 +49,7 @@ - {% for posting in transaction.postings if posting.account == account %} - {% set _ = running_total.add(posting.amount()) %} - {% endfor %} - {% for amount in running_total.amounts %} + {% for amount in running_totals[transaction].amounts %} {# FIXME: Assumes at most one posting per commodity #} {% for posting in transaction.postings if posting.commodity == amount.commodity and posting.account == account %} @@ -74,7 +71,6 @@ {% endfor %} {% endfor %} - {% set _ = running_total.clean() %} {% endfor %} diff --git a/drcr/views.py b/drcr/views.py index 3e94685..eea17a6 100644 --- a/drcr/views.py +++ b/drcr/views.py @@ -83,17 +83,39 @@ def account_transactions(): transactions = [t for t in all_transactions() if any(p.account == request.args['account'] for p in t.postings)] if request.args.get('commodity_detail', '0') == '1': + # Pre-compute running totals + # At the level of individual transactions + running_totals = {} + running_total = Balance() + for transaction in sorted(transactions, key=lambda t: t.dt): + for posting in transaction.postings: + if posting.account == request.args['account']: + running_total.add(posting.amount()) + + running_totals[transaction] = running_total.clone() + running_total.clean() + return render_template( 'transactions_commodity_detail.html', account=request.args['account'], - running_total=Balance(), + running_totals=running_totals, transactions=reversed(sorted(transactions, key=lambda t: t.dt)) ) else: + # Pre-compute running totals + # There can be more than one posting per account per transaction, so track the running total at the level of individual postings + running_totals = {} + running_total = Amount(0, '$') + for transaction in sorted(transactions, key=lambda t: t.dt): + for posting in transaction.postings: + if posting.account == request.args['account']: + running_total += posting.amount().as_cost() + running_totals[posting] = running_total + return render_template( 'transactions.html', account=request.args['account'], - running_total=Amount(0, '$'), + running_totals=running_totals, transactions=reversed(sorted(transactions, key=lambda t: t.dt)) )