# ============================================================================= # Projet : AIR — Accounting Intermediate Representation # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : model.py # Description : ALSL v0.1 data model — policy sets, tax/classification policies, account roles. # ============================================================================= """ALSL (Accounting Language for Standards and Legislation) v0.1 model. ALSL is AIR's TableGen: ALL fiscal and policy parameters (tax rates, rounding modes, capitalization thresholds, chart-of-account mappings) live in versioned policy sets — never in compiler code. v0.1 is a YAML subset; the full DSL comes later. Every tax policy must cite its source (a docs/research/ file or an official URL) — uncited rates are rejected by the loader. """ from __future__ import annotations from dataclasses import dataclass, field from decimal import Decimal from core.journal import Account, AccountType from core.money import RoundingMode ALSL_VERSION = "0.1" @dataclass(frozen=True, slots=True) class WhenClause: """Conditions under which a policy fires. All present fields must match.""" jurisdiction: str | None = None # exact match, e.g. "CA-QC" jurisdiction_in: tuple[str, ...] | None = None event_type: str | None = None # e.g. "Purchase" min_amount: Decimal | None = None # inclusive threshold on the subtotal currency: str | None = None # currency the threshold is expressed in @dataclass(frozen=True, slots=True) class TaxComponent: """One tax to apply (e.g. GST at 0.05 on the subtotal).""" code: str # "GST", "QST", "HST" rate: Decimal base: str = "subtotal" # v0.1: only "subtotal" (price before other taxes) payable_role: str = "" # account role credited on sales receivable_role: str = "" # account role debited on purchases (ITC/ITR) recoverable_on_purchase: bool = True @dataclass(frozen=True, slots=True) class TaxPolicy: name: str when: WhenClause components: tuple[TaxComponent, ...] source: str # citation — mandatory @dataclass(frozen=True, slots=True) class ClassificationPolicy: """e.g. capitalize purchases >= threshold as assets.""" name: str when: WhenClause classify_as: str # "asset" | "expense" account_role: str # role receiving the debit (e.g. "equipment") source: str = "" @dataclass(frozen=True, slots=True) class RoundingPolicy: mode: RoundingMode = RoundingMode.HALF_UP source: str = "" @dataclass(frozen=True, slots=True) class PolicySet: """A versioned, self-contained rule set for one compilation.""" alsl_version: str name: str # e.g. "ca-qc" version: str # e.g. "2026.08" description: str = "" functional_currency: str = "CAD" rounding: RoundingPolicy = field(default=RoundingPolicy()) accounts: dict[str, Account] = field(default_factory=dict) # role -> Account tax_policies: tuple[TaxPolicy, ...] = () classification_policies: tuple[ClassificationPolicy, ...] = () def account(self, role: str) -> Account: try: return self.accounts[role] except KeyError: raise KeyError( f"policy set '{self.name}' defines no account for role '{role}'" ) from None def parse_account_type(value: str) -> AccountType: return AccountType(value)