pl_mobile_stop_meta.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. """Optional independent stop metadata. No old-table columns or default-path queries."""
  2. from decimal import Decimal
  3. from fastapi import HTTPException
  4. META_FIELDS = ('alias_name', 'brand_mode', 'brand_id', 'brand_name')
  5. IDENTITY_FIELDS = ('contract_id', 'contract_product_id', 'product_id', 'package_spec', 'model', 'unit')
  6. SOURCE_FIELDS = ('product_name', 'brand_mode', 'brand_id', 'brand_name')
  7. def _value(row, field):
  8. return str(getattr(row, field, None) or '').strip()
  9. def _saved_meta_values(row):
  10. values = {field: _value(row, field) for field in META_FIELDS}
  11. # NULL asks the mobile reader to infer the current Web name; empty means explicitly cleared.
  12. if getattr(row, 'alias_name', None) is None:
  13. values['alias_name'] = None
  14. return values
  15. def assert_mobile_meta_enabled(enabled, requested):
  16. """Kept for existing callers. Alias and brand now follow the Web plan-product fields."""
  17. return None
  18. def fold_mobile_selection_into_web_fields(payload):
  19. """Write the mobile alias into product_name and the load-line brand onto the plan product."""
  20. if not getattr(payload, 'mobile_product_meta', False):
  21. return
  22. by_key, by_id = {}, {}
  23. for product in payload.products or []:
  24. key = _value(product, 'client_key')
  25. if key:
  26. by_key[key] = product
  27. row_id = _value(product, 'id')
  28. if row_id:
  29. by_id[row_id] = product
  30. load_rows = list(payload.load.products) if getattr(payload, 'load', None) else []
  31. for stop in load_rows:
  32. _apply_mobile_alias(stop)
  33. linked = by_key.get(_value(stop, 'plan_product_client_key')) or by_id.get(_value(stop, 'plan_product_id'))
  34. if linked is None:
  35. continue
  36. meta = getattr(stop, 'mobile_product_meta', None)
  37. alias = getattr(meta, 'alias_name', None) if meta is not None else None
  38. if isinstance(alias, str) and alias.strip():
  39. linked.product_name = alias.strip()
  40. _apply_mobile_brand(linked, meta)
  41. for delivery in getattr(payload, 'deliveries', None) or []:
  42. for stop in delivery.products or []:
  43. _apply_mobile_alias(stop)
  44. def _apply_mobile_alias(stop):
  45. meta = getattr(stop, 'mobile_product_meta', None)
  46. alias = getattr(meta, 'alias_name', None) if meta is not None else None
  47. if isinstance(alias, str) and alias.strip():
  48. stop.product_name = alias.strip()
  49. def _apply_mobile_brand(plan_product, meta):
  50. if meta is None:
  51. return
  52. mode = _value(meta, 'brand_mode')
  53. if mode == 'required':
  54. plan_product.brand_mode = 'required'
  55. plan_product.brand_id = _value(meta, 'brand_id') or None
  56. plan_product.brand_name = _value(meta, 'brand_name') or None
  57. elif mode == 'any':
  58. plan_product.brand_mode = 'any'
  59. plan_product.brand_id = None
  60. plan_product.brand_name = None
  61. def validate_mobile_meta_request(payload, enabled):
  62. requested = bool(getattr(payload, 'mobile_product_meta', False))
  63. stops = ([payload.load] if payload.load else []) + list(payload.deliveries or [])
  64. rows = [p for stop in stops for p in stop.products]
  65. if not requested and any(getattr(p, 'mobile_product_meta', None) is not None or getattr(p, 'unlinked_draft', False) for p in rows):
  66. raise HTTPException(422, '段产品快照必须显式启用移动保存')
  67. if requested:
  68. 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):
  69. raise HTTPException(422, '移动产品关联键不完整,请重新加载')
  70. values = [getattr(p, 'plan_qty', None) for p in rows] + [getattr(p, 'quantity', None) for p in payload.products or []]
  71. for value in values:
  72. if value is not None:
  73. qty = Decimal(str(value))
  74. if not qty.is_finite() or qty < 0:
  75. raise HTTPException(422, '移动产品数量须为非负有限数')
  76. def resolve_stop_meta(snapshots, used, tenant_no, plan_id, stop_type, stop_id, product, plan_product):
  77. explicit = getattr(product, 'mobile_product_meta', None)
  78. if explicit is not None:
  79. return {field: _value(explicit, field) for field in META_FIELDS}
  80. matches = [row for row in snapshots if row.id not in used
  81. and row.tenant_no == tenant_no and row.plan_id == plan_id
  82. and row.stop_type == stop_type and row.stop_id == stop_id
  83. and all(_value(row, field) == _value(product, field) for field in IDENTITY_FIELDS)]
  84. source_id = _value(product, 'id')
  85. if source_id:
  86. matches = [row for row in matches if row.stop_product_id == source_id]
  87. if len(matches) > 1:
  88. raise HTTPException(422, '该段有无法唯一关联的产品快照,请在移动端重新加载后保存')
  89. if not matches:
  90. return None
  91. row = matches[0]
  92. used.add(row.id)
  93. result = _saved_meta_values(row)
  94. # Existing Web legal fields take precedence only when actually changed.
  95. if (_value(product, 'product_name') != _value(row, 'product_name')
  96. or _value(plan_product, 'product_name') != _value(row, 'source_product_name')):
  97. result['alias_name'] = None
  98. if any(_value(plan_product, f) != _value(row, 'source_' + f) for f in SOURCE_FIELDS[1:]):
  99. result.update({f: _value(plan_product, f) for f in SOURCE_FIELDS[1:]})
  100. return result
  101. def load_stop_meta(db, tenant_no, plan_id, enabled):
  102. if not enabled:
  103. return []
  104. from app.models.pl_mobile_stop_meta import PlPlanStopProductMeta
  105. from sqlalchemy.exc import OperationalError, ProgrammingError
  106. try:
  107. return db.query(PlPlanStopProductMeta).filter(
  108. PlPlanStopProductMeta.tenant_no == tenant_no,
  109. PlPlanStopProductMeta.plan_id == plan_id,
  110. PlPlanStopProductMeta.del_flag == 0,
  111. ).all()
  112. except (OperationalError, ProgrammingError) as exc:
  113. raise HTTPException(503, '移动运输产品快照不可用,请联系管理员核查迁移后重试;本次未保存') from exc
  114. def write_stop_meta(db, snapshots, used, tenant_no, plan_id, stop_type, stop_id,
  115. stop_product_id, product, plan_product, user_id, dept_id, now):
  116. values = resolve_stop_meta(snapshots, used, tenant_no, plan_id, stop_type, stop_id, product, plan_product)
  117. if values is None:
  118. return
  119. from app.models.pl_mobile_stop_meta import PlPlanStopProductMeta
  120. from app.core.utils import generate_uuid
  121. db.add(PlPlanStopProductMeta(
  122. id=generate_uuid(), tenant_no=tenant_no, plan_id=plan_id, stop_type=stop_type,
  123. stop_id=stop_id, stop_product_id=stop_product_id,
  124. **{f: _value(product, f) for f in IDENTITY_FIELDS},
  125. product_name=_value(product, 'product_name'),
  126. **{'source_' + f: _value(plan_product, f) for f in SOURCE_FIELDS},
  127. **values, order_num=0, create_by=user_id, create_dept_id=dept_id or '',
  128. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  129. ))
  130. def attach_stop_meta(result, snapshots):
  131. from app.schemas.pl.plan import PlanStopProductMeta
  132. by_id = {(row.stop_type, row.stop_product_id): row for row in snapshots}
  133. stops = [('load', result.load)] + [('delivery', row) for row in result.deliveries or []]
  134. for stop_type, stop in stops:
  135. if stop is None:
  136. continue
  137. for product in stop.products:
  138. row = by_id.get((stop_type, product.id))
  139. if row:
  140. product.mobile_product_meta = PlanStopProductMeta(**_saved_meta_values(row))
  141. def attach_web_selection_as_mobile_meta(result, standard_by_id, alias_options_by_id):
  142. """Expose the Web plan-product alias and brand in the mobile read shape. No side table."""
  143. from app.schemas.pl.plan import PlanStopProductMeta
  144. from app.services.pl_product_display import resolve_alias_from_saved_name
  145. products = {row.id: row for row in result.products or []}
  146. stops = [result.load] + list(result.deliveries or [])
  147. for stop in stops:
  148. if stop is None:
  149. continue
  150. for product in stop.products:
  151. linked = products.get(getattr(product, 'plan_product_id', None))
  152. if linked is None:
  153. continue
  154. product_id = _value(linked, 'product_id')
  155. alias = resolve_alias_from_saved_name(
  156. getattr(linked, 'product_name', None),
  157. standard_by_id.get(product_id),
  158. alias_options_by_id.get(product_id) or [],
  159. )
  160. product.mobile_product_meta = PlanStopProductMeta(
  161. alias_name=alias or '',
  162. brand_mode=_value(linked, 'brand_mode'),
  163. brand_id=_value(linked, 'brand_id'),
  164. brand_name=_value(linked, 'brand_name'),
  165. )