Phase 4: Operations
Status: design, approved section by section by the owner 2026-09-30; not built. Branch
phase-4-design. Roadmap: Phase 4 (procurement, sales, inventory, fixed assets).
Architecture spec §19 Phase 4 (“the document chain generalized so sales reuses it unchanged”;
“costing posts through the same posting engine”; “depreciation is a River cron emitting a
posting event”) and §19.1 item 3 (inventory before payroll).
Four lanes, one plan each:
| Lane | What | Migrations | Depends on |
|---|---|---|---|
| O1 | Document chain and item master | 00700–00719 |
— |
| O2 | Stock and costing | 00720–00739 |
O1 |
| O3 | Procurement and sales | 00740–00769 |
O1, O2 |
| O4 | Fixed assets | 00770–00789 |
— |
O1 first; O2 and O4 in parallel; O3 after O1 and O2.
Owner decisions (2026-09-30)
Section titled “Owner decisions (2026-09-30)”- OD-1 Costing: one method per organization, FIFO or weighted average, locked once the first stock movement posts (MFRS 102 ¶25; a change would be an MFRS 108 policy change).
- OD-2 Negative stock: blocked.
- OD-3 Three-way match: a mismatch beyond tolerance blocks the purchase invoice; an Owner who did not prepare it may override with a reason, audited (the BR-11 shape).
- OD-4 Depreciation: book depreciation only. Capital allowance (ITA Schedule 3) stays with the tax agent and is added to the roadmap’s Needs an outside authority list.
- OD-5 Chain storage: one
trade_document/trade_linepair for every chain kind, linked by a self foreign key.
What exists (surveyed 2026-09-30)
Section titled “What exists (surveyed 2026-09-30)”- Posting seam (
platform/posting): the workflow engine dispatches anEventwith aSemantic(draft,pending,accrual,payout,acknowledged,dispute,rejected,cancelled,custom) and, onaccrual, aControlKind; accounting’sPosterwrites the entry in the same transaction. Reversal is enteringrejected/cancelledafter a posting. - Control kinds are a closed CHECK on
account.control_kindandworkflow_state.control_kind(00030,00050). - AP / AR:
purchase_invoice_lineand the sales invoice line carry account, tax code, amounts and analyticshares; no item, no quantity. platform/documentsis the file store (S3, OCR, thumbnails), not a chain. The chain lives in a newplatform/trade.- Phase 3:
moduleinstall(C1),field_schemacustom fields (C2),platform/printinglayouts (C4), the import framework (M1) and opening balances (M4). - Purchase requests (branch
fa-i-purchase-requests, spec only) are an employee’s pre-approval that becomes an expense claim. They are not the procurement PR. Its D3 proposes acommittedsemantic; this spec adopts the same one, so the two stay compatible.
O1. Document chain and item master
Section titled “O1. Document chain and item master”O1.1 Chain (platform/trade, 00700–00709)
Section titled “O1.1 Chain (platform/trade, 00700–00709)”trade_document:kind∈purchase_requisition,purchase_order,goods_receipt,purchase_return,quotation,sales_order,delivery_order,sales_return,stock_adjustment; party (supplier or customer, none for a requisition or adjustment), currency, document and due dates, number,location_id(receipts, deliveries, returns, adjustments),closed_at,close_reason, workflow instance.trade_line:item_idor a description (non-stock text line), quantity, UOM, quantity in base UOM, unit priceNUMERIC(19,4), tax code, analyticshares,source_line_id(self FK, same org).- Allowed pairs, enforced in the schema (a trigger comparing source and target kind): requisition → order → receipt → purchase return; quotation → sales order → delivery → sales return. A source line must share the target’s organization and party.
- Open quantity = base quantity − Σ live children’s base quantity. A child that would take
it below zero is refused (lock the source line
FOR UPDATE). Over-receipt means amending the order. - Copy-forward:
trade.Draft(ctx, tx, sourceDocID, targetKind)builds the target’s draft lines at each source line’s open quantity. Procurement and sales both call it; sales adds nothing to it. - Close: a document closes itself when every line’s open quantity is zero; a member closes it by hand with a reason (audited). A closed document takes no new children.
- Reversal: a receipt, delivery or return is never deleted; cancelling it through its workflow reverses its posting and its stock and gives the quantity back to its source.
- Workflow and numbering: each kind has its own seeded workflow definition and numbering
series. Requisition, order, quotation and sales order approvals end in the new semantic
committed(“approved to act on; nothing posts”). - Invoice link:
purchase_invoice_lineand the sales invoice line gain a nullabletrade_line_id— an AP line may point at a receipt line, an AR line at a delivery line.
O1.2 Item master (platform/inventory, 00710–00719)
Section titled “O1.2 Item master (platform/inventory, 00710–00719)”item: code, name and description as translation rows (en/ms/zh),type∈stock,service,non_stock; base UOM; tax code; inventory, COGS, sales and purchase accounts;tracking∈none,serial,batch; active. Retired, never deleted.uomis aplatform/registrylookup;item_uomholds each alternate UOM’s factor to base asNUMERIC(19,6).location: warehouses. No bins.- Categories:
platform/registry. Custom fields:itemjoins the C2 entities. Import: anitemimport kind on M1. - Module install:
inventory,procurementandsalesjoin C1; procurement and sales declare a dependency on inventory.
O2. Stock and costing (platform/inventory, 00720–00739)
Section titled “O2. Stock and costing (platform/inventory, 00720–00739)”O2.1 Stock ledger
Section titled “O2.1 Stock ledger”stock_move, append-only (the app role holds no UPDATE or DELETE): item, location, signed base quantity, unit cost and total costNUMERIC(19,4), move date, serial or batch, source (trade line, transfer or adjustment),journal_entry_id.- The move date is
clock.Today. No back-dated moves; with no negative stock, nothing is ever re-costed. A correction is a reversing move. stock_balance(org, item, location): updated in the same transaction as each move under a row lock, withCHECK (on_hand >= 0)— OD-2 enforced by the database.- Transfers between locations: two moves, no workflow, no journal entry, cost unchanged.
O2.2 Costing
Section titled “O2.2 Costing”company.costing_method∈fifo,weighted_average; a trigger refuses a change once the organization has astock_move(OD-1).- Cost is per item across the organization, not per location.
- FIFO: each receipt adds a
cost_layer(remaining quantity, unit cost); an issue locks and consumes the oldest layers. - Weighted average:
item_cost(quantity, average) recomputed on each receipt; an issue takes the average, rounded to 4 places; the issue that empties the item takes the residue, so valuation never drifts.
O2.3 Posting
Section titled “O2.3 Posting”| Event | Semantic | Entry |
|---|---|---|
| Goods receipt | accrual, new control kind grni |
Dr Inventory (Expense for service / non-stock lines) / Cr GRNI |
| Purchase invoice on receipt lines | existing AP accrual | Dr GRNI / Cr AP; price difference to Purchase price variance; FX difference to realised FX |
| Delivery | new semantic effect |
Dr COGS / Cr Inventory at layer or average cost |
| Stock adjustment | effect |
Inventory against the reason code’s account, direction by sign |
| Returns, cancellations | existing reversal path | reversing entry and reversing moves |
effect means “approved, and its effect posts with no control account”; delivery, adjustment
and asset disposal (O4) share it. Provisioning adds GRNI (liability, grni), Inventory, COGS,
Purchase price variance and Stock adjustment accounts.
O2.4 Serial, batch, stock-take
Section titled “O2.4 Serial, batch, stock-take”serial_nounique per item;batchwith an optional expiry. A tracked item’s move without its serial (one per unit) or batch is refused by the database.- Stock-take: a count sheet that generates a
stock_adjustmentfor the differences.
O2.5 Integrity
Section titled “O2.5 Integrity”Stock valuation = the GL balance of the inventory accounts — a test, and a pass/fail count in the operator console’s integrity checks.
O3. Procurement and sales (platform/trade, 00740–00769)
Section titled “O3. Procurement and sales (platform/trade, 00740–00769)”O3.1 Procurement
Section titled “O3.1 Procurement”- Requisition: internal; items or text, quantity, estimated price, needed-by date. Approver resolved from grants, never chosen by the requester.
- Order: from a requisition or direct; supplier, currency, prices, tax, delivery date. PDF is a new C4 layout kind; no email until a mailer exists.
- Receipt: only against an order; partial; into a location; serials and batches captured. Posts at the order price, a foreign-currency order converted at the receipt date’s rate. Service and non-stock lines are received too (acceptance), posting Expense / GRNI with no stock move, so every order line matches the same way.
- Separation of duties: the member who approved an order cannot post a receipt against it (database-enforced, as a payment batch’s preparer ≠ approver).
- Three-way match at purchase invoice approval: invoiced quantity ≤ received − already invoiced; unit price within tolerance of the order price. Tolerance per organization as a percentage and an RM amount, both default 0. Beyond it, approval is refused (OD-3); an Owner who did not prepare the invoice may override with a reason, written to the audit log.
- Purchase return: from a receipt line; stock leaves at that receipt’s cost (under FIFO it consumes that receipt’s own layer, so the goods must still be held). The AP supplier credit note links to the return line.
Ruling R-1: purchase price variance goes to P&L, not back into stock still held. MFRS 102 prefers the adjustment when material. Upgrade: re-cost the FIFO layer with remaining quantity.
O3.2 Sales
Section titled “O3.2 Sales”- Quotation: validity date, PDF, one-step default workflow ending
committed; expiry derived, never stored. - Sales order: from a quotation or direct. No credit-limit check (customers have no limit today).
- Delivery: from a sales order; partial; a location; serials and batches. Posts
effect. - Sales invoice: an AR line may point at a delivery line; invoiced ≤ delivered − already invoiced. MyInvois is unchanged.
- Sales return: from a delivery line; stock returns at the cost it left at (the delivery’s own move), reversing COGS. The AR credit note links to the return line.
O3.3 Reports and permissions
Section titled “O3.3 Reports and permissions”- Reports on
platform/reporting: open orders, GRNI aging, open sales orders and backorders, delivered not invoiced, stock on hand, stock movement, stock valuation. - Permissions:
procurement.requisition.*,procurement.order.*,procurement.receipt.*,sales.quotation.*,sales.order.*,sales.delivery.*,inventory.item.*,inventory.adjustment.*,inventory.transfer.*; granted to roles by provisioning.
O4. Fixed assets (platform/assets, 00770–00789)
Section titled “O4. Fixed assets (platform/assets, 00770–00789)”asset_class: name (translation rows); cost, accumulated depreciation, depreciation expense and disposal gain/loss accounts; method ∈straight_line,reducing_balance; useful life in months; reducing-balance annual rate; residual percentage.asset: number (own series), class, description (en/ms), cost, residual value, in-service date, analyticshares, source — a posted AP invoice line, a journal line, or an opening import. Registration never posts: the source already posted the cost. A source line registers once (unique).- Depreciation:
depreciation_rununique on (org, period); a River periodic job on the 1st for the previous month, and a manual trigger. One journal entry per run, one line per asset with its shares. A closed period is skipped and flagged, never posted into. From the in-service month, whole months only. Straight-line: (cost − residual) ÷ life, rounding to the final month. Reducing balance: NBV × rate ÷ 12, floored at residual. - Disposal / write-off: workflow kind
asset_disposal, approved; depreciates to the disposal month, then posts (effect) Dr accumulated depreciation, Cr cost, NBV to disposal gain/loss. Proceeds post separately through an AR invoice or receipt to the same account. A write-off is a disposal without proceeds. - Opening assets: a
fixed_assetimport kind (cost, accumulated depreciation to date, in-service date) registering without posting — M4 carried the balances. - Reports: register, depreciation schedule (forecast), cost and accumulated depreciation roll-forward, register vs GL.
- Integrity: register cost and accumulated depreciation = the class accounts’ GL balances — a test and an operator console check.
- Permissions:
assets.asset.*,assets.depreciation.run,assets.disposal.*.
Out of scope
Section titled “Out of scope”Price lists and discount tiers, landed cost, bins, in-transit transfers, per-location or standard cost, back-dated stock moves, drop-ship, back-to-back and blanket orders, consignment, customer credit limits, emailing documents, capital allowance, revaluation, impairment, componentisation, inter-company asset transfer.
Drift tests these lanes add (presence first, each seen failing once)
Section titled “Drift tests these lanes add (presence first, each seen failing once)”- A chain link outside the allowed pairs, across organizations or parties, or past the source’s open quantity is refused (O1).
stock_balanceequals Σstock_movefor every item and location; a move taking on-hand below zero is refused (O2).costing_methodcannot change once a move exists (O2).- Stock valuation equals the inventory accounts’ GL balance (O2).
- The order approver cannot post its receipt; a three-way mismatch blocks approval unless an Owner who is not the preparer overrides (O3).
- A second depreciation run for the same period posts nothing; register totals equal the class accounts’ GL balances (O4).
- Every new table is tenant-isolated (the existing drift test, extended by construction).
Roadmap and outside-authority additions
Section titled “Roadmap and outside-authority additions”- Tick Phase 4 items as each lane lands.
- Needs an outside authority: capital allowance rates and treatment (ITA Schedule 3), the tax agent.