192 lines
8.7 KiB
Python
192 lines
8.7 KiB
Python
"""Optional independent stop metadata. No old-table columns or default-path queries."""
|
|
from decimal import Decimal
|
|
from fastapi import HTTPException
|
|
|
|
META_FIELDS = ('alias_name', 'brand_mode', 'brand_id', 'brand_name')
|
|
IDENTITY_FIELDS = ('contract_id', 'contract_product_id', 'product_id', 'package_spec', 'model', 'unit')
|
|
SOURCE_FIELDS = ('product_name', 'brand_mode', 'brand_id', 'brand_name')
|
|
|
|
|
|
def _value(row, field):
|
|
return str(getattr(row, field, None) or '').strip()
|
|
|
|
|
|
def _saved_meta_values(row):
|
|
values = {field: _value(row, field) for field in META_FIELDS}
|
|
# NULL asks the mobile reader to infer the current Web name; empty means explicitly cleared.
|
|
if getattr(row, 'alias_name', None) is None:
|
|
values['alias_name'] = None
|
|
return values
|
|
|
|
|
|
def assert_mobile_meta_enabled(enabled, requested):
|
|
"""Kept for existing callers. Alias and brand now follow the Web plan-product fields."""
|
|
return None
|
|
|
|
|
|
def fold_mobile_selection_into_web_fields(payload):
|
|
"""Write the mobile alias into product_name and the load-line brand onto the plan product."""
|
|
if not getattr(payload, 'mobile_product_meta', False):
|
|
return
|
|
by_key, by_id = {}, {}
|
|
for product in payload.products or []:
|
|
key = _value(product, 'client_key')
|
|
if key:
|
|
by_key[key] = product
|
|
row_id = _value(product, 'id')
|
|
if row_id:
|
|
by_id[row_id] = product
|
|
load_rows = list(payload.load.products) if getattr(payload, 'load', None) else []
|
|
for stop in load_rows:
|
|
_apply_mobile_alias(stop)
|
|
linked = by_key.get(_value(stop, 'plan_product_client_key')) or by_id.get(_value(stop, 'plan_product_id'))
|
|
if linked is None:
|
|
continue
|
|
meta = getattr(stop, 'mobile_product_meta', None)
|
|
alias = getattr(meta, 'alias_name', None) if meta is not None else None
|
|
if isinstance(alias, str) and alias.strip():
|
|
linked.product_name = alias.strip()
|
|
_apply_mobile_brand(linked, meta)
|
|
for delivery in getattr(payload, 'deliveries', None) or []:
|
|
for stop in delivery.products or []:
|
|
_apply_mobile_alias(stop)
|
|
|
|
|
|
def _apply_mobile_alias(stop):
|
|
meta = getattr(stop, 'mobile_product_meta', None)
|
|
alias = getattr(meta, 'alias_name', None) if meta is not None else None
|
|
if isinstance(alias, str) and alias.strip():
|
|
stop.product_name = alias.strip()
|
|
|
|
|
|
def _apply_mobile_brand(plan_product, meta):
|
|
if meta is None:
|
|
return
|
|
mode = _value(meta, 'brand_mode')
|
|
if mode == 'required':
|
|
plan_product.brand_mode = 'required'
|
|
plan_product.brand_id = _value(meta, 'brand_id') or None
|
|
plan_product.brand_name = _value(meta, 'brand_name') or None
|
|
elif mode == 'any':
|
|
plan_product.brand_mode = 'any'
|
|
plan_product.brand_id = None
|
|
plan_product.brand_name = None
|
|
|
|
|
|
def validate_mobile_meta_request(payload, enabled):
|
|
requested = bool(getattr(payload, 'mobile_product_meta', False))
|
|
stops = ([payload.load] if payload.load else []) + list(payload.deliveries or [])
|
|
rows = [p for stop in stops for p in stop.products]
|
|
if not requested and any(getattr(p, 'mobile_product_meta', None) is not None or getattr(p, 'unlinked_draft', False) for p in rows):
|
|
raise HTTPException(422, '段产品快照必须显式启用移动保存')
|
|
|
|
if requested:
|
|
if any(not _value(p, 'client_key') for p in payload.products or []) or any(not _value(p, 'plan_product_client_key') and not (getattr(payload, 'save_mode', '') == 'draft' and getattr(p, 'unlinked_draft', False)) for p in rows):
|
|
raise HTTPException(422, '移动产品关联键不完整,请重新加载')
|
|
values = [getattr(p, 'plan_qty', None) for p in rows] + [getattr(p, 'quantity', None) for p in payload.products or []]
|
|
for value in values:
|
|
if value is not None:
|
|
qty = Decimal(str(value))
|
|
if not qty.is_finite() or qty < 0:
|
|
raise HTTPException(422, '移动产品数量须为非负有限数')
|
|
|
|
|
|
def resolve_stop_meta(snapshots, used, tenant_no, plan_id, stop_type, stop_id, product, plan_product):
|
|
explicit = getattr(product, 'mobile_product_meta', None)
|
|
if explicit is not None:
|
|
return {field: _value(explicit, field) for field in META_FIELDS}
|
|
matches = [row for row in snapshots if row.id not in used
|
|
and row.tenant_no == tenant_no and row.plan_id == plan_id
|
|
and row.stop_type == stop_type and row.stop_id == stop_id
|
|
and all(_value(row, field) == _value(product, field) for field in IDENTITY_FIELDS)]
|
|
source_id = _value(product, 'id')
|
|
if source_id:
|
|
matches = [row for row in matches if row.stop_product_id == source_id]
|
|
if len(matches) > 1:
|
|
raise HTTPException(422, '该段有无法唯一关联的产品快照,请在移动端重新加载后保存')
|
|
if not matches:
|
|
return None
|
|
row = matches[0]
|
|
used.add(row.id)
|
|
result = _saved_meta_values(row)
|
|
# Existing Web legal fields take precedence only when actually changed.
|
|
if (_value(product, 'product_name') != _value(row, 'product_name')
|
|
or _value(plan_product, 'product_name') != _value(row, 'source_product_name')):
|
|
result['alias_name'] = None
|
|
if any(_value(plan_product, f) != _value(row, 'source_' + f) for f in SOURCE_FIELDS[1:]):
|
|
result.update({f: _value(plan_product, f) for f in SOURCE_FIELDS[1:]})
|
|
return result
|
|
|
|
|
|
def load_stop_meta(db, tenant_no, plan_id, enabled):
|
|
if not enabled:
|
|
return []
|
|
from app.models.pl_mobile_stop_meta import PlPlanStopProductMeta
|
|
from sqlalchemy.exc import OperationalError, ProgrammingError
|
|
try:
|
|
return db.query(PlPlanStopProductMeta).filter(
|
|
PlPlanStopProductMeta.tenant_no == tenant_no,
|
|
PlPlanStopProductMeta.plan_id == plan_id,
|
|
PlPlanStopProductMeta.del_flag == 0,
|
|
).all()
|
|
except (OperationalError, ProgrammingError) as exc:
|
|
raise HTTPException(503, '移动运输产品快照不可用,请联系管理员核查迁移后重试;本次未保存') from exc
|
|
|
|
|
|
def write_stop_meta(db, snapshots, used, tenant_no, plan_id, stop_type, stop_id,
|
|
stop_product_id, product, plan_product, user_id, dept_id, now):
|
|
values = resolve_stop_meta(snapshots, used, tenant_no, plan_id, stop_type, stop_id, product, plan_product)
|
|
if values is None:
|
|
return
|
|
from app.models.pl_mobile_stop_meta import PlPlanStopProductMeta
|
|
from app.core.utils import generate_uuid
|
|
db.add(PlPlanStopProductMeta(
|
|
id=generate_uuid(), tenant_no=tenant_no, plan_id=plan_id, stop_type=stop_type,
|
|
stop_id=stop_id, stop_product_id=stop_product_id,
|
|
**{f: _value(product, f) for f in IDENTITY_FIELDS},
|
|
product_name=_value(product, 'product_name'),
|
|
**{'source_' + f: _value(plan_product, f) for f in SOURCE_FIELDS},
|
|
**values, order_num=0, create_by=user_id, create_dept_id=dept_id or '',
|
|
create_time=now, update_by=user_id, update_time=now, del_flag=0,
|
|
))
|
|
|
|
|
|
def attach_stop_meta(result, snapshots):
|
|
from app.schemas.pl.plan import PlanStopProductMeta
|
|
by_id = {(row.stop_type, row.stop_product_id): row for row in snapshots}
|
|
stops = [('load', result.load)] + [('delivery', row) for row in result.deliveries or []]
|
|
for stop_type, stop in stops:
|
|
if stop is None:
|
|
continue
|
|
for product in stop.products:
|
|
row = by_id.get((stop_type, product.id))
|
|
if row:
|
|
product.mobile_product_meta = PlanStopProductMeta(**_saved_meta_values(row))
|
|
|
|
|
|
def attach_web_selection_as_mobile_meta(result, standard_by_id, alias_options_by_id):
|
|
"""Expose the Web plan-product alias and brand in the mobile read shape. No side table."""
|
|
from app.schemas.pl.plan import PlanStopProductMeta
|
|
from app.services.pl_product_display import resolve_alias_from_saved_name
|
|
products = {row.id: row for row in result.products or []}
|
|
stops = [result.load] + list(result.deliveries or [])
|
|
for stop in stops:
|
|
if stop is None:
|
|
continue
|
|
for product in stop.products:
|
|
linked = products.get(getattr(product, 'plan_product_id', None))
|
|
if linked is None:
|
|
continue
|
|
product_id = _value(linked, 'product_id')
|
|
alias = resolve_alias_from_saved_name(
|
|
getattr(linked, 'product_name', None),
|
|
standard_by_id.get(product_id),
|
|
alias_options_by_id.get(product_id) or [],
|
|
)
|
|
product.mobile_product_meta = PlanStopProductMeta(
|
|
alias_name=alias or '',
|
|
brand_mode=_value(linked, 'brand_mode'),
|
|
brand_id=_value(linked, 'brand_id'),
|
|
brand_name=_value(linked, 'brand_name'),
|
|
)
|