8d5a05a419
- Implemented smoke tests for UOM directionality, ensuring conversions are one-directional and correctly validated. - Added tests for receiving and selling items in different UOMs, verifying correct quantity handling and error responses. - Created a UOM conversions panel in the frontend to allow users to manage UOM conversions for items. - Introduced hooks for allowed UOMs to optimize fetching and caching of UOM data for document line forms. - Developed utility functions for consistent UOM formatting and conversion handling across the application.
82 lines
3.6 KiB
Python
82 lines
3.6 KiB
Python
"""Smoke test — UOM conversions are one-directional, and the API says so.
|
|
|
|
`UomConverter` looks up exactly one shape, `FromUom -> ToUom = item.BaseUomId`, and never
|
|
inverts a factor. `UpdateUomConversionsAsync` used to accept *any* pair, so saving the more
|
|
natural-reading `base -> BOX` produced a row that returned 200, appeared in the item detail
|
|
response, and was then silently invisible to every consumer — surfacing much later as
|
|
"no UOM conversion" 422 at GRN confirm or stage start, on an item that visibly had one.
|
|
|
|
* base -> other is rejected with 422 (the direction that used to save and then not work)
|
|
* other -> base is accepted
|
|
* a self-conversion and a base-as-source row are rejected
|
|
* a zero/negative factor is rejected (UomConverter divides unit cost by it)
|
|
* changing an item's base UOM while conversions exist is refused rather than orphaning them
|
|
|
|
python Backend/smoke/uom_direction.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
from smoke_common import bootstrap
|
|
|
|
|
|
def main():
|
|
c, chk, args = bootstrap(__doc__)
|
|
print(f"API {args.api}")
|
|
|
|
item = c.get("/items?pageSize=1&status=Active").body["items"]
|
|
if not item:
|
|
sys.exit("FATAL: no active items.")
|
|
item = item[0]
|
|
item_id, base_uom = item["itemId"], item["baseUomId"]
|
|
|
|
uoms = c.get("/uoms?pageSize=50").body["items"]
|
|
other = next((u["uomId"] for u in uoms if u["uomId"] != base_uom), None)
|
|
if other is None:
|
|
sys.exit("FATAL: need at least 2 UOMs.")
|
|
print(f"item={item_id} baseUom={base_uom} otherUom={other}")
|
|
|
|
def put(conversions):
|
|
return c.put(f"/items/{item_id}/uom-conversions", {"conversions": conversions})
|
|
|
|
chk.section("1. The correct direction is accepted")
|
|
ok = put([{"fromUom": other, "toUom": base_uom, "factor": 12}])
|
|
chk.status("other -> base", ok, 200)
|
|
if ok.status == 200:
|
|
chk.check("stored with the base UOM as target", ok.body["conversions"][0]["toUom"], base_uom)
|
|
|
|
chk.section("2. The reverse direction is rejected, not silently stored")
|
|
chk.status("base -> other", put([{"fromUom": base_uom, "toUom": other, "factor": 12}]), 422)
|
|
|
|
chk.section("3. Degenerate rows are rejected")
|
|
chk.status("self-conversion (other -> other)", put([{"fromUom": other, "toUom": other, "factor": 2}]), 422)
|
|
chk.status("zero factor", put([{"fromUom": other, "toUom": base_uom, "factor": 0}]), 422)
|
|
chk.status("negative factor", put([{"fromUom": other, "toUom": base_uom, "factor": -3}]), 422)
|
|
|
|
chk.section("4. Base UOM cannot be repointed while conversions exist")
|
|
# Restore a valid conversion first, so the guard has something to protect.
|
|
put([{"fromUom": other, "toUom": base_uom, "factor": 12}])
|
|
head = c.get(f"/items/{item_id}")
|
|
if head.status == 200:
|
|
body = head.body
|
|
moved = c.put(f"/items/{item_id}", {
|
|
"sku": body["sku"], "name": body["name"], "description": body.get("description"),
|
|
"categoryId": body["categoryId"], "subCategoryId": body.get("subCategoryId"),
|
|
"brandId": body.get("brandId"),
|
|
"baseUomId": other, # <- the repoint being guarded
|
|
"defaultVendorId": body.get("defaultVendorId"),
|
|
"stockNature": body["stockNature"], "trackingMode": body["trackingMode"],
|
|
"taxClass": body.get("taxClass"), "salePrice": body.get("salePrice"),
|
|
}, if_match=head.etag)
|
|
chk.status("change base UOM with conversions defined", moved, 422)
|
|
else:
|
|
chk.check("could read the item for the repoint test", head.status, 200)
|
|
|
|
return chk.finish("UOM-DIRECTION")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|