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.
- 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.
| Job | What a person does, by scan | Ledger transaction |
|---|---|---|
| Receiving | Scans the dock label and each supplier pallet label, keys the quantity | po_receipt into DOCK-IN |
| Put-away | Moves a received pallet to the location the rules chose, scans both labels | transfer_in from the dock to the shelf |
| Replenishment | Tops up a pick face (the shelf or bin pickers take from all day) from reserve | transfer_in from reserve to the pick face |
| Picking | Takes allocated stock to staging, scanning location and unit | transfer_in to staging, available to allocated |
| Packing | Scans items into cartons; the route makes the shipment and one load task per carton | none of its own |
| Shipping | Scans staging and each carton onto the truck; a supervisor ships | shipment from allocated |
| Cycle counting | Counts locations on a schedule instead of the whole building at once | cycle_count, only for lines with a variance |
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.
| Capability | What it does | Tier |
|---|---|---|
| Location management | Location profiles on spine storage locations: zone type, capacity (volume, weight, pallets), pick and put-away sequences, temperature class, mixing rules, blocking | MVP |
| Receiving | ASN-driven or blind receipt, label printing (GS1-128, SSCC), over and under tolerance, damage capture, quality routing to quarantine | MVP |
| Put-away | Directed put-away by rules (fixed, nearest empty, consolidate, velocity), scan to confirm | MVP |
| Inventory control | License-plate tracking, status changes, holds from quality, FEFO and FIFO, moves and consolidations | MVP |
| Picking and packing | Scan picking on a handheld or phone with short-pick handling, packing with cartonisation, labels | MVP |
| Cycle counting | ABC, location, item and ad-hoc counts; blind counts; recount thresholds; approval of variances | MVP |
| Replenishment | Min/max and demand replenishment to pick faces; kanban or pull to line-side for production | STD |
| Order allocation and waves | Allocation by rules, wave, batch, zone, cluster and discrete picking, pick path sequencing | STD |
| Shipping | Staging, loading verification, bill of lading, carrier labels and tracking, ASN out | STD |
| Task interleaving and labour | Interleaved tasks, productivity tracking against engineered standards | BIC |
| Slotting and yard | Velocity-based slotting recommendations; dock appointments | BIC |
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
| Standard | Used in this module for | Where you meet it |
|---|---|---|
| GS1 General Specifications | GTIN, SSCC and the GS1-128 data fields: identifiers 01 GTIN, 10 lot, 17 expiry, 21 serial and 00 SSCC | Chapter 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 applies | Serial 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 principles | Shelf-life-driven allocation and rotation | Chapter 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
- 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
- Made4net: which is the best WMS for business (core and extended capabilities). https://made4net.com/knowledge-center/which-is-the-best-wms-for-business/
- Solutions Review: 2023 Magic Quadrant for WMS, key takeaways. https://solutionsreview.com/enterprise-resource-planning/?p=5624
- U.S. FDA: Drug Supply Chain Security Act (DSCSA). https://www.fda.gov/drugs/drug-supply-chain-integrity/drug-supply-chain-security-act
- ASC X12: the standards body for the X12 EDI transaction sets. https://x12.org/
- 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 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.
| Table | Holds | Columns: type, req = required |
|---|---|---|
| wms_location_profile | WMS attributes on a spine storage location | storage_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_unit | A license plate: pallet, case, tote, container or carton, with an optional SSCC | lpn 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_shipment | An 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_line | One line of the ASN | inbound_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_receipt | A scanned receipt into a handling unit | inbound_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_rule | A directed put-away rule | priority 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_rule | Pick-face or line-side replenishment | storage_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_wave | A group of order lines released for picking | wave_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_task | A directed warehouse task | task_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_order | An order to ship, from a sales order, a transfer or a production line-side request | order_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_line | A line to pick | outbound_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_shipment | A shipment with its carrier documents | shipment_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_package | A packed carton or pallet in a shipment | shipment_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_count | One instance of a count programme | count_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_line | A counted location and item, with its variance | cycle_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.
| Location | Zone type | Pickable | Pick seq. | Put-away seq. | Max pallets |
|---|---|---|---|---|---|
| DOCK-IN | receiving | no | none | none | 6 |
| QUAR-1 | quarantine | no | none | none | 2 |
| RES-A1 | reserve | yes | 20 | 10 | 4 |
| RES-A2 | reserve | yes | 30 | 20 | 4 |
| PICK-B1 | forward_pick | yes | 10 | none | none |
| STAGE-OUT | staging | no | none | none | 6 |
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.
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].
| AI | Meaning | Format | Example |
|---|---|---|---|
| 00 | SSCC | 18 digits, fixed length | 076543210000001012 |
| 01 | GTIN | 14 digits, fixed length | 11234567000104 |
| 10 | Lot (batch) | up to 20 characters, variable length | L-5001 |
| 17 | Expiry date | 6 digits, YYMMDD, fixed length; read as 20YY | 270331 is 2027-03-31 |
| 21 | Serial number | up to 20 characters, variable length | SN-0007 |
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 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 label | Result | Why |
|---|---|---|
| (00)076543210000001012(01)11234567000104(17)270331(10)L-5001 | decodes | both check digits correct, expiry 2027-03-31, lot L-5001 |
| (01)11234567000104(21)SN-0007, character 29, (10)L-5001 | decodes | the variable-length serial is closed by the separator before the lot |
| (01)11234567000105 | refused | the check digit should be 4, not 5 |
| (17)270231 | refused | 31 February does not exist |
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
- GS1: GS1 General Specifications. https://www.gs1.org/standards/barcodes-epcrfid-id-keys/gs1-general-specifications
- GS1: how to calculate a check digit manually. https://www.gs1.org/services/how-calculate-check-digit-manually
- GS1: reference list of GS1 application identifiers. https://ref.gs1.org/ai/
- PostgreSQL documentation: Constraints. https://www.postgresql.org/docs/current/ddl-constraints.html
- 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.
| Role | Does | Reads directly (own site only) |
|---|---|---|
| wh_receiver | Receives against ASNs, puts away, completes moves | ASNs, receipts, handling units, location profiles, put-away and move tasks |
| wh_picker | Picks, replenishes, moves and counts | Pick, replenish and move tasks, orders, waves, count lines without system_qty and variance |
| wh_packer | Packs cartons and loads them | Orders, pick and load tasks, shipments, packages |
| wh_supervisor | Enters orders and ASNs, allocates, releases waves, assigns tasks, ships, approves counts | Every wms_ table, both views, the ledger tables and its balance view, and the core_ lookups (items, parties, lots, locations, people, addresses) |
| inventory_control | Owns locations and rules, releases stock from quarantine, plans and approves counts | Every 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.
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.
| Route | Called by | Posts to the ledger |
|---|---|---|
| wms-orders | wh_supervisor | nothing; enters orders and ASNs, cancels orders |
| wms-receive | wh_receiver; wh_supervisor closes | po_receipt |
| wms-task | receiver, picker, supervisor, inventory control | transfer_in |
| wms-allocate | wh_supervisor | nothing; allocation does not change the ledger |
| wms-wave | wh_supervisor | nothing |
| wms-pack | wh_packer | nothing; makes cartons and load tasks |
| wms-ship | wh_packer loads; wh_supervisor ships | shipment |
| wms-release | inventory_control | status_change, quarantine to available |
| wms-count | inventory_control plans; picker counts; supervisor or inventory control approves | cycle_count |
| wms-import | wms_loader | adjustment with reason OPENING |
| Screen | Surface | For |
|---|---|---|
| /warehouse/receive | Operator, on the phone | wh_receiver |
| /warehouse/tasks | Operator, on the phone | wh_receiver and wh_picker: put-away, move, pick, replenish |
| /warehouse/pack and /warehouse/load | Operator, on the phone | wh_packer |
| /warehouse/count | Operator, on the phone | wh_picker, blind |
| /warehouse/orders | Desk | wh_supervisor: orders, ASNs, allocation, waves, assignment |
| /warehouse/shipping | Desk | wh_supervisor; wh_packer reads: shipments and the bill of lading |
| /warehouse/control | Desk | inventory_control: locations, rules, release, count planning; count approvals for wh_supervisor too |
| /warehouse/kpis | Manager | wh_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.
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.
- Each route calls inventory-post inside its own database transaction, with a
client_refbuilt 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.
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 lot | Expiry | Quantity | Where | Status | Offered? |
|---|---|---|---|---|---|
| L-4003 | 2026-11-30 | 20 | RES-A1 | on hold | never |
| L-4002 | 2026-12-31 | 30 | RES-A1 | released | first |
| L-4001 | 2027-01-31 | 50 | PICK-B1 | released | second |
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_type | What it means |
|---|---|
| wave | Order lines released together for picking at a planned time |
| batch | Lines for the same item from several orders picked in one trip, then split by order |
| zone | Each picker works one area, and orders pass between areas |
| cluster | One picker fills several orders' containers on one trip |
| discrete | One 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.
| Step | Ledger transaction | Status change |
|---|---|---|
| Receive | po_receipt into DOCK-IN | available (quarantine for class INSPECT) |
| Put-away, move, replenish | transfer_in, from and to locations both set, and from_status and to_status both set to the status the stock is in now | unchanged |
| Release from quarantine | status_change at QUAR-1 | quarantine to available |
| Allocate | none | none |
| Pick | transfer_in to STAGE-OUT (a short pick posts the quantity found) | available to allocated |
| Load | shipment from STAGE-OUT | from allocated |
| Count variance | cycle_count out of the counted location when it finds less, into it when it finds more | from the line's balance_status, kept in the line's ext |
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
- PostgreSQL documentation: Row security policies (including restrictive policies). https://www.postgresql.org/docs/current/ddl-rowsecurity.html
- PostgreSQL documentation: CREATE VIEW (the security_invoker option). https://www.postgresql.org/docs/current/sql-createview.html
- W3C: Understanding WCAG 2.2, Contrast (Minimum). https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
- 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.
- 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.
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.
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.
| KPI | Definition | How the module measures it |
|---|---|---|
| Dock-to-stock time | Hours from arrival to put-away confirmed | The put-away task's completed_at minus the ASN's arrival time kept in ext |
| Pick accuracy | Lines picked correctly ÷ lines picked | Done pick tasks with no SCAN-REFUSED comment ÷ done plus short pick tasks, so a short pick counts against it |
| Order cycle time | Release to ship hours | From the wave's released_at to the shipment's shipped_at |
| Inventory record accuracy | Count lines within tolerance ÷ count lines | From wms_cycle_count_line |
| Lines picked per labour hour | Throughput productivity | Not 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.
| Check | Passes when |
|---|---|
| Parallel week | On-hand by location, open orders and open ASNs are compared each day, and every difference has a written cause |
| Measures | Pick accuracy and inventory record accuracy for both systems sit beside the Session 1 baselines, and the module is at least as accurate |
| Module checks | Both 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 count | Blind, recounted, approved by a person who did not count, with the lines counted, recounted and posted written down |
| Phone check | The 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 plan | A 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 loader | wms_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.
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
- Made4net: which is the best WMS for business (core and extended capabilities). https://made4net.com/knowledge-center/which-is-the-best-wms-for-business/
- Solutions Review: 2023 Magic Quadrant for WMS, key takeaways. https://solutionsreview.com/enterprise-resource-planning/?p=5624
- Infor: Gartner Magic Quadrant for WMS (core versus extended capabilities). https://dam.infor.com/api/public/content/d8957bc1ac164bf1843965deeaa7e26a
- 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
Your result
CivOps AI Academy
Warehouse (WMS): Scan-Verified Moves Posted to the Ledger
Element M13 complete · Learner
Your LMS records this completion. For the CivOps Foundation certificate, finish the Foundation Course at https://civops.io/learn.