AFTEC Job Costing & WIP
Verified against live AFTEC (MRP-PROD) via the UniData API and against the unencrypted PRO-III source in
PBSBASE/VBBASE. Reviewed and corrected by John Tymes, 2026-08-07.
This page is about job costing only. The weekly last-cost rollup (CA1771 / ITEMCA, the
LLC-descending part-level replacement-cost cascade) is a completely separate mechanism that happens to have a
similar shape. It does not feed the job ledger and nothing here depends on it. See the separate
last-cost brief.
Companion pages: CABASE — Cost Accounting (all 108 cost-accounting
programs from the vendor manual), Reading AFTEC Source — and Knowing What Actually Runs
(the method behind everything here, and the catalog-drift hazard),
Inventory & Work Orders (the flattened wipledger.csv export view),
AFTEC Subroutine Catalog.
Which source is real — the catalog decides
Several directories under \\ad.ptihome.com\DFS\Data\LinuxShare\pro3prog\ hold items with the same program
name. Which one runs is determined by the UniData catalog, not by any directory-precedence rule.
Cataloging is a deliberate, per-program act: someone ran CATALOG <file> <item>, and that registration is
what a program name resolves to. Finding source in a directory proves nothing about whether it executes.
You can see the mechanism in the VOC. A cataloged program is a type-C record whose attribute 2 is the
object path and attribute 3 records the source file and item it was cataloged from:
C ⁞ /home/pro3/CTLG/CA1745.CA ⁞ PBSBASE CA1745.CA
That’s the receipt for a manual CATALOG PBSBASE CA1745.CA. /home/pro3 is S:\LinuxShare\pro3.
Most programs are globally cataloged — the objects live in $UDTHOME/sys/CTLG (a copy of UniData home is
at S:\mrp1\DataBackup\ud.backup). The catalog holds 3,318 objects. Hashing every one of them against
every compiled _NAME object in pro3prog resolves what actually runs:
| Result | Count | Meaning |
|---|---|---|
| Exact byte match to a source object | 2,877 | Provenance proven |
| Same name, different bytes | 91 | Recompiled after cataloging — the source on disk is not what runs |
No matching source item in pro3prog | 350 | Source lives elsewhere, or was deleted |
43 different source directories feed the catalog — PBSBASE (864), VBBASE (317), CABASE (193),
SFBASE (115), IBASE (113), SYBASE (111), UBASE (94), APBASE (84), POBASE (83) and more. There is
no two-layer hierarchy; the module directories all run, and which item a name resolves to was set one
CATALOG command at a time.
AFTEC is the sole exception: 0 of its 2,202 items are the source of any cataloged object. It has no
compiled objects, so there is nothing to catalog. It genuinely does not execute.
Cataloging is a copy, so source can drift from what runs
CATALOG copies the object into the catalog directory. Recompile without re-cataloging and the two diverge —
and the catalog copy is the one that executes. 91 programs are in that state, including four in the
job-costing path: END.OF.SHIFT, PBS1803, VB_W.7285 and VB_JOB.COST.REV1, plus GET.MAT,
RPT.WIPLEDGER, TS.UPD and FIXJOBHRS.
This is not theoretical. END.OF.SHIFT’s catalog copy is dated 2024-09-11; the object in PBSBASE is
2025-02-24, 8 bytes larger, with an identical literal table — a pure control-flow change. That change
is the bare STOP after the outside-ops step discussed below. It was compiled but never re-cataloged, so
it never went live — which is why the evening steps still run every night. Anyone reading PBSBASE source
for these eight programs is reading an edit that production has never seen.
What runs, for the programs on this page
| Name | Resolves to |
|---|---|
1656 | PBSBASE/I.1656 |
CA1745.CA, GL7013C, VOUCHUPD, CA1795, CA1768, UPD.JOB.INPROCESS, SGA.UPD, CA1796, CA1797, CA1799, CA1771, CA1778, ACT.CA1778, AUTO.TS.UPD, AUTO.WIP.CLOSE | PBSBASE/<same name> |
VB_I.1656, VB_I.1656B, VB_CA1768, VB_GL7013 | VBBASE/<same name> |
END.OF.SHIFT, PBS1803 | PBSBASE — but the catalog copy is older than the source |
VB_W.7285, VB_JOB.COST.REV1 | VBBASE — but the catalog copy is older than the source |
Note that END.OF.SHIFT calling VB_I.1656 rather than I.1656 is not an override — those are two
different program names, and VB_I.1656 is a wrapper that goes on to EXECUTE '1656', which resolves to
PBSBASE/I.1656. *.1287 programs and files are PSI-specific extensions to base AFTEC.
Reproducing this. Copy
$UDTHOME/sys/CTLGand every_*object underpro3prog, then match by SHA-256. Exact match = proof of provenance; same-name-different-bytes = drift; the catalog copy wins. Note the catalog directory is hashed into single-letter subdirectories (CTLG/e/END.OF.SHIFT), so recurse.
The model in one paragraph
Every transaction program posts twice, in the same pass: once into the work-order accumulator
(WIPLEDGER.*) and once into the job accumulator (JOBLEDGER.*). Both are multivalued matrices of
fiscal period × cost-accounting class or cost centre × cost element. GL7013C is literally titled
“JOURNAL UPDATE FOR WIPLEDGER AND JOBLEDGER”. Job cost is accumulated as it happens, not derived from
the work orders — and some cost reaches the job with no work order at all.
Separately, the general ledger carries the same money as account balances (WIP 1235-000, inventory
1200-000 / 1210-000 / 1220-000). Those two accumulations are maintained independently, which is why
they can — and do — drift. Closing that gap is the whole point of the Cost Differ column in SO Manager.
Correction (2026-08-07). An earlier version of this page claimed job cost was derived by a nightly roll-up of the work-order ledger (
ACT.CA1778). That was wrong, and John Tymes caught it: if job cost were re-derived fromWIPLEDGER, the cost differences would not exist. Verified —ACT.CA1778has no callers anywhere in the codebase, andCA1778is only reachable from an interactive menu. Both are repair tools.
The ledger file pair
| Suffix | WO level | Job level | Holds |
|---|---|---|---|
.MAT | WIPLEDGER.MAT | JOBLEDGER.MAT | Material cost + material overhead, by CA class |
.LAB | WIPLEDGER.LAB | JOBLEDGER.LAB | Labor, fixed OH, variable OH, rework, indirect — by cost dept |
.OUT | WIPLEDGER.OUT | JOBLEDGER.OUT | Outside processing + outside overhead |
.AP | WIPLEDGER.AP | JOBLEDGER.AP | Direct A/P charges (ODC), by transaction type |
.HRS | WIPLEDGER.HRS | JOBLEDGER.HRS | Labor hours by cost centre ! grade |
.CON | WIPLEDGER.CON | JOBLEDGER.CON | Consignment value issued |
.STR | WIPLEDGER.STR | JOBLEDGER.STR | “Cost stream” — flat element totals per period |
.SUM | WIPLEDGER.SUM | JOBLEDGER.SUM | Rolled totals — the record everything else reads |
.INV | (n/a) | JOBLEDGER.INV | Job-pegged inventory not issued to any WO |
.XFER | (n/a) | JOBLEDGER.XFER | Cost transferred on/off the job |
.SGA | (n/a) | JOBLEDGER.SGA | SG&A burden, % and $ by period |
Keys are CO!WONO and CO!JOBNO (company is always 1 at PSI).
WIPLEDGER.MAT shows the internal shape. Attribute 1 is the fiscal-period list in reverse time order; every
other attribute is value-aligned to it and sub-valued by CA class:
| Attr | Name | Meaning |
|---|---|---|
| 1 | FPER | Fiscal periods (YYPP), newest first |
| 2 | CLASS | Cost accounting class, SVM’d within each period |
| 3 / 5 | STD.INCR.MAT / ACT.INCR.MAT | Standard / actual incremental material |
| 4 / 6 | STD.INCR.OVH.PO / ACT.INCR.OVH.PO | Material OH applied at purchase receipt |
| 9 / 10 | STD.INCR.OVH.ISS / ACT.INCR.OVH.ISS | Material OH applied at issue |
| 7 / 8 | QTY.COMP / QTY.SCRAP | Quantity completed / scrapped per period |
| 11–16 | *.CUMM.* | The same six figures as cumulative carry-forward |
WIPLEDGER.SUM F21 (finished-goods cost — what lands in inventory at WIP receipt) should equal CSUM.ALL,
the sum of all cumulative + incremental elements. A dictionary I-descriptor CSUM.DIFF exists to police that
identity. The dictionary comment on F21, added by JPT on 8/1/16: “FINISHED GOODS. THIS APPEARS TO BE THE
COST THAT GOES INTO INVENTORY DURING WIP RECEIPT. IT SHOULD EQUAL CSUM.ALL”.
The PROJ work order
Each job carries a job-level work order whose number equals the job number, with part PROJ and WO type
N (non-assembly). Job 2412 has WO 2412. There are 5,421 such WOs, 5,418 of them type N.
This is the vehicle for cost that isn’t tied to a manufacturing work order — direct invoices, project
engineering labor, ODC. Programs that need a WO to find the job (GL7013C, VOUCHUPD, CA1745.CA) resolve
it through the PROJ WO. I.1656 special-cases it explicitly (IF JOBNO = WONO THEN …), and the WIP.CLOSE
dictionary flags it (IF NUM(PARTNO) OR JOBNO = WONO THEN 'Y').
For the labor side of this WO — who charges it, what they charge, and the two-axis Project Task / Work Type design that replaces it in BC — see Project-Charged Labor & the Business Central Container. It carries 54.8% of all job-charged hours, so it is the majority case, not an exception.
How cost gets in
Inventory transactions — I.1656, driven by VB_I.1656
I.1656 (“DAILY INVAUDIT UPDATE”) drains the INVAUDIT queue and posts every inventory transaction into
both ledger levels. It is the only writer of JOBLEDGER.XFER and JOBLEDGER.INV.
| Type | Event | Effect |
|---|---|---|
01–04 | Receipt into inventory | Material OH-in applied (MOVPCTIN); lot cost stamped on LOTHIST |
05,11,13,24,31,37 | Issue out of inventory | Material OH-out applied (MOVPCTOUT) |
23, 47 | Scrap | Writes SCRAPREG, reduces WO material |
30 | WIP receipt / completion | Posts the cost stream (*.STR) at both levels |
29 | Job transfer | Sets XFERFLAG → JOBLEDGER.XFER |
The internal comment on its update routine — “UPDATE LEDGREC AND SUMREC (WIP INCR, JOB CUMM)” — is the rule: WO ledger takes the increment, job ledger takes the cumulative.
VB_I.1656 is the live entry point. It reads the fiscal period from LOCKFILE "1656VAR1", refuses to run if
it doesn’t match the caller’s, then chains VB_I.1656B → VB_I.1656C → VB_I.1656GL → 1656 → 1656D.
Labor — CA1745.CA
Timesheets (TS.1287 → LABOR.HEAD / LABOR) are swept by CA1745.CA, which writes WIPLEDGER.LAB/.HRS
and JOBLEDGER.LAB / .HRS / .HRS.1287 / .SUM directly. As of 2026-08-07 there are 0
un-updated LABOR.HEAD rows.
G/L journal entries — GL7013C
Signature: GL7013C(HEADFILE, LINEFILE, …, JOBLEDGER.AP, JOBLEDGER.LAB, JOBLEDGER.MAT, JOBLEDGER.OUT, JOBLEDGER.SUM, OPENWO, WIPLEDGER.AP, WIPLEDGER.LAB, WIPLEDGER.MAT, WIPLEDGER.OUT, WIPLEDGER.SUM, …).
Each journal line carries a WO and a type prefix that routes it: M* → material, L* → labor, O* →
outside, D/A → direct/ODC, S → subassembly. It writes the WIP ledger, then — IF JOBNO # "" — the job
ledger. Accrual lines (ACCFLAG = "Y") post a second time into the accrual period.
A/P vouchers — VOUCHUPD
Called from VOUCHREG. Job comes from POHIST attr 29 or READV JOBNO FROM OPENWO,CO:"!":WONO,1. Writes
WIPLEDGER.AP/.SUM and JOBLEDGER.AP/.SUM.
Outside operations — CA1768 via VB_CA1768
Outside-operation receipts write JOBLEDGER.OUT and JOBLEDGER.SUM directly.
Manual maintenance — CA1794 / CA1795
CA1795 (“Job Ledger Standard Costs Entry and Maintenance”) writes JOBLEDGER.SUM, .AP, .LAB,
.MAT, .OUT, .HRS keyed straight by job; CA1794 is the WIP-level equivalent. Per the vendor narrative
these maintain the standard columns by fiscal period and cost class — not actuals. Since PSI leaves the
standard columns empty, these screens are effectively unused here.
The paths with no work order
Purchased inventory. A job-pegged lot lands in JOBLEDGER.INV via I.1656, keyed CO!JOBNO2 where
JOBNO2 comes off the inventory transaction itself (INVHIST attr 10, value 7 for types 11/12/15/24/31/37,
else value 5). No work order is involved. JOBLEDGER.INV is also rebuildable from LOTHIST/LOTCOST by
RESET.JL.INV.
Direct invoices. Coded to the job (through its PROJ WO) and posted by GL7013C / VOUCHUPD into
JOBLEDGER.AP. Job 2412 carries a recurring MISC charge of $1,500–$2,400 per period this way.
JOBLEDGER.SUM F14 (INV.ACT.TOT) — the job-pegged-inventory term — is one of the five terms in incurred
job cost. A purchased part hits job cost at receipt against a job-pegged lot, before any WO consumption.
The dual-posting identity, verified against production
Dual posting makes a falsifiable claim: a job’s ledger must equal the sum of its work orders’ ledgers, exactly. Tested 2026-09-02 against live MRP-PROD — it holds to the cent.
For every open job in the JOB.IP.1287 snapshot of 2026-09-01, the four incremental actual columns on
JOBLEDGER.SUM were compared against the same four summed across every WO carrying that job number
(joined via WIPLEDGER.SUM’s own JOBNO I-descriptor, which resolves through OPENWO):
| Open jobs compared | 184 |
| Work orders behind them | 22,241 |
Job ledger — SUM<3,6,9,12> | $35,133,085.46 |
| Σ work-order ledgers — same four | $35,133,085.46 |
| Jobs tying to the cent | 184 of 184 |
| Breaks over $0.01 | 0 |
Job-pegged inventory SUM<14> (no WO counterpart) | $1,719,883.68 |
Identical on a job carrying 862 work orders as surely as on one carrying three. This is what “posted twice, in one pass” buys — the levels cannot drift, because nothing derives one from the other. Job-pegged inventory sits outside the identity by design (4.7% of incurred across these jobs, though far higher on any job mid-procurement).
Two rules, or you manufacture a break that isn’t there
Compare the incremental columns, never the cumulative ones. On WIPLEDGER.SUM, attributes 3/6/9/12 are
*.ACT.INCR — that WO’s own cost — and 13–20 are the cumulative twins (AP.ACT.CUMM 14,
LAB.ACT.CUMM 16, MAT.ACT.CUMM 18, OUT.ACT.CUMM 20), carrying cost rolled up from lower-level work
orders. That is I.1656’s own rule: “UPDATE LEDGREC AND SUMREC (WIP INCR, JOB CUMM).” Summing cumulative
across a job double-counts every sub-assembly.
Attribute 14 means different things in the two files. On WIPLEDGER.SUM it is AP.ACT.CUMM;
on JOBLEDGER.SUM it is INV.ACT.TOT, job-pegged inventory — PSI reused the position the vendor’s job-side
code zeroes. Confirmed from both live dictionaries. Compare them positionally and you are silently
differencing cumulative A/P against job inventory.
Both rules are exactly what ACT.CA1778 encodes, which is unsurprising: rebuilding the job ledger from the
work orders is what that program was written to do.
The roll-up programs are repair tools, not the posting path
| Program | What it does | Called from |
|---|---|---|
CA1778 | Clears and rebuilds the standard columns of the job ledger from WIPLEDGER.SUM | CA1778P / CA1779P — interactive menu (CAOP) only |
ACT.CA1778 | Same for the actual columns | Nothing. Zero callers in the codebase. |
REBUILD.LEDGER.MAT, REBUILD.LEDGER.OUT, FIX_JOBLEDGER.*, RESET.JL.INV, FIXJOBHRS | Targeted rebuilds | Manual |
Both roll-ups do a full destructive rebuild — they blank their target attributes and re-add from every WO
carrying the job number. Running ACT.CA1778 would overwrite the posted actuals with a WO-derived figure,
which is precisely why it isn’t scheduled: it would erase exactly the differences that need investigating.
The original AFTEC documentation confirms this was never the actual-cost path. The vendor’s own program
narratives (S:\Dept\Computer\Documents\PTIS2\AFTEC\CABASE.wpd):
CA1775 — Work-in-Process Financial Release. “Sets the financial status flag on the OPENWO file to 1, and generates standard costs on the WIPBMFCOST and WIPROUTE.COST files.”
CA1777 — WIP Ledger Standard Cost Roll-Up. “Takes the standard costs and hours generated by the financial release program (CA1775) and rolls them up into the various WIPLEDGER files.”
CA1778 — Job Ledger Cost Roll-Up. “Takes the standard costs and hours on the various WIPLEDGER files and rolls them up into the corresponding JOBLEDGER files.”
CA1779 — “a combination of the financial release, WIPLEDGER roll-up, and JOBLEDGER roll-up programs (CA1775, CA1777, CA1778).”
So the whole roll-up chain is the standard-cost side of the design, gated on financial release. PSI never
adopted it — WIP.RELIEF holds 0 records and the standard columns are empty on live jobs — which is exactly
why actuals have always been posted directly and why the chain sits dormant.
Neither touches JOBLEDGER.SUM attrs 13/14 (CA.CLASS.INV / INV.ACT.TOT) or JOBLEDGER.XFER.
The nightly — END.OF.SHIFT
PBSBASE/END.OF.SHIFT (JPT, 2015, maintained through 2024) is the driver. It runs more than once a day; the
7 PM (TIME() > 68400) pass does the financial work. It notifies JPT,AMD on start and completion and
JPT,AMD,BMC on error.
What schedules it — and the silent failure that stops it.
END.OF.SHIFTis not in the recurring batch queue. It is submitted fresh every afternoon byMRP.DAILY(16:45), which defers one run to 20:00 — the pass that clears the 19:00 gate and does the financial block — and another to 08:00 the next morning, which does not.MRP.DAILYexits with a bareRETURNif theMRPBATCHLOCKFILErecord is still held, so a stale lock silently cancels the entire nightly close for that day, with no message and no email. A missing date inJOB.IP.1287is the cheapest detector. See The AFTEC Batch Scheduler.
Guard locks — it aborts and emails if any of these are held: WORK1768 (outside-ops receipt),
WORK0161 (PO receipt), WORK1745 (labor update), WORKPBS1803.1287 (inventory valuation),
LOCKFILE "1656VAR1" (INVAUDIT), LOCKFILE "END.OF.SHIFT", LOCKFILE "AUTO.TS.UPD", and after 7 PM
JOURNAL.HEAD "LOCK" (GL7013, unless held by root).
Every pass:
VB_PO0161— PO receipts →VB_I.1656VB_CA1768— outside-operation receipts →VB_I.1656AUTO.TS.UPD— timesheets →VB_I.1656VB_WO.VERIFY.REV1+VB_W.7285— WIP receipts for production WOs →VB_I.1656AUTO.WIP.CLOSE— WIP closeRPT.WELDCONT— weld continuity
Evening pass adds:
UPD.JOB.INPROCESS(with@USER1 = "JOB.IP") — the job-ledger side ofJOB.IP.1287VB_CA1803→PBS1803— inventory valuation by G/L by job by part; writes the inventory side ofJOB.IP.1287(attrs 3/4) when@USER1 = "JOB.IP"VB_CA1832— open work order reportVB_JOB.COST.REV1— the job cost / WIP cost report behind SO Manager’s Job Cost tabVB_POHIST.REV1+UCAP.WO.JOB2— unconfirmed A/PVB_UPD.WIPCOST→WIPCOST.1287, plus two exception reports:WITH COST.DIFF # 0 WITH WIP.GLNO = '1235-000'and closed-WOCOSTvsFGCOSTVB_GL7013— G/L updateVB_GL7023— closing trial balance; thenGL.BAL.1287totallingGLSL.DIFF(G/L vs subledger)SEND.NOTIFICATIONS
An undeployed change sits in
END.OF.SHIFT. ThePBSBASEsource (and its object, both 2025-02-24) has a bareSTOPimmediately after step 2, which would strand steps 3–15. It is not running — the cataloged copy is the 2024-09-11 build, which predates the edit, and that is what executes. Confirmed by behaviour:JOB.IP.1287is written every day and that only happens at step 7.So there is a February 2025 change to the nightly that was written, compiled, and never re-cataloged. If anyone ever recompiles and catalogs
END.OF.SHIFTfrom current source, timesheets, WIP receipts, WIP close and the entire evening financial block stop running. Worth resolving deliberately rather than discovering it later.
Why Cost Differ exists
SO Manager’s Job Cost tab shows three columns that come straight from JOB.IP.1287:
| Column | Field | What it is |
|---|---|---|
| Job Summary Cost | JOBCOST.TTL = JOBCOST.INCR + JOBCOST.XFER | The job ledger |
| Job In Process Cost | JOBWIP.TTL = SUM(JOB.WIP) + SUM(JOB.INV) | The general ledger — WIP 1235-000 plus inventory accounts |
| Cost Differ | COST.DIFF = JOBCOST.TTL − JOBWIP.TTL | The break |
JOBCOST.INCR is assembled by UPD.JOB.INPROCESS straight out of JOBLEDGER.SUM:
JOB.TTL = SUM(JOBLEDGER.SUM<3>) ← A/P actual
+ SUM(JOBLEDGER.SUM<6>) ← labor actual
+ SUM(JOBLEDGER.SUM<9>) ← material actual
+ SUM(JOBLEDGER.SUM<12>) ← outside actual
+ SUM(JOBLEDGER.SUM<14>) ← job-pegged inventory actual
JOB.WIP / JOB.INV come from a different program on a different basis — PBS1803, the inventory
valuation, walking LOTHIST by G/L account. Two independent accumulations of the same money. Every
orange cell in SO Manager is a place they disagree.
The JOB.IP.1287 record, and its two storage scales
Key is CO!JOBNO!date — one row per open job per day. Two programs write it, and they do not agree on
decimal scaling:
| Attr | Written by | Holds | Scale |
|---|---|---|---|
| 1 / 2 | PBS1803 | WIP G/L account / amount pairs (1235-000, 1395-000, 2719-000, other) | MD2 |
| 3 / 4 | PBS1803 | Inventory G/L account / amount pairs (1200-000, 1210-000, 1220-000, other) | MD4 |
| 5 / 6 | UPD.JOB.INPROCESS | Open PO commitment / unconfirmed A/P | MD2 |
| 7 / 8 | UPD.JOB.INPROCESS | JOB.TTL (incurred) / JOB.XFR (transfers) | MD4 |
| 9 / 10 / 11 | UPD.JOB.INPROCESS | Open A/R, open SO, commission | MD2 |
| 12 / 13 / 14 | UPD.JOB.INPROCESS | Sell price, invoice price, PO error | MD2 |
VB_JOB.COST.REV1 sums the two halves in one expression —
JOB.WIP.COST = OCONV(<2>…,"MD2") + OCONV(<4>…,"MD4") — which is correct, because each side is read
with the conversion its writer used. Verified against job 2412:
151,263.87 + 151,213.61 + 143,747.13 = 446,224.61, exact.
But nothing asserts that invariant. If either writer’s scaling ever changes, the sum silently moves by 100×
and the $5 Cost Differ threshold flags the entire fleet. Anyone reading JOB.IP.1287 directly — a new API
endpoint, an extract, a BC migration script — must apply the per-attribute scale above, not one blanket
conversion.
No drill-down
The Job Cost tab is display-only. The sole click target is Job No, which broadcasts the project number on
PSI’s inter-app bus so other open PSI apps retarget — no window opens and no second query runs.
WIPCOST.1287, the WO-level twin, has zero references anywhere in PSI.All. So when a job flags orange
there is no supported path from the number to the work order that caused it.
Live, job 2412 on 2026-08-06:
G/L WIP 1235-000 | $151,263.87 |
G/L inventory 1200-000 | $151,213.61 |
G/L inventory 1210-000 | $143,747.13 |
| Job In Process Cost | $446,224.61 |
| Job ledger incurred | $447,048.14 |
| Job ledger transfers | −$557.69 |
| Job Summary Cost | $446,490.44 |
| Cost Differ | $265.83 |
Note that two-thirds of this job’s balance-sheet position — $294,960.74 of $446,224.61 — is sitting in
inventory accounts, not WIP. On the ledger side, INV.ACT.TOT alone is $151,213.61, 34% of incurred
job cost, for material received against the job and not yet issued to any work order.
Known causes of a break
Receiving a work order at quantity 0. (John Tymes) When weld discovers a mistake, the correct sequence is receive the WO, then scrap it. Receiving at qty 0 instead — which auto-creates a replacement WO the next day, so it looks like the easy path — drives the WIP cost to zero while the job ledger keeps the labor and material already posted to that WO. The value of the diff is exactly the labor charged and material issued to the first WO.
History. These were held to $0.01 per project when Brian was here. The accumulated error is also what produced the $1.5M discrepancy PSI had to reconcile and pay taxes on. Cindy has the detail.
WIPCOST.1287 is the WO-level twin — a daily snapshot per open work order (7,003 rows on 2026-08-05), keyed
CO!WONO!date, with AP.DIFF / LAB.DIFF / MAT.DIFF / OS.DIFF / COST.DIFF I-descriptors that
self-difference against the prior business day. The nightly prints the non-zero ones.
Overhead
Labor and outside overhead — the COCE rate card
Live as of 2026-08-06. COCE attr 2 (COST.DEPT) is the roll-up key; the file also carries the G/L accounts
each element posts to (REG.GLNO, OT.PREM.GLNO, FIX.GLNO, VAR.GLNO, OUT.GLNO, …).
| Cost centre | Description | Std rate $/hr | Fixed OH % | Outside OH % |
|---|---|---|---|---|
| 100 | Maintenance | — | 99.460 | |
| 101 | General | 63.21 | 99.460 | |
| 102 | Machining | 52.43 | 106.990 | |
| 104 | Welding | 52.43 | 106.990 | |
| 106 | Mech. Assembly | 60.17 | 83.930 | |
| 108 | Elec. Assembly | 59.42 | 93.070 | |
| 110 | Engineering | 54.05 | 43.380 | |
| 110E | Controls Engineering | 52.83 | 38.930 | |
| 110M | Mechanical Engineering | 54.05 | 43.380 | |
| 111 | Controls Engineering | 52.83 | 38.930 | |
| 120 | Machine Service | 73.77 | 43.070 | |
| 122 | Test & Runoff | 73.77 | 43.070 | |
| 125 | Process Services | 35.27 | 107.030 | |
| 1000 | Outside Processing | — | — | 5.810 |
| 115, 121, 130, 131, 135, 136, 140, 150, 160, 170, 180 | Proposal Eng, Temporary, Process Dev, Temp R&D, Spare Parts Sales, Sales, Purchasing, Mtl Handling/Ship, Computer Ops, Acct/Admin | — | 0.000 |
Variable overhead is 0.000 on every cost centre — PSI applies fixed overhead only. The alternative
hourly-rate columns (FIX.RATE / VAR.RATE) are empty.
On the BC dimension question: these are AFTEC cost centres, and they are not the same list as PSI’s canonical department codes — note
110E/110Msplitting engineering, and111duplicating110E’s description and rates. Anything that maps these onto BC department dimensions needs that reconciled first, because the burden rates are attached to the cost centre, not the department.
Material overhead
Applied twice and tracked separately: at purchase receipt (MOVPCTIN → *.OVH.PO) and at issue to a work
order (MOVPCTOUT → *.OVH.ISS).
SG&A — SGA.UPD
A percentage per fiscal period over material + A/P + outside + inventory + transfers + labor. Rate from
CO.STDS.COST.1287 attrs 1/2, overridden per job by JOBMASTER.1287 attrs 2/3. Called from PROJ.CST.RPT /
PROJ.CST.INQ — computed at report time, not posted.
Cost transfers on and off a job
JOBLEDGER.XFER mirrors the cost-stream layout by fiscal period, signed. I.1656 sets the flag in two cases:
CASE JOBNO = "" ; XFERFLAG = 0
CASE TYPE = "29" ; XFERFLAG = 1 ← explicit job-to-job transfer
CASE JOBNO2 = "" AND NETQTY < 0 ; XFERFLAG = 1 ← issue from common (non-job) stock into a job
CASE 1 ; XFERFLAG = 0
Job 2412: −$39.07 material / −$340.62 labor / −$822.99 outside in 2607; −$61.79 / −$335.83 / −$29.10 in 2606; +$3.20 / +$80.12 / $0 in 2605.
WIP close and estimates to complete
WIP.CLOSE (key CO!WONO) records the close date and a REP flag — “representative of last cost?
determines if last cost is updated” — which decides whether that WO’s actual cost may overwrite the part’s
LAST cost. 2,403 WOs closed in the 30 days to 2026-08-06. AUTO.WIP.CLOSE runs it; history lands in
WIPCLOSEHIST.
The stock AFTEC “WIP financial release / relief” cycle (CA1775A, CA1792, CA1831A, CA1793) is dormant —
WIP.RELIEF holds 0 records.
Percent complete is entered, not derived: WO.PCT.COMP / JOB.PCT.COMP hold separate MAT / LAB / OUT /
AP percentages maintained through CA1790 / CA1791, rolled by CA1812; CA1774A rolls open-PO dollars
committed. There is no earned-value calculation.
What’s live vs dormant
| Capability | Status |
|---|---|
| Dual posting of actuals to WIPLEDGER + JOBLEDGER | Live — this is the mechanism |
| Standard cost columns | Unused — JOBLEDGER.MAT 3/4/9 and .LAB 3–5 are empty on live jobs |
ACT.CA1778 actual roll-up | Never called |
CA1778 standard roll-up | Menu-only repair tool |
| Fixed overhead | Live — 38.9%–107.0% of labor by cost centre |
| Variable overhead | Unused — 0.000 everywhere |
| Material overhead (receipt + issue) | Live |
| Outside overhead | Live — 5.81% |
| SG&A | Live, computed at report time |
| Job cost transfers | Live |
| WIP financial relief | Dead — 0 records |
| WIP close | Live — ~2,400/month |
JOB.IP.1287 daily job position | Live, PSI-built, daily including weekends |
| Month-end job cost pack | MONTHEND123 — “job ledger cost/hours transfer to Lotus 123”, written 1993, still FTPs a flat file |
What this means for the Business Central migration
Cost attaches at receipt into job-pegged stock, not at consumption. JOBLEDGER.SUM F14 is one of the five
terms in incurred job cost — 34% of job 2412’s. Purchased inventory reaches the job with no work order.
BC’s project WIP recognises cost when material is consumed onto a job. This is the biggest semantic gap.
One posting event, two levels. In AFTEC, GL7013C / I.1656 / CA1745.CA update the WO ledger and the
job ledger in the same pass, so manufacturing WIP and project WIP cannot diverge by design — only through
data errors like the 0-qty receipt. BC has two WIP engines with no crossover between them. Any BC design has
to bridge production-order WIP into project WIP explicitly, and do it atomically, or PSI inherits a
structural version of the problem they currently only get from operator error.
The job ledger and the G/L are reconciled daily, and the reconciliation is load-bearing. COST.DIFF is
watched, and letting it drift once cost PSI a $1.5M reconciliation. Whatever replaces this needs an
equivalent daily control with a visible variance — not a month-end close.
The PROJ work order has no BC equivalent and shouldn’t get one. It exists because AFTEC’s posting
programs need a WO to find the job. In BC, direct costs post to the job directly. Anyone migrating
JOBLEDGER history needs to know that PROJ-WO cost is job-level overhead, not manufacturing cost.
Non-negotiables for a replacement design
- Cost attaches at PO receipt into job-pegged stock, not at consumption.
- Open PO commitment and unconfirmed A/P visible against the job without being cost.
- Cost broken five ways — material, labor, outside, A/P/ODC, job inventory — plus overhead pools, at cost-centre and CA-class grain, by fiscal period.
- Signed cost transfers on/off a job as a first-class concept.
- A daily ledger-to-G/L reconciliation with a visible variance.
- Hours tracked independently of dollars — the shop manages to hours, finance to dollars, off the same postings.
Querying it
$t = az account get-access-token --resource "api://b3db69d9-5d15-457d-b660-88b336fc00fa" --query accessToken -o tsv
$h = @{ Authorization = "Bearer $t" }
# Full cost ledger for one job
Invoke-RestMethod -Uri "https://api.progressivesurface.com/api/data/JOBLEDGER.SUM/1!2412" -Headers $h
# The SO Manager Job Cost tab, as data
$b = @{ command = 'SORT JOB.IP.1287 WITH R.DATE = "08/06/26" AND WITH JOB.NO # "NULL" BY.DSND JOBCOST.TTL
JOB.NO JOBCOST.TTL JOBWIP.TTL COST.DIFF JOB.PO JOB.UCAP JOB.PRICE JOB.INVOICE FIN.STAT'
format = 'raw' } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.progressivesurface.com/api/query" -Headers $h -Method Post -Body $b `
-ContentType 'application/json'- Money is
MD4in the ledger files,MD2inJOB.IP.1287. /api/data/{table}returnsrawAttributeswithý(VM) andü(SVM) intact — you need both to read the period × class matrix.LIST … TOXML(format: json) does not parse on these files; useformat: raw.
Source locations
| What | Where |
|---|---|
| Live PSI logic | \\ad.ptihome.com\DFS\Data\LinuxShare\pro3prog\PBSBASE\ (~948 of 1,885 items unencrypted) and VBBASE\ |
| Vendor source archive — does not execute | …\pro3prog\AFTEC\ |
| Cost accounting module (mostly encrypted) | …\pro3prog\CABASE\ — the _NAME companion object carries a readable literal / file-open table |
| Driver PROCs | …\pro3prog\CAPR\ — plain text |
| Batch paragraphs | …\pro3prog\PBS.PARA\ |
| Menu tree | …\pro3prog\MENUFILE |
| Original AFTEC vendor documentation | S:\Dept\Computer\Documents\PTIS2\AFTEC\ — per-module WordPerfect manuals with screen layouts, field descriptions and program narratives. CABASE.wpd (710 KB) is cost accounting; also AftecPro3Master1–3.pdf. Convert with soffice --headless --convert-to txt:Text. |
Menu paths: CA → Cost Accounting; CAOP → operations; CAOP.WIP → standards release/update;
CAREQ.LEDGER → job and WIP ledger cost reports; JCL → Job Cost Ledger; PB45xx → project budgeting and
financial control.