88 lines
3.9 KiB
Python
88 lines
3.9 KiB
Python
"""Opt-in request-local keys. No storage, configuration, or database access."""
|
|
from fastapi import HTTPException
|
|
|
|
# Packaging is editable independently on each stop and does not change its linked product.
|
|
LINK_IDENTITY_FIELDS = ('product_id', 'model', 'unit')
|
|
|
|
# CRM dict value + historical Chinese label; empty also means ton in plan forms.
|
|
_TON_UNIT_ALIASES = frozenset({'dun', '吨', ''})
|
|
|
|
|
|
def _key(row, name):
|
|
return str(getattr(row, name, None) or '').strip()
|
|
|
|
|
|
def _identity_value(row, name):
|
|
value = _key(row, name)
|
|
if name == 'unit' and value in _TON_UNIT_ALIASES:
|
|
return 'dun'
|
|
return value
|
|
|
|
|
|
def _identity_mismatch(left, right):
|
|
return any(_identity_value(left, field) != _identity_value(right, field) for field in LINK_IDENTITY_FIELDS)
|
|
|
|
|
|
def validate_mobile_product_links(products, stop_products, *, creating=False, allow_unlinked_draft=False):
|
|
products, stops = list(products or []), list(stop_products or [])
|
|
if not any(_key(p, 'client_key') for p in products) and not any(_key(p, 'plan_product_client_key') or getattr(p, 'unlinked_draft', False) for p in stops):
|
|
return
|
|
keys, ids = {}, set()
|
|
for product in products:
|
|
key, row_id = _key(product, 'client_key'), _key(product, 'id')
|
|
if not key or len(key) > 64 or key in keys:
|
|
raise HTTPException(422, '移动计划产品关联键缺失或重复')
|
|
if creating and row_id:
|
|
raise HTTPException(422, '新建计划不能指定已有计划产品ID')
|
|
if row_id and row_id in ids:
|
|
raise HTTPException(422, '移动计划产品行ID重复,请重新加载并修复关联')
|
|
keys[key] = product
|
|
if row_id:
|
|
ids.add(row_id)
|
|
for stop in stops:
|
|
if getattr(stop, 'unlinked_draft', False):
|
|
if not allow_unlinked_draft or _key(stop, 'plan_product_id') or _key(stop, 'plan_product_client_key'):
|
|
raise HTTPException(422, '装货产品尚未匹配完成,请核对装货信息后再正式保存')
|
|
continue
|
|
key = _key(stop, 'plan_product_client_key')
|
|
if not key or key not in keys:
|
|
raise HTTPException(422, '移动段产品关联键无效')
|
|
product = keys[key]
|
|
explicit = _key(stop, 'plan_product_id')
|
|
if explicit and explicit != _key(product, 'id'):
|
|
raise HTTPException(422, '段产品关联ID与客户端关联键矛盾')
|
|
if _identity_mismatch(stop, product):
|
|
raise HTTPException(422, '段产品与所关联计划产品不一致')
|
|
|
|
|
|
def bind_mobile_product_key(row, product):
|
|
key = _key(product, 'client_key')
|
|
if key:
|
|
# Plain transient Python attribute: never an ORM column or client-supplied DB id.
|
|
row._mobile_request_client_key = key
|
|
|
|
|
|
def resolve_mobile_product_link(product, plan_products):
|
|
key = _key(product, 'plan_product_client_key')
|
|
if not key:
|
|
return None
|
|
matches = [row for row in plan_products if _key(row, '_mobile_request_client_key') == key]
|
|
if len(matches) != 1:
|
|
raise HTTPException(422, '移动段产品关联键未能唯一匹配计划产品')
|
|
row = matches[0]
|
|
explicit = _key(product, 'plan_product_id')
|
|
if explicit and explicit != str(row.id):
|
|
raise HTTPException(422, '段产品关联ID与客户端关联键矛盾')
|
|
if _identity_mismatch(product, row):
|
|
raise HTTPException(422, '段产品与所关联计划产品不一致')
|
|
return row.id
|
|
|
|
|
|
def is_existing_pending_delivery(product, delivery_id, pending_rows):
|
|
"""An unrelated Web draft save cannot silently choose an arbitrary load row."""
|
|
if _key(product, 'plan_product_id') or _key(product, 'plan_product_client_key'):
|
|
return False
|
|
fields = ('product_id', 'contract_id', 'contract_product_id', 'package_spec', 'model', 'unit')
|
|
return any(row.delivery_id == delivery_id and all(_key(row, f) == _key(product, f) for f in fields)
|
|
for row in pending_rows)
|