Swiss VAT (MWST) calculation and quarterly reporting
bookkeeping/vat-reporting-and-rates
| Kind | backend |
|---|---|
| Domain | bookkeeping |
| Maturity | prod |
| Reusable | extract |
| Stack | fastapi-0.118, sqlalchemy-2.0-sync, postgres, pydantic-2.12, alembic-1.16 |
| Depends on | _shared/fastapi-project-layout, _shared/alembic-migrations, _shared/auth-dependency-pattern, _shared/workspace-scoping, _shared/test-layout |
| Frontends | bookkeeping/ui/bookkeeping-financial-dashboard |
| Reference implementation |
|
| Measured on | alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12 |
| Version | 1 |
Purpose
A workspace converts an amount between net and gross at one of Switzerland's four MWST codes, checks a partner's UID/MWST number's check digit, and turns its own posted journal entries for one calendar period into a quarterly VAT return by either the effective (Vorsteuerabzug) or the Saldosteuersatz (SSS, flat-rate) method, then submits that return once. The invariant that makes the return trustworthy is that it is never a stored balance read back: both generate_report_* methods re-aggregate JournalEntryLine rows straight from Postgres, scoped to JournalEntry.status == posted and the exact [period_start, period_end] window, every time they are called (vat_service.py 83 to 140, 172 to 188), so a report reflects exactly what was posted at generation time, not a running total that could drift from the ledger.
When to use
Read it before touching VatService.net_to_gross/gross_to_net, validate_uid, generate_report_effective, generate_report_sss, or submit_report; before adding a fifth VAT code or changing the 2024+ rate table; before wiring the frontend's filing_method selector to actually offer sss (right now it does not, see Gotchas); and when someone asks "why is the VAT payable wrong for this quarter", "why didn't Vorsteuer show up", "is this UID valid", or "can I regenerate this quarter's report". Trigger phrases: "MWST", "Vorsteuer", "Saldosteuersatz", "SSS", "N81/R26/L38", "Form 773", "CHE-", "UID check digit", "effective method", "vat_payable".
Not this recipe: the chart-of-accounts and journal-entry posting pipeline this report reads from (services/accounting/journal_service.py, a separate recipe, not read for this one, cited here only by table name); balance_sheet/profit_loss/trial_balance/account_ledger in the same router (report_service, a sibling service, different file, not touched here); capital_service's dividend/partner endpoints in the same router file (lines 97 to 134 of accounting_reports_router.py, unrelated domain sharing a router); any actual e-VAT / ESTV portal submission, submit_report only flips a status column and stamps a timestamp in this database, it never calls out to a Swiss tax authority API, there is no such integration in this codebase at this commit (Gotchas); AccountingAuditLog's own read/query surface, this recipe only calls audit_service.log_action as a one-way write on every generate and submit.
Stack assumptions
At 62ba4fc: fastapi==0.118.2, synchronous handlers on sqlalchemy.orm.Session (SQLAlchemy==2.0.43), pydantic==2.12.0 for the request/response schemas, psycopg2-binary==2.9.10, alembic==1.16.5, Postgres (test harness on localhost:5434, docker-compose.test.yml's postgres-test service, per tests/conftest.py 44 to 48). No httpx/requests and no scheduler involvement anywhere in this recipe, everything runs synchronously inside one request. Foundations assumed: S4 (fastapi-project-layout) for mount_router and the app wiring; S8 (alembic-migrations) for the one migration this recipe's table comes from; S5 (auth-dependency-pattern) for ActiveUser; S6 (workspace-scoping) for WorkspaceID; S9 (test-layout) for the savepoint-isolated db_session/test_user fixtures both test files depend on.
No dedicated environment variable exists for this recipe: no feature flag, no external credential, nothing in core/config.py gates VAT calculation or reporting. Fail behaviour is unremarkable and synchronous: submit_report raises HTTPException directly (404/400, see API surface) rather than catching and translating; generate_report_effective/generate_report_sss catch nothing, an aggregation or a database error propagates as an unhandled 500 through FastAPI's default handler, there is no module-level try/except analogous to charge_if_due's outer wrapper in the billing-credits recipe.
Data model
accounting_vat_reports, one row per generated report, no upsert, no update after generation except by submit_report (models/accounting_models.py 479 to 508; migrations/versions/20260214_add_accounting_tables.py 344 to 370):
accounting_vat_reports (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
period_start date NOT NULL,
period_end date NOT NULL,
filing_method vat_filing_method NOT NULL, -- 'effective' | 'sss'
status vat_report_status NOT NULL DEFAULT 'draft', -- draft | submitted | accepted
-- effective method (Form 773)
total_revenue numeric(15,2),
taxable_revenue_standard numeric(15,2),
taxable_revenue_reduced numeric(15,2),
taxable_revenue_lodging numeric(15,2),
output_vat numeric(15,2),
input_vat numeric(15,2),
vat_payable numeric(15,2),
-- SSS method
sss_rate numeric(5,2),
sss_revenue numeric(15,2),
sss_tax numeric(15,2),
report_data json,
submitted_at timestamptz,
created_by uuid NOT NULL REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz DEFAULT now(),
updated_at timestamptz
);
UNIQUE INDEX ix_vat_reports_ws_period (workspace_id, period_start, period_end)
vat_payable is shared by both methods (effective: output_vat - input_vat; SSS: equal to sss_tax), which is what lets one response schema and one frontend card serve either filing method. report_data (a raw json column) is declared on the model and the schema but never written by either generate method at this commit, it is dead storage, nothing populates it (grep -n "report_data" services/accounting/vat_service.py matches nothing but the import list).
This recipe reads, never writes: accounting_journal_entries (entry_date, status) and accounting_journal_entry_lines (debit_amount, credit_amount, vat_code, vat_amount) joined through accounting_accounts (account_type, account_number), all defined in journal_service's recipe, cited here by column name only. What is NOT stored anywhere in this recipe: the UID/MWST number checked by validate_uid, the function is a pure calculation with no database read or write on either side; a computed VAT rate against a partner record, net_to_gross/gross_to_net/the /vat/calculate route take the rate as a caller-supplied argument, nothing here looks a rate up by product or customer.
API surface
Mounted at /api/accounting (app_fastapi.py 918, mount_router(app, accounting_reports_router.router, prefix="/api/accounting", tags=["Accounting Reports"], surfaces=LABS_SURFACES)), all five routes require ActiveUser and, except the two stateless calculation routes, WorkspaceID:
| Route | Auth | Tenancy | Body / params | Codes | Notes |
|---|---|---|---|---|---|
GET /vat/rates (accounting_reports_router.py 57 to 59) | ActiveUser | none | none | 200 {code: "rate"} for all five VAT_RATES codes | stateless, calls vat_service.get_rates() |
POST /vat/calculate (62 to 68) | ActiveUser | none | query params net_amount: Decimal, vat_code: str | 200 net_to_gross shape; 400 Unknown VAT code: {code} | stateless; the router imports VAT_RATES locally inside the function body (64), not at module scope |
POST /vat/reports (71 to 84), response_model=VatReportResponse | ActiveUser | WorkspaceID | VatReportCreate: period_start, period_end, filing_method, sss_rate: Optional[Decimal] = Field(None, ge=0, le=100) | 200 the persisted report row; 500 on an unhandled aggregation/DB error (no explicit catch) | branches on filing_method (73 to 84); when sss and sss_rate is omitted, the router defaults it to Decimal("6.5") (80), the service itself takes the rate as a required positional argument and applies no default of its own |
POST /vat/reports/{report_id}/submit (87 to 89), response_model=VatReportResponse | ActiveUser | WorkspaceID | path report_id: UUID | 200 the now-submitted report; 404 VAT report not found (wrong id OR wrong workspace, same message either way); 400 Report already submitted | one-way: draft -> submitted, no route or method transitions submitted -> accepted at this commit |
GET /vat/validate-uid (92 to 94) | ActiveUser | none | query uid: str | 200 {"uid": ..., "valid": bool} | never 400s on a malformed string, an unparsable UID simply returns valid: false |
Application logic
net_to_gross(net, rate) (vat_service.py 37 to 45): vat = round_half_up(net * rate / 100, 2); returns net_amount, vat_rate, vat_amount, gross_amount = net + vat. gross_to_net(gross, rate) (48 to 59): guards rate == -100 with a ValueError before dividing (the only exception-raising guard in either conversion function); net = round_half_up(gross * 100 / (100 + rate), 2); vat_amount = gross - net (a subtraction, not its own rounding pass, so net + vat_amount always reconstructs the exact input gross, the invariant test_rounding checks). Both use ROUND_HALF_UP on a two-decimal Decimal quantize, never banker's rounding.
validate_uid(uid) (61 to 74): strips the literal substrings "CHE-", "CHE", ".", " ", "MWST" (in that order, 64) rather than a regex, then requires exactly 9 remaining digits; computes the ISO 7064 MOD 11-10-style check digit with weights [5,4,3,2,7,6,5,4] against the first 8 digits, check = 11 - (total % 11), a check == 10 is refused outright (invalid, 70 to 71) and check == 11 is normalized to 0 (72 to 73) before comparing to the 9th digit.
generate_report_effective(db, workspace_id, user_id, period_start, period_end) (76 to 165), in commit order:
- One grouped query sums
credit_amount - debit_amountandvat_amountpervat_code, joinedJournalEntryLine -> JournalEntry -> AccountingAccount, filtered to this workspace,status == posted,entry_dateinside[period_start, period_end],account_type == revenue(83 to 102). - In Python, each row buckets into
taxable_standard/taxable_reduced/taxable_lodgingand accumulatesoutput_vatby matchingvat_codeagainst the literal strings"N81"/"R26"/"L38"(110 to 123); any othervat_codeon a revenue line ("E00","X00",None, or a code the ledger invented) still adds tototal_revenue(113) but is silently excluded from every taxable bucket and fromoutput_vat, there is noelsebranch and no warning. - A second query sums
debit_amount - credit_amounton lines whose account number is"1170"or"1171"(Vorsteuer Material / Investition), workspace- and period-scoped the same way, and calls thatinput_vat(126 to 140), it does not filter byvat_codeor byaccount_type, only by the two hardcoded account numbers. vat_payable = output_vat - input_vat(142); oneAccountingVatReportrow is constructed withstatus=draftand inserted,db.flush(), one audit log entry (audit_service.log_action(..., "vat_report", report.id, "generate")),db.commit(),db.refresh(report)(144 to 165).
generate_report_sss(db, workspace_id, user_id, period_start, period_end, sss_rate) (167 to 210): one query sums credit_amount - debit_amount over every account_type == revenue line in the period, with no VAT-code bucketing at all (172 to 188); sss_tax = round_half_up(sss_revenue * sss_rate / 100, 2); vat_payable is set equal to sss_tax; same insert/flush/audit("generate_sss")/commit/refresh shape as the effective path.
submit_report(db, workspace_id, report_id, user_id) (212 to 232): looks the report up filtered by both id AND workspace_id in one WHERE (213 to 220, the only tenancy check in this file, a query filter rather than a post-fetch comparison); None raises 404 before anything about status is inspected; status != draft raises 400; otherwise sets status = submitted, submitted_at = datetime.now(timezone.utc), flushes, logs ("submit"), commits, refreshes.
Invariants
- A report's figures are always a fresh aggregation over posted journal entries for the exact requested period, never a read of a prior report or a running balance (
generate_report_effective/generate_report_sss, both re-query from scratch every call;test_buckets_and_payable,test_sss_tax_is_revenue_times_rate). - A
draftjournal entry and a posted entry outside[period_start, period_end]never contribute to a report, regardless of amount (test_excludes_draft_and_out_of_period, seeded with a 9999.00 draft line and a 7777.00 out-of-period line specifically to prove neither leaks in). - A workspace's report never aggregates another workspace's journal entries (
test_cross_tenant_revenue_not_aggregated,test_sss_ignores_non_revenue_and_other_tenant); a report cannot be submitted under a workspace that does not own it, and a failed cross-tenant submit leaves the victim report untouched, stilldraft,submitted_atstillNone(test_cross_tenant_submit_raises_404_and_no_mutation). net_to_gross/gross_to_netround-trip exactly at the cent for any input,net + vat_amount == grosseven where the unrounded division produces a repeating decimal (test_rounding,Decimal("99.99")at8.1%).gross_to_netrefuses a-100%rate rather than dividing by zero (test_minus_100_rate_raises_value_error); every other rate, including the four real VAT codes, computes normally.submit_reportis a one-shot state transition: a second submit on the same report always 400s, "already submitted" appears in the lowercased detail string, and the report is never silently re-submitted (test_double_submit_raises_400).- An empty period (no posted revenue at all) still returns a complete, all-zero report rather than raising or returning
None(test_empty_period_yields_zero_report).
Frontend
bookkeeping/ui/bookkeeping-financial-dashboard the workspace-facing report screen these two
generate methods and submit_report feed; at
62ba4fc the reference frontend (accountingApi.ts,
Reports.tsx) wires only the effective method with
a hardcoded filing_method: 'effective' literal
(Reports.tsx 42), the sss_rate input, the
filing-method switch, the submit action and UID
validation are not reachable from that screen at
this commit even though every one of their
endpoints exists and is tested server-side (Gotchas)
Minimum admin/debug surface: no dedicated route exists beyond the five listed under API surface; GET /vat/rates and GET /vat/validate-uid are themselves read-only enough to double as an ad hoc check from a browser or curl with a session cookie. SELECT id, period_start, period_end, filing_method, status, vat_payable FROM accounting_vat_reports WHERE workspace_id = :ws ORDER BY period_start DESC is the query a human runs to see what has been generated and submitted for a workspace.
Security & tenancy
Auth dependency: ActiveUser (api/dependencies.py 338 to 349, Depends(get_current_active_user), itself layered on get_current_user; a user whose is_active is false gets 403 even with an otherwise valid session) on every route in this file; the router is mounted with surfaces=LABS_SURFACES (app_fastapi.py 918, core/surfaces.py 177), the ordinary authenticated-session gate, not surface_exempt, there is no public/webhook path into this recipe unlike the HMAC-seam recipes in billing-credits. Tenancy: WorkspaceID (api/dependencies.py 779, Depends(_get_workspace_id)) resolves and access-checks the workspace from the JWT session state, the X-Workspace-ID header, or a single-workspace fallback, before any route body runs (680 to 698); every query inside generate_report_effective/generate_report_sss additionally filters by that workspace_id in its own WHERE (never trusts a caller-supplied id inside the body, VatReportCreate carries no workspace_id field at all); submit_report repeats the same filter as part of its lookup (213 to 220), which is what turns a cross-tenant submit attempt into a 404 rather than a 403 or a successful mutation, the row is invisible, not merely forbidden.
Fail direction: closed on tenancy (a report outside the resolved workspace simply does not match any query, effective 404/empty-aggregation); open on aggregation errors, there is no closed-by-default catch analogous to the billing-credits recipe's outer wrapper, an unexpected exception in either generate method is a 500, not a safe fallback value. Secrets and PII at rest: none, this recipe stores no card data, no external credential, and no partner PII beyond the UID string itself, which validate_uid never persists. Rate limits: none dedicated; these are ordinary session-authenticated CRUD-shaped routes with no external cost per call. Attacker-controlled inputs (all behind ActiveUser + workspace access): net_amount/vat_code on /vat/calculate (an unknown vat_code is refused with 400, not silently defaulted); period_start/period_end/filing_method/sss_rate on /vat/reports (sss_rate is schema-bounded ge=0, le=100; period_start/period_end are not checked against each other, a period_end before period_start is not rejected by the schema or the service, it simply aggregates an empty or backwards range, see Gotchas); uid on /vat/validate-uid (any string, the function degrades to False rather than raising on malformed input).
Tests
Run 2026-09-12 against the running postgres-test container already bound to localhost:5434 (docker port showed 5432/tcp -> 0.0.0.0:5434), from alchemy_labs/backend with its own .venv:
.venv/bin/python -m pytest tests/accounting/test_vat.py -q --no-cov -o addopts="" -p no:cacheprovider
prints 14 passed (0 failed). This file is pure/hermetic: it imports only vat_service/VAT_RATES and touches no database, no fixture from conftest.py is used by any test in it.
.venv/bin/python -m pytest "tests/v2/coverage/test_voila_g6_vat_mwst_service_report_generation_effective_sss_submit_uid_service_level_db_calc_already_covered_.py" -q --no-cov -o addopts="" -p no:cacheprovider
prints 14 passed (0 failed). This file is DB-backed: it seeds real Workspace/WorkspaceUser/AccountingFiscalYear/AccountingAccount/JournalEntry/JournalEntryLine rows through the savepoint-isolated db_session/test_user fixtures (tests/conftest.py, S9's reference impl) and asserts against the real SQLAlchemy aggregation, not a mock.
Known-answer vectors: net_to_gross(100.00, 8.1) == {vat_amount: 8.10, gross_amount: 108.10}; net_to_gross(200.00, 3.8) == {vat_amount: 7.60, gross_amount: 207.60}; the effective-method fixture (seeded_effective) asserts output_vat == 101.60 (81.00 + 13.00 + 7.60), input_vat == 50.00 (40.00 on account 1170 + 10.00 on 1171), vat_payable == 51.60; the SSS fixture asserts 1234.55 revenue at 6.5% rounds to sss_tax == 80.25 (80.24575 half-up). Both files are hermetic against any external service; the coverage file is live against a real transactional Postgres session, never mocked at the ORM layer.
What a rebuild adds and this commit lacks: no test exercises /vat/calculate, /vat/rates, or /vat/validate-uid through the actual FastAPI route (an HTTP-level test with TestClient), both test files call VatService/module-level functions directly, the router's own branching (filing_method dispatch, the Unknown VAT code 400, the sss_rate default-to-6.5) is therefore unverified by any test at this commit; no test asserts period_end < period_start behavior; no test asserts the ix_vat_reports_ws_period unique index's effect on calling generate_report_effective twice for the same workspace and period (Gotchas).
Deploy & ops
Config keys: none beyond the standard DATABASE_URL/SYNC_DATABASE_URL every accounting table needs, no dedicated flag or credential gates this recipe. Migration: 20260214_add_accounting_tables.py, revision 20260214_accounting_tables, down-revision 20260206_webhook_events (a single migration among many others in the same file, not a dedicated one for VAT), creates accounting_vat_reports and its unique index (342 to 370) alongside the rest of the Abakus/accounting schema in that same revision; it sits inside the single alembic head chain (stone_repos_002 -> prima_device_codes_001 (head); the accounting revision chains forward through 20260215_accounting_google_auth), alembic upgrade head from empty applies it cleanly (verified 2026-09-12 against the running test database, no separate scheduler job or Argus-style check exists for this recipe, both generate routes are synchronous request/response with no background component.
Gotchas
- 2026-09-12 (measured,
frontend/src/services/accountingApi.ts346 vsaccounting_reports_router.py57):getVatRatescalls${BASE}/vat-rates(a hyphen), the real route isGET /vat/rates(a slash); as written this frontend call resolves to/api/accounting/vat-rates, which does not exist as a route in this router and would 404. It is also never called from anywhere infrontend/src/pages/accounting/(grep -rn "getVatRates" frontend/src/matches only its own definition), so the mismatch has no observed runtime effect at this commit, but the function is broken if anyone wires it up as written. - 2026-09-12 (measured,
frontend/src/pages/accounting/Reports.tsx39 to 43): the VAT tab's only generate call isaccountingApi.generateVatReport({ period_start, period_end, filing_method: 'effective' }), a hardcoded literal. Nothing in the reference frontend lets a user picksss, supply ansss_rate, callsubmitVatReport, or callvalidateUid(grep -rn "submitVatReport\|validateUid" frontend/src/finds each only inaccountingApi.ts's own definition). The SSS method and submit flow are fully implemented and tested on the backend but have no UI path to reach them at this commit. - 2026-09-12 (measured from source, not exercised live in this session):
ix_vat_reports_ws_periodis a unique index on(workspace_id, period_start, period_end), but neithergenerate_report_effectivenorgenerate_report_ssschecks for an existing report before inserting a new one (db.add(report)unconditionally, 160 and 205). Calling either generate method twice for the same workspace and period should raise anIntegrityErroron the second insert, uncaught, surfacing as a 500 rather than a friendly "already generated" response; the two test files never exercise this path, so this is read from the migration and the service source, not confirmed against a live error. - 2026-09-12 (measured,
vat_service.py110 to 123): a revenue line whosevat_codeis"E00","X00",None, or anything other than"N81"/"R26"/"L38"still adds its amount tototal_revenuebut to no taxable bucket and to nooutput_vat. This is very likely intentional (exempt/excluded revenue has no output VAT by definition) but it meanstotal_revenueandtaxable_standard + taxable_reduced + taxable_lodgingcan legitimately differ, and nothing in the report response surfaces that gap as its own labeled figure. - 2026-09-12 (measured,
accounting_schemas.py524 to 528 vsaccounting_reports_router.py79 to 80):VatReportCreate.sss_rateisOptional[Decimal] = Field(None, ge=0, le=100), but the 6.5% default lives in the router (data.sss_rate or Decimal("6.5")), not in the schema and not ingenerate_report_sss, which takessss_rateas a required positional argument with no default of its own. Anyone calling the service directly (as both test files do) must supply a rate; only the HTTP route defaults it.
Reference implementation
Paths relative to the repo root; all at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.
alchemy_labs/backend/services/accounting/vat_service.py(239 lines, whole file in scope):VAT_RATES(25 to 31);net_to_gross/gross_to_net(37 to 59);validate_uid(61 to 74);generate_report_effective(76 to 165);generate_report_sss(167 to 210);submit_report(212 to 232);get_rates(234 to 236).alchemy_labs/backend/api/routers/accounting_reports_router.py(135 lines): the VAT block (55 to 94); the router mount,app_fastapi.py918.alchemy_labs/backend/models/accounting_models.py:VatFilingMethod/VatReportStatus(63 to 74);AccountingAccount(103 to 128);JournalEntry(158 to 183);JournalEntryLine(195 to 220);AccountingVatReport(479 to 509).alchemy_labs/backend/schemas/accounting_schemas.py:VatReportCreate(524 to 528);VatReportResponse(531 to 552).alchemy_labs/backend/migrations/versions/20260214_add_accounting_tables.py:accounting_vat_reportstable andix_vat_reports_ws_period(342 to 370).alchemy_labs/backend/services/accounting/audit_service.py:log_action(19 to 30), called on every generate and submit.alchemy_labs/backend/api/dependencies.py:get_current_active_user(338 to 349);_get_workspace_id/WorkspaceID(680 to 779).- Tests:
alchemy_labs/backend/tests/accounting/test_vat.py(84 lines, whole file in scope);alchemy_labs/backend/tests/v2/coverage/test_voila_g6_vat_mwst_service_report_generation_effective_sss_submit_uid_service_level_db_calc_already_covered_.py(451 lines, whole file in scope), andtests/conftest.pyfor thedb_session/test_userfixtures the coverage file depends on. - Reference frontend (not this recipe's subject, cited only for the Gotchas above):
alchemy_labs/frontend/src/services/accountingApi.ts(346, 373 to 375);alchemy_labs/frontend/src/pages/accounting/Reports.tsx(11, 39 to 43).
Deviations from an ideal implementation, all dated 2026-09-12: no HTTP-level test exists for any of the five routes, both test files call the service directly; report_data is declared but never populated; the unique period index has no application-level "already generated" guard in front of it; the frontend never offers the SSS method, submit, or UID validation even though the backend fully supports all three; getVatRates's frontend call targets a path the router does not expose.
Acceptance checks
Run from alchemy_labs/backend at 62ba4fc with the postgres-test container already listening on localhost:5434 (verified running in this session, docker port deap-postgres-test -> 5432/tcp -> 0.0.0.0:5434).
.venv/bin/python -m pytest tests/accounting/test_vat.py -q --no-cov -o addopts="" -p no:cacheproviderprints14 passedwith no failures (measured 2026-09-12)..venv/bin/python -m pytest "tests/v2/coverage/test_voila_g6_vat_mwst_service_report_generation_effective_sss_submit_uid_service_level_db_calc_already_covered_.py" -q --no-cov -o addopts="" -p no:cacheproviderprints14 passedwith no failures (measured 2026-09-12).grep -c " def test_" tests/accounting/test_vat.py "tests/v2/coverage/test_voila_g6_vat_mwst_service_report_generation_effective_sss_submit_uid_service_level_db_calc_already_covered_.py"prints14for each file, matching the two pytest runs above one-for-one (no parametrization in either file)..venv/bin/python -m alembic history | grep 20260214_accounting_tablesshows20260206_webhook_events -> 20260214_accounting_tables, Add accounting tables for Abakus module, and.venv/bin/python -m alembic headsprints exactly one head (prima_device_codes_001), confirming the VAT table's migration is on the live chain, not an orphaned branch.grep -n "class VatFilingMethod" -A 3 models/accounting_models.pyshows exactly two members,effectiveandsss;grep -n "class VatReportStatus" -A 4 models/accounting_models.pyshows exactly three,draft,submitted,accepted, confirmingsubmit_report's reachable states match the enum (no code path invat_service.pyever setsaccepted).git merge-base --is-ancestor 62ba4fc41009b454e1c033e5036c7e8577db2061 origin/main; echo $?prints0, andgit log --oneline -1 origin/mainnames this same commit, confirmingmaturity: prod(this measurement's commit is origin/main's own tip, not a feature branch).
Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.