dodil/erp-brick-purchasing

DODIL ERP brick: purchasing

tests/test_stories_cis.py

"""CIS — PUR-60 (labour and materials, the deduction at payment) and PUR-61 (recorded with tax)."""
from conftest import ALICE, CLERK, PAYAPPROVER, PAYAPPROVER2, SUPPLIER_A, SYSTEM, api, bill, confirmed, must, world
from test_stories_pay import propose


def cis_bill(neighbours, status="net", rate="20"):
    world()
    neighbours.tax.cis[SUPPLIER_A] = {"status": status, "rate_pct": rate}
    must(api("POST", f"/suppliers/{SUPPLIER_A}/profile", ALICE, {"cis_subcontractor": True, "valid_from": "2026-11-20"}))
    o = confirmed(lines=[{"description": "Labour", "quantity": "1", "unit_code": "C62", "unit_price": "3600.00", "kind": "service"},
                         {"description": "Materials", "quantity": "1", "unit_code": "C62", "unit_price": "1400.00", "kind": "service"}])
    s, b = bill(o, lines=[{"order_line_no": 1, "quantity": "1", "unit_code": "C62", "unit_price": "3600.00", "cis_part": "labour"},
                          {"order_line_no": 2, "quantity": "1", "unit_code": "C62", "unit_price": "1400.00", "cis_part": "materials"}])
    assert s == 200, b
    must(api("POST", f"/bills/{b['bill_id']}/approve", PAYAPPROVER, {}))
    return o, b


def test_pur60_the_run_proposes_net_of_the_deduction_on_the_labour_share(neighbours):
    cis_bill(neighbours)
    run = must(propose())
    p = run["payments"][0]
    assert str(p["cis_deduction"]) == "720.00" and float(p["gross"]) - float(p["amount"]) == 720.0                    # 20% of the 3,600.00 labour only
    assert p["cis_rate_pct"] in ("20", "20.0000")


def test_pur60_confirming_posts_payables_bank_and_the_cis_liability_and_records_the_deduction_with_tax(neighbours):
    cis_bill(neighbours)
    run = must(propose())
    must(api("POST", f"/payment-runs/{run['run_id']}/approve", PAYAPPROVER2, {}))
    must(api("POST", f"/payment-runs/{run['run_id']}/export", SYSTEM, {}))
    pid = run["payments"][0]["payment_id"]
    must(api("POST", f"/payments/{pid}/confirm", CLERK, {"bank_reference": "B1", "paid_on": "2026-11-20"}))
    (j,) = neighbours.gl.posted(pid, "pay")
    credits = {x.get("purpose") or x.get("account_id"): x.get("credit") for x in j["lines"] if x.get("credit")}
    assert credits.get("cis_liability") == "720.00"
    ded = neighbours.tax.deductions[f"pur:{pid}"]
    assert str(ded["bp_id"]) == str(SUPPLIER_A) and ded["bill_ids"]


def test_pur60_a_cis_supplier_without_verification_is_refused_unless_tax_answers_unmatched(neighbours):
    cis_bill(neighbours, status="net")
    neighbours.tax.cis[SUPPLIER_A]["readiness"] = [{"gate": "cis_verified", "met": False, "reason": "no verification in force", "fix": "verify in Tax"}]
    s, b = propose()
    assert s == 409 and "cis_verified" in str(b)
    neighbours.tax.cis[SUPPLIER_A].update(status="unmatched", rate_pct="30")
    run = must(propose())
    assert str(run["payments"][0]["cis_deduction"]) == "1080.00"


def test_pur61_the_deduction_is_recorded_once_and_a_rejected_payment_reverses_it_with_the_same_key(neighbours):
    cis_bill(neighbours)
    run = must(propose())
    must(api("POST", f"/payment-runs/{run['run_id']}/approve", PAYAPPROVER2, {}))
    must(api("POST", f"/payment-runs/{run['run_id']}/export", SYSTEM, {}))
    pid = run["payments"][0]["payment_id"]
    must(api("POST", f"/payments/{pid}/confirm", CLERK, {"bank_reference": "B1", "paid_on": "2026-11-20"}))
    must(api("POST", f"/payments/{pid}/confirm", CLERK, {"bank_reference": "B1", "paid_on": "2026-11-20"}))      # a retry
    assert len(neighbours.tax.deductions) == 1
    got = must(api("GET", f"/payments/{pid}", CLERK))
    assert str(got["cis_deduction"]) == "720.00" and got["cis_tax_month"] and got["cis_ref"]                     # purchasing keeps tax's answer
    must(api("POST", f"/payments/{pid}/reject", CLERK, {"reason": "the bank returned it"}))
    assert neighbours.tax.reversed == [got["cis_ref"]]