STAFFPRO/i — Staffing Agency Placements, Timesheets, Contractor Pay & Client Billing

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

STAFFPRO/i is a staffing-agency application: it places contractors with clients on job-order placements (each carrying a distinct pay-rate the agency pays the contractor and a distinct bill-rate the agency charges the client), captures weekly timesheets against a placement, then runs two parallel calculations off the same validated hours — contractor gross-to-net pay and client billing — whose spread, after employer burden, is the agency's gross margin. Everything posts to a balanced GL feed so billed − paid − burden reconciles to the margin the report shows. The logic is classic externally-described RPG IV over keyed physical/logical files, with one ILE COBOL trial balance and one SQLRPGLE GL-distribution program; CL drives four job cycles. This manual is the reference for the operator who runs the online screens and the periodic cycles, and for the developer maintaining the application. It is grounded entirely in the committed source (staffing-app/src/sources.mjs, src/seed.mjs, and the test/sp_build.mjs / sp_daily.mjs / sp_cycles.mjs drivers). Everything runs in library STAFFPRO.

Contents

A. Overview & Architecture ↑ top

A.1 What it does

STAFFPRO/i services the full weekly life of a staffing engagement:

A.2 The pay / bill / margin model (two ledgers off one timesheet)

The central design idea is that one validated timesheet drives two independent ledgers. From a single (REGHRS, OTHRS) split the weekly engine SPCALC derives:

The gross margin is billed − gross pay − burden. Because gross pay = net + tax + fee by construction and billed = AR = revenue by construction, the GL batch balances and the same margin figure is recoverable straight from the GL feed (revenue − wage expense − burden expense). The pay side and the bill side are genuinely separate ledgers keyed alike on (week, PLNO) so they join trivially, but each is its own set of records: one is what the contractor is paid, the other is what the client owes.

A.3 Component & flow

  REFERENCE              DAILY                  WEEKLY                 MONTHLY / ANNUAL
  ---------              -----                  ------                 ----------------
  SPREFLD (seed)         SPTIMLD (load raw)     SPCALC   (pay + bill)  SPARAGE  (AR aging)
    SPCLNT/SPCONT/       SPTIMED (edit/split)   SPPAYPRT (pay reg.)    SPMARGIN (margin+YTD)
    SPPLACE              SPTIMRP (reject rpt)   SPINVPRT (inv. reg.)   SPGLDST  (GL feed)
                                               SPGLRPT  (trial bal.)  SPGLRPT  (trial bal.)
                            \                  /                       SPYRRPT  (annual roll)
                             \                /
                              v              v
   SPTIME  (timesheet: E raw -> V valid / R reject -> P processed)
       |
       +--> SPPAYH/SPPAYD (contractor pay)   +--> SPINVH/SPINVD (client invoice)
                    \                              /
                     +--> SPAR (AR ledger)  ------+
                     +--> SPGLFEED (SQL GL, balanced DR/CR)
                     +--> SPYTD (fiscal YTD margin by client)

   ONLINE:  SPMENU --> SPPLCIQ (placement inquiry)  /  SPTIMIQ (timesheet pay inquiry, subfile)

A week's flow: SPTIMLD writes raw (TSTAT='E') timesheets → SPTIMED validates and splits them to V (or rejects to R) → SPCALC reads each V card, writes the pay header/lines and the invoice header/lines, posts one AR row, and flips the card to P (processed) → the print, aging, margin, GL and annual programs read those posted records.

A.4 Object inventory

ObjectTypeRole
SPCLNTPFClient master (spine CLNO).
SPCONTPFContractor master (spine CONO); pay-rate default, withholding, admin fee.
SPPLACEPFPlacement / job order (spine PLNO): pay-rate, bill-rate, OT factor, standard week.
SPPLACLFLFPlacement access path keyed CONO (what a contractor is placed on).
SPTIMEPFWeekly timesheet (key TWEEK+PLNO); status E/V/R/P.
SPTIMLFLFTimesheet access path keyed TSTAT (status enquiry / reject report).
SPPAYH / SPPAYDPFContractor pay header / itemised pay lines.
SPINVH / SPINVDPFClient invoice header / itemised invoice lines.
SPINVLFLFInvoice access path keyed CLNO+ISTAT (client AR aging).
SPARPFAR ledger — durable audit trail, stable business key.
SPYTDPFFiscal YTD margin accumulator, one row per client/year.
SPGLFEEDSQL tableGL distribution feed (double-entry DR/CR); index SPGLFACC.
SPPLACD / SPTIMD / SPMENUDDSPFPlacement inquiry / subfile pay inquiry / main menu.
SPPAYP / SPINVPPRTFPay register / invoice register printer files.
SPREFLDRPGLESeed reference data (clients/contractors/placements).
SPTIMLD / SPTIMED / SPTIMRPRPGLEDaily: raw load / edit-split / reject report.
SPCALCRPGLEWeekly pay + bill engine (the heart of the app).
SPPAYPRT / SPINVPRTRPGLEWeekly pay register / invoice register print.
SPARAGE / SPMARGINRPGLEMonthly AR aging / gross-margin + YTD roll.
SPGLDSTSQLRPGLEMonthly GL distribution (embedded EXEC SQL INSERT).
SPYRRPTRPGLEAnnual margin summary + YTD reset.
SPPLCIQ / SPTIMIQ / SPMENURPGLEOnline placement inquiry / subfile pay inquiry / menu.
SPGLRPTCBLLEILE COBOL trial balance (reconciliation proof).
SPSETUPCLPCreate every object and compile every program.
SPDAILY / SPWEEK / SPMONTH / SPYEARCLPThe four job cycles.

The full catalogue is 10 PFs + 3 LFs + 1 SQL table (+ index), 3 DSPF and 2 PRTF, driven by 14 RPG programs, 1 ILE COBOL program and 5 CL programs. Sections D and F expand each.

B. Online Transactions & Screens ↑ top

B.1 The command/entry line

STAFFPRO/i has no CICS transaction identifiers and no menu-driven transid switch. On IBM i, each program is reached by name from a 5250 command-entry line (or a JOBQ/scheduler for the batch cycles). The operator equivalent of "type a transid and Enter" is "type a CALL command and Enter". Before invoking anything, the job's library list must include STAFFPRO — the tested jobs run with LIBL = QSYS QGPL STAFFPRO QTEMP and CURLIB = STAFFPRO.

To do thisType on the command line
Open the main menu (both online inquiries)CALL STAFFPRO/SPMENU
Open placement inquiry directlyCALL STAFFPRO/SPPLCIQ
Open the timesheet pay inquiry (subfile) directlyCALL STAFFPRO/SPTIMIQ
Run the daily timesheet cycleCALL STAFFPRO/SPDAILY (or SBMJOB it)
Run the weekly pay + bill cycleCALL STAFFPRO/SPWEEK
Run the monthly AR/margin/GL cycleCALL STAFFPRO/SPMONTH
Run the annual rollCALL STAFFPRO/SPYEAR
Build the whole application from sourceCALL STAFFPRO/SPSETUP then CALL STAFFPRO/SPREFLD

None of the programs take CALL parameters. The batch cycles process a fixed work week — the tested run uses week 202631, an as-of/aging date of 20260831 and a GL batch of 202608, all coded as literals in the driver source (the same fixed-period convention the sibling PAYCHEK app uses). A scheduled submission is therefore a bare CALL.

B.2 The menu & inquiry screens

Three interactive programs share three display files. SPMENU (over SPMENUD) is a plain menu that CALLs the two inquiries; each inquiry can also be called directly.

Main menu (SPMENU / SPMENUD)

STAFFPRO/i Main Menu 1. Placement Inquiry 2. Timesheet Pay Inquiry Option . . . . : _ F3=Exit Enter=Select

Key 1 or 2 and Enter; any other non-blank option shows Invalid option.. F3 exits.

Placement inquiry (SPPLCIQ / SPPLACD) — plain screen

Type a placement number and Enter. SPPLCIQ CHAINs SPPLACE for the placement, then SPCONT and SPCLNT for the contractor and client names, and derives the standard-week margin before burden as STDHRS × (BILLRATE − PAYRATE).

Placement Inquiry - STAFFPRO/i Placement number: P00001 Contractor . . : MARGUERITE FONTAINE Client . . . . : ACME MANUFACTURING Pay rate . . . : 20.0000 Bill rate . . : 35.0000 Standard hours : 40.00 Std week margin: 600.00 Status . . . . : A Placement found. F3=Exit Enter=Inquire

A not-found placement blanks the detail fields and shows Placement not found.. The standard-week margin shown is a reference figure (STDHRS×rate-spread, before burden), not the actual weekly margin, which depends on real hours and burden (section A.2).

Timesheet pay inquiry (SPTIMIQ / SPTIMD) — subfile screen

SPTIMIQ is the app's subfile program. It presents a subfile (DDS record SSFL under control record SCTL, SFLPAG(5) per page, SFLSIZ(20)) of a placement's itemised pay lines for the fixed week 202631. Type a placement number; it CHAINs SPPAYH for the gross/net header and SPINVH for the billed figure, then loads the SPPAYD lines on a partial (PWEEK, PLNO) key so exactly one pay stub loads in LSEQ order. The subfile is cleared and reloaded on each inquiry, so a fresh key shows a fresh stub (real subfile-refresh discipline).

Timesheet Pay Inquiry - STAFFPRO/i Placement: P00002 Gross pay . . : 1187.50 Net pay . . . : 958.75 Billed . . . . : 1890.00 Seq T Description Hours Amount 1 E REGULAR HOURS 40.00 1000.00 2 E OVERTIME HOURS 5.00 187.50 3 T TAX WITHHELD 0.00 213.75 4 F ADMIN FEE 0.00 15.00 5 B EMPLOYER BURDEN 0.00 142.50 Pay record found. F3=Exit Roll=Page Enter=Inquire
FieldType (DDS)Shows
IPLNO6A input/outputPlacement number keyed by the operator.
DPGROSS / DPNET13A outputGross / net pay from SPPAYH.
DPBILL13A outputBilled from SPINVH.
SSEQ / STYP / SDESC / SHRS / SAMTsubfilePay-line seq, type (E/T/F/B), description, hours, amount from SPPAYD.
A placement with no pay record for the week shows No pay record for this placement. and an empty subfile. The SFLCLR/rewrite of SCTL happens after the EXFMT returns the new key, immediately before the reload — so paging (Roll Up/Roll Down) works within a stub and a new key replaces it cleanly.

B.3 Controls & audit workflow (no four-eyes maker–checker)

Honest statement: STAFFPRO/i does not model a true four-eyes maker–checker / separate-authorization workflow. There is no "one user enters, a second user approves" step in the code: a timesheet is validated by the daily edit and paid+billed by the weekly run without a human approval gate, and the inquiry screens are strictly read-only. The manual documents the control model the application does have:

In sum, the control posture is a timesheet state machine + master-status gating + a durable, stably-keyed AR ledger + a self-proving balanced GL, rather than a segregation-of-duties approval workflow.

C. Batch Jobs & the Periodic Cycle ↑ top

STAFFPRO/i runs as four CL-driven cycles rather than one monolithic nightly job: a daily cycle (timesheet capture + edit), a weekly cycle (pay + bill + registers), a monthly cycle (AR aging + margin + GL + trial balance), and an annual cycle (fiscal-year margin summary + YTD reset). Each CL program does an ADDLIBLE LIB(STAFFPRO) (guarded by MONMSG SFF0000), CALLs its programs in order, and ends with a completion banner via SNDPGMMSG. None takes CALL parameters; the processing week, aging date and GL batch are literals in the program source.

-- the four cycles are bare, parameterless submissions
SBMJOB CMD(CALL PGM(STAFFPRO/SPDAILY)) JOB(SPDAILY)
SBMJOB CMD(CALL PGM(STAFFPRO/SPWEEK))  JOB(SPWEEK)
SBMJOB CMD(CALL PGM(STAFFPRO/SPMONTH)) JOB(SPMONTH)
SBMJOB CMD(CALL PGM(STAFFPRO/SPYEAR))  JOB(SPYEAR)

C.1 Full batch program set

ProgramCyclePurposeReadsWrites / DSPLY
SPTIMLDDailyLoad one week of raw timesheets (incl. deliberate exception cards).SPTIME (E rows); SPTIMLD CARDS=n SKIP=n.
SPTIMEDDailyEdit/validate every E card; split REG/OT against STDHRS.SPTIME, SPPLACESPTIME (V or R + reason); SPTIMED OK=n REJ=n.
SPTIMRPDailyReject report over the STATUS logical (SPTIMLF), reason-code breakdown.SPTIMLFSPTIMRP REJ=n E1=n E2=n E3=n.
SPCALCWeeklyPay + bill each V card; post AR; flip card to P.SPTIME, SPPLACE, SPCONTSPPAYH/D, SPINVH/D, SPAR, SPTIME(P); SPCALC PAID=n SKIP=n.
SPPAYPRTWeeklyPrint the contractor pay register (PRTF, page overflow).SPPAYH/D, SPCONTSPPAYP spool; SPPAYPRT SLIPS=n NET=n.
SPINVPRTWeeklyPrint the client invoice register (PRTF, page overflow).SPINVH/D, SPCLNTSPINVP spool; SPINVPRT INVS=n BILLED=n.
SPARAGEMonthlyAge open invoices into CURRENT/B31/B61/B90 buckets (as of 20260831).SPINVHSPARAGE INVS=/CUR=/B31=/B61=/B90=/TOT=.
SPMARGINMonthlyMargin (billed−gross−burden) per client; roll SPYTD.SPINVH, SPPAYH, SPYTDSPYTD; SPMARGIN INVS=/BILLED=/PAID=/BURDEN=/MARGIN=.
SPGLDSTMonthlyBalanced double-entry GL distribution into the SQL feed.SPINVH, SPPAYHSPGLFEED (SQL); SPGLDST BATCH=/ROWS=/BILLED=.
SPGLRPTWeekly & MonthlyILE COBOL trial balance: billed/gross/net/tax/fee/burden/paydiff/margin.SPPAYH, SPINVHSPGLRPT BILLED/GROSS/.../MARGIN.
SPYRRPTAnnualFiscal-year margin summary per client; reset SPYTD.SPYTD, SPCLNTSPYTD(reset); SPYRRPT CLIENTS=/TOTBILL=/TOTMARGIN=.
SPREFLDSetupSeed clients / contractors / placements.SPCLNT/SPCONT/SPPLACE; SPREFLD CLI=4 CON=5 PLC=5.

C.2 Daily / weekly / monthly / annual detail

Daily — SPDAILY (SPTIMLD → SPTIMED → SPTIMRP)

SPTIMLD writes seven raw cards for week 202631: four clean (P00001 40.00, P00002 45.00, P00003 35.00, P00004 50.00 raw hours), plus three exception cards (an unknown placement P00009, an ended placement P00005, and a duplicate P00003 which the UNIQUE (TWEEK, PLNO) key refuses) — so it lands 6 cards written, 1 skipped. SPTIMED then edits every E card: it CHAINs SPPLACE, rejects unknown (E1), ended (E2), over-80.00-hour (E3) and non-positive (E4) cards, and for a valid card splits raw hours — anything above the placement's own STDHRS becomes overtime.

Expected DSPLY (the seeded week):
  SPTIMLD CARDS=6 SKIP=1
  SPTIMED OK=4 REJ=2        4 validated, 2 rejected (E1 unknown, E2 ended)
  SPTIMRP REJ=2 E1=1 E2=1 E3=0

The REG/OT split (raw vs the placement's own standard week):
  P00001  40.00 raw, std 40.00  -> REG 40.00  OT  0.00
  P00002  45.00 raw, std 40.00  -> REG 40.00  OT  5.00
  P00003  35.00 raw, std 35.00  -> REG 35.00  OT  0.00   (shorter standard week)
  P00004  50.00 raw, std 40.00  -> REG 40.00  OT 10.00
  totals: 155.00 regular, 15.00 overtime
The standard week is per-placement, not a global constant: P00003 carries STDHRS 35.00 while every other placement uses 40.00. The edit reads each placement's own ceiling, so 35.00 raw against a 35.00 standard week is all regular, no OT.

Weekly — SPWEEK (SPCALC → SPPAYPRT → SPINVPRT → SPGLRPT)

SPCALC is the twin engine. For each V card it computes the pay side and the bill side (section F.2), writes the pay header (SPPAYH) and itemised lines (SPPAYD: REG earning, OT earning if any, TAX, FEE if any, BURDEN), the invoice header (SPINVH) and lines (SPINVD: REGULAR, OVERTIME if any), posts one INVC row to the AR ledger under its stable key, and flips the timesheet to P. SPPAYPRT and SPINVPRT then print the registers, and SPGLRPT runs the trial balance.

Expected DSPLY (the seeded week):
  SPCALC PAID=4 SKIP=2       4 validated cards paid+billed, 2 rejects skipped
  SPPAYPRT SLIPS=4 NET=3477.75
  SPINVPRT INVS=4 BILLED=6840.00
  SPGLRPT MARGIN     2060.40  PAYDIFF 0.00

Monthly — SPMONTH (SPARAGE → SPMARGIN → SPGLDST → SPGLRPT)

SPARAGE walks open invoices and buckets the outstanding balance (BILLED − PAIDAMT) by age from INVDT to the fixed as-of date 20260831, using a 30-day-month julian approximation good enough for same-year deltas: 0-30→CURRENT, 31-60→B31, 61-90→B61, 90+→B90. SPMARGIN joins each invoice header to its pay header on the shared (week, PLNO) key, computes billed − gross − burden, and rolls the figures into SPYTD per client. SPGLDST sums the period's billing and pay and writes the balanced GL batch (below). SPGLRPT re-proves the reconciliation.

Expected DSPLY (seeded week + one injected aged 500.00 orphan invoice):
  SPARAGE INVS=5 CUR=6840.00 B61=500.00 TOT=7340.00
  SPMARGIN INVS=5 BILLED=7340.00 PAID=4267.50 BURDEN=512.10 MARGIN=2560.40
  SPGLDST  BATCH=202608 ROWS=8 BILLED=7340.00
  SPGLRPT  MARGIN 2560.40   PAYDIFF 0.00

The GL journal (SPGLDST)

SPGLDST posts three balanced journals with free-form exec sql insert into STAFFPRO.SPGLFEED ...;:

Journal (GLREF)DebitCredit(s)
BILLING1200-AR = billed4100-REV = billed
CONTRPAY6100-CPAY = gross2100-NETP net + 2200-TAXP tax + 2400-FEEP fee
BURDEN6200-BURD = burden2500-BURP = burden

Billed = AR = revenue by construction, and gross = net + tax + fee by construction, so total debits equal total credits. Margin (billed − paid − burden) is simply 4100-REV − 6100-CPAY − 6200-BURD read back from the same feed.

Annual — SPYEAR (SPYRRPT)

SPYRRPT prints one summary block per client from the YTD accumulator, totals billed / paid / margin, then zeroes the accumulator for the new fiscal year (the invoice headers themselves are left alone). An already-reset accumulator (YBILLED ≤ 0) is skipped, so a second run finds nothing to summarise.

Expected DSPLY:
  SPYRRPT CLIENTS=4 SKIP=0 TOTBILL=7340.00
  SPYRRPT TOTPAID=4267.50 TOTMARGIN=2560.40
  re-run: SPYRRPT CLIENTS=0 SKIP=4  (all accumulators already reset)

C.3 Ordering & dependencies

D. Data Files (data dictionary) ↑ top

All objects live in library STAFFPRO, grounded in the DDS/SQL in staffing-app/src/sources.mjs. The physical/logical files are externally-described DDS; the GL feed is an SQL table. RPG packed-decimal notation is written nPd (n digits, d after the point, e.g. 11P 2); zoned is nSd. Dates are stored as 8S 0 in YYYYMMDD form (weeks as 6S 0 YYYYWW); rates are 7P 4 (so 0.1500 = 15%, and 1.5000 is the OT factor); money is packed 11P 2 on the pay/invoice ledgers and 13P 2 on the YTD/GL aggregates. Every physical file below is UNIQUE on its key.

SPCLNT — Client master (key CLNO)

FieldTypeMeaning
CLNO6AClient number (key), K00001 style (letter + 5 digits).
CNAME30AClient name.
CADDR30AClient address.
CTERMS2S 0Payment terms in days (e.g. 30, 15).
CSTAT1AA active, H on hold (no new placements, existing ones still bill), C closed.
CSTARTDT8S 0Client relationship start date (YYYYMMDD).

SPCONT — Contractor master (key CONO)

FieldTypeMeaning
CONO6AContractor number (key), W00001 style.
WNAME30AContractor name.
SKILL4ASkill code (MACH, WHSE, ENGR…).
PAYRATE7P 4Contractor default hourly pay rate (reference only — the placement is authoritative).
WHRATE7P 4Flat withholding-election rate (e.g. 0.1500 = 15%).
ADMINFEE9P 2Flat weekly post-tax deduction (e.g. equipment rental). Note the width — see F.5.
WSTAT1AA active, T terminated, L leave of absence.
HIREDT8S 0Hire date (YYYYMMDD).

SPPLACE — Placement / job order (key PLNO)

FieldTypeMeaning
PLNO6APlacement number (key), P00001 style.
CONO6AContractor placed (→ SPCONT).
CLNO6AClient served (→ SPCLNT).
PAYRATE7P 4Authoritative pay rate the agency pays the contractor on this placement.
BILLRATE7P 4Bill rate the agency charges the client on this placement.
OTFACT7P 4OT premium factor on the PAY side (seeded 1.5000); the bill side carries no OT premium.
STDHRS5P 2Per-placement standard week; raw hours above this become overtime.
PSTARTDT / PENDDT8S 0Placement start / end dates (PENDDT 0 when open).
PSTAT1AA active, E ended (E is skipped by the edit and the weekly engine).

Seeded placements: P00001 W00001→K00001 pay 20/bill 35; P00002 W00002→K00001 pay 25/bill 42; P00003 W00003→K00002 pay 18/bill 30 (STDHRS 35.00); P00004 W00004→K00003 pay 30/bill 50; P00005 W00005→K00002 pay 19/bill 32 ENDED. OTFACT 1.5000 throughout.

SPPLACLF — Placement access path (LF over SPPLACE, key CONO+PLNO)

Non-unique logical keyed by contractor then placement, so “what is this contractor placed on” is a keyed read rather than a full-file scan.

SPTIME — Weekly timesheet (key TWEEK+PLNO)

FieldTypeMeaning
TWEEK6S 0Work week YYYYWW (key part 1); the tested week is 202631.
PLNO6APlacement (key part 2).
REGHRS / OTHRS5P 2Regular / overtime hours after the daily edit's split (both 0 until validated).
RAWHRS5P 2Raw hours as loaded, before the REG/OT split.
TDT8S 0Timesheet date (YYYYMMDD).
TSTAT1AE entered/raw, V validated, R rejected, P processed (paid+billed).
TERRCD2AReject reason code on an R card (E1–E4, section F.4); blank otherwise.

SPTIMLF — Timesheet access path (LF over SPTIME, key TSTAT+PLNO)

Non-unique logical keyed by status then placement, so the reject report reads only the R rows as a keyed range (proving the access path is really keyed on TSTAT).

SPPAYH — Contractor pay header (key PWEEK+PLNO)

FieldTypeMeaning
PWEEK / PLNO6S 0 / 6APay week + placement (composite key; joins to SPINVH on the same pair).
CONO6AContractor paid.
REGPAY / OTPAY / GROSS11P 2Regular pay, OT pay, gross = regular + OT.
TAXWH11P 2Flat tax withheld = gross × WHRATE.
PADMFEE11P 2Admin-fee deduction posted this week. Renamed from ADMINFEE — see F.5.
NETPAY11P 2Net = gross − tax − fee.
BURDEN11P 2Employer burden = gross × 12% (informational; the agency's cost, never netted from pay).
PAYDT8S 0Pay date (YYYYMMDD).
HSTAT1AHeader status (P processed).

SPPAYD — Contractor pay detail lines (key PWEEK+PLNO+LSEQ)

FieldTypeMeaning
PWEEK / PLNO / LSEQ6S 0 / 6A / 3S 0Header key + line sequence.
LTYPE1AE earning, T tax, F post-tax flat deduction, B employer burden (informational).
LDESC20ALine description (REGULAR HOURS, OVERTIME HOURS, TAX WITHHELD, ADMIN FEE, EMPLOYER BURDEN).
LHRS5P 2Hours on this line (0 on TAX/FEE/BURDEN).
LRATE7P 4Rate applied (pay-rate, OT effective rate, WH rate, burden rate).
LAMT11P 2Line amount.

SPINVH — Client invoice header (key IWEEK+PLNO)

FieldTypeMeaning
IWEEK / PLNO6S 0 / 6ABilling week + placement (mirrors SPPAYH's key so the two ledgers join trivially).
CLNO6AClient billed.
REGBILL / OTBILL / BILLED11P 2Regular billed, OT billed (no premium), billed = regular + OT.
INVDT / DUEDT8S 0Invoice / due dates (YYYYMMDD).
PAIDAMT11P 2Cash applied so far (0 when open; drives the AR-aging outstanding balance).
ISTAT1AO open, P paid.

SPINVD — Client invoice detail lines (key IWEEK+PLNO+LSEQ)

FieldTypeMeaning
IWEEK / PLNO / LSEQ6S 0 / 6A / 3S 0Header key + line sequence.
LDESC20AREGULAR HOURS / OVERTIME HOURS.
LHRS / LRATE / LAMT5P 2 / 7P 4 / 11P 2Hours, bill-rate, line amount.

SPINVLF — Invoice access path (LF over SPINVH, key CLNO+ISTAT+IWEEK)

Non-unique logical keyed client / invoice-status / week, so the AR-aging run reads one client's open invoices as a keyed range rather than a full-file scan.

SPAR — AR ledger (key ARSEQ)

FieldTypeMeaning
ARSEQ8S 0Stable business key (not an identity): invoice = 20000000 + placement×100 + week-offset; cash = 40000000 + ….
CLNO / PLNO6AClient / placement the entry concerns.
ARTYP4AEntry type (INVC invoice posting; cash applications reserved on the 40000000 base).
ARAMT11P 2Amount (billed, for an INVC row).
ARDT8S 0Entry date (YYYYMMDD).
ARMEMO25AFree-text memo (e.g. WEEKLY CLIENT INVOICE).

SPYTD — Fiscal YTD margin accumulator (key FISCYR+CLNO)

FieldTypeMeaning
FISCYR / CLNO4S 0 / 6AFiscal year + client (one accumulator row each).
YBILLED / YPAID13P 2YTD billed / YTD gross pay for the client.
YBURDEN / YMARGIN13P 2YTD burden / YTD margin (billed − paid − burden).
YWEEKS3S 0Weeks rolled into the accumulator (the annual roll zeroes all five money/count fields).

SPGLFEED — GL distribution feed (SQL table, PK GLSEQ)

ColumnTypeMeaning
GLSEQDECIMAL(8,0)Journal-row sequence (PK).
GLBATCHDECIMAL(6,0)GL batch YYYYMM (the tested batch is 202608).
ACCTCHAR(9)Account (1200-AR, 4100-REV, 6100-CPAY, 2100-NETP, 2200-TAXP, 2400-FEEP, 6200-BURD, 2500-BURP).
DRCRCHAR(1)D debit, C credit.
AMTDECIMAL(13,2)Journal amount.
GLREFCHAR(8)Journal reference (BILLING, CONTRPAY, BURDEN).
GLDTDECIMAL(8,0)GL date (YYYYMMDD).

Index SPGLFACC on (ACCT, DRCR) supports reading the margin back per account.

Relationships

E. Operations Runbook ↑ top

STAFFPRO/i has no control row to advance: the processing week (202631), the AR as-of date (20260831) and the GL batch (202608) are literals in the driver source (section B.1). An operator runs the four cycles in order and checks the DSPLY banner each program emits. The figures below are the seeded reference dataset; on a live period they change, but the reconciliation relationships (net + tax + fee = gross; billed − gross − burden = margin; total debits = total credits; PAYDIFF = 0.00) must hold every time.

E.1 Day-in-the-life

  1. Confirm the job's library list includes STAFFPRO (the tested job runs LIBL = QSYS QGPL STAFFPRO QTEMP, CURLIB = STAFFPRO).
  2. Submit the daily timesheet cycle: SBMJOB CMD(CALL PGM(STAFFPRO/SPDAILY)). It loads the week's raw cards, edits/splits them, and reports the rejects.
  3. Once the week's cards are validated, submit the weekly pay+bill cycle: SBMJOB CMD(CALL PGM(STAFFPRO/SPWEEK)). Collect the two spooled registers (SPPAYP pay, SPINVP invoice) and read the trial-balance banner.
  4. Answer online inquiries through CALL STAFFPRO/SPMENU (placement inquiry / timesheet pay inquiry) as they arrive — both are read-only.

Post-checks after the daily cycle (seeded week):

Post-checks after the weekly cycle (seeded week):

E.2 Month-end & year-end close (with the margin reconciliation)

Monthly: SBMJOB CMD(CALL PGM(STAFFPRO/SPMONTH)) runs aging → margin → GL → trial balance. On the seeded run one aged orphan invoice (client K00002, 500.00, dated 20260617) is injected so the aging buckets and the “invoice with no matching pay header” path are both exercised:

Expected monthly DSPLY:
  SPARAGE  INVS=5 CUR=6840.00 B61=500.00 TOT=7340.00
  SPMARGIN INVS=5 BILLED=7340.00 PAID=4267.50 BURDEN=512.10 MARGIN=2560.40
  SPGLDST  BATCH=202608 ROWS=8 BILLED=7340.00
  SPGLRPT  MARGIN 2560.40   PAYDIFF 0.00

The margin reconciliation, step by step. The whole point of the app is that two independent ledgers reconcile to one margin figure, provable three different ways:

  1. Off the pay ledger: net + tax + fee must equal gross. Seeded week: 3477.75 + 759.75 + 30.00 = 4267.50 = gross pay — so SPGLRPT PAYDIFF is exactly 0.00. If PAYDIFF is non-zero, a pay line was mis-posted; stop and investigate.
  2. Off both ledgers: margin = billed − gross − burden. Weekly (4 real invoices): 6840.00 − 4267.50 − 512.10 = 2060.40. Monthly (+ the 500.00 orphan, which adds billing but no pay/burden): 7340.00 − 4267.50 − 512.10 = 2560.40 — the orphan flows straight through to margin because it has no matching pay header (burden is unchanged at 512.10).
  3. Off the GL feed: read margin straight back as 4100-REV − 6100-CPAY − 6200-BURD. Because billed = AR = revenue and gross = net + tax + fee by construction, total debits equal total credits, and this equals the same 2560.40.

Per-client the monthly margin lands as K00001 1064.00 (P00001 504.00 + P00002 560.00), K00002 344.40 + the 500.00 orphan, K00003 652.00 — rolled into SPYTD per client.

Annual: once the fiscal year's months are closed, SBMJOB CMD(CALL PGM(STAFFPRO/SPYEAR)) summarises the YTD accumulator per client and zeroes it:

Expected annual DSPLY:
  SPYRRPT CLIENTS=4 SKIP=0 TOTBILL=7340.00
  SPYRRPT TOTPAID=4267.50 TOTMARGIN=2560.40
  re-run: SPYRRPT CLIENTS=0 SKIP=4   (all accumulators already reset)

E.3 Failure & re-run rules

Every program is written to be safely re-runnable; a re-run reports zero new work rather than double-posting. If a cycle aborts partway, just run it again — the guards below make it a no-op for whatever already committed.

F. Developer Reference ↑ top

F.1 Program set

14 RPG programs (13 RPGLE + 1 SQLRPGLE), 1 ILE COBOL, 5 CL, over 10 PF + 3 LF + 1 SQL table, seeded as source-physical-file members by src/seed.mjs (seedStaffpro()) and built by SPSETUP. All are classic externally-described I/O: F-specs open the DDS files with the E extension; the RPG is a mix of fixed-form C-specs (KLIST/CHAIN/SETLL/READE loops) and /free blocks. Language split by kind: RPGLE for the online + batch logic, SQLRPGLE (SPGLDST) for the embedded-SQL GL distribution, ILE COBOL (SPGLRPT) for the trial balance reading the PFs directly, CL for setup and the four cycles.

F.2 The gross-to-net / billing arithmetic (SPCALC)

SPCALC is the twin engine. All money is computed half-adjusted (eval(h)) so a rate like 20.0000 × 40.00 hours lands on exact cents. For each V timesheet it uses the placement's pay-rate/bill-rate/OT-factor (not the contractor default) and the contractor's withholding rate + admin fee:

  -- PAY SIDE
  regular pay    = REGHRS * PAYRATE
  overtime pay   = OTHRS  * PAYRATE * OTFACT        (OTFACT 1.5000)
  gross pay      = regular pay + overtime pay
  tax withheld   = gross pay * WHRATE               (flat, from SPCONT)
  admin fee      = SPCONT.ADMINFEE  (if > 0)         -> PADMFEE on SPPAYH
  net pay        = gross pay - tax withheld - admin fee
  burden         = gross pay * 0.1200               (WBRT; informational, agency cost)
  -- BILL SIDE (no OT premium to the client)
  regular billed = REGHRS * BILLRATE
  overtime billed= OTHRS  * BILLRATE
  billed         = regular billed + overtime billed
  -- so, by construction:
  margin         = billed - gross pay - burden

Pay lines are emitted in order REG earning → OT earning (only if OTHRS > 0) → TAX → FEE (only if fee > 0) → BURDEN, each with its own LSEQ; invoice lines are REGULAR → OVERTIME (only if OTHRS > 0). The burden rate WBRT is a hard-coded 0.1200 in the program (not a master field).

F.3 Idempotency & access-path patterns

F.4 Reject reason codes (SPTIMED → SPTIME.TERRCD)

CodeMeaningSet when
E1No such placementThe CHAIN on SPPLACE misses (unknown PLNO, e.g. P00009).
E2Placement not activeThe placement exists but PSTAT ≠ 'A' (ended, e.g. P00005).
E3Hours over ceilingRaw hours > 80.00 for one week.
E4Non-positive hoursRaw hours ≤ 0.

A rejected card gets REGHRS = OTHRS = 0, TSTAT = 'R' and the reason in TERRCD; the weekly engine only ever pays a V card, so a reject never bills.

F.5 Platform note (data-corruption finding)

Confirmed platform finding (STAFFPRO/i FINDINGS.md, Finding 1). This emulator's RPG compiler silently shares working storage between same-named fields from DIFFERENT externally-described files when their length/decimals differ — with no compile-time diagnostic. ibmi/rpg.js keeps one flat symbol map keyed only on the uppercased field name (no per-file qualification); the F-spec registration loop registers each name only once, so whichever file is listed first in the F-specs “wins” the shared symbol, and the load/unload MOVEs then run through that (wrongly-sized) shadow variable, corrupting the value.

The concrete case this app hit. SPCONT declared ADMINFEE as 9P 2 (the contractor's default fee election). SPPAYH originally also declared a field named ADMINFEE, as 11P 2 (the fee posted to a pay period). SPCALC opens FSPCONT before FSPPAYH, so the narrower 9P 2 layout won the shared symbol. Reading a $15.00 fee from SPCONT and writing it into what should have been SPPAYH.ADMINFEE before WRITE SPPAYHR produced a value the downstream COBOL trial balance (SPGLRPT, which reads SPPAYH straight from disk, independent of the RPG compiler) read back 1000× too large — two $15.00 fees became 30000.00 instead of 30.00, i.e. $30 became $30,000. Because SPGLRPT read the corruption straight off the physical file's stored bytes, the bad value was baked into the PF, not merely an RPG-side display artifact.

Why it matters: real IBM i RPG IV also auto-creates one global field per unique name across E-extension files, but the real compiler validates length/type/decimals across every definition and issues an SNF0512/SNF0620-class diagnostic on a conflict (or requires RENAME/PREFIX). This emulator neither checks compatibility nor raises any diagnostic — it silently drops the second definition and reuses the first file's layout for both, so a legitimate same-name coincidence turns into silent, plausible-looking data corruption (worse than a hard failure).

App-level fix (already in the source): the pay-header field was renamed ADMINFEE → PADMFEE (DDS SPPAYH, plus SPCALC and SPGLDST) so the two fields no longer collide — a legitimate rename, independent of the platform gap. The platform gap itself (no compile-time conflict check, and no DDS RENAME/PREFIX support in rpg.js to safely open two files that share a field name by coincidence) remains the finding; the recommendation is to either raise a diagnostic on a conflicting shared field or support DDS-side qualification.

G. Glossary ↑ top

Placement (job order)
The binding of one contractor to one client at an agreed pay-rate, bill-rate, OT factor and standard week (SPPLACE, spine PLNO). The placement — not the contractor master — is authoritative for the rates used in pay and billing.
Pay-rate vs bill-rate
PAYRATE is what the agency pays the contractor per hour; BILLRATE is what the agency charges the client per hour. Their spread, less burden, funds the agency's margin.
Standard week (STDHRS)
The per-placement hour ceiling before overtime; raw hours above STDHRS become OT. It is per-placement, not a global constant (P00003 carries 35.00 while others use 40.00).
REG / OT split
The daily edit (SPTIMED) partitions a card's RAWHRS into REGHRS (up to STDHRS) and OTHRS (the remainder), which are the inputs to every pay and bill figure.
OT factor (OTFACT)
The overtime premium multiplier on the pay side (seeded 1.5000). The bill side carries no OT premium — that 0.5× is the agency's cost.
Gross / net pay
Gross = regular pay + OT pay. Net = gross − tax withheld − admin fee. Held on SPPAYH; itemised on SPPAYD.
Withholding (WHRATE)
A flat per-contractor tax-withholding rate (7P 4) applied to gross pay — deliberately flat, unlike the sibling PAYCHEK app's graduated bands.
Admin fee (ADMINFEE / PADMFEE)
A flat weekly post-tax deduction some contractors carry (e.g. equipment rental). ADMINFEE on SPCONT is the election; PADMFEE on SPPAYH is the amount posted (renamed to avoid the collision in F.5).
Employer burden (BURDEN)
The agency's own on-cost of employing the contractor (FICA/WC/etc), computed as gross × 12%. Informational (LTYPE='B'): never subtracted from what the contractor is paid, but subtracted when computing margin.
Billed
What the client owes for the week = regular billed + OT billed (SPINVH). A genuinely separate ledger from pay, though keyed alike on (week, PLNO).
Gross margin
billed − gross pay − burden — the agency's gross profit on the placement/week. Recoverable three ways (E.2): off the ledgers, off the GL feed, and proven by the trial balance's PAYDIFF = 0.00.
Timesheet state machine (TSTAT)
E entered (raw) → V validated or R rejected (with a reason code) → P processed. The weekly engine only pays a V card and flips it to P, so it cannot be paid twice.
Reject reason (TERRCD)
E1 unknown placement, E2 ended placement, E3 hours over the 80.00 ceiling, E4 non-positive hours (F.4).
AR ledger (SPAR)
The durable audit trail of invoice postings (and reserved cash applications), keyed on a stable, derived ARSEQ so a re-run is refused rather than double-posting.
AR aging
Bucketing each open invoice's outstanding balance (BILLED − PAIDAMT) by age from INVDT to the as-of date into CURRENT / B31 / B61 / B90 (SPARAGE), using a 30-day-month julian approximation.
Fiscal YTD accumulator (SPYTD)
One row per client per fiscal year holding YTD billed / paid / burden / margin / weeks; filled by the monthly margin roll and zeroed by the annual roll.
GL feed (SPGLFEED)
The balanced double-entry SQL journal the monthly close posts (BILLING / CONTRPAY / BURDEN), from which margin is readable back as 4100-REV − 6100-CPAY − 6200-BURD.
Trial balance (SPGLRPT)
The ILE COBOL reconciliation proof reading SPPAYH/SPINVH straight from disk: its PAYDIFF line is exactly 0.00 and its MARGIN matches the reported margin.
Idempotency guard
A UNIQUE key or composite-key CHAIN that makes a program safely re-runnable (F.3), reporting zero new work rather than double-posting.