PROCURE/i is a purchase-to-pay application: a vendor master, purchase-order raising
and approval, goods receipt, the classic three-way match (PO vs receipt vs invoice), invoice
registration with hold/release, a weekly payment run that captures early-settlement discount, remittance
advice, AP ageing, a received-not-invoiced accrual, an expense/AP GL distribution, and an annual vendor
spend / 1099-style summary with accrual reversal. It is a classic mixed-language IBM i estate: fixed-form
and free-form RPG for the batch posting logic, one SQLRPGLE program for the GL post, one ILE
COBOL control report, DDS physical/logical/display/printer files, a SQL table, and
CL drivers for setup and the four job cycles. This manual is the reference for the operator who
runs the online inquiry screens and the periodic batch cycles, and for the developer maintaining the
application. It is grounded entirely in the committed source (ap-app/src/sources.mjs,
src/seed.mjs, and the test/ap_build.mjs, ap_daily.mjs,
ap_cycles.mjs and ap_interactive.mjs drivers). Everything lives in library
PROCURE.
PROCURE/i runs the purchase-to-pay cycle end to end:
VDISCPC%, within VDISCDY days, else net VNETDY), so the
payment run computes settlement without a terms-table lookup. Purchase orders are raised with a
header (APPOH) and lines (APPOL).SUM(LAMT) IS the control balanceThe application has no single “business-rules layer” the way a SQL-PL app does; the logic
lives in the RPG posting programs. What ties them together is one durable file, APLEDG, that
every posting program contributes to. Its design carries the two invariants the whole app is built on:
LAMT is signed: an invoice registration adds the
invoice total (positive), a payment subtracts the gross settled (negative). Therefore
SUM(LAMT) across the ledger is the accounts-payable control balance. When a
vendor is fully invoiced and paid, its ledger rows net to zero — the COBOL report
APAPRPT exists to prove exactly this.LSEQ is derived from a
stable business key in a range disjoint per program (receipts 1000xxxx, invoices 2000xxxx,
payments 3000xxxx, accruals 4000xxxx, reversals 5000xxxx). Before posting, each program does a
CHAIN(EN) on the key it is about to write; if the row already exists it skips. A
re-run therefore regenerates the same key and is refused rather than doubling the posting. This is
what makes every cycle safely re-runnable — see F.2.Two further invariants ride on top: the payment invariant PNET = PGROSS − PDISC on
every cheque, and the “discount is income” rule — the vendor balance falls by the full
gross settled, never by the net cash, because the captured discount is discount-received income,
not a reduction of what was owed.
REFERENCE / ONLINE BATCH (four CL-driven cycles)
----------------- ----------------------------
APREFLD (seed data) DAILY APDAILY:
APMENU (5250) APPOAPP re-total + approve POs
1 -> APVENDIQ (vendor) APRCPOST goods-receipt posting --> APLEDG 'R'
2 -> APPOIQ (PO+subfile) APMATCH 3-way match (hold/approve)
APINVREG invoice registration --> APLEDG 'I' (+VBAL)
APHOLDRP hold/release report
APPOCLS close fully-invoiced POs
WEEKLY APWEEK:
APPAYRUN payment run + discount --> APLEDG 'P' (-VBAL)
APREMIT remittance advice (PRTF)
APVEND APPOH APPOL MONTHLY APMONTH:
| APRCPT APINVH APINVL APAGERP AP ageing snapshot --> APAGE
| APPAY APACCR GRNI accrual --> APLEDG 'A'
v APGLPST expense/AP GL post --> APGLDIST (SQL)
APLEDG <--- every APAPRPT AP control report (COBOL, reads APLEDG)
posting program writes
| ANNUAL APYEAR:
+--> APAPRPT proves APSPEND vendor spend + 1099 --> APSPND
SUM(LAMT) balances APREVACC accrual reversal --> APLEDG 'V'
APAPRPT AP control report
A single event — say a goods receipt — flows: an entered APRCPT row is read
by APRCPOST → its quantity is added into the matching APPOL line (capped
at ordered) → a receipt-valued APLEDG row is written under a stable key → the
receipt flips to posted. Downstream, an invoice for those goods must pass APMATCH before it
can ever reach APPAYRUN.
| Object | Type | Role |
|---|---|---|
| APVEND | PF | Vendor master (terms, AP balance, 1099 flag). |
| APPOH | PF | Purchase-order header. |
| APPOL | PF | Purchase-order lines (composite key PONO+POLINE). |
| APPOLLF | LF | PO lines keyed by ITEMNO (alternate path). |
| APRCPT | PF | Goods receipts. |
| APINVH | PF | Invoice header. |
| APINVLF | LF | Invoices keyed by DUEDT (payment-run path; fmt APINVDR). |
| APINVL | PF | Invoice lines (tie to PO line via IPOLINE). |
| APPAY | PF | Payment header (one per cheque). |
| APLEDG | PF | The AP ledger — the durable audit spine. |
| APAGE | PF | AP ageing snapshot (per run + vendor). |
| APSPND | PF | Vendor spend analysis (per year + vendor). |
| APGLDIST | SQL table | Expense/AP GL distribution (+ index APGLDACC). |
| APVENDD / APPOD / APMENUD | DSPF | Vendor inquiry / PO+subfile inquiry / menu. |
| APREMP | PRTF | Remittance advice / report layout. |
| APREFLD | RPG | Reference-data seeder (vendors, POs, PO lines). |
| APPOAPP APRCPOST APMATCH APINVREG APHOLDRP APPOCLS | RPG | The six daily posting programs. |
| APPAYRUN APREMIT | RPG | Weekly payment run + remittance. |
| APAGERP APACCR | RPG | Monthly ageing + GRNI accrual. |
| APGLPST | SQLRPGLE | Monthly expense/AP GL post (embedded SQL). |
| APSPEND APREVACC | RPG | Annual spend analysis + accrual reversal. |
| APVENDIQ APPOIQ APMENU | RPG | Interactive vendor / PO inquiry + menu. |
| APAPRPT | ILE COBOL | AP control report over the ledger. |
| APSETUP APDAILY APWEEK APMONTH APYEAR | CL | Object build + the four job cycles. |
The catalogue is 12 PF/LF files, 1 SQL table + 1 index, 3 DSPF, 1 PRTF, 17 RPG programs (one of them SQLRPGLE), 1 ILE COBOL program and 5 CL programs. Sections D and F expand each.
PROCURE/i has no CICS transaction identifiers. On IBM i each program is reached by name from a
5250 command-entry line (interactive programs) or from a JOBQ / scheduler (batch). Before invoking
anything the job's library list must include PROCURE — the tested jobs run with
LIBL = QSYS QGPL PROCURE QTEMP and CURLIB = PROCURE. Object creation is done
once by the setup CL, which itself does ADDLIBLE LIB(PROCURE).
| To do this | Type on the command line |
|---|---|
| Build every object & compile every program (once) | CALL PROCURE/APSETUP |
| Load the reference data (vendors, POs, PO lines) | CALL PROCURE/APREFLD |
| Open the operator menu (routes to the two inquiries) | CALL PROCURE/APMENU |
| Open the vendor inquiry directly | CALL PROCURE/APVENDIQ |
| Open the PO inquiry (with a line subfile) directly | CALL PROCURE/APPOIQ |
| Run the daily / weekly / monthly / annual cycle | CALL PROCURE/APDAILY … APWEEK / APMONTH / APYEAR (or SBMJOB them) |
The batch cycles take no CALL parameters: each is a CL program that simply calls its posting
programs in order, so a scheduled submission is a bare CALL of the cycle. The three
interactive programs are inquiry-only (see B.2); all posting happens in batch.
WRUNDT = 20260810 and pay-through
WTHRU = 20260910; the ageing uses WASOF = 20260831, run 202608;
the accrual/GL post stamp 20260831 / period 202608; the annual programs use
year 2026. Advancing the business date is therefore a source change and recompile,
not a data update. This is called out plainly so operators do not go looking for a control file that does
not exist.APMENU presents a two-option menu and routes by program-to-program CALL:
option 1 calls APVENDIQ, option 2 calls APPOIQ; any other
non-blank option shows Invalid option. on the menu itself. F3 ends the
program; each inquiry's F3 returns to the menu.
A non-subfile screen: key a vendor number, press Enter, and it
CHAINs APVEND and renders the master record plus the derived terms. A miss
clears the detail fields and shows Vendor not found. rather than leaving stale values on screen.
The AP balance shown is the live VBAL the daily cycle posted — not a re-derivation.
| Field (DDS) | Shows |
|---|---|
| IVEND | Input: vendor number to CHAIN. |
| DNAME / DTERMS | Vendor name; terms code (e.g. 2/10 N30). |
| DDISC / DDDAYS / DNDAYS | Discount % (rendered 2.00), discount days, net days. |
| DBAL | AP balance, edited with thousands separators (%EDITC 'K', e.g. 2,250.00). |
| D1099 / DMSG | 1099 flag; status/found message line. |
The app's subfile screen. Key a PO number; the header (APPOH) renders vendor, re-derived
order total and status, and a subfile (DDS record PSFL under control PCTL,
SFLPAG(5) / SFLSIZ(20), ROLLUP/ROLLDOWN) loads the
order's lines showing ordered / received / invoiced quantities and line status. Enquiring a second PO
clears and reloads the subfile, so the row count tracks the new order (a one-line PO shows one
row); an unknown PO shows Purchase order not found. with an empty subfile.
| Field (DDS) | Shows |
|---|---|
| IPONO | Input: PO number to CHAIN. |
| DPVEND / DPTOT / DPSTAT | Header vendor, order total (edited), status. |
| SLINE / SITEM | Subfile: line number, item. |
| SORD / SRCV / SINV | Subfile: ordered / received / invoiced quantity (edited). |
| SSTAT | Subfile: line status (O open, R received, I invoiced, C closed). |
Honest statement: PROCURE/i does not model a true four-eyes maker–checker /
separate-authorization workflow. The PO approval sweep APPOAPP approves automatically for any
active vendor; the manual band POAPLM='M' that an over-10000.00 order receives is a
label recording that it exceeded the auto limit, not a hold-for-a-second-approver gate —
the order is approved in the same pass. The screens are inquiry-only and post nothing. The manual
documents the control model the application does have, and it is a strong one, enforced in the
posting programs:
APMATCH is what stops bad money
going out: an invoice that over-bills a receipt (Q), varies on price (P),
or names a non-existent PO line (N) is set ISTAT='H' and consumes no
PO quantity. Only a clean invoice reaches ISTAT='O', and only an 'O'
invoice is ever picked up by the payment run — a held invoice is verifiably never paid.APMATCH tests every invoice line first, consuming nothing;
only if the whole invoice passes does a second pass consume the PO quantities. A partial failure
cannot half-consume an order.APRCPOST caps a delivery at the
ordered quantity (a supplier over-delivery is trimmed, not absorbed) and refuses a delivery against
a PO already at POSTAT='X', leaving the line untouched and the receipt un-posted.SUM(LAMT) across it is the AP control balance the COBOL
report re-proves.In sum, the control posture is match-gating + receipt-bounding + a signed audit ledger + hard idempotency, all enforced in the posting programs, rather than a segregation-of-duties approval workflow.
PROCURE/i runs as four CL-driven cycles: a daily purchase-to-pay cycle, a weekly payment
cycle, a monthly close and an annual roll. Each CL simply ADDLIBLEs
PROCURE and calls its posting programs in a fixed order, then sends a completion message. No
cycle takes parameters (see the B.1 note on how dates are set).
-- one-time build, then load reference data CALL PROCURE/APSETUP CALL PROCURE/APREFLD -- then submit a (parameterless) cycle SBMJOB CMD(CALL PGM(PROCURE/APDAILY)) JOB(APDAILY)
| Program | Cycle | Purpose | Reads | Writes / result |
|---|---|---|---|---|
| APREFLD | setup | Seed 4 vendors, 4 POs, 7 PO lines. | — | APVEND/APPOH/APPOL; DSPLY VENDORS=4 POS=4 POLINES=7. |
| APPOAPP | daily | Re-total entered POs & approve (active vendor only). | APPOH/APPOL/APVEND | POTOT/POSTAT/POAPLM; DSPLY APPROVED=/SKIP=. |
| APRCPOST | daily | Post entered receipts into PO lines (capped, cancel-aware). | APRCPT/APPOL/APPOH | RCVQTY, receipt→P, ledger 'R'; DSPLY POSTED=/SKIP=/CAPPED=. |
| APMATCH | daily | 3-way match; approve (O) or hold (H) each entered invoice. | APINVH/APINVL/APPOL | ISTAT/IHOLDCD, INVQTY on clean match; DSPLY MATCHED=/HELD=/SKIP=. |
| APINVREG | daily | Register matched invoices; raise VBAL; stamp discount date. | APINVH/APVEND | VBAL/VYTD, DISCDT, ledger 'I' (+); DSPLY REGISTERED=/SKIP=. |
| APHOLDRP | daily | Report held invoices split by hold code. | APINVH/APVEND | DSPLY HELD=/QTY=/PRC=/NOPO=/AMT=. |
| APPOCLS | daily | Close a PO whose every line is invoiced. | APPOH/APPOL | POSTAT→C; DSPLY CLOSED=/SKIP=. |
| APPAYRUN | weekly | Pay open invoices due by pay-through; capture discount. | APINVLF/APINVH/APVEND | APPAY row, VBAL−gross, ledger 'P' (−); DSPLY PAID=/GROSS=/DISC=/SKIP=. |
| APREMIT | weekly | Print remittance advice (PRTF, page overflow). | APPAY/APVEND/APINVH | Spooled advice; DSPLY LINES=/CASH=. |
| APAGERP | monthly | Age every unpaid invoice into current/1-30/31-60/61+ buckets. | APVEND/APINVH | APAGE row per vendor; DSPLY RUN=/VENDORS=/SKIP=. |
| APACCR | monthly | Accrue received-not-invoiced (received>invoiced) at PO price. | APPOL/APPOH | Ledger 'A'; DSPLY ACCRUED=/AMT=/SKIP=. |
| APGLPST | monthly | Post expense/AP GL distribution from the ledger (SQLRPGLE). | APLEDG + APGLDIST | APGLDIST DR/CR rows; DSPLY PER=/ROWS=. |
| APAPRPT | monthly/annual | AP control report: proves SUM(LAMT) balances (ILE COBOL). | APLEDG | DSPLY INVOICED/PAID/DISCOUNT/CONTROL/ROWS. |
| APSPEND | annual | Vendor spend + 1099-style summary from the ledger. | APVEND/APLEDG | APSPND row per vendor; DSPLY YEAR=/VENDORS=/R1099=/SKIP=. |
| APREVACC | annual | Reverse every GRNI accrual with an equal-and-opposite row. | APLEDG | Ledger 'V' (−); DSPLY REVERSED=/AMT=/SKIP=. |
The purchase-to-pay chain. Approve orders, post receipts, match invoices, register the clean ones, report the held ones, close finished orders. Worked against the seeded reference data plus the receipts and invoices the tests plant, the expected results are hand-derivable:
Expected DSPLY (reference data + the day's receipts/invoices): APPOAPP APPROVED=3 SKIP=1 3 active-vendor POs; V00004 on hold -> skipped APRCPOST POSTED=4 SKIP=0 four receipts posted to the right (PO,line) APMATCH MATCHED=1 HELD=3 1 clean (O); 3 held Q/P/N APINVREG REGISTERED=1 only the matched invoice registers; VBAL +2250.00 APHOLDRP HELD=3 ... AMT=20137.50 APPOCLS CLOSED=0 PO000001 line 3 never invoiced -> stays open
Q; IN000003 bills 19500.0000 against a PO price of 18000.0000 → hold
P; IN000004 names a non-existent PO line → hold N. None register, so their
vendors' balances stay 0.00, and none can reach the payment run.APPAYRUN walks the due-date logical APINVLF and settles every 'O'
invoice due on or before the pay-through date. If the run date is on or before the invoice discount date
it captures VDISCPC% of the total, else it pays gross. One APPAY cheque per
invoice with PNET = PGROSS − PDISC; the vendor balance falls by the full gross.
Expected DSPLY (2 open invoices; 1 held is skipped): APPAYRUN PAID=2 GROSS=3450.00 DISC=24.00 1200.00 paid inside 2/10 window -> 24.00 discount, 1176.00 cash 2250.00 past its window -> gross, 0.00 discount APREMIT LINES=2 CASH=3426.00 total net cash = 1176.00 + 2250.00
Age the unpaid invoices, accrue received-not-invoiced goods, post the GL distribution, and prove the
control balance. Ageing buckets by whole days past due using exact civil-days arithmetic (F.3); the GRNI
accrual is (RCVQTY−INVQTY)×POPRICE per PO line; the GL post writes balanced DR/CR
pairs and posts only the movement since the last close.
Expected DSPLY / GL (as of 20260831, period 202608): APAGERP RUN=202608 VENDORS=4 the one open (held) 600.00 invoice ages to the 1-30 bucket APACCR ACCRUED=2 AMT=18360.00 360.00 (120x3) + 18000.00 (1x18000) APGLPST PER=202608 ROWS=... 5100-EXP DR 21810.00; 1010-CSH CR 3426.00; 4900-DSC CR 24.00; 2100-AP both sides 3450.00 APAPRPT CONTROL ... COBOL re-proves SUM(LAMT)
Summarise each vendor's year from the ledger (gross invoiced, discount taken, net cash, invoice count, 1099 flag), then reverse the GRNI accruals so they do not double-count into the new year.
Expected DSPLY (year 2026): APSPEND YEAR=2026 VENDORS=4 R1099=1 V00001 gross 3450.00 / disc 24.00 / net 3426.00; V00003 flagged APREVACC REVERSED=2 AMT=18360.00 two 'V' rows totalling -18360.00; accrual nets to 0
'O' invoices register), and the match relies on receipts already posted
(it tests invoiced-plus-new against RCVQTY).'O'; the payment run pays only 'O' invoices, so the day's registration
must precede the week's payment run.'A' rows the monthly
accrual wrote; the spend analysis reads the whole year's ledger. Run the annual roll last.All files are in library PROCURE, grounded in src/sources.mjs. Dates are
stored as signed 8S0 integers in YYYYMMDD form (or YYYYMM /
YYYY for periods); money is packed decimal; unit prices are 11P4 (four decimals);
the discount percent is 5P2.
| Field | Type | Meaning |
|---|---|---|
| VENDNO | 6A | Vendor number (key), e.g. V00001. |
| VNAME | 30A | Vendor name. |
| VTERMS | 10A | Terms code text, e.g. 2/10 N30 (display only). |
| VDISCPC | 5P2 | Early-settlement discount percent (2.00 = 2%). |
| VDISCDY | 3S0 | Days within which the discount applies. |
| VNETDY | 3S0 | Net days (informational). |
| VBAL | 13P2 | Open AP balance: +invoice total on registration, −gross on payment. |
| VYTD | 13P2 | Year-to-date invoiced. |
| V1099 | 1A | Y = 1099-reportable vendor. |
| VSTAT | 1A | A active, H hold (approval skips it). |
Seeded: V00001 ACME 2/10 N30; V00002 BOREAL net 30 (no discount); V00003 CALDER 1/15 N45 (1099); V00004 DELTA net 60 (on hold).
| Field | Type | Meaning |
|---|---|---|
| PONO | 8A | PO number (key), e.g. PO000001. |
| VENDNO | 6A | Owning vendor. |
| PODT / PONEED | 8S0 | Order date / needed-by date. |
| POTOT | 13P2 | Extended order value, re-derived from the lines by the approval sweep. |
| POSTAT | 1A | E entered, A approved, R received, C closed, X cancelled. |
| POAPLM | 1A | Approval band: A auto (≤10000), M manual band (>10000). Label, not a hold. |
| POBUYER | 6A | Buyer code. |
| Field | Type | Meaning |
|---|---|---|
| PONO / POLINE | 8A / 3S0 | PO + line number (composite key, stable order). |
| ITEMNO / PLDESC | 8A / 25A | Item number; line description. |
| POQTY | 9P2 | Ordered quantity. |
| POPRICE | 11P4 | Unit price to four decimals (the match's price test). |
| RCVQTY | 9P2 | Quantity goods receipt has booked (capped at POQTY). |
| INVQTY | 9P2 | Quantity invoicing has consumed (only on a clean match). |
| PLACCT | 9A | Expense account code. |
| PLSTAT | 1A | O open, R received, I invoiced, C closed. |
APPOLLF is a non-unique logical over these lines keyed by ITEMNO then
PONO — “which orders is this part on?” as a keyed read rather than a scan.
| Field | Type | Meaning |
|---|---|---|
| RCPTNO | 8A | Receipt number (key). |
| PONO / RLINE | 8A / 3S0 | The PO line this delivery is against. |
| RQTY / RDT | 9P2 / 8S0 | Delivered quantity; receipt date. |
| RSTAT | 1A | E entered (not posted), P posted. The flip to P is part of the idempotency guard. |
| Field | Type | Meaning |
|---|---|---|
| INVNO | 8A | Invoice number (key). |
| VENDNO / PONO | 6A / 8A | Vendor; the PO being billed. |
| INVDT / DUEDT / DISCDT | 8S0 | Invoice date; due date (drives payment selection); discount date (stamped at registration). |
| INVTOT | 13P2 | Invoice total. |
| IDISCA / IPAID | 11P2 / 13P2 | Discount captured; net cash paid. |
| ISTAT | 1A | E entered, H held, O open/approved, P paid, X cancelled. |
| IHOLDCD | 1A | Hold reason: Q qty over receipt, P price variance, N no matching PO. |
| IPAYNO | 8A | Payment/cheque number once settled. |
APINVLF is a logical over the invoices keyed by DUEDT then
INVNO, record format APINVDR (renamed so the payment run can open both this LF for the
due-date read and the base APINVH for the keyed write-back).
| Field | Type | Meaning |
|---|---|---|
| INVNO / ILINE | 8A / 3S0 | Invoice + line number (composite key). |
| IPOLINE | 3S0 | The PO line this line claims to bill (the match join). |
| IQTY / IPRICE | 9P2 / 11P4 | Billed quantity; billed unit price (compared to POPRICE). |
| IAMT | 13P2 | Extended line value. |
| ILSTAT | 1A | Line status. |
| Field | Type | Meaning |
|---|---|---|
| PAYNO | 8A | Cheque number (key), derived from the invoice for stability. |
| VENDNO / PAYDT | 6A / 8S0 | Vendor; payment date. |
| PGROSS | 13P2 | Gross settled (the invoice total). |
| PDISC | 11P2 | Early-settlement discount captured. |
| PNET | 13P2 | Cash paid — the invariant is PNET = PGROSS − PDISC. |
| PCNT / PSTAT | 3S0 / 1A | Invoice count on the cheque; status. |
| Field | Type | Meaning |
|---|---|---|
| LSEQ | 8S0 | Ledger key (unique), derived from a stable business key in a per-program range (F.2). |
| VENDNO / LTYPE | 6A / 1A | Vendor; class: R receipt, I invoice, P payment, A accrual, V reversal. |
| LDT | 8S0 | Posting date. |
| LAMT | 13P2 | Signed amount: +invoice, −payment, +accrual, −reversal. SUM(LAMT) = AP control balance. |
| LDISC | 11P2 | Discount on a payment row. |
| LREF / LMEMO | 8A / 25A | Source reference (receipt/invoice/PO); memo text. |
| Field | Type | Meaning |
|---|---|---|
| AGRUN / VENDNO | 6S0 / 6A | Ageing run period (YYYYMM) + vendor (composite key; re-run refused). |
| AGCURR / AG30 / AG60 / AG90 | 13P2 | Current, 1-30, 31-60, 61+ days-past-due buckets. |
| AGTOT | 13P2 | Vendor total across buckets. |
| Field | Type | Meaning |
|---|---|---|
| SPYR / VENDNO | 4S0 / 6A | Year + vendor (composite key; re-run refused). |
| SPGROSS / SPDISC / SPNET | 13P2 / 11P2 / 13P2 | Gross invoiced; discount taken; net cash paid. |
| SPINVS / SP1099 | 5S0 / 1A | Invoice count; 1099-reportable flag carried from the vendor. |
| Field | Type | Meaning |
|---|---|---|
| GLSEQ | DECIMAL(8,0) | GL sequence (PK); continues across runs so months do not collide. |
| GLPER | DECIMAL(6,0) | Period YYYYMM. |
| ACCT / DRCR | CHAR(9) / CHAR(1) | Account (5100-EXP, 2100-AP, 1010-CSH, 4900-DSC, 2150-GRN); D/C. |
| AMT | DECIMAL(13,2) | Posting amount. |
| GLREF / GLDT | CHAR(8) / DECIMAL(8,0) | Posting group (INVOICES/PAYMENTS/ACCRUAL); post date. |
Index APGLDACC is over (ACCT, DRCR).
SUM(LAMT) is the control total.CALL PROCURE/APSETUP (build objects & compile), then
CALL PROCURE/APREFLD (load reference vendors/POs/lines). Confirm the setup-complete
banner and APREFLD VENDORS=4 POS=4 POLINES=7.APRCPT (RSTAT='E') and invoices into
APINVH/APINVL (ISTAT='E'), by whatever data-entry path your
site uses.SBMJOB CMD(CALL PGM(PROCURE/APDAILY)).APWEEK; check PAID=/GROSS=/DISC= and the
remittance CASH=, and confirm the spooled advice.CALL PROCURE/APMENU (inquiry-only).Pre-checks: confirm the job's library list includes PROCURE; confirm the entered
receipts/invoices carry 'E' status so the posting programs pick them up.
Post-checks after the daily cycle:
APPOAPP approved only active-vendor orders; an on-hold vendor's orders stay 'E'.APRCPOST POSTED= equals the entered receipts; CAPPED= flags any
over-delivery that was trimmed; a cancelled-PO delivery shows as skipped and un-posted.APMATCH MATCHED=/HELD= reconcile; each held invoice shows the right
IHOLDCD (Q/P/N) and consumed no PO quantity.APINVREG: each registered invoice raised its vendor's VBAL by exactly
the invoice total; held invoices did not move any balance. One positive ledger row per invoice.LSEQ is unique.APMONTH. Confirm APAGERP wrote one row per
vendor, APACCR accrued the received-not-invoiced lines, and APGLPST posted
balanced DR/CR rows for the period.APYEAR. Confirm the spend analysis and that every accrual was
reversed (the accrual class nets to zero).Reconciling figures (the same ones the volume/battle suites check against hand-derived oracles):
SUM(LAMT) over APLEDG equals the sum of
open vendor VBALs. A fully invoiced-and-paid vendor nets to 0.00 (invoices +3450.00,
payments −3450.00). APAPRPT re-proves this and prints INVOICED / PAID / DISCOUNT /
CONTROL / ROWS.APPAY row satisfies
PNET = PGROSS − PDISC; total cash sent = SUM(PNET).4900-DSC credit in the GL, not as a smaller balance relief.APGLDIST, SUM(AMT) WHERE DRCR='D' equals
SUM(AMT) WHERE DRCR='C'; expense (5100-EXP) = invoices + accrual; cash (1010-CSH) =
net cash; AP (2100-AP) is posted on both sides for the gross.'V' rows equal and oppose the 'A'
rows, so SUM(LAMT) WHERE LTYPE IN ('A','V') is 0.00.-- reconcile the AP control balance and the GL balance
STRSQL
SELECT LTYPE, SUM(LAMT) FROM PROCURE.APLEDG GROUP BY LTYPE;
SELECT DRCR, SUM(AMT) FROM PROCURE.APGLDIST GROUP BY DRCR;
Each program DSPLYs a result line naming its counts (F.5). The overriding rule is that every posting program is idempotent by a stable ledger/status key, so a partial or repeated run is safe.
| Situation | Behaviour | Action |
|---|---|---|
| Daily cycle fails partway | Only-entered guards (POSTAT/RSTAT/ISTAT='E') plus stable ledger keys. | Re-submit APDAILY: done work is skipped, the rest completes. Balances do not double. |
| Re-run receipt posting | RSTAT='P' and the 1000xxxx ledger key already exist. | Safe no-op: POSTED=0; RCVQTY does not grow. |
| Re-run the match | Only ISTAT='E' invoices are considered. | Safe no-op: MATCHED=0 HELD=0. |
| Re-run invoice registration | 2000xxxx ledger key already exists. | Safe no-op: REGISTERED=0; VBAL does not double. |
| Re-run the payment run | 3000xxxx ledger key already exists. | Safe no-op: PAID=0; no second cheque; balance does not go negative. This is the app's most important guard. |
| Re-run ageing / accrual / spend / reversal | Composite-key or 4000/5000xxxx ledger guards already satisfied. | Safe no-op (VENDORS=0 / ACCRUED=0 / REVERSED=0); totals unchanged. |
| GL post re-run in a new month | Posts only the movement since the last close; GLSEQ continues, only SQLCOD=0 rows counted. | Re-runnable; a refused insert is not reported as posted (a historical bug, now guarded). |
| Held invoice needs paying | A 'H' invoice is never selected by the payment run. | Correct the receipt/price/PO, set it back to 'E' and re-match so it becomes 'O'. |
APLEDG with a memo,
any cycle's effect is fully reconstructable after the fact — and SUM(LAMT) is always a
one-query proof that the AP sub-ledger balances.The program surface, from ap-app/src/sources.mjs. RPG programs are built column-exact
fixed-form C/D specs interleaved with /free blocks (the same helper convention as
ibmi/samples.js). All objects are in library PROCURE.
POTOT=0; the approval sweep
re-derives the total.POSTAT='E' order: CHAINs the vendor (skip unless VSTAT='A'),
re-derives POTOT from the lines (Σ POQTY×POPRICE, excluding
cancelled lines), sets band A/M at the 10000.00 threshold, flips to
'A'. Idempotent by status.KLIST CHAIN on (PONO,RLINE). Caps the delivery at the ordered quantity,
refuses a delivery against a cancelled PO, adds into RCVQTY, flips the line to
R when fully received, writes a ledger 'R' row, flips the receipt to
P. Idempotent by key 10000000+receipt digits.N/Q/P),
holds the invoice and returns. Only a clean invoice runs pass 2, consuming INVQTY and
approving to 'O'. Idempotent (only 'E' considered).'O' invoice not yet on the ledger: raises VBAL/VYTD
by the total, computes the discount date by exact civil-days arithmetic (F.3), writes a positive
ledger 'I' row, stamps DISCDT. Idempotent by key 20000000+invoice.A/R order whose every line is 'I'; leaves it if any
line is still open. Idempotent by status.APINVLF in due-date order; pays each 'O' invoice due by the
pay-through date. Captures VDISCPC% if the run date is on or before the discount date.
Writes one APPAY cheque (PNET=PGROSS−PDISC), relieves VBAL
by the full gross, writes a negative ledger 'P' row. Idempotent by key 30000000+invoice.APREMP PRTF with page overflow
(OFLIND(*IN90)) and a cash total. Printer fields are R-prefixed to avoid
a namespace collision with the APPAY record format.O/H) invoice by whole days past due at the as-of
date (exact civil-days) into current/1-30/31-60/61+. One row per vendor. Idempotent by composite
(AGRUN,VENDNO). Named APAGERP so it does not collide with the APAGE file.RCVQTY>INVQTY: accrues the difference at PO price as a ledger
'A' row. Idempotent by key 40000000+PO digits×10+line.APGLDIST. Continues
GLSEQ; counts only SQLCOD=0 inserts.'I'), discount + net cash
('P'), and the carried 1099 flag. One row per (year,vendor). Idempotent by composite key.'A' row with an equal-and-opposite 'V' row at key
(accrual key + 10000000). Collects the accruals in one pass and writes only into the disjoint
50000000+ range so the new rows are not read back. Idempotent by that key.APLEDG, accumulating INVOICED, PAID, DISCOUNT and the CONTROL total
(SUM(LG-AMT)), and DISPLAYs them — the independent proof that the ledger balances.Every posting program derives its LSEQ from a stable business key in a disjoint range,
and guards each post with a CHAIN(EN) that skips when the row already exists. This is what
makes each cycle re-runnable in place.
| Program | Class | Key formula | Range |
|---|---|---|---|
| APRCPOST | R | 10000000 + receipt digits | 1000xxxx |
| APINVREG | I | 20000000 + invoice digits | 2000xxxx |
| APPAYRUN | P | 30000000 + invoice digits | 3000xxxx |
| APACCR | A | 40000000 + PO digits×10 + line | 4000xxxx |
| APREVACC | V | accrual key + 10000000 (= 50000000 + PO×10 + line) | 5000xxxx |
The composite-key report files (APAGE, APSPND) use the same idea with a
KLIST guard on their period+vendor key instead of a synthetic LSEQ.
The engine's %DATE builtin works on ISO text, not on an 8S0
YYYYMMDD number, so PROCURE/i does its own exact civil-days conversion (Howard Hinnant
days_from_civil / civil_from_days) in DSER (date→serial) and
DADD (serial→date) subroutines. This matters in two places: APINVREG adds the
discount days to the invoice date to compute DISCDT (a plain integer add of 10 to
20260825 would produce the non-existent 20260835, corrupting the payment run's
discount test), and APAGERP measures whole days past due. Both are exact for every Gregorian date
with no 30-day-month approximation.
N31 SFLDSP/SFLDSPCTL, 31 SFLCLR, 90 SFLEND) are laid out at
the exact offset so they parse as 31/90, not a truncated 3/9. Written one column further right,
SFLCLR would never fire and the subfile would silently keep the previous enquiry's rows — a
bug that only shows on a second enquiry, which is why ap_build.mjs pins the
parsed conditions at build time.APINVH for the keyed write-back; two files in one program may not
share a record-format name, so the LF's format is renamed.APPAY record format, which already owns PGROSS/PDISC/PNET/PCNT; the PRTF fields are
R-prefixed to avoid a compile-time collision.APPOAPP re-CHAINs the header after walking the lines and vendor).| Program | Result line |
|---|---|
| APREFLD | APREFLD VENDORS=n POS=n POLINES=n |
| APPOAPP | APPOAPP APPROVED=n SKIP=n |
| APRCPOST | APRCPOST POSTED=n SKIP=n CAPPED=n |
| APMATCH | APMATCH MATCHED=n HELD=n SKIP=n |
| APINVREG | APINVREG REGISTERED=n SKIP=n |
| APHOLDRP | APHOLDRP HELD=n QTY=n PRC=n NOPO=n AMT=n |
| APPOCLS | APPOCLS CLOSED=n SKIP=n |
| APPAYRUN | APPAYRUN PAID=n GROSS=n DISC=n SKIP=n |
| APREMIT | APREMIT LINES=n CASH=n |
| APAGERP | APAGERP RUN=yyyymm VENDORS=n SKIP=n |
| APACCR | APACCR ACCRUED=n AMT=n SKIP=n |
| APGLPST | APGLPST PER=yyyymm ROWS=n |
| APSPEND | APSPEND YEAR=yyyy VENDORS=n R1099=n SKIP=n |
| APREVACC | APREVACC REVERSED=n AMT=n SKIP=n |
| APAPRPT | APAPRPT INVOICED/PAID/DISCOUNT/CONTROL/ROWS (COBOL) |
A auto (at or under 10000.00), M
manual band (over it). It is descriptive — PROCURE/i approves both in one sweep; there is no
second-approver hold.SUM(LAMT) over the AP ledger — the running accounts-payable liability. Invoices
add, payments subtract; the COBOL report APAPRPT re-proves it.(RCVQTY−INVQTY)×POPRICE per PO line,
accrued at month-end (APACCR) and reversed at year-end (APREVACC).Q quantity over receipt, P
price variance, N no matching PO line. A held invoice consumes nothing and is never
paid.PNET = PGROSS − PDISC on every cheque — the discount is exactly the gap
between gross owed and net cash.SBMJOB CMD(CALL PGM(PROCURE/APDAILY)).APGLPST uses it here, to read/write the
APGLDIST GL table; the rest of the app is native record-level I/O.APPOIQ's
order-line list is a subfile; it is cleared and reloaded on each enquiry.VDISCPC),
discount days (VDISCDY), net days (VNETDY) — so the payment run
computes settlement without a terms-table lookup.APMATCH). PROCURE/i's real payment-control gate.