dodil/erp-brick-purchasing
DODIL ERP brick: purchasing
src/brick_purchasing/application/process.yaml
# The brick's process — module.yaml's workflows, 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: purchasing
name: Purchasing & suppliers
version: 8
actors:
- id: requester
name: whoever needs something
does: asks for what they need and sees where the request got to
sees: their own requests
permissions:
- purchasing:request:write
- purchasing:request:read_own
- id: spend_approver
name: budget holder
does: approves or refuses spend before it is committed, within their limit, and asks for things themselves like anyone else
sees: requests and orders routed to them, and the orders that came from them
permissions:
- purchasing:request:approve
- purchasing:order:approve
- purchasing:agreement:approve
- purchasing:request:write
- purchasing:request:read
- purchasing:order:read
- id: buyer
name: whoever places orders
does: turns approved requests into orders, keeps supplier prices, confirms and sends orders, closes them short
sees: every request and order, every supplier price
permissions:
- purchasing:order:write
- purchasing:order:confirm
- purchasing:order:send
- purchasing:request:read
- purchasing:order:read
- purchasing:supplier:write
- purchasing:supplier:read
- purchasing:supplier_price:write
- purchasing:receipt:read
- purchasing:rfq:write
- purchasing:agreement:write
- purchasing:source:write
- purchasing:evaluation:write
- purchasing:down_payment:request
- id: receiver
name: whoever takes delivery (stores, site, reception)
does: records what actually arrived and what goes back to the supplier
sees: orders still to arrive, and receipts
permissions:
- purchasing:receipt:write
- purchasing:receipt:read
- purchasing:order:read_expected
- purchasing:subcontract:provide
- id: payables_clerk
name: accounts payable
does: records supplier invoices and credit notes, disputes them, proposes payment runs, confirms what the bank paid, keeps supplier bank details
sees: every bill, credit, run and payment
permissions:
- purchasing:bill:write
- purchasing:bill:read
- purchasing:payment:write
- purchasing:payment:read
- purchasing:order:read
- purchasing:receipt:read
- purchasing:supplier:read
- purchasing:supplier_bank:write
- purchasing:down_payment:clear
- purchasing:self_billing:write
- purchasing:ers:run
- purchasing:consignment:settle
- id: payables_approver
name: whoever releases money (finance manager)
does: approves bills, accepts match variances with a reason, approves payment runs and supplier bank-detail changes, keeps day-to-day settings
sees: everything payables sees
permissions:
- purchasing:bill:approve
- purchasing:bill:read
- purchasing:payment:approve
- purchasing:payment:read
- purchasing:supplier_bank:approve
- purchasing:supplier:read
- purchasing:settings:write
- purchasing:migration:read
- purchasing:down_payment:approve
- purchasing:self_billing:approve
- id: owner
name: The owner (or whoever the owner names)
does: holds the controls on the people who spend and pay (approval limits and the release strategy, tolerance keys, the order approval threshold, number formats, the payment-file format and the BACS service user number, vendor-evaluation weights, the self-billing maximum) and signs the migration reconciliation
sees: everything
permissions:
- purchasing:bill:read
- purchasing:order:read
- purchasing:settings:controls
- purchasing:migration:read
- purchasing:migration:sign
note: 'the controls are never held by whoever they control: a spend_approver, buyer, clerk or payables_approver holding purchasing:settings:controls is a configuration error the access test refuses'
- id: supplier
name: The supplier
external: true
does: receives the order, delivers, sends an invoice, is paid
sees: the order sent to them; nothing in the system
permissions: []
- id: system
name: The system
does: stamps numbers, hands receipts to inventory, computes the match, asks tax, posts to the ledger, writes the bank file, reads a supplier's PDF into a draft (suggest), loads and reconciles a migration
sees: everything, through its own service account
permissions:
- purchasing:number_series:claim
- purchasing:receipt:hand_over
- purchasing:bill:post
- purchasing:payment:post
- purchasing:payment:export
- purchasing:bill:ai_read
- purchasing:bill:ai_check
- purchasing:migration:load
- purchasing:migration:reconcile
- purchasing:consignment:intake
- purchasing:source:propose
- purchasing:asset:hand_over
- purchasing:cis:record
- purchasing:request:intake
- purchasing:payment:confirm
note: each is an exact grant to one service client, never reached by a wildcard (permissions.machine); purchasing:consignment:intake and purchasing:source:propose go to inventory's client
workflows:
- entity: request
process: procure
states:
- draft
- awaiting_approval
- approved
- refused
- ordered
- closed
- cancelled
terminal:
- refused
- closed
- cancelled
transitions:
- key: draft_request
name: Ask for what you need
from: none
to: draft
by: requester
effect: request_id derived from the requester and the moment they asked
- key: intake_request_from_sales_order
name: Take a request from a confirmed third-party order line (PUR-65)
from: none
to: awaiting_approval
by: system
gate: request_ref so:{sales_order_id}:{line_no}@r{revision} unique (a repeat answers the same); the product can be bought
effect: a request carrying the ship-to party and address and the sales order line; the order raised from it is a drop-ship order
- key: intake_request_from_replenishment
name: Take a request from inventory's replenishment (PUR-65)
from: none
to: awaiting_approval
by: system
gate: request_ref inv:repl:{warehouse_id}:{product_id}:{business_date} unique (a repeat answers the same); the product can be bought
effect: a request for the warehouse's goods-in location, needed by the date inventory named
- key: submit_request
name: Submit the request
from: draft
to: awaiting_approval
by: requester
gate: a line, an amount, a cost centre and a reason
effect: routed to the approvers whose limit in force today covers it
- key: approve_request_level
name: Approve one release level
from: awaiting_approval
to: awaiting_approval
by: spend_approver
approval: true
gate: the release strategy in force on the submit date needs more levels than are approved; the approver's limit meets this level; the approver is not the requester and approved no earlier level (email AND sub)
effect: a release row (request, level, who, email and sub, when) (v4, PUR-38)
- key: approve_request
name: Approve the spend
from: awaiting_approval
to: approved
by: spend_approver
approval: true
gate: 'the final level: the amount is within this approver''s limit in force today; the approver is not the requester nor any earlier level''s approver (email AND sub)'
effect: who, when (today in the business time zone) and any note recorded
- key: refuse_request
name: Refuse with a reason
from: awaiting_approval
to: refused
by: spend_approver
requires: reason
- key: raise_order
name: Raise the order
from: approved
to: ordered
by: buyer
gate: a supplier is chosen
effect: an order exists, keyed req:<request_id>; the approval travels with it
- key: close_request
name: Close without ordering
from: approved
to: closed
by: buyer
requires: reason
- key: cancel_request
name: Cancel the request (v8; PUR-67)
from:
- draft
- awaiting_approval
- approved
to: cancelled
by:
- requester
- buyer
requires: reason
gate: not ordered; a requester cancels only their own request (purchasing:request:write), a buyer any request (purchasing:order:write)
effect: who (email and sub), when and why recorded; the request leaves the approval queue and inventory's expected stock
- key: cancel_request_from_sales_order
name: Cancel a request sales-order raised (v8; PUR-67)
from:
- awaiting_approval
- approved
to: cancelled
by: system
requires: reason
gate: request_ref so:... raised by the caller; not ordered (an ordered request answers not_cancellable naming its order and supplier)
effect: as cancel_request; a repeat answers the same
- key: cancel_with_order
name: Cancel the request with its order (v8; PUR-67)
from: ordered
to: cancelled
by: system
gate: the purchase order raised from it (req:<request_id>) is cancelled
effect: the request ends with the order; the order's reason copied
note: 'v8: ordered is no longer an end state: a request ordered stays ordered unless its order is cancelled, which cancels the request with it. An ordered request is never cancelled on its own, whether or not the supplier has confirmed the order: the commitment is the order''s, and cancel_purchase_order (nothing received, nothing billed) is the one control over it.'
readiness:
- transition: cancel_request
gate: not_ordered
reason: the request is ordered as {order_no} at {supplier}
fix: cancel the purchase order (PUR-08); the request follows it
- transition: cancel_request_from_sales_order
gate: not_cancellable
reason: the request is ordered as {order_no} at {supplier}
fix: the buyer cancels the purchase order; then cancel again
- transition: submit_request
gate: complete
reason: the request has no line, amount, cost centre or reason
fix: fill in what is missing
- transition: approve_request
gate: within_limit
reason: the amount is above your limit in force today for this category
fix: 'an approver with a higher limit (Settings: approval limits)'
- transition: approve_request
gate: second_person
reason: you asked for this (same email or same sign-in)
fix: another approver
- transition: approve_request_level
gate: second_person
reason: you asked for this, or approved an earlier level (same email or same sign-in)
fix: another approver
- transition: approve_request_level
gate: level_limit
reason: your limit is below this level's threshold
fix: 'an approver of this level (Settings: release strategy)'
- transition: raise_order
gate: supplier_known
reason: the supplier is not in the partner register
fix: add it (promote, role supplier)
- entity: order
process: procure
states:
- draft
- to_approve
- approved
- confirmed
- closed
- cancelled
terminal:
- closed
- cancelled
transitions:
- key: raise_from_request
name: Draft the order from the request
from: none
to: draft
by: buyer
gate: an approved request
effect: keyed req:<request_id>; a second raise returns the same order
- key: draft_order
name: Draft an order directly
from: none
to: draft
by: buyer
effect: keyed po:<buyer>:<moment>
- key: raise_from_quote
name: Draft the order from the awarded quote
from: none
to: draft
by: buyer
gate: an awarded quote (PUR-40)
effect: keyed rfq:<rfq_id>:<bp_id>; the quoted prices fixed on the lines; the request's approval carried (v4)
- key: raise_release_order
name: Draft a release order against a contract
from: none
to: draft
by: buyer
gate: an active quantity or value contract in its validity
effect: keyed po:<buyer>:<moment> with agreement_id; prices from the agreement line; the agreement's approval is the spend approval within its target (v4, PUR-42)
- key: open_call_off
name: Open a call-off from a JIT release
from: none
to: confirmed
by: system
gate: a JIT release of an active scheduling agreement
effect: keyed sa:<agreement_id>:<release_no>, confirmed at once with order_no from the series; the agreement claims written (v4, PUR-43)
- key: price_lines
name: Price the lines in the supplier's unit
from: draft
to: draft
by: buyer
effect: unit_code and its exact conversion copied from the product; the order price unit, its conversion (the supplier price row's own, else the product's), pricing_quantity and unit_price from the supplier price in force today, fixed on the line; net = quantity_priced x unit_price / pricing_quantity, rounded once; the purchase rounding gate shown (v3); the source list checked, the quota's proposal shown, the account assignment (stock | cost_centre | asset | project), item category (standard | consignment | subcontract), subcontract components and planned delivery costs set (v4)
- key: accept_rounding
name: Accept an order off the pack rule
from: draft
to: draft
by: spend_approver
approval: true
requires: reason
gate: a line fails purchase_rounding; the acceptor is not the buyer (email AND sub)
effect: an override row (order, revision, line, gate purchase_rounding, who, why), before the order is sent for approval or confirmed; the buyer confirms (v3)
- key: approve_order_level
name: Approve one release level
from: to_approve
to: to_approve
by: spend_approver
approval: true
gate: the release strategy in force on the submit date needs more levels than are approved; the approver's limit meets this level; not the buyer nor an earlier level's approver (email AND sub)
effect: a release row (order, revision, level, who) (v4, PUR-38)
- key: submit_order
name: Send for approval
from: draft
to: to_approve
by: buyer
gate: no approved request covers the total, and the total is above the owner's order threshold
- key: approve_order
name: Approve the order
from: to_approve
to: approved
by: spend_approver
approval: true
gate: 'the final level: the total is within the approver''s limit in force today; the approver is not the buyer nor an earlier level''s approver (email AND sub)'
- key: reject_order
name: Send back with a reason
from: to_approve
to: draft
by: spend_approver
requires: reason
- key: confirm_purchase_order
name: Confirm the order
from:
- draft
- approved
to: confirmed
by: buyer
gate: within authority (an approved request or approval covers the total, or below the threshold); every line has a unit and a pricing unit that convert; every line meets its purchase rounding or carries an override for this revision (v3); every line's supplier is allowed by the source list; a release order fits what is left on its agreement; an asset-assigned line is assignable; a subcontract line has its components (v4)
effect: order_no claimed from the series; contents, each line's pricing conversion and account assignment frozen; product told each unit and pricing unit was used; agreement claims written (v4)
- key: send_order
name: Send it to the supplier
from: confirmed
to: confirmed
by: buyer
effect: each send recorded with when and to whom
- key: provide_components
name: Send the subcontractor our components
from: confirmed
to: confirmed
by: receiver
gate: a subcontract line; not more than the line needs, unless a reason is given
effect: 'inventory''s POST /subcontract-provisions per component, keyed pur:<purchase_order_id>:<line_no>:prov:<seq>: the components move to stock held at the supplier, still ours (v4, PUR-57)'
- key: receive_order
name: The supplier delivers
from: confirmed
to: confirmed
by: supplier
- key: amend_purchase_order
name: Amend the confirmed order
from: confirmed
to: confirmed
by: buyer
gate: the new total is still within what was approved; a line is never reduced below what was received or billed
effect: a new revision row; the previous revision kept
- key: amend_beyond_approval
name: Amend beyond what was approved
from: confirmed
to: to_approve
by: buyer
gate: the new total exceeds what was approved
- key: close_short
name: Close short with a reason
from: confirmed
to: closed
by: buyer
requires: reason
effect: what was not received stops being outstanding and committed
- key: finish_purchase_order
name: Finish the order
from: confirmed
to: closed
by: system
gate: 'every line received and billed in full (or two-way: billed)'
- key: cancel_purchase_order
name: Cancel the order
from:
- draft
- to_approve
- approved
- confirmed
to: cancelled
by: buyer
requires: reason
gate: nothing received and nothing billed
effect: 'a confirmed order keeps its number; v8: the request it was raised from is cancelled with it (cancel_with_order)'
note: received, billed and outstanding are DERIVED from receipt and bill rows; part_received is not a state. A stored status and a receipt table disagree the first time someone returns goods.
readiness:
- transition: confirm_purchase_order
gate: authority
reason: the total is above the order threshold and no approval covers it
fix: send it for approval (PUR-09)
- transition: confirm_purchase_order
gate: units_convert
reason: a line's unit has no exact conversion to the product's base unit
fix: 'Product: add the unit with its conversion'
- transition: confirm_purchase_order
gate: purchase_rounding
reason: 'a line is not a multiple (or is below the minimum) of its purchase unit''s rule: COLA-075 is bought in multiples of 80 PK24'
fix: order the next valid quantity (shown), or ask a second person to accept it with a reason (PUR-37)
- transition: confirm_purchase_order
gate: pricing_unit_converts
reason: a line's order price unit has no exact conversion to the product's base unit
fix: 'Product: add the unit, or price the line in its order unit'
- transition: accept_rounding
gate: second_person
reason: you raised this order (same email or same sign-in)
fix: another budget holder
- transition: confirm_purchase_order
gate: number_series
reason: no order number series for this year
fix: 'Settings: number format (owner)'
- transition: approve_order
gate: within_limit
reason: the total is above your limit in force today
fix: an approver with a higher limit
- transition: approve_order
gate: second_person
reason: you raised this order (same email or same sign-in)
fix: another approver
- transition: approve_order_level
gate: second_person
reason: you raised this order, or approved an earlier level (same email or same sign-in)
fix: another approver
- transition: confirm_purchase_order
gate: source_allowed
reason: the supplier is not on the product's source list, or is blocked on the order date
fix: pick an allowed supplier, or ask the buyer who keeps the list (PUR-44)
- transition: confirm_purchase_order
gate: within_agreement
reason: the release is more than what is left on the contract (or the contract has expired)
fix: release up to what is left, or revise the contract (a new revision approved)
- transition: confirm_purchase_order
gate: asset_assignable
reason: the asset does not exist, is of another company, is retired, or the line is below the capitalisation threshold
fix: pick another asset or class, or expense the line (cost_centre)
- transition: confirm_purchase_order
gate: components_listed
reason: a subcontract line has no components
fix: list what we provide to the supplier
- transition: provide_components
gate: within_need
reason: more than the line needs of this component
fix: lower the quantity, or give the reason (scrap allowance)
- transition: amend_purchase_order
gate: not_below_received
reason: a line would fall below what was received or billed
fix: return the goods first (PUR-26) or ask the supplier for a credit
- transition: cancel_purchase_order
gate: nothing_received
reason: goods were received or billed against it
fix: close it short instead (PUR-08)
- entity: receipt
process: procure
states:
- recorded
- handed_over
terminal:
- handed_over
transitions:
- key: record_goods_receipt
name: Record what arrived
from: none
to: recorded
by: receiver
gate: a confirmed order; the delivery date is today or earlier; each quantity is above zero
effect: keyed rcpt:<purchase_order_id>:<delivery_note>; quantity also kept in the base unit
- key: record_return
name: Return goods to the supplier
from: none
to: recorded
by: receiver
requires: reason
gate: no more than was received on the receipt it names
effect: a reversing receipt row (kind return) dated today; the original receipt untouched
- key: hand_to_inventory
name: Tell inventory what arrived and where
from: recorded
to: handed_over
by: system
gate: inventory is installed
effect: 'inventory''s POST /receipts (a return: POST /returns-to-supplier) per line, keyed move_ref pur:<receipt_id>:<line_no>; {move_id, quantity_base} answered and the move id kept on the line; a consignment line names stock_owner_bp_id, a subcontract line consume_provided, a line with planned delivery costs a unit cost including them (v4)'
- key: no_stock_kept
name: Keep the receipt without stock
from: recorded
to: handed_over
by: system
gate: inventory is not installed, the line is a service, or the line is assigned to an asset, a cost centre or a project (non-valuated; v4)
note: a receipt is a fact and is never edited; a wrong receipt is corrected by a return row, and a missing one by another receipt
readiness:
- transition: record_goods_receipt
gate: not_future
reason: a delivery cannot be dated after today
fix: record it on the day it arrives
- transition: record_goods_receipt
gate: location
reason: inventory is installed and the line has no location
fix: pick the receiving location
- transition: record_return
gate: within_received
reason: the return is more than was received on that delivery
fix: lower the quantity
- entity: bill
process: payables
states:
- draft
- recorded
- approved
- disputed
- cancelled
terminal:
- approved
- cancelled
transitions:
- key: ai_read_bill
name: AI reads the supplier's PDF into a draft (suggest)
from: none
to: draft
by: system
gate: a PDF was received and the step is on
effect: each field a suggestion with its evidence, in pur_ai_finding
- key: accept_ai_fields
name: Accept or correct each read field
from: draft
to: draft
by: payables_clerk
effect: each decision recorded; nothing is recorded without the clerk
- key: record_bill
name: Record the supplier's invoice or credit
from:
- none
- draft
to: recorded
by: payables_clerk
gate: supplier, their number and date, lines against order lines, consignment withdrawals or delivery-cost conditions (or none for a two-way service bill); each line's cis_part for a CIS subcontractor (v4)
effect: keyed (bp_id, supplier_invoice_no, invoice_date); tax asked of the tax module; the match computed
- key: record_intercompany_bill
name: Record the bill another of our companies sent (PUR-64)
from: none
to: recorded
by: system
gate: the sending company and the receiving company are both companies of these books; bill_id ic:<invoice_id> unique (a repeat answers the same bill)
effect: a bill of the receiving company with ar's lines and frozen tax mapped to purchase codes, ic_reference on every line; matched two-way to those lines
- key: ai_check_bill
name: AI checks the bill for duplicates and anomalies (suggest)
from: recorded
to: recorded
by: system
gate: the step is on
effect: a likely duplicate (same amount, near date, other number), an unusual amount or a changed bank listed for the clerk
- key: match_bill
name: Match against the order and the receipts
from: recorded
to: recorded
by: system
effect: 'per line: price (per pricing quantity of the line''s pricing unit), quantity (in the pricing unit, through the line''s fixed conversion, v3) and tax, each pass or fail with the difference as an exact fraction and the tolerance in force on the invoice date, named by its key (PP, DQ, AN, AP, ST, KW, LA, LD) for the supplier''s tolerance group; a total difference within BD accepted by the system as a small difference (v4); derived, never stored'
- key: accept_variance
name: Accept a match difference with a reason
from: recorded
to: recorded
by: payables_approver
requires: reason
approval: true
gate: the line fails; the approver is not the clerk who recorded it (email AND sub)
effect: an acceptance row (bill, line, kind, difference, who, why)
- key: dispute_bill
name: Dispute the bill
from:
- recorded
- approved
to: disputed
by: payables_clerk
requires: reason
- key: resolve_bill_dispute
name: Resolve the dispute
from: disputed
to: recorded
by: payables_clerk
requires: outcome
effect: back to recorded; an approved bill disputed and resolved returns to approved through approve_bill again (a no-op re-check)
- key: approve_bill
name: Approve the bill
from: recorded
to: approved
by: payables_approver
approval: true
gate: 'every line matches or its difference is accepted; tax contents pass; the period is open; the billed quantity still fits under what was received; v8: every cost centre on the bill''s lines is open in gl on the invoice date (GET /cost-centres/{cost_centre})'
effect: bill_no claimed; tax frozen; billed quantity claimed per order line; posted to the ledger
- key: post_bill
name: Post to the ledger
from: approved
to: approved
by: system
gate: the ledger is installed
effect: a balanced journal keyed on the bill and event approve; an asset-assigned line debits asset_clearing, a consignment line consignment_clearing, a delivery-cost line delivery_cost_clearing (v4)
- key: hand_asset_to_register
name: Hand asset lines to the asset register
from: approved
to: approved
by: system
gate: fixed-assets is installed and a line is assigned to an asset
effect: fixed-assets' POST /acquisitions per line keyed pur:<bill_id>:<line_no>; the transaction id kept on the line; a refusal listed and retried (v4, PUR-56)
- key: hand_cost_difference
name: Hand a delivery-cost difference to inventory
from: approved
to: approved
by: system
gate: a delivery-cost line differs from its planned cost and inventory is installed
effect: inventory's POST /valuation-adjustments keyed pur:<bill_id>:<line_no> (v4, PUR-63)
- key: settle_ers
name: Settle receipts by self-billing
from: none
to: approved
by: payables_clerk
gate: an active self-billing agreement on each receipt's date; order lines flagged ers; receipts not yet billed
effect: a self-billed invoice per supplier, currency and tax point, numbered from our SB series; tax computed (self_billed true) and frozen; recorded and approved by the run; posted; sent to the supplier (v4, PUR-51)
- key: cancel_bill
name: Cancel the bill
from:
- recorded
- disputed
to: cancelled
by: payables_approver
requires: reason
gate: never approved, nothing allocated
note: 'PAID is derived, like invoicing''s settlement: outstanding = total - SUM(allocations of payments and credits). The match is derived from order, receipt and bill rows plus acceptance rows. An approved bill is never edited; a correction is a supplier credit (kind credit, PUR-27). A migrated bill (source_kind odoo) is approved history: post_bill and the match never run on it; allocations do.'
readiness:
- transition: record_bill
gate: not_duplicate
reason: this supplier already has that invoice number on that date
fix: open the existing bill
- transition: record_bill
gate: order_open
reason: the order it names is cancelled
fix: ask the supplier, or record it without an order and dispute it
- transition: approve_bill
gate: match
reason: a line fails on price, quantity or tax beyond the tolerance, and no acceptance is recorded
fix: accept the difference with a reason (PUR-12), or dispute it
- transition: approve_bill
gate: not_over_billed
reason: the order line has already been billed for what was received (another bill claimed it)
fix: wait for the receipt, or dispute
- transition: approve_bill
gate: tax_content
reason: 'tax''s content check failed: the supplier''s VAT number or a required particular is missing'
fix: ask the supplier for a valid VAT invoice, or record the VAT as not reclaimable
- transition: approve_bill
gate: period_open
reason: the invoice date falls in a closed ledger period
fix: ask the accountant to reopen, or post in the next open period
- transition: approve_bill
gate: cost_centre_open
reason: line {line_no}'s cost centre {cost_centre} is unknown to the ledger, or closed on {invoice_date}
fix: pick an open cost centre (gl), or ask the controller to open it
- transition: approve_bill
gate: account_map
reason: no account in force on the invoice date for a category or tax code on the bill
fix: 'Settings: account map'
- transition: accept_variance
gate: second_person
reason: you recorded this bill (same email or same sign-in)
fix: another approver
- transition: settle_ers
gate: self_billing_active
reason: no self-billing agreement in force for this supplier on the receipt's date
fix: 'Suppliers: self-billing agreement (PUR-50), or wait for the supplier''s invoice'
- transition: record_bill
gate: cis_parts
reason: a CIS subcontractor's bill line has no labour or materials split
fix: split each line (materials only as the supplier's own cost of materials)
- entity: payment_run
process: payables
states:
- draft
- approved
- exported
- done
- cancelled
terminal:
- done
- cancelled
transitions:
- key: propose_run
name: Propose a run of what falls due
from: none
to: draft
by: payables_clerk
gate: a pay date today or later and a bank account
effect: approved, undisputed bills due by the pay date and approved down payments due by it, one payment per supplier and currency; a CIS subcontractor's payment net of the deduction on the labour share paid (tax's rate on the pay date); a bill whose order has an uncleared down payment proposed for its balance after clearing, else left out; left-out bills named (v4)
- key: approve_run
name: Approve the run
from: draft
to: approved
by: payables_approver
approval: true
gate: 'the approver is not the proposer nor the clerk who recorded any bill in it (email AND sub); every payee''s bank details are approved and in force on the pay date; for a BACS file: GBP, UK sort code and account per payee, the service user number set and the pay date at least the BACS cycle away (v4)'
- key: cancel_run
name: Cancel the run
from:
- draft
- approved
to: cancelled
by: payables_approver
requires: reason
gate: no file written
- key: export_bank_file
name: Write the bank file
from: approved
to: exported
by: system
effect: pain.001, BACS Standard 18 (v4, PUR-59) or CSV kept once as raw/purchasing/runs/<run_id>.<ext>; a second export returns the same file
- key: upload_to_bank
name: Upload the file to the bank
from: exported
to: exported
by: payables_clerk
effect: 'outside DODIL: a person signs it in the bank''s portal'
- key: confirm_paid
name: Confirm what the bank paid
from: exported
to: exported
by: payables_clerk
gate: the bank's reference; the bank's date today or earlier
effect: allocations written per bill; posted payables / bank (and CIS liability); a down payment posts supplier down payments / bank; each CIS deduction recorded with tax (v4)
- key: confirm_paid_from_bank
name: Confirm what the bank paid from the statement (PUR-66)
from: exported
to: exported
by: system
gate: 'payments matched a statement debit to this payment: the bank''s reference; the bank''s date today or earlier; the payment not already confirmed (a repeat answers the same)'
effect: 'as confirm_paid: allocations written per bill; the payment posts (post_payment)'
- key: post_payment
name: Post the payment to the ledger
from: exported
to: exported
by: system
gate: the ledger is installed
effect: journal keyed on the payment and event pay
- key: reject_payment
name: Record a payment the bank rejected
from: exported
to: exported
by: payables_clerk
requires: reason
effect: reversing allocations and a reversing journal, dated today; the bills are open again; a CIS deduction reversed with tax (v4)
- key: finish_run
name: Finish the run
from: exported
to: done
by: system
gate: every payment confirmed or rejected
readiness:
- transition: propose_run
gate: not_past
reason: a pay date cannot be before today
fix: pick today or later
- transition: approve_run
gate: second_person
reason: you proposed this run, or recorded a bill in it (same email or same sign-in)
fix: another approver
- transition: approve_run
gate: bank_details
reason: a payee has no approved bank details in force on the pay date
fix: 'Suppliers: bank details (PUR-30), or leave the payee out'
- transition: propose_run
gate: cis_verified
reason: a CIS subcontractor has no verification in force with HMRC
fix: verify them in Tax (CIS), or pay at the unmatched rate tax answers
- transition: approve_run
gate: bacs_ready
reason: 'the BACS file cannot be written: a payee without a UK sort code and account, a currency other than GBP, no service user number, or the pay date is inside the BACS cycle'
fix: fix the payee, pick pain.001, or move the pay date to the earliest BACS date shown
- transition: confirm_paid
gate: not_future
reason: a payment cannot be dated after today
fix: confirm it on the day the bank shows it
- entity: supplier_bank
process: payables
states:
- proposed
- approved
- rejected
terminal:
- approved
- rejected
transitions:
- key: propose_bank
name: Enter new bank details
from: none
to: proposed
by: payables_clerk
gate: starts after today
effect: a dated row, masked on every screen but this one
- key: ai_flag_bank_change
name: AI flags a risky bank change (suggest)
from: proposed
to: proposed
by: system
gate: the step is on
effect: a change requested by email, a new country, or just before a large run listed for the approver
- key: approve_bank
name: Approve the change
from: proposed
to: approved
by: payables_approver
approval: true
gate: the approver is not the person who entered it (email AND sub); confirmed with the supplier by a known phone number (recorded)
- key: reject_bank
name: Reject the change
from: proposed
to: rejected
by: payables_approver
requires: reason
readiness:
- transition: propose_bank
gate: starts_after_today
reason: new bank details start tomorrow at the earliest
fix: pick a later date
- transition: approve_bank
gate: second_person
reason: you entered these details (same email or same sign-in)
fix: another approver
- transition: approve_bank
gate: call_back
reason: the change has not been confirmed with the supplier on a number we already held
fix: call them and record who confirmed
- entity: rfq
process: procure
states:
- draft
- sent
- awarded
- closed
terminal:
- awarded
- closed
transitions:
- key: draft_rfq
name: Draft a request for quotation
from: none
to: draft
by: buyer
gate: an approved request, or lines with units
effect: keyed rfq:<request_id> (or rfq:<buyer>:<moment>); a second draft from the same request returns it
- key: send_rfq
name: Send it to the suppliers
from: draft
to: sent
by: buyer
gate: at least one supplier; a deadline after today; each supplier allowed by the source list
effect: rfq_no claimed from the series; one invitation per supplier with when and to whom
- key: record_quote
name: Record a supplier's quote
from: sent
to: sent
by: buyer
gate: an invited supplier
effect: a quote (rfq_id, bp_id, revision) with a price, unit and delivery days per line; after the deadline flagged late
- key: award_quote
name: Award the quote
from: sent
to: awarded
by: buyer
gate: a quote for every line awarded; a reason when it is not the cheapest
effect: the order drafted keyed rfq:<rfq_id>:<bp_id> (raise_from_quote); the other suppliers listed to be told
- key: close_rfq
name: Close without an award
from:
- draft
- sent
to: closed
by: buyer
requires: reason
readiness:
- transition: send_rfq
gate: deadline_after_today
reason: the deadline is today or earlier
fix: pick a later deadline
- transition: send_rfq
gate: source_allowed
reason: a supplier is blocked, or not on the source list, for a product that needs one
fix: remove the supplier, or ask for the source list to be changed
- transition: award_quote
gate: reason_if_not_cheapest
reason: the awarded quote is not the cheapest and no reason is given
fix: give the reason (delivery, quality, terms)
- entity: purchase_agreement
process: procure
states:
- draft
- to_approve
- active
- closed
terminal:
- closed
transitions:
- key: draft_agreement
name: Draft a contract or scheduling agreement
from: none
to: draft
by: buyer
effect: keyed ag:<bp_id>:<kind>:<valid_from>:<supplier_ref>; kind quantity_contract | value_contract | scheduling_agreement
- key: submit_agreement
name: Send for approval
from: draft
to: to_approve
by: buyer
gate: lines with prices, a target (quantity or value), a validity starting today or later
- key: approve_agreement
name: Approve the agreement
from: to_approve
to: active
by: spend_approver
approval: true
gate: the target value is within the approver's limit in force today and the release strategy's levels are met; the approver is not the buyer (email AND sub)
effect: agreement_no claimed; the spend approval for every release within its target and validity
- key: reject_agreement
name: Send back with a reason
from: to_approve
to: draft
by: spend_approver
requires: reason
- key: release_against_agreement
name: Release against the contract
from: active
to: active
by: buyer
gate: within validity and what is left (claims)
effect: a release order's lines claim the agreement lines (pur_agreement_claim) at confirm_purchase_order
- key: maintain_schedule
name: Update the delivery schedule
from: active
to: active
by: buyer
gate: a scheduling agreement; lines dated today or later; a line released JIT inside the firm zone changes only with the supplier's agreement (reason)
effect: a new schedule revision; the previous kept
- key: send_schedule_release
name: Send a forecast or JIT release
from: active
to: active
by: buyer
gate: a scheduling agreement with schedule lines
effect: release_no claimed per agreement, the lines frozen in it; jit opens a call-off order (open_call_off); forecast commits nothing
- key: revise_agreement
name: Revise the target or prices
from: active
to: to_approve
by: buyer
gate: the target or a price is raised
effect: a new revision; releases continue under the approved one until the new one is approved
- key: close_agreement
name: Close the agreement
from: active
to: closed
by: buyer
requires: reason
- key: expire_agreement
name: The agreement ends
from: active
to: closed
by: system
gate: valid_to is before today (the business date)
note: released, remaining and the call-offs are DERIVED from claims and releases, never counters
readiness:
- transition: approve_agreement
gate: second_person
reason: you drafted this agreement (same email or same sign-in)
fix: another approver
- transition: approve_agreement
gate: within_limit
reason: the target is above your limit in force today
fix: an approver with a higher limit
- transition: release_against_agreement
gate: within_agreement
reason: the release is more than is left, or the agreement is outside its validity
fix: release what is left, or revise the agreement
- transition: maintain_schedule
gate: firm_zone
reason: a JIT-released line inside the firm zone would change
fix: record the supplier's agreement as the reason
- entity: down_payment
process: payables
states:
- requested
- approved
- paid
- cleared
- refunded
- cancelled
terminal:
- cleared
- refunded
- cancelled
transitions:
- key: request_down_payment
name: Request a down payment on the order
from: none
to: requested
by: buyer
gate: a confirmed order; requests together not above the order total
effect: keyed dp:<purchase_order_id>:<seq>; dp_no claimed; net, tax code and gross; a noted item, not posted
- key: approve_down_payment
name: Approve the down payment
from: requested
to: approved
by: payables_approver
approval: true
gate: the approver is not the requester (email AND sub)
effect: proposed in the run of its due date
- key: pay_down_payment
name: The down payment is paid
from: approved
to: paid
by: system
gate: its payment confirmed (in a run or outside one)
effect: posted debit supplier down payments, credit bank
- key: record_down_payment_invoice
name: Record the supplier's VAT invoice for it
from: paid
to: paid
by: payables_clerk
gate: the supplier's VAT invoice for the down payment
effect: a bill of kind down_payment_invoice; at approval its VAT moved from supplier down payments to input VAT
- key: clear_down_payment_part
name: Clear part against a bill
from: paid
to: paid
by: payables_clerk
gate: a bill of the same order; not above what remains nor the bill's outstanding
effect: a clearing row claiming the down payment; an allocation of source_kind down_payment on the bill; the clearing line carries the down payment's tax code
- key: clear_down_payment_rest
name: Clear the rest against a bill
from: paid
to: cleared
by: payables_clerk
gate: what remains fits the bill's outstanding
effect: as clear_down_payment_part, leaving nothing
- key: refund_down_payment
name: Record the supplier's refund
from: paid
to: refunded
by: payables_clerk
requires: bank_reference
gate: the order is closed or cancelled
effect: debit bank, credit supplier down payments for what remains; dated by the bank, today or earlier
- key: cancel_down_payment
name: Cancel the request
from:
- requested
- approved
to: cancelled
by: buyer
requires: reason
gate: not paid and not in an approved run
readiness:
- transition: request_down_payment
gate: within_order_total
reason: down payments on the order would exceed its total
fix: lower it to what remains (shown)
- transition: approve_down_payment
gate: second_person
reason: you requested this down payment (same email or same sign-in)
fix: another approver
- transition: clear_down_payment_part
gate: not_over_cleared
reason: more than remains on the down payment, or more than the bill's outstanding (another clearing claimed it)
fix: lower the amount
- transition: refund_down_payment
gate: order_finished
reason: 'the order is still open: clear the down payment against its bills'
fix: close the order first, or clear it
- entity: self_billing_agreement
process: payables
states:
- proposed
- active
- ended
- rejected
terminal:
- ended
- rejected
transitions:
- key: propose_self_billing
name: Record the self-billing agreement
from: none
to: proposed
by: payables_clerk
gate: the signed agreement; an end date within the owner's maximum; the supplier's VAT number where they are registered
effect: keyed (bp_id, valid_from); the PDF kept as an object
- key: approve_self_billing
name: Approve the agreement
from: proposed
to: active
by: payables_approver
approval: true
gate: the approver is not the person who recorded it (email AND sub)
- key: reject_self_billing
name: Reject it
from: proposed
to: rejected
by: payables_approver
requires: reason
- key: end_self_billing
name: End it early
from: active
to: ended
by: payables_clerk
requires: reason
effect: ERS stops from tomorrow
- key: expire_self_billing
name: The agreement runs out
from: active
to: ended
by: system
gate: valid_to is before today (the business date)
effect: listed for renewal 30 days before
readiness:
- transition: propose_self_billing
gate: within_max_months
reason: the agreement has no end date or runs longer than the owner's maximum
fix: set an end date within the maximum (renew it then)
- transition: propose_self_billing
gate: supplier_vat_number
reason: the supplier is VAT-registered and their VAT number is not on file
fix: 'Tax: record the supplier''s VAT number'
- transition: approve_self_billing
gate: second_person
reason: you recorded this agreement (same email or same sign-in)
fix: another approver
- entity: consignment_withdrawal
process: procure
states:
- recorded
- settled
- reversed
terminal:
- settled
- reversed
transitions:
- key: take_consignment_withdrawal
name: Take a withdrawal from inventory
from: none
to: recorded
by: system
gate: a consignment price in force on the withdrawal date; the date today or earlier
effect: keyed move_ref; priced; {withdrawal_id, unit_price, amount} answered to inventory
- key: reverse_consignment_withdrawal
name: Reverse a withdrawal
from: recorded
to: reversed
by: system
gate: not settled (after settlement a credit in the next one)
- key: settle_consignment
name: Settle the withdrawals
from: recorded
to: settled
by: payables_clerk
gate: the supplier's invoice for the period matched two-way against the withdrawals, or ERS under an agreement
effect: bill lines against withdrawals; debit consignment clearing, credit payables
readiness:
- transition: take_consignment_withdrawal
gate: consignment_price
reason: no consignment price in force for this supplier and product on the withdrawal date
fix: 'Suppliers: record the consignment price; the stock stays the supplier''s'
- entity: migration_run
process: payables
states:
- loaded
- reconciled
- signed
- discarded
terminal:
- signed
- discarded
transitions:
- key: load_purchasing_from_odoo
name: Load the Odoo history
from: none
to: loaded
by: system
gate: the doors are decided; business-partner, product and tax have mapped every partner, product, unit and purchase tax used
effect: documents written read-only, keyed odoo:<db>:<model>:<id>; a re-run changes nothing (PUR-34)
- key: reconcile_purchasing_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_purchasing_difference
name: Explain each difference
from: reconciled
to: reconciled
by: payables_approver
requires: reason
gate: a check is not equal and may be explained
- key: reload_purchasing_migration
name: Load again after a fix
from:
- loaded
- reconciled
to: loaded
by: system
gate: not signed
- key: sign_purchasing_migration
name: Sign the reconciliation
from: reconciled
to: signed
by: owner
approval: true
gate: every check equal, or explained where explained is allowed
effect: who (email and sub), when and the run's checksums recorded; purchasing takes writes for these suppliers from now
- key: discard_purchasing_migration
name: Discard the run
from:
- loaded
- reconciled
to: discarded
by: owner
requires: reason
readiness:
- transition: load_purchasing_from_odoo
gate: doors_decided
reason: a migration door is undecided
fix: 'Migration: answer it'
- transition: load_purchasing_from_odoo
gate: partners_mapped
reason: Odoo suppliers used have no bp_id
fix: run the business-partner migration first
- transition: load_purchasing_from_odoo
gate: products_mapped
reason: Odoo products or units used have no product_id or unit_code
fix: run the product migration first
- transition: load_purchasing_from_odoo
gate: purchase_taxes_mapped
reason: Odoo purchase taxes used have no tax_code
fix: run the tax migration with purchase taxes included
- transition: load_purchasing_from_odoo
gate: not_signed
reason: 'this run is signed: signed history is never rewritten'
fix: start a new run for what changed after cutover
- transition: sign_purchasing_migration
gate: zero_unexplained
reason: 'checks neither equal nor explained: named'
fix: explain each, or fix and reload
- transition: sign_purchasing_migration
gate: must_equal
reason: a check that must be equal differs
fix: fix the mapping and reload