spb/air Public MIT
AIR — The Language of Accounting.
Python 100%
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