StabisoDocs

Accounting and finance

The ledger and its controls

The guarantees in the first table are enforced inside PostgreSQL, with triggers, constraints, row-level security and grants. A bug in the application, or an agent that misbehaves, cannot break them. The later sections describe rules that the posting engine and service layer enforce. Most rules have a test in tests/erp; the control matrix says which, and which do not yet.

Double entry

Invariant Mechanism
Every posted entry balances in functional currency, with at least two lines A trigger on journal_entry checks the lines when the entry becomes posted
Each line is either a debit or a credit, never negative CHECK constraints
Postings go only to open periods A trigger compares the entry date with period.status
A date has exactly one regular period A trigger refuses regular periods that overlap; an adjustment period is a single day (CHECK)
Posted entries and their lines are immutable A trigger blocks UPDATE and DELETE once posted. Corrections of manual entries are reversals (POST /v1/journal-entries/{id}/reverse, a person, with a reason, through the approval policy); subledger entries are corrected through their document, payment or period run
Posted and void invoices and bills are immutable, and lines change only on drafts Triggers on trade_document and trade_document_line. Corrections are credit notes; a voided draft stays as it was
Numbers are gap-free per entity and fiscal year A number_sequence row locked in the posting transaction (2026-000042); uniqueness is a database constraint
Tampering is evident A SHA-256 hash chain per entity: each posted entry hashes its content plus the previous hash. The application computes it and the database requires it; GET /v1/entities/{e}/chain-verification (and the close checklist) recompute it. The hash is versioned (hash_version): entries posted since migration 0017 are hashed over every column of the entry and its lines (version 2), older ones keep verifying with the content-only version 1, and a version never goes back (ADR-0002)
The audit trail is append-only erp_app has no UPDATE or DELETE on audit_event, and a trigger blocks them for every role
Only active customers and vendors are booked and paid A trigger refuses to post a document whose party is a draft, and to release a payment (pain.001 item) to a draft party or one whose new bank details wait for approval. See customer and vendor controls
Released payment files are kept as sent, and a payment is sent once payment_export (the pain.001 or pain.008 file and its SHA-256) and its items are append-only in the same way; payment_export_item.payment_id is unique
A final VAT return locks its period's VAT postings A trigger refuses to post an entry with tax-tagged lines or lines on VAT accounts dated in the period of a final return (its own settlement excepted) until the return is amended; a final return and its box lines are immutable
Tenants are isolated Row-level security with FORCE on every table, keyed on app.org_id
The runtime cannot change the schema Migrations run as erp_owner; the API runs as erp_app with DML only

Explainable numbers

Each entry stores its provenance (JSON):

  • the source document (invoice, bill, payment, bank line, asset or run);
  • the actor who created it and the actor who posted it;
  • the data-vNext dataset version;
  • for tax lines, the rate ids, treatment, rule trace and legal reference;
  • for FX lines, the ECB fixing date and rate, the official VAT rate when the entity books documents at it, or manual when a rate was supplied;
  • for tax lines of foreign-currency documents in Swiss and German entities, the amount at the official VAT exchange rate (tax.tax_fx), or the series and tax point while it is not published yet.

Trade documents keep the tax determination of each line. The VAT report is built from these tagged lines and reconciled against movements on the VAT accounts; any unexplained_difference is shown. VAT returns (CH ESTV, DE UStVA and ZM, FR CA3, ES Modelo 303, 390 and 349, IT LIPE and Dichiarazione IVA, GB VAT100, SE momsdeklaration, FI kausiveroilmoitus, DK momsangivelse, NO MVA-melding) are built the same way with the official box mappings and the official VAT exchange rates, keep the ledger lines behind every box, and settle the VAT through a normal posting; see the API guide.

Nordic VAT returns

Sweden, Finland, Denmark and Norway file their VAT returns from the tagged ledger lines like the others (data/tax/returns/se.yaml, fi.yaml, dk.yaml, no.yaml, ADR-0100). A VAT-registered importer in each of them reports the import VAT on the return, so an import bill books it as VAT due and as input VAT. Each return is recorded as submitted and paid here and filed by hand on the authority's e-service. Filing and payment are due on the same day, moved off a weekend (public holidays are not known, so a date may be shown a day early, never late).

  • Sweden: momsdeklaration (SKV 4700), per quarter (the default), month or year, in whole kronor. Taxable sales in ruta 05 with their VAT by rate in rutor 10 to 12 (25, 12, 6 %); goods to other EU countries in 35, exports in 36, services to EU businesses in 39, other services abroad in 40; purchases where the company is liable as buyer in 20 to 24 with their VAT in 30 to 32; imports in ruta 50 with their VAT in 60 to 62; input VAT in ruta 48 and the result in ruta 49, paid to or refunded from the tax account. Due on the 12th of the second month after a quarter (17 August for the second), the 26th of the month after a month, the 26th of the second month after a year. Not derived: self-supply (06), the margin scheme (07), voluntary liability on letting (08), triangular trade (37); the periodisk sammanställning is not prepared.
  • Finland: kausiveroilmoitus, per month (the default), quarter or year. The VAT on domestic sales per rate (25.5, 13.5 and 10 %) and the VAT owed as the customer: on goods and services from other EU countries, on building work and scrap metal under the domestic reverse charge, and on imports. Services bought from outside the EU are declared with the domestic sales at their rate. All deductible VAT is in one field, and the result is paid to or refunded from the verotili. Beside the VAT it lists the zero-rated turnover, sales and purchases with other EU countries, imports and the reverse-charged building work. Due on the 12th of the second month after the period, a year by 28 February. The margin schemes and restricted deductions are adjustments.
  • Denmark: momsangivelse, per quarter (the default for a new business), half year or month, in whole kroner: salgsmoms, moms af varekøb i udlandet (goods from other EU countries and imports), moms af ydelseskøb i udlandet (services bought abroad under the reverse charge), købsmoms and the momstilsvar, with rubrik A (purchases from other EU countries), rubrik B (sales to other EU countries) and rubrik C (exports, services to customers outside the EU, zero-rated sales and domestic reverse-charge sales). The energy duties a business deducts are entered by hand. Due on the 1st of the third month after a quarter or half year, the 25th of the month after a month (17 August for June). EU-salg uden moms is not prepared.
  • Norway: MVA-melding, per term of two months (2026-T1, January and February, to 2026-T6), the default, or per year for a business allowed the årstermin. Each mva-kode with its basis and VAT: 3, 31 and 33 for sales at 25, 15 and 12 %, 5 for zero-rated sales, 52 for exports and services to customers abroad (the EU included: Norway is not in its VAT area), 1, 11 and 13 for the VAT deducted, 81 and 83 for imports and 86 and 88 for services bought from abroad, each with its deduction. Codes without deduction and the raw fish rate are adjustments. Due one month and ten days after the term (31 August for May and June), a year by 10 March. Norway has no One-Stop Shop and no EC sales list.

The box mappings follow the forms' published guidance, which could not be opened where they were written: the sources carry no verified_at, and a local accountant is still to review them.

Spanish VAT returns

A Spanish company files the Modelo 303 per quarter (per month for the large companies, VAT groups and REDEME), the Modelo 349 of intra-Community operations and the annual Modelo 390, all computed from the tagged ledger lines with the casilla numbers of the forms (data/tax/returns/es.yaml, each rule citing the Ley 37/1992 del IVA). They are recorded as submitted and paid like the other returns and filed by hand on the AEAT's sede electrónica.

  • Sales at 21, 10 and 4 % fill casillas 07, 04 and 01; the cuota (09, 06, 03) is the base times the rate. Credit notes go to casillas 14 and 15 (IVA due) and 40 and 41 (IVA deducted), as the AEAT asks for facturas rectificativas (art. 80), not netted in the rate rows.
  • Intra-Community acquisitions of goods and services received from other Member States are self- assessed in 10 and 11 and deducted in 36 and 37; other reverse-charged purchases (services from outside the EU, the domestic inversión del sujeto pasivo of art. 84.Uno.2.º) in 12 and 13 and 28 and 29. Capital goods (bienes de inversión, art. 108: property, plant and equipment of at least 3,005.06 euros a unit) go to 30/31 and 38/39; journal lines carry capital_goods and correction for this.
  • Import IVA is paid at customs on the DUA, so casillas 32 to 35 are entered as adjustments with the account it is booked to.
  • A negative result before the year's last period is carried forward (a compensar, art. 99.Cinco): the settlement debits Hacienda Pública, deudora por IVA (470000, role vat_carry_forward) and the return is done once submitted. The next 303 fills casilla 110 with what the last final 303 left (its 87 plus its negative 71) and applies it in 78 up to casilla 66, crediting 470000; the last period applies all of it, and a negative result there is refunded (art. 115): the settlement debits Hacienda Pública, deudora por devolución de impuestos (470900, role vat_refund_receivable), and the refund received is linked to the return as its payment. Entering 110 or 78 by hand replaces the amounts carried. Without an account of role vat_carry_forward (a custom chart) a negative result is an error, never booked to the settlement account; when the amount carried differs from that account's balance (an earlier return amended, a manual posting) the return warns. The IFRS chart has both roles too (1420 and 1430), for a Spanish company that keeps it.
  • The Modelo 390 is a return of its own kind, annual_summary: it settles nothing. Beside the ledger it takes from the year's final 303s their import IVA (52 to 55 from 32 to 35), the regularisations (63, 522 and 658 from 43, 44 and 76), their results (95, 97 and 98) and the previous year's compensation applied (85), and warns when its casillas 64 and 65 differ from the sums of their 45 and 46. Blocks whose per-rate casillas are not filled show their totals. Monthly filers keep their books on the SII and file no 390, so it is not scheduled for them.
  • The Modelo 349 lists one row per operator and clave, in euros and cents. A correction is a declaración sustitutiva: it lists every operator again (the German ZM's correction lists the changed rows alone). A quarterly 349 whose supplies of goods and services (claves E, M, H and S) pass 50,000 euros carries the warning monthly_statement: from that month it is filed monthly (RIVA art. 81).
  • The printouts are in Spanish: casilla, base imponible and cuota, amounts as 1.000,00, dates as dd/mm/aaaa.
  • The Canary Islands, Ceuta and Melilla are outside the Spanish IVA: goods shipped there are exports (casilla 60), services to a business there are not subject (120), goods bought from there are imports and services bought from there are reverse charged.

Due dates: the 303 of a quarter by the 20th of the next month, of a month by the 30th, the last period by 30 January; the 349 by the 20th; the 390 by 30 January. A weekend moves a date to the Monday; national holidays are not known. Paid by direct debit (domiciliación), a return is due by the 15th, or 25 January, instead.

Subledgers and control accounts

Receivables, payables, bank, fixed-asset cost and accumulated depreciation are control accounts. The posting engine lets only their subledger (documents, payments, assets, bank lines) post to them. Control lines carry subledger_type and subledger_id. As a result, the aging reports equal the control accounts, and the close checklist verifies that they do.

The documents of a customer or vendor that is another entity of the organization post to a pair of control accounts of their own, ic_ar_control and ic_ap_control (receivables from and payables to group companies, shown on their own balance sheet lines), chosen by trade.control_role; the aging and the control checks take the group account together with the ordinary one, and the partner of a line is read through the view ic_journal_line. Loans, accrued interest and group dividends have group accounts of their own too (ic_loan_*, ic_other_*), whose lines name the other entity as the partner. See intercompany.

Payments in transit

An outgoing payment is booked to the bank account only when the bank executes it (ADR-0060). Until then its entry credits the control account payments_in_transit (Swiss KMU 1091 "Zahlungen unterwegs (Geldtransfer)", HGB 2490 "Geldtransit", 1090 "Payments in transit" in the other templates), beside the bank in the cash group:

Step Entry
Payment created (proposal run or Record payment), approved payables (2000) debit, payments in transit (1091) credit
Bank line matched (camt.054 or camt.053, by the matcher or a person) payments in transit debit at what the payment booked there, bank (1020) credit; a conversion by the bank: the bank at the line's amount, the difference as realised FX (6949 / 6999)
Match undone the line's entry reversed: back in transit, the bill stays paid
Payment reversed while in transit the payment's entry reversed: payables credit, payments in transit debit
Rejected by the bank (pain.002 status report) the payment's entry reversed, as above, with the bank's reason
Returned by the bank (a credit line after the execution) the payment's entry reversed by the line, but the bank (1020) debited by what came back instead of payments in transit; a conversion by the bank: the difference as realised FX

A payment recorded from a bank line, or recorded as already executed (executed: true, "The bank already executed it": a company that pays in e-banking, by card or in cash and makes no payment files), goes to the bank account at once and into no payment file (payment_recorded_paid). Receipts always go to the bank account. The statement balance check therefore compares the bank with what the bank executed, and the close check payments_in_transit warns of payments still waiting for their bank line more than 10 days after their date. The warning that a payment takes the bank account below zero is given when the payment is made, on the bank's balance less what is still in transit from it (overdrawn_in_transit), and not again when the bank executes it. A payment in transit in a foreign currency is remeasured by the period-end FX revaluation at the closing rate, per currency, like the bank balance it will leave; the revaluation reverses on the first day of the next period, so the bank line still moves it to the bank at the payment's own rate.

A payment the bank rejected or returned is reversed by the bank's own message (ADR-0062), and the bills it paid are open again: Bank statements imports the bank's status report on a payment file (pain.002, Format "Payment status report"), and the matcher takes a credit line the bank marks as a return, or that carries the payment's number as its end-to-end id and comes from the payee, as that payment's return; a person links any other return from the line's candidates ("Payments that came back"). The reversal follows the approval policy and the four-eyes rules; the bank's message stands for the confirmation that it did not pay. Vendor Payments says why ("Rejected by the bank on …: the account is closed"). Importing the same report or statement twice changes nothing.

Chart templates

An entity's accounts are copied from a template in src/erp/seed/charts when it is created. POST /v1/entities uses the request's chart, ifrs_generic if it names none. Provisioning a customer (POST /v1/platform/organizations, the operator portal's New customer wizard) instead starts each entity on the standard chart of its country: the template whose countries name it (ch_kmu for CH and LI, de_hgb for DE, fr_pcg for FR, it_civil_code for IT, es_pgc for ES, se_bas for SE, fi_kpl for FI, dk_standard for DK, no_ns4102 for NO, uk_frs102 for GB, us_gaap for US, ca_standard for CA), else ifrs_generic (setup.standard_chart; GET /v1/charts?country= and GET /v1/platform/charts?country= mark it standard, and the wizard preselects it). A customer whose administrator sets it up at the first sign-in (ADR-0041) starts on the chart of the accounting standard they chose (GET /v1/setup/options). After that the chart is the customer's: accounts are added (POST /v1/entities/{entity}/accounts or the accounts import), renamed, moved to another statement line, given or relieved of a role, and deactivated (PATCH /v1/entities/{entity}/accounts/{code}); an account's number, type and currency are fixed.

Account names in the reader's language. An account has one name, the one the company books under (name in GET /v1/entities/{entity}/accounts; what archive exports, documents and filings print). The API answers with it as stored; showing it in the reader's language is the UI's job, like every other text of the app: the web UI shows a standard account name (one of the shipped chart templates, or one the app makes itself such as the US sales tax account of a state) in the language its user picked, from its own translation files (ui/src/i18n/terms.json), and a standard name followed by a code in capitals keeps the code ("Bankguthaben EUR" reads "Bank EUR"). Any other name, one the company made up or changed, shows as it is in every language. A test holds every template account name to all five UI languages.

The German template is our own, not DATEV's SKR03 or SKR04 (a company that keeps one of those with its Steuerberater imports it, B-65). Its accounts follow the order of the HGB balance sheet (§ 266) and income statement (§ 275, Gesamtkostenverfahren), and the first digit names the part: 1 Anlagevermögen, 2 Umlaufvermögen with the aktive Rechnungsabgrenzung and aktive latente Steuern, 3 Eigenkapital, 4 Rückstellungen, Verbindlichkeiten, passive Abgrenzung and latente Steuern (4900 clears the opening balances, 4910 is the suspense account), 5 Umsatzerlöse and sonstige betriebliche Erträge, 6 Material, Personal, Abschreibungen and sonstige betriebliche Aufwendungen, 7 Finanzergebnis and Steuern. Exchange differences and disposal results are sonstige betriebliche Erträge and Aufwendungen (§ 275), not the financial result. HGB writes off fixed assets directly; the fixed-asset subledger keeps cost and cumulative depreciation apart (1300 and 1309), and the balance sheet nets them. A German accountant is still to review it (B-79). The French, Italian and Spanish templates follow their national charts. The French one is the Plan comptable général (règlement ANC 2014-03 as amended by 2022-06, in force for fiscal years from 2025): the official account numbers padded to six digits (411000 Clients, 445710 TVA collectée, 512000 Banques), cash discounts in the financial result (665 and 765), and, under 2022-06, gains and losses on disposals in the current result (757 and 657), no longer exceptional. The Spanish one is the PGC PYMES (Real Decreto 1515/2007, as amended by RD 1/2021), numbered the same way (430000 Clientes, 477000 HP IVA repercutido, 475000 HP acreedora por IVA, 572000 Bancos), with cash discounts as 606 and 706 and disposals (671, 771) in the operating result. In both, accumulated depreciation is in class 28 (218300 and 281830, 217000 and 281700). Italy has no official numbered chart: the Italian template is our own, like the German one, in the order of the Civil Code statements (art. 2424 and 2425 c.c. as amended by D.Lgs. 139/2015, which removed the extraordinary section), its first digit the part (1 immobilizzazioni, 2 attivo circolante, 3 patrimonio netto, 4 fondi, TFR and debiti, 5 valore della produzione, 6 costi della produzione, 7 proventi e oneri finanziari and imposte), cost and fondo ammortamento paired as 1300 and 1309. Names are in the chart's language with English, German, French and Italian translations. The three charts use the same statement lines as the others, so their statements group the accounts by those lines, not by the national form's own headings. VAT is booked at the country's rates (20, 22 and 21 percent standard) and reported per rate. France files the CA3 from the books (input VAT on fixed assets, class 2, goes to 445620 for line 19), Spain the Modelo 303, 390 and 349 (see Spanish VAT returns); there is no Italian VAT return form yet, and the setup wizard says so. A local accountant is still to review them.

The Swedish, Finnish, Danish and Norwegian templates (ADR-0100) start a company on its national chart, IFRS the other choice, with four-digit accounts named in the country's language. Sweden's se_bas is an abridged BAS-kontoplan for K2 and K3 (classes 1 tillgångar to 8 finansiella poster: 1510 receivables, 2440 payables, 1930 bank, 2610 output VAT, 2614 VAT as buyer, 2640 input VAT, 2650 Redovisningskonto för moms, 1630 skattekontot); BAS splits output VAT by rate, the ledger keeps one account and the rate on each line. Finland's fi_kpl is our own, as Finnish law prescribes no chart, in the order of the Kirjanpitoasetus statements (1 vastaavaa, 2 vastattavaa, 3 liikevaihto to 9 rahoitustuotot ja -kulut and tuloverot; 2930 myynnin alv, 1763 ostojen alv, 2939 alv-tilitys). Denmark's dk_standard follows the order of Erhvervsstyrelsen's standardkontoplan under bogføringsloven 2022 with numbers of our own, the income statement first (1 to 4) and then the balance sheet (5 aktiver, 6 passiver; 6800 salgsmoms, 6810 købsmoms, 6840 momsafregning). Norway's no_ns4102 follows NS 4102 (1 eiendeler, 2 egenkapital og gjeld, 3 salgs- og driftsinntekt to 8 finansposter og skatt; 2700 utgående, 2710 inngående, 2740 oppgjørskonto merverdiavgift) and has no One-Stop Shop account, Norway being outside the EU. In all four each class of fixed assets keeps its accumulated depreciation on the partner ending in 9 (1220 and 1229 in BAS); the balance sheet nets them. Leases are expensed (leases.terms.LOCAL_CHARTS). Local accountants are still to review them.

tests/unit/test_chart_templates.py holds every template to the same rules: an account's type matches its statement line (assets on bs.assets, expenses on pl.opex, ...), every system role (setup.REQUIRED_ROLES, VAT settlement, deferred tax) is on exactly one account of the type the role needs, the control accounts are marked, and a contra account (accumulated depreciation, Wertberichtigung) is an asset on the line and next to the cost it corrects.

The Swiss template follows the Schweizer Kontenrahmen KMU (Sterchi, Mattle, Helbling; veb.ch and Verlag SKV, Ausgabe 1/2013; the official Schulversion is published by SwissAccounting on kmu.admin.ch). The classes are 1 and 2 (20 to 26 liabilities, 28 and 29 equity) the balance sheet, 3 revenue, 4 and 5 expense, 6 other operating expense, depreciation (6800) and the financial result (6900 Finanzaufwand, 6950 Finanzertrag, sub-accounts up to 6999), 7 Betrieblicher Nebenerfolg (Nebenbetrieb and Liegenschaften), 8 betriebsfremder, ausserordentlicher, einmaliger oder periodenfremder Aufwand und Ertrag (8000 and 8100, 8500 and 8510) and 8900 Direkte Steuern. The template's accounts for the system roles, as of 2026-10-02:

Role Swiss template Basis
bank_fees 6940 Bankspesen sub-account of 694 Übriger Finanzaufwand: W&W Branchenstandard (with H. Mattle), treuland
fx_loss, fx_gain 6949 Währungsverluste, 6999 Währungsgewinne the same two sources
fx_unrealized 6948 Unrealisierte Währungsdifferenzen extension in group 694 (net, either side)
asset_disposal_loss, asset_disposal_gain 8504, 8514 Ausserordentlicher Verlust, Gewinn aus Veräusserung von Anlagevermögen sub-accounts of 8500 and 8510: treuland
rounding 6799 Rundungsdifferenzen KMU has none; group 679 Sonstiger betrieblicher Aufwand
opening_balance_clearing, suspense 2298, 2299 group 22 Übrige kurzfristige Verbindlichkeiten: 29 is equity in KMU
ic_ar_control, ic_ap_control 1110, 2010 Forderungen und Verbindlichkeiten aus L+L gegenüber Konzerngesellschaften (generic: 1210, 2010) our proposal in groups 11 and 20 for intercompany, not in the public publications (B-66)
ic_other_receivable, ic_other_payable 1150, 2160 Übrige kurzfristige Forderungen und Verbindlichkeiten gegenüber Konzerngesellschaften (generic: 1220, 2020) accrued interest and declared dividends between group companies; our proposal in groups 11 and 21
ic_loan_receivable, ic_loan_payable 1445 Darlehen an, 2455 Darlehen von Konzerngesellschaften (generic: 1810, 2610) sub-accounts of 1440 and 2450 Darlehen in groups 14 and 24
ic_interest_income, ic_interest_expense, ic_dividend_income 6952 Zinsertrag, 6902 Zinsaufwand, 6951 Beteiligungsertrag von Konzerngesellschaften (generic: 7010, 7110, 7020) extensions in groups 690 and 695
withholding_tax_payable, withholding_tax_receivable 2206, 1176 Verrechnungssteuer (generic: 2230, 1410) the KMU chart's own accounts in groups 22 and 117: the tax a dividend deducts and the recipient's claim (year-end close)
vendor_withholding_payable 2207 Einbehaltene Quellensteuer auf Lieferantenrechnungen (generic: 2231) our extension in group 22 beside 2206: the tax kept back from vendor bills (withholding tax)
fixed_asset_cost, accumulated_depreciation 1500 Maschinen und Apparate, 1509 Wertberichtigungen default pair; 1510/1519 Mobiliar, 1520/1529 Büromaschinen, Informatik, Kommunikationstechnologie, 1530/1539 Fahrzeuge

6900 Finanzaufwand and 6950 Finanzertrag are plain accounts (interest). The full book is sold by Verlag SKV and is not public: the veb.ch and SwissAccounting publications list the groups and main accounts, the sub-accounts above come from charts built on it. Earlier versions of the template had bank charges at 6840 (in KMU 68 is depreciation), a Finanzertrag at 6900, fx gains and losses at 6940 and 6950, unrealised differences at 6960, rounding at 6990 and one account typed revenue at 7900 for gains and losses on disposal (class 7 is Betrieblicher Nebenerfolg, and a loss was a debit on a revenue account), and the data-import clearing and suspense accounts at 2998 and 2999, in the equity range. Entities created from the old template keep their numbers: accounts with postings are never renumbered, and no migration changes a chart. Their services keep working through the roles; an entity that has only the old role asset_disposal books gains and losses on it, and that role counts as both new ones, so POST /v1/entities/{e}/role-accounts and the opening-balance import add nothing for it. 1480 (Aktive latente Ertragssteuern) is in the KMU range of Beteiligungen, and 2650 and 8910 are not listed by the sources used: B-66.

An asset class has its own cost and accumulated-depreciation accounts. The roles fixed_asset_cost and accumulated_depreciation name only the default pair. An asset of another class names both accounts when it is registered (cost_account and accumulated_account: 1520 and 1529 for a laptop, booked on its purchase bill to the same cost account); naming one without the other, the same account twice or an account that is not an asset is refused. Depreciation, disposal and the deferred-tax run read the accounts from the asset. A table that maps an asset class to its accounts (and its depreciation expense account) is B-64.

A gain and a loss on a disposal have their own accounts (roles asset_disposal_gain and asset_disposal_loss): ausserordentlicher Ertrag and Aufwand in the Swiss chart, other income and other expense (7510 and 7520) in the generic one, where IFRS has no extraordinary items.

The chart of accounts belongs to the customer: the templates are starting points, and a company can instead import its own (a German company typically the SKR03 or SKR04 chart its Steuerberater keeps) with the accounts import and assign the system roles. The German demo company uses the generic (IFRS) chart in English.

Opening balances and open items

A customer's existing books enter through the data import (ADR-0016). The accounting is built so that nothing is counted twice:

Step Posting
Opening balances (opening_balances), one entry per entity dated as_of, source opening Dr/Cr each account of the old trial balance, except the receivables and payables control accounts (their balances are the open items). The difference of the posted rows goes to the account with the role opening_balance_clearing (2298 in the Swiss KMU chart, 2980 in the generic one)
Open receivable (open_receivables), one posted document and one entry each invoice: Dr receivables control (linked to the document) / Cr clearing; credit note: the mirror image
Open payable (open_payables) bill: Dr clearing / Cr payables control (linked to the document); credit note: the mirror image

With a whole trial balance the clearing account carries receivables - payables after the opening entry, the open items clear it, and it is zero when the trial balance and the open items agree. A remainder is a difference between the two lists of the old books and a finding for a person (the import answer shows the balance). The control accounts equal their subledgers from the first day (the close checks ar_control_reconciled and ap_control_reconciled pass), the trial balance of the new books equals the old one, and the close checklist's opening_clearing check wants the clearing account at zero. VAT is untouched: open items carry no tax lines, and the VAT report and the return reconciliation exclude opening entries as they do settlements (a VAT account's opening balance is no movement of a return period), so the old period's VAT return stays with the old system. A Swiss entity gets a warning with the import of open receivables and payables: the import assumes the old books accounted for VAT on the agreed fees (vereinbarte Entgelte); on collected fees (vereinnahmte Entgelte) the VAT on those receivables falls due, and the input tax on the payables becomes deductible, when they are paid (Art. 40 para. 2 MWSTG), and this system will not report it.

A document keeps its number (a customer's invoice) or its vendor number (a bill, which also gets a BILL- number of this system), the document and due date of the original and the open gross amount as a single line; it is posted, immutable, settled by payments and credit notes like any other. The entry date is the opening date (the period must be open), so the original date stays on the document only. Foreign-currency items carry the rate of the file (or the ECB rate of the opening date). The default approval policy sends an opening entry to a second person (rule opening_balances_need_a_second_person; four eyes between people); a one-person organization allows post for the source opening in its own policy before it imports, and the import's answer says so. Open items post at once. Documents created by an import carry provenance.opening (file, line, opening date): they are not documents this system issued, so output (printing, sending, e-invoicing) must refuse them.

Vendors imported by the first administrator are drafts: their bills are imported only after a second person has activated them (customer and vendor controls). Fixed assets are not imported: the cost and depreciation accounts of the trial balance post their balances, but the asset register is loaded separately (gap analysis, P2).

A bank account below zero

A payment (POST /v1/payments), a payment run and an intercompany loan's payout, repayment or interest settlement that takes a bank account below zero in the books is made, with a warning: warnings in English and warning_reasons by code (bankBalance.warning.overdrawn, with the account, the entity, the balance before, the balance after, the currency and the day), which the screens word in the person's language. The money may be in the bank and not booked yet (an overdraft facility, a receipt not recorded), so it is never refused. The balance is bank_accounts.book_balance: the account's posted entries up to the day of the payment, in the currency the account holds (a EUR bill paid from the CHF account counts with its CHF amount), without the new entries; they count whether they are posted or wait for approval. A run warns once per account for all its payments. Before the run, the proposal gives each currency's paying account and its balance (bank_balances), and the screen warns as the bills are ticked; the loan's payout dialog warns before the payout where the lender's account holds the loan's currency. A receipt never warns. A manual journal entry cannot post to a bank account (bank accounts are control accounts), so it has no such warning.

Cash discounts

A cash discount (Skonto) taken on a payment is a reduction of the consideration (CH MWSTG Art. 41, DE UStG §17), so it is booked as a proportional correction of the document's own posting. With the ratio discount / gross total, every revenue or expense line and every VAT line of the invoice's or bill's entry is scaled and booked on the opposite side in the payment's entry, keeping its tax tag:

Document Payment entry (2% on 1,081.00 = 1,000.00 + 81.00 VAT)
Purchase bill Dr payables 1,081.00 / Cr bank 1,059.38 / Cr cash discounts received 20.00 / Cr input VAT 1.62
Sales invoice Dr bank 1,059.38 / Dr cash discounts granted 20.00 / Dr output VAT 1.62 / Cr receivables 1,081.00
  • Revenue and expense lines go to the accounts with the roles cash_discount_granted and cash_discount_received (in both chart templates: 3801 / 4900 in the Swiss SME chart, 4900 / 5900 in the IFRS chart; other charts add them with POST /v1/entities/{e}/accounts).
  • Reverse-charge bills correct both sides of the self-assessed VAT; zero-rated and exempt lines reduce their base only. Rounding goes to the largest revenue or expense line, so the lines add up to the discount exactly.
  • The tagged lines feed the VAT report and the VAT returns. The Swiss return keeps Ziffer 200 as declared and shows the discount granted as a deduction in Ziffer 235 (Entgeltsminderungen wie Skonti, Rabatte, Verluste); the turnover per rate (303 to 343, or 323 under the net tax rate method, gross) falls by it, so the tax is corrected through the rate boxes. The German UStVA nets the base in Kz 81/86 (§17 UStG), Spain reports it in its correction boxes (LIVA art. 80; casillas 14/15), France and Italy net the base of the rate (a bad-debt write-off, unlike a discount, goes to line 21 of the French CA3: see bad debts). The input tax boxes fall by the corrected VAT. The rules are the line fact reduction (discount or write_off) and the rule output deduct in data/tax/returns/ch.yaml. The official VAT exchange rate of the document applies to foreign-currency discounts; on the cash basis, where the law converts what is paid at the rate of the payment (DE, ES), the discount and a bad-debt write-off are converted at the rate of their own day, as the part they settle is, so an unpaid invoice written off leaves nothing on the return. Money received after a write-off takes its correction back as a correction that makes the VAT due again (not a reduction): see bad debts.
  • After a credit note, the discount corrects what is still supplied: the invoice's lines net of its credit notes' lines, scaled by discount / (total less what was credited). Under the net tax rate method the correction stays in the activity of the account it corrects (MWSTG Art. 37).
  • The allocation records amount (paid) and discount; the document's open amount falls by both, and its carrying amount in functional currency is relieved completely.
  • A discount is proportional to what a payment settles (at most the terms' percentage of amount
    • discount) and to what is left of the document's discount; the document's row is locked while a payment is booked. The correction is dated on the payment, so it falls in the payment's VAT period; in a period locked by a final VAT return it is refused like any VAT posting.
  • Each correction line takes the rate of the document line it corrects (ECB, or the official VAT rate where the entity books at it), so corrected VAT is booked at the rate it was booked with.

Withholding tax on vendor bills

Some laws make the payer keep tax back from what it pays a vendor and pay it to the tax authority itself: the Italian ritenuta d'acconto on a professional's fee, the Spanish IRPF retención, the German tax of Paragraph 50a EStG on a royalty paid abroad, the US NRA and backup withholding. A vendor carries a withholding code of the company country's table (GET /v1/entities/{entity}/withholding-tax/codes; the rate comes with it) or custom with a rate of its own (a treaty rate, a Swiss cantonal source tax on an artist's fee). Switzerland has none by default.

Code Rate What
DE-50A 15.825 % Paragraph 50a EStG: 15 % plus the 5.5 % solidarity surcharge on it; Bundeszentralamt für Steuern
IT-RA-20, IT-NR-30 20 %, 30 % ritenuta d'acconto on a professional's fee (art. 25 DPR 600/1973), on a non-resident's; F24 code 1040
ES-IRPF-15, ES-IRPF-7 15 %, 7 % IRPF on a professional's fee, 7 % in the first three years (Modelo 111)
ES-IRNR-24, ES-IRNR-19 24 %, 19 % non-resident income tax, 19 % for EU and EEA residents (Modelo 216)
FR-182B-25 25 % art. 182 B CGI on services used in France paid to a non-resident (form 2494)
GB-ROY-20 20 % income tax on royalties paid to a non-resident (CT61)
US-NRA-30, US-BACKUP-24 30 %, 24 % IRC 1441/1442 (Form 1042-S), IRC 3406 (Form 945)
CA-XIII-25 25 % Part XIII tax on payments to non-residents (NR4)

The bill keeps it back (SAP's classic withholding at the invoice). When a bill of such a vendor is drafted, its net amount before VAT times the rate, rounded to the currency, is its withholding_amount; its total is net plus VAT less it, what the vendor is paid. It posts:

Italian professional, EUR 1,000.00 + 22 % IVA, ritenuta 20 % Debit Credit
6110 Consulenze e compensi professionali 1,000.00
2150 IVA a credito 220.00
4300 Debiti verso fornitori 1,020.00
4431 Erario c/ritenute su compensi da versare (vendor_withholding_payable) 200.00
  • Payments, payment proposals and the pain.001 file pay the open amount, 1,020.00; the tax stays on the withholding account until the company pays the authority.
  • A credit note of such a bill gives the tax back at the bill's code and rate on the net amount it credits (Dr the withholding account); the credit note that credits the rest of the bill takes the rest of its tax, so the two end at zero to the cent. A credit note of no bill takes the vendor's setting.
  • A cash discount reduces the consideration, so it reduces the base and the tax with it, in proportion: on the bill above with 2 % within 10 days, paying 999.60 and taking 20.40 books Dr payables 1,020.00, Cr bank 999.60, Cr cash discount received 20.00, Cr VAT 4.40 and Dr the withholding account 4.00: the ritenuta ends at 196.00, 20 % of 980.00, and the company files and certifies 196.00.
  • In a foreign currency every line, the tax included, is converted at the rate the bill books at. The authority is owed the company's own currency at the rate of the day the vendor is paid, so each payment moves the part of the tax it releases from the bill's rate to its own on the withholding account, against realised FX gain or loss; a payment's amount on account applied to the bill later does so at the payment's rate.
  • The vendor's code and rate are key details: once the vendor is past draft or a document of it is posted, a change waits for a second person (change_party_details). A bill keeps the code and rate it was drafted with. A bill cannot switch the tax off: change the vendor (with a second person) instead.
  • Paying the authority: the withholding account is not a control account, so a bank line is booked to it (or a journal entry debits it). The report says how much.

The report (GET /v1/entities/{entity}/withholding-tax?start=&end=, Reports → Withholding tax, CSV with format=csv) lists every posted bill and vendor credit note that kept tax back: vendor, bill and the vendor's number, net amount, rate, the tax in the document currency and as booked in the functional currency, the code and whom it is paid to, with totals per code and currency. basis=bill_date (default) counts the documents dated in the period, and the cash discounts taken in it (a row cash_discount of the bill, dated the payment day, with the base and tax it took off), and its functional total is what they booked on the account at the bills' rates; basis=payment_date counts the part of each bill's tax that payments made in the period released (its tax times what was paid over what it left to pay, the last payment all that is left, net of what a cash discount took off), in the functional currency at each payment day's rate, as Italy, Germany, Spain and the US are owed the tax for the month or quarter the vendor is paid; its base and tax are the bill's less the cash discounts up to the period's end, the vendor certificate's figures.

Auto-reversing entries

An entry with auto_reverse_on (an accrual, the FX revaluation) is reversed on that date when it is posted, directly or on approval, in the same transaction: the reversal swaps debits and credits, is posted without a second approval and links both ways (reverses_id, reversed_by_id). It runs once, even if posting hooks run again. The date must be after the entry date and in an open period (a year-end accrual needs the next fiscal year opened); a reversal period closed while the entry waits for approval refuses the approval (409) and leaves it pending. Entries posted before manual entries were auto-reversed (October 2026) are not reversed retroactively; a person reverses them with POST /v1/journal-entries/{id}/reverse, "on" and a "reason".

Reversing a manual entry

POST /v1/journal-entries/{id}/reverse is for manual entries and for people: agents are refused (403, policy action reverse_entry). It needs a reason (kept on the reversal and in the audit trail) and an open period. Whoever created, posted or approved the entry reverses it alone; if another person did, or the approval policy limits what one person posts (the policy is asked for reverse_entry and for post of the same source and amount), or the entry is above the company's posting approval limit, the answer is 202 with an approval request of type journal_reversal. A second person approves it (never the requester); the entry, the period and the policy are checked again at that moment, and the reversal is posted only then. The opening entry of a data import is reversed too, always after a second person approves (the default rule opening_reversal_needs_a_second_person, and the service insists whatever a policy says); it is the correction path of an import: reverse it, then import the balances again. The reversal leaves the open items and the opening balance clearing account in place, so the close check opening_clearing fails until a new import (or corrections of the open items) brings the clearing account back to zero. A request nobody decides blocks the close of the entry's period (the approvals check) until it is approved or rejected. Entries of a subledger or process (ar, ap, fa, bank, fx, close, appropriation, vat) are refused with 409 that names where the correction belongs, so that the documents, payments and assets and the ledger cannot drift apart. Internal reversals (the auto-reversal) call the posting engine directly after their own policy decision; a test pins the list of callers.

Credit note defaults

A new credit note copies only the quantities remaining after posted credits and credits waiting for approval. Fully credited lines are omitted, discounts follow the remaining quantity, and a vendor credit takes the remaining printed VAT. The form shows the amount still available; once none remains the invoice's Credit note action is disabled. This allowance is independent of payments: a paid invoice can still be credited. A prior price correction or an unlinked credit can make the remaining quantities ambiguous; enter those credit lines manually instead of copying the original amount. The final posting checks the allowance again.

Foreign currency

  • Documents are converted at the ECB fixing for the document date. The document stores its functional total. A credit note of a document is booked at the rate that document is carried at (its functional total over its total; the last credit note takes what is left), so it relieves exactly what the books hold for the part it credits and no exchange difference is left on the control account with nothing open (IAS 21.23, 21.28).
  • VAT on foreign-currency documents of Swiss and German entities is converted at the official rate instead (CH: ESTV monthly average or daily selling rate, MWSTV Art. 45; DE: BMF monthly average, UStG §16 Abs. 6), stored on the document and its lines next to the booking rate and used by the VAT returns. The ledger keeps the ECB translation unless the entity elects to book at the official rate (tax_fx_booking in the VAT settings); the difference is a computational difference of the return, cleared with the settlement. See Foreign currency in the API guide.
  • Payments settle documents at their historical rates. The difference is posted as realised FX gain or loss.
  • Revaluation at period end remeasures open foreign-currency items at the closing rate (IAS 21.23): every posted invoice and bill in a foreign currency, receivable or payable, each foreign-currency asset account (bank accounts), what is in transit on the payments-in-transit account in each foreign currency, and each foreign-currency liability account (loans, accruals, deposits; the receivables and payables control accounts are left to the documents, and the intercompany loan and balance accounts to the intercompany provider, so none is remeasured twice). Each family of items is a registered position provider (modules/fx_positions/providers, one file each) that the exposure view reads too, so a capability with balances of its own adds one without editing the run, and the view shows what the run will book. The lines carry no transaction-currency amount, only a functional amount. The entry reverses automatically on the first day of the next period.

Fiscal calendars

Each fiscal year is opened with a calendar (POST /v1/entities/{e}/fiscal-years with calendar, or fiscal_calendar when the entity is created). Fiscal year N is the year whose year-end month falls in calendar year N; the year-end month is the month before the entity's fiscal_year_start_month.

calendar Periods
monthly (default) Twelve calendar months. Years opened before calendars existed are monthly
4-4-5, 4-5-4, 5-4-4 A 52/53-week year: four 13-week quarters, each split into periods of 4, 4 and 5 weeks (or 4-5-4, 5-4-4)
13x4 Thirteen periods of four weeks

A week-based year ends on the same weekday every year (end_weekday, default Saturday):

  • year_end: last_weekday (default): the last such weekday of the year-end month, e.g. the last Saturday of January;
  • year_end: nearest_weekday: the such weekday nearest the last day of the year-end month, up to three days into the next month (the US retail "Saturday nearest 31 January").

A year starts the day after the previous one ends, so it has 364 days, or 371 when the weekday has drifted a full week (every five or six years). The 53rd week goes to the last period (period 12 has 6 weeks on 4-4-5, or period 13 has 5 weeks on 13x4). Reports note when a period and its comparative have different lengths. Changing calendar is possible from one year to the next: the new year starts the day after the old one ended, and its first (monthly) or last (week-based) period absorbs the difference. Without calendar, a new year keeps the calendar of the latest year opened with one. A year's calendar cannot be changed once it is open.

Adjustment periods. adjustment_periods (0 to 2) adds one-day periods on the last day of the year: periods 13 and 14 on a twelve-period calendar, 14 and 15 on 13x4. They take audit and closing adjustments after the last regular period is closed. A posting dated the last day goes to the regular period unless it names the adjustment period ("adjustment_period": 13 on POST /v1/journal-entries). Each adjustment period is closed and reopened like any other; the close checklist runs on it without depreciation, FX revaluation and bank checks (those belong to the last regular period), and it counts as later than every regular period of its year.

Reports by period. reports/period-trial-balance and reports/period-profit-and-loss take fiscal_year and period and select entries by the period they were posted in, not by date. On the last day of the year the last regular period gives the trial balance before adjustments, an adjustment period after them. The income statement has the period, year to date, and the same period of the previous fiscal year (same number and kind) as comparatives. Date-based reports (trial-balance?as_of=, profit-and-loss?start=&end=) still work and include every period on those dates.

Depreciation is spread over a fiscal year's regular periods: evenly per month on a monthly calendar (as before), by days on a week-based one; adjustment periods get none. Only the months inside the useful life share in it: from the month in service (half of it under mid_month) to the end of the life, so an asset in service from July with a five-year life is charged January to June of its last year and nothing after (IAS 16.55). A disposal stops the charge after its period without moving the rest of the year's amount into the months before it. A run books what the year's schedule reaches by the end of its period less what is already posted to the asset in that fiscal year, never less than zero: a month booked under another spread (before this rule, or by hand) is caught up in the next run, and the year books its scheduled amount once its last period has run. Where a later period has already run (a run rejected and made again), the schedule counts to that period.

Runs go in order. A period's run is refused (409 depreciation_earlier_period, naming the month) while an earlier open period of the fiscal year still has depreciation to book: its run not made, or an asset that still owes it. Each month's depreciation is booked in that month, rather than October booking September with it and September then nothing. A closed earlier period does not hold the run up: the next open run books what it lacks.

An asset registered late is depreciated in its own months. Once a period has run, an asset registered afterwards with an in-service date in that period or earlier owes its share: the close check Depreciation posted fails for the period ("run depreciation again"), the next period's run is refused until it is done, and running the period again makes a top-up that books only what is missing (the year's schedule through the period less what is posted), in an entry DEP-YYYY-NN-T1 (-T2 …) dated at the period end and recorded under the same run (summary.top_ups). With nothing missing, running a period again answers 409. Because the check fails, the close job makes the top-up on its day like any run that is due.

Dates and values of an asset. A residual value at or above the cost is refused (422 salvage_not_below_cost), so is an in-service date before the acquisition date (in_service_before_acquisition). The account an acquisition posted with the asset is credited to follows the rule for the accounts of documents (entry_checks.document_account): not a control account (receivables, payables, bank, the asset accounts), not a tax account, neither revenue nor expense. A disposal is dated on or after the in-service date (disposal_before_in_service) and not after today where the company is (disposal_in_future).

Reversing an acquisition. An asset registered or posted by mistake, with no depreciation and no disposal, is taken off the register with Reverse acquisition on its card (POST /v1/assets/{asset}/reverse-acquisition, a reason required). Its acquisition entry is reversed like a journal entry: dated on its own date unless another is given, through the approval policy, and with four eyes when someone else created, posted or approved it. Once the reversal is posted the asset is reversed: the register shows it with 0, and nothing more is booked to it. An asset whose cost came with a purchase bill has no entry of its own to reverse: it leaves the register at once, and the bill is corrected with a credit note. The journal still refuses to reverse an asset entry directly and points to the card.

A disposal books the asset's depreciation up to the end of its period (what that period's run would have booked) in the disposal entry itself, then derecognises the cost and all of the accumulated depreciation, so the gain or loss is against the carrying amount on the disposal date and nothing stays on the subledger. A disposal dated before periods of its fiscal year that already ran takes back what they booked after its date in the same entry (IAS 16.55); one dated before depreciation booked in a later fiscal year is refused (409): reject that entry first, or date the disposal in the later year. Runs skip an asset whose disposal waits for approval; if the disposal is rejected, the next run books what the asset missed. A disposal is refused (409) while a depreciation entry of the asset or an earlier disposal of it waits for approval (decide that entry first), or while its acquisition entry is not posted. Runs and the year's schedule check leave out an asset whose acquisition entry waits for approval or was rejected: its cost is not on the books. A run and a disposal of the same asset at the same time wait for each other, so the depreciation is booked once: the run locks the entity's active assets until its transaction ends (for the close review agent with run_adjustments, until the review ends), and a disposal of one of them waits that long.

Once the year's last regular period has run, the close check Depreciation matches the year's schedule (depreciation_schedule) compares every active asset's year with its schedule. The last period's run is due while any asset booked more than its schedule, even with no share of its own in that period. It is not a run to do itself (a shortfall also fails Depreciation posted, which the close job runs), but the year-end close is not proposed while it fails. It fails when:

  • an asset is short of its schedule (registered after the run, or a disposal rejected after it), or booked more than it (by hand, or a schedule changed after its runs): run depreciation for the last period again. That top-up books only the difference (the excess as Dr accumulated depreciation / Cr depreciation expense), in an entry DEP-YYYY-NN-T1 (-T2 …) recorded under the same run (summary.top_ups). Two top-ups at once book it once: the second answers 409. A run again with nothing to book answers 409. When the period is closed, reopen it first;
  • an asset's disposal waits for approval and the year reads differently from the schedule: the check names it until the approver decides.

Each year's schedule is booked in the currency's minor unit, rounded so that the years add up: what the schedule reaches by the end of the year, rounded, less what it reached at the start. A schedule at two decimals (333.33, 333.33 and 333.34 for 1,000 over three years) books 333, 334 and 333 yen in JPY, so the life books the full 1,000. In a currency with two or more decimals each year books the schedule's own amount.

Depreciation stops at cost less salvage, rounded down to the currency's minor unit (a cost of 1,000.5 yen depreciates 1,000). An asset that holds exactly that is never short of a year's schedule, since no run can book more: a cost or salvage finer than the currency's minor unit (1,000.5 yen, or a salvage of 0.005 BHD under a two-decimal schedule).

The schedule is at two decimals whatever the currency, so a BHD or KWD cost with a third decimal (1,000.001) never schedules its last fils.

Run deferred tax after a top-up, since it reads the ledger's accumulated depreciation.

Book and tax depreciation

A new asset takes your own asset tag as its code, or, left out, the company's next asset number (FA-00001, FA-00002, ..., counted without gaps; src/erp/domain/master_numbers.py).

An asset is depreciated in the accounting book (BOOK: its own schedule under the entity's framework, IFRS or the Swiss CO; the only book that posts to the ledger) and in any number of tax books of its entity (POST /v1/entities/{e}/depreciation-books, e.g. TAX-CH for ESTV Merkblatt A 1995 declining-balance rates, TAX-DE for the AfA tables, TAX-US for MACRS; without a code, one is made from the book's name, and the screen names a new book after the tax rules chosen, Tax book Germany (AfA)). A tax book's schedule for an asset comes from the data-vNext depreciation engine for a data-vNext asset class (ch.estv.it_hardware_software, us.macrs.gds.5y …) and is stored with its dataset version. A reducing balance (ESTV) has no end of its own, so its schedule ends with the asset's accounting schedule: in that last year the asset is scrapped and what is left of its tax value is written off. A pool (UK capital allowances) keeps it, and so does a CH class with land (only the building is depreciated). The end of the accounting life is a projection of the disposal, not the disposal itself: an asset kept in use after its book value reaches zero is still on hand, and the schedule has then deducted the rest a little early. Where that matters (a long-lived asset kept well beyond its life), give the tax book periods instead. Tax books post nothing.

Deferred tax (IAS 12). One tax book per entity (a unique index) can carry deferred tax with a tax_rate_percent. At a period end, per asset:

  • carrying amount = cost - accumulated depreciation in the ledger;
  • tax base = cost - the tax schedule's depreciation to that period (earlier years in full, the current year spread over its periods like book depreciation);
  • temporary difference = carrying amount - tax base; deferred tax = difference x rate.

The book's total is shown net (IAS 12.74, one taxation authority): positive is a deferred tax liability, negative a deferred tax asset. POST /v1/periods/{id}/deferred-tax posts the change since the last run as an ordinary entry through the posting engine and approval policy: Cr deferred tax liability / Dr deferred tax asset against Dr deferred tax expense (or Cr income when it falls). Lines on the liability and asset accounts carry subledger_type=deferred_tax and the book's id, so the next run knows what is already posted (as of its own period end). Periods run in order: a run is refused while the latest earlier period with deferred tax due (at year end only, the previous year end) is open and not run. A change of tax rate (PATCH …/depreciation-books/{code}) is remeasured by the next run. The run refuses to go before the period's depreciation. Accounts come from the roles deferred_tax_liability, deferred_tax_asset and deferred_tax_expense (in both chart templates) or are named on the book.

Period close

GET /v1/periods/{id}/close-checklist reports each check as pass, fail, warn or n/a:

  • no pending approvals and no drafts;
  • depreciation run;
  • FX revaluation run (or nothing foreign to revalue);
  • deferred tax run, when a tax book carries deferred tax: every period (deferred_tax_cadence: period) or only the last regular period of the year (year_end, the default);
  • AR and AP subledgers equal their control accounts;
  • every bank statement line reconciled, and statement closing balances equal the ledger (the account's balance in the currency it holds: on an account in the functional currency the functional amounts, so a EUR payment from the CHF account counts with what it booked in CHF); the bank account holds only what the bank executed, outgoing payments waiting on payments_in_transit until their bank line is matched;
  • no outgoing payment waits more than 10 days for the bank to execute it (payments_in_transit, a warning naming the payments, the first day and the amounts);
  • trial balance and balance sheet balance;
  • hash chain intact;
  • earlier periods closed (a warning, not a failure); the periods of a year whose year-end close is posted count as closed, since the year lock holds them;
  • on the last period of a fiscal year, the year-end closing entry posted (year_close, a warning);
  • a closed year whose general meeting is overdue is appropriated (appropriation_pending, a warning). Both are described in year-end close;
  • the organization's own close tasks of the period are done (close_tasks, a warning naming the tasks not done, never a failure: see close tracker).

The screen's summary says "No check fails: the period can be closed" only when nothing warns either; with a warning it says how many checks warn, and the period can still be closed.

POST /v1/periods/{id}/close locks the period only when no check fails. It needs a human key with the approve scope. Reopening follows the same rule and needs a reason (POST /v1/periods/{id}/reopen with {"reason": "…"}), kept in the period.reopen audit event. A period of a fiscal year that a year-end close holds is reopened only by reopening the year close (409 otherwise): see year-end close.

Who approves: the AI reviewer or a second person

Settings → Approval policy → Who approves sets who decides a step that would wait for a second person (ADR-0101; PUT /v1/settings/controls, an administrator, recorded as controls.change):

  • The AI reviewer (the default), for a company run alone or with a small team. Fixed checks decide each step. It clears what they pass: a new customer or vendor (duplicates, own bank accounts, the VAT register), and the entry of a period-end run that Autopilot or the close job makes (depreciation, the FX revaluation, deferred tax), when it is the run's own entry, unchanged since and booked only once; the reviewer posts it as system:reviewer. Everything else that would wait (an agent's entry, an entry above the posting limit, a step it has no checks for yet) waits under Approvals, its review in the audit trail. Where the person who asked, or whose agent asked, is the only person who may approve, they release it themselves on the screen with a passkey touch in the last five minutes (review.release); nobody outside the company is asked. In a company with more people, another person decides.
  • A second person (four eyes), for a company whose auditor or law requires it (a Swiss ordinary audit, an SEC registrant): another person of the company decides, never the one who asked.

Switching to the AI reviewer decides at once what was waiting for a second person, and every deploy does the same for requests asked before the reviewer had checks for their step.

Approval policy

The policy is a data-vNext decision table (src/erp/seed/policies/default.yaml). A human admin can replace it with the organization's own table (PUT /v1/approval-policy; every change is a version and audited, and agents stay denied everything but posting; see API). The default, evaluated first match wins:

  1. Agents never approve, close or reopen periods, manage keys, release payment files (pain.001), or finalise or amend VAT returns.
  2. Agents never activate customers or vendors or change their bank or key details.
  3. Agents cannot change the chart of accounts.
  4. Manual journal entries created by agents always need approval.
  5. Agent postings above 5,000 (functional currency) need approval.
  6. Other agent postings (routine subledger documents) are allowed.
  7. System processes are allowed (demo data and migrations create active vendors).
  8. Segregation of duties: the person who created a payment does not release it (release_own_payments), and the person who prepared a VAT return does not finalise it (finalize_own_vat_return). An organization without a second person can allow either.
  9. A person's new vendor waits for a second person (activate_party, source vendor).
  10. A purchase order is approved by a second person (approve_purchase_order); agents never approve purchase orders or invoice variances (approve_match_variance) nor record receipts (record_receipt).
  11. Segregation of duties between people of the depth series (ADR-0023 to ADR-0029): the person who posted the origin of an intercompany document does not accept its mirror (accept_own_ic_document), and the person who prepared a consolidation does not finalise it (finalize_own_consolidation); both are refused by default.
  12. A second person approves the terms of an intercompany relationship (built, guide) or agreement, the group structure, a revenue contract and a recurring invoice (approve_ic_relationship, approve_ic_agreement, approve_group_structure, activate_revenue_contract, activate_recurring_invoice: require_approval), a change of the account that holds a control or required role (change_required_account_role), a new bank account of the company after its first or a new IBAN or QR-IBAN (change_company_bank_details, ADR-0043), raising or removing a company's posting approval limit (change_posting_limit), and the year-end entries: a posting from the source close (the closing entry and the catch-up) or appropriation waits for a second person (close_entries_need_a_second_person). Agents are denied all of these actions, and approve_consolidation_adjustment, finalize_consolidation, satisfy_obligation, release_dunning, close_fiscal_year, send_support_reply, close_support_case, record_quote_answer and erase_personal_data are human-only too.
  13. Humans with the required scope are allowed.

Changing the policy needs a second administrator when it relaxes it. The policy decides what needs a second person, so one administrator alone must not be able to switch that off (for example allow change_posting_limit, remove the limit, post CHF 5,405,000.00 alone and restore both; or do the same with bank details or account roles). A change, from Settings > Approval Policy or PUT / DELETE /v1/approval-policy, is compared with the policy in force over every actor type (human, agent, system), action, amount, source and actor name the two tables tell apart (policy.relaxations). If it relaxes any decision (deny to require_approval or allow, or require_approval to allow), it is an approval request (action change_approval_policy, audited as approval_policy.request) that a second person with the admin scope approves under Approvals; the person who asked never does, and agents never do. Until then the policy in force holds, and the request shows on the settings page and in pending_change with what it relaxes. One such change waits at a time (409 approval_policy_change_waiting) until it is decided or the person who asked withdraws it (below); approving one made on a version that has been replaced since is refused (409 approval_policy_changed): reject it and ask again. A return to the default table counts the same way: where the default is looser than the organization's table, it waits. A change that only tightens applies at once. An organization with fewer than two active people (signed-in users; keys and agents are not people) applies every change at once, since nobody could approve it, unless another user was disabled or lost the admin scope in the last 30 days (else an admin could do that first and relax the policy alone); the audit event approval_policy.update lists what it relaxed, and, after an approval, who asked and the request.

The person who asked withdraws her own request. A request to raise or remove a posting approval limit or to relax the policy that nobody else may decide (an administrator whose colleagues are controllers, accountants or viewers) would otherwise wait forever and block every later one. Four eyes is about approving: taking back one's own request relaxes nothing, so the person who asked withdraws it with Withdraw on the settings page (the posting approval limit on Settings > Company, the waiting change on Settings > Approval Policy) or POST /v1/approvals/{approval_id}/withdraw (admin scope). Nobody withdraws for someone else (403 approval_withdraw_own_only, also for someone without the admin scope; a second person rejects instead), the person who asked needs the admin scope still (403 approval_withdraw_admin_only), agents never do, and other requests are not withdrawn this way (409 approval_not_withdrawable). The request becomes rejected, decided by the person who asked, with the comment withdrawn, and is audited as approval.withdrawn; the limit or policy in force stays, and a new request may be asked. When she is alone in the organization, her next change of the same kind applies at once and withdraws her own older request the same way, audited with alone (a request someone else made stays for a second person to reject). A change of the posting approval limit that applies at once because it lowers the limit or the policy allows it withdraws her own older limit request too (without alone). The API shows a withdrawn request with withdrawn true, and Approvals with a Withdrawn badge under Rejected, not as a rejection by the person who asked: withdrawn is derived in one place (withdrawn_requests.shown) from the status, the comment and the person who decided (whichever key or session), so a second person who writes "withdrawn" as her reason still shows as a rejection.

A company one person runs

One's own request always needs a second person: nobody approves what they asked for, created or prepared, through any key, session or agent acting for them, and no setting changes that (ADR-0096). A screen never offers Approve on one's own request: it says "You asked for this or prepared it: another person decides". When nobody else in the organization may approve yet (no other active or invited user with the approve right), the refusal says so (four_eyes with nobody_else: true; GET /v1/me gives other_approvers), and the refusal, Approvals and Users offer Invite a person to approve, which opens the invitation with the Controller role, for the company's accountant or another person the administrator trusts.

Giving a person the right to approve (inviting a Controller or Administrator, or raising someone to one of those roles) is itself an audited act (ADR-0098). The administrator says who the person is: full name, relationship to the company (external accountant, board member, partner, employee or other) and their firm, and confirms that it is a different real person and that the declaration is recorded for the auditor (approver on POST /v1/users and PATCH /v1/users/{user_id}; without it approver_declaration_missing). A second account of the same person is refused (approver_same_person): the administrator's own mailbox with a +tag or dots, or the administrator's own name. The person named confirms once, at sign-in, that their approvals carry their name and are recorded (POST /v1/me/approver-acknowledgement); until then they decide nothing (approver_acknowledgement_needed). Settings → Approver register (GET /v1/approvers, for administrators and auditors) lists everyone who may approve, what was declared, by whom and when, when they confirmed, and every change of their access.

Posting approval limit

Each company has a posting approval limit in its functional currency. A new company starts at about CHF 100,000 in its currency, as a round figure: 100,000 CHF, EUR, USD, GBP or CAD; 1,000,000 SEK or NOK; 750,000 DKK; 2,500,000 CZK; 40,000,000 HUF; 450,000 PLN; 500,000 RON; 15,000,000 JPY (the table is DEFAULTS in erp.modules.posting_limit; a currency it does not name starts at 100,000, which is never more than about CHF 100,000). The companies that existed in October 2026 got the same figure for their currency. A test copy keeps each company's limit.

When a person submits an entry whose functional amount (its total debits) is above the limit, the entry does not post: it waits for a second person, whatever the approval policy would allow. That is everything a person posts through the ledger: manual journals; bills, invoices and credit notes; payments, receipts, refunds and applications; bank postings; intercompany postings; asset acquisitions and disposals; and the runs a person starts: the VAT settlement, the FX revaluation, depreciation, deferred tax, revenue recognition and the year-end close (where the policy already asks a second person, the request names both reasons). A document in another currency is compared after conversion at the rate it is booked at (a EUR 200,000.00 bill at 0.9438 is CHF 188,760.00). The approval request says why, for example Four eyes: CHF 5,405,000.00 is above the company's posting approval limit of CHF 100,000.00; a second person approves it. A second person with the approve scope approves it under Approvals, and it posts then; the person who asked never approves it. Reversing an entry above the limit waits for a second person too. An entry at the limit (100,000.00) posts directly.

Agents keep the approval policy's own limits (rule 5 below), and the system's processes (the scheduled close job, imports the system runs) their own rules: the limit holds people. A posting that an earlier approval already named (the credit notes of a subscription's early end) is not asked again.

The limit is on Settings > Company, where everyone sees it and a request to change it that waits. Only a person with the admin scope changes it (PATCH /v1/entities/{entity} with posting_approval_limit; 403 posting_limit_admin_only otherwise, agents always):

  • Lowering it (or setting one where there was none) applies at once.
  • Raising or removing it (null: no limit, meant for a one-person company; the screen asks first) is the policy action change_posting_limit: by default it waits for a second person, who approves it under Approvals; the person who asked never does, and agents are denied it. Until then the old limit holds, and one request per company waits at a time (409 posting_limit_change_waiting) until it is decided or the person who asked withdraws it (above). GET /v1/entities/{entity} shows it in posting_limit_request. If the limit was lowered meanwhile, approving is refused (409 posting_limit_changed): reject or withdraw it and ask again.
  • A person alone in the organization changes it at once, with the default policy: one active person (signed-in users; keys and agents are not people) and no other user disabled or stripped of the admin scope in the last 30 days (people.alone, the same test as for the policy). A one-person company removes its limit with No limit and nothing waits. An organization that wants it without a second person anyway allows change_posting_limit in its approval policy (with more than one active person, that change waits for a second administrator like any change that relaxes the policy).

Each change is audited as entity.posting_limit with the value before and after (and, after an approval, who asked and the request; alone when it applied without a second person because nobody else was there); each request as entity.posting_limit_request.

Approvals apply the four-eyes rule: nobody approves their own request, nor a journal entry they drafted. A key without the post scope can still submit, and its work always waits for approval.

Four eyes compare people, not credentials (erp.modules.people, ADR-0011). A user is a person; a built-in agent counts as the person it ran for; an API key counts as the person who created it (its api_key.create audit event). One person with two keys, or a key and a browser session, is one person. A key the system issued (an organization's first admin key, and every key created with it) belongs to no known person: it may submit and request, but never approves, releases payments or finalises VAT returns. The same rule decides approvals of journal entries, customers and vendors, payment release (who created the payment) and VAT finalisation (who created or last computed the return).

Customer and vendor controls

Numbers. A new customer or vendor gets the company's next number by itself: C-00001, C-00002, ... for customers and V-00001, ... for vendors, counted per company and kind (src/erp/domain/master_numbers.py). Nobody types one, on the screen, over the API or MCP, from a bill, an e-invoice, a converted lead or an import. A creation that fails gives its number back, so the numbers follow one another without gaps. Only an import (or an API call) that brings the number of the system before keeps it; the count skips a number someone wrote that way. A number never changes.

A customer or vendor is draft, active, blocked or inactive. The controls (src/erp/modules/party_controls.py, party_rules.py, ADR-0010, ADR-0046) keep an unreviewed or blocked party, or an IBAN nobody checked, away from the ledger and the bank.

The New customer and New vendor forms accept the street, building number, postal code and town at creation. Enter them there to avoid completing the address separately when issuing the first invoice. An address is optional at creation; the invoice output checks its required content. An activation cleared by the AI reviewer names system:reviewer as the activator; the person who requested it remains recorded separately.

What the database enforces (migration 0016): a trade document is never inserted or updated as posted while its party is not active; a payment-file item is never inserted for a payment to a party that is not active or whose bank details wait for approval; at most one pending request of each kind per party.

What the services enforce (the database does not yet, backlog B-31):

Control How
Drafts are not booked or paid Submitting or posting a draft party's document answers 409; its payments are not released
Who starts as a draft Every vendor a person creates (default policy: activate_party for vendors is require_approval; an organization can allow it), with an activation request; vendors created from an e-invoice; vendors, and parties with bank details, created by agents. E-invoice import and bill capture keep such a bill as a draft with a warning
Activation: four eyes A human with the approve scope who is not the person who created the party activates it (POST …/parties/{party}/activate). Anyone else, including the creator, only requests it, and a second person decides in Approvals or on the card; the approver can be neither the requester nor the creator
Activation: the AI reviewer (ADR-0101) In the ai_reviewer controls mode (the default) the AI reviewer decides an activation at once: when its checks (possible duplicates, a company's own bank account, the VAT register) pass it activates the party as system:reviewer; when one warns, the request waits with the reviewer's reasons for another person, or for the owner who runs the company alone to release it with a fresh passkey touch. Requests that were already waiting for a second person are decided the same way when an administrator switches to the AI reviewer, and by the migrate job of every deploy
Deactivation Back to draft (humans only, policy action activate_party) blocks the party's documents and payments again
Block (ADR-0046) A blocked party gets no new documents (credit notes included), purchase orders, subscriptions, recurring invoices, contracts or opportunities, its drafts are not submitted or posted, and nothing is paid to it: payment proposals leave its bills out (skipped), payments to it are refused when created and approved, and it is a payment blocker. What it pays still settles its open items. While a block request waits, payments to it are held
Inactive (ADR-0046) An inactive party is refused like a blocked one, incoming payments too, and hidden from pickers. Only a party with no open items, no documents or payments waiting for approval, no open purchase orders and no active subscriptions or recurring invoices becomes inactive (checked again on approval)
Status changes: four eyes Blocking, unblocking, setting inactive and making active again are requests (change_party_status, POST …/parties/{party}/status, with a reason) a second person decides under the change_party_details policy; activation and PATCH status cannot move a blocked or inactive party. Audit: vendor.status_change.request and the decision (customer.* for customers)
Bank details: four eyes A new IBAN or further bank account is a request (change_bank_details) a second person decides; while it waits, payments to the party are blocked
Key details: four eyes Once the party is past draft (active, blocked or inactive) or a document of it is posted or pending approval, a new name, VAT number, country, subdivision, currency, payment terms, cash discount, credit limit or a vendor's withholding tax is a request (change_party_details) a second person decides; the card shows the values now and after approval. A draft changes at once, since its activation needs a second person anyway. Other fields (address, contacts, language, e-invoicing) change at once
Credit limit See Customer credit limits below: an invoice above a customer's limit waits for a second person, a sales order above it is not confirmed
Currency A party's currency is the company's functional currency or one the European Central Bank publishes rates for (422 party_currency_without_rates otherwise, e.g. for XXX), on create and on change. Left out, it is its country's when the company has rates for it, else the company's (what the system fills in)
Our own bank accounts An IBAN or further bank account of a customer or vendor that is a bank account or QR-IBAN of any company of the organization is refused (422 party_own_bank_account: "CH93 0076 2011 6238 5295 7 is your own company's bank account, not the vendor's"), on create, on a bank-detail request and again when it is approved. Group parties are not checked: their accounts are the other entity's
Possible duplicates Another party of the same kind and company with the same VAT number (compared as a registration), an IBAN in common or the same name (case, punctuation and legal forms such as AG or GmbH ignored) is named on the card, in the answer that creates the party (possible_duplicates) and in its activation request, for the approver: "Possible duplicate of V-ALP Alpen Werkzeug AG: same VAT number and IBAN". It is a warning; the second person decides
Bills recorded twice A vendor invoice number is recorded once per supplier: case, spaces and punctuation are ignored, and vendors with the same VAT number or an IBAN in common count as one supplier (409 vendor_invoice_twice_supplier: "vendor invoice alp 1001 was recorded again (BILL-2026-000003 of V-ALP Alpen Werkzeug AG, the same supplier by VAT number or IBAN); void one of them", when the draft is created, submitted or reopened). A bill of the same vendor, total, currency and date as another bill posted or waiting for approval (another number, or none) does not post directly: its posting waits for a second person with the reason "possible duplicate of BILL-2026-000012: same vendor, amount 777.00 EUR and date 2026-09-24"
Pay only confirmed accounts A payment is released only to the party's IBAN or one of its further bank accounts, so a bill (for example an e-invoice from someone posing as a known vendor) cannot redirect it
People, not credentials Four eyes compare the person behind an actor: a user; a built-in agent counts as its caller; an API key as the person who created it (its api_key.create audit event). One person with two keys, or a key and a session, is one person. A key the system issued (an organization's first admin key) belongs to no known person: it may request but never activates or decides
Agents Never activate, deactivate, change bank or key details, or decide, whatever an organization's policy says
Audit trail vendor.create (the creator the checks use), vendor.update with the previous and new values of every changed field, approval.request, vendor.activate, vendor.deactivate, vendor.bank_change.request / .approve / .reject, vendor.details_change.*, approval.approved / .rejected (customer.* for customers)

Who created and who activated a party is read from the append-only audit trail, not from a column the runtime could change. Parties that existed before these controls keep their status; a bill of an existing vendor whose payment IBAN (for example a QR-IBAN) is not in the vendor's bank accounts is not released until the account is added and confirmed.

Residual risks: people are only as distinct as their sign-ins. An administrator can invite a second user at an address they control, and the holder of the first admin key can request a change that their own user session then approves. A customer created by a person (with an IBAN) is active at once.

Customer credit limits

A customer may have a credit limit in its own currency (credit_limit on the customer card and in the parties API; empty for none; src/erp/features/invoicing/credit_limits.py, migration 0114). Setting it on a new customer applies at once; changing or removing it once the customer is past draft or has a document posted or waiting for approval is a key detail a second person approves.

Credit exposure is what the customer would owe if everything under way went through, in its currency:

Part What counts
Owed now (receivables) Open posted invoices less open credit notes and payments kept on account: the balance its card shows
Invoices awaiting approval (pending_invoices) The full total of each invoice waiting for a second person
Open orders (open_orders) What is left to invoice on confirmed sales orders, VAT included. A draft invoice made from an order takes its part out of the order; any other draft invoice counts once it is submitted

An amount in another currency is converted at today's ECB reference rate, today being the day where the company is. Available credit is the limit less the exposure (negative above the limit).

The checks. When an invoice is submitted and the exposure plus the invoice's total is above the limit, the invoice does not post: it waits for a second person (the submit check credit_limit, through the ordinary approval of the posting), with the reason in plain words, for example "Credit limit: Stockholm Design AB would owe SEK 128,000.00, above its credit limit of SEK 100,000.00; a second person approves." The approver sees the limit and what would be owed in the request. Exactly at the limit still posts. When today's rate is missing for an amount in another currency, the invoice waits for a second person too, saying so. A sales order has no approval of its own, so confirming one (or accepting a quote into one) above the limit is refused (409 credit_limit_exceeded, naming what would be owed and the limit); the order stays a draft until payments come in or a second person raises the limit. Credit notes and receipts are never held; they lower the exposure.

Both checks lock the customer's row (SELECT ... FOR NO KEY UPDATE) before they add up the exposure, so two invoices of one customer submitted at the same moment are checked one after the other: the second sees the first and waits for a second person when together they go above the limit, instead of both passing on room that was there for one.

Where to see it. The customer card's Credit limit tab shows the limit, each part of the exposure, the total and the credit available, with "Over the limit" when it is. The Credit exposure screen (Sales, GET /v1/entities/{entity}/credit-exposure) lists every customer with a limit, those over it first, and downloads the same as CSV (format=csv; columns customer, name, currency, credit_limit, receivables, pending_invoices, open_orders, exposure, available, over_limit, note). The organization export carries the limit as party.credit_limit.

Purchase orders and matching

Purchase orders, receipts and matching (the purchasing module, features/purchasing/: orders, matching, variances, status, rules in rules; ADR-0013) post nothing. An order is a commitment, not a liability, and a receipt of goods or services is not booked either: the liability is the vendor bill, posted as before. Accruing goods received but not invoiced (GR/IR) at period end is a later step.

What they control is paying a bill: a bill whose order reference names a purchase order and that varies from it beyond the entity's tolerance is blocked for payment until a second person approves the variance. See API for the rules.

What the database enforces (migration 0018): an order's lines change only while it is a draft, and once submitted only its status, approval and cancel reason change; a cancelled order does not change; closing an approved order for the rest (migration 0062, ADR-0046) sets each line's closed quantity once and the order's close reason, after which it does not change, and receipts stay within the ordered quantity less what was closed; an order is approved only while its vendor is active and only with an approver recorded; receipts are append-only and keep each line's received quantity between zero and the ordered quantity, on approved orders only; purchase_match.payment_block is derived (a variance not approved), an approval is stored with its digest and amount, and no allocation (a payment, money on account or a credit note, migration 0107) and no payment-file item is accepted for a blocked bill; the tolerances stay within 10 %, 1,000 per line and 10,000 per bill. Bank reconciliation does not propose blocked bills, and matching a bank line to one is refused (409), the line staying unmatched.

What the services enforce:

Control How
Orders need a second person Submitting asks the policy (approve_purchase_order); by default it waits for a person who neither created nor submitted the order. Agents submit but never approve
Matching On submitting a bill and after each receipt; deterministic, 2-way against ordered and 3-way against received quantities, earlier bills counting first
Payment block Payment proposals leave blocked bills out; a payment, money on account or a credit note applied to one and the payment-file release refuse it with the same words, all through purchase_port.ensure_payable (bill_payment_blocked); a credit note of the bill posts but does not settle it (hooks.settlement_held): the credit stays open, with a warning, to apply once the variance is approved
Credit notes lower what an order invoiced A posted credit note of a bill matched to an order, on a line that names the bill line it credits, lowers that order line's invoiced amount, and its invoiced quantity when the line is at the bill line's unit price (goods returned, or the line credited as it was; a price reduction gives back no quantity, so it never makes room for more units than ordered); order card, matching of later bills, the order's status
Variance approval: four eyes, with a reason approve_match_variance; not a person who entered, changed, submitted or matched the bill or requested the approval; a key the system issued never decides; the reason is kept on the match and in the audit trail
An approval is bound to what was approved The digest of the bill and its order and the overcharge approved are stored; a changed draft or a reopened bill clears the approval (purchase_match.approval_cleared), a larger overcharge at the next match, and the bill is blocked until approved again
Pairing evidence Similarity and runner-up per line; a pairing by price or position with a different description needs approval
Closing the rest An approved order partly received or invoiced is closed for the rest with a reason (no second person, as cancelling: it only lowers the commitment). Each line keeps what was received or invoiced, the larger; the rest is closed and no longer expected, so 2-way matching counts the ordered quantity less what was closed and 3-way the received quantity, which receipts keep within it. Bills are matched again
Receipts People only (record_receipt); every unpaid bill of the order is matched again; a paid bill a receipt would block is flagged for review on its match (order card, order list filter) until the receipts no longer block it
Audit trail purchase_order.create / .update / .approve / .reject / .cancel / .close / .receive / .status, approval.* on the order; purchase_match.match, approval.request, approval.withdraw, purchase_match.variance_approved / _rejected on the bill
Stabiso, the Enterprise System of Record · generated from the code and its guides · release 20b7dd5b0c4d