Skip to main content

Payroll

Sub-ledger 3, migration 0005.

The rule that shapes everything else

Employment terms are a time series. The payroll for period P reads the contract in effect as of P:
This is exactly the FX rate-as-of-date rule applied to a different domain: the applicable term, not the latest term. contract_asof raises when no contract covers the date, rather than falling back to the most recent one. A payroll that reads the current contract to compute a past period restates history every time someone gets a raise.

Employer cost, and where it lands

Every person’s employer cost is allocated to exactly one cost bracket, and the bracket decides which GL account it lands on. The first row is the cross-family tie. A production-direct person’s labour is a production cost line, which is exactly the labour the WIP balance consumes.

What becomes payable

The accrual splits into three payables that then settle like any other bill, on rails: Plus two provisions: 2050 (VAS 335) for the 13th-month accrual and 2250 (VAS 352) for leave. Payroll bill aggregation carries the PIT payable; it does not pay it. Only the typed PIT-remittance operation clears 2220 and cash, and net pay, social insurance and PIT remittance each create canonical rail evidence linked to the exact journal entry. Full and partial remittances retain the unpaid carried balance. That separation prevents one aggregation step from paying PIT and a later remittance paying the same tax again.
What this sub-ledger does not do. It does not re-derive Vietnamese PIT brackets. Gross, deductions, PIT and employer contributions are the payroll engine’s computed figures; this sub-ledger records them, posts the balanced accrual, and enforces the identities. That boundary is deliberate: a second implementation of a tax table is a second thing to keep current, and the identities catch a wrong figure regardless of which engine produced it.

The identities it enforces

The checks are the value here, because they are what makes a recorded figure verifiable. The PIT, net and obligation ties are conditional: they compare the payroll to what was billed, so they only run once the period’s bills exist. Before that, unremitted net and PIT are a payable, not a discrepancy, and reporting them as a break would be a false alarm every month until billing day. That conditionality is a design choice worth naming. A check that fires spuriously for two weeks of every month is a check people learn to ignore.

Where it is enforced

The test that would fail if it broke

backend/tests/test_payroll.py covers the operations and the identities, including partial/full PIT remittance and rail evidence, and ties to the v12 worked figures: net 61,135,250, PIT 2,297,932, social insurance 16,960,000, total employer cost 80,393,182, with 29,447,727 of production labour flowing into 154.

What goes wrong without it

Payroll is the largest recurring cash outflow in most businesses and the one with the most statutory exposure. The failure without the identities is not a wrong total; it is a total that nobody can decompose. When the tax authority asks why declared PIT differs from the payroll register by 40,000 dong, “the payroll system said so” is not an answer. The failure without the contract time series is quieter and worse. A retroactive raise restates every prior period’s labour cost, which restates WIP, which restates COGS, which restates margin, in months that were already reported.