Skip to main content

The dual IFRS + TT200 chart

What it guarantees

The ledger posts to one chart of accounts, designed on IFRS lines. Vietnam’s Circular 200 (Thông tư 200, “TT200”) codes are carried as a mapping, not as a second set of accounts. The consequence that matters: re-mapping a VAS code later is metadata, not a ledger correction. No posted entry is touched.

The two layers

The IFRS core lives in accounts. Sixty seeded accounts: 16 asset, 15 liability, 5 equity, 6 revenue, 7 cost of sales, 11 operating expense. Codes are structured 1xxx assets, 2xxx liabilities, 3xxx equity, 4xxx revenue, 5xxx cost of sales, 6xxx operating expense. The statutory projection lives in account_tt200_map. One IFRS account maps to zero or more TT200 codes. It is freely editable and never touches a posted entry.

A sample of the mapping

Two shapes worth noticing. 2210 maps to three VAS codes, because Vietnamese social insurance splits into distinct statutory accounts that a single management account covers. 6300 maps to two, because depreciation is presented differently depending on whether it is production or administrative.

Management-only accounts have no VAS twin

Four accounts map to nothing, on purpose: An empty mapping is a deliberate statement, and the statutory report reports accounts that have activity and no mapping, because those would otherwise vanish silently from the return.

The suspense accounts are reserved for humans

4900 and 6900 exist, and no agent may use them. The legacy poster this replaced did BILL_KIND_ACCOUNT.get(kind, "6900"), quietly booking anything it could not code into suspense. The agent posting rules raise UnmappedAccountError instead, and the accountant’s adjustment rules refuse a suspense account even when a caller names one explicitly. Suspense is for an explicit, human-chosen coding decision. It is not a default, because a default suspense account is how a book accumulates a growing pile of “we will look at that later” that nobody ever does.

Detail is dimensions, not sub-accounts

There is no 2000-SUPPLIER-A. Per-party, per-order and per-activity detail is carried as dimensions on the line. That is why the chart is around sixty accounts and stays that size as the business scales to thousands of counterparties.

Where it is enforced

The seed asserts at import that no account code is duplicated and that every type is one of the six known types. It is find-or-create by (org_id, code) and never overwrites an existing account, so re-seeding an org is safe.

The test that would fail if it broke

backend/tests/test_coa_seed.py covers the seed’s idempotency and the mapping rows. langgraph_chat/agents/accountant/tests/ covers the TT200 projection, including the unmapped-account report.

What goes wrong without it

The single-chart alternative is to post directly to VAS codes. That reads as simpler and costs you the ability to change your mind: every statutory reclassification becomes a ledger correction, in a ledger where corrections are reversals, so a presentational change generates real journal entries that a reader has to understand as noise. The other failure is sub-accounts. A chart with one payable account per supplier grows without bound, makes every report a LIKE '2000-%' query, and turns “show me payables by activity” into a string-parsing exercise.