dodil/erp-brick-tax
DODIL ERP brick: tax
src/brick_tax/application/process.yaml
# The brick's process — module.yaml's workflows (v4), copied (pattern.md §15). TAX.declare_process serves
# it as BPMN 2.0 at GET /process/bpmn; it is identical to `erp-compose bpmn --from module.yaml`.
# A transition's key is frozen once signed: it is the BPMN id. Signed 2026-10-22 (tax v4).
id: tax
name: Tax (VAT)
version: 6
actors:
- id: tax_admin
name: Whoever is responsible for VAT (the owner, the bookkeeper, the accountant)
does: records the VAT registration, keeps tax codes, rates, categories, treatments and the rules that join them
sees: everything
permissions:
- tax:code:read
- tax:code:write
- tax:code:approve
- tax:category:write
- tax:registration:write
- tax:party_tax:write
- tax:document_tax:approve_override
- tax:settings:write
- tax:vat_return:read
- tax:vat_return:prepare
- tax:vat_return:submit
- tax:vat_adjustment:write
- tax:bad_debt:claim
- tax:cis:write
- tax:cis_return:submit
- tax:trade_declaration:write
- tax:import_vat:write
- tax:partial_exemption:write
- id: owner
name: The owner or a finance controller
does: sets the controls on whoever keeps VAT (whether a rate, rule or registration change waits for a second person, who approves an override that lowers tax, the business time zone)
sees: everything
permissions:
- tax:code:read
- tax:code:approve
- tax:settings:controls
- tax:hmrc:connect
- tax:vat_return:read
- tax:vat_return:submit
- tax:cis_return:submit
note: 'v4: the owner also connects the business to HMRC (TAX-26) and may submit a VAT or CIS return; submitting is a legal declaration a person makes, never a service client'
- id: biller
name: Whoever raises orders and invoices
does: sees the tax worked out on a document, records a customer's VAT number, overrides a line's code with a reason
sees: tax codes, a document's tax
permissions:
- tax:code:read
- tax:party_tax:write
- tax:document_tax:override
- id: system
name: The system
does: loads the country's codes, determines and computes tax for a document in its direction (sale or purchase), suggests a code where no rule covers a line, checks its contents, freezes it at issue, runs the one-off Odoo migration; v4 - reads HMRC obligations, liabilities and payments, checks VAT numbers, answers whether a tax point is filed, computes a CIS deduction and records one suffered, records import entries, finds bad debts eligible for relief
sees: everything, through its own service account (one service client per calling brick, each with only its exact grants)
permissions:
- tax:code:read
- tax:code:seed
- tax:document_tax:compute
- tax:document_tax:suggest
- tax:history:import
- tax:migration:read
- tax:obligation:sync
- tax:vat_number:check
- tax:tax_point:read
- tax:cis:deduct
- tax:cis:record_suffered
- tax:import_vat:record
- tax:bad_debt:suggest
- tax:cis:read
workflows:
- entity: registration
states:
- not_registered
- registered
- deregistered
terminal:
- deregistered
transitions:
- key: record_not_registered
name: Record that we are not registered
from: none
to: not_registered
by: tax_admin
gate: 'the first registration row (it may carry any date: nothing has used it)'
effect: documents charge no VAT and are not titled VAT invoice
- key: register
name: Record our VAT registration
from: not_registered
to: registered
by: tax_admin
requires: vat_number
gate: a VAT number in the GB format; effective_from after today; a second person's approval where tax_changes_need_second_person
effect: tax points from that date charge VAT
- key: record_registered
name: Record an existing registration
from: none
to: registered
by: tax_admin
requires: vat_number
gate: 'the first registration row (a new installation or the migration): it carries the date HMRC registered us'
- key: change_vat_scheme
name: Change the accounting basis or the flat rate scheme (v4)
from: registered
to: registered
by: tax_admin
requires: scheme
gate: 'from the first day of a VAT period after today; eligibility from the dated tax_statutory limits (cash accounting: expected taxable turnover at most the joining limit; flat rate: at most its joining limit, a sector and its rate); a second person''s approval where tax_changes_need_second_person'
effect: 'a new registration row: accounting_basis invoice | cash, flat_rate with sector, rate and first_year_until; returns of later periods compute under it (TAX-36, TAX-37)'
- key: deregister
name: Record deregistration
from: registered
to: deregistered
by: tax_admin
requires: effective_date
approval: true
gate: effective_date after today; the approver is not the author
effect: tax points from that date charge no VAT
- entity: tax_code
states:
- draft
- active
- retired
terminal:
- retired
transitions:
- key: load_pack
name: Load the country's codes
from: none
to: draft
by: system
gate: a new installation with no codes, and no migration planned
effect: 'the UK pack: S, R, Z, E, OS, RC, NV, with their rates from their real dates'
- key: add_code
name: Add a tax code
from: none
to: draft
by: tax_admin
gate: the code is not in use
- key: activate_code
name: Activate the code
from: draft
to: active
by: tax_admin
gate: 'a treatment, a first rate (any date: no document has used the code), and the document wording where the treatment needs one (reverse charge)'
- key: schedule_rate
name: Schedule a rate change
from: active
to: active
by: tax_admin
gate: valid_from after today; no other rate for the code on that date; a second person's approval where tax_changes_need_second_person
effect: a dated rate row; issued documents unchanged
- key: withdraw_rate
name: Withdraw a rate not yet in effect
from: active
to: active
by: tax_admin
gate: its valid_from is after today
effect: the row is marked withdrawn, with who and when; it never takes effect
- key: retire_code
name: Retire the code
from: active
to: retired
by: tax_admin
gate: no rule in effect today or later points at it
effect: no new determinations; documents keep it
note: a code's treatment and reporting_box are fixed once a document has used it (a different meaning is a new code); its description and wording may change, because each issued document keeps the wording it froze
- entity: document_tax
states:
- computed
- frozen
terminal:
- frozen
transitions:
- key: determine
name: Determine each line's code
from: none
to: computed
by: system
gate: 'a rule of the document''s direction (sale | purchase) valid on the tax point covers every (category, treatment) pair, except a line pinned to an earlier frozen document (v4, TAX-47), which takes that document''s code and rate; the party has a treatment: the customer''s for a sale, the SUPPLIER''s for a purchase (v6, TAX-53: or, with no bp_id, the treatment_code the document names; neither refuses with gate party_or_treatment); v6, TAX-51: a line''s consumption (eat_in | takeaway) selects among rules that distinguish it'
effect: code, rate, VAT per line; the per-rate summary; the total; for a purchase, VAT split into reclaimable and non-reclaimable by each code's reclaimable_pct
- key: ai_suggest_code
name: AI suggests a code for a line no rule covers (suggest)
from: computed
to: computed
by: system
gate: a line has no rule, or no product, and the step is on
effect: a proposed code with its reason; nothing is applied
- key: accept_tax_suggestion
name: Accept or reject the suggestion
from: computed
to: computed
by: biller
gate: a suggestion is open on the line
effect: 'accepted: recorded as an override with the suggestion, the model and who accepted; rejected: the line stays refused until a person chooses'
- key: override_line
name: Override a line's code
from: computed
to: computed
by: biller
requires: reason
gate: the biller disagrees with the rule's code
- key: approve_override
name: Approve an override that lowers tax
from: computed
to: computed
by: tax_admin
approval: true
gate: an override lowers the tax; the approver is not the person who overrode, unless override_approval is self_with_reason
- key: check_contents
name: Check the invoice contents
from: computed
to: computed
by: system
gate: the document is about to issue (a sale) or be approved (a purchase)
effect: 'every para 16.3.1 gate as {gate, met, reason, fix}; for a purchase the supplier is the SUPPLIER (its name, address and VAT number on the bill) and we are the customer; v6, TAX-50: a purchase at or under simplified_invoice_threshold (gross including VAT) is a less detailed invoice (VAT Notice 700 para 16.6.1, VAT Regulations 1995 reg. 16): our name and address are not required and the title says so'
- key: freeze
name: Freeze the tax at issue
from: computed
to: frozen
by: system
gate: 'every readiness gate met: contents complete, no open suggestion, no override waiting for approval'
effect: never recomputed
note: the document (an order, an invoice, a credit note) owns its own copy of the result; this lifecycle is the tax on it
- entity: party_tax
states:
- recorded
terminal: []
transitions:
- key: set_party_tax
name: Record a customer's or supplier's treatment and VAT number
from: none
to: recorded
by: biller
gate: a treatment that needs a VAT number has one; the first row of a party holds from the start
effect: the party's first tax row (TAX-05)
- key: change_party_tax
name: Change a party's treatment, number or end-user notice (v4)
from: recorded
to: recorded
by: biller
gate: valid_from after today; a construction end-user or intermediary notice carries its reference and date
effect: a new dated row; documents not yet issued with a tax point from then use it (TAX-05, TAX-46)
- key: check_party_vat_number
name: Check the VAT number with HMRC (v4)
from: recorded
to: recorded
by: system
gate: a treatment that needs a VAT number was saved, or its last check is older than vat_number_check_days before a reverse-charge document issues
effect: check_result valid | invalid | could_not_check, HMRC's name and address and consultation number stored on the row (TAX-13)
- key: confirm_party_vat_number
name: Confirm a number HMRC could not check (v4)
from: recorded
to: recorded
by: tax_admin
requires: reason
gate: the last check is could_not_check (a non-GB number, or HMRC unavailable)
effect: recorded as confirmed by a person, with the reason
note: 'v4: the party''s tax rows were a table only; drawing them makes the VAT-number check and the end-user notice visible in the signed process'
- entity: vat_return
states:
- open
- draft
- locked
- submitted
terminal:
- submitted
transitions:
- key: sync_vat_obligations
name: Read our obligations from HMRC
from: none
to: open
by: system
gate: an HMRC connection for the VRN
effect: one vat_obligation per periodKey with HMRC's dates; a period HMRC shows filed is marked so (TAX-27)
timer: daily
- key: prepare_vat_return
name: Prepare the return
from: open
to: draft
by: tax_admin
gate: the obligation is open at HMRC
effect: 'boxes 1-9 from frozen document tax not yet in a return (claimed per [document_ref, tax_code]), under the period''s scheme: invoice or cash basis, flat rate, partial exemption, bad debt claims and repayments, PVA entries, adjustments (TAX-28, TAX-35..TAX-37, TAX-48)'
- key: refresh_vat_return
name: Prepare the draft again
from: draft
to: draft
by: tax_admin
effect: this draft's entries replaced; entries of other returns untouched
- key: record_vat_adjustment
name: Correct an earlier period's error on this return
from: draft
to: draft
by: tax_admin
requires: reason
gate: net errors within the error-correction threshold and the error within four years
effect: an adjustment row naming the period it corrects, in box 1 or 4 (TAX-29)
- key: record_vat_error_notice
name: Record an error notified to HMRC (VAT652)
from: draft
to: draft
by: tax_admin
requires: reference
gate: an error above the threshold
effect: the notice kept with its date, periods and amount; it is not on the return
- key: ai_review_vat_return
name: AI reviews the draft return (suggest)
from: draft
to: draft
by: system
gate: the step is on
effect: findings (a box moving far from the last four periods, a document with an unusual code) listed for the tax admin; nothing changes
- key: lock_vat_return
name: Lock the return for submission
from: draft
to: locked
by: tax_admin
gate: the period has ended (its end before today in business_timezone); the period's postponed-import statements reconciled; flat-rate eligibility answered; no adjustment above the threshold
effect: boxes and entries frozen; a new document with a tax point in the period is refused (tax_point_open)
- key: reopen_vat_return
name: Reopen a locked return
from: locked
to: draft
by: tax_admin
requires: reason
gate: not submitted
- key: submit_vat_return
name: Submit to HMRC with the declaration
from: locked
to: submitted
by: owner
approval: true
requires: declaration
gate: the HMRC connection is valid; the person ticked the legal declaration; a second person where vat_return_needs_second_person (not the identity that locked it, by email or sub); every fraud-prevention header present; HMRC still shows the obligation open
effect: HMRC's receipt stored; the return immutable; its tax points filed (TAX-30, TAX-31)
- key: settle_vat_return
name: Post the return's settlement to the ledger
from: submitted
to: submitted
by: system
gate: the ledger is installed
effect: each tax code's VAT against vat_control, journal id derived from the return; once (TAX-32)
- key: refresh_vat_liability
name: Read HMRC's liabilities and payments
from: submitted
to: submitted
by: system
gate: an HMRC connection
effect: liability, paid and outstanding per period as HMRC holds them (TAX-32)
timer: daily
note: a submission that times out is settled by GET /organisations/vat/{vrn}/returns/{periodKey} before any retry, so a return is never filed twice; a correction after submission is an adjustment on a later return or a VAT652 notice, never an edit
- entity: bad_debt_claim
states:
- eligible
- claimed
- dismissed
terminal:
- claimed
- dismissed
transitions:
- key: suggest_bad_debt_relief
name: List invoices eligible for relief
from: none
to: eligible
by: system
gate: an invoice six months past the later of its due and supply dates, written off, its VAT in a submitted return
effect: one eligible row per invoice
timer: when a return is prepared
- key: claim_bad_debt_relief
name: Claim the relief on this return
from: eligible
to: claimed
by: tax_admin
gate: every eligibility gate met; within four years and six months
effect: the VAT fraction of the unpaid amount in box 4 (TAX-34)
- key: dismiss_bad_debt_relief
name: Do not claim
from: eligible
to: dismissed
by: tax_admin
requires: reason
- key: repay_bad_debt_relief
name: Repay relief on a later receipt
from: claimed
to: claimed
by: system
gate: a receipt allocated to the invoice after the claim
effect: its VAT fraction in box 1 of the next return, once per allocation (TAX-35)
- entity: cis_subcontractor
states:
- unverified
- verified
terminal: []
transitions:
- key: add_cis_subcontractor
name: Add a subcontractor
from: none
to: unverified
by: tax_admin
requires: utr
gate: a business partner with a UTR (and a company number or NI number)
- key: verify_cis_subcontractor
name: Verify with HMRC
from: unverified
to: verified
by: tax_admin
gate: an HMRC connection, or a verification number a person read from HMRC
effect: status gross | net | unmatched, rate and verification number from today (TAX-38)
- key: reverify_cis_subcontractor
name: Verify again
from: verified
to: verified
by: tax_admin
effect: a new dated row; deductions from then use it
- entity: cis_return
states:
- open
- prepared
- submitted
terminal:
- submitted
transitions:
- key: compute_cis_deduction
name: Compute the deduction on a subcontractor's payment
from: open
to: open
by: system
gate: the subcontractor is verified, or was paid in this or the last two tax years
effect: a deduction row keyed [payment_ref, bp_id] in the payment's tax month (TAX-39)
- key: reverse_cis_deduction
name: Reverse a deduction when the payment is rejected (v4)
from: open
to: open
by: system
gate: the deduction's tax month is not submitted (cis_return_open); a repeat answers the same
effect: a reversing deduction row keyed [payment_ref, bp_id]; the month's return and the subcontractor's statement no longer count it
- key: prepare_cis_return
name: Prepare the monthly CIS return
from: open
to: prepared
by: tax_admin
gate: the tax month (6th to 5th) has ended
effect: the CIS300 lines per subcontractor, or a nil return
- key: submit_cis_return
name: Submit the CIS return
from: prepared
to: submitted
by: tax_admin
approval: true
requires: declaration
gate: the employment-status and verification declarations confirmed by a person; an HMRC connection
effect: HMRC's receipt kept (TAX-40)
- key: send_cis_statements
name: Send each subcontractor its statement
from: submitted
to: submitted
by: tax_admin
effect: a payment and deduction statement per subcontractor, the send recorded
- entity: trade_declaration
states:
- draft
- filed
terminal:
- filed
transitions:
- key: prepare_trade_declaration
name: Prepare the EC sales list or Intrastat declaration
from: none
to: draft
by: tax_admin
gate: ni_goods registered; for Intrastat, the flow's threshold passed
effect: lines from ar's (and purchasing's) goods documents with EU partners; the upload file (TAX-42, TAX-43)
- key: record_trade_declaration_filed
name: Record that it was filed with HMRC
from: draft
to: filed
by: tax_admin
requires: submission_ref
gate: the EC sales list ties to box 8 for its months; every Intrastat line has a commodity code
effect: filed with the reference and date; an amendment is a new version
- entity: pva_statement
states:
- imported
- reconciled
terminal:
- reconciled
transitions:
- key: import_pva_statement
name: Upload the month's postponed import VAT statement
from: none
to: imported
by: tax_admin
gate: one statement per month
effect: statement lines kept, the file kept as an object
- key: match_pva_statement
name: Match statement lines to import entries
from: imported
to: imported
by: system
effect: each line matched by MRN, with any difference
- key: explain_pva_difference
name: Explain a difference
from: imported
to: imported
by: tax_admin
requires: reason
- key: reconcile_pva_statement
name: Reconcile the statement
from: imported
to: reconciled
by: tax_admin
gate: every line matched or explained
effect: the return uses the reconciled figures (TAX-45)