> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vinmake.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Payables and input VAT

> What a supplier bill posts: the three-leg VAT split onto account 1300, the AP obligation the control credit stands for, GRNI clearing, landed cost and foreign-contractor tax.

# Payables and input VAT

The payables family is the money path GreatBook was built to get right, and the one with the most rules per document.

A payable is never one leg and never one row.
It is a **balanced entry**, an **AP sub-ledger obligation**, and - when the document states VAT - a **VAT register line**, all written in one transaction.

## A supplier bill, in full

The canonical case: a Vietnamese supplier VAT invoice for 1,100,000 dong of raw materials, 1,000,000 net plus 100,000 recoverable VAT.

| Leg                              | Account                           | VAS        | Dr        | Cr        |
| -------------------------------- | --------------------------------- | ---------- | --------- | --------- |
| the coded leg, at **net**        | `1200` inventory - raw materials  | 152        | 1,000,000 |           |
| recoverable input VAT            | `1300` input VAT recoverable      | 133 / 1331 | 100,000   |           |
| the AP control leg, at **gross** | `2000` accounts payable (control) | 331        |           | 1,100,000 |

Written alongside it, in the same transaction:

| Row               | Table         | Carries                                                                                                                          |
| ----------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| AP obligation     | `obligations` | party, the document's own `doc_no`, `doc_date`, `due_date`, currency, the **gross** and its base-currency twin                   |
| VAT register line | `tax_entries` | direction `input`, the net as `base_amount`, the derived `rate`, the VAT as `tax_amount`, `gl_account` `1300`, the received date |

## The gross is the anchor

What the supplier is owed is the **total on the document**, so the AP credit carries the gross and the coded leg is *derived* as `gross - VAT`.

That direction is not a preference.
Three independently extracted figures cannot be trusted to add up, and an entry that does not balance is not a rounding question - the ledger refuses it outright.
Deriving one of the three from the other two makes the entry balance by construction, however the document was written.

```mermaid theme={null}
flowchart TD
  D["the document"] --> G{"what does it state?"}
  G -- "a total" --> A["gross = total"]
  G -- "a net + a VAT amount" --> S["gross = net + VAT"]
  G -- "a net + only a RATE" --> R["no gross resolved<br/>refuse at propose"]
  A --> V["split_input_vat(gross, vat, net)"]
  S --> V
  V --> N["net leg = gross - VAT<br/>VAT leg = VAT<br/>AP credit = gross"]
```

## The VAT is read, never inferred

The recoverable VAT - *thuế GTGT được khấu trừ* - is an **asset** the business offsets against output VAT, not a cost.
Burying it inside the coded leg overstates the cost of everything purchased, understates a real asset, and leaves the VAT return with nothing to declare.

| Source                           | Accepted?             | Why                                                                                                                                     |
| -------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| a stated VAT **amount**          | yes                   | it is the VAT                                                                                                                           |
| a stated **net**, with the gross | yes                   | the VAT is `gross - net`, which is arithmetic                                                                                           |
| neither                          | yes - no VAT to split | the whole gross is the coded leg                                                                                                        |
| a VAT **rate**                   | **no**                | `10` versus `0.1` is a 10x error on a money path, and the one `creates_ap` type that states a rate alone states no total to apply it to |

A document stating a net and a rate and no total therefore resolves **no gross** and refuses at propose time, rather than crediting AP its pretax figure.

<Warning>
  Crediting AP the pretax figure would **balance**, chain, land in an open period, and pass every Close checksum, while understating what a supplier is owed.
  That is the entire class of defect this family's rules exist to close: the wrong number that survives every check because it is internally consistent.
</Warning>

A document that states a VAT amount **and** a net that disagree with the gross raises `InconsistentVatError` and posts nothing.
So does a negative VAT, and a VAT that consumes the whole payable.
The system does not choose between two figures a human can reconcile in a minute.

<Info>
  **The rate on the register line is derived, not read.**
  `tax_entries.rate` is `NUMERIC(20,6)`, and the Close list re-checks each line as `tax_amount == ROUND(base_amount × rate, 0)`.
  So the rate is computed from the two amounts that actually posted and quantized to the column's own scale - and because a 6dp rate cannot always reproduce a VAT that was itself rounded, the nearest rate is tried first and its two neighbours after it.
  A pair no 6dp rate can express falls back to the nearest and legitimately fails that checksum, because the checksum exists to flag a VAT that does not match its base.
</Info>

## The AP obligation is half the accounting fact

A credit to the control account and the sub-ledger row that explains it are **one** fact.

Before this was true, the Bookkeeper posted `Dr expense / Cr 2000` and opened nothing, so GL `2000` accumulated a balance no bill accounted for and `close_checks.ap_control_tie` failed on every document-originated payable.
It was found by the first live Close run.

| Column                   | Value                                                                                                             |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `party_code`             | the **resolved party-master id** when the counterparty resolved, the extracted supplier name only when it did not |
| `doc_no`                 | the document's own reference - the number a human would write on the payment - not the namespaced ledger key      |
| `amount` / `amount_base` | the gross, and the base amount read back off the *posted* control leg                                             |
| `due_date`               | stated outright, else `doc_date + payment terms`, else NULL                                                       |

A supplier is therefore **required**: `MissingCounterpartyError`, because posting the control leg alone would leave the ledger owing money to nobody.
A due date is not - the column is nullable, the tie does not read it, and inventing one to fill a column is worse than an honest NULL.

<Note>
  Preferring the master id **narrows** the one-supplier-two-keys split; it does not close it.
  Resolution happens at post time, and the party master can be unavailable, so the same supplier can land under the master id on one bill and under the free-text name on the next.
  Every posting therefore records `party_resolution` in its audit detail, so the fallback rows are identifiable for a later normalisation rather than indistinguishable from coded ones.
</Note>

## The account map for the coded leg

Resolved from the bill's **kind** first, and from the registry **document type** only when the source did not tag a kind.

| Kind                                       | Account | VAS                        |
| ------------------------------------------ | ------- | -------------------------- |
| `material`, `material-VAT`, `raw-material` | `1200`  | 152 raw materials          |
| `CMT-154`, `subcontract`                   | `1210`  | 154 work in progress       |
| `consumable`, `consumable-627`             | `5120`  | 627 manufacturing overhead |
| `service`, `professional`                  | `6410`  | 6425 professional fees     |
| `rent`                                     | `6200`  | 6427                       |
| `utilities`                                | `6420`  | 6237                       |
| `freight`, `freight-641`, `logistics`      | `6110`  | 6417 / 6412                |
| `asset`, `fixed-asset`                     | `1500`  | 211 fixed assets - cost    |
| `canteen`                                  | `6400`  | 6428 office and admin      |
| `telecom`, `software`                      | `6430`  | 6426                       |

Document-type fallbacks cover every `creates_ap` type in the registry, so the common path never needs a kind:

| Document type          | Account                                                   |
| ---------------------- | --------------------------------------------------------- |
| `supplier_vat_invoice` | `1200` - a VAT purchase invoice defaults to raw materials |
| `service_invoice`      | `6410`                                                    |
| `canteen_invoice`      | `6400`                                                    |
| `consumables_invoice`  | `5120`                                                    |

There is no wildcard default, in either table.

## The ledger idempotency key

`source_type` is the constant `"bill"` for every `creates_ap` document, and `source_id` is **namespaced by the intake row id**: `"<intake_id>:<doc_no>"`.

Keying on the document's natural reference alone made a model-extracted field decide whether a second document was a new liability or a replay.
Two distinct bills quoting the same reference collapsed onto the first entry, and the second row was flipped to `posted` pointing at a journal entry that never contained it - the reviewer was told "posted" for money that never reached the book.

Genuine cross-upload duplicates are the Clerk's `find_duplicate` and the Auditor's reconciliation to catch, not the ledger key's.

## A material bill that clears a goods receipt

When goods arrived before the invoice, the receipt already posted `Dr 1200 / Cr 2010 GRNI` at PO price.
The bill then **clears** the GRNI rather than debiting inventory a second time:

| Operation                        | Entry                                                       |
| -------------------------------- | ----------------------------------------------------------- |
| goods receipt (GRN), at PO price | `Dr 1200` (152) / `Cr 2010` (GRNI)                          |
| the material bill arrives        | `Dr 2010` (GRNI) / `Dr 1300` (133 VAT) / `Cr 2000` (331 AP) |

<Note>
  The GRNI path is replay-safe through the obligation's durable journal source identity.
  An exact retry returns the same obligation; conflicting accounting facts under that identity are refused.
  The key is not `(org, kind, doc_no)`, so two genuine documents that quote the same human bill number do not collapse into one liability.
</Note>

## Landed cost, and foreign-contractor tax

| Operation                                   | Entry                                                                                          | Note                                                                                                                                                                                                                      |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| inbound freight into landed material cost   | `Dr 1200` (152) / `Cr 2000` (331)                                                              | the total posts once; each receipt's share is added to the item's `1200` value as a value-only adjustment movement so it flows into the weighted average, prorated by quantity with an exact-sum split so no dong is lost |
| FCT withheld from a foreign contractor      | `Dr <expense>` at gross / `Cr 2000` (331) at net / `Cr 2130` (333821 VAT-FCT + 333822 CIT-FCT) | both FCT register lines are recorded with it                                                                                                                                                                              |
| a `627` pool line sourced from a supplier   | `Dr 5120` (627) / `Cr 2000` (331)                                                              | rent, utilities and depreciation feed the overhead pool - see [inventory and payroll](/greatbook/gl/rules-inventory-and-payroll)                                                                                          |
| an intercompany purchase (the buyer's side) | `Dr <expense>` / `Cr 2000` (331)                                                               | mirrors the seller's AR and eliminates on consolidation                                                                                                                                                                   |

## Where it is enforced

| Concern                                                                    | Code                                                                                    |
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| the document path: the split, the map, the obligation, the currency        | `langgraph_chat/agents/bookkeeper/posting_rules.py`                                     |
| resolving the gross from what the document actually stated                 | `langgraph_chat/agents/bookkeeper/intake_source.py`, `resolve_gross`                    |
| the one-transaction post-back, and the read-back of the posted control leg | `langgraph_chat/agents/bookkeeper/ledger_bridge.py`                                     |
| `create_bill`, the single obligation writer                                | `backend/app/logics/finance/settlement.py`                                              |
| GRNI, landed cost, the `627` pool                                          | `backend/app/logics/finance/costing.py`                                                 |
| the VAT register and FCT                                                   | `backend/app/logics/finance/tax.py`                                                     |
| the ties that read all of it back                                          | `close_checks.ap_control_tie`, `close_checks.input_vat_tie`, `vat_line_integrity_input` |

## The test that would fail if it broke

`agents/bookkeeper/tests/test_input_vat_split.py` and `test_ap_control_tie.py`.

Read `test_the_pre_fix_shape_leaves_input_vat_off_the_books` before adding a Close checksum here: unlike the AP tie's twin, **no** Close checksum fails on the pre-split shape, because the register ties at 0 == 0.
That is precisely why the split is asserted directly rather than left to a reconciliation.

## Related

* [Posting rules: the map](/greatbook/gl/posting-rules) - the law all three rule surfaces share
* [VN VAT and statutory tax](/greatbook/capabilities/subledgers/vat-statutory) - the register, the positions, and the declaration
* [Settlement, rails and the AP/AR spine](/greatbook/capabilities/subledgers/settlement) - the sub-ledger these obligations live in
* [Inventory and costing](/greatbook/capabilities/subledgers/inventory-costing) - GRNI and the costing chain in full
