SPB Git

spb/air Public MIT

AIR — The Language of Accounting.

Python 100%
3.7 KB · 95 lines python
Raw Blame History
1# =============================================================================2# Projet : AIR — Accounting Intermediate Representation3# Auteur : Simon-Pierre Boucher4# Contact : contact@spboucher.ai5# Fichier : backend.py6# Description : QuickBooks Online backend — optional target, fully testable offline.7# =============================================================================8"""QuickBooks Online backend.910QuickBooks is an OPTIONAL export target: AIR's books of record live in the11native ledger regardless. This backend lowers compiled journals to QBO12JournalEntry payloads and posts them through a pluggable transport — the13offline mock by default (no QuickBooks account needed anywhere in the14project), the HTTP transport only when the user supplies credentials.1516Idempotency: every create carries requestid = f"{key}:{DocNumber}" so retries17never double-post. Reversal: QBO has no native journal reversal, so reverse()18posts exact contra entries (never deletes).19"""20from __future__ import annotations2122from typing import Any2324from backends.base import (25    Backend,26    BackendCapabilities,27    PostingReceipt,28    ReversalReceipt,29    TargetPayload,30)31from backends.quickbooks.mapper import contra_body, map_journal32from backends.quickbooks.transport import MockQboTransport, QboTransport33from core.journal import CompiledJournal343536class QuickBooksBackend(Backend):37    def __init__(38        self,39        transport: QboTransport | None = None,40        account_map: dict[str, dict[str, str]] | None = None,41    ):42        self.transport = transport if transport is not None else MockQboTransport()43        self.account_map = account_map44        self._posted: dict[str, dict[str, Any]] = {}   # DocNumber -> body posted4546    def capabilities(self) -> BackendCapabilities:47        return BackendCapabilities(48            name="quickbooks",49            posts_remotely=True,50            native_reversal=False,          # contra-entry strategy51            multi_currency=True,52            idempotency="requestid",53            notes="QBO JournalEntry API, minorversion=75; optional target — "54                  "native ledger remains the book of record",55        )5657    def compile(self, journal: CompiledJournal) -> TargetPayload:58        return TargetPayload(59            backend="quickbooks",60            format="qbo.journalentry",61            body=map_journal(journal, self.account_map),62        )6364    def post(self, payload: TargetPayload, idempotency_key: str) -> PostingReceipt:65        bodies = payload.body66        assert isinstance(bodies, list)67        details: dict[str, str] = {}68        for body in bodies:69            request_id = f"{idempotency_key}:{body['DocNumber']}"70            entity = self.transport.create_journal_entry(body, request_id)71            details[body["DocNumber"]] = entity["Id"]72            self._posted[body["DocNumber"]] = body73        return PostingReceipt(74            backend="quickbooks",75            reference=",".join(details.values()),76            idempotency_key=idempotency_key,77            details=details,78        )7980    def reverse(self, receipt: PostingReceipt) -> ReversalReceipt:81        reversed_ids: list[str] = []82        for doc_number in receipt.details:83            body = self._posted.get(doc_number)84            if body is None:85                continue86            contra = contra_body(body)87            request_id = f"rev:{receipt.idempotency_key}:{contra['DocNumber']}"88            entity = self.transport.create_journal_entry(contra, request_id)89            reversed_ids.append(entity["Id"])90        return ReversalReceipt(91            backend="quickbooks",92            reference=",".join(reversed_ids),93            reversed_reference=receipt.reference,94        )95