dodil/erp-brick-purchasing

DODIL ERP brick: purchasing

tests/test_stories_ers.py

"""Self-billing agreements (PUR-50) and ERS settlement (PUR-51)."""
import base64

from conftest import (
    ALICE,
    BUYER,
    CLERK,
    PAYAPPROVER,
    RECEIVER,
    SUPPLIER_A,
    api,
    confirmed,
    must,
    on_day,
    receive,
    supplier_price,
    world,
)
from test_stories_pay import propose

PDF = base64.b64encode(b"%PDF-1.4 signed agreement").decode()


def agreement(start="2026-11-01", end="2027-10-31", who=CLERK, **kw):
    return api("POST", f"/suppliers/{SUPPLIER_A}/self-billing", who, {"valid_from": start, "valid_to": end, "document_base64": PDF, **kw})


def agreed():
    must(agreement())
    return must(api("POST", f"/suppliers/{SUPPLIER_A}/self-billing/2026-11-01/approve", PAYAPPROVER, {}))


def ers_order(qty="10"):
    supplier_price()
    return confirmed(lines=[{"product_id": "COLA-075", "quantity": qty, "ers": True}])


def test_pur50_an_agreement_keeps_the_signed_pdf_and_a_second_person_approves_it(neighbours):
    world()
    a = must(agreement())
    assert a["status"] == "proposed" and a["document_key"].startswith("raw/purchasing/self-billing/") and a["document_key"].endswith(".pdf")
    assert a["document_key"] in neighbours.objects.store and a["supplier_vat_number"] == "GB123"
    s, b = api("POST", f"/suppliers/{SUPPLIER_A}/self-billing/2026-11-01/approve", ALICE, {})
    assert s == 200                                                                    # ALICE is another person than the clerk
    s, b = api("POST", f"/suppliers/{SUPPLIER_A}/self-billing", ALICE, {"valid_from": "2026-12-01", "valid_to": "2027-11-30", "document_base64": PDF})
    assert s == 200
    s, b = api("POST", f"/suppliers/{SUPPLIER_A}/self-billing/2026-12-01/approve", ALICE, {})
    assert s == 409 and "second_person" in str(b)


def test_pur50_no_end_date_too_long_no_vat_number_or_not_a_pdf_is_refused_naming_the_rule(neighbours):
    world()
    assert "end_date" in str(api("POST", f"/suppliers/{SUPPLIER_A}/self-billing", CLERK, {"valid_from": "2026-11-01", "valid_to": None, "document_base64": PDF})[1])
    s, b = agreement(end="2027-11-02")
    assert s == 409 and "max_months" in str(b)
    assert agreement(end="2027-10-31", document_base64=base64.b64encode(b"not a pdf").decode())[0] == 422
    neighbours.tax.party = lambda bp: {"bp_id": bp, "current": {"treatment_code": "standard", "vat_number": "", "valid_from": "2026-01-01"}, "rows": []}
    s, b = agreement()
    assert s == 409 and "supplier_vat_number" in str(b)


def test_pur50_an_agreement_is_listed_for_renewal_30_days_before_its_end():
    world()
    agreed()
    with on_day("2026-11-20T09:00:00+00:00"):
        assert must(api("GET", "/self-billing-agreements?renewal_due=true", CLERK))["items"] == []
    with on_day("2027-10-05T09:00:00+00:00"):
        got = must(api("GET", "/self-billing-agreements?renewal_due=true", CLERK))["items"]
        assert len(got) == 1 and got[0]["renewal_due"] is True and got[0]["in_force"] is True


def test_pur51_receipts_become_one_approved_posted_self_billed_invoice_and_a_second_run_settles_nothing(neighbours):
    world()
    agreed()
    o = ers_order()
    receive(o, quantity="6", note="DN-1")
    receive(o, quantity="4", note="DN-2")
    r = must(api("POST", "/ers/settle", ALICE, {"up_to": "2026-11-20"}))
    (inv,) = r["raised"]
    assert inv["supplier_invoice_no"] == "SB-2026-00001" and str(inv["total"]) == "216.00" and inv["kind"] == "bill"
    got = must(api("GET", f"/bills/{inv['bill_id']}", ALICE))
    assert got["status"] == "approved" and got["self_billed"] is True and got["source_kind"] == "ers"
    assert neighbours.tax.docs[got["tax_ref"]]["self_billed"] is True and got["tax_ref"] in neighbours.tax.frozen
    assert neighbours.gl.posted(inv["bill_id"], "approve")
    assert "SELF-BILLED INVOICE" in neighbours.objects.store[inv["sent_to_supplier"]].decode()
    again = must(api("POST", "/ers/settle", ALICE, {"up_to": "2026-11-20"}))
    assert again["raised"] == [] and again["settled_nothing_new"] is True


def test_pur51_no_agreement_on_the_receipts_date_means_it_is_listed_never_billed():
    world()
    must(agreement(start="2026-11-20", end="2027-11-19"))
    must(api("POST", f"/suppliers/{SUPPLIER_A}/self-billing/2026-11-20/approve", PAYAPPROVER, {}))
    o = ers_order()
    receive(o, quantity="10", on="2026-11-19")
    r = must(api("POST", "/ers/settle", ALICE, {"up_to": "2026-11-20"}))
    assert r["raised"] == [] and "no approved self-billing agreement" in r["refused"][0]["reason"]


def test_pur51_a_receipt_the_supplier_already_billed_is_never_self_billed():
    world()
    agreed()
    o = ers_order()
    receive(o, quantity="10")
    from conftest import bill
    s, b = bill(o)
    must(api("POST", f"/bills/{b['bill_id']}/approve", PAYAPPROVER, {}))
    assert must(api("POST", "/ers/settle", ALICE, {"up_to": "2026-11-20"}))["raised"] == []


def test_pur51_a_return_after_settlement_becomes_a_self_billed_credit_in_the_next_run():
    world()
    agreed()
    o = ers_order()
    receive(o, quantity="10")
    must(api("POST", "/ers/settle", ALICE, {"up_to": "2026-11-20"}))
    rid = f"rcpt:{o['purchase_order_id']}:DN-1"
    must(api("POST", f"/receipts/{rid}/returns", RECEIVER, {"reason": "damaged", "lines": [{"line_no": 1, "quantity": "3"}]}))
    r = must(api("POST", "/ers/settle", ALICE, {"up_to": "2026-11-20"}))
    (c,) = r["raised"]
    assert c["kind"] == "credit" and str(c["total"]) == "64.80"
    assert must(api("POST", "/ers/settle", ALICE, {"up_to": "2026-11-20"}))["raised"] == []


def test_pur51_the_person_who_ran_it_cannot_approve_a_payment_run_with_the_invoice():
    world()
    agreed()
    o = ers_order()
    receive(o, quantity="10")
    must(api("POST", "/ers/settle", ALICE, {"up_to": "2026-11-20"}))
    run = must(propose())
    s, b = api("POST", f"/payment-runs/{run['run_id']}/approve", ALICE, {})
    assert s == 409, (s, b)


def test_pur50_51_access_only_the_holders_of_the_permission():
    world()
    assert agreement(who=BUYER)[0] == 403
    assert api("POST", "/ers/settle", BUYER, {"up_to": "2026-11-20"})[0] == 403
    must(agreement())
    assert api("POST", f"/suppliers/{SUPPLIER_A}/self-billing/2026-11-01/approve", CLERK, {})[0] == 403
    assert api("GET", "/self-billing-agreements", BUYER)[0] in (200, 403)