SPB Git

spb/air Public MIT

AIR — The Language of Accounting.

Python 100%
3.6 KB · 102 lines python
Raw Blame History
1# =============================================================================2# Projet : AIR — Accounting Intermediate Representation3# Auteur : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : model.py6# Description : ALSL v0.1 data model — policy sets, tax/classification policies, account roles.7# =============================================================================8"""ALSL (Accounting Language for Standards and Legislation) v0.1 model.910ALSL is AIR's TableGen: ALL fiscal and policy parameters (tax rates, rounding11modes, capitalization thresholds, chart-of-account mappings) live in versioned12policy sets — never in compiler code. v0.1 is a YAML subset; the full DSL13comes later.1415Every tax policy must cite its source (a docs/research/ file or an official16URL) — uncited rates are rejected by the loader.17"""18from __future__ import annotations1920from dataclasses import dataclass, field21from decimal import Decimal2223from core.journal import Account, AccountType24from core.money import RoundingMode2526ALSL_VERSION = "0.1"272829@dataclass(frozen=True, slots=True)30class WhenClause:31    """Conditions under which a policy fires. All present fields must match."""3233    jurisdiction: str | None = None            # exact match, e.g. "CA-QC"34    jurisdiction_in: tuple[str, ...] | None = None35    event_type: str | None = None              # e.g. "Purchase"36    min_amount: Decimal | None = None          # inclusive threshold on the subtotal37    currency: str | None = None                # currency the threshold is expressed in383940@dataclass(frozen=True, slots=True)41class TaxComponent:42    """One tax to apply (e.g. GST at 0.05 on the subtotal)."""4344    code: str                                  # "GST", "QST", "HST"45    rate: Decimal46    base: str = "subtotal"                     # v0.1: only "subtotal" (price before other taxes)47    payable_role: str = ""                     # account role credited on sales48    receivable_role: str = ""                  # account role debited on purchases (ITC/ITR)49    recoverable_on_purchase: bool = True505152@dataclass(frozen=True, slots=True)53class TaxPolicy:54    name: str55    when: WhenClause56    components: tuple[TaxComponent, ...]57    source: str                                # citation — mandatory585960@dataclass(frozen=True, slots=True)61class ClassificationPolicy:62    """e.g. capitalize purchases >= threshold as assets."""6364    name: str65    when: WhenClause66    classify_as: str                           # "asset" | "expense"67    account_role: str                          # role receiving the debit (e.g. "equipment")68    source: str = ""697071@dataclass(frozen=True, slots=True)72class RoundingPolicy:73    mode: RoundingMode = RoundingMode.HALF_UP74    source: str = ""757677@dataclass(frozen=True, slots=True)78class PolicySet:79    """A versioned, self-contained rule set for one compilation."""8081    alsl_version: str82    name: str                                  # e.g. "ca-qc"83    version: str                               # e.g. "2026.08"84    description: str = ""85    functional_currency: str = "CAD"86    rounding: RoundingPolicy = field(default=RoundingPolicy())87    accounts: dict[str, Account] = field(default_factory=dict)   # role -> Account88    tax_policies: tuple[TaxPolicy, ...] = ()89    classification_policies: tuple[ClassificationPolicy, ...] = ()9091    def account(self, role: str) -> Account:92        try:93            return self.accounts[role]94        except KeyError:95            raise KeyError(96                f"policy set '{self.name}' defines no account for role '{role}'"97            ) from None9899100def parse_account_type(value: str) -> AccountType:101    return AccountType(value)102