dodil/erp-brick-tax
DODIL ERP brick: tax
tests/test_purchase.py
"""Tax on a supplier's bill (v3, stories TAX-23, TAX-24, TAX-25) and what the selling bricks keep (TAX-20, TAX-21)."""
from conftest import (
ADMIN,
SYSTEM,
api,
compute,
compute_purchase,
customer,
gates_of,
line,
must,
purchase_setup,
setup,
)
def test_tax_23_a_bill_takes_the_input_code_and_entertainment_is_a_cost_not_a_reclaim():
purchase_setup()
out = must(compute_purchase([line(1, "1000.00", description="goods", supply="goods"),
line(2, "100.00", category="entertainment", description="client lunch")]))
one, two = out["lines"]
assert (one["tax_code"], one["rate_pct"], one["vat"], one["vat_reclaimable"], one["vat_non_reclaimable"]) == ("PS", "20", "200.00", "200.00", "0.00")
assert (two["tax_code"], two["vat"], two["vat_reclaimable"], two["vat_non_reclaimable"]) == ("PE", "20.00", "0.00", "20.00")
t = out["totals"]
assert (t["vat"], t["vat_reclaimable"], t["vat_non_reclaimable"]) == ("220.00", "200.00", "20.00")
assert out["direction"] == "purchase" and {"net", "vat", "vat_due", "gross", "method"} <= set(t)
def test_tax_23_a_sale_rule_is_never_used_for_a_purchase_and_a_pair_with_no_purchase_rule_is_refused():
purchase_setup()
# (zero, uk_business) has a SALE rule (Z) and no purchase rule: the bill is refused, never 0% and never Z
out = must(compute_purchase([line(1, "50.00", category="zero")]))
assert out["lines"][0]["tax_code"] is None and out["lines"][0]["refusal"] == "no tax rule for (zero, uk_business)"
assert out["totals"] is None and out["gates"][0]["gate"] == "rule_covers_pair" and not out["gates"][0]["met"]
sale = must(compute([line(1, "50.00", category="zero")]))
assert sale["lines"][0]["tax_code"] == "Z" and "direction" in sale and sale["direction"] == "sale"
def test_tax_23_the_suppliers_treatment_decides_and_a_sale_code_cannot_be_a_purchase_rule():
purchase_setup()
must(api("POST", "/parties/8802/set-party-tax", ADMIN, {"treatment_code": "uk_consumer"}))
out = must(compute_purchase([line(1, "10.00")], ref="PB-2", bp_id=8802))
assert out["lines"][0]["refusal"] == "no tax rule for (standard, uk_consumer)"
status, body = api("POST", "/rules", ADMIN, {"category_code": "zero", "treatment_code": "uk_business", "tax_code": "Z", "direction": "purchase"})
assert status == 422 and "is a sale code" in body["detail"]
status, body = api("POST", "/documents/compute", SYSTEM, {"lines": [line(1, "10.00")], "tax_point": "2026-11-20", "direction": "purchase"})
assert status == 422 and body["gate"] == "party_or_treatment" # v6 (TAX-53): neither a partner nor a treatment
def test_direction_defaults_to_sale_so_a_v2_call_gives_the_same_bytes():
purchase_setup()
a = must(compute([line(1, "100.00")], document_ref="Q-9"))
b = must(api("POST", "/documents/compute", SYSTEM, {"lines": [line(1, "100.00")], "tax_point": "2026-11-20", "bp_id": 7311,
"document_ref": "Q-9", "direction": "sale"}))
assert a | {"version": 0} == b | {"version": 0} and "vat_reclaimable" not in a["lines"][0] and "vat_reclaimable" not in a["totals"]
def test_tax_24_a_bill_with_no_supplier_vat_number_is_refused_input_vat():
purchase_setup()
must(compute_purchase([line(1, "1000.00")])) # 1,200.00 gross: above the less detailed invoice limit, so a full invoice (TAX-50)
bill = {"invoice_number": "AS-2210", "issue_date": "2026-11-19", "supplier_name": "Acme Supplies Ltd",
"supplier_address": "9 Dock Road, Hull", "customer_name": "Dodil Ltd"}
out = must(api("POST", "/documents/PB-1/check-contents", SYSTEM, {"direction": "purchase", "contents": bill}))
unmet = {g["gate"]: g for g in out["gates"] if not g["met"]}
assert set(unmet) == {"supplier_vat_number", "contents_complete"} and out["ready"] is False
assert unmet["supplier_vat_number"]["reason"] == "the bill shows no VAT number for Acme Supplies Ltd: input VAT cannot be reclaimed on it"
ok = must(api("POST", "/documents/PB-1/check-contents", SYSTEM, {"direction": "purchase", "contents": {**bill, "supplier_vat_number": "GB987654321"}}))
assert ok["ready"] is True and {"invoice_number", "issue_date", "supplier_name", "supplier_address", "customer_name"} <= {g["gate"] for g in ok["gates"]}
# with direction sale the gates still check OUR name and VAT number, as v2
setup_sale = must(compute([line(1, "10.00")], document_ref="INV-S"))
sale = must(api("POST", "/documents/INV-S/check-contents", SYSTEM, {"direction": "sale", "contents": {}}))
assert setup_sale["document_ref"] == "INV-S" and "supplier_vat_number" in {g["gate"] for g in sale["gates"]}
status, _ = api("POST", "/documents/PB-1/check-contents", SYSTEM, {"direction": "sale", "contents": {}})
assert status == 409 # computed as a purchase: not checkable as a sale
def test_tax_25_an_overseas_service_takes_the_reverse_charge_and_nets_to_nothing():
purchase_setup()
out = must(compute_purchase([line(1, "500.00", supply="services", description="consultancy")], ref="PB-3", bp_id=8801))
ln = out["lines"][0]
assert (ln["tax_code"], ln["vat"], ln["vat_due"], ln["vat_reclaimable"], ln["vat_non_reclaimable"]) == ("PRC", "0.00", "100.00", "100.00", "0.00")
assert out["totals"]["vat"] == "0.00" and out["totals"]["vat_due"] == "100.00" and out["totals"]["vat_reclaimable"] == "100.00"
# the code's boxes: 100.00 due as output (box 1) and reclaimed as input (box 4): the net cost of VAT is nil
codes = {c["tax_code"]: c for c in must(api("GET", "/codes", ADMIN))["codes"]}
assert codes["PRC"]["reporting_box"] == "1" and codes["PRC"]["input_reporting_box"] == "4"
# not reclaimable where the code's reclaimable_pct is 0
must(api("POST", "/codes", ADMIN, {"tax_code": "PRC0", "treatment": "reverse_charge", "rate_pct": "20", "charged_pct": "0", "direction": "purchase",
"valid_from": "2011-01-04", "reclaimable_pct": "0", "document_wording": "RC"}))
must(api("POST", "/codes/PRC0/activate-code", ADMIN, {}))
must(api("POST", "/rules", ADMIN, {"category_code": "entertainment", "treatment_code": "overseas_business", "tax_code": "PRC0", "direction": "purchase"}))
out = must(compute_purchase([line(1, "500.00", category="entertainment")], ref="PB-4", bp_id=8801))
assert out["lines"][0]["vat_reclaimable"] == "0.00" and out["lines"][0]["vat_non_reclaimable"] == "100.00"
def test_tax_21_the_sale_a_ledger_posts_has_vat_in_s_and_a_reverse_charge_that_posts_none():
setup()
customer(7312, "uk_cis_contractor", "GB555555555")
must(api("POST", "/rules", ADMIN, {"category_code": "standard", "treatment_code": "uk_cis_contractor", "tax_code": "S"}))
out = must(compute([line(1, "1000.00"), line(2, "500.00", category="construction_service")], bp_id=7312))
by = {s["tax_code"]: s for s in out["summary"]}
assert by["S"]["vat"] == "200.00" # credits the ledger's S account
assert (by["RC"]["vat"], by["RC"]["vat_due"], by["RC"]["net"]) == ("0.00", "100.00", "500.00") # no VAT posted; recorded with its net
assert out["totals"]["vat"] == "200.00" and out["totals"]["vat_due"] == "300.00"