dodil/erp-brick-purchasing

DODIL ERP brick: purchasing

tests/test_stories_evaluation.py

"""PUR-46: the supplier scorecard."""
from conftest import ALICE, BUYER, CLERK, OWNER, SUPPLIER_A, api, confirmed, must, receive, supplier_price, world


def trade(n_late=1, n=5):
    world()
    supplier_price()
    o = confirmed(lines=[{"product_id": "COLA-075", "quantity": "1", "expected_on": "2026-11-10"}])
    for i in range(n):
        receive(o, quantity="1", note=f"DN-{i}", on="2026-11-19" if i < n_late else "2026-11-10")
    return o


def test_pur46_scores_are_derived_from_rows_and_the_overall_is_the_weighted_sum():
    trade()
    got = must(api("GET", f"/suppliers/{SUPPLIER_A}/evaluation?period_id=2026-Q4", BUYER))
    assert got["scores"]["delivery"] == 80 and got["scores"]["quality"] == 100 and got["scores"]["service"] is None
    must(api("POST", f"/suppliers/{SUPPLIER_A}/service-score", BUYER, {"period_id": "2026-Q4", "score": 4}))
    got = must(api("GET", f"/suppliers/{SUPPLIER_A}/evaluation?period_id=2026-Q4", BUYER))
    w = got["weights"]
    want = sum(w[c] * got["scores"][c] for c in w) / sum(w.values())
    assert got["scores"]["service"] == 80 and abs(float(got["overall"]) - want) < 0.01


def test_pur46_freezing_writes_once_and_a_reevaluation_is_a_new_row_with_a_reason():
    trade()
    f = must(api("POST", f"/suppliers/{SUPPLIER_A}/evaluations", BUYER, {"period_id": "2026-Q4"}))
    assert f["seq"] == 1 and f["frozen"] is True
    s, b = api("POST", f"/suppliers/{SUPPLIER_A}/evaluations", BUYER, {"period_id": "2026-Q4"})
    assert s == 409 and "reason" in str(b)
    assert must(api("POST", f"/suppliers/{SUPPLIER_A}/evaluations", BUYER, {"period_id": "2026-Q4", "reason": "late receipt booked"}))["seq"] == 2
    assert api("POST", f"/suppliers/{SUPPLIER_A}/service-score", BUYER, {"period_id": "2026-Q4", "score": 5})[0] == 409
    items = must(api("GET", f"/suppliers/{SUPPLIER_A}/evaluations", BUYER))["items"]
    assert [i["seq"] for i in items] == [1, 2]


def test_pur46_weights_are_owner_controlled_dated_rows_and_access_is_scoped():
    trade()
    assert api("POST", "/evaluation-weights", BUYER, {"valid_from": "2026-12-01", "price": 0, "delivery": 100, "quantity": 0, "quality": 0, "service": 0})[0] == 403
    assert api("POST", "/evaluation-weights", OWNER, {"valid_from": "2026-11-20", "price": 0, "delivery": 100, "quantity": 0, "quality": 0, "service": 0})[0] == 409
    must(api("POST", "/evaluation-weights", OWNER, {"valid_from": "2026-12-01", "price": 0, "delivery": 100, "quantity": 0, "quality": 0, "service": 0}))
    assert must(api("GET", f"/suppliers/{SUPPLIER_A}/evaluation?period_id=2026-Q4", BUYER))["weights"]["delivery"] == 100  # in force at the period's end
    assert must(api("GET", f"/suppliers/{SUPPLIER_A}/evaluation?period_id=2026-Q3", BUYER))["weights"]["delivery"] == 25
    assert api("GET", f"/suppliers/{SUPPLIER_A}/evaluation?period_id=2026-Q4", CLERK)[0] == 403
    assert api("GET", f"/suppliers/{SUPPLIER_A}/evaluation?period_id=nonsense", ALICE)[0] == 422