dodil/erp-brick-sales-order
DODIL ERP brick: sales order
src/brick_sales_order/application/process.yaml
# The brick's process — module.yaml's workflows (v10), copied (pattern.md §15). SO.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 (sales-order v10, seemo 2026-10-25, erp-method v0.1.38). Regenerated from the signed
# module.yaml, never edited by hand: tests/test_process.py compares it with the spec.
id: sales-order
name: Sales orders
version: 10
actors:
- id: sales_rep
name: Sales
does: creates orders of any type (from a quote or by hand), confirms them, amends them before delivery, raises returns (RMAs) and credit or debit memo requests, tells the customer the promised dates
sees: the orders they own; a holder of so:sales_order:read_all sees every order
permissions:
- so:order:read
- so:order:write
- so:order:confirm
- id: order_admin
name: Sales operations / the owner
does: approves amendments and cancellations of confirmed orders, overrides a credit hold, sets and releases delivery and billing blocks, keeps order types, condition records and rebate agreements, asks for a rebate settlement, sets order policy
sees: every order
permissions:
- so:order:read
- so:sales_order:read_all
- so:order:approve
- so:order:cancel
- so:order:block
- so:pricing:write
- so:rebate:write
- so:settings:write
- id: sales_controller
name: A second person for money going back (the finance controller, the owner)
does: approves credit and debit memo requests, free-of-charge orders above the limit and rebate settlements; never one they raised or set up (compared by email AND sign-in subject)
sees: every order, condition and rebate agreement with its accruals
permissions:
- so:order:read
- so:sales_order:read_all
- so:memo:approve
- so:rebate:approve
- id: owner
name: The owner or a finance controller
does: sets the controls on order takers (whether amendments need approval, whether an accepted quote confirms itself, the business time zone, which AI steps are on), each from a later date
sees: every order
permissions:
- so:order:read
- so:sales_order:read_all
- so:settings:controls
- id: fulfiller
name: Whoever delivers (operations, the project lead, the warehouse)
does: records what has been delivered or done, line by line
sees: confirmed orders to deliver; never prices
permissions:
- so:order:read
- so:order:deliver
- id: biller
name: Whoever invoices
does: asks for the invoice of what is due, raises the deposit
sees: every confirmed order
permissions:
- so:order:read
- so:sales_order:read_all
- so:order:invoice
- id: customer
name: The customer
external: true
does: sends their purchase order, receives the confirmation
sees: their own order confirmation, by email or the portal
permissions: []
- id: system
name: The system
does: creates the order on acceptance, suggests a draft from a PO and compares a PO with the order, derives fulfilment and invoice status, numbers confirmed orders, locks finished orders, re-promises unconfirmed quantities, accrues rebates, records return inspections and supplier shipments reported by inventory and purchasing, imports the Odoo orders once
sees: everything, through its own service account
permissions:
- so:order:read
- so:order:intake
- so:order:suggest
- so:order:finish
- so:number_series:claim
- so:order:import
- so:order:reschedule
- so:rebate:accrue
- so:return:inspect
- so:demand:read
- so:order:deliver
workflows:
- entity: sales_order
states:
- draft
- confirmed
- done
- cancelled
terminal:
- done
- cancelled
transitions:
- key: order_from_quote
name: Create the order from the accepted quote
from: none
to: draft
by: system
gate: 'a quote version was accepted; readiness: priced_by_product (every quote line priced by product''s find_price, v5)'
effect: type STANDARD; partners defaulted (v5); lines copied, keyed quote:{quote_id}@v{version}; a second hand-over returns the same order
- key: draft_by_hand
name: Raise an order by hand
from: none
to: draft
by: sales_rep
gate: 'ordered_on is today in business_timezone; every unit is one the product sells in; an order type (v5); readiness: partners_valid (sold-to, ship-to, bill-to, payer, v5), reference_required (returns, memo requests, consignment issue: the order line or invoice referenced, v5)'
effect: each line's item category determined from its type and the product's type (v5), priced by the pricing procedure, the BASE condition from find_price (priced_on today); taxed by the tax module for the ship-to
- key: ai_order_from_po
name: AI suggests the order from the customer's PO (suggest)
from: none
to: draft
by: system
gate: a purchase order arrived and the owner turned the step on
effect: 'an empty draft with a suggestion: lines matched to product and unit, the PO number, differences from the quote; nothing on the order yet'
- key: accept_order_suggestion
name: Accept, change or reject the AI suggestion
from: draft
to: draft
by: sales_rep
effect: accepted lines priced by the pricing procedure; what was accepted, changed or rejected, by whom, logged in so_suggestion
- key: customer_sends_po
name: The customer sends their purchase order
from: draft
to: draft
by: customer
gate: the customer requires a PO
- key: ai_check_po
name: AI compares the PO with the order (suggest)
from: draft
to: draft
by: system
gate: a PO document is attached
effect: price, quantity and term differences listed for the rep
- key: price_order
name: Price the lines and the header (pricing procedure)
from: draft
to: draft
by: sales_rep
gate: 'readiness: base_price_found (find_price answered every line), manual_within_limit (a typed condition within its type''s limit), freight_computable (every line''s weight known where FREIGHT applies)'
effect: line_condition rows per line and the header's shares, each naming its record; nets rounded once and summed exactly (v5)
- key: promise_dates
name: Check availability and promise dates (ATP)
from: draft
to: draft
by: system
gate: inventory installed and the line is stocked (else the requested date is kept and said so)
effect: 'schedule lines per line from inventory''s GET /atp: confirmed quantities and dates (v5); a shortfall never blocks'
- key: review_order
name: Review lines
prices and tax: null
from: draft
to: draft
by: sales_rep
- key: confirm_sales_order
name: Confirm the order
from: draft
to: confirmed
by: sales_rep
gate: 'readiness all met: customer_active, lines_priced (every line has priced_from), units_valid, lines_taxed, credit (the PAYER not on hold, or an admin overrode it with a reason; mode warn is met with its reason; skipped for CASH, v5), po_ref (where the customer''s policy on that date requires one), no_open_suggestion, delivery_multiple (v4: each line a multiple of its product''s delivery rule; mode warn is met with its reason, block is not), incomplete:* (v5: every field the type''s incompletion rules require for confirmation), site_allows_sale (v5: product site status at the delivering warehouse), plan_complete (v5: milestones total 100%), reference_required (v5)'
effect: numbered from the order type's series (v5); prices, conditions and tax frozen; repeating lines handed to invoicing as schedules; with inventory installed, its goods lines reserved with their schedule lines (POST /reservations/sales-orders/{sales_order_id}, revision 1) and each line's stocked flag kept from the answer; third-party lines sent to purchasing as purchase requests (v5); RUSH asks for a delivery today; CASH has its delivery and invoice made at once; CREDIT_REQ, DEBIT_REQ and FREE_OF_CHARGE above the limit take the type's APPROVAL block; RETURNS follow the return_order workflow
- key: cancel_draft_order
name: Cancel the draft
from: draft
to: cancelled
by: sales_rep
- key: send_confirmation
name: Send the order confirmation
from: confirmed
to: confirmed
by: sales_rep
effect: the document from the frozen order, with the promised dates per schedule line (v5); the send recorded
- key: receive_confirmation
name: The customer receives the confirmation
from: confirmed
to: confirmed
by: customer
- key: approve_memo_request
name: Approve a credit or debit memo request, or a free-of-charge order
from: confirmed
to: confirmed
by: sales_controller
approval: true
gate: 'readiness: approver_not_requester (neither the rep who raised it nor anyone with the same email or sign-in subject), within_invoiced (a credit never above the invoice line''s net less earlier credits)'
effect: the APPROVAL block released; a credit request asks invoicing for a credit note against the referenced invoice, a debit request for a draft; a free-of-charge order may be delivered (v5)
- key: set_order_block
name: Set a delivery or billing block
from: confirmed
to: confirmed
by: order_admin
requires: reason
gate: a block reason of the right kind (delivery | billing)
effect: a block row appended; the fulfilment answer or the invoice request stops at it (v5)
- key: release_order_block
name: Release a block
from: confirmed
to: confirmed
by: order_admin
requires: reason
gate: the block is set; an APPROVAL block only by approve_memo_request
effect: a release row appended
- key: reschedule_order
name: Re-promise open quantities (daily)
from: confirmed
to: confirmed
by: system
gate: an open schedule line not yet delivered; once per business date
effect: a new schedule version with the reason; a later date raises a task for the owner; the reservation re-sent with the same revision and the new schedule version (v5)
- key: raise_deposit
name: Raise the deposit invoice
from: confirmed
to: confirmed
by: biller
gate: 'readiness: deposit_set (the order''s deposit percentage is above zero), within_total (deposits never exceed the order total); not CASH'
effect: invoicing raises a deposit draft; later invoices deduct it in part or in full (v5)
- key: record_delivery
name: Record what was delivered or done
from: confirmed
to: confirmed
by: fulfiller
gate: delivered_on not after today nor before the confirmation; no line beyond its ordered quantity plus its over-delivery tolerance (v5); a correction never below what is invoiced; the quantity converts exactly to the base unit (v3); with inventory installed, a stocked line only with source inventory (v3); a third-party line only with source purchasing (v5); max_partials (v5); no delivery block (v5)
effect: delivered quantities per line, with the base quantity beside them, a correction as a new row; a line within its under-delivery tolerance is closed (v5); fulfilment status derived
- key: inventory_delivers
name: Inventory delivers a stocked line
from: confirmed
to: confirmed
by: system
gate: inventory installed; the line is stocked; delivery_ref inv:<delivery number>
effect: record_delivery with source inventory, called inside inventory's validation; a repeat with the same delivery_ref records once
- key: supplier_delivers
name: The supplier ships a third-party line
from: confirmed
to: confirmed
by: system
gate: the line's item category is THIRD_PARTY; delivery_ref pur:<shipping notice>
effect: record_delivery with source purchasing, called by purchasing; a repeat records once (v5)
- key: release_milestone
name: Release a billing milestone
from: confirmed
to: confirmed
by: fulfiller
gate: the milestone's event happened (or its date is today or past); not released before
effect: the milestone's billing block released; its value becomes due (v5)
- key: request_invoice
name: Ask invoicing for what is due
from: confirmed
to: confirmed
by: biller
gate: 'readiness: something_due (a line has quantity or milestone value to invoice, per its policy), billing_block (none set, v5), incomplete:* (the fields the type requires for billing, v5), billing_relevant (not a FREE_OF_CHARGE or CONS_FILL order, v5), deduction_within_remaining (no deposit deducted beyond what is left, v5)'
effect: one draft request with its deposit deductions per deposit, each carrying the deposit's tax category (v5); invoice status derived
- key: amend_sales_order
name: Amend the order
from: confirmed
to: confirmed
by: sales_rep
approval: true
requires: reason
gate: 'readiness: not_done, not_below_done (no line below what is delivered or invoiced), approver_not_amender (where amendment_needs_approval), delivery_multiple (v4, on changed lines), partners_unchanged_after_delivery (v5)'
effect: revision n+1; only changed lines re-priced by the pricing procedure (priced_on today), the header shares re-spread; changed lines re-promised; a new confirmation can be sent; with inventory installed, the reservation re-sent with revision n+1
- key: finish_sales_order
name: Lock the finished order
from: confirmed
to: done
by: system
gate: every line delivered (or closed within tolerance) and invoiced in full, or not billing-relevant
effect: edits refused
- key: cancel_sales_order
name: Cancel the confirmed order
from: confirmed
to: cancelled
by: order_admin
requires: reason
approval: true
gate: 'readiness: nothing_delivered, nothing_invoiced, third_party_cancellable (purchasing agreed, v5)'
effect: drafts it requested are cancelled in invoicing; an unused deposit is credited (v5); with inventory installed, the reservation released (POST /reservations/sales-orders/{sales_order_id}/release)
note: 'Fulfilment status (not started, partly delivered, delivered) and invoice status (nothing to invoice, to invoice, partly invoiced, invoiced) are DERIVED from the quantities on read; nothing stores them. For the screens and agents, the brick answers each order''s readiness for its next transitions as a list of {gate, met, reason, fix}: the gate''s key (as named in each transition''s gate), met true or false, the reason in words ("this customer requires a PO number"), and a fix (the field or the person who can clear it), so no screen or agent re-derives a rule. "Today" is the date in business_timezone. v5: the order TYPE decides which transitions apply (a FREE_OF_CHARGE order is never invoiced, a CREDIT_REQ never delivered, a RETURNS order follows the return_order workflow), and the incompletion rules add their incomplete:<field> gates to confirm, delivery (the fulfilment answer) and billing.'
readiness:
- transition: order_from_quote
gate: priced_by_product
reason: a quote line was priced from the quote's own price book, not by product
fix: re-price the quote from product (PRD-20) before it is sent
- transition: draft_by_hand
gate: partners_valid
reason: a ship-to, bill-to or payer is not one of the sold-to's partners in force today, or is not an active customer
fix: 'Settings: customer partners, or pick another'
- transition: draft_by_hand
gate: reference_required
reason: this order type names the order line or invoice it refers to
fix: pick the original order line or invoice
- transition: price_order
gate: base_price_found
reason: product has no price for this product, unit, customer and currency today
fix: ask the pricing manager to add one (Price lists)
- transition: price_order
gate: manual_within_limit
reason: a condition typed by hand is beyond its type's manual limit
fix: stay within the limit, or ask an order admin to change the condition record
- transition: price_order
gate: freight_computable
reason: a line's unit has no gross weight and freight is charged by weight
fix: record the unit's weight in Products
- transition: confirm_sales_order
gate: site_allows_sale
reason: the product is blocked for sale at the delivering warehouse
fix: 'deliver from another warehouse, or ask the planner (Products: sites)'
- transition: confirm_sales_order
gate: plan_complete
reason: the milestones of a line do not add up to 100%
fix: 'Order: billing plan'
- transition: approve_memo_request
gate: approver_not_requester
reason: 'you raised this request (same email or sign-in subject): a second person approves'
fix: ask a sales controller
- transition: approve_memo_request
gate: within_invoiced
reason: the credit is above what is left of the invoice line after earlier credits
fix: reduce it to the amount left
- transition: record_delivery
gate: max_partials
reason: the customer accepts at most this many part deliveries per line
fix: deliver the rest in one, or change the order's rule before delivery
- transition: record_delivery
gate: within_tolerance
reason: more than ordered plus the over-delivery tolerance
fix: deliver no more than the tolerance allows
- transition: request_invoice
gate: billing_block
reason: the order has a billing block
fix: an order admin releases it
- transition: request_invoice
gate: deduction_within_remaining
reason: a deposit has less left than the deduction asked
fix: deduct what is left
- transition: cancel_sales_order
gate: third_party_cancellable
reason: purchasing refused to cancel the purchase for a third-party line (named)
fix: ask the buyer to cancel it with the supplier first
- entity: return_order
states:
- draft
- authorised
- received
- closed
- cancelled
terminal:
- closed
- cancelled
transitions:
- key: raise_return
name: Raise the return
from: none
to: draft
by: sales_rep
gate: 'readiness: reference_required (the original order line, its delivery and invoice), within_delivered (no more than delivered less already returned on that line), return_reason'
effect: a sales_order of type RETURNS; each line copies the original line's price, unit and tax category, never re-priced
- key: authorise_return
name: Authorise the return (RMA)
from: draft
to: authorised
by: sales_rep
gate: 'readiness: incomplete:* (the RETURNS type''s rules)'
effect: 'numbered RE-{yyyy}-{seq:4}: the RMA the customer quotes; the confirmation sent; inventory told to expect it (POST /expected-returns)'
- key: receive_returned_goods
name: The goods come back
from: authorised
to: received
by: system
gate: inventory received them against the RMA; a serial is one delivered to this customer
effect: record_delivery on the return line with source inventory (positive, received)
- key: record_return_inspection
name: Inventory reports the inspection
from: received
to: received
by: system
gate: accepted + rejected never above received
effect: accepted and rejected quantities per line with the decision (restock, scrap, to supplier) and reason (POST /sales-orders/{sales_order_id}/return-inspections)
- key: request_return_credit
name: Ask invoicing for the credit
from: received
to: received
by: system
gate: 'readiness: credit_point_reached (return_credit_on receipt, or the line inspected), within_invoiced (never above the original invoice line''s net less earlier credits)'
effect: one credit note against the original invoice for what is accepted, less a restocking fee where the reason has one, keyed so:{sales_order_id}:{request_no}, its lines carrying the returns order's line
- key: close_return
name: Close the return
from: received
to: closed
by: system
gate: every line inspected, and credited or rejected
effect: edits refused
- key: cancel_return
name: Cancel the return
from:
- draft
- authorised
to: cancelled
by: sales_rep
requires: reason
gate: nothing received
effect: inventory's expected return withdrawn
note: 'a return never changes the original order: it is its own order (type RETURNS) whose lines reference the original line, delivery and invoice; returned and credited quantities per original line are derived from the returns orders that name it'
readiness:
- transition: raise_return
gate: within_delivered
reason: more than was delivered on that line less what was already returned
fix: return at most the quantity named
- transition: raise_return
gate: return_reason
reason: a return needs its reason
fix: pick a return reason
- transition: request_return_credit
gate: credit_point_reached
reason: the goods are not yet inspected (credit on inspection)
fix: wait for inventory's inspection
- entity: rebate_agreement
states:
- draft
- active
- ended
- settled
- cancelled
terminal:
- settled
- cancelled
transitions:
- key: open_rebate_agreement
name: Open a rebate agreement
from: none
to: draft
by: order_admin
gate: a customer or price group; a basis (products, categories or all); a validity period
effect: the agreement and its scale, editable
- key: activate_rebate_agreement
name: Activate the agreement
from: draft
to: active
by: order_admin
gate: 'readiness: starts_after_today, scale_valid (tiers ascending, rates from 0 to 100 percent)'
effect: invoices from its start accrue against it
- key: accrue_rebate
name: Accrue on issued invoices (nightly)
from: active
to: active
by: system
gate: an invoice or credit note in the basis issued on the business date
effect: accrual rows keyed (agreement, document, line) and the accrual journal posted to the ledger
- key: request_partial_settlement
name: Ask for a partial settlement
from: active
to: active
by: order_admin
requires: reason
effect: 'a settlement draft: the rate reached so far on the basis so far, less settled'
- key: approve_partial_settlement
name: Approve the partial settlement
from: active
to: active
by: sales_controller
approval: true
gate: 'readiness: approver_not_requester (by email and sign-in subject)'
effect: a credit note in invoicing keyed rebate:{agreement_id}:{settlement_no}; the accrual released by its amount
- key: end_rebate_agreement
name: The agreement period ends
from: active
to: ended
by: system
gate: valid_to is before today
effect: no further accruals; the final basis fixed once the last day's invoices are accrued
- key: request_final_settlement
name: Ask for the final settlement
from: ended
to: ended
by: order_admin
effect: the tier reached on the whole basis, retroactive, less what partial settlements paid
- key: approve_final_settlement
name: Approve the final settlement
from: ended
to: settled
by: sales_controller
approval: true
gate: 'readiness: approver_not_requester'
effect: the accrual trued up in the ledger, one credit note in invoicing, the accrual account for the agreement nets to 0.00
- key: cancel_rebate_agreement
name: Cancel a draft agreement
from: draft
to: cancelled
by: order_admin
readiness:
- transition: activate_rebate_agreement
gate: starts_after_today
reason: an agreement starts tomorrow at the earliest (business time zone)
fix: pick a later start
- transition: activate_rebate_agreement
gate: scale_valid
reason: the tiers are not ascending, or a rate is outside 0 to 100 percent
fix: correct the scale
- transition: approve_final_settlement
gate: approver_not_requester
reason: you asked for this settlement or set the agreement up (same email or sign-in subject)
fix: ask a sales controller