dodil/erp-brick-inventory

DODIL ERP brick: inventory

tests/scenarios/catalogue.py

"""inv's scenario catalogue (module.yaml tests.scenarios), each mapped to its stories and written ONCE over
the composition's doors — routes, MCP tools, public links — never a table. A skeleton: each scenario says its
Given/When/Then and is skipped until its steps are written.

    erp-scenarios list tests/scenarios/catalogue.py
    erp-scenarios run  tests/scenarios/catalogue.py
"""
import pathlib

from erp_platform.scenarios import Catalogue, Skip

HERE = pathlib.Path(__file__).resolve().parent
CAT = Catalogue("inv", compose=HERE / "compose.yaml", roles=HERE / "roles.yaml")

# Generated over the composition's doors, both HTTP and MCP (erp_platform.scenarios.probes): every role ×
# every use case — 401 with no identity, 403 without the permission, served with it. The finer probes (a role
# the workflow does not name, state guards, a terminal parent, separation of duties, seams, idempotency,
# mass assignment) are tests/test_probes.py. TODO(scenario): `rows=Rows(make, owner, other)` for row visibility.
CAT.permission_matrix()


@CAT.scenario("SC-INV-01", "buy in packs, sell by the bottle, count in litres", stories=["INV-05", "INV-07", "INV-10", "INV-11", "INV-25"])
def sc_sc_inv_01(s):
    s.given("COLA-075 base LTR, bottle 0.75 LTR, PK24 = 18 LTR; PO-0101 expecting 10 PK24 at 21.60; an order of 48 bottles")
    s.when("8 PK24 are received, 24 bottles delivered, the bin counted at 117 LTR and the count applied")
    s.then("on hand 144, 126, then 117 LTR; the order shows 24 delivered; a count worth -10.80 applies without approval and posts a loss of 10.80")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-02", "the last unit, twice", stories=["INV-08", "INV-13", "INV-22"])
def sc_sc_inv_02(s):
    s.given("one DRILL-500 at WH1/A/03; two orders, one reserved, one not")
    s.when("both deliveries are validated concurrently, then two reservations race for a newly received unit")
    s.then("the reserved order ships; the unreserved delivery is refused and writes nothing; of the reservations one is met and one is short")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-03", "average cost to zero", stories=["INV-16", "INV-31", "INV-32"])
def sc_sc_inv_03(s):
    s.given("COLA-075 at UK1, no stock")
    s.when("144 LTR received at 1.2000, 72 LTR at 1.3500, 90 LTR then 126 LTR delivered")
    s.then("averages 1.2000, 1.2500, 1.2500; values 172.80, 270.00, 157.50, 0.00; four journals, each once; the stock account in the ledger nets to 0 (exact, no residue)")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-04", "a large count needs a second person", stories=["INV-10", "INV-11", "INV-12", "INV-33"])
def sc_sc_inv_04(s):
    s.given("threshold 500.00; count C-15 worth -1,250.00 submitted by the warehouse manager, who also holds inv:adjustment:approve under a second account")
    s.when("the manager approves with the second account; then a stock controller approves")
    s.then("the first is refused (same email or same subject); the second applies the adjustment and posts it; the AI suggestion is shown, never applied")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-05", "a return reverses the delivery's cost", stories=["INV-14", "INV-23"])
def sc_sc_inv_05(s):
    s.given("WH1/OUT/00001 delivered 24 bottles valued 21.60; the average cost has since moved to 1.3000 a litre")
    s.when("6 bottles come back")
    s.then("the return is valued 5.40 (the cost they left at); the order records -6 with corrects_ref inv:WH1/OUT/00001; returning 30 is refused")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-06", "Odoo opening stock reconciles and stays signed", stories=["INV-26"])
def sc_sc_inv_06(s):
    s.given("an Odoo test database with two warehouses, a lot-tracked product, a negative quant and AVCO costs")
    s.when("the migration runs twice, the reconciliation is signed, Odoo is changed and the migration runs again")
    s.then("every reconcile check equal or explained (the negative quant listed); the second run writes nothing; the third writes nothing and lists the change as an adjustment to make")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-08", "received into inspection, counted in cases, shipped by weight", stories=["INV-36", "INV-34", "INV-35", "INV-20"])
def sc_sc_inv_08(s):
    s.given("WH1/QC (quality), WH1/A/01; COLA-075 warehouse unit PK24 with measures; a purchase line priced 21.6000 per PK24; an order of 48 C62")
    s.when("8 PK24 are received into WH1/QC, the order asks availability, 6 PK24 are released to WH1/A/01, WH1/A/01 is counted as 5 PK24 and 6 C62, and 2 PK24 are delivered")
    s.then("the receipt is valued 172.80 and shows 153.6 KGM; available is 0 until the release, then 108 LTR; the count line is 94.5 LTR from two entries; the delivery shows gross 38.4 KGM")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-09", "issue, scrap and FEFO", stories=["INV-37", "INV-38", "INV-39"])
def sc_sc_inv_09(s):
    s.given("GLOVES at 0.0800; COLA-075 lots L-2611 (29 days left), L-2612, L-2601 (expired); threshold 500.00")
    s.when("3 BX100 GLOVES are issued to MAINT and reversed, 36 LTR scrapped, a 7311 delivery picked, and an issue worth 600.00 approved by its submitter's second account")
    s.then("24.00 issued and reversed once; scrap posted with its reason; L-2612 proposed; the second account refused")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-10", "plan, promise and replenish", stories=["INV-42", "INV-43", "INV-44", "INV-49", "INV-54"])
def sc_sc_inv_10(s):
    s.given("twelve months of moves at WH1; site rows for WH1 and WH2")
    s.when("ABC runs, the cycle plan is opened, ATP asked for 480 C62, the nightly proposal runs twice and is accepted")
    s.then("COLA-075 class A and due for counting; 171 LTR now and the rest by date; one proposal of 60 PK24 and one purchase request")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-11", "special stocks", stories=["INV-45", "INV-46", "INV-47"])
def sc_sc_inv_11(s):
    s.given("consignment at 7311; supplier 9100's consignment SCREW-M4; subcontractor 9200 with components")
    s.when("a fill, an issue and a pick-up; a delivery from 9100's stock; KIT-A received from 9200")
    s.then("customer consignment on hand and never available; one withdrawal valued at purchasing's price; KIT-A valued at components + service")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-12", "standard cost to zero", stories=["INV-48", "INV-56"])
def sc_sc_inv_12(s):
    s.given("UK1 switches to standard on 2026-12-01 at 1.2000; COLA-075 126 LTR at average 1.2500")
    s.when("the switch, a receipt at 1.3500, a delivery, a new standard from 2027-01-01, and the last litre delivered")
    s.then("revaluation -6.30; price difference 21.60; revaluation +9.00 once; value 0.00 at zero quantity")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")


@CAT.scenario("SC-INV-07", "today at midnight", stories=["INV-07", "INV-30"])
def sc_sc_inv_07(s):
    s.given("business_timezone Europe/London, 00:30 on 2026-11-01 local (23:30 on 2026-10-31 UTC)")
    s.when("a delivery is validated")
    s.then("its moves and the sales order's delivered_on are 2026-11-01, not the UTC date")
    raise Skip("TODO(scenario): write the steps over the doors (routes, tools, links)")