finance.ledger.journal-validate@1.0.0
impl/python.py
3,049 bytes · the Python implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import List, Optional, Sequence
from .finance_ledger_journal_validate_types import JournalCheck, JournalLine, JournalProblem
from .money_add import add_money ← from money.add ^1.0.0 · built alongside by fune
from .money_amount import money ← from money.amount ^1.0.0 · built alongside by fune
def _is_alnum(ch: str) -> bool:
return ("0" <= ch <= "9") or ("A" <= ch <= "Z") or ("a" <= ch <= "z")
def _is_account_code(account: str) -> bool:
"""1-20 ASCII letters and digits, in groups joined by a single - . or /"""
if len(account) < 1 or len(account) > 20:
return False
previous_was_alnum = False
for ch in account:
if _is_alnum(ch):
previous_was_alnum = True
elif ch in "-./" and previous_was_alnum:
previous_was_alnum = False
else:
return False
return previous_was_alnum
def validate_journal(lines: Sequence[JournalLine], currency: str) -> JournalCheck:
"""Check a journal before it is posted, reporting every problem at once.
A line in the wrong currency or with a negative amount is left out of the
totals, since adding it would make them meaningless; the other problems
leave the line's amounts in.
"""
debit_total = money(0, currency)
credit_total = money(0, currency)
problems: List[JournalProblem] = []
def report(line: Optional[int], code: str, message: str) -> None:
problems.append(JournalProblem(line=line, code=code, message=message))
if len(lines) == 0:
report(None, "empty", "the journal has no lines")
for index, entry in enumerate(lines):
n = index + 1
if not _is_account_code(entry.account):
report(n, "invalid-account", 'line %d: account "%s" is not a valid account code' % (n, entry.account))
foreign = None
if entry.debit.currency != currency:
foreign = entry.debit.currency
elif entry.credit.currency != currency:
foreign = entry.credit.currency
if foreign is not None:
report(n, "currency-mismatch", "line %d: amount is in %s, not the journal currency %s" % (n, foreign, currency))
continue
if entry.debit.minor < 0 or entry.credit.minor < 0:
report(n, "negative-amount", "line %d: debit and credit must not be negative" % (n,))
continue
if entry.debit.minor == 0 and entry.credit.minor == 0:
report(n, "zero-line", "line %d: debit and credit are both zero" % (n,))
elif entry.debit.minor != 0 and entry.credit.minor != 0:
report(n, "both-sides", "line %d: has both a debit and a credit" % (n,))
debit_total = add_money(debit_total, entry.debit)
credit_total = add_money(credit_total, entry.credit)
difference = money(debit_total.minor - credit_total.minor, currency)
if difference.minor != 0:
report(None, "unbalanced", "debits do not equal credits")
return JournalCheck(
valid=len(problems) == 0,
debit_total=debit_total,
credit_total=credit_total,
difference=difference,
problems=problems,
)