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"]]