| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- """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'),
- )
|