dodil/erp-brick-inventory
DODIL ERP brick: inventory
src/brick_inventory/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: inventory
name: Stock & warehouses
version: 6
actors:
- id: warehouse_manager
name: Whoever runs the warehouse (operations lead, the owner in a small firm)
does: sets up warehouses, zones and bins, putaway rules and pick sequence, decides which products are tracked by lot or serial and how lots are picked, plans transfers, batches deliveries, starts counts (from the cycle count plan) and applies small adjustments, changes stock types and blocks lots, accepts replenishment proposals, reserves stock by hand for a priority order
sees: every warehouse, location, document and move; quantities and the value of a count's difference
permissions:
- inv:stock:read
- inv:warehouse:write
- inv:product:write
- inv:receipt:validate
- inv:delivery:validate
- inv:transfer:write
- inv:count:write
- inv:adjustment:apply
- inv:reservation:write
- inv:move:reverse
- inv:issue:write
- inv:stock_type:write
- inv:replenishment:write
- inv:settings:write
- id: stock_operator
name: The person on the floor (picker, packer, goods-in)
does: receives goods and returns, inspects returns, puts away, picks, packs and ships deliveries, moves stock between bins, issues stock to a cost centre or scraps it, records what a count found
sees: products, locations, quantities and the documents of their warehouse; never a cost or a value
permissions:
- inv:stock:read
- inv:receipt:validate
- inv:delivery:validate
- inv:transfer:write
- inv:count:write
- inv:issue:write
- id: stock_controller
name: A second person for large differences (finance controller, the owner)
does: approves a count adjustment, a goods issue or a scrap whose value is over the threshold, and a transit shortfall written off; never one they counted, submitted or requested
sees: every count, issue and scrap, its difference and its value
permissions:
- inv:stock:read
- inv:valuation:read
- inv:adjustment:approve
- inv:issue:approve
- id: owner
name: The owner
does: sets the controls on the warehouse (negative stock, the approval thresholds, lots and serials, the ledger mode, the valuation method per company, whether replenishment sends on its own, whether expired stock may be picked, the AI step's mode)
sees: everything
permissions:
- inv:stock:read
- inv:valuation:read
- inv:settings:controls
- id: accountant
name: Finance (bookkeeper, outside accountant)
does: reads stock value per product and company, the average or standard cost, and the journals posted for each movement; keeps the account map and the standard prices
sees: quantities, values and postings; changes no stock
permissions:
- inv:stock:read
- inv:valuation:read
- inv:account_map:write
- inv:valuation:write
- id: sales_rep
name: Sales
does: asks what is available before promising a date
sees: available quantities per product and warehouse; never a cost, a value or a bin
permissions:
- inv:availability:read
- id: system
name: The system
does: posts the stock move of each receipt line and return purchasing records, takes reservations from sales orders, expects returns from them, answers ATP, derives on hand and availability, appends average or standard cost, posts valuation to the ledger, claims document numbers, classifies ABC and proposes replenishment, suggests a cause for a count difference, imports the Odoo opening stock once
sees: everything, through its own service account
permissions:
- inv:stock:read
- inv:availability:read
- inv:receipt:intake
- inv:reservation:intake
- inv:return:expect
- inv:valuation:post
- inv:number_series:claim
- inv:count:suggest
- inv:replenishment:propose
- inv:abc:classify
- inv:migration:import
- inv:migration:read
- inv:issue:intake
- inv:standard_price:intake
workflows:
- entity: receipt
states:
- scanned
- done
terminal:
- done
transitions:
- key: scan_delivery
name: Scan what arrived
from: none
to: scanned
by: stock_operator
gate: 'readiness: on_order (purchasing''s GET /purchase-orders/{purchase_order_id}/expected), units_exact, tracking_complete, within_tolerance; the bin is internal and not archived'
effect: 'purchasing''s POST /purchase-orders/{purchase_order_id}/receipts called once, keyed by the delivery note: the ONE way a receipt is recorded; nothing in stock until purchasing posts each line back'
- key: receive_customer_return
name: Receive a customer return against its RMA
from: none
to: done
by: stock_operator
gate: 'readiness: rma_open (sales-order''s expected return for that customer and product), within_rma, serial_was_delivered (a serial delivered to that customer)'
effect: numbered {warehouse}/RET/{seq:5}; a move CUSTOMER -> the return location in stock type quality where the reason inspects, valued at the cost it left at; record_delivery on the returns order (v3)
- key: inspect_return
name: Inspect the returned goods
from: done
to: done
by: stock_operator
gate: the return is in quality stock; every unit decided
effect: restock (a type change to unrestricted, or a putaway), scrap (a goods issue, purpose scrap) or back to the customer at the value it came in; accepted and rejected reported to sales-order once (POST /sales-orders/{sales_order_id}/return-inspections)
- key: receive_from_source
name: Receive what another module produced or took back (v6, INV-59)
from: none
to: done
by: system
gate: 'readiness: source_kind production_receipt | by_product_receipt | pos_refund_restock; move_ref unique (a known one answers its move); units convert exactly; the location is internal; a pos_refund_restock names the move it reverses'
effect: 'numbered {warehouse}/IN/{seq:5}; a move PRODUCTION (or CUSTOMER) -> location; valued at unit_cost (production: standard or the order''s cost; by-product: its net realisable value) or, for a restock, at the reversed move''s cost; credits wip (production) or cost_of_sales (restock); a lot link per input lot named'
- key: post_receipt
name: Post the received stock
from: scanned
to: done
by: system
gate: purchasing calls POST /receipts per line, keyed move_ref pur:<receipt_id>:<line_no> (a receipt recorded on purchasing's own screen starts here)
effect: 'numbered {warehouse}/IN/{seq:5}; a move SUPPLIER -> location per line, converted exactly, valued at quantity_priced x unit_cost / pricing_quantity rounded once (v2: purchasing''s pricing unit and quantity); the unit''s weights and volume copied onto the line (v2); average cost appended; scanned lots and serials attached; the valuation posted; {move_id, quantity_base} answered'
note: 'v3: a receipt lands in WH1/IN when receipt_steps is two and a putaway transfer follows (put_away_stock); a supplier-consignment line carries the owner and no value; a subcontract receipt consumes the components at the subcontractor (INV-47). What is still to arrive is purchasing''s (its outstanding quantities), never an inventory document; a return to the supplier is purchasing''s return row posted through POST /returns-to-supplier (INV-28), valued at the receipt''s cost; a return from a customer is a receipt from CUSTOMER naming the delivery it reverses (INV-14), valued at the cost it left at'
- entity: delivery
states:
- waiting
- ready
- done
- cancelled
terminal:
- done
- cancelled
transitions:
- key: reserve_order
name: Reserve stock for the confirmed order
from: none
to: waiting
by: system
gate: a confirmed sales order with goods lines
effect: reservation rows per line at the warehouse, as much as is available (never from a quality or blocked location, v2); the shortfall stated
- key: reserve_by_hand
name: Reserve or release by hand
from: waiting
to: waiting
by: warehouse_manager
requires: reason
gate: no more than is available
effect: a reservation row appended, with who and why
- key: stock_arrives
name: Stock becomes available
from: waiting
to: ready
by: system
gate: every line reserved in full, or the order accepts part shipments
effect: the delivery appears on the pick list
- key: batch_deliveries
name: Batch deliveries into one pick
from: ready
to: ready
by: warehouse_manager
gate: deliveries of one warehouse, ready, not in another batch
effect: a pick batch {warehouse}/PB/{seq:5} with one pick list in pick_sequence order (v3)
- key: pick_goods
name: Pick from the bins
from: ready
to: ready
by: stock_operator
gate: 'each bin holds what is picked from it (unless negative stock is allowed there) and the stock picked is unrestricted (v3: by stock row); a lot or serial per tracked unit; readiness: shelf_life (the lot proposed by the removal strategy, FEFO or FIFO, with the customer''s minimum remaining shelf life; another lot only by a warehouse manager with a reason; never an expired lot unless the owner allows it); the line''s unit converts exactly to the order line''s unit'
effect: picked quantities recorded on the delivery, proposed in the warehouse unit (v2) and by lot (v3); with two-step picking, a transfer bin -> staging
- key: pack_delivery
name: Pack into packages
from: ready
to: ready
by: stock_operator
gate: contents never more than picked; a package type
effect: packages with an SSCC each (claimed, never reused), contents and gross weight (v3)
- key: validate_delivery
name: Validate the delivery
from: ready
to: done
by: stock_operator
gate: 'readiness: something_picked, stock_claimed (the claim rows on every bin and the warehouse were won), order_accepts (the sales order''s record_delivery answered ok), weights_known (v2, only with delivery_needs_weights), not_blocked (v3: the order''s fulfilment answer has no delivery block or incomplete delivery field), all_packed (v3, only with pick_pack_ship)'
effect: numbered {warehouse}/OUT/{seq:5}; gross and net weight and volume answered for shipping (v2); supplier-consignment stock picked is withdrawn first (purchasing told, v3); moves bin -> CUSTOMER at average or standard cost, or to the customer's consignment location for a CONS_FILL order (v3); the reservation consumed; the order records the delivery; the cost of goods posted to the purpose's account (sale, free_of_charge, consignment issue); what is left goes to a back-order delivery unless the order asks for complete delivery or the line closed within tolerance
- key: cancel_delivery
name: Cancel the delivery
from: ready
to: cancelled
by: warehouse_manager
requires: reason
gate: nothing on it validated
effect: its reservations released
note: 'a delivery is undone only by a return receipt (INV-14). Two validations for the last unit: one wins the claim row, the other is refused and writes nothing (INV-08)'
- entity: transfer
states:
- draft
- in_transit
- done
- cancelled
terminal:
- done
- cancelled
transitions:
- key: plan_transfer
name: Plan the move
from: none
to: draft
by: warehouse_manager
gate: both locations internal, of one company (a subcontractor's or a customer's consignment location of the company counts as one, v3)
effect: a draft transfer; between an unrestricted and a quality or blocked location it is a release or a block (v2); a transfer to a subcontractor provides components (v3); from a replenishment proposal, the supplying site's stock (v3)
- key: put_away_stock
name: Put away from goods-in
from: draft
to: done
by: stock_operator
gate: 'readiness: bin_capacity (quantity, weight and volume within the bin''s capacity, from the copied measures)'
effect: the bin proposed by the putaway rules (fixed bin, addition to stock, next bin with room), or the one chosen with who; no value change (v3)
- key: provide_to_subcontractor
name: Send components to a subcontractor (v3)
from: none
to: done
by: system
gate: purchasing's subcontract order line names them (POST /subcontract-provisions, idempotent on move_ref); the source location holds them
effect: a transfer to the subcontractor's location as stock still ours (special stock O), consumed later by the subcontract receipt
- key: change_stock_type
name: Change the stock type where it stands
from: none
to: done
by: warehouse_manager
requires: reason
gate: the location accepts the new type; the stock is there
effect: a move between types at the same location, no value change; blocking a lot changes it everywhere it is (v3)
- key: move_within_site
name: Move between bins
from: draft
to: done
by: stock_operator
gate: both bins in one warehouse; the source holds it; a lot or serial per tracked unit
effect: numbered {warehouse}/INT/{seq:5}; one move bin -> bin; no value change
- key: ship_to_site
name: Ship to the other warehouse
from: draft
to: in_transit
by: stock_operator
gate: the source holds it
effect: a move bin -> TRANSIT; counted in transit, available nowhere
- key: receive_at_site
name: Receive at the other warehouse
from: in_transit
to: done
by: stock_operator
gate: no more received than was shipped
effect: a move TRANSIT -> bin; a shortfall is left in transit until a count of TRANSIT or a loss approved by a second person is applied (gate transit_cleared, v3); expected_arrival_on was set when shipped, from the site's planned days
- key: cancel_transfer
name: Cancel the planned move
from: draft
to: cancelled
by: warehouse_manager
note: a move recorded by mistake is reversed by a new move naming it (INV-15), never edited
- entity: stock_count
states:
- counting
- submitted
- approved
- applied
- rejected
terminal:
- applied
transitions:
- key: start_count
name: Start a count
from: none
to: counting
by: warehouse_manager
gate: an internal location subtree, or a list of products over every bin that holds them (v3); no other open count on it
effect: every product (and stock type) with stock in scope listed, with no expected quantity shown to the counter
- key: record_counted
name: Record what is on the shelf
from: counting
to: counting
by: stock_operator
gate: a quantity within bounds and the unit's decimals, in any unit of the product (v2); a lot or serial for a tracked product
effect: one or more entries per line (5 PK24, 6 C62) summed exactly into counted_base; counted_at per line; expected is what the moves said at counted_at
- key: submit_count
name: Submit the count
from: counting
to: submitted
by: warehouse_manager
effect: differences and their value at average cost fixed on the count
- key: ai_explain_variance
name: AI suggests likely causes (suggest)
from: submitted
to: submitted
by: system
gate: the step is on and a line's difference is over the threshold
effect: a suggestion per line; nothing adjusted
- key: apply_small
name: Apply a count under the threshold
from: submitted
to: applied
by: warehouse_manager
gate: the sum of the absolute VALUE differences across the lines is under count_approval_threshold, and no single line's absolute value difference is at or above count_line_value_limit
effect: an adjustment move per line between the bin and LOSS; the loss or gain posted
- key: approve_count
name: A second person approves the count
from: submitted
to: approved
by: stock_controller
approval: true
gate: 'readiness: approver_not_counter (neither a counter nor the submitter, by email and by sign-in subject)'
effect: the decision recorded
- key: apply_approved
name: Apply the approved count
from: approved
to: applied
by: system
effect: adjustment moves; the loss or gain posted
- key: reject_count
name: Reject and recount
from: submitted
to: rejected
by: stock_controller
requires: reason
effect: nothing applied
- key: recount
name: Recount
from: rejected
to: counting
by: warehouse_manager
effect: the lines are recorded again
note: 'a count never sets a quantity: it appends the difference as moves to or from LOSS. Moves recorded after a line was counted are kept, because the difference is taken at counted_at'
- entity: goods_issue
states:
- draft
- awaiting_approval
- posted
- cancelled
terminal:
- posted
- cancelled
transitions:
- key: plan_goods_issue
name: Plan an issue or a scrap
from: none
to: draft
by: stock_operator
gate: 'readiness: cost_centre_valid (the ledger''s cost centre, for purpose cost_centre), scrap_reason (for purpose scrap); the product''s issue unit proposed (product PRD-29)'
effect: a draft goods issue with its purpose (cost_centre | scrap | internal_use), lines in any unit of the product
- key: post_goods_issue
name: Post the issue
from: draft
to: posted
by: stock_operator
gate: 'readiness: stock_claimed (bin and warehouse claim rows won), under_issue_threshold (the value is under issue_approval_threshold)'
effect: numbered {warehouse}/GI/{seq:5}; moves bin -> {company}:CONSUMPTION or :LOSS at average or standard cost; a journal per purpose (consumption with the cost centre on the line, scrap) posted
- key: submit_goods_issue
name: Submit an issue over the threshold
from: draft
to: awaiting_approval
by: stock_operator
gate: the value is at or above issue_approval_threshold
effect: waits for a stock controller; nothing moves
- key: approve_goods_issue
name: A second person approves the issue
from: awaiting_approval
to: posted
by: stock_controller
approval: true
gate: 'readiness: approver_not_requester (by email AND sign-in subject), stock_claimed'
effect: posted as post_goods_issue
- key: reject_goods_issue
name: Reject the issue
from: awaiting_approval
to: cancelled
by: stock_controller
requires: reason
- key: cancel_goods_issue
name: Cancel the draft issue
from: draft
to: cancelled
by: stock_operator
- key: issue_from_source
name: Take out what another module consumed (v6, INV-58)
from: none
to: posted
by: system
gate: 'readiness: source_kind pos_sale | production_issue; each move_ref unique (a known one answers its move); units convert exactly; stock_claimed; on_hand for production_issue (a pos_sale posts and may go negative, listed for the stock controller)'
effect: 'numbered {warehouse}/GI/{seq:5}; moves location -> {company}:CONSUMPTION per line at average or standard cost (the lot named, else FEFO); a journal per call: cost_of_sales (pos_sale) or wip (production_issue) / stock_valuation; {move_ref, move_id, quantity_base, value} answered'
- key: reverse_goods_issue
name: Reverse a posted issue
from: posted
to: posted
by: warehouse_manager
requires: reason
gate: not reversed before; the issue's period open in the ledger
effect: reversing moves naming the original, valued as it was, and a reversing journal (SAP 202 / 552)
readiness:
- transition: issue_from_source
gate: on_hand
reason: '{location} holds {on_hand} of {product}; the production issue needs {quantity}'
fix: move stock there first, or the owner allows negative stock at the location
- transition: plan_goods_issue
gate: cost_centre_valid
reason: the ledger has no open cost centre with that code
fix: pick a cost centre (gl)
- transition: post_goods_issue
gate: under_issue_threshold
reason: the issue's value is at or above the approval threshold
fix: submit it for a stock controller
- transition: approve_goods_issue
gate: approver_not_requester
reason: you planned or submitted this issue (same email or sign-in subject)
fix: ask another stock controller
- entity: replenishment_proposal
states:
- proposed
- sent
- dismissed
terminal:
- sent
- dismissed
transitions:
- key: propose_replenishment
name: Propose what to buy or move (nightly)
from: none
to: proposed
by: system
gate: 'a product planned at the site (product''s GET /product-sites) with procurement buy or transfer (v6: make or mrp is manufacturing''s, skipped); projected available (v6: less manufacturing''s dependent requirements in the horizon, GET /dependent-requirements, when manufacturing is installed) at or below its reorder point; once per (site, product, business date)'
effect: 'a proposal: projected figures, the site row''s valid_from, quantity to the max (min_max) or the lot size, rounded by product''s round-proposal; buy or transfer from the supplying site'
- key: accept_replenishment
name: Accept the proposal
from: proposed
to: sent
by: warehouse_manager
gate: the quantity may be changed; it still meets the purchase rule
effect: 'buy: one purchase request in purchasing (POST /purchase-requests/from-replenishment); transfer: a draft transfer from the supplying site'
- key: auto_send_replenishment
name: Send the proposal automatically (auto mode)
from: proposed
to: sent
by: system
gate: the owner's replenishment_mode is auto from a date on or before today
effect: as accept_replenishment, by the system's client
- key: dismiss_replenishment
name: Dismiss the proposal
from: proposed
to: dismissed
by: warehouse_manager
requires: reason
- entity: standard_price
states:
- scheduled
- effective
terminal:
- effective
transitions:
- key: schedule_standard_price
name: Set a standard price from a date
from: none
to: scheduled
by: accountant
gate: 'readiness: starts_after_today; the company values at standard on that date, or switches to it that day'
effect: a dated standard price per company and product (NUMERIC(19,4) per base unit)
- key: intake_standard_price
name: Take a released roll-up's standard price (v6, INV-60)
from: none
to: scheduled
by: system
gate: 'readiness: starts_after_today; values_at_standard (the company values at standard on valid_from); standard_price_taken (no other price for that company, product and date)'
effect: as schedule_standard_price, with set_by the releaser and source_ref; idempotent on (company_code, product_id, valid_from, source_ref)
- key: apply_standard_price
name: The standard price takes effect
from: scheduled
to: effective
by: system
gate: valid_from is today (business_timezone)
effect: a revaluation row of the quantity on hand at the old and new price, and its journal (stock_valuation against revaluation), journal_id derived (company, product, valid_from), posted once
readiness:
- transition: schedule_standard_price
gate: starts_after_today
reason: a standard price starts tomorrow at the earliest
fix: pick a later date
- transition: intake_standard_price
gate: standard_price_taken
reason: a standard price of {price} is already set for {valid_from} by {who}
fix: the accountant withdraws theirs, or manufacturing releases for a later date
- transition: intake_standard_price
gate: values_at_standard
reason: '{company} values at average cost on {valid_from}'
fix: the owner switches the company to standard from a later month (INV-48), or release for average cost (no standard price)