Add smoke tests for UOM conversions and enhance frontend UOM management
- 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.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user