pl_mobile_stop_meta.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  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. if requested and not enabled:
  17. raise HTTPException(503, '移动运输产品独立快照尚未启用,请联系管理员完成迁移并启用后重试;本次未保存')
  18. def validate_mobile_meta_request(payload, enabled):
  19. requested = bool(getattr(payload, 'mobile_product_meta', False))
  20. stops = ([payload.load] if payload.load else []) + list(payload.deliveries or [])
  21. rows = [p for stop in stops for p in stop.products]
  22. if not requested and any(getattr(p, 'mobile_product_meta', None) is not None or getattr(p, 'unlinked_draft', False) for p in rows):
  23. raise HTTPException(422, '段产品快照必须显式启用移动保存')
  24. assert_mobile_meta_enabled(enabled, requested)
  25. if requested and any(getattr(p, 'mobile_product_meta', None) is None for p in rows):
  26. raise HTTPException(422, '移动段产品快照不完整,请重新加载')
  27. if requested:
  28. 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):
  29. raise HTTPException(422, '移动产品关联键不完整,请重新加载')
  30. values = [getattr(p, 'plan_qty', None) for p in rows] + [getattr(p, 'quantity', None) for p in payload.products or []]
  31. for value in values:
  32. if value is not None:
  33. qty = Decimal(str(value))
  34. if not qty.is_finite() or qty < 0:
  35. raise HTTPException(422, '移动产品数量须为非负有限数')
  36. def resolve_stop_meta(snapshots, used, tenant_no, plan_id, stop_type, stop_id, product, plan_product):
  37. explicit = getattr(product, 'mobile_product_meta', None)
  38. if explicit is not None:
  39. return {field: _value(explicit, field) for field in META_FIELDS}
  40. matches = [row for row in snapshots if row.id not in used
  41. and row.tenant_no == tenant_no and row.plan_id == plan_id
  42. and row.stop_type == stop_type and row.stop_id == stop_id
  43. and all(_value(row, field) == _value(product, field) for field in IDENTITY_FIELDS)]
  44. source_id = _value(product, 'id')
  45. if source_id:
  46. matches = [row for row in matches if row.stop_product_id == source_id]
  47. if len(matches) > 1:
  48. raise HTTPException(422, '该段有无法唯一关联的产品快照,请在移动端重新加载后保存')
  49. if not matches:
  50. return None
  51. row = matches[0]
  52. used.add(row.id)
  53. result = _saved_meta_values(row)
  54. # Existing Web legal fields take precedence only when actually changed.
  55. if (_value(product, 'product_name') != _value(row, 'product_name')
  56. or _value(plan_product, 'product_name') != _value(row, 'source_product_name')):
  57. result['alias_name'] = None
  58. if any(_value(plan_product, f) != _value(row, 'source_' + f) for f in SOURCE_FIELDS[1:]):
  59. result.update({f: _value(plan_product, f) for f in SOURCE_FIELDS[1:]})
  60. return result
  61. def load_stop_meta(db, tenant_no, plan_id, enabled):
  62. if not enabled:
  63. return []
  64. from app.models.pl_mobile_stop_meta import PlPlanStopProductMeta
  65. from sqlalchemy.exc import OperationalError, ProgrammingError
  66. try:
  67. return db.query(PlPlanStopProductMeta).filter(
  68. PlPlanStopProductMeta.tenant_no == tenant_no,
  69. PlPlanStopProductMeta.plan_id == plan_id,
  70. PlPlanStopProductMeta.del_flag == 0,
  71. ).all()
  72. except (OperationalError, ProgrammingError) as exc:
  73. raise HTTPException(503, '移动运输产品快照不可用,请联系管理员核查迁移后重试;本次未保存') from exc
  74. def write_stop_meta(db, snapshots, used, tenant_no, plan_id, stop_type, stop_id,
  75. stop_product_id, product, plan_product, user_id, dept_id, now):
  76. values = resolve_stop_meta(snapshots, used, tenant_no, plan_id, stop_type, stop_id, product, plan_product)
  77. if values is None:
  78. return
  79. from app.models.pl_mobile_stop_meta import PlPlanStopProductMeta
  80. from app.core.utils import generate_uuid
  81. db.add(PlPlanStopProductMeta(
  82. id=generate_uuid(), tenant_no=tenant_no, plan_id=plan_id, stop_type=stop_type,
  83. stop_id=stop_id, stop_product_id=stop_product_id,
  84. **{f: _value(product, f) for f in IDENTITY_FIELDS},
  85. product_name=_value(product, 'product_name'),
  86. **{'source_' + f: _value(plan_product, f) for f in SOURCE_FIELDS},
  87. **values, order_num=0, create_by=user_id, create_dept_id=dept_id or '',
  88. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  89. ))
  90. def attach_stop_meta(result, snapshots):
  91. from app.schemas.pl.plan import PlanStopProductMeta
  92. by_id = {(row.stop_type, row.stop_product_id): row for row in snapshots}
  93. stops = [('load', result.load)] + [('delivery', row) for row in result.deliveries or []]
  94. for stop_type, stop in stops:
  95. if stop is None:
  96. continue
  97. for product in stop.products:
  98. row = by_id.get((stop_type, product.id))
  99. if row:
  100. product.mobile_product_meta = PlanStopProductMeta(**_saved_meta_values(row))