dodil/erp-brick-ar
DODIL ERP brick: accounts receivable
src/brick_ar/application/process.yaml
# The brick's process — module.yaml's workflows (v6; workflows unchanged since v4), copied (pattern.md §15). BRICK.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 (or its derived <entity>_<from>_to_<to>) is frozen once signed: it is the BPMN id.
id: ar
name: Invoicing
version: 7
actors:
- id: biller
name: Whoever sends the invoices (office, bookkeeper)
does: reviews drafts, issues, sends, raises credit notes, exports to the accountant
sees: every invoice
permissions:
- ar:invoice:read
- ar:invoice:write
- ar:invoice:issue
- ar:invoice:send
- ar:credit_note:write
- ar:writeoff:write
- ar:schedule:read
- ar:export_batch:run
- ar:schedule:write
- ar:provision:write
- id: collector
name: Whoever chases money and records it
does: chases overdue invoices by hand, records what was received (in payments)
sees: every open invoice and its history
permissions:
- ar:invoice:read
- ar:invoice:collect
- id: approver
name: Owner / finance manager
does: approves credit notes, voids and write-offs, sets terms, customers' billing profiles, the account map and the document layout
sees: everything
permissions:
- ar:invoice:read
- ar:invoice:void
- ar:credit_note:approve
- ar:writeoff:approve
- ar:customer:write
- ar:settings:write
- ar:migration:read
- ar:provision:approve
- id: owner
name: The owner (or whoever the owner names)
does: holds the controls on the people who invoice and approve (time zone, numbering, ledger mode, auto-issue, void policy, write-off threshold, second approver) and signs the migration reconciliation
sees: everything
permissions:
- ar:invoice:read
- ar:settings:controls
- ar:migration:read
- ar:migration:sign
note: 'the controls are never held by whoever they control: a biller or approver holding ar:settings:controls is a configuration error the access test refuses'
- id: sales
name: Sales (read-only)
does: checks whether a customer is overdue before quoting them again
sees: invoice status and balance for a customer; never lines or margin
permissions:
- ar:status:read
- id: customer
name: The customer
external: true
does: receives the invoice and pays it
sees: their own invoices, by email or the portal
permissions: []
- id: system
name: The system
does: raises drafts from schedules and sources, checks a draft before issue, posts to the ledger, plans, stops and finishes schedules, numbers issued documents, derives what is due, loads and reconciles a migration
sees: everything, through its own service account
permissions:
- ar:invoice:read
- ar:invoice:raise
- ar:invoice:check
- ar:invoice:post
- ar:schedule:plan
- ar:schedule:stop
- ar:schedule:finish
- ar:number_series:claim
- ar:migration:load
- ar:migration:reconcile
- ar:invoice:settle_discount
- ar:payment_request:settle
- ar:revenue:release
- ar:einvoice:deliver
- ar:intercompany:hand_over
note: each is an exact grant to one service client, never reached by a wildcard (permissions.machine)
workflows:
- entity: schedule
states:
- active
- finished
- stopped
terminal:
- finished
- stopped
transitions:
- key: plan_billing
name: Create a schedule for the sold line
from: none
to: active
by: system
gate: a source confirmed a recurring, instalment or usage line
effect: one schedule per line, keyed <source_id>|<line_id>
- key: plan_manual_schedule
name: Set up a recurring invoice by hand (v4)
from: none
to: active
by: biller
gate: a customer with a billing profile; starts_on today or later
effect: a schedule keyed man:<uuid>|1, source_kind manual, on the same engine (AR-42)
- key: finish_schedule
name: Finish the schedule
from: active
to: finished
by: system
gate: every period raised
- key: stop_billing
name: Stop billing after the current period
from: active
to: stopped
by: system
gate: the source ended early (AR-28); the stop is dated today, never earlier
effect: issued invoices are untouched; money owed back is a credit note
note: which period is due, how many are billed and what remains are DERIVED from starts_on, periods and the invoices raised; nothing increments a counter
readiness:
- transition: plan_billing
gate: source_line_repeats
reason: the line is not recurring, instalment or usage
fix: bill it once through raise_draft
- transition: plan_billing
gate: customer_profile
reason: the customer has no billing profile
fix: 'Customer billing: set bill-to and terms (AR-25)'
- transition: stop_billing
gate: not_back_dated
reason: a stop cannot be dated before today
fix: stop today and credit the periods already issued (AR-04)
- transition: plan_manual_schedule
gate: starts_not_past
reason: a recurring invoice set up by hand starts today or later
fix: pick today or a later start
- entity: invoice
states:
- draft
- issued
- void
- cancelled
terminal:
- issued
- void
- cancelled
transitions:
- key: raise_due
name: Raise this period's draft
from: none
to: draft
by: system
gate: a schedule period is due
effect: keyed rec:schedule:period, so a second run makes nothing
timer: the billing day of the period
- key: raise_from_order
name: Raise the draft the order asked for
from: none
to: draft
by: system
gate: a source called raise_draft (an order, a deposit, an adapter)
effect: keyed on the source; lines keep their source keys
- key: raise_for_acceptance
name: Raise the draft for the accepted item
from: none
to: draft
by: system
gate: an item was accepted (AR-29)
effect: keyed acc:item
- key: draft_invoice
name: Raise an invoice by hand
from: none
to: draft
by: biller
gate: work with no order (AR-02)
effect: totals and tax computed, never typed
- key: raise_interest_draft
name: Raise the late-payment interest note (v4)
from: none
to: draft
by: biller
gate: late_interest_mode is not off; the customer is a business; an invoice paid late or still overdue
effect: a draft kind interest at the statutory rate plus fixed compensation, code OS (AR-39)
- key: settle_payment_request_invoice
name: Raise the deposit invoice a paid request became (v4)
from: none
to: draft
by: system
gate: payments settled a payment request (AR-36)
effect: keyed dep:<request_id>:<n>, tax point = received_on; issued at once (it is the money already received)
- key: ai_check_draft
name: AI checks the draft before issue (suggest)
from: draft
to: draft
by: system
gate: the pre-issue check is on
effect: unusual amounts, a missing PO, an odd tax code or a duplicate of last month listed for the biller
- key: review_draft
name: Review the draft
from: draft
to: draft
by: biller
effect: drafts are not issued automatically unless the setting says so for the source
- key: fill_usage
name: Enter the period's usage
from: draft
to: draft
by: biller
gate: the draft has a usage line
- key: accept_invoice_findings
name: Accept or dismiss each AI finding
from: draft
to: draft
by: biller
gate: the draft has findings (AR-32)
effect: each decision recorded in ar_ai_finding; nothing changes without the biller
- key: cancel_draft_invoice
name: Cancel the draft
from: draft
to: cancelled
by: biller
effect: no number used; a zero-usage period ends here
- key: issue
name: Issue the invoice
from: draft
to: issued
by: biller
gate: 'at least one line; usage filled in; the customer has a billing profile in force today; the tax content check passes; the tax point is not in a locked or filed VAT return (tax GET /vat-returns/tax-point-status) or a closed period; v4: each deposit deduction within what remains of its deposit; a cost centre on every revenue line where the ledger requires one'
effect: numbered from the company's series; issue date today; due date from the terms; tax and bill-to frozen; revenue for a service period to deferred revenue (AR-46)
- key: post_to_ledger
name: Post to the ledger
from: issued
to: issued
by: system
gate: the ledger mode is post
effect: a balanced journal keyed on the invoice
- key: send
name: Send it (a person presses Send)
from: issued
to: issued
by: biller
effect: the send recorded; delivery tracked by the mail capability, or by the e-invoice access point (v4)
- key: deliver_einvoice
name: Deliver as a Peppol e-invoice (v4)
from: issued
to: issued
by: system
gate: the customer's channel is peppol with a participant id; the UBL passes EN 16931 and Peppol BIS 3 validation
effect: UBL 2.1 to the access point, the message id on the send (AR-43)
- key: record_einvoice_status
name: Record the e-invoice's delivery and the buyer's response (v4)
from: issued
to: issued
by: system
effect: each status with its time on the send row (AR-44)
- key: hand_to_partner_company
name: Hand the intercompany invoice to the partner company (v4)
from: issued
to: issued
by: system
gate: trading_partner names a company in this installation
effect: a bill keyed ic:<invoice_id> in that company's purchasing (AR-41)
- key: release_deferred_revenue
name: Release revenue for the period (v4)
from: issued
to: issued
by: system
gate: a line with a service period not fully released
effect: deferred to revenue for the days in the period, keyed [invoice_id, line_no, period_id] (AR-46)
timer: the month end
- key: receive_invoice
name: Receive the invoice
from: issued
to: issued
by: customer
- key: chase
name: Chase by hand
from: issued
to: issued
by: collector
gate: overdue and outstanding above zero, and reminders are by hand
timer: the due date passes unpaid
- key: pay
name: Pay
from: issued
to: issued
by: customer
- key: record_payment
name: Record the receipt and what it settles
from: issued
to: issued
by: collector
effect: 'in payments: the receipt and its allocations; outstanding is derived'
- key: export
name: Include in the monthly export to the accountant
from: issued
to: issued
by: biller
gate: the ledger mode is export
effect: in one export batch, once
- key: void_invoice
name: Void the invoice
from: issued
to: void
by: approver
requires: reason
approval: true
gate: no allocations, and the owner's void_policy allows it (default never_sent)
effect: drops out of every balance; reversing journal dated today in post mode
note: 'SETTLEMENT is derived, never stored: outstanding = total - SUM(allocations of payments, credit notes and write-offs), computed by payments; open, part paid, paid and written off follow from it, so there is no "paid" state to set. For the screens the brick answers each document''s readiness: every gate of its next transitions as {gate, met, reason, fix} (fix a link to the screen that clears it), so no screen re-derives a rule. A migrated invoice (source_kind odoo) is issued history: post_to_ledger, send and raise never run on it; credit notes and allocations do.'
readiness:
- transition: issue
gate: has_lines
reason: the invoice has no lines
fix: add a line
- transition: issue
gate: usage_filled
reason: a usage line has no quantity
fix: enter the period's usage (AR-19)
- transition: issue
gate: billing_profile
reason: the customer has no bill-to address, invoice email or terms in force today
fix: Customer billing (AR-25)
- transition: issue
gate: tax_content
reason: 'the tax module''s content check failed: its missing[] named'
fix: the field it names, or the tax code on the line
- transition: issue
gate: tax_point_open
reason: the tax point falls in a filed VAT return or a closed ledger period
fix: issue with a tax point of today, or ask the accountant to reopen
- transition: issue
gate: number_series
reason: no series for this document type and year
fix: 'Settings: number format'
- transition: post_to_ledger
gate: account_map
reason: no account in force today for a purpose or tax code on the invoice
fix: 'Settings: account map'
- transition: void_invoice
gate: no_allocations
reason: a receipt, credit or write-off is allocated to it
fix: credit it instead (AR-04)
- transition: void_invoice
gate: void_policy
reason: the owner's void_policy does not allow voiding this invoice (it was sent, or its VAT return is filed)
fix: credit it instead (AR-04)
- transition: export
gate: not_exported
reason: already in batch <period>:<format>
fix: 'none needed: it is exported once'
- transition: issue
gate: deposit_remaining
reason: a deduction takes more than what remains of deposit <invoice_no>
fix: deduct at most what remains
- transition: issue
gate: cost_centre
reason: the ledger splits by cost centre and a revenue line has none
fix: the line's cost centre, or the customer's default on Customer billing
- transition:
- issue
- deliver_einvoice
gate: einvoice_valid
reason: 'the e-invoice fails rule <rule id>: <text>'
fix: the field the rule names
- transition: deliver_einvoice
gate: participant_id
reason: the customer's billing profile has no Peppol participant id
fix: 'Customer billing: e-invoice channel'
- transition: raise_interest_draft
gate: business_customer
reason: statutory interest applies only between businesses
fix: 'none: a consumer is never charged'
- transition: raise_interest_draft
gate: interest_mode_on
reason: late_interest_mode is off
fix: Settings (the owner's control)
- entity: credit_note
states:
- draft
- approved
- cancelled
terminal:
- approved
- cancelled
transitions:
- key: draft_credit
name: Draft a credit note
from: none
to: draft
by: biller
gate: an issued invoice to correct, or a goodwill credit on the customer
- key: approve_credit
name: Approve the credit note
from: draft
to: approved
by: approver
approval: true
gate: lines and tax present; it does not exceed what remains creditable on the invoice it names; the approver is not its author where second_approver is on
effect: numbered; credit date today; tax frozen; posted in post mode
- key: allocate_credit
name: Allocate the credit
from: approved
to: approved
by: biller
effect: 'in payments: against one or more invoices, or left on the customer'
- key: cancel_credit
name: Cancel the draft credit
from: draft
to: cancelled
by: biller
- key: raise_settlement_discount
name: Credit the early-settlement discount taken (v4)
from: none
to: approved
by: system
gate: received within the discount window; the amount is the terms' percentage of the gross outstanding at the receipt; settlement_discount_vat is credit_note
effect: numbered, at the invoice's frozen rates pro rata (tax TAX-47), approved by the terms, posted, allocated by payments (AR-38)
readiness:
- transition: approve_credit
gate: has_lines
reason: the credit note has no lines
fix: add a line, or credit the invoice in full
- transition: approve_credit
gate: within_invoice
reason: it exceeds the invoice total less what is already credited
fix: lower the amount, or credit the customer as goodwill (no invoice)
- transition: approve_credit
gate: second_person
reason: you drafted it, and the owner asks for a second person
fix: ask another approver
- transition: raise_settlement_discount
gate: within_discount_window
reason: received on <received_on>, after the discount ended on <date>
fix: 'none: the short payment stays outstanding'
- transition: raise_settlement_discount
gate: discount_matches_terms
reason: <amount> is not <pct>% of the gross outstanding <gross>
fix: allocate the receipt; the difference stays outstanding or goes to tolerance
- entity: writeoff
states:
- proposed
- approved
- rejected
terminal:
- approved
- rejected
transitions:
- key: propose_writeoff
name: Propose a write-off
from: none
to: proposed
by: biller
requires: reason
- key: approve_writeoff
name: Approve the write-off
from: proposed
to: approved
by: approver
approval: true
gate: below the threshold, or the approver accepts it above
effect: allocated in payments; bad debt posted in post mode
- key: reject_writeoff
name: Reject the write-off
from: proposed
to: rejected
by: approver
requires: reason
readiness:
- transition: approve_writeoff
gate: outstanding
reason: the amount exceeds what is outstanding on the invoice
fix: write off the outstanding amount
- transition: approve_writeoff
gate: threshold
reason: 'above the owner''s write-off threshold: an explicit confirmation is asked'
fix: confirm, with the reason
- transition: approve_writeoff
gate: second_person
reason: you proposed it, and the owner asks for a second person
fix: ask another approver
- entity: migration_run
states:
- loaded
- reconciled
- signed
- discarded
terminal:
- signed
- discarded
transitions:
- key: load_invoices_from_odoo
name: Load the Odoo history
from: none
to: loaded
by: system
gate: the customer's doors are decided (migration.odoo.door); business-partner and tax have mapped every partner and tax the moves use
effect: documents written read-only, keyed odoo:<db>:<move id>; a re-run changes nothing (AR-34)
- key: reconcile_invoice_migration
name: Reconcile against Odoo
from: loaded
to: reconciled
by: system
effect: every migration.odoo.reconcile check computed on both sides, with the difference
- key: explain_invoice_difference
name: Explain each difference
from: reconciled
to: reconciled
by: approver
requires: reason
gate: a check is not equal and may be explained
effect: the explanation recorded beside the check
- key: reload_invoice_migration
name: Load again after a fix
from:
- loaded
- reconciled
to: loaded
by: system
gate: not signed
effect: the same keys; explanations on unchanged checks kept
- key: sign_invoice_reconciliation
name: Sign the reconciliation
from: reconciled
to: signed
by: owner
approval: true
gate: every check equal, or explained where explained is allowed
effect: who, when and the run's checksums recorded; invoicing takes writes for these customers from now
- key: discard_invoice_migration
name: Discard the run
from:
- loaded
- reconciled
to: discarded
by: owner
requires: reason
effect: the run's documents are removed; nothing was ever issued from them
readiness:
- transition: load_invoices_from_odoo
gate: doors_decided
reason: a migration door is undecided
fix: 'Migration: answer it'
- transition: load_invoices_from_odoo
gate: partners_mapped
reason: Odoo partners used by the moves have no bp_id
fix: run the business-partner migration first
- transition: load_invoices_from_odoo
gate: taxes_mapped
reason: Odoo taxes used by the moves have no tax_code
fix: run the tax migration first
- transition: sign_invoice_reconciliation
gate: zero_unexplained
reason: 'checks neither equal nor explained: named'
fix: explain each, or fix and reload
- transition: sign_invoice_reconciliation
gate: must_equal
reason: a check that must be equal differs
fix: fix the mapping and reload; it cannot be explained away
- entity: payment_request
states:
- draft
- issued
- settled
- cancelled
terminal:
- settled
- cancelled
transitions:
- key: raise_payment_request
name: Ask for a down payment, or raise a pro forma (v4)
from: none
to: draft
by: biller
gate: deposit_mode request, or a pro forma asked; the amount within the order total less deposits already asked (sales-order SO-09)
effect: keyed dpr:<sales_order_id>:<request_no> from an order, a uuid by hand; amounts computed by tax for display only (AR-35)
- key: issue_payment_request
name: Issue the payment request
from: draft
to: issued
by: biller
gate: a billing profile in force today
effect: numbered DPR-; titled not a VAT invoice; nothing posted, nothing in a VAT return
- key: settle_payment_request
name: Settle the request from the receipt that paid it
from: issued
to:
- issued
- settled
by: system
gate: payments names a receipt and its amount, at most what remains of the request
effect: a deposit invoice for the amount received, tax point the received date (AR-36); settled when nothing remains
- key: cancel_payment_request
name: Cancel the payment request
from:
- draft
- issued
to: cancelled
by: biller
requires: reason
gate: nothing received against it
readiness:
- transition: settle_payment_request
gate: within_request
reason: the receipt is more than what remains of the request
fix: settle what remains; the rest stays unallocated on the customer
- transition: cancel_payment_request
gate: nothing_received
reason: a receipt already settled part of it
fix: credit the deposit invoice it became (AR-04)
- entity: provision_run
states:
- proposed
- approved
- rejected
terminal:
- approved
- rejected
transitions:
- key: propose_doubtful_provision
name: Propose the month's doubtful-debt provision (v4)
from: none
to: proposed
by: biller
gate: the month has ended; payments' aging as at its last day
effect: 'a run keyed prov:<company_code>:<period_id>: per bucket and per disputed invoice, net of VAT (AR-45)'
- key: approve_doubtful_provision
name: Approve and post the provision
from: proposed
to: approved
by: approver
approval: true
gate: the approver is not the proposer where second_approver; the ledger period is open
effect: the movement since the last run posted, dated the period end
- key: reject_doubtful_provision
name: Reject the provision
from: proposed
to: rejected
by: approver
requires: reason
readiness:
- transition: approve_doubtful_provision
gate: second_person
reason: you proposed it, and the owner asks for a second person
fix: ask another approver
- transition: approve_doubtful_provision
gate: period_open
reason: the ledger period is closed
fix: ask the accountant, or post in the next period