Skip to content

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.

  • 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_line pair for every chain kind, linked by a self foreign key.
  • Posting seam (platform/posting): the workflow engine dispatches an Event with a Semantic (draft, pending, accrual, payout, acknowledged, dispute, rejected, cancelled, custom) and, on accrual, a ControlKind; accounting’s Poster writes the entry in the same transaction. Reversal is entering rejected/cancelled after a posting.
  • Control kinds are a closed CHECK on account.control_kind and workflow_state.control_kind (00030, 00050).
  • AP / AR: purchase_invoice_line and the sales invoice line carry account, tax code, amounts and analytic shares; no item, no quantity.
  • platform/documents is the file store (S3, OCR, thumbnails), not a chain. The chain lives in a new platform/trade.
  • Phase 3: moduleinstall (C1), field_schema custom fields (C2), platform/printing layouts (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 a committed semantic; this spec adopts the same one, so the two stay compatible.

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_id or a description (non-stock text line), quantity, UOM, quantity in base UOM, unit price NUMERIC(19,4), tax code, analytic shares, 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_line and the sales invoice line gain a nullable trade_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.
  • uom is a platform/registry lookup; item_uom holds each alternate UOM’s factor to base as NUMERIC(19,6).
  • location: warehouses. No bins.
  • Categories: platform/registry. Custom fields: item joins the C2 entities. Import: an item import kind on M1.
  • Module install: inventory, procurement and sales join 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)”
  • stock_move, append-only (the app role holds no UPDATE or DELETE): item, location, signed base quantity, unit cost and total cost NUMERIC(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, with CHECK (on_hand >= 0) — OD-2 enforced by the database.
  • Transfers between locations: two moves, no workflow, no journal entry, cost unchanged.
  • company.costing_method ∈ fifo, weighted_average; a trigger refuses a change once the organization has a stock_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.
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.

  • serial_no unique per item; batch with 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_adjustment for the differences.

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)”
  • 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.

  • 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.
  • 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, analytic shares, 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_run unique 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_asset import 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.*.

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_balance equals Σ stock_move for every item and location; a move taking on-hand below zero is refused (O2).
  • costing_method cannot 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).
  • Tick Phase 4 items as each lane lands.
  • Needs an outside authority: capital allowance rates and treatment (ITA Schedule 3), the tax agent.