Recompute running balances when required

This commit is contained in:
RunasSudo 2024-11-09 23:41:42 +11:00
parent 18392d6917
commit 9373212482
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
2 changed files with 40 additions and 3 deletions

View File

@ -18,7 +18,7 @@ from flask import abort, redirect, render_template, request, url_for
from .. import AMOUNT_DPS from .. import AMOUNT_DPS
from ..database import db from ..database import db
from ..models import Amount, Posting, Transaction, TrialBalancer, reporting_commodity from ..models import Amount, Posting, Transaction, TrialBalancer, queue_invalidate_running_balances, reporting_commodity
from ..webapp import all_accounts, all_transactions, app from ..webapp import all_accounts, all_transactions, app
from .models import BalanceAssertion from .models import BalanceAssertion
from ..statements.models import StatementLineReconciliation from ..statements.models import StatementLineReconciliation
@ -59,9 +59,12 @@ def journal_new_transaction():
) )
transaction.postings.append(posting) transaction.postings.append(posting)
transaction.assert_valid() # Invalidate future running balances
queue_invalidate_running_balances(account, transaction.dt)
transaction.assert_valid()
db.session.add(transaction) db.session.add(transaction)
db.session.commit() db.session.commit()
return redirect(request.form.get('referrer', '') or url_for('journal')) return redirect(request.form.get('referrer', '') or url_for('journal'))
@ -104,6 +107,9 @@ def journal_edit_transaction():
) )
new_postings.append(posting) new_postings.append(posting)
# Invalidate future running balances
queue_invalidate_running_balances(account, transaction.dt)
# Fix up reconciliations # Fix up reconciliations
for old_posting in transaction.postings: for old_posting in transaction.postings:
for reconciliation in StatementLineReconciliation.query.filter(StatementLineReconciliation.posting == old_posting): for reconciliation in StatementLineReconciliation.query.filter(StatementLineReconciliation.posting == old_posting):

View File

@ -70,15 +70,26 @@ class Posting(db.Model):
transaction = db.relationship('Transaction', back_populates='postings') transaction = db.relationship('Transaction', back_populates='postings')
def __init__(self, description=None, account=None, quantity=None, commodity=None): def __init__(self, description=None, account=None, quantity=None, commodity=None, running_balance=None):
self.description = description self.description = description
self.account = account self.account = account
self.quantity = quantity self.quantity = quantity
self.commodity = commodity self.commodity = commodity
self.running_balance = running_balance
def amount(self): def amount(self):
return Amount(self.quantity, self.commodity) return Amount(self.quantity, self.commodity)
def queue_invalidate_running_balances(account, dt_from):
"""
Invalidate running_balances for Postings in the specified account, from the given date onwards
NOTE: Does not call db.session.commit()
"""
for posting in db.session.scalars(db.select(Posting).join(Posting.transaction).where((Transaction.dt >= dt_from) & (Posting.account == account))).all():
posting.running_balance = None
class Amount: class Amount:
__slots__ = ['quantity', 'commodity'] __slots__ = ['quantity', 'commodity']
@ -204,6 +215,26 @@ class TrialBalancer:
def from_cached(cls, start_date=None, end_date=None): def from_cached(cls, start_date=None, end_date=None):
"""Obtain a TrialBalancer based on the cached running_balance""" """Obtain a TrialBalancer based on the cached running_balance"""
# First, recompute any running_balance if required
stale_accounts = db.session.scalars('SELECT DISTINCT account FROM postings WHERE running_balance IS NULL').all()
if stale_accounts:
# Get all relevant Postings in database in correct order
# FIXME: Recompute balances only from the last non-stale balance to be more efficient
postings = db.session.scalars(db.select(Posting).join(Posting.transaction).where(Posting.account.in_(stale_accounts)).order_by(Transaction.dt, Transaction.id)).all()
accounts = {}
for posting in postings:
if posting.account not in accounts:
accounts[posting.account] = Amount(0, reporting_commodity())
# FIXME: Handle commodities better (ensure compatible commodities)
accounts[posting.account].quantity += posting.amount().as_cost().quantity
posting.running_balance = accounts[posting.account].quantity
db.session.commit()
if start_date is not None: if start_date is not None:
result_start_date = cls() result_start_date = cls()