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