from typing import Dict, List, Sequence, Tuple from .finance_ledger_journal_validate import validate_journal from .finance_ledger_journal_validate_types import JournalLine from .finance_ledger_trial_balance_types import TrialBalance, TrialBalanceRow from .money_add import add_money from .money_amount import Money, money _FATAL = ("invalid-account", "currency-mismatch", "negative-amount") def trial_balance(lines: Sequence[JournalLine], currency: str) -> TrialBalance: """A trial balance: each account's balance on its own side, and whether the debit column equals the credit column. Lines that cannot be totalled are raised with journal-validate's message; an unbalanced ledger is an answer, not an error. """ check = validate_journal(lines, currency) for problem in check.problems: if problem.code in _FATAL: raise ValueError(problem.message) zero = money(0, currency) totals: Dict[str, Tuple[Money, Money]] = {} for line in lines: if line.debit.minor == 0 and line.credit.minor == 0: continue debits, credits = totals.get(line.account, (zero, zero)) totals[line.account] = (add_money(debits, line.debit), add_money(credits, line.credit)) rows: List[TrialBalanceRow] = [] total_debit = zero total_credit = zero for account in sorted(totals): debits, credits = totals[account] net = debits.minor - credits.minor debit_balance = money(net if net > 0 else 0, currency) credit_balance = money(-net if net < 0 else 0, currency) rows.append( TrialBalanceRow( account=account, debits=debits, credits=credits, debit_balance=debit_balance, credit_balance=credit_balance, ) ) total_debit = add_money(total_debit, debit_balance) total_credit = add_money(total_credit, credit_balance) return TrialBalance( rows=rows, total_debit=total_debit, total_credit=total_credit, balanced=total_debit.minor == total_credit.minor, )