Skip to the lesson
CivOps AI Academy · M12Purchasing and Inventory: Ledger, MRP and Purchasing on Your Own Platform
0%

Chapter 1 · Inventory ledger, lots and status

The ledger is the truth

A stock count in a spreadsheet is a number someone typed. A stock count in this module is a sum of everything that ever happened to the stock, so you can always ask why it is what it is. This chapter says what the module builds, what it leaves to accounting, and how the ledger, the balances and the statuses fit together.

30 min11 capabilities12 transaction types7 stock statuses

By the end of this chapter you can

  • Say what the module replaces and which work it leaves to the accounting system and to other modules.
  • Explain why every movement is an append-only transaction, balances are derived, and a mistake is corrected by a reversal.
  • Read stock by lot, location and status, and say which statuses count as available.

What the module does

Purchasing and inventory replaces the stock, material-planning and purchasing parts of a manufacturing ERP. The specification names Epicor Kinetic, Plex, NetSuite Manufacturing, Acumatica, SAP Business One, Odoo MRP, Katana, MRPeasy, Fishbowl, ECI JobBOSS2 and E2, and ProShop. Tools such as MRPeasy list the same core: MRP, lot tracking, routings and costing [5].

The module keeps an exact stock record by item, lot, serial number, location and status. It plans material with MRP (material requirements planning: working out what to buy or make, and when, from what is needed). It runs sales orders and purchasing through receipt, and it hands the invoice match status to accounting. The general ledger, payment of suppliers, collections and tax stay in the accounting system.

The edge of the moduleInside the module: ledger, lots and status, planning and MRP, sales orders, purchasing, and receiving with invoice match status. Outside: the accounting system (general ledger, payment, tax), M13 warehouse tasks and M03 finite scheduling. INSIDE M12 · PURCHASING AND INVENTORYInventory ledgerevery movement a transactionLots and statusavailable, quarantine, holdPlanning and MRPparameters, planned ordersSales ordersavailable to promisePurchasingrequisition to approved orderReceiving and matchreceipt, invoice match statusOUTSIDE, WITH AN OWNERAccounting systemgeneral ledger, AP, AR, taxM13 Warehousedirected warehouse tasksM03 Schedulingfinite scheduling
The edge of the module. Inside: ledger, lots and status, planning and MRP, sales orders, purchasing, and receiving with the invoice match status. Outside, each with its owner: the accounting system, M13 for directed warehouse tasks, and M03 for finite scheduling.

The eleven capabilities

CapabilityWhat it doesTier
Inventory ledgerEvery movement an immutable transaction with a reason code; balances derived; reversals, not editsMVP
Lot, serial and statusQuantity by lot, serial, location and status; holds from quality enforcedMVP
CostingStandard, moving average and FIFO layers; roll-up from the bill of materials; variancesSTD
Planning parametersPer item and site: method, lot-sizing rule, multiples, safety stock, lead time, ABC classMVP
DemandSales orders with available-to-promise, forecasts, master production scheduleSTD
MRP runExplosion, gross-to-net, lead-time offset, pegging, planned orders, exception messagesMVP
PurchasingSupplier prices and lead times, requisitions, orders with approval thresholds, changesMVP
ReceivingReceipt against order lines, lot capture, inspection routing, return to vendorMVP
Invoice match hand-offTwo- and three-way match status with variance flags, exported to accountingSTD
SubcontractingOutside processing with material sent to suppliersBIC
Inventory analyticsTurns, days on hand, excess and obsolete, stock-outs, forecast accuracy, supplier reliabilitySTD

Standards behind it

StandardUse in this module
ASCM/APICS CPIM body of knowledgeMRP logic, lot-sizing rules, safety stock, master production schedule
US GAAP ASC 330 and IAS 2How inventory is measured and costed [2]
ISA-95 Parts 2 and 5, B2MMLExchange of schedules, material lots and performance between ERP and MES [1]
OAGIS, UBL 2.x, ANSI X12 850, 855, 856, 810Business documents for purchase orders, acknowledgements, ship notices and invoices
SOX IT general controls (where they apply)Different people approve and receive a purchase order

Every movement is a transaction

The table inv_inventory_transaction is the ledger. One row is one movement of one item (and lot, and serial unit where tracked) from a location and status to another. Nothing else changes a quantity.

txn_typeWhat happened
po_receiptGoods arrived from a supplier against a purchase order line
production_issue / production_receiptMaterial used by production; finished goods booked in
transfer_out / transfer_inStock left one location and arrived at another
adjustmentA quantity corrected, always with a reason code
scrapStock written off as unusable
shipment / customer_returnGoods left for a customer; goods came back
vendor_returnGoods sent back to a supplier
cycle_countThe difference found by a count
status_changeStock moved between statuses, such as available to hold
  • Quantity is always above zero. The direction comes from from_location_id and to_location_id, never from a negative number.
  • The ledger refuses edits. A database trigger (a rule the database runs on every change) raises an error on any update or delete of a ledger row, whatever the application does [4].
  • Rules the database checks. A unique txn_no, allowed values for the type, and a unique key that stops one request from posting twice [3].
  • Balances are derived. inv_inventory_balance holds the current quantity by item, site, location, lot, serial unit and status. It is the sum of the ledger, kept up to date by the one route that posts.
The posting routeA caller reaches the inventory-post route. In one database transaction it locks balance rows, takes the next transaction number, inserts the ledger row and changes the balance rows. It refuses a negative balance, a type the caller may not post, and a repeated client_ref. Phone or deskstock, receiver, plannerONE DATABASE TRANSACTION · inventory-post1 · Lock the balance rows (select for update)2 · Take the next txn_no3 · Insert the ledger row4 · Change the balance rowsLedger rowappend-onlyBalance rowsderived, never editedRefused: a negative balance, a type the caller may not post, a repeated client_ref
One route posts. In one database transaction it locks the balance rows it touches, numbers the transaction, inserts the ledger row and changes the balances. The lock makes two phones that try to take the last unit wait in turn [6].

The fifteen tables

Your agent builds the module's tables from this list in Session 2. Every table also has the standard columns id, tenant_id, created_at, created_by, updated_at, updated_by, row_version, archived_at and ext. An arrow (→) names the table a column points at; qty is a quantity, money an amount, ts a date and time, json structured data. Where a column lists its values, no other value is allowed. The specification also describes inv_cost_record and inv_demand_forecast, which this course does not build.

TableColumns: type, req = required
inv_inventory_transaction (append-only ledger)txn_no text req, unique; txn_type req: po_receipt, production_issue, production_receipt, transfer_out, transfer_in, adjustment, scrap, shipment, customer_return, vendor_return, cycle_count or status_change; item_id → core_item req; lot_id → core_lot; serial_unit_id → core_serial_unit; from_location_id → core_storage_location; to_location_id → core_storage_location; from_status text; to_status text; qty qty req; uom_id → core_uom req; unit_cost money; ref_type text; ref_id uuid; reason_code_id → core_reason_code; txn_at ts req; posted_by → core_person; reversal_of_id → inv_inventory_transaction
inv_inventory_balance (derived from the ledger)item_id → core_item req; site_id → core_site req; storage_location_id → core_storage_location req; lot_id → core_lot; serial_unit_id → core_serial_unit; status req: available, quarantine, hold, allocated, in_transit, consigned or damaged; qty_on_hand qty req; uom_id → core_uom req; last_txn_at ts
inv_item_site_planitem_id → core_item req; site_id → core_site req; planning_method req: mrp, reorder_point, min_max, kanban or none; lot_size_rule req: l4l, foq, eoq, poq or min_max; fixed_qty qty; min_qty qty; max_qty qty; order_multiple qty; safety_stock qty; lead_time_days int; abc_class: a, b or c; planner_id → core_person; buyer_id → core_person; make_buy req: make, buy or transfer
inv_bom_simpleparent_item_id → core_item req; component_item_id → core_item req; qty_per qty req; uom_id → core_uom req; scrap_pct num; effective_from date; effective_to date
inv_supplier_itemsupplier_party_id → core_party req; item_id → core_item req; supplier_part_no text; unit_price money; currency text; moq qty; lead_time_days int; preferred bool req; valid_from date; valid_to date
inv_purchase_requisitionreq_no text req, unique; requested_by → core_person req; item_id → core_item; description text; qty qty req; uom_id → core_uom; need_by date; planned_order_id → inv_planned_order; cost_center_id → core_cost_center; status req: draft, submitted, approved, rejected or converted
inv_purchase_orderpo_no text req, unique; supplier_party_id → core_party req; order_date date req; status req: draft, pending_approval, approved, sent, acknowledged, partially_received, received, closed or cancelled; currency text req; payment_terms text; incoterm text; ship_to_site_id → core_site; buyer_id → core_person; total money; approved_signature_id → core_e_signature
inv_purchase_order_linepurchase_order_id → inv_purchase_order req; line_no int req; item_id → core_item; description text; qty qty req; uom_id → core_uom req; unit_price money req; need_by date; qty_received qty; qty_invoiced qty; account_hint text; requisition_id → inv_purchase_requisition; project_ref → prj_project (a soft link)
inv_goods_receiptreceipt_no text req, unique; purchase_order_line_id → inv_purchase_order_line req; qty qty req; lot_id → core_lot; storage_location_id → core_storage_location; received_at ts req; received_by → core_person; inspection_required bool req; inventory_transaction_id → inv_inventory_transaction
inv_invoice_matchpurchase_order_line_id → inv_purchase_order_line req; supplier_invoice_no text req; invoice_qty qty; invoice_amount money; match_status req: pending, matched, price_variance, qty_variance or rejected; exported_at ts
inv_mrp_runrun_no text req, unique; site_id → core_site; run_at ts req; horizon_days int req; parameters json; status req: running, complete or failed; triggered_by → core_person
inv_planned_ordermrp_run_id → inv_mrp_run req; item_id → core_item req; site_id → core_site req; order_type req: make, buy or transfer; qty qty req; due_at date req; release_at date req; status req: planned, firmed, released or cancelled; pegging json
inv_mrp_exceptionmrp_run_id → inv_mrp_run req; item_id → core_item req; exception_type req: expedite, defer, cancel, past_due, below_safety_stock, no_source or lead_time_violation; ref_type text; ref_id uuid; message text req; suggested_date date; status req: open, actioned or ignored
inv_sales_orderso_no text req, unique; customer_party_id → core_party req; order_date date req; requested_at date; promised_at date; status req: draft, confirmed, in_production, partially_shipped, shipped, invoiced, closed or cancelled; currency text req; ship_to_address_id → core_address; crm_opportunity_ref → crm_opportunity (a soft link); quote_ref → crm_quote (a soft link); customer_po text
inv_sales_order_linesales_order_id → inv_sales_order req; line_no int req; item_id → core_item req; qty qty req; uom_id → core_uom req; unit_price money req; requested_at date; promised_at date; qty_shipped qty; status req: open, allocated, in_production, shipped or cancelled

The module also listens for events from other modules. This course builds only one of them, qms.hold.placed from the quality module. The others the specification lists (material consumed and lots produced in production, a quote accepted in CRM, a shipment shipped from the warehouse, an engineering change implemented), and its master production schedule, forecast and request-for-quote features, are out of scope here.

Screens and routes

Screen or routeWho uses itWhat it does
/inventory/receiveReceiver, on the phoneReceive order lines with lot and quantity; return to vendor
/inventory/stockStock user, on the phoneIssue, transfer, hold and count
/inventory/approvalsApproverApprove requisitions and purchase orders with an e-signature
/inventory/purchasingBuyerSupplier prices, purchase orders, change orders and the invoice match
/inventory/planningPlannerPlanning parameters, MRP runs, planned orders and the exception queue
/inventory/boardBoard, read-onlyThree tiles: on hand by status, open exceptions by age, open purchase orders
/api/inventory/postStock user, receiver, planner, loaderThe one route that posts to the ledger and changes balances
/api/inventory/receiveReceiverReceipts and returns against an order line
/api/inventory/poStock user, planner, buyerRequisitions and purchase orders, numbered and totalled on the server
/api/inventory/mrpPlannerRuns MRP and writes planned orders and exception messages
/api/inventory/matchBuyerWrites the invoice match status
/api/inventory/importLoader, onceLoads the old tool's data in Session 5; closed in Session 6

A mistake is a reversal

If a row is wrong, you do not fix it. You post a second row that undoes it: the same item and quantity with from and to swapped, reason REVERSAL, and reversal_of_id pointing at the original. History shows the mistake and its correction, and the balance at any past date can still be reproduced from the ledger.

A reversalAn original adjustment of 5 to STOCK, and a new reversal row that swaps the location, carries reason REVERSAL and points back with reversal_of_id. The balance goes 20, 25, 20 and the original row is never changed. Original, posted by mistaketxn_type: adjustment · txn 0007to_location: STOCKqty: 5 (always above zero)reason: COUNT-VARReversal, a new rowtxn_type: adjustment · txn 0008from_location: STOCK (swapped)qty: 5 · reason: REVERSALreversal_of_id: row 0007points toBalance of the item at STOCK: 20 before, 25 after the original, 20 after the reversal.The original row is never touched; the database refuses any update or delete of it.A second reversal of row 0007, or a reversal of a reversal, is refused.
A reversal. The original adjustment stays. The reversal swaps the location and points back to it, so the balance returns to 20. A row can be reversed only once.

Lots, serials and statuses

A lot is a quantity made or received together, so it can be traced. A serial unit is one individual item. The balance row also carries a status: available, quarantine, hold, allocated, in_transit, consigned or damaged. Only available stock can be promised to a customer or issued to production.

When the quality module places a hold on a lot, the module receives the event qms.hold.placed and the lot moves to status hold. It stays in the balances, so you can see it, but it is outside the view of available stock and of the available-to-promise answer, so no order can use it. Releasing a hold is a status_change back to available, and it needs the planner.

Stock by lot and statusA bar to scale of 59 units on hand for item B-100: 19 available in lot LOT-B-001, 20 available in lot LOT-B-002 and 20 on hold in lot LOT-B-002. Available is 39 while the hold stands. Item B-100 at STOCK, drawn to scale (one unit = 12 px)LOT-B-001 · available 19LOT-B-002 · available 20LOT-B-002 · hold 20On hand: 59Available: 39, the only stock an order or allocation can useHeld: 20Quarantine, allocated, in_transit, consigned and damaged are not available either.When the planner releases the hold, the 20 return to available: 59 on hand and 59 available.
Item B-100 at STOCK after a hold. 59 on hand, 39 available while 20 are held, drawn to scale. Releasing the hold returns the 20.

Knowledge check

A planner posted an adjustment by mistake. How is it corrected?

Knowledge check

Lot LOT-B-002 holds 40 units and 20 of them are on hold. How many are available?

References

  1. ISA: ISA-95 standard, enterprise-control system integration. https://www.isa.org/standards-and-publications/isa-standards/isa-95-standard
  2. IFRS Foundation: IAS 2 Inventories. https://www.ifrs.org/issued-standards/list-of-standards/ias-2-inventories/
  3. PostgreSQL documentation: constraints. https://www.postgresql.org/docs/current/ddl-constraints.html
  4. PostgreSQL documentation: trigger functions. https://www.postgresql.org/docs/current/plpgsql-trigger.html
  5. MRPeasy: features by plan (MRP II, routings, lot tracking, costing). https://www.madesmarter.uk/media/b5fpo5ai/mrpeasy_features_by_plan.pdf
  6. PostgreSQL documentation: explicit locking. https://www.postgresql.org/docs/current/explicit-locking.html

Chapter 2 · Planning parameters and MRP

Plan what to buy and when

MRP answers one question for every item: given what is needed, what is on hand and what is already on order, what must still be bought or made, and by when. This chapter sets the parameters that steer it and works one run through by hand, with numbers small enough to check on paper.

25 min5 planning methods5 lot-sizing rules7 exception types

By the end of this chapter you can

  • Say what each planning parameter on inv_item_site_plan controls.
  • Work gross-to-net, lot sizing and lead-time offset by hand for one item.
  • Explain pegging, and read an exception message such as expedite.

Parameters per item and site

MRP does not decide by itself how an item is planned. Each item at each site has one row in inv_item_site_plan, and the MRP run reads it. The method and lot-sizing rule come from the CPIM body of knowledge [1]. Vendor tools such as MRPeasy and Katana offer the same ideas under their own names, with reorder points and lot traceability [2][3].

ParameterAllowed valuesWhat it controls
planning_methodmrp, reorder_point, min_max, kanban, noneHow the item is planned at all
lot_size_rulel4l, foq, eoq, poq, min_maxHow big each planned order is
fixed_qty, min_qty, max_qty, order_multiplequantitiesInputs to the rule above
safety_stocka quantityThe cushion projected stock must not fall below
lead_time_daysdaysHow long between release and arrival
abc_classa, b, cA ranking of items by importance
make_buymake, buy, transferWhere the item comes from
planner_id, buyer_idpeopleWho acts on the messages
lot_size_ruleIn plain words
l4l (lot for lot)Order exactly what is short, no more
foq (fixed order quantity)Order a fixed amount each time; a bigger shortfall is rounded up to a whole multiple
eoq (economic order quantity)Order the amount that balances the cost of ordering against the cost of holding stock: the square root of (2 x annual demand x cost per order / holding cost per unit per year)
poq (period order quantity)Order enough to cover a set number of periods of demand
min_maxWhen stock falls to the minimum, order up to the maximum

The course seed has two bought items. A-100 is planned by MRP with a fixed order quantity of 40, safety stock 10 and a lead time of 14 days. B-100 is planned by reorder point, with its min_qty of 30 used as the reorder point, a fixed quantity of 100 and a lead time of 7 days. P-100 is made, and one bill of materials row says it needs 2 of A-100.

Demand

Demand comes from confirmed sales orders, from forecasts by period, and from the orders that consume components. A sales order line also gets an available-to-promise (ATP) answer: how many units can be promised for a date from available stock and scheduled receipts, less what is already committed. Held and quarantined stock is not in that answer.

Gross-to-net, step by step

  1. Explode. Turn each demand for a made item into demand for its components using the bill of materials.
  2. Gross requirements. List the need for the component by date.
  3. Net. Start from available stock and add scheduled receipts (open purchase order lines), firmed planned orders, and released planned orders whose requisition is not yet on an order line. Take away each need in date order. Wherever projected stock would fall below the safety stock, the shortfall is a net requirement.
  4. Size. Round the shortfall up with the lot-sizing rule.
  5. Offset. Subtract the lead time from the due date to find the release date.
  6. Peg. Store, with each planned order, the demands it covers.
From orders to gross requirementsA sales order for 10 of A-100 and three firmed P-100 orders of 15, 20 and 12. Each P-100 needs 2 of A-100, so the gross requirements for A-100 are 10, 30, 40 and 24 on 2 November, 16 November, 30 November and 14 December. Sales order10 of A-100 · 2026-11-02taken as isNeed 10 of A-100on 11-02P-100 order: 15released 2026-11-16× 2 per P-100Need 30 of A-100on 11-16P-100 order: 20released 2026-11-30× 2 per P-100Need 40 of A-100on 11-30P-100 order: 12released 2026-12-14× 2 per P-100Need 24 of A-100on 12-14inv_bom_simple: parent P-100, component A-100, qty_per 2.Gross requirements for A-100: 10, 30, 40 and 24.
From orders to gross requirements. A sales order for 10 of A-100 and three firmed orders for P-100. Each P-100 needs 2 of A-100, taken at its release date.

The worked run for A-100

Available stock is 50. Another 10 are on hold in a second lot and are left out. One open purchase order brings 20 on 2026-11-09. Safety stock is 10 and the fixed order quantity is 40.

Projected stock of A-100A step line to scale. Available stock is 50. It falls to 40 after the sales order, rises to 60 when the open purchase order arrives, falls to 30 on 16 November, then to minus 10 on 30 November, which is 20 below the safety stock of 10, answered by a planned order of 40. On 14 December it falls to 6, 4 below safety stock, answered by a second planned order of 40. A-100 projected on-hand stock (units), from 2026-11-020204060safety 10−10 order+20 due−30−40−24short 20 → +40 plannedshort 4 → +40 planned11-0211-0911-1611-3012-14
Projected stock of A-100, to scale. On 30 November the projection would fall to −10, which is 20 short of the safety stock of 10, so MRP plans a buy of 40. On 14 December it would fall to 6, which is 4 short, so it plans a second buy of 40.
DateGross needScheduled receiptPlanned receiptProjected stock after
2026-11-021040
2026-11-092060
2026-11-163030
2026-11-30404030
2026-12-14244046

Each shortfall is smaller than the fixed quantity, so each planned buy is the full 40. Then lead-time offsetting counts back 14 days from each due date.

Lead-time offset and peggingTwo bars to scale. Planned buy 1 is released on 16 November and due on 30 November, a lead time of 14 days. Planned buy 2 is released on 30 November and due on 14 December. Each planned order stores pegging, the demand it covers. 11-1611-3012-14lead time 14 daysBuy 1 · 40lead time 14 daysBuy 2 · 40Planned buy 1: 40due 11-30 · release 11-16peggingDemand it coversP-100 order of 20 (released 11-30) needs 40 of A-100Planned buy 2: 40due 12-14 · release 11-30peggingDemand it coversP-100 order of 12 (released 12-14) needs 24 of A-100
Release dates and pegging. A buy due on 30 November is released on 16 November; a buy due on 14 December is released on 30 November. Each stores the demand it covers.

Exception messages

The run also writes messages for the planner in inv_mrp_exception. Each has a type and a status of open, actioned or ignored.

exception_typeMeaning
expediteAn open order arrives after the date it is needed: move it earlier
deferAn order arrives well before it is needed: move it later
cancelAn order is no longer needed
past_dueAn order should already have arrived
below_safety_stockProjected stock falls below the safety stock
no_sourceNothing says where the item can come from
lead_time_violationThe need date is nearer than the lead time allows
Exercise · Net one item by hand, then compare with the module25 minutes

You need: Your pilot item family from Session 1, your database, and your AI coding agent

Pick one bought item from your own pilot family that has a planning method of MRP, one sales or production demand, and one open purchase order. Work on paper first.

Outcome: A hand calculation for one real item that matches the module's planned orders, or a written list of every difference with its cause.

Knowledge check

Projected stock of A-100 would fall to −10 on 30 November. Safety stock is 10 and orders are in fixed quantities of 40. What does MRP plan?

Knowledge check

A planned buy is due on 2026-12-14 and the lead time is 14 days. When is it released?

References

  1. ASCM: CPIM certification (the body of knowledge for MRP, lot sizing and safety stock). https://www.ascm.org/learning-development/certifications-credentials/cpim/
  2. MRPeasy: MRPeasy vs Katana. https://www.mrpeasy.com/?p=7838
  3. Dupple: MRPeasy review (lot traceability, reorder points). https://dupple.com/reviews/mrpeasy

Chapter 3 · Purchasing, receiving and the invoice match

Buy it, receive it, match it

A purchase is a short chain: someone asks, someone approves, a supplier is told, goods arrive, an invoice comes. Each link is a person with a different job, and the module records every hand-over. This chapter follows an order for 100 units of B-100 from request to match.

25 min9 order statuses5 roles3-way match

By the end of this chapter you can

  • Follow a purchase from requisition to purchase order, approval, receipt and invoice match status.
  • Explain why the approver of an order must not be the person who receives against it.
  • Say what a receipt, a return and a quarantine do to the ledger.

From need to order

A need becomes a requisition (inv_purchase_requisition): an internal request to buy, which may come from a person or from a released planned order. Its statuses are draft, submitted, approved, rejected and converted. Once approved, the buyer converts it to a purchase order.

The buyer does not type the price. inv_supplier_item holds each supplier's price, minimum order quantity (MOQ), lead time and a preferred flag, and the order takes supplier, price and lead time from the preferred row. The seed has Supplier One selling B-100 at 2.5000 with a minimum of 20 and a lead time of 7 days. Currency is a text code, such as the ISO 4217 codes [6]. The total is computed on the server, never taken from a browser.

The purchase order statusesEight statuses in two rows: draft, pending approval, approved, sent, acknowledged, partially received, received and closed, with who moves each on. Cancelled is a status too. draftbuyer builds itpending_approvalapprovers askedapprovedsignature recordedsentbuyer sends itacknowledgedsupplier confirmspartially_receivedreceiver books goodsreceivedall lines inclosedbuyer closes itcancelled is also a status; nothing is ever deleted.
The purchase order statuses. Amber steps are the approvals; green steps are the receipts. Each step is a different person's job.

Approval by threshold

Each business sets its own approval amount; the course gives none. At or under it, an order needs one approval step. Over it, two steps by two different approvers. When the buyer submits, the module creates the pending approval rows and the order becomes pending_approval. Each approver confirms their password and an e-signature is stored with a hash of the order header and lines, so any later change to the order is detectable. The last signature is kept on the order as approved_signature_id.

Keep the duties apart

Where SOX applies, approving and receiving an order must be different people. The same idea runs through security guidance as separation of duties: no one person can complete a sensitive process alone [1]. The module writes it as rules in the Role and Exposure Matrix.

Duties kept apartA grid of five roles against five actions. Buyer creates orders, approver approves them, receiver receives goods, stock user issues, moves and counts, and planner releases a hold. Blank cells are refused. The approver of an order cannot receive against it. Create POApprove POReceive goodsIssue, move, countRelease a holdBuyerApproverReceiverStock userPlannera right the matrix grants · blank: refusedWhoever approved an order cannot receive against it, and one person cannot hold both roles on a site.An approver cannot approve what they raised, and a second approval step needs a different person.
Five roles, five actions. A dot is a right the matrix grants. Everything blank is refused by the rules generated from the matrix, not just hidden from the screen.

Receiving

A goods receipt (inv_goods_receipt) is booked against one order line. The route creates the lot, inserts the receipt, posts a po_receipt to the ledger with the unit cost from the line, raises qty_received and moves the order to partially_received or received. It refuses a receipt that would take the line above the quantity ordered.

  • Lot capture. Every receipt of a lot-tracked item names a lot, so the stock can be traced to the delivery.
  • Inspection routing. If inspection_required is true, the goods arrive in status quarantine. The planner releases them with a status_change once they pass.
  • Return to vendor. The receiver posts a vendor_return with a reason code through the receiving route. It takes stock out and lowers qty_received. It is a new transaction, not an edit of the receipt.

Invoice match hand-off

The supplier's invoice is compared with what was ordered and what was received. A two-way match compares order and invoice. A three-way match adds the receipt, so you pay only for goods that arrived. The result is stored in inv_invoice_match as pending, matched, price_variance, qty_variance or rejected.

Matching the invoiceA purchase order line for 100 at 2.5000 is 250.0000. Goods received total 90, worth 225.0000. An invoice for 100 units sets qty_variance; a corrected invoice for 90 units and 225.0000 sets matched, and matched rows are exported to accounting. PO lineordered 100 of B-100price 2.5000 each100 × 2.5000 = 250.0000Goods receiptsqty_received 9060 − 10 returned + 4090 × 2.5000 = 225.0000Supplier invoiceINV-DEMO-1 · 100 unitsinvoiced 250.0000qty_varianceInvoice quantity 100 is above received 90The buyer asks for a corrected invoicematchedCorrected invoice: 90 units, 225.0000Quantity and price agree: exported to accountingThe module hands match status and amounts over. Accounting posts them,pays the supplier and keeps the general ledger.
One order, two invoices. 100 ordered, 90 received after a return, and an invoice for 100 sets qty_variance. A corrected invoice for 90 sets matched, and matched rows are exported to accounting.

Documents that cross the fence

Orders, acknowledgements, ship notices and invoices travel between businesses as standard documents. In ANSI X12 they are 850 (purchase order), 855 (purchase order acknowledgement), 856 (ship notice, also called an ASN) and 810 (invoice) [2]. OAGIS business object documents [4] and UBL 2.x [3] cover the same ground in XML. Inventory tools in this class tie purchasing, receiving and stock together in the same way [5]. The module sends nothing to suppliers by itself: the buyer sends the order the way the business already does and sets the status.

Exercise · Walk one purchase end to end30 minutes

You need: Your module with demo sign-ins for buyer, two approvers, receiver, stock user and planner

Use the course seed so your numbers can be compared with this chapter. Use a different browser profile or private window for each person so you really are signed in as them.

Outcome: An order for 100 of B-100 approved by the right people, received in part with a return and two lots, and matched at 90 units and 225.0000, with the three refusals recorded.

Knowledge check

A person approved a purchase order. Can they also receive the goods against it?

Knowledge check

100 units were ordered and 90 received. The supplier invoices 100. What is the match status?

References

  1. NIST SP 800-53 Rev. 5: security and privacy controls (includes separation of duties). https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final
  2. X12: the standards body for ANSI X12 transaction sets. https://x12.org/
  3. OASIS: Universal Business Language (UBL) 2.1. https://docs.oasis-open.org/ubl/UBL-2.1.html
  4. OAGi: Open Applications Group, OAGIS. https://oagi.org/
  5. OneCart: manufacturing inventory management software. https://www.getonecart.com/manufacturing-inventory-management-software/
  6. ISO 4217: currency codes. https://www.iso.org/iso-4217-currency-codes.html

Chapter 4 · Costing, measures and migration

Cost it, measure it, cut over

Stock is also money on a balance sheet, and a replacement is only proved when its numbers match the old tool's. This chapter covers how stock is costed, the five measures the module reports, and the steps from the old tool's export to the day you cancel it.

20 min3 costing methods5 measures8 migration steps

By the end of this chapter you can

  • Compare standard, moving average and FIFO costing on one issue.
  • Calculate the five inventory measures from their definitions.
  • Load an opening balance once, reconcile it to the old valuation report, and cut over.

Costing

Each stock movement carries a unit cost, and the specification provides for inv_cost_record, which would hold an item's cost by type and date, split into material, labor, overhead and outside processing. The specification provides for three methods; the six sessions of this course record the unit cost on each movement and do not build cost records, forecasts or the journal export. The stock balances can be rebuilt at any time by a database view over the ledger [3]. IAS 2 and US GAAP ASC 330 govern how inventory is measured, and your accountant owns the choice of method; the module records the unit cost it is given and does not choose the method [1].

MethodHow an issue is costed
StandardOne fixed cost per item. What was actually paid, above or below it, is a variance.
Moving averageThe running average of what is on hand, recalculated at each receipt.
FIFO layersFirst in, first out: each receipt is a layer, and issues use the oldest layer first.
One issue, costed by layersTwo receipts of 40 units, at 1.5000 and then 1.8000, drawn to scale. A first-in first-out issue of 50 takes 40 from the first layer and 10 from the second, costing 78.0000 and leaving 30 worth 54.0000. A moving average of 1.6500 would cost the issue at 82.5000. Received40 × 1.5000 first40 × 1.8000 secondIssued 5040 × 1.5000 = 60.000010 × 1.8000 = 18.0000FIFO cost of the issue: 78.0000Left on hand30 × 1.8000 = 54.0000Moving average: (40 × 1.5000 + 40 × 1.8000) ÷ 80 = 1.6500, so the same issue costs 82.5000 and 49.5000 stays.Standard cost: one fixed value per item; what was paid above or below it is shown as a variance.
One issue, costed by layers. Receipts of 40 at 1.5000 and 40 at 1.8000 are made-up numbers. Issuing 50 costs 78.0000 by FIFO and 82.5000 by moving average. Both are valid methods; they give different profit and different stock value.

Cost roll-up works up the bill of materials: a made item's cost is its components plus labor, overhead and outside processing. Activity-based costing and alignment with GAAP are core CivOps work, so the specification's inv_cost_record and journal export follow those design bases. In this course the hand-off to accounting is a CSV file of invoice match status and amounts, not the journal export. Turns, forecast error and stock-out measures are standard planning measures [2][4].

Five measures

MeasureDefinitionQuestion it answers
Inventory accuracyLocations where |system − counted| is within tolerance, divided by locations countedCan people trust the count?
Inventory turnsCost of goods sold ÷ average inventory valueHow fast does stock sell?
Supplier on-time deliveryReceipts on or before need-by ÷ receiptsDo suppliers keep their dates?
Forecast accuracy (MAPE)Mean absolute percentage error, by item familyHow wrong is the forecast on average?
MRP exception backlogOpen exception messages, by ageIs the planner keeping up?

Move in: the opening balance

Nothing in the old tool has to stay alive for its history. You load three things: the item planning parameters, the open purchase and sales orders, and the stock. Stock goes in as one dated opening transaction per item, lot and location, an adjustment with reason OPENING, posted through the same route as every other movement and never written into a balance directly. A counted quantity is better than the old tool's number.

The loader runs as a dry run first: it prints counts and every row it cannot match and writes nothing until it is run with --apply. The key of each opening row (item, lot, location) is its client reference, so running the loader twice leaves the stock the same.

The idempotent opening balance and the value check
opening row key   = item + lot + location
run the loader    -> 1 adjustment per key, reason OPENING, at the cut-off time
run it again      -> the key is already used, nothing is added
new value         = old value + (counted - old quantity) x unit cost
From the old tool to cutoverEight steps in order: export from the old tool, dry run, blind count, post the opening balance, reconcile quantity and value, run side by side, cut over, and cancel last, once the final export is stored. Exportfrom the old tool's pagesDry runcounts and rejects onlyBlind countsystem quantity hiddenOpening balanceadjustment, reason OPENINGReconcilequantity and value agreeSide by sideone week or moreCut overold tool read-onlyCancel lastafter final export storedA difference you cannot explain means you are not ready to cut over.
From the old tool to cutover. Reconciliation comes before the parallel week, and cancelling comes last, once the final export is stored.

Reconcile before you trust it

Compare quantity and value with the old tool's stock valuation report for the same date. For each item, the new value must equal the old value plus the counted difference times the unit cost, and the totals must agree. A difference that cannot be explained is an error to find, not a rounding to accept. Do the same for open orders: the count of lines and the value still to receive.

Run the old tool and the module side by side for at least one week, with the same people entering the same movements in both. At cutover, take a final export and set the old tool read-only. Close the loader by removing its row from the matrix and showing that an insert through it is now refused. Cancel only once the new way works, and write the saving as arithmetic from your own invoices.

Exercise · Prove the ledger and reconcile one item30 minutes

You need: Your module with the seed or your loaded pilot data, a scratch database, and your AI coding agent

This is the check you will run before cutover, done on one item. Use a scratch copy of the database for step 4.

Outcome: A check that passes on real data and fails on a hand-edited balance, and one item whose new value equals its old value plus the counted difference times its unit cost.

Knowledge check

The loader is run twice by mistake. What should happen to the opening stock?

Knowledge check

You count ten locations with the system quantity hidden. Seven are within your tolerance. What is inventory accuracy?

References

  1. IFRS Foundation: IAS 2 Inventories. https://www.ifrs.org/issued-standards/list-of-standards/ias-2-inventories/
  2. ASCM: CPIM certification (inventory, planning and control body of knowledge). https://www.ascm.org/learning-development/certifications-credentials/cpim/
  3. PostgreSQL documentation: CREATE VIEW (balances recomputed from the ledger). https://www.postgresql.org/docs/current/sql-createview.html
  4. OneCart: manufacturing inventory management software. https://www.getonecart.com/manufacturing-inventory-management-software/

Chapter 5 · 12 questions · 80% passes

Final assessment

Twelve questions across the element. Score 80% (10 of 12) to pass. Your LMS records your score and each answer; you can review the chapters and try again.

15 min12 questions≈ 15 minutesRetake allowed

Choose one answer for each question, then submit. You will see the right answer and why for every question.

1. Which of these does the module leave to the accounting system?
2. A quantity on the ledger is always above zero. How does a transaction show direction?
3. What makes the ledger append-only?
4. A balance row is wrong. What does the module do?
5. 20 units of a lot are on hold and 40 are on hand in total. Which statement is right?
6. Why does the posting route lock the balance rows it touches?
7. In gross-to-net for A-100, available stock is 50 and projected stock would fall to −10 with a safety stock of 10. The shortfall is:
8. What does lead-time offsetting produce?
9. Why does each planned order store pegging?
10. Who should approve a purchase order and who should receive against it?
11. Which statement about the opening balance is right?
12. Ten locations are counted with the system quantity hidden, and eight are within your tolerance. What is inventory accuracy?