INV/i — Inventory, Warehouse & MRP

SteelFrame X application operation manual  ·  ← back to Operation Manuals  ·  Sign On

INV/i is an inventory / warehouse-management / MRP application: goods receipts with a weighted-average cost roll, stock issues with an oversell veto, bin-to-bin putaways, a net-requirements MRP planning run, and a period cost roll that posts a balanced GL revaluation. It is a pure SQL PL application — the entire business layer is DB2 for i tables, scalar functions, stored procedures and triggers created from plain SQL/SQL-PL DDL, with no 5250 screen and no DDS. The application is driven from STRSQL (interactive CALL), from a batch driver, or through embedded EXEC SQL CALL in an SQLRPGLE program. This manual is the reference for the operator who runs the receiving/issuing/MRP/cost-roll cycle and for the developer maintaining the routines. It is grounded entirely in the committed source (sqlpl-app-inv/src/schema.sql, routines.sql, seed.mjs, and the test/inv_daily.mjs / test/inv_volume.mjs drivers). Everything runs in library INVLIB.

Contents

A. Overview & Architecture ↑ top

A.1 What it does

INV/i manages the physical and cost life of stocked inventory:

A.2 SQL-first architecture: everything is SQL PL

Unlike the RPG+DDS applications in this catalogue, INV/i has no RPG business layer and no display file. Every table is created via plain SQL DDL (schema.sql), and every rule — receiving, the cost roll, the oversell veto, putaway, MRP netting, GL posting — is implemented as a compound (BEGIN…END) DB2 for i scalar function, stored procedure, or table trigger in routines.sql. There is no orchestration layer between the operator and the logic: the operator (or a scheduler, or a report tool) calls the procedures directly.

The benefit for operations: the rules are one auditable place (the SQL-PL routines), reachable identically from STRSQL, a batch CALL, or an SQLRPGLE EXEC SQL CALL; and every money/quantity movement is journaled in an append-only ledger keyed by a source-document token, making re-posting a safe no-op.

A.3 Component & flow

  RECEIVING / ISSUING          PLANNING                 PERIODIC
  -------------------          --------                 --------
  SP_RECEIVE_STOCK  --+        SP_MRP_RUN   --reads-->   SP_COST_ROLL
    rolls WAVGCOST    |          ITMAST (active)           FN_WAVG_COST per item
  SP_ISSUE_STOCK  ----+        MRPDMD (demand)             revalue -> GLENTRY (+/-)
    oversell veto     |          |                         GLACCT balances
  SP_PUTAWAY  --------+          v
    net-zero pair     |        MRPPLAN (planned orders)
                      |
                      v
        STKBAL (on-hand / allocated, per item x whse x bin)
          |   \                                triggers on STKBAL:
          |    +--> STKLEDG (append-only ledger,   TR_STKBAL_VETO       (75010)
          |         one row per R/I/A, RUNTOKEN    TR_STKBAL_ALLOC_VETO (75011)
          |         = idempotency key)             TR_STKBAL_AUDIT   -> STKAUDIT
          +------> STKAUDIT (trigger-written)      TR_STKBAL_INSERT_AUDIT
                                                   TR_STKSTATUS_IOI (view)

A single receipt flows: operator stages an RCPTHDR row (status E) → calls SP_RECEIVE_STOCK(rcptno) → the procedure validates hold/bin/capacity, updates or inserts the STKBAL row, rolls ITMAST.WAVGCOST, writes one STKLEDG row keyed by the receipt number, and flips the header to Posted. The STKBAL change fires the audit trigger into STKAUDIT. Re-calling with the same receipt number is a no-op (RSTAT='P' already).

A.4 Object inventory

ObjectTypeRole
ITMASTTableItem master (reorder policy, standard & weighted-average cost, status).
BINMASTTableBin master (warehouse/bin, capacity, bin type).
STKBALTableOn-hand / allocated balance per item × whse × bin.
STKLEDGTableAppend-only movement ledger (idempotency key RUNTOKEN,RUNSEQ).
RCPTHDRTableInbound receipts staged for posting.
ISSHDRTableOutbound issues staged for posting.
MRPDMDTableDemand requirements feeding an MRP run.
MRPPLANTableMRP run output (planned orders).
GLACCT / GLENTRYTableSimple GL account balances & the cost-roll's posted entries.
STKAUDITTableTrigger-written on-hand change audit trail.
STKSTATUSViewPer-item total-on-hand summary; updatable via INSTEAD OF trigger.
FN_* (5)SQL functionsOn-hand, allocated, ATP, reorder qty, ledger weighted-average cost.
SP_* (6)SQL proceduresReceive / issue / putaway / MRP run / cost roll.
TR_* (5)TriggersOversell veto, over-alloc veto, audit (update & insert), INSTEAD OF view.

The full catalogue is 5 functions + 6 procedures + 5 triggers, over 11 tables and 1 view, with three unique indexes carrying the ledger/plan/GL idempotency keys. Sections D and F expand each. There is no program object and no DDS member — the SQLRPGLE reach shown in the daily driver (INVCALL) is a test-built caller, not a shipped part of the application.

B. Online Access (there is no screen) ↑ top

B.1 STRSQL CALL invocation

Honest statement: INV/i has no 5250 screen, no menu, and no DDS display file. It is a pure SQL PL application — its "online" surface is the DB2 for i SQL layer itself. An operator interacts with it by starting an interactive SQL session (STRSQL) and issuing CALL statements and queries against the routines and tables in library INVLIB. The same calls run unchanged from a batch driver, from a scheduler, or through embedded EXEC SQL CALL in an SQLRPGLE program. Before invoking anything, the job's library list must include INVLIB — the tested jobs run with LIBL = QSYS QGPL INVLIB QTEMP and CURLIB = INVLIB.

Because these are SQL procedures, "run a transaction" means "type a CALL and read the OUT parameters" — there is no data-entry panel; the input is a staged header row (for receive/issue) or literal CALL arguments (for putaway/MRP/cost-roll). Every procedure reports its outcome through OUT parameters: a numeric return code (0 = success) and, for the posting procedures, a human-readable message.

To do thisType in STRSQL
Post a staged goods receiptCALL INVLIB.SP_RECEIVE_STOCK('RC0001', ?, ?)
Post a staged stock issueCALL INVLIB.SP_ISSUE_STOCK('IS0001', ?, ?)
Move stock bin-to-bin (putaway)CALL INVLIB.SP_PUTAWAY('MV0001','ITM001','WH1','A01','WH1','B01',20,20260804, ?, ?)
Run MRP for a planning tokenCALL INVLIB.SP_MRP_RUN('MRPRUN1', 20260801, ?, ?)
Run the cost rollCALL INVLIB.SP_COST_ROLL('CROLL1', ?, ?)
Check available-to-promiseSELECT FN_ATP('ITM001') FROM SYSIBM.SYSDUMMY1
See per-item total on-handSELECT * FROM INVLIB.STKSTATUS

The receive and issue procedures take only the document number and read every detail (item, warehouse, bin, quantity, cost, date) from the staged RCPTHDR / ISSHDR row, so a typical online flow is a two-step: INSERT the header, then CALL the posting procedure. Putaway, MRP and cost-roll take their arguments inline.

-- stage then post a receipt in one STRSQL session
INSERT INTO INVLIB.RCPTHDR (RCPTNO, ITEMNO, WHSE, BIN, QTY, UNITCOST, RDATE)
  VALUES ('RC0001', 'ITM001', 'WH1', 'A01', 100, 10.0000, 20260801);
CALL INVLIB.SP_RECEIVE_STOCK('RC0001', ?, ?);   -- OUT rc, OUT msg  ->  0 / 'posted'
The ? markers are the OUT parameters. In STRSQL they are shown back after the call; from SQLRPGLE they are host variables, e.g. exec sql call INVLIB.SP_RECEIVE_STOCK(:rcptno, :rc, :msg); then DSPLY the values. The daily driver's INVCALL RPG member proves this reach: it posts RC0007 via EXEC SQL CALL and reads FN_ATP('ITM001') via EXEC SQL SELECT … INTO, DSPLYing RCPT=0/posted/0 and the live ATP.

B.2 Controls & audit

INV/i does not model a four-eyes maker–checker workflow: a document posted through SP_RECEIVE_STOCK / SP_ISSUE_STOCK is applied immediately. The control posture is data-layer enforcement + an append-only ledger + a trigger-written audit trail, all independent of who calls the procedure:

The audit stamps CHGBY = 'TRIGGER' rather than a user special register: on this platform CURRENT_USER/USER are not implemented as value expressions outside the SET SCHEMA parser (SQLPL-PLAT-04). Operationally the audit still captures every on-hand change with old/new quantities; only the actor label is a constant.

C. Batch / Cycle Procedures ↑ top

INV/i has no monolithic nightly job. Instead, activity runs as procedure calls grouped into a natural cycle: continuous receiving/issuing/putaway through the working day, then periodic planning (an MRP run) and revaluation (a cost roll). Every procedure is parameterised at the CALL — there is no control-row indirection; receive/issue read their detail from the staged header, and MRP/cost-roll take a run token (and, for MRP, an as-of date) inline. A batch driver is simply a job that issues these CALL statements in order.

-- a batch cycle is a sequence of CALLs (schematic):
CALL INVLIB.SP_RECEIVE_STOCK('RCnnnn', ?, ?);   -- per staged receipt
CALL INVLIB.SP_ISSUE_STOCK  ('ISnnnn', ?, ?);   -- per staged issue
CALL INVLIB.SP_PUTAWAY(...);                     -- per bin move
CALL INVLIB.SP_MRP_RUN('MRPRUN1', 20260801, ?, ?);   -- one planning run
CALL INVLIB.SP_COST_ROLL('CROLL1', ?, ?);            -- one revaluation

C.1 Full procedure / trigger set

ObjectKindPurposeKey inputsOutputs / effectFrequency
SP_RECEIVE_STOCKproc Post one goods receipt; roll weighted-average cost. RCPTHDR row keyed by P_RCPTNO. OUT rc (0/1/2/3/4), OUT msg; STKBAL +qty, ITMAST.WAVGCOST, one STKLEDG(R), header→P. Continuous.
SP_ISSUE_STOCKproc Post one stock issue with oversell veto. ISSHDR row keyed by P_ISSNO. OUT rc (0/1/2/3/5), OUT msg; STKBAL −qty, one STKLEDG(I), header→P; rc 5 on oversell (handler-caught). Continuous.
SP_PUTAWAYproc Move qty bin-to-bin, same item, capacity-checked. move no, item, from whse/bin, to whse/bin, qty, date (all inline). OUT rc (0/1/3/4/5), OUT msg; two STKLEDG(A) rows (RUNSEQ 0/1) netting to zero. Continuous.
SP_MRP_RUNproc Net-requirements plan over every active item. run token, as-of date (horizon = as-of + 90). OUT planned, OUT skipped; one MRPPLAN row per short item. Periodic (per planning run).
SP_COST_ROLLproc Recompute WAVG from ledger; revalue & post balanced GL. run token. OUT revalued, OUT unchanged; ITMAST.WAVGCOST, GLENTRY debit/credit pair, GLACCT balances. Periodic (per revaluation).
TR_STKBAL_VETOtrigger BEFORE UPDATE — refuse on-hand going negative. fires on any STKBAL update. SIGNAL SQLSTATE 75010 when N.QTYOH < 0. Automatic.
TR_STKBAL_ALLOC_VETOtrigger BEFORE UPDATE — refuse allocated > on-hand. fires on any STKBAL update. SIGNAL SQLSTATE 75011 when N.QTYALLOC > N.QTYOH. Automatic.
TR_STKBAL_AUDITtrigger AFTER UPDATE OF QTYOH — audit each change. fires on QTYOH update. INSERT STKAUDIT (old→new qty, CHGBY='TRIGGER'). Automatic.
TR_STKBAL_INSERT_AUDITtrigger AFTER INSERT — audit a new balance row. fires on STKBAL insert. INSERT STKAUDIT (0→opening qty). Automatic.
TR_STKSTATUS_IOItrigger INSTEAD OF UPDATE on the STKSTATUS view. fires on view update. Applies the total-on-hand delta to the item's largest bin in STKBAL. On demand (manual correction).

C.2 Cycle detail

Receiving — SP_RECEIVE_STOCK

Reads the staged RCPTHDR row. If already posted → rc 1 (no-op). Rejects an on-hold item (rc 2), an unknown bin (rc 3), or a receipt that would push the bin's current on-hand past BINMAST.CAPACITY (rc 4) — each rejection also flips the header to R. On acceptance it adds to (or inserts) the STKBAL row, rolls the item's weighted-average cost, and posts one STKLEDG(R) row keyed by the receipt number.

Weighted-average cost roll at receipt (from inv_daily oracle):
  RC0001  ITM001  A01  100 @ 10.0000  -> on-hand 100,  wavg 10.0000
  RC0002  ITM001  A01   50 @ 13.0000  -> on-hand 150,
          wavg = (100*10 + 50*13) / 150 = 1650/150 = 11.0000
The cost-roll math forces genuine decimal division with a * 1.0 factor: DECIMAL/DECIMAL truncates to an integer on this platform when both operands are numerically whole (SQLPL-PLAT-01), e.g. 2650/160 → 16 instead of 16.5625. Every division in the routines that could hit that case is written (… * 1.0) / ….

Issuing — SP_ISSUE_STOCK

Reads the staged ISSHDR row. Already posted → rc 1; on-hold item → rc 2; no balance row in that bin → rc 3. Otherwise it deducts on-hand with a plain UPDATE STKBAL — and that update drives the BEFORE trigger TR_STKBAL_VETO, which SIGNALs 75010 if the result would go negative. An EXIT HANDLER FOR SQLSTATE '75010' catches it, marks the header R, and returns rc 5 ("oversell rejected") — the whole CALL still succeeds (SQLCODE 0) with a business return code, rather than aborting on an unhandled condition. On acceptance it posts one STKLEDG(I) row (signed −qty) at the item's current weighted-average cost.

Putaway — SP_PUTAWAY

An internal transfer of the same item between bins. Guards: already-posted move token → rc 1; insufficient stock in the source bin → rc 5; destination bin not found → rc 3; destination over capacity → rc 4. On acceptance it deducts the source bin, adds/inserts the destination bin, and writes two STKLEDG(A) rows under one RUNTOKENRUNSEQ 0 (source, −qty) and RUNSEQ 1 (destination, +qty) — so the pair nets to zero and the item's total on-hand is conserved.

MRP run — SP_MRP_RUN

A FOR-cursor over active items. For each, it nets alloc + demand(due ≤ as-of+90) − on-hand, floored so it never drops below the safety-stock shortfall. If net requirement is positive it rounds up to a whole multiple of the item's REORDQ (integer ceil-divide) and writes one MRPPLAN row with a release date = due − lead days, unless a row already exists for this (RUNTOKEN, ITEMNO) (then it counts a skip). Idempotent per token.

Worked example (inv_daily): ITM002, on-hand 0, safety 10, reorder qty 60, demand 45 @ 20260820
  netreq  = alloc(0) + demand(45) - onhand(0) = 45   (>= safety floor 10)
  planqty = ceil(45/60) * 60 = 60
  release = due(20260820) - lead(3d) = 20260817
  -> MRPPLAN row: onhand=0 totdmd=45 netreq=45 planqty=60 due=20260820 rel=20260817
The production cursor aliases its select-list columns (CI_ITEMNO…) so no bare cursor-column name collides with a target column in a later INSERT column list. This works around SQLPL-PLAT-06 (inside a FOR loop the engine could rewrite a matching INSERT target column name to the cursor row's value). The gap is fixed in the engine; the volume driver re-tests the un-aliased shape to confirm the fix holds.

Cost roll — SP_COST_ROLL

A FOR-cursor over active items. For each it recomputes the weighted-average cost straight from ledger receipts (FN_WAVG_COST) and compares to the stored WAVGCOST. If it moved by more than a rounding epsilon (0.0001), it revalues on-hand at the delta, updates ITMAST.WAVGCOST, and posts a balanced pair of GLENTRY rows — debit INVASSET by the delta, credit COSTVAR by the same — and mirrors both into GLACCT.BAL. Items whose cost is unchanged (or never received, so ledger WAVG is 0) count as unchanged with no GL. Because the incremental roll at receipt time and the full ledger recompute are two independent computations, a run right after receipts (before any issue) reports everything unchanged — the two agree bit-for-bit.

By design, FN_WAVG_COST sums gross receipts only, while the incremental roll at receipt time bases its formula on current on-hand (net of issues). Once stock has been issued the two figures legitimately diverge (e.g. gross 16.5625 vs. incremental 17.8462 in inv_daily). That divergence is expected, proven deliberately in the driver, and is not a defect — the cost roll only revalues when FN_WAVG_COST itself has moved from the stored value.

C.3 Ordering & idempotency

D. Data Files (data dictionary) ↑ top

All tables are in library INVLIB, grounded in schema.sql. Dates are stored as INT in YYYYMMDD form; quantities are INT; costs are DECIMAL(11,4); GL amounts are DECIMAL(13,2) and signed (debit positive, credit negative).

ITMAST — Item master (PK ITEMNO)

FieldTypeMeaning
ITEMNOVARCHAR(10)Item number (PK).
DESCRVARCHAR(40)Item description.
UOMCHAR(3)Unit of measure.
REORDPTINTReorder point.
REORDQINTStandard reorder quantity (fixed EOQ-ish lot size).
LEADDAYSINTReplenishment lead time in days (MRP release-date offset).
SAFETYSTKINTSafety-stock floor.
STDCOSTDECIMAL(11,4)Standard cost (last PO / receipt price basis).
WAVGCOSTDECIMAL(11,4)Rolling weighted-average cost (default 0).
ISTATCHAR(1)A active, H hold (no receipts/issues; excluded from MRP & cost roll). Default A.

BINMAST — Bin master (PK WHSE, BIN)

FieldTypeMeaning
WHSECHAR(3)Warehouse (PK part 1).
BINVARCHAR(8)Bin (PK part 2).
CAPACITYINTMaximum on-hand the bin can hold (checked on receipt/putaway).
BINTYPECHAR(1)S storage, R receiving, Q quarantine. Default S.

STKBAL — Stock balance (PK ITEMNO, WHSE, BIN)

FieldTypeMeaning
ITEMNO / WHSE / BINVARCHAR(10) / CHAR(3) / VARCHAR(8)The balance's item and location (PK).
QTYOHINTQuantity on hand (default 0; never negative — trigger-vetoed).
QTYALLOCINTQuantity allocated to open issues (default 0; ≤ QTYOH — trigger-vetoed).

STKLEDG — Movement ledger (surrogate LSEQ; unique RUNTOKEN,RUNSEQ)

FieldTypeMeaning
LSEQINT identityGenerated-always surrogate key.
MVTYPECHAR(1)R receipt, I issue, A adjustment (putaway leg), C cost-roll.
ITEMNO / WHSE / BINVARCHAR/CHARWhat moved and where.
QTYINTSigned: +receipt/adjust-in, −issue/adjust-out.
UNITCOSTDECIMAL(11,4)Unit cost at the movement.
MVDATEINTMovement date (YYYYMMDD).
RUNTOKENVARCHAR(20)Source document number — the idempotency key.
RUNSEQINTDisambiguates >1 ledger row per document (putaway writes 0 and 1). Default 0.

Unique index STKLEDG_TOKEN (RUNTOKEN, RUNSEQ) enforces idempotency: a document can never post twice.

RCPTHDR — Inbound receipts staged for posting (PK RCPTNO)

FieldTypeMeaning
RCPTNOVARCHAR(10)Receipt document number (PK; becomes the ledger RUNTOKEN).
ITEMNO / WHSE / BINVARCHAR/CHARItem and destination location.
QTYINTQuantity received.
UNITCOSTDECIMAL(11,4)Receipt unit cost (drives the WAVG roll).
RDATEINTReceipt date (YYYYMMDD).
RSTATCHAR(1)E entered, P posted, R rejected. Default E.

ISSHDR — Outbound issues staged for posting (PK ISSNO)

FieldTypeMeaning
ISSNOVARCHAR(10)Issue document number (PK; becomes the ledger RUNTOKEN).
ITEMNO / WHSE / BINVARCHAR/CHARItem and source location.
QTYINTQuantity to issue.
IDATEINTIssue date (YYYYMMDD).
ISTATCHAR(1)E entered, P posted, R rejected (oversell / hold / no balance). Default E.

MRPDMD — Demand requirements (surrogate DMDID)

FieldTypeMeaning
DMDIDINT identityGenerated-always demand row id.
ITEMNOVARCHAR(10)Item the demand is for.
DUEDATEINTDemand due date (YYYYMMDD).
QTYREQINTQuantity required (independent + dependent demand, pre-netted by the caller).

MRPPLAN — MRP run output (surrogate PLANID; unique RUNTOKEN,ITEMNO)

FieldTypeMeaning
PLANIDINT identityGenerated-always plan row id.
RUNTOKENVARCHAR(20)Planning-run token (idempotency key with ITEMNO).
ITEMNOVARCHAR(10)Planned item.
ONHANDINTOn-hand at run time.
TOTDMDINTTotal demand within horizon.
NETREQINTNet requirement (safety-floored).
PLANQTYINTPlanned order qty (rounded up to REORDQ multiples).
DUEDATEINTEarliest demand due date driving the order.
RELDATEINTRelease date = DUEDATE − lead time.

Unique index MRPPLAN_TOKEN (RUNTOKEN, ITEMNO) makes a re-run with the same token a no-op per item.

GLACCT / GLENTRY — Simple GL (PK ACCTNO / surrogate GLSEQ)

FieldTypeMeaning
GLACCT.ACCTNOVARCHAR(10)Account number (PK). Seeded: INVASSET, COSTVAR.
GLACCT.DESCR / BALVARCHAR(40) / DECIMAL(13,2)Account name and running balance (default 0).
GLENTRY.GLSEQINT identityGenerated-always entry id.
GLENTRY.ACCTNOVARCHAR(10)Posted account.
GLENTRY.AMTDECIMAL(13,2)Signed amount (debit +, credit −).
GLENTRY.RUNTOKEN / RUNSEQVARCHAR(20) / INTCost-roll token & sequence (unique index).
GLENTRY.GLDESCVARCHAR(60)Entry description (e.g. cost roll revalue ITM001).

Unique index GLENTRY_TOKEN (RUNTOKEN, RUNSEQ). The cost roll always posts a matched debit/credit pair, so SUM(AMT)=0 and SUM(BAL)=0 hold across every run.

STKAUDIT — On-hand audit trail (surrogate AUDSEQ)

FieldTypeMeaning
AUDSEQINT identityGenerated-always audit id.
ITEMNO / WHSE / BINVARCHAR/CHARThe balance that changed.
OLDQTY / NEWQTYINTOn-hand before / after the change.
CHGBYVARCHAR(20)Actor label; stamped constant 'TRIGGER' (SQLPL-PLAT-04).

STKSTATUS — Per-item on-hand summary view

Defined as ITMAST LEFT JOIN STKBAL grouped by item, exposing ITEMNO, DESCR, TOTONHAND (COALESCE(SUM(QTYOH),0)). Updatable only through TR_STKSTATUS_IOI: an UPDATE STKSTATUS SET TOTONHAND = … applies the delta to the item's single largest bin, so the view stays consistent with the detail table.

Relationships

E. Operations Runbook ↑ top

E.1 The daily cycle

Pre-checks: confirm the job's library list includes INVLIB (LIBL = QSYS QGPL INVLIB QTEMP, CURLIB = INVLIB); confirm the item, bin and GL master data are seeded (the tested set is 3 items — ITM001 widget, ITM002 gadget, ITM003 on-hold — 3 bins, and GL accounts INVASSET/COSTVAR seeded to zero).

  1. For each inbound receipt: INSERT the RCPTHDR row, then CALL INVLIB.SP_RECEIVE_STOCK('RCnnnn', ?, ?). Confirm rc 0/'posted'.
  2. For each outbound issue: INSERT the ISSHDR row, then CALL INVLIB.SP_ISSUE_STOCK('ISnnnn', ?, ?). Confirm rc 0; an rc 5 is a legitimate oversell rejection, not a failure.
  3. For each bin move: CALL INVLIB.SP_PUTAWAY(...). Confirm rc 0 (or a legitimate capacity/stock veto rc 4/5).
  4. Run the planning pass: CALL INVLIB.SP_MRP_RUN('MRPRUN<date>', <asof>, ?, ?); review the MRPPLAN rows for that token.
  5. Run the revaluation: CALL INVLIB.SP_COST_ROLL('CROLL<date>', ?, ?); confirm GL nets to zero.

Post-checks after the cycle:

E.2 Volume cycle & reconciling figures

The volume driver (inv_volume.mjs) runs the same procedures at scale — 40 items (every 10th on hold), 12 bins, five simulated business days of receipts/issues/putaways (several hundred documents), one MRP run and one cost roll per day, each re-run for idempotency — and reconciles the engine against an independent JS oracle computed from the same seed data. These are the figures an operator reconciles after a period:

Ad-hoc position review: SELECT * FROM INVLIB.STKSTATUS gives per-item total on-hand; SELECT FN_ATP('ITEM'), FN_ONHAND, FN_ALLOC, and FN_REORDER_QTY answer availability and reorder questions without a report program.

E.3 Failure & re-run rules

Every procedure reports through its OUT return code; a business rejection is a non-zero code with SQLCODE still 0 (the routine handles the condition internally), not a hard failure. Genuine SQL errors surface as a negative SQLCODE with an application SQLSTATE (section F.6).

SituationBehaviourAction
Re-post a completed receipt/issueHeader is already P; procedure returns rc 1.Safe no-op — on-hand and the ledger are untouched. Idempotent by document.
Re-run a completed putawayLedger already has rows for the move number; returns rc 1.Safe no-op; stock not moved again.
Oversell (issue > on-hand)BEFORE trigger SIGNALs 75010; EXIT handler catches it.Header marked R, rc 5, on-hand unchanged, no ledger row. Correct the qty and re-stage.
Receipt into a full binrc 4; header marked R; nothing landed.Re-stage to a bin with capacity, or move stock out first.
Receipt/issue against on-hold itemrc 2; header marked R.Release the item (ISTAT='A') or route elsewhere, then re-stage.
MRP re-run, same tokenExisting (RUNTOKEN,ITEMNO) rows are skipped.0 planned / N skipped — idempotent. Use a fresh token to plan again against current positions.
Cost roll re-run, no new activityLedger WAVG unchanged → 0 revalued, no GL entries.Safe; GL stays balanced. Use a fresh token; a same-token re-run is blocked by the GLENTRY unique index if it tried to post.
Because every movement is journaled to STKLEDG and every on-hand change to STKAUDIT, any period's effect is fully reconstructable after the fact — conservation can always be re-derived from the ledger, and the audit shows every quantity transition with old/new values.

F. Developer Reference ↑ top

The complete SQL surface, from schema.sql and routines.sql. All objects are in library INVLIB. Signatures are given exactly as declared; return codes and SQLSTATEs are consolidated in F.6.

F.1 Tables & the view (11 tables, 3 unique indexes, 1 view)

ObjectKeyNotes
ITMASTPK ITEMNOItem master; WAVGCOST rolled by receipts & the cost roll.
BINMASTPK (WHSE,BIN)Bin capacity enforced by receive/putaway.
STKBALPK (ITEMNO,WHSE,BIN)On-hand/allocated; carries all three balance triggers.
STKLEDGIDENTITY LSEQ; UQ (RUNTOKEN,RUNSEQ)Append-only movement ledger; idempotency key.
RCPTHDRPK RCPTNOStaged receipts; RSTAT E/P/R.
ISSHDRPK ISSNOStaged issues; ISTAT E/P/R.
MRPDMDIDENTITY DMDIDDemand feeding SP_MRP_RUN.
MRPPLANIDENTITY PLANID; UQ (RUNTOKEN,ITEMNO)MRP output; per-token idempotency.
GLACCTPK ACCTNOAccount balances; seeded INVASSET, COSTVAR.
GLENTRYIDENTITY GLSEQ; UQ (RUNTOKEN,RUNSEQ)Cost-roll postings; matched debit/credit pairs.
STKAUDITIDENTITY AUDSEQTrigger-written on-hand audit.
STKSTATUSViewITMAST LEFT JOIN STKBAL grouped; updatable via INSTEAD OF.

F.2 Functions (5)

FN_ONHAND (P_ITEMNO VARCHAR(10)) RETURNS INT
Total on-hand for an item across all bins: SELECT COALESCE(SUM(QTYOH),0) INTO V FROM STKBAL WHERE ITEMNO=P_ITEMNO. Compound body with a local variable.
FN_ALLOC (P_ITEMNO VARCHAR(10)) RETURNS INT
Total allocated for an item across all bins (COALESCE(SUM(QTYALLOC),0)).
FN_ATP (P_ITEMNO VARCHAR(10)) RETURNS INT
Available-to-promise = FN_ONHAND − FN_ALLOC, floored at 0 (never negative). Calls the two scalar functions and clamps.
FN_REORDER_QTY (P_ITEMNO VARCHAR(10)) RETURNS INT
Compound decision function. Reads REORDPT, REORDQ, SAFETYSTK; sets PROJ = on-hand − allocated. If PROJ > REORDPT returns 0 (no reorder). Else computes the safety shortfall SHORT = SAFETYSTK − PROJ (floored 0) and returns SHORT if it exceeds REORDQ, otherwise REORDQ.
FN_WAVG_COST (P_ITEMNO VARCHAR(10)) RETURNS DECIMAL(11,4)
Recomputes weighted-average cost straight from ledger receipts only (MVTYPE='R'), independent of the stored WAVGCOST column — used by SP_COST_ROLL to verify/refresh it. Deliberately a FOR-cursor loop accumulating TOTQTY/TOTCOST (not a single aggregate) to exercise the loop form. Returns 0 if no receipts; otherwise (TOTCOST * 1.0) / TOTQTY — the * 1.0 forces genuine decimal division (SQLPL-PLAT-01).

F.3 Procedures (6)

SP_RECEIVE_STOCK (IN P_RCPTNO VARCHAR(10), OUT P_RC INT, OUT P_MSG VARCHAR(80))
Posts one goods receipt from its RCPTHDR row. Guards in order: already posted → rc 1; item on hold → rc 2 (header→R); bin not in BINMAST → rc 3 (header→R); current + qty > CAPACITY → rc 4 (header→R). On acceptance: update-or-insert STKBAL; roll WAVGCOST as ((oldQty×oldCost + qty×cost) * 1.0) / (oldQty+qty) (or the receipt cost if prior on-hand ≤ 0); INSERT one STKLEDG('R', …, RUNTOKEN=P_RCPTNO, RUNSEQ=0); header→P; rc 0 / 'posted'.
SP_ISSUE_STOCK (IN P_ISSNO VARCHAR(10), OUT P_RC INT, OUT P_MSG VARCHAR(80))
Posts one issue from its ISSHDR row. Declares EXIT HANDLER FOR SQLSTATE '75010' (marks header R, rc 5). Guards: already posted → rc 1; item on hold → rc 2; no STKBAL row in that bin → rc 3. Otherwise UPDATE STKBAL SET QTYOH = QTYOH − qty — which drives TR_STKBAL_VETO; if it would go negative the veto SIGNALs 75010 and the handler reports rc 5 cleanly. On success INSERT one STKLEDG('I', …, −qty) at the item's WAVGCOST; header→P; rc 0.
SP_PUTAWAY (IN P_MOVENO, P_ITEMNO, P_FROMWHSE, P_FROMBIN, P_TOWHSE, P_TOBIN, P_QTY, P_MVDATE; OUT P_RC, P_MSG)
Bin-to-bin transfer of one item. Guards: existing ledger rows for P_MOVENO → rc 1; source on-hand NULL or < qty → rc 5; destination bin absent → rc 3; destination total + qty > CAPACITY → rc 4. On acceptance: deduct source, add/insert destination, and INSERT two STKLEDG('A', …) rows under one RUNTOKEN — RUNSEQ 0 (source, −qty), RUNSEQ 1 (destination, +qty). rc 0 / 'moved'.
SP_MRP_RUN (IN P_RUNTOKEN VARCHAR(20), IN P_ASOF INT, OUT P_PLANNED INT, OUT P_SKIPPED INT)
FOR-cursor over ITMAST WHERE ISTAT='A' (columns aliased CI_*, see the note). Per item: totdmd = SUM(QTYREQ) where DUEDATE ≤ P_ASOF+90; netreq = (alloc + totdmd) − onhand, floored so it is at least the safety-stock shortfall. If netreq > 0: mult = (netreq + REORDQ − 1) / REORDQ (integer ceil-divide, min 1), planqty = mult × REORDQ, rel = due − lead; INSERT one MRPPLAN row unless one exists for (RUNTOKEN,ITEMNO) (then skip). Idempotent per token.
SP_COST_ROLL (IN P_RUNTOKEN VARCHAR(20), OUT P_REVALUED INT, OUT P_UNCHANGED INT)
FOR-cursor over active items. Per item: newcost = FN_WAVG_COST(item). If newcost > 0 AND ABS(newcost − oldcost) ≥ 0.0001: delta = (newcost − oldcost) × onhand; UPDATE WAVGCOST; INSERT a paired GLENTRY('INVASSET', +delta, …, RUNSEQ n) and ('COSTVAR', −delta, …, RUNSEQ n+1); UPDATE GLACCT.BAL for both; count revalued. Else count unchanged. The pairing guarantees GL nets to zero.
All six procedures are LANGUAGE SQL with compound BEGIN…END bodies; none raises an unhandled exception for an expected business rejection — each returns a numeric OUT code and, where present, an OUT message. The only SIGNAL that leaves the data layer is the oversell veto, and even that is caught inside SP_ISSUE_STOCK.

F.4 Triggers (5)

TR_STKBAL_VETO — BEFORE UPDATE ON STKBAL, WHEN (N.QTYOH < 0)
SIGNAL SQLSTATE 75010 ('oversell rejected: on-hand cannot go negative'). The hard floor under on-hand, independent of any procedure.
TR_STKBAL_ALLOC_VETO — BEFORE UPDATE ON STKBAL, WHEN (N.QTYALLOC > N.QTYOH)
SIGNAL SQLSTATE 75011 ('allocation cannot exceed on-hand'). Cannot promise more than is present.
TR_STKBAL_AUDIT — AFTER UPDATE OF QTYOH ON STKBAL (BEGIN ATOMIC)
INSERT STKAUDIT with (O.QTYOH → N.QTYOH, CHGBY='TRIGGER'). Fires only on a QTYOH change, and only after the BEFORE vetoes have passed — so a rejected oversell writes no audit row.
TR_STKBAL_INSERT_AUDIT — AFTER INSERT ON STKBAL
INSERT STKAUDIT for a brand-new balance row (first receipt into a bin), from 0 to its opening quantity.
TR_STKSTATUS_IOI — INSTEAD OF UPDATE ON STKSTATUS (BEGIN ATOMIC)
Computes delta = N.TOTONHAND − O.TOTONHAND, finds the item's single largest bin (ORDER BY QTYOH DESC FETCH FIRST 1 ROW ONLY), and applies the whole delta there with an UPDATE STKBAL — keeping the summary view consistent with the detail table. That detail update itself trips TR_STKBAL_AUDIT, so the manual correction is audited too.

F.5 SQL-PL patterns

The application exercises the SQL-PL surface deliberately; the idioms worth knowing when maintaining it:

F.6 SQLSTATE & return-code table

Application SQLSTATEs (raised by triggers)

SQLSTATERaised byMeaning
75010TR_STKBAL_VETOOn-hand cannot go negative (oversell). Caught by SP_ISSUE_STOCK's EXIT handler → rc 5.
75011TR_STKBAL_ALLOC_VETOAllocation cannot exceed on-hand. Surfaces to the caller as an SQL error.

Procedure return codes (OUT P_RC)

rcProceduresMeaning
0allSuccess ('posted' / 'moved').
1RECEIVE / ISSUE / PUTAWAYAlready posted — idempotent no-op.
2RECEIVE / ISSUEItem on hold (ISTAT='H'); header marked R.
3RECEIVE / ISSUE / PUTAWAYBin/balance not found (unknown bin, or no STKBAL row / dest bin absent).
4RECEIVE / PUTAWAYBin over capacity; header marked R (receive).
5ISSUE / PUTAWAYOversell rejected (issue, via 75010 handler) / insufficient source stock (putaway).

MRP and the cost roll have no failure return codes — they report counts (planned/skipped, revalued/unchanged). A same-token re-run of the cost roll that attempted to post again would trip the GLENTRY_TOKEN unique index; in normal operation nothing is re-posted because unchanged costs write no GL.

G. Glossary ↑ top

Allocated (QTYALLOC)
Quantity reserved against open issues; capped at on-hand by TR_STKBAL_ALLOC_VETO. Subtracted from on-hand to give ATP.
ATP — Available to Promise
On-hand minus allocated, floored at zero (FN_ATP): what can still be promised to new demand.
Bin / capacity
A storage location within a warehouse (BINMAST), with a maximum on-hand (CAPACITY) enforced on receipt and putaway.
Cost roll
Recomputing weighted-average cost from ledger history and, where it moved, revaluing on-hand and posting a balanced GL entry (SP_COST_ROLL).
Conservation
The invariant that an item's on-hand equals the signed sum of its ledger movements (plus any un-ledgered STKSTATUS correction) — the core reconciliation check.
GL (INVASSET / COSTVAR)
The simple two-account general ledger the cost roll posts to; every revaluation is a matched debit (inventory asset) and credit (cost variance) so the ledger nets to zero.
Idempotency key (RUNTOKEN)
The source-document number carried on every ledger/plan/GL row under a unique index, so re-posting the same document is a safe no-op.
Issue
An outbound stock movement out of a bin (SP_ISSUE_STOCK), vetoed if it would drive on-hand negative.
MRP — Material Requirements Planning
Netting on-hand and allocated against demand to decide planned orders (SP_MRP_RUN), sized to reorder-quantity multiples and released back by lead time.
Net requirement (NETREQ)
(allocated + demand) − on-hand, floored so it never drops below the safety-stock shortfall; a positive value triggers a planned order.
On hold (ISTAT='H')
An item state that refuses all receipts and issues and is excluded from MRP and cost-roll cursors.
Oversell veto
The BEFORE-UPDATE trigger (75010) that refuses any on-hand update going negative — the hard floor under stock, independent of the issue procedure.
Putaway
An internal bin-to-bin transfer of the same item (SP_PUTAWAY), written as two ledger legs that net to zero.
Receipt
An inbound stock movement into a bin (SP_RECEIVE_STOCK), which also rolls the item's weighted-average cost.
STKLEDG (ledger)
The append-only journal of every stock movement (R/I/A/C), the source of truth for conservation and cost recomputation.
STRSQL
Interactive SQL — the IBM i facility from which an operator CALLs the routines and queries the tables, since INV/i has no 5250 screen.
Weighted-average cost (WAVGCOST)
The rolling average unit cost, moved forward at each receipt and re-derivable from gross receipts by FN_WAVG_COST; the two computations agree only before any issue consumes received stock.