Prima Get an API key

Module kit / Bookkeeping

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
  • alchemy_labs/backend/services/accounting/vat_service.py
  • alchemy_labs/backend/api/routers/accounting_reports_router.py
  • alchemy_labs/backend/models/accounting_models.py
  • alchemy_labs/backend/schemas/accounting_schemas.py
  • alchemy_labs/backend/migrations/versions/20260214_add_accounting_tables.py
  • alchemy_labs/backend/tests/accounting/test_vat.py
  • 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
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:

RouteAuthTenancyBody / paramsCodesNotes
GET /vat/rates (accounting_reports_router.py 57 to 59)ActiveUsernonenone200 {code: "rate"} for all five VAT_RATES codesstateless, calls vat_service.get_rates()
POST /vat/calculate (62 to 68)ActiveUsernonequery params net_amount: Decimal, vat_code: str200 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=VatReportResponseActiveUserWorkspaceIDVatReportCreate: 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=VatReportResponseActiveUserWorkspaceIDpath report_id: UUID200 the now-submitted report; 404 VAT report not found (wrong id OR wrong workspace, same message either way); 400 Report already submittedone-way: draft -> submitted, no route or method transitions submitted -> accepted at this commit
GET /vat/validate-uid (92 to 94)ActiveUsernonequery uid: str200 {"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:

  1. One grouped query sums credit_amount - debit_amount and vat_amount per vat_code, joined JournalEntryLine -> JournalEntry -> AccountingAccount, filtered to this workspace, status == posted, entry_date inside [period_start, period_end], account_type == revenue (83 to 102).
  2. In Python, each row buckets into taxable_standard/taxable_reduced/taxable_lodging and accumulates output_vat by matching vat_code against the literal strings "N81"/"R26"/"L38" (110 to 123); any other vat_code on a revenue line ("E00", "X00", None, or a code the ledger invented) still adds to total_revenue (113) but is silently excluded from every taxable bucket and from output_vat, there is no else branch and no warning.
  3. A second query sums debit_amount - credit_amount on lines whose account number is "1170" or "1171" (Vorsteuer Material / Investition), workspace- and period-scoped the same way, and calls that input_vat (126 to 140), it does not filter by vat_code or by account_type, only by the two hardcoded account numbers.
  4. vat_payable = output_vat - input_vat (142); one AccountingVatReport row is constructed with status=draft and 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 draft journal 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, still draft, submitted_at still None (test_cross_tenant_submit_raises_404_and_no_mutation).
  • net_to_gross/gross_to_net round-trip exactly at the cent for any input, net + vat_amount == gross even where the unrounded division produces a repeating decimal (test_rounding, Decimal("99.99") at 8.1%).
  • gross_to_net refuses 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_report is 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.ts 346 vs accounting_reports_router.py 57): getVatRates calls ${BASE}/vat-rates (a hyphen), the real route is GET /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 in frontend/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.tsx 39 to 43): the VAT tab's only generate call is accountingApi.generateVatReport({ period_start, period_end, filing_method: 'effective' }), a hardcoded literal. Nothing in the reference frontend lets a user pick sss, supply an sss_rate, call submitVatReport, or call validateUid (grep -rn "submitVatReport\|validateUid" frontend/src/ finds each only in accountingApi.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_period is a unique index on (workspace_id, period_start, period_end), but neither generate_report_effective nor generate_report_sss checks 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 an IntegrityError on 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.py 110 to 123): a revenue line whose vat_code is "E00", "X00", None, or anything other than "N81"/"R26"/"L38" still adds its amount to total_revenue but to no taxable bucket and to no output_vat. This is very likely intentional (exempt/excluded revenue has no output VAT by definition) but it means total_revenue and taxable_standard + taxable_reduced + taxable_lodging can legitimately differ, and nothing in the report response surfaces that gap as its own labeled figure.
  • 2026-09-12 (measured, accounting_schemas.py 524 to 528 vs accounting_reports_router.py 79 to 80): VatReportCreate.sss_rate is Optional[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 in generate_report_sss, which takes sss_rate as 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.py 918.
  • 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_reports table and ix_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), and tests/conftest.py for the db_session/test_user fixtures 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).

  1. .venv/bin/python -m pytest tests/accounting/test_vat.py -q --no-cov -o addopts="" -p no:cacheprovider prints 14 passed with no failures (measured 2026-09-12).
  2. .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 with no failures (measured 2026-09-12).
  3. 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" prints 14 for each file, matching the two pytest runs above one-for-one (no parametrization in either file).
  4. .venv/bin/python -m alembic history | grep 20260214_accounting_tables shows 20260206_webhook_events -> 20260214_accounting_tables, Add accounting tables for Abakus module, and .venv/bin/python -m alembic heads prints exactly one head (prima_device_codes_001), confirming the VAT table's migration is on the live chain, not an orphaned branch.
  5. grep -n "class VatFilingMethod" -A 3 models/accounting_models.py shows exactly two members, effective and sss; grep -n "class VatReportStatus" -A 4 models/accounting_models.py shows exactly three, draft, submitted, accepted, confirming submit_report's reachable states match the enum (no code path in vat_service.py ever sets accepted).
  6. git merge-base --is-ancestor 62ba4fc41009b454e1c033e5036c7e8577db2061 origin/main; echo $? prints 0, and git log --oneline -1 origin/main names this same commit, confirming maturity: 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.