from typing import List, Optional, Sequence from .finance_ledger_journal_validate_types import JournalCheck, JournalLine, JournalProblem from .money_add import add_money from .money_amount import money 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, )