dodil/erp-brick-sales-order
DODIL ERP brick: sales order
tests/test_so51.py
"""SO-51 (spec v9, kept in v10): timesheets delivers approved hours on a service line; a correction moves the line by the
difference; the same delivery_ref records once. timesheets is not built: `TimesheetsFake` is its outbound TS-12 call, checked on
both sides against the signed specs (contract_checks.inbound_check)."""
from decimal import Decimal
from conftest import ADMIN, BILLER, FULFILLER, api, confirm, confirmed, get, hand, must, person, refused
from contract_checks import inbound_check
from test_stories import FEES, TODAY, WIDGETS, D, invoice, line
TIMESHEETS = {**person("timesheets@shop.example", ["so:order:deliver", "so:order:read"]), "X-Dodil-Auth-Source": "platform"} # timesheets' service client
HOURS = lambda n: [{"product_id": "SETUP", "quantity": str(n), "unit_code": "HUR"}] # noqa: E731 a service line, delivered policy
class TimesheetsFake:
"""What timesheets sends on approval (its TS-12): one delivery per approved line revision, a correction naming the earlier one."""
def deliver(self, order, line_no, quantity, sheet="4410:2026-W47", rev=1, corrects=None, on=TODAY, who=TIMESHEETS):
body = {"line_no": line_no, "quantity": f"{Decimal(quantity):.4f}", "source": "timesheets",
"delivery_ref": f"ts:{sheet}:r{rev}:{line_no}", "delivered_on": on}
if corrects:
body["corrects_ref"] = corrects
path = f"/sales-orders/{order['sales_order_id']}/deliveries"
inbound_check("timesheets", "POST", path, body)
return api("POST", path, who, body)
def test_SO_51_hours_are_delivered_once_and_a_correction_moves_the_line_by_the_difference():
o = confirmed(lines=HOURS(120))
ts = TimesheetsFake()
must(ts.deliver(o, 1, 40, rev=0, sheet="4410:2026-W46")) # 40 already delivered
first = must(ts.deliver(o, 1, 19))
assert D(line(first, 1)["delivered"]) == 59
again = must(ts.deliver(o, 1, 19)) # the same ref twice records once
assert D(line(again, 1)["delivered"]) == 59 and len(again["deliveries"]) == 2 and again["version"] == first["version"]
fixed = must(ts.deliver(o, 1, -2, rev=2, corrects="ts:4410:2026-W47:r1:1"))
assert D(line(fixed, 1)["delivered"]) == 57
assert [(d["delivery_ref"], d["kind"], d["source"]) for d in fixed["deliveries"]][-1] == ("ts:4410:2026-W47:r2:1", "correction", "timesheets")
assert D(line(get(o["sales_order_id"]), 1)["to_invoice"]) == 57 # the order invoices delivered quantities (SO-21)
def test_SO_51_a_goods_line_is_refused_and_so_is_more_than_ordered():
o = confirmed(lines=WIDGETS(12) + HOURS(10))
ts = TimesheetsFake()
s, b = ts.deliver(o, 1, 2)
assert s == 409 and b.get("gate") == "service_line" and b["fix"], b
must(ts.deliver(o, 2, 8))
s, b = ts.deliver(o, 2, 3, sheet="4411:2026-W47")
assert s == 409 and b["gate"] == "within_ordered"
assert D(line(get(o["sales_order_id"]), 2)["delivered"]) == 8
def test_SO_51_a_correction_below_what_is_invoiced_is_refused_with_the_way_out():
o = confirmed(lines=HOURS(20))
ts = TimesheetsFake()
must(ts.deliver(o, 1, 10))
must(invoice(get(o["sales_order_id"]))) # 10 hours invoiced
s, b = ts.deliver(o, 1, -4, rev=2, corrects="ts:4410:2026-W47:r1:1")
assert s == 409 and b["gate"] == "not_below_invoiced" and "credit the invoice in invoicing" in b["fix"]
assert D(line(get(o["sales_order_id"]), 1)["delivered"]) == 10
def test_SO_51_only_timesheets_service_client_names_that_source_and_its_ref_is_a_ts_ref():
o = confirmed(lines=HOURS(10))
body = {"line_no": 1, "quantity": "2", "source": "timesheets", "delivery_ref": "ts:1:r1:1", "delivered_on": TODAY}
path = f"/sales-orders/{o['sales_order_id']}/deliveries"
assert api("POST", path, FULFILLER, body)[0] == 403 # a person cannot claim to be timesheets
assert api("POST", path, TIMESHEETS, {**body, "delivery_ref": "manual-1"})[0] == 422
assert api("POST", path, TIMESHEETS, {k: v for k, v in body.items() if k != "delivered_on"})[0] == 422
assert D(line(get(o["sales_order_id"]), 1)["delivered"]) == 0
def test_the_fake_cannot_send_what_the_signed_specs_do_not_allow():
o = confirmed(lines=HOURS(10))
path = f"/sales-orders/{o['sales_order_id']}/deliveries"
import pytest
with pytest.raises(AssertionError, match="breaks the signed specs"):
inbound_check("timesheets", "POST", path, {"line_no": 1, "quantity": "1", "source": "manual", "delivery_ref": "ts:1:r1:1", "delivered_on": TODAY})
from contract_checks import CONTRACT
CONTRACT.violations.clear()