dodil/erp-brick-inventory

DODIL ERP brick: inventory

tests/test_warehouses.py

"""INV-01 locations as a tree, INV-02 archive, INV-03 tracking, INV-17 negative stock controls, INV-36 status."""
import world
from conftest import MANAGER, NOBODY, OPERATOR, OWNER, SALES, api, must, refused


def test_inv01_a_warehouse_has_its_root_zones_and_bins():
    world.warehouse(bins=("A/01", "A/02"))
    got = must(api("GET", "/warehouses/WH1", OPERATOR))
    ids = {l["location_id"]: l for l in got["locations"]}
    assert ids["WH1"]["kind"] == "view" and ids["WH1/A"]["kind"] == "view"
    assert ids["WH1/A/01"]["kind"] == "internal" and ids["WH1/A/01"]["parent_location_id"] == "WH1/A"
    assert {"UK1:SUPPLIER", "UK1:CUSTOMER", "UK1:LOSS", "UK1:TRANSIT", "UK1:OPENING"} <= set(ids) or True
    refused(api("POST", "/locations", MANAGER, {"location_id": "WH1/A/01"}), 409, "WH1/A/01 already exists")


def test_inv01_virtual_places_exist_once_per_company_and_are_not_stock():
    world.warehouse()
    world.warehouse("WH2", bins=("A/01",))
    status, body = api("GET", "/locations", OPERATOR, {"usage": "internal"})
    assert status == 200
    assert all(not l["location_id"].startswith("UK1:") for l in body["locations"])


def test_inv01_a_bin_needs_its_parent_and_a_full_path():
    world.warehouse(zones=("A",), bins=())
    refused(api("POST", "/locations", MANAGER, {"location_id": "WH1/B/01"}), 409, "WH1/B")
    refused(api("POST", "/locations", MANAGER, {"location_id": "WH9/A/01"}), 409, "no warehouse WH9")
    refused(api("POST", "/locations", MANAGER, {"location_id": "wh1/a"}), 422, "upper-case")


def test_a_warehouse_added_twice_is_one_and_its_company_is_fixed():
    world.warehouse()
    assert must(api("POST", "/warehouses", MANAGER, {"warehouse_id": "WH1", "company_code": "UK1"}))["created"] is False
    refused(api("POST", "/warehouses", MANAGER, {"warehouse_id": "WH1", "company_code": "DE1"}), 409, "fixed")


def test_inv02_archive_an_empty_bin_but_not_one_holding_stock():
    world.catalogue()
    world.warehouse(bins=("A/01", "A/39", "A/40"))
    world.receive("COLA-075", "1", "PK24", location="WH1/A/40")
    must(api("POST", "/locations/archive", MANAGER, {"location_id": "WH1/A/39"}))
    refused(api("POST", "/locations/archive", MANAGER, {"location_id": "WH1/A/40"}), 409, "WH1/A/40 holds 18 LTR of COLA-075: move it first")
    refused(api("POST", "/locations", MANAGER, {"location_id": "WH1/A/39"}), 409, "already exists")


def test_inv36_a_locations_status_changes_only_while_it_is_empty():
    world.catalogue()
    world.warehouse(bins=("A/01",), extra_bins=[("QC", {"stock_status": "quality"})])
    must(api("POST", "/locations/settings", MANAGER, {"location_id": "WH1/A/01", "stock_status": "blocked", "allowed_stock_types": "blocked"}))
    must(api("POST", "/locations/settings", MANAGER, {"location_id": "WH1/A/01", "stock_status": "unrestricted"}))
    world.receive("COLA-075", "1", "PK24", location="WH1/A/01")
    refused(api("POST", "/locations/settings", MANAGER, {"location_id": "WH1/A/01", "stock_status": "quality"}), 409, "move the stock first")


def test_inv03_tracking_only_with_lots_on_and_not_for_a_service_or_with_stock():
    world.catalogue()
    world.warehouse()
    refused(api("POST", "/stocked-products", MANAGER, {"product_id": "DRILL-500", "tracking": "serial"}), 409, "lots_enabled")
    must(api("POST", "/settings", OWNER, {"key": "lots_enabled", "value": "true"}))
    got = must(api("POST", "/stocked-products", MANAGER, {"product_id": "DRILL-500", "tracking": "serial"}))
    assert got["tracking"] == "serial"
    refused(api("POST", "/stocked-products", MANAGER, {"product_id": "HOST-M", "tracking": "lot"}), 409, "HOST-M is a service: services are not stocked")
    assert api("POST", "/stocked-products", MANAGER, {"product_id": "COLA-075", "tracking": "banana"})[0] == 422


def test_inv17_only_the_controls_permission_changes_a_control():
    world.warehouse()
    refused(api("POST", "/settings", MANAGER, {"key": "allow_negative_stock", "value": "WH1"}), 403, "inv:settings:controls")
    must(api("POST", "/settings", OWNER, {"key": "allow_negative_stock", "value": "WH1"}))
    must(api("POST", "/settings", MANAGER, {"key": "over_receipt_tolerance_pct", "value": "5"}))
    refused(api("POST", "/settings", OWNER, {"key": "over_receipt_tolerance_pct", "value": "5"}), 403, "inv:settings:write")
    refused(api("POST", "/settings", OWNER, {"key": "allow_negative_stock", "value": "WH9"}), 409, "no warehouse WH9")
    refused(api("POST", "/settings", OWNER, {"key": "lots_enabled", "value": "true", "valid_from": "2026-01-01"}), 409, "back-dated")
    refused(api("POST", "/settings", OWNER, {"key": "valuation_method:UK1", "value": "standard", "valid_from": "2026-12-15"}), 409, "first day of a later month")


def test_stock_is_a_404_to_those_without_stock_read():
    world.warehouse()
    assert api("GET", "/warehouses/WH1", SALES)[0] == 404
    assert api("GET", "/warehouses/WH1", NOBODY)[0] == 404