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"