feat(pl): require contract package spec on plan stop save
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
ff96f2ec25
commit
ded6aeaa85
3 files changed
+135
-2
No files matched your search
@@ -937,6 +937,45 @@ def build_contract_qty_reminder_payload(
|
||||
return {"items": reminders}
|
||||
|
||||
|
||||
def validate_stop_products_package_against_contract(
|
||||
*,
|
||||
contract_products: Iterable[Any],
|
||||
stop_products: Iterable[Any],
|
||||
) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
绑合同时:计划段产品包装必填,且必须属于本合同该产品的包装集合。
|
||||
返回 (ok, error_message)。
|
||||
"""
|
||||
allowed_by_pid: DefaultDict[str, Set[str]] = defaultdict(set)
|
||||
contract_pids: Set[str] = set()
|
||||
for row in contract_products or []:
|
||||
pid = str(getattr(row, "product_id", None) or "").strip()
|
||||
if not pid:
|
||||
continue
|
||||
contract_pids.add(pid)
|
||||
pkg = normalize_package_spec(getattr(row, "package_spec", None))
|
||||
if pkg:
|
||||
allowed_by_pid[pid].add(pkg)
|
||||
|
||||
for row in stop_products or []:
|
||||
pid = str(getattr(row, "product_id", None) or "").strip()
|
||||
if not pid or pid not in contract_pids:
|
||||
continue
|
||||
allowed = allowed_by_pid.get(pid) or set()
|
||||
if not allowed:
|
||||
continue
|
||||
pkg = normalize_package_spec(getattr(row, "package_spec", None))
|
||||
name = (
|
||||
str(getattr(row, "product_name", None) or "").strip()
|
||||
or pid
|
||||
)
|
||||
if not pkg:
|
||||
return False, f"请选择包装规格:{name}"
|
||||
if pkg not in allowed:
|
||||
return False, f"包装规格不在合同明细中:{name}({pkg})"
|
||||
return True, None
|
||||
|
||||
|
||||
def get_contract_product_arrange_qty_detail(
|
||||
db: Session,
|
||||
*,
|
||||
|
||||
@@ -496,8 +496,8 @@ def _assert_product_rows_in_contract_allowed(
|
||||
)
|
||||
|
||||
|
||||
def _contract_allowed_product_ids(db: Session, tenant_no: str, contract_id: str) -> set[str]:
|
||||
rows = (
|
||||
def _contract_product_rows(db: Session, tenant_no: str, contract_id: str) -> list:
|
||||
return (
|
||||
db.query(CrmContractProduct)
|
||||
.filter(
|
||||
CrmContractProduct.contract_id == contract_id,
|
||||
@@ -506,6 +506,10 @@ def _contract_allowed_product_ids(db: Session, tenant_no: str, contract_id: str)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def _contract_allowed_product_ids(db: Session, tenant_no: str, contract_id: str) -> set[str]:
|
||||
rows = _contract_product_rows(db, tenant_no, contract_id)
|
||||
return {
|
||||
str(r.product_id).strip()
|
||||
for r in rows
|
||||
@@ -513,6 +517,25 @@ def _contract_allowed_product_ids(db: Session, tenant_no: str, contract_id: str)
|
||||
}
|
||||
|
||||
|
||||
def _assert_stop_products_package_for_contract(
|
||||
db: Session,
|
||||
tenant_no: str,
|
||||
contract_id: str,
|
||||
stop_products: Optional[list],
|
||||
) -> None:
|
||||
from app.services.pl_contract_plan_qty import (
|
||||
validate_stop_products_package_against_contract,
|
||||
)
|
||||
|
||||
rows = _contract_product_rows(db, tenant_no, contract_id)
|
||||
ok, err = validate_stop_products_package_against_contract(
|
||||
contract_products=rows,
|
||||
stop_products=stop_products or [],
|
||||
)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=422, detail=err or "包装规格不符合合同明细")
|
||||
|
||||
|
||||
def assert_load_products_in_selected_contract(
|
||||
db: Session,
|
||||
tenant_no: str,
|
||||
@@ -530,6 +553,8 @@ def assert_load_products_in_selected_contract(
|
||||
allowed = _contract_allowed_product_ids(db, tenant_no, contract_id)
|
||||
_assert_product_rows_in_contract_allowed(load.products, allowed, row_label="装货产品")
|
||||
_assert_product_rows_in_contract_allowed(plan_products, allowed, row_label="装货产品")
|
||||
merged = list(load.products or []) + list(plan_products or [])
|
||||
_assert_stop_products_package_for_contract(db, tenant_no, contract_id, merged)
|
||||
|
||||
|
||||
def assert_delivery_products_in_selected_contract(
|
||||
@@ -552,6 +577,9 @@ def assert_delivery_products_in_selected_contract(
|
||||
allowed,
|
||||
row_label=f"送货段#{seq}产品",
|
||||
)
|
||||
_assert_stop_products_package_for_contract(
|
||||
db, tenant_no, contract_id, delivery.products
|
||||
)
|
||||
|
||||
|
||||
def assert_stop_contract_required(
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""绑合同时计划段产品包装规格校验。"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.services.pl_contract_plan_qty import validate_stop_products_package_against_contract
|
||||
|
||||
|
||||
def test_require_package_when_contract_bound():
|
||||
contract_products = [
|
||||
SimpleNamespace(product_id="p1", package_spec="25kg/桶"),
|
||||
SimpleNamespace(product_id="p1", package_spec="槽罐车"),
|
||||
]
|
||||
ok, err = validate_stop_products_package_against_contract(
|
||||
contract_products=contract_products,
|
||||
stop_products=[SimpleNamespace(product_id="p1", package_spec="", product_name="次氯酸钠")],
|
||||
)
|
||||
assert ok is False
|
||||
assert err and "包装" in err
|
||||
|
||||
|
||||
def test_package_must_be_in_contract_set():
|
||||
contract_products = [
|
||||
SimpleNamespace(product_id="p1", package_spec="25kg/桶"),
|
||||
SimpleNamespace(product_id="p1", package_spec="槽罐车"),
|
||||
]
|
||||
ok, err = validate_stop_products_package_against_contract(
|
||||
contract_products=contract_products,
|
||||
stop_products=[SimpleNamespace(product_id="p1", package_spec="吨桶", product_name="次氯酸钠")],
|
||||
)
|
||||
assert ok is False
|
||||
assert err and "合同" in err
|
||||
|
||||
|
||||
def test_valid_package_passes():
|
||||
contract_products = [
|
||||
SimpleNamespace(product_id="p1", package_spec="25kg/桶"),
|
||||
SimpleNamespace(product_id="p1", package_spec="槽罐车"),
|
||||
]
|
||||
ok, err = validate_stop_products_package_against_contract(
|
||||
contract_products=contract_products,
|
||||
stop_products=[SimpleNamespace(product_id="p1", package_spec="槽罐车")],
|
||||
)
|
||||
assert ok is True
|
||||
assert err is None
|
||||
|
||||
|
||||
def test_product_not_on_contract_skipped():
|
||||
contract_products = [
|
||||
SimpleNamespace(product_id="p1", package_spec="槽罐车"),
|
||||
]
|
||||
ok, err = validate_stop_products_package_against_contract(
|
||||
contract_products=contract_products,
|
||||
stop_products=[SimpleNamespace(product_id="p2", package_spec="")],
|
||||
)
|
||||
assert ok is True
|
||||
|
||||
|
||||
def test_contract_empty_package_allows_empty_plan():
|
||||
"""脏数据防御:合同行包装为空时不强制计划填包装。"""
|
||||
contract_products = [
|
||||
SimpleNamespace(product_id="p1", package_spec=""),
|
||||
]
|
||||
ok, err = validate_stop_products_package_against_contract(
|
||||
contract_products=contract_products,
|
||||
stop_products=[SimpleNamespace(product_id="p1", package_spec="")],
|
||||
)
|
||||
assert ok is True
|
||||
Reference in new issue
Block a user