Skip to main content

Inventory, costing and payroll

These two families are where a manufacturing book is either right or quietly wrong, because both turn a stream of small events into a unit cost that everything downstream believes.

The costing chain runs one way

RECEIPT → ISSUE → PRODUCTION_IN → SHIP, and every movement’s unit cost is stamped immutably on the movement at post time. Because a movement’s cost is frozen when it posts, a later price change cannot retroactively move both the WIP-in and the WIP-out leg. That tautology is exactly what the source workbook could not avoid - its stock-movement cost column was a live formula - and it is why the equivalent check there had no teeth.
Stock on hand, the weighted-average unit cost, the WIP balance, the GRNI balance and the 627 pool are all derived from movements plus the GL. None of them is a stored total that can drift from the rows it summarises.

The 627 overhead pool must equal what was absorbed

The pool is sourced from real expenses and absorbed into WIP, and the two sides are checked against each other. Depreciation lines are recorded with source_type: ASSET_REGISTER, so the pool can be checked against an independent re-derivation from each asset’s cost and life rather than against itself. A pool that does not equal the overhead absorbed into 154 is a Close-list break.

An inventory adjustment always carries its stock movement

A signed adjustment on a stock account posts its GL legs at the positive magnitude and a matching stock movement with the signed quantity and value. An adjustment to a stock account with no stock movement behind it is a break, by design: a value change with no quantity story is how inventory quietly stops matching the warehouse.

Payroll: one person, one bracket, three bills

Employment terms are a time series. The payroll for a period reads the contract in effect as of that period, exactly as FX reads the rate as of the transaction date - the applicable term, not the latest one. Two identities hold by construction and are checked:
  • employer_cost = gross + employer social insurance
  • net = gross - employee social insurance - PIT
Each person’s cost is allocated to one cost bracket. A production-direct person’s labour therefore flows into 154, which is what ties the payroll sub-ledger to the costing sub-ledger - the same number, reached two ways.
Reversing a 13th-month accrual for someone who left is real information, not an error to hide. A negative allocation keeps the reconciliation tied, which is why the reversal is a first-class operation rather than a deletion.
The three payables the accrual creates - net pay, PIT, and social insurance - become obligations that settle on rails like any other bill, which is what ties payroll to the settlement sub-ledger. PIT carries its own conservation checksum: computed == paid + carried.
This sub-ledger records and enforces; it does not re-derive Vietnamese PIT brackets. The gross, the deductions, the PIT and the employer contributions are the payroll engine’s computed figures. What GreatBook guarantees is that they post as a balanced accrual, that the two identities hold, and that the allocation reconciles - not that the bracket arithmetic upstream was right.

Where it is enforced

The test that would fail if it broke

backend/tests/test_costing.py drives the chain end to end and asserts a stamped cost does not move. backend/tests/test_payroll.py covers the identities, the allocation reconciliation and the PIT conservation.
The payroll Close family runs only when a period is supplied, and its bill-level checksums appear only once that period’s payroll bills are raised. That is another reason a live run’s executed count differs from the 104 defined checksums.