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)