v15 (get_item_details.py:768): get_item_tax_map(company, item_tax_template, ...) builds the map only from the Item Tax Template. The document's own taxes rows never enter it, so two rows sharing "VAT - SC" can't collide — the "On
Net Total" row keeps rate 15.
v16 (get_item_details.py:850-854): the signature changed to get_item_tax_map(doc=..., tax_template=...) and it now seeds the map from doc.taxes (item_tax_map[account_head] = rate, last-write-wins) before layering the template on
top. That's the new line that lets the Actual shipping row (rate None) clobber the VAT row's 15 to 0.
Reference summary
┌──────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │ │
├──────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Trigger │ update_item_tax_map → get_item_tax_map(doc=self.doc, ...) — taxes_and_totals.py:143-149 │
├──────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Root cause │ get_item_tax_map last-write-wins on account_head — erpnext/stock/get_item_details.py:852-854 (v16 only) │
├──────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Where the 0 is read back │ _get_tax_rate returns the poisoned item_tax_map["VAT - SC"] = 0 — taxes_and_totals.py:384-388 │
├──────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Net effect │ "On Net Total" VAT row computes at rate 0 in calculate_taxes (line 616), never reaching 15 │
├──────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ v15 contrast │ get_item_tax_map(company, item_tax_template) — template-only, no doc.taxes — get_item_details.py:768 │
└──────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────┘
The mechanism I originally stated ("v16 zeros the computed row when accounts collide") was directionally right but wrong about how — it's not a post-hoc zeroing, it's the per-item rate map being poisoned to 0 before computation.
The fix implication is the same as before (don't emit a second "VAT - SC" row), but now we know any fix that keeps two rows on one account is futile on v16 — the duplicate account itself is what breaks it.
● There are two line-item-tax paths and shipping always uses f_n_f_tax_account:
- Template path (enable_tax_lines_sync && not use_actual_tax_type): VAT account is inside the sales_taxes_and_charges_template rows.
- Actual path (use_actual_tax_type): VAT account is wc_server.tax_account.
● The collision only breaks the template path (not use_actual_tax_type), because that's the only path that produces a computed "On Net Total" row. In actual-tax mode every line is
"Actual" (rate unused), so duplicate accounts there are harmless. So the v16 requirement only needs to apply when a tax template is in use.