Skip to the lesson
CivOps AI Academy · M13Warehouse (WMS): Scan-Verified Moves Posted to the Ledger
0%

Chapter 1 · Purpose, scope and standards

What a WMS adds to the ledger

A stock ledger knows how many units of which lot sit in which location. It does not know that a pallet is waiting at the dock, which shelf it belongs on, who should carry it there, or whether they carried it to the right shelf. A warehouse management system (WMS) adds that: it directs every physical move, checks it by scan, and writes the result to the ledger. This chapter says what the module does, what it leaves to others, and which standards it is measured against.

20 min7 warehouse jobs11 capabilities in 3 tiers0 stock quantities of its own

By the end of this chapter you can

  • Say what a WMS adds to a stock ledger, and why the module keeps no stock quantity of its own.
  • Name the seven warehouse jobs, the ledger transaction each one posts, and the work the module leaves to M12 and to later extensions.
  • Name the standards the module is built against and say what each is used for.

What a WMS adds to the ledger

Module M13 directs and verifies every physical warehouse move by scan: receiving against advance ship notices (ASNs, the supplier's message saying what is on the truck), quality quarantine, directed put-away, replenishment to pick faces and, by kanban, to line-side locations, wave, batch or zone picking, packing, shipping and cycle counting. Every completed move is posted to the M12 inventory ledger. The module replaces the warehouse management systems a business usually pays for: Manhattan Active WM, Blue Yonder WMS, SAP EWM, Oracle WMS Cloud, Infios, Infor WMS and Made4net at the large end, and Fishbowl, NetSuite WMS, Logiwa and Extensiv for smaller businesses [1][2].

Think of two books. The M12 ledger is the book of how much: item, lot, location and status, one row for every movement. The warehouse module is the book of what has to happen and who did it: which locations exist and what each allows, which pallets carry which labels, which task is open, which person scanned what. The second book never copies a quantity from the first. It points at it.

The edge of the warehouse moduleInside: location profiles, handling units and labels, ASNs and receipts, directed tasks, waves with packing and shipping, cycle counting and replenishment to pick faces and, by kanban, to line-side. Outside, each with its owner: valuation and purchasing in M12, transportation planning in catalogue Wave 4, labour standards and slotting in best-in-class extensions, and the pull signal a production system sends for line-side demand, which the course does not build. INSIDE THE WAREHOUSE MODULE (M13)Location profilesHandling units, labelsASNs and receiptsDirected tasksWaves and shippingwith packingCycle countingReplenishmentpick face, line-sideOUTSIDE, WITH ITS OWNERValuation and purchasingowned by M12Transportation planningcatalogue Wave 4Labour standards, slottingBIC extensionsProduction pull signalline-side demand; not built
The edge of the module. Inside: location profiles, handling units and labels, ASNs and receipts, directed tasks, waves with packing and shipping, cycle counting and replenishment to pick faces and line-side. Outside, each with its owner: valuation and purchasing (M12), transportation planning (catalogue Wave 4) and labour standards and slotting (best-in-class extensions); the pull signal a production system sends is not built here.
  • In: location profiles (capacity, zone type, sequences); handling units and license plates with SSCC labels; inbound ASNs and receipts with quality routing; directed tasks for put-away, pick, replenish, move, count and load; waves and pick strategies, packing and shipping; cycle counting programmes; min/max replenishment to pick faces and kanban replenishment to line-side.
  • Out: inventory valuation and purchasing (M12); transportation planning (catalogue Wave 4); labour management standards and slotting optimisation (best-in-class extensions).
  • Described, not built in the course: damage capture with an under-receipt tolerance at receiving, and the pull signal a production system sends for line-side demand (with outbound orders of type line_side). Line-side kanban is built: the course's fixed seed has no line-side location, so Session 4 proves one kanban card scan in a test that rolls back, and Session 5 adds your real line-side locations and kanban rules.

The seven warehouse jobs

Every warehouse does the same seven jobs, whatever software runs it. The table lists them in the order stock moves, with what a person scans and the M12 ledger transaction each one posts. The transaction names are those of the M12 posting route inventory-post.

JobWhat a person does, by scanLedger transaction
ReceivingScans the dock label and each supplier pallet label, keys the quantitypo_receipt into DOCK-IN
Put-awayMoves a received pallet to the location the rules chose, scans both labelstransfer_in from the dock to the shelf
ReplenishmentTops up a pick face (the shelf or bin pickers take from all day) from reservetransfer_in from reserve to the pick face
PickingTakes allocated stock to staging, scanning location and unittransfer_in to staging, available to allocated
PackingScans items into cartons; the route makes the shipment and one load task per cartonnone of its own
ShippingScans staging and each carton onto the truck; a supervisor shipsshipment from allocated
Cycle countingCounts locations on a schedule instead of the whole building at oncecycle_count, only for lines with a variance
The seven warehouse jobsReceiving, put-away, replenishment and picking in a row, then packing, loading and shipping, with cycle counting apart. Each stock move posts one ledger row: po_receipt, transfer_in, shipment or cycle_count. Packing posts no row itself. Receivingledger: po_receiptPut-awayledger: transfer_inReplenishmentledger: transfer_inPickingledger: transfer_inPackingno ledger row;creates load tasksLoading, shippingledger: shipmentCycle countingledger: cycle_countEvery box that moves stock posts one row to the M12 ledger; counting posts only the variance.
Seven jobs, one ledger. Receiving, put-away, replenishment and picking each post one transfer or receipt. Packing posts nothing itself but creates the load tasks. Loading and shipping post the shipment. Counting posts only the variance.
One task, one ledger transactionA scan on the phone is checked again by the route, which calls inventory-post. That writes one ledger row and moves the balance. The ledger row id is written back to the task's inventory_txn_ref in the same database transaction. Scan on the phonelocation and unitWMS routechecks the scan againinventory-postthe only door to M12Ledger rowinv_inventory_transactionTask is doneinventory_txn_ref setBalance rowinv_inventory_balancesame databasetransactionledger row id written backledger moves itNo code outside inventory-post writes the balance table, and a unique index lets one ledger row belong to only one task.
One task, one transaction. The phone scan is checked again by the route, which calls inventory-post. That writes one ledger row, which moves the balance. The ledger row's id is written back to the task in the same database transaction.

Two mechanisms hold this together. Each route calls inventory-post inside its own database transaction, which means the ledger row and the warehouse rows are saved together or not at all. And each post carries a client_ref built from the task, so sending the same request twice changes the data once (this property is called idempotency: repeating an action leaves the same result as doing it once). A unique index, a database rule that refuses a second row with the same value, lets one ledger transaction belong to only one task, receipt or count line.

Eleven capabilities in three tiers

Each capability carries a tier. MVP must exist for a business to cancel its incumbent subscription with confidence. STD is parity with mainstream tools, what a buyer expects in a demo. BIC marks best-in-class extensions. Analyst write-ups of WMS products separate a core set of capabilities from extended ones [2][3], and the tiers follow the same idea: nine core rows, two extensions.

CapabilityWhat it doesTier
Location managementLocation profiles on spine storage locations: zone type, capacity (volume, weight, pallets), pick and put-away sequences, temperature class, mixing rules, blockingMVP
ReceivingASN-driven or blind receipt, label printing (GS1-128, SSCC), over and under tolerance, damage capture, quality routing to quarantineMVP
Put-awayDirected put-away by rules (fixed, nearest empty, consolidate, velocity), scan to confirmMVP
Inventory controlLicense-plate tracking, status changes, holds from quality, FEFO and FIFO, moves and consolidationsMVP
Picking and packingScan picking on a handheld or phone with short-pick handling, packing with cartonisation, labelsMVP
Cycle countingABC, location, item and ad-hoc counts; blind counts; recount thresholds; approval of variancesMVP
ReplenishmentMin/max and demand replenishment to pick faces; kanban or pull to line-side for productionSTD
Order allocation and wavesAllocation by rules, wave, batch, zone, cluster and discrete picking, pick path sequencingSTD
ShippingStaging, loading verification, bill of lading, carrier labels and tracking, ASN outSTD
Task interleaving and labourInterleaved tasks, productivity tracking against engineered standardsBIC
Slotting and yardVelocity-based slotting recommendations; dock appointmentsBIC

The course builds the MVP and STD rows that its six sessions can reach, and leaves the two BIC rows as extensions. Several items inside the built rows are described but not built: blind receipt, damage capture with an under-receipt tolerance, and SSCC labels for the course's own cartons (Receiving); the velocity and fefo_zone put-away strategies (Put-away); demand replenishment (Replenishment); and carrier labels and an outbound ASN (Shipping), where the supervisor keys the tracking number by hand. In Replenishment, kanban to line-side is built and proven in a test, and the pull signal from a production system is not built. Cartonisation here means recording what went into which carton and its measures; choosing carton sizes automatically is not built.

The standards, and what each is used for

StandardUsed in this module forWhere you meet it
GS1 General SpecificationsGTIN, SSCC and the GS1-128 data fields: identifiers 01 GTIN, 10 lot, 17 expiry, 21 serial and 00 SSCCChapter 2; the label parser in Session 2
ANSI X12 856, 940 and 945 [5]; UN/EDIFACT DESADV [6]Advance ship notices (856, DESADV) and warehouse shipping orders and advices (940, 945)The tables hold what these messages say; in the course the supervisor enters the ASN, and no EDI reader is built
FDA Drug Supply Chain Security Act, DSCSA [4]Serialised product tracing for certain prescription drugs, where it appliesSerial number (identifier 21) is decoded; a DSCSA flow needs a serial-level scan at ship, which the course does not build
FEFO and FIFO allocation principlesShelf-life-driven allocation and rotationChapter 3

FIFO (first in, first out) uses the stock that arrived first. FEFO (first expired, first out) uses the stock that expires first, which is what matters for goods with a shelf life: a lot received last week can expire before one received last month.

Knowledge check

Where does the warehouse module keep how many units of a lot are in a location?

Knowledge check

A colleague asks the warehouse module to value stock at its purchase cost. Where does that belong?

References

  1. Infios: Leader in the 2025 Gartner Magic Quadrant for WMS (core capability list). https://www.infios.com/en/knowledge-center/news/infios-leader-in-2025-gartner-magic-quadrant-wms
  2. Made4net: which is the best WMS for business (core and extended capabilities). https://made4net.com/knowledge-center/which-is-the-best-wms-for-business/
  3. Solutions Review: 2023 Magic Quadrant for WMS, key takeaways. https://solutionsreview.com/enterprise-resource-planning/?p=5624
  4. U.S. FDA: Drug Supply Chain Security Act (DSCSA). https://www.fda.gov/drugs/drug-supply-chain-integrity/drug-supply-chain-security-act
  5. ASC X12: the standards body for the X12 EDI transaction sets. https://x12.org/
  6. UNECE: UN/EDIFACT, the United Nations rules for electronic data interchange. https://unece.org/trade/uncefact/unedifact

Chapter 2 · The tables and the GS1 label

Locations, handling units and labels

A warehouse module is mostly a map and a vocabulary. The map says where things can be and what each place allows; fifteen tables hold it. The vocabulary says what a label means; a handful of GS1 identifiers hold it. Get both exact and the rest of the module is rules on top.

25 min15 tables6 seed locations5 GS1 identifiers

By the end of this chapter you can

  • Name the fifteen wms_ tables, what each holds, and which five columns are soft links to M12.
  • Read a location profile and a handling unit, and say why a unit holds one item and one lot.
  • Decode a GS1-128 pallet label by hand, including its check digit, and say when a parser must refuse it.

Fifteen tables on the shared spine

Every table in the module starts with the standard columns every platform table has: id, tenant_id, created_at, created_by, updated_at, updated_by, row_version, archived_at and ext. The module has no location list, item list, lot table or people table of its own: it points at core_storage_location, core_item, core_item_class, core_lot, core_uom, core_party, core_address and core_person. Its own tables share the prefix wms_.

The fifteen wms_ tablesFifteen tables in five groups: places and units (2), inbound (3), counting (2), rules and work (4) and outbound (4). A sixth panel lists the M12 tables reached by soft link: inv_inventory_transaction, inv_purchase_order and inv_sales_order. A strip below lists the spine tables they point at. PLACES AND UNITSwms_location_profilewms_handling_unitINBOUNDwms_inbound_shipmentwms_inbound_linewms_receiptCOUNTINGwms_cycle_countwms_cycle_count_lineRULES AND WORKwms_putaway_rulewms_replenishment_rulewms_wavewms_taskOUTBOUNDwms_outbound_orderwms_outbound_linewms_shipmentwms_packageSOFT LINKS TO M12inv_inventory_transactioninv_purchase_orderinv_sales_orderSpine tables they point at:core_storage_location, core_item, core_lot, core_party, core_person, core_uom
The fifteen tables. Places and units (2), inbound (3), counting (2), rules and work (4) and outbound (4). At the lower right, the three M12 tables reached by soft link. Along the bottom, the spine tables everything points at.

The table below is the full definition: every column the module adds to the standard ones, with its type and whether it is required (marked req). Types are written as in the specification: text, int, num (a decimal number), bool, date, ts (a timestamp with time zone), uuid, json and qty. A qty is an exact decimal quantity; the course stores it as numeric(18,6), up to 18 digits with six after the point, so quantities never pick up the rounding errors of floating-point numbers. An arrow means a foreign key to that table, and allowed values are written after a colon. It is what your agent builds from in Session 2.

TableHoldsColumns: type, req = required
wms_location_profileWMS attributes on a spine storage locationstorage_location_id → core_storage_location req, unique; zone_type text req: receiving, reserve, forward_pick, staging, shipping, quarantine, line_side or returns; max_volume_l num; max_weight_kg num; max_pallets int; pick_sequence int; putaway_sequence int; temperature_class text: ambient, cool, frozen or controlled; mixed_items_allowed bool; mixed_lots_allowed bool; pickable bool req
wms_handling_unitA license plate: pallet, case, tote, container or carton, with an optional SSCClpn text req, unique; hu_type text req: pallet, case, tote, container or carton; sscc text; parent_hu_id → wms_handling_unit; storage_location_id → core_storage_location; status text req: open, closed, in_transit, shipped or consumed; weight_kg num
wms_inbound_shipmentAn expected receipt (the ASN)asn_no text req, unique; supplier_party_id → core_party; carrier_party_id → core_party; po_ref uuid, a soft link to inv_purchase_order; expected_at ts; dock_door text; status text req: expected, arrived, receiving, received or closed
wms_inbound_lineOne line of the ASNinbound_shipment_id → wms_inbound_shipment req; item_id → core_item req; qty_expected qty req; qty_received qty; uom_id → core_uom req; lot_no text; expiry_date date
wms_receiptA scanned receipt into a handling unitinbound_line_id → wms_inbound_line; item_id → core_item req; qty qty req; uom_id → core_uom req; lot_id → core_lot; handling_unit_id → wms_handling_unit; received_by → core_person req; received_at ts req; qc_required bool req; damage bool; inventory_txn_ref uuid, a soft link to inv_inventory_transaction
wms_putaway_ruleA directed put-away rulepriority int req; item_class_id → core_item_class; zone_type text; strategy text req: fixed, nearest_empty, consolidate, velocity or fefo_zone; params json; active bool req
wms_replenishment_rulePick-face or line-side replenishmentstorage_location_id → core_storage_location req; item_id → core_item req; min_qty qty req; max_qty qty req; trigger text req: min_max, kanban or demand; source_zone text; active bool req
wms_waveA group of order lines released for pickingwave_no text req, unique; wave_type text req: wave, batch, zone, cluster or discrete; status text req: planned, released, picking, picked or closed; released_at ts; planned_ship_at ts
wms_taskA directed warehouse tasktask_no text req, unique; task_type text req: putaway, pick, replenish, move, count, load or unload; status text req: open, assigned, in_progress, done, short or cancelled; priority int; item_id → core_item; lot_id → core_lot; qty qty; uom_id → core_uom; from_location_id → core_storage_location; to_location_id → core_storage_location; handling_unit_id → wms_handling_unit; wave_id → wms_wave; order_line_ref uuid; assigned_to → core_person; created_at_task ts; completed_at ts; inventory_txn_ref uuid, a soft link to inv_inventory_transaction
wms_outbound_orderAn order to ship, from a sales order, a transfer or a production line-side requestorder_no text req, unique; order_type text req: customer, transfer, line_side or vendor_return; customer_party_id → core_party; sales_order_ref uuid, a soft link to inv_sales_order; ship_by ts; carrier_party_id → core_party; service_level text; ship_to_address_id → core_address; status text req: new, allocated, released, picking, packed, shipped or cancelled
wms_outbound_lineA line to pickoutbound_order_id → wms_outbound_order req; item_id → core_item req; qty_ordered qty req; qty_allocated qty; qty_picked qty; qty_shipped qty; uom_id → core_uom req; lot_requirement text
wms_shipmentA shipment with its carrier documentsshipment_no text req, unique; carrier_party_id → core_party; tracking_no text; bol_no text; shipped_at ts; total_weight_kg num; package_count int; status text req: open, loaded, shipped or delivered
wms_packageA packed carton or pallet in a shipmentshipment_id → wms_shipment req; handling_unit_id → wms_handling_unit; outbound_order_id → wms_outbound_order; length_cm num; width_cm num; height_cm num; weight_kg num; label_zpl text
wms_cycle_countOne instance of a count programmecount_no text req, unique; method text req: abc, location, item, ad_hoc or wall_to_wall; status text req: planned, counting, review or posted; scheduled_at date; blind bool req
wms_cycle_count_lineA counted location and item, with its variancecycle_count_id → wms_cycle_count req; storage_location_id → core_storage_location req; item_id → core_item; lot_id → core_lot; system_qty qty; counted_qty qty; variance qty; counted_by → core_person; recount bool; approved_by → core_person; inventory_txn_ref uuid, a soft link to inv_inventory_transaction

Four things in that table are easy to miss. First, a soft link is a column that holds the id of a row in another module's table without a database foreign key, so the modules install in any order. Five columns are soft: po_ref, sales_order_ref and the three inventory_txn_ref columns. Each inventory_txn_ref gets a unique index where it is not empty [5]. Second, the ASN table has no column for the time the truck arrived, so the route keeps it in ext as arrived_at; the dock-to-stock measure in chapter 4 reads it from there. Third, wms_task.qty is the quantity of the task, not a stock level. Fourth, wms_cycle_count_line has no status column, yet a count must post held and quarantined stock from the status it is in, so the route keeps the balance's status in the line's ext as balance_status, copied with system_qty when counting starts.

The specification has a sixteenth table, wms_dock_appointment (dock door, direction, carrier, start and end, status). It belongs to yard management, a best-in-class extension, and the course does not build it.

Locations: what a place allows

A location exists once, in the spine, as a core_storage_location (a code, a name, a parent and a status of active, blocked or inactive). The location profile adds what the warehouse needs to know about it: its zone type (receiving, reserve, forward_pick, staging, shipping, quarantine, line_side or returns), whether it is pickable, its capacity in volume, weight and pallets, its temperature class, whether it may mix items or lots, and two sequences. The pick sequence is the order a picker walks. The put-away sequence is the order put-away rules prefer. A location whose status is not active refuses every task into or out of it; that is how a location is frozen for a count.

LocationZone typePickablePick seq.Put-away seq.Max pallets
DOCK-INreceivingnononenone6
QUAR-1quarantinenononenone2
RES-A1reserveyes20104
RES-A2reserveyes30204
PICK-B1forward_pickyes10nonenone
STAGE-OUTstagingnononenone6

These six locations are the fixed seed that the course works by hand in Session 4. The dock, quarantine and staging locations may hold mixed items; the two reserve locations and the pick face take one item at a time, though they may hold several lots of it.

The six locations of the seedDOCK-IN receiving feeds RES-A1 and RES-A2 reserve and QUAR-1 quarantine by put-away. RES-A1 replenishes the PICK-B1 forward pick face. Picks go from PICK-B1, RES-A1 and RES-A2 to STAGE-OUT. A released quarantine lot is moved on to reserve, to RES-A2 in the worked example because RES-A1 is full. DOCK-INreceiving · 6 palletsRES-A1reserve · 4 palletsput-away seq 10QUAR-1quarantine · 2 palletsRES-A2reserve · 4 palletsput-away seq 20PICK-B1forward pick · min 45STAGE-OUTstaging6 palletsput-awayreplenishpickpick from reservepick from reservereleased: move to RES-A2
Six locations, four kinds of move. Put-away takes pallets from DOCK-IN to reserve or quarantine. Replenishment tops up the PICK-B1 face from RES-A1. Picks go to STAGE-OUT from the pick face and from both reserve locations. A quarantined lot, once released, is moved on to reserve: RES-A2 in the worked example, because RES-A1 is full.

Handling units and license plates

A license plate (LPN) is one unique number printed as a barcode on a pallet, case, tote, container or carton. One scan stands for everything on it. The wms_handling_unit table gives it a type, an optional SSCC, a location, a status and, for a carton on a pallet, a parent unit. A unit's status runs open, closed, in_transit, shipped, and consumed once it has been emptied.

The module follows one rule that keeps the data honest: a handling unit holds one item and one lot, and mixing is a property of a location, not of a unit. The unit does not hold a quantity either. Its item and lot are found by a view called wms_hu_lot, from its receipt; for a unit that was there at the start, from the opening ledger row; or, for a unit loaded in check mode, from the item and lot the loader stored in the unit's ext. The seed has three units: LPN-0001, a tote at PICK-B1, and LPN-0002 and LPN-0003, pallets at RES-A1 with SSCCs. Units the module creates are numbered from LPN-0101.

GS1 labels: SSCC, GTIN and the data fields

GS1 publishes the identification keys and barcode rules that suppliers, carriers and warehouses share [1]. Two keys matter here. A GTIN (Global Trade Item Number) identifies a kind of item. An SSCC (Serial Shipping Container Code) identifies one physical logistic unit, such as a pallet, so no two pallets share one. The supplier prints both, with the lot and expiry, on a GS1-128 label: Code 128 barcode symbology holding data fields, each starting with an application identifier (AI), a two-digit tag that says what the next characters mean [3].

AIMeaningFormatExample
00SSCC18 digits, fixed length076543210000001012
01GTIN14 digits, fixed length11234567000104
10Lot (batch)up to 20 characters, variable lengthL-5001
17Expiry date6 digits, YYMMDD, fixed length; read as 20YY270331 is 2027-03-31
21Serial numberup to 20 characters, variable lengthSN-0007
A GS1-128 label decodedThe label string (00)076543210000001012(01)11234567000104(17)270331(10)L-5001 split into four fields: AI 00 SSCC of 18 digits, AI 01 GTIN of 14 digits, AI 17 expiry in YYMMDD form, here 2027-03-31, and AI 10 lot, here L-5001. (00)076543210000001012(01)11234567000104(17)270331(10)L-500100 · SSCC07654321000000101218 digits, fixedthis pallet01 · GTIN1123456700010414 digits, fixedthe item17 · Expiry270331YYMMDD, fixed2027-03-3110 · LotL-5001up to 20 characterslast, so no separatorA field of variable length (10 lot, 21 serial) ends with character 29 (FNC1) unless it is last.A raw scan may start with the symbology identifier ]C1; the human form puts each identifier in brackets.
One label, four fields. The string in brackets is the human form. Identifier 00 is the pallet's SSCC, 01 the item's GTIN, 17 the expiry and 10 the lot. Fixed-length fields need no separator; the variable-length lot would need one only if another field followed it.

The SSCC is 18 digits: an extension digit, then the company's GS1 company prefix and a serial reference (16 digits together), then a check digit. To print SSCCs on your own cartons you need a GS1 company prefix from your national GS1 organisation; look up its price on GS1's own site rather than assuming one. Until you need it, the license plate alone identifies a carton.

The last digit of an SSCC or GTIN is a check digit, computed from the others so that a misread or mistyped digit is caught [2]. Starting from the right of the digits before it, multiply alternately by 3 and 1, add the products, and the check digit is what brings the sum up to the next multiple of ten.

A check digit worked by handThe thirteen digits 1 1 2 3 4 5 6 7 0 0 0 1 0 with weights 3 and 1 alternating from the right give products 3 1 6 3 12 5 18 7 0 0 0 1 0, a sum of 56, and so a check digit of 4. Weights run from the right: the digit next to the check digit is multiplied by 3, the next by 1, and so on.digitweightproduct133111236313431251563187170300100301110304check= 56Sum 56. The next multiple of ten is 60, and 60 minus 56 is 4.So the check digit is 4 and the full GTIN-14 is 11234567000104. A scan with any other last digit is refused.
A check digit worked by hand. Thirteen digits, weights 3 and 1 from the right, products added to 56. The next multiple of ten is 60, so the check digit is 4. The same method checks an SSCC.

A parser that reads labels does five things, and refuses everything else. It splits the string by the identifier table. It recomputes the check digit of 00 and 01 and refuses a mismatch. It refuses an impossible date, a lot longer than 20 characters and an unknown identifier. It accepts the bracketed human form, and the raw form with character 29 between variable fields and a leading ]C1. And the server runs it, not just the phone, because the server is where a scan is believed. The course simplifies two GS1 rules: GS1 reads the two-digit year inside a window around the current year, while the course always reads it as 20YY, which gives the same date for expiry dates from 2000 to 2050; and GS1 allows day 00 to mean no day specified, while the course refuses it with every other impossible date.

Test labelResultWhy
(00)076543210000001012(01)11234567000104(17)270331(10)L-5001decodesboth check digits correct, expiry 2027-03-31, lot L-5001
(01)11234567000104(21)SN-0007, character 29, (10)L-5001decodesthe variable-length serial is closed by the separator before the lot
(01)11234567000105refusedthe check digit should be 4, not 5
(17)270231refused31 February does not exist
Exercise · Read a pallet label by hand15 minutes

You need: Pen and paper or a text file, the identifier table from this chapter, and your AI coding agent for the last step

Work with this label from the seed's inbound shipment: (00)076543210000001029(01)11234567000104(17)270331(10)L-5001. Do steps 1 to 6 by hand before the agent sees it.

Outcome: A page of hand-worked checks for one label, two written reasons for refusal, and a parser your agent wrote that agrees with every one of them.

Knowledge check

A label reads (00)076543210000001012(01)11234567000104(17)270331(10)L-5001. What is the expiry date?

Knowledge check

A scan decodes as (01)11234567000105. What must the parser do?

References

  1. GS1: GS1 General Specifications. https://www.gs1.org/standards/barcodes-epcrfid-id-keys/gs1-general-specifications
  2. GS1: how to calculate a check digit manually. https://www.gs1.org/services/how-calculate-check-digit-manually
  3. GS1: reference list of GS1 application identifiers. https://ref.gs1.org/ai/
  4. PostgreSQL documentation: Constraints. https://www.postgresql.org/docs/current/ddl-constraints.html
  5. PostgreSQL documentation: Unique indexes. https://www.postgresql.org/docs/current/indexes-unique.html

Chapter 3 · Who does what, and how it is checked

Roles, screens and the scan-verified workflow

This chapter follows the seed's inbound shipment, ASN-1001, through the building. It starts with who is allowed to do each job and what the phone screen can and cannot decide, then walks receiving, quarantine, put-away, allocation, waves, picking, packing and shipping, saying at each step which ledger transaction is posted.

30 min5 roles + 2 identities10 routes, 9 screensFEFO: earliest expiry first

By the end of this chapter you can

  • Name the five roles, the two identities that are not people, and the rules that keep one person from approving their own work.
  • Describe a scan-verified task and say what the server does with a scan the screen accepted.
  • Work put-away and first-expired-first-out allocation by hand on the seed, and say which ledger transaction each step posts.

Five roles and two identities that are not people

Roles are written as jobs, not names, and each maps to a row in core_job_role. Everyone's access comes from the Role and Exposure Matrix, and row-level security (the database refusing rows a role may not see or change [1]) is generated from it.

RoleDoesReads directly (own site only)
wh_receiverReceives against ASNs, puts away, completes movesASNs, receipts, handling units, location profiles, put-away and move tasks
wh_pickerPicks, replenishes, moves and countsPick, replenish and move tasks, orders, waves, count lines without system_qty and variance
wh_packerPacks cartons and loads themOrders, pick and load tasks, shipments, packages
wh_supervisorEnters orders and ASNs, allocates, releases waves, assigns tasks, ships, approves countsEvery wms_ table, both views, the ledger tables and its balance view, and the core_ lookups (items, parties, lots, locations, people, addresses)
inventory_controlOwns locations and rules, releases stock from quarantine, plans and approves countsEvery wms_ table, both views, the ledger tables and its balance view, and the core_ lookups (items, parties, lots, locations, people, addresses)

Two identities are not people. wms_loader is a route-only identity used once, to import data, and removed at cut-over. wms_check is a read-only service identity for the script that proves the ledger agrees with the tasks, and for the reconciliation at cut-over. It reads the tasks, receipts, units, counts and ledger, and the location, item and lot tables so that it can turn the old tool's codes into ids; it writes nothing.

Roles and the jobs they holdFive roles against nine jobs. Receiver: receive and put away. Picker: pick and count. Packer: pack and load. Supervisor: ship and approve counts. Inventory control: approve counts and release stock from quarantine. ReceivePut awayPickPackLoadShipCountApproveReleasewh_receiverwh_pickerwh_packerwh_supervisorinventory_controlThe supervisor also enters orders and ASNs, allocates and releases waves; inventory control owns locations and rules and plans counts.Two rules bind everyone: the counter cannot approve their own line, and the receiver of a lot cannot release it.
Five roles, nine jobs. A large dot means the role holds the job. The receiver receives and puts away, the picker picks and counts, the packer packs and loads, the supervisor ships and approves counts, and inventory control approves counts and releases stock from quarantine.

Some rights are taken away on purpose. A restrictive rule can only remove rights: a row must pass it as well as the rule that grants the right, so no other grant can get around it [1]. Four are written. The person who counted a line cannot approve it. The person who received a lot cannot release it from quarantine. The loader inserts storage locations, location profiles, units and lots only with ext marked imported. And a picker's read of system_qty is refused, so a blind count, one where the counter is not shown the system quantity, stays blind. A column right is what lets a role read some columns of a table and not others.

Ten routes and nine screens

A route is one server address of your app, such as /api/warehouse/receive, that applies the rules in code so that a screen cannot skip them. Each route is written as its own row in the matrix, and each posts to the ledger only through inventory-post, which refuses a caller whose transaction type is not on its list.

RouteCalled byPosts to the ledger
wms-orderswh_supervisornothing; enters orders and ASNs, cancels orders
wms-receivewh_receiver; wh_supervisor closespo_receipt
wms-taskreceiver, picker, supervisor, inventory controltransfer_in
wms-allocatewh_supervisornothing; allocation does not change the ledger
wms-wavewh_supervisornothing
wms-packwh_packernothing; makes cartons and load tasks
wms-shipwh_packer loads; wh_supervisor shipsshipment
wms-releaseinventory_controlstatus_change, quarantine to available
wms-countinventory_control plans; picker counts; supervisor or inventory control approvescycle_count
wms-importwms_loaderadjustment with reason OPENING
ScreenSurfaceFor
/warehouse/receiveOperator, on the phonewh_receiver
/warehouse/tasksOperator, on the phonewh_receiver and wh_picker: put-away, move, pick, replenish
/warehouse/pack and /warehouse/loadOperator, on the phonewh_packer
/warehouse/countOperator, on the phonewh_picker, blind
/warehouse/ordersDeskwh_supervisor: orders, ASNs, allocation, waves, assignment
/warehouse/shippingDeskwh_supervisor; wh_packer reads: shipments and the bill of lading
/warehouse/controlDeskinventory_control: locations, rules, release, count planning; count approvals for wh_supervisor too
/warehouse/kpisManagerwh_supervisor and inventory_control, read only

A task is one verified move

A task is one directed move. The specification allows seven task types (putaway, pick, replenish, move, count, load, unload); the course uses putaway, pick, replenish, move and load, and treats the count lines as the work list for counts. A task is created by a route, claimed or assigned to a person, done by scan, and ends in one of a few statuses.

Task statusA task moves open, assigned, in progress, done. From open it can be cancelled when its order is cancelled; from in progress it can end short when fewer were found than asked; the quantity found is still posted to the ledger. opencreated by a route;a picker may claim itassigneda supervisor nameda personin_progressthe person isscanningdoneevery scan matched;ledger row id keptcancelledorder cancelled whilenew or allocatedshortfewer found than asked;the line stays openorder cancelledfewer foundA task is the unit of work and of proof: each completed task points at its own ledger row.
Task status. A task runs open, assigned, in progress and done. It is cancelled when its order is cancelled while still new or allocated, and ends short when fewer were found than asked, leaving the order line open. The quantity that was found is still posted to the ledger.

The scan screen shows one task at a time in large type: the location to go to and the unit to scan. It takes a scan two ways: the phone's camera, read by a barcode library in the page (free, and Code 128 is the symbology GS1-128 is printed in), or a handheld scanner that types the code into the focused box like a keyboard (a hardware cost if you do not own one, so use the camera first). The confirm button stays disabled until the scans match the task. But the screen is not the rule.

A scan-verified moveFive screen steps: the task is shown, the location is scanned, the unit is scanned, a quantity is keyed where needed, and confirm is enabled when the scans match. The server then checks every scan again: a match posts one ledger row, a mismatch writes a SCAN-REFUSED comment and posts nothing. 1 · Task shownone at a time,in large type2 · Locationscan the labelon shelf or floor3 · Unitscan the pallet orcarton plate4 · Quantitykeyed only wherethe task needs it5 · Confirmenabled whenthe scans matchSCAN-REFUSEDcomment on the task;nothing is postedServer checks againwhatever the screen didLedger row postedone per task,same transactionno matchmatchThe confirm button is a convenience; the server's check is the rule.
The phone guides; the server decides. Five screen steps lead to a confirm button that is enabled only when the scans match. The server then checks every scan again. A match posts one ledger row. A mismatch writes a SCAN-REFUSED comment on the task and posts nothing.
  • Each route calls inventory-post inside its own database transaction, with a client_ref built from the task, and writes the transaction id back to the task.
  • The server checks every scan again, whatever the screen did.
  • A refused scan is written as a SCAN-REFUSED comment on the task, and refused.
  • The screen refuses to confirm with no connection instead of queueing the confirm, because a queued confirm could post stock into a location that was frozen in the meantime.

Receiving, quarantine and put-away

An ASN runs expected, arrived, receiving, received, closed. For ASN-1001 the receiver marks the truck arrived, then scans the DOCK-IN label and each supplier pallet label and keys the pallet quantity. For each pallet the route decodes the label, matches the GTIN to the item and the lot and expiry to the ASN line, creates the core_lot once, creates the handling unit with the label's SSCC and the next license plate, inserts the receipt, and posts a po_receipt into DOCK-IN. It refuses a quantity that would take the line above the ASN quantity (receipt_over_pct is 0 in the course), a label whose lot differs from the line, and a scan that matches no line. Blind receipt, receiving with no ASN, is not built: the supervisor enters the ASN first. Damage capture and an under-receipt tolerance, which the specification lists for receiving, are not built either: wms_receipt.damage exists and stays false, and each pallet is received in full.

Quarantine. Items of class INSPECT are received with qc_required true. Their lot is created with status quarantine (other lots, such as FG-10's L-5001, are created released), the po_receipt goes in with to_status quarantine, and the event wms.receipt.qc_required is raised. Stock in quarantine, or on hold from quality, is never offered to an order. It leaves quarantine only when inventory control posts a status change from quarantine to available at the location, with the quality result in a comment, and the route refuses the person who received it.

Directed put-away. Receiving creates one put-away task per pallet. The route reads the active rules in priority order and takes the first that finds a location which is active, has room under max_pallets (counting units there not yet shipped or consumed), and accepts the mixing. The specification names five strategies: fixed, nearest_empty, consolidate, velocity and fefo_zone. The course uses the first three.

Put-away rules in priority orderThree rules: priority 10, class INSPECT, fixed to QUAR-1; priority 20, class STD-FG, consolidate into the reserve location already holding the item; priority 90, any class, nearest empty reserve location. The first rule that finds a qualifying location wins. Rules are tried in priority order; the first one that finds a location wins.10 · class INSPECTzone quarantinefixedalways the location in params: QUAR-1no location found: next rule20 · class STD-FGzone reserveconsolidatethe reserve location that already holds the item,lowest put-away sequence, with pallet room leftno location found: next rule90 · any other classzone reservenearest_emptythe reserve location with no unit in it,lowest put-away sequenceA location qualifies only if it is active, has room under max_pallets and accepts the mixing.
Three seed rules, first match wins. Priority 10 sends class INSPECT to QUAR-1. Priority 20 consolidates class STD-FG into the reserve location already holding the item. Priority 90 takes the nearest empty reserve location for anything else.

For ASN-1001 the expected result is: the two FG-10 pallets, LPN-0101 and LPN-0102, to RES-A1 by consolidate (which then holds 4 of its 4 pallets), and the FG-20 pallet, LPN-0103, to QUAR-1 by the fixed rule. The receiver completes each task by scanning the destination location and the pallet; the route posts a transfer_in from DOCK-IN to the destination, with from_status and to_status both set to the status the stock is in now, and sets the task done. A fifth pallet for RES-A1 would go to RES-A2 by the nearest_empty rule. Dock-to-stock time for each receipt is the put-away's completed_at minus the ASN's arrival time.

Allocation: first expired, first out

Allocation decides which stock an order takes. The rule is FEFO: earliest expiry first, then the lowest pick sequence, then the lowest license plate. A lot with no expiry date sorts after all dated lots, oldest lot first (FIFO, first in first out, by the lot's created_at). It reads only a view called wms_allocatable: stock in locations that are pickable, in a reserve or forward_pick zone and active, for lots whose status is released, less the quantity of pick and replenish tasks still open for the same item, lot and location. That one view is why held and quarantined stock cannot be offered: it is not in the view. The view is created with security_invoker, which makes it apply the rights of the person reading it, not of its owner [2].

FG-10 lotExpiryQuantityWhereStatusOffered?
L-40032026-11-3020RES-A1on holdnever
L-40022026-12-3130RES-A1releasedfirst
L-40012027-01-3150PICK-B1releasedsecond
First expired, first outBars to scale for FG-10: lot L-4003, 20 units, expires 2026-11-30 and is on hold; lot L-4002, 30 units, expires 2026-12-31; lot L-4001, 50 units, expires 2027-01-31. An order for 40 takes 30 from L-4002 and 10 from L-4001 and never touches the held lot. Stock of FG-10 by earliest expiry, bar length to scale (units)01020304050L-4003 · expires 2026-11-3020 · on hold, never offeredL-4002 · expires 2026-12-3130 · RES-A1, availableL-4001 · expires 2027-01-3150 · PICK-B1, availableOUT-2001 line 1: 40 EA30 from L-4002, then 10 from L-4001L-4003 expires first but is on hold, so it is never offered; the order takes the earliest expiry among released lots.
FEFO on the seed, to scale. An order for 40 of FG-10 takes 30 from L-4002 and then 10 from L-4001. Lot L-4003 expires first but is on hold, so it is never offered.

Allocation creates one open pick task per source, to STAGE-OUT, with no wave yet. It does not change the ledger: stock stays available until a picker moves it. For OUT-2001 line 2 (12 of FG-20) nothing is offered at first, because lot L-6001 is in quarantine; qty_allocated stays 0 and the order stays new until inventory control releases the lot and the line is allocated again.

Waves, picking, replenishing and shipping

A wave is a group of order lines released together for picking. The specification allows five wave types. The course builds the first.

wave_typeWhat it means
waveOrder lines released together for picking at a planned time
batchLines for the same item from several orders picked in one trip, then split by order
zoneEach picker works one area, and orders pass between areas
clusterOne picker fills several orders' containers on one trip
discreteOne order picked at a time, start to finish

Releasing the wave gives the open pick tasks the wave and orders them for walking by the locations' pick sequence (PICK-B1 10, then RES-A1 20, then RES-A2 30). The order becomes released. A picker claims a task, goes to the location, scans the location and the unit, keys the quantity, answers whether the unit is now empty, and scans the destination. The route posts a transfer_in to STAGE-OUT from status available to status allocated and moves the line's qty_picked. If fewer are found than asked, the task ends short with the quantity found, that quantity is posted in the same way, and the line stays open. A unit that is emptied becomes consumed and has no location.

Replenishment follows a pick. After the first pick, PICK-B1 holds 40 of FG-10, below the rule's minimum of 45, so the route creates one replenish task for 60 (the maximum 100 less 40) from the earliest-expiry free unit in a reserve location (one that is not on hold and not held by an open pick task). A min/max rule works like that: top up to the maximum when stock falls below the minimum. A kanban trigger is the line-side version: the production line hands back a card when a bin is empty, a scan of the card at the line-side location creates one replenish task for the quantity the card asks for (the rule's max_qty in the course), and the task moves that quantity from reserve. The seed has no line-side location, so Session 4 proves it in a test that rolls back. A pull signal sent by a production system is not built.

Packing and shipping. The packer scans each item and a new carton label into cartons. An item's GTIN names the item but not the lot, so where an order was picked from two lots the pack screen lists the order's done pick tasks and the packer chooses the lot from them; a label that also carries identifier 10 fills the lot in. The route makes the shipment, one wms_package per carton and one load task per carton, and sets package_count and total_weight_kg itself, ignoring any total a browser sends. The label text is stored in wms_package.label_zpl (ZPL is the language Zebra label printers read; the free way is a plain label printed from the browser with the same lines). The packer then loads: scanning STAGE-OUT and each carton posts a shipment from status allocated. The supervisor ships with a tracking number and the route sets the bill of lading number; the module calls no carrier service, so there is nothing to pay. The bill of lading prints the ship-to address when the order has one; the seed's OUT-2001 has none, so it shows the customer by name only.

StepLedger transactionStatus change
Receivepo_receipt into DOCK-INavailable (quarantine for class INSPECT)
Put-away, move, replenishtransfer_in, from and to locations both set, and from_status and to_status both set to the status the stock is in nowunchanged
Release from quarantinestatus_change at QUAR-1quarantine to available
Allocatenonenone
Picktransfer_in to STAGE-OUT (a short pick posts the quantity found)available to allocated
Loadshipment from STAGE-OUTfrom allocated
Count variancecycle_count out of the counted location when it finds less, into it when it finds morefrom the line's balance_status, kept in the line's ext
Exercise · Allocate first expired, first out by hand15 minutes

You need: Paper or a text file, the FG-10 stock table from this chapter, and your AI coding agent for the last step

Use the seed's FG-10 stock and its order OUT-2001. Do steps 1 to 5 by hand, then let the agent try to match you.

Outcome: Hand-worked allocations of 40 and 200 units, with the pick tasks listed and the held lot untouched, and a test that proves your route gives the same answer.

Knowledge check

OUT-2001 asks for 40 of FG-10. Lot L-4003 expires first, but it is on hold. Which lot does FEFO take first?

Knowledge check

Lot L-6001 is in quarantine and an order needs it. Who may release it, and what must they record?

References

  1. PostgreSQL documentation: Row security policies (including restrictive policies). https://www.postgresql.org/docs/current/ddl-rowsecurity.html
  2. PostgreSQL documentation: CREATE VIEW (the security_invoker option). https://www.postgresql.org/docs/current/sql-createview.html
  3. W3C: Understanding WCAG 2.2, Contrast (Minimum). https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
  4. W3C: Understanding WCAG 2.2, Target Size (Enhanced). https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html

Chapter 4 · From the old tool to the new book

Move the data, count, and cut over

A warehouse module earns trust on the day its numbers match the old tool's, and keeps it by being counted. This chapter loads the location master and the stock on every license plate as ledger transactions, shows how a blind count finds and corrects a difference, lists the checks that prove the book, and ends with the checks to pass before the old tool is switched off.

20 min5 count methods19 ledger rows at site WH1 in the worked example5 KPIs

By the end of this chapter you can

  • Load locations and opening balances by license plate as ledger transactions that are safe to run twice.
  • Run a blind cycle count and a wall-to-wall count with the variance approved by someone other than the counter.
  • Prove the book with the two check scripts, measure the KPIs, and list what must pass before cut-over.

Opening balances: the location master, then stock by license plate

The specification's migration rule is short: load the location master, then load opening balances by license plate through M12 opening transactions, and run a wall-to-wall count at cut-over. Two things must already be in place: items and units of measure, which M12's import owns (the warehouse loader creates neither; that import also loads locations and lots, which the loader looks up by code, and by item and lot number, and reuses, creating one only when none exists), and the old tool's exports, taken again from the vendor's own export page just before the load.

Loading the warehouseFive steps in order: items and units loaded first by M12's import, then locations (reused when M12 already loaded them, otherwise created) with parents before children, then handling units, then opening rows as adjustment transactions with reason OPENING, then a three-way reconciliation. The loader runs as a dry run by default, in post mode or in check mode. Items, unitsloaded first byM12's importLocationsreuse or create;parents firstHandling unitsone item, one lot;pallets firstOpening rowsadjustments withreason OPENINGReconcileold report, exportand ledger agreeThree ways to run the loader (as the wms_loader identity, through the wms-import route)Dry run (the default)reports, changes nothingPost modeone OPENING row per unitCheck modecompares, posts nothing
Order of loading. Items and units first (M12), then locations with parents before children, then handling units, then the opening rows, then a reconciliation. The loader runs as a dry run by default, in post mode, or in check mode.
  • Choose the mode. If M12 already posted opening stock by item, lot and location, run the loader in check mode: it reuses the locations and lots M12 loaded, adds the location profiles and units (writing each unit's item and lot into its ext, so wms_hu_lot can name them), and compares quantities with the ledger, posting nothing. Otherwise run post mode. Write the choice and the reason down.
  • Write the mapping first. Old location to core_storage_location and wms_location_profile; old zone names to the eight zone types; old unit types to hu_type; old stock statuses to a balance status (available, quarantine, hold, damaged) and to a lot status (released, quarantine, on_hold, rejected).
  • One item, one lot per unit. A license plate the old tool shows with mixed contents is refused and listed. Split it in the old tool or on the floor, then load it.
  • Closed locations load blocked or inactive, never active.
  • Each accepted row is one transaction of type adjustment, reason OPENING, ref_type wms_handling_unit and ref_id the unit, dated at the cut-off and carrying the status the old tool shows, posted through inventory-post and never written into the balance table.
  • Safe to run twice. The key of each row (license plate, item, lot, status) is its client_ref, so a second run adds nothing.
  • Dry-run first. The default run reports what it would do and changes nothing; it writes only with an explicit flag. Fix the mapping, not the data, until nothing is unmatched or every reject is explained.

Then reconcile on-hand by location three ways: the old tool's own report, your export, and the ledger (the view that recomputes balances from the ledger, grouped by location, item, lot and status). They agree, or each difference is written down with its cause. Anything you cannot explain is an error to find, not a rounding to accept. Check also that stock on hold or in quarantine sits in the balance table with that status and is absent from wms_allocatable.

Counting: cycle counts, blind counts and approval

A cycle count counts a few locations or items on a schedule instead of the whole building at once. The specification allows five methods: abc (items ranked by value or movement, the top class counted most often), location, item, ad_hoc, and wall_to_wall (everything, used at cut-over). Three controls make a count worth believing.

  • Blind. The counter is not shown the system quantity, so the count is what they saw and not what they expect. The route copies the system quantity when counting starts, with the balance's status kept in the line's ext as balance_status so held and quarantined stock is posted from its own status, and the picker's role cannot read the quantity.
  • Recount. A variance beyond the tolerance (recount_tolerance_units, 1 in the course) flags the line for a recount before anything else happens.
  • Approval by someone else. A person who did not count the line approves it. Only then does the route post one cycle_count transaction for each line with a variance. A line with no variance posts nothing, because a ledger quantity must be above zero.
A cycle count from plan to postSix steps: plan the count, count blind, recount when the variance is beyond the tolerance, review, approve by a person who did not count the line, and post one cycle_count transaction for each line with a variance. 1 · Planinventory control: method,blind true, locations2 · Count blindthe picker counts; thesystem quantity is hidden3 · Recounta variance beyond tolerance(1 unit) forces a recount4 · Reviewcounts side by side;status review5 · Approvea person who did notcount the line6 · Postone cycle_count row perline with a varianceCNT-0001, PICK-B1: lot L-4001 system 40, counted 38 twice, variance minus 2; lot L-5001 system 60, counted 60.Only the first line posts. Inventory record accuracy: 1 of 2 lines within tolerance, 50 percent.
A count from plan to post. Plan, count blind, recount if needed, review, approve by someone other than the counter, post. The example is the course's CNT-0001: one line off by two and posted, one line right and not posted.

The measure that comes out is inventory record accuracy: count lines within tolerance divided by count lines. In the example, line one is off by 2 against a tolerance of 1 and line two is exact, so 1 of 2 lines is within tolerance: 50 percent. For the wall-to-wall count at cut-over, block every location in the pilot area first (a blocked location refuses moves but allows counting), count blind in pick-sequence order, recount, approve, and unblock.

Proving the book

Two scripts prove the module against the ledger. scripts/check-wms.mjs, run as the read-only wms_check identity [4], checks that every complete task, meaning done, or short with a quantity above zero, has exactly one ledger row (not a reversal) whose ref_type is wms_task and whose id matches the task's inventory_txn_ref; that no ledger row names a task that is not complete (a short task with nothing picked has no row); and that every receipt and every approved, non-zero count line has its row. M12's scripts/check-balances.mjs checks that balances recomputed from the ledger equal the stored balances. A check you have never seen fail proves nothing, so in a scratch copy edit one balance by hand and see check-balances.mjs fail; then, in the scratch copy, clear the inventory_txn_ref of one done task (or mark a task done with no ledger row) and see check-wms.mjs fail.

The ledger rows of the workflow at site WH1Bars to scale, counting only rows whose from or to location is one of the six WH1 locations, so M12's own rows are left out: opening balances 3, receipts 3, put-aways 3, quality release 1, move 1, picks 3, replenishment 1, loads 3 and count variance 1, which makes 19 rows. Eleven of them, one for each done task, are put-aways, the move, picks, the replenishment and loads. Ledger rows at site WH1 after the Session 4 workflow, bar length to scaleOpening balances3Receipts (po_receipt)3Put-aways3Quality release1Move1Picks3Replenishment1Loads (shipment)3Count variance119 rows at WH111 green: one per done task8 cyan: receipts, the openingload, a release and a count
19 ledger rows at site WH1, to scale. Counting only rows whose from or to location is one of the six WH1 locations, so M12's own rows in the same ledger are left out, the Session 4 workflow posts 3 opening rows, 3 receipts, 3 put-aways, 1 quality release, 1 move, 3 picks, 1 replenishment, 3 loads and 1 count variance. The 11 green rows are one for each of the 11 done tasks.
FG-10 on hand, to scaleA bar of 158 units drawn to scale: 38 of lot L-4001 and 60 of lot L-5001 at PICK-B1, 40 of lot L-5001 at RES-A1, all available, and 20 of lot L-4003 at RES-A1 on hold. 138 are available. A second short bar on the same scale shows FG-20: 12 units at RES-A2. FG-10 on hand after the workflow: 158 units, drawn to scale38PICK-B1 · L-4001available60PICK-B1 · L-5001available40RES-A1 · L-5001available20RES-A1 · L-4003on hold138 available20 on holdFG-20 on hand, same scale: 12 units12 · RES-A2
FG-10 on hand at the end, to scale. 158 units: 38 and 60 at PICK-B1, 40 at RES-A1, all available, and the 20 of lot L-4003 on hold. 138 are available. FG-20 has 12 units at RES-A2.

Those numbers can all be checked by hand. The 19 ledger rows are those at site WH1, whose from or to location is one of the six WH1 locations: M12's own seed and workflow rows sit in the same ledger at M12's locations and are not counted. The 11 done tasks are 3 put-aways, 1 move, 3 picks, 1 replenishment and 3 loads, and 11 task rows appear in the ledger. 17 events reach the outbox, counted on the owner connection your migrations run as because no role in the matrix reads it: 3 receipt.posted, 1 receipt.qc_required, 11 task.completed, 1 shipment.shipped and 1 count.variance.

KPIDefinitionHow the module measures it
Dock-to-stock timeHours from arrival to put-away confirmedThe put-away task's completed_at minus the ASN's arrival time kept in ext
Pick accuracyLines picked correctly ÷ lines pickedDone pick tasks with no SCAN-REFUSED comment ÷ done plus short pick tasks, so a short pick counts against it
Order cycle timeRelease to ship hoursFrom the wave's released_at to the shipment's shipped_at
Inventory record accuracyCount lines within tolerance ÷ count linesFrom wms_cycle_count_line
Lines picked per labour hourThroughput productivityNot measured in the course: labour standards are a best-in-class extension

Cutting over

The old tool and the module run side by side for at least one week, with every receipt, put-away, pick, pack, shipment and count in the pilot area entered in both by the same people. Compare the week's four measures for both systems against the baselines you took in Session 1. The two pick-accuracy figures are not the same measure: the module's counts refused scans and short picks, the old tool's usually counts mistakes found at pack check or by customers, so say so beside the comparison. The module should be at least as accurate as the old tool, and every miss needs a reason. A difference you cannot explain means you are not ready.

The path to cut-overEight steps in order: a parallel week, comparing on-hand and open orders, the module checks, a wall-to-wall count, the cut-over, closing the loader, watching day one, then cancelling the old tool. Parallel weekboth tools at once,a week or moreCompareon-hand by location,open orders and ASNsModule checksboth check scripts,hold test, parserWall-to-wall countlocations blocked,blind, approvedCut overfinal export stored;old tool read-onlyClose the loaderremove wms_loader;inserts now refusedWatch day oneKPIs, one receiptand one order shippedCancelafter the export;saving from invoicesA difference you cannot explain means you are not ready: go back a step.
The path to cut-over. A parallel week, a comparison, the module checks, a wall-to-wall count, the cut-over itself, closing the loader, watching day one, then cancelling. Each step must pass before the next.
CheckPasses when
Parallel weekOn-hand by location, open orders and open ASNs are compared each day, and every difference has a written cause
MeasuresPick accuracy and inventory record accuracy for both systems sit beside the Session 1 baselines, and the module is at least as accurate
Module checksBoth check scripts pass; a held lot is not offered by allocation; the parser decodes its test labels; the access tests pass, including that a counter cannot approve their own line
Wall-to-wall countBlind, recounted, approved by a person who did not count, with the lines counted, recounted and posted written down
Phone checkThe receive, task, pack, load and count screens pass at 360 and 390 pixels, and a receiver, a picker and a packer have used them on a real phone
Cut-over planA date, a rollback plan, the last transaction time in the old tool, who tells the floor, and a final export of locations, on-hand, history, users and reports stored by the company
Close the loaderwms_loader is removed from the matrix, and a test shows an insert through wms-import is now refused

Cancel last. Read the vendor's cancellation terms from your own account page: the notice period, the renewal date and how long you can still export after cancelling. Cancel only after the cut-over works and the final export is stored. The saving is the old cost for a year taken from the invoice, minus what the module costs to run on the services you already pay for, plus any scanners or printers you bought, shown as arithmetic from real invoices and never as an estimate. Record any leased devices on the old bill as their own line.

Exercise · Reconcile one location against the ledger15 minutes

You need: Your old tool's on-hand-by-location report, your loaded module data or a scratch copy of it, and your AI coding agent

Work with one real or made-up location from your pilot area. Use your own data, never a row of someone else's.

Outcome: A one-location reconciliation with the old report, the ledger quantity, the difference and a checked cause for each row, and a decision of explained or not ready.

Knowledge check

The loader is run twice by mistake. Why does it not double the stock?

Knowledge check

recount_tolerance_units is 1. A counted line is 2 units short of the system quantity. What happens next?

References

  1. Made4net: which is the best WMS for business (core and extended capabilities). https://made4net.com/knowledge-center/which-is-the-best-wms-for-business/
  2. Solutions Review: 2023 Magic Quadrant for WMS, key takeaways. https://solutionsreview.com/enterprise-resource-planning/?p=5624
  3. Infor: Gartner Magic Quadrant for WMS (core versus extended capabilities). https://dam.infor.com/api/public/content/d8957bc1ac164bf1843965deeaa7e26a
  4. PostgreSQL documentation: Privileges (granting and revoking rights). https://www.postgresql.org/docs/current/ddl-priv.html

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. Where does the warehouse module keep the quantity of a lot in a location?
2. What confirms a pick in the module?
3. How many ledger transactions does each completed task produce?
4. In a GS1-128 label, which application identifier carries the lot number?
5. The first thirteen digits of a GTIN-14 are 1123456700010. What is its check digit?
6. Under the seed's put-away rules, where does the pallet of FG-20 (class INSPECT) go?
7. OUT-2001 asks for 40 of FG-10. Lot L-4003 expires first but is on hold. What does allocation take?
8. A receiver scanned in lot L-6001, which is in quarantine. Who may release it?
9. A blind count of PICK-B1 finds lot L-4001 at 38 against a system quantity of 40, with a tolerance of 1. What happens?
10. How does the loader bring in opening stock?
11. Which messages do ANSI X12 856 and UN/EDIFACT DESADV carry?
12. When may the old warehouse system's subscription be cancelled?