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:

ResultCountMeaning
Exact byte match to a source object2,877Provenance proven
Same name, different bytes91Recompiled after cataloging — the source on disk is not what runs
No matching source item in pro3prog350Source 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

NameResolves to
1656PBSBASE/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.CLOSEPBSBASE/<same name>
VB_I.1656, VB_I.1656B, VB_CA1768, VB_GL7013VBBASE/<same name>
END.OF.SHIFT, PBS1803PBSBASEbut the catalog copy is older than the source
VB_W.7285, VB_JOB.COST.REV1VBBASEbut 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/CTLG and every _* object under pro3prog, 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 from WIPLEDGER, the cost differences would not exist. Verified — ACT.CA1778 has no callers anywhere in the codebase, and CA1778 is only reachable from an interactive menu. Both are repair tools.


The ledger file pair

SuffixWO levelJob levelHolds
.MATWIPLEDGER.MATJOBLEDGER.MATMaterial cost + material overhead, by CA class
.LABWIPLEDGER.LABJOBLEDGER.LABLabor, fixed OH, variable OH, rework, indirect — by cost dept
.OUTWIPLEDGER.OUTJOBLEDGER.OUTOutside processing + outside overhead
.APWIPLEDGER.APJOBLEDGER.APDirect A/P charges (ODC), by transaction type
.HRSWIPLEDGER.HRSJOBLEDGER.HRSLabor hours by cost centre ! grade
.CONWIPLEDGER.CONJOBLEDGER.CONConsignment value issued
.STRWIPLEDGER.STRJOBLEDGER.STR“Cost stream” — flat element totals per period
.SUMWIPLEDGER.SUMJOBLEDGER.SUMRolled totals — the record everything else reads
.INV(n/a)JOBLEDGER.INVJob-pegged inventory not issued to any WO
.XFER(n/a)JOBLEDGER.XFERCost transferred on/off the job
.SGA(n/a)JOBLEDGER.SGASG&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:

AttrNameMeaning
1FPERFiscal periods (YYPP), newest first
2CLASSCost accounting class, SVM’d within each period
3 / 5STD.INCR.MAT / ACT.INCR.MATStandard / actual incremental material
4 / 6STD.INCR.OVH.PO / ACT.INCR.OVH.POMaterial OH applied at purchase receipt
9 / 10STD.INCR.OVH.ISS / ACT.INCR.OVH.ISSMaterial OH applied at issue
7 / 8QTY.COMP / QTY.SCRAPQuantity 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.

TypeEventEffect
0104Receipt into inventoryMaterial OH-in applied (MOVPCTIN); lot cost stamped on LOTHIST
05,11,13,24,31,37Issue out of inventoryMaterial OH-out applied (MOVPCTOUT)
23, 47ScrapWrites SCRAPREG, reduces WO material
30WIP receipt / completionPosts the cost stream (*.STR) at both levels
29Job transferSets XFERFLAGJOBLEDGER.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.1656BVB_I.1656CVB_I.1656GL16561656D.

Labor — CA1745.CA

Timesheets (TS.1287LABOR.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 compared184
Work orders behind them22,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 cent184 of 184
Breaks over $0.010
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

ProgramWhat it doesCalled from
CA1778Clears and rebuilds the standard columns of the job ledger from WIPLEDGER.SUMCA1778P / CA1779P — interactive menu (CAOP) only
ACT.CA1778Same for the actual columnsNothing. Zero callers in the codebase.
REBUILD.LEDGER.MAT, REBUILD.LEDGER.OUT, FIX_JOBLEDGER.*, RESET.JL.INV, FIXJOBHRSTargeted rebuildsManual

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.SHIFT is not in the recurring batch queue. It is submitted fresh every afternoon by MRP.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.DAILY exits with a bare RETURN if the MRPBATCH LOCKFILE record 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 in JOB.IP.1287 is 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:

  1. VB_PO0161 — PO receipts → VB_I.1656
  2. VB_CA1768 — outside-operation receipts → VB_I.1656
  3. AUTO.TS.UPD — timesheets → VB_I.1656
  4. VB_WO.VERIFY.REV1 + VB_W.7285 — WIP receipts for production WOs → VB_I.1656
  5. AUTO.WIP.CLOSE — WIP close
  6. RPT.WELDCONT — weld continuity

Evening pass adds:

  1. UPD.JOB.INPROCESS (with @USER1 = "JOB.IP") — the job-ledger side of JOB.IP.1287
  2. VB_CA1803PBS1803 — inventory valuation by G/L by job by part; writes the inventory side of JOB.IP.1287 (attrs 3/4) when @USER1 = "JOB.IP"
  3. VB_CA1832 — open work order report
  4. VB_JOB.COST.REV1the job cost / WIP cost report behind SO Manager’s Job Cost tab
  5. VB_POHIST.REV1 + UCAP.WO.JOB2 — unconfirmed A/P
  6. VB_UPD.WIPCOSTWIPCOST.1287, plus two exception reports: WITH COST.DIFF # 0 WITH WIP.GLNO = '1235-000' and closed-WO COST vs FGCOST
  7. VB_GL7013 — G/L update
  8. VB_GL7023 — closing trial balance; then GL.BAL.1287 totalling GLSL.DIFF (G/L vs subledger)
  9. SEND.NOTIFICATIONS

An undeployed change sits in END.OF.SHIFT. The PBSBASE source (and its object, both 2025-02-24) has a bare STOP immediately 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.1287 is 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.SHIFT from 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:

ColumnFieldWhat it is
Job Summary CostJOBCOST.TTL = JOBCOST.INCR + JOBCOST.XFERThe job ledger
Job In Process CostJOBWIP.TTL = SUM(JOB.WIP) + SUM(JOB.INV)The general ledger — WIP 1235-000 plus inventory accounts
Cost DifferCOST.DIFF = JOBCOST.TTLJOBWIP.TTLThe 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:

AttrWritten byHoldsScale
1 / 2PBS1803WIP G/L account / amount pairs (1235-000, 1395-000, 2719-000, other)MD2
3 / 4PBS1803Inventory G/L account / amount pairs (1200-000, 1210-000, 1220-000, other)MD4
5 / 6UPD.JOB.INPROCESSOpen PO commitment / unconfirmed A/PMD2
7 / 8UPD.JOB.INPROCESSJOB.TTL (incurred) / JOB.XFR (transfers)MD4
9 / 10 / 11UPD.JOB.INPROCESSOpen A/R, open SO, commissionMD2
12 / 13 / 14UPD.JOB.INPROCESSSell price, invoice price, PO errorMD2

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 centreDescriptionStd rate $/hrFixed OH %Outside OH %
100Maintenance99.460
101General63.2199.460
102Machining52.43106.990
104Welding52.43106.990
106Mech. Assembly60.1783.930
108Elec. Assembly59.4293.070
110Engineering54.0543.380
110EControls Engineering52.8338.930
110MMechanical Engineering54.0543.380
111Controls Engineering52.8338.930
120Machine Service73.7743.070
122Test & Runoff73.7743.070
125Process Services35.27107.030
1000Outside Processing5.810
115, 121, 130, 131, 135, 136, 140, 150, 160, 170, 180Proposal Eng, Temporary, Process Dev, Temp R&D, Spare Parts Sales, Sales, Purchasing, Mtl Handling/Ship, Computer Ops, Acct/Admin0.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 / 110M splitting engineering, and 111 duplicating 110E’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

CapabilityStatus
Dual posting of actuals to WIPLEDGER + JOBLEDGERLive — this is the mechanism
Standard cost columnsUnusedJOBLEDGER.MAT 3/4/9 and .LAB 3–5 are empty on live jobs
ACT.CA1778 actual roll-upNever called
CA1778 standard roll-upMenu-only repair tool
Fixed overheadLive — 38.9%–107.0% of labor by cost centre
Variable overheadUnused — 0.000 everywhere
Material overhead (receipt + issue)Live
Outside overheadLive — 5.81%
SG&ALive, computed at report time
Job cost transfersLive
WIP financial reliefDead — 0 records
WIP closeLive — ~2,400/month
JOB.IP.1287 daily job positionLive, PSI-built, daily including weekends
Month-end job cost packMONTHEND123“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

  1. Cost attaches at PO receipt into job-pegged stock, not at consumption.
  2. Open PO commitment and unconfirmed A/P visible against the job without being cost.
  3. 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.
  4. Signed cost transfers on/off a job as a first-class concept.
  5. A daily ledger-to-G/L reconciliation with a visible variance.
  6. 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 MD4 in the ledger files, MD2 in JOB.IP.1287.
  • /api/data/{table} returns rawAttributes with ý (VM) and ü (SVM) intact — you need both to read the period × class matrix.
  • LIST … TOXML (format: json) does not parse on these files; use format: raw.

Source locations

WhatWhere
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 documentationS:\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.