Files
shanxia2.0/backend/app/services/pl_mobile_product_identity.py
T

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)