pl_feedback_brand_qty.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. """现场入出库明细与运输回传的品牌数量校验与持久化"""
  2. from decimal import Decimal
  3. from typing import Any, Optional, Type
  4. from sqlalchemy.orm import Session
  5. from app.core.utils import generate_uuid
  6. from app.services.pl_brand_archive import validate_brand_qty_rows
  7. def format_brand_qty_summary(rows: list[dict]) -> Optional[str]:
  8. parts = []
  9. for r in rows:
  10. qty = Decimal(str(r.get("qty") or 0))
  11. if qty <= 0:
  12. continue
  13. name = (r.get("brandName") or r.get("brand_name") or "").strip() or "未命名品牌"
  14. qty_text = format(qty.normalize(), "f")
  15. parts.append(f"{name} {qty_text}")
  16. return " / ".join(parts) if parts else None
  17. def require_brand_qtys_if_needed(
  18. *,
  19. brand_mode: Optional[str],
  20. required_brand_id: Optional[str],
  21. actual_qty: Decimal,
  22. rows: list[dict],
  23. enforce: bool,
  24. ) -> None:
  25. if not brand_mode:
  26. return
  27. if not enforce:
  28. return
  29. positive = [r for r in rows if Decimal(str(r.get("qty") or 0)) > 0]
  30. if not positive:
  31. raise ValueError("有品牌产品须填写至少一个品牌数量")
  32. validate_brand_qty_rows(
  33. brand_mode=brand_mode,
  34. required_brand_id=required_brand_id,
  35. actual_qty=Decimal(str(actual_qty)),
  36. rows=rows,
  37. )
  38. def persist_io_item_brand_qtys(
  39. db: Session,
  40. *,
  41. model: Type[Any],
  42. tenant_no: str,
  43. order_id: str,
  44. order_item_id: str,
  45. plan_product_id: str,
  46. brand_mode: Optional[str],
  47. rows: list[dict],
  48. user_id: Optional[str] = None,
  49. ) -> None:
  50. from app.services.pl_brand_archive import prepare_brand_qty_rows
  51. from app.services.pl_child_sync import apply_keyed_rows
  52. if rows is None:
  53. return
  54. existing = db.query(model).filter(model.order_item_id == order_item_id,
  55. model.tenant_no == tenant_no, model.del_flag == 0).all()
  56. prepared = prepare_brand_qty_rows(existing, rows,
  57. dict(tenant_no=tenant_no, order_id=order_id, order_item_id=order_item_id,
  58. plan_product_id=plan_product_id))
  59. apply_keyed_rows(db, model, prepared, user_id=user_id)
  60. def _dict_field(item: dict, *keys: str):
  61. for key in keys:
  62. if key in item and item[key] is not None:
  63. return item[key]
  64. return None
  65. def persist_transport_product_feedback(
  66. db: Session,
  67. *,
  68. tenant_no: str,
  69. transport,
  70. plan_id: str,
  71. products: list[dict],
  72. user_id: Optional[str] = None,
  73. enforce_brand: bool = True,
  74. ) -> None:
  75. from fastapi import HTTPException
  76. from app.models.pl_models import PlPlanProduct, PlTransportProductBrandQty, PlTransportProductFeedback
  77. from app.services.pl_brand_archive import prepare_brand_qty_rows
  78. from app.services.pl_child_sync import apply_keyed_rows, prepare_keyed_rows
  79. if products is None:
  80. return
  81. if transport.tenant_no != tenant_no:
  82. raise HTTPException(422, '运输单不属于当前租户')
  83. transport_id = transport.id
  84. existing = db.query(PlTransportProductFeedback).filter(
  85. PlTransportProductFeedback.transport_id == transport_id,
  86. PlTransportProductFeedback.plan_id == plan_id,
  87. PlTransportProductFeedback.tenant_no == tenant_no,
  88. PlTransportProductFeedback.del_flag == 0).all()
  89. brands = db.query(PlTransportProductBrandQty).filter(
  90. PlTransportProductBrandQty.transport_id == transport_id,
  91. PlTransportProductBrandQty.tenant_no == tenant_no,
  92. PlTransportProductBrandQty.del_flag == 0).all()
  93. plan_products = {row.id: row for row in db.query(PlPlanProduct).filter(
  94. PlPlanProduct.plan_id == plan_id, PlPlanProduct.tenant_no == tenant_no,
  95. PlPlanProduct.del_flag == 0).all()}
  96. values, brand_plans = [], {}
  97. total_qty = Decimal('0')
  98. for item in products:
  99. pid = _dict_field(item, 'planProductId', 'plan_product_id')
  100. product = plan_products.get(pid)
  101. if product is None:
  102. raise HTTPException(422, '回传产品不存在、已删除或不属于当前计划')
  103. qty = Decimal(str(_dict_field(item, 'actualQty', 'actual_qty') or 0))
  104. if not qty.is_finite() or qty < 0:
  105. raise HTTPException(422, '回传数量须为非负有限数')
  106. total_qty += qty
  107. value = dict(tenant_no=tenant_no, transport_id=transport_id, plan_id=plan_id,
  108. plan_product_id=pid, actual_qty=qty)
  109. if item.get('id'):
  110. value['id'] = item['id']
  111. values.append(value)
  112. brand_rows = _dict_field(item, 'brandQtys', 'brand_qtys')
  113. if brand_rows is not None:
  114. require_brand_qtys_if_needed(brand_mode=product.brand_mode, required_brand_id=product.brand_id,
  115. actual_qty=qty, rows=brand_rows, enforce=enforce_brand)
  116. brand_plans[pid] = prepare_brand_qty_rows(
  117. [row for row in brands if row.plan_product_id == pid], brand_rows,
  118. dict(tenant_no=tenant_no, transport_id=transport_id, plan_product_id=pid))
  119. prepared = prepare_keyed_rows(existing, values, ('plan_product_id',), '运输产品回传')
  120. feedbacks = apply_keyed_rows(db, PlTransportProductFeedback, prepared, user_id=user_id)
  121. for feedback in feedbacks:
  122. brand_plan = brand_plans.get(feedback.plan_product_id)
  123. if brand_plan is not None:
  124. for _, fields in brand_plan[0]:
  125. fields['feedback_id'] = feedback.id
  126. apply_keyed_rows(db, PlTransportProductBrandQty, brand_plan, user_id=user_id)
  127. removed_feedback_ids = {row.id for row in prepared[1]}
  128. for row in brands:
  129. if row.feedback_id in removed_feedback_ids:
  130. row.del_flag = 1
  131. row.update_by = user_id
  132. transport.feedback_qty = total_qty
  133. def load_transport_driver_product_feedbacks(
  134. db: Session,
  135. *,
  136. tenant_no: str,
  137. transport_id: str,
  138. ) -> list[dict]:
  139. """管理端读运输单时附司机按产品回传及品牌摘要。"""
  140. from app.models.pl_models import (
  141. PlPlanProduct,
  142. PlTransportProductBrandQty,
  143. PlTransportProductFeedback,
  144. )
  145. feedback_rows = (
  146. db.query(PlTransportProductFeedback)
  147. .filter(
  148. PlTransportProductFeedback.transport_id == transport_id,
  149. PlTransportProductFeedback.tenant_no == tenant_no,
  150. PlTransportProductFeedback.del_flag == 0,
  151. )
  152. .order_by(PlTransportProductFeedback.order_num.asc())
  153. .all()
  154. )
  155. if not feedback_rows:
  156. return []
  157. plan_product_ids = [fb.plan_product_id for fb in feedback_rows if fb.plan_product_id]
  158. products_by_id: dict[str, PlPlanProduct] = {}
  159. if plan_product_ids:
  160. product_rows = (
  161. db.query(PlPlanProduct)
  162. .filter(
  163. PlPlanProduct.id.in_(plan_product_ids),
  164. PlPlanProduct.tenant_no == tenant_no,
  165. PlPlanProduct.del_flag == 0,
  166. )
  167. .all()
  168. )
  169. products_by_id = {row.id: row for row in product_rows}
  170. brand_qtys_by_plan_product: dict[str, list[dict]] = {}
  171. feedback_ids = [fb.id for fb in feedback_rows]
  172. brand_qty_rows = (
  173. db.query(PlTransportProductBrandQty)
  174. .filter(
  175. PlTransportProductBrandQty.transport_id == transport_id,
  176. PlTransportProductBrandQty.tenant_no == tenant_no,
  177. PlTransportProductBrandQty.del_flag == 0,
  178. PlTransportProductBrandQty.feedback_id.in_(feedback_ids),
  179. )
  180. .order_by(PlTransportProductBrandQty.order_num.asc())
  181. .all()
  182. )
  183. for row in brand_qty_rows:
  184. brand_qtys_by_plan_product.setdefault(row.plan_product_id, []).append(
  185. {
  186. "brandId": row.brand_id or "",
  187. "brandName": row.brand_name,
  188. "qty": row.qty,
  189. }
  190. )
  191. result: list[dict] = []
  192. for fb in feedback_rows:
  193. brand_qtys = brand_qtys_by_plan_product.get(fb.plan_product_id, [])
  194. pp = products_by_id.get(fb.plan_product_id or "")
  195. result.append(
  196. {
  197. "planProductId": fb.plan_product_id,
  198. "productName": (pp.product_name if pp else None) or "",
  199. "productNo": (pp.product_no if pp else None) or None,
  200. "model": (pp.model if pp else None) or None,
  201. "unit": (pp.unit if pp else None) or None,
  202. "packageSpec": (pp.package_spec if pp else None) or None,
  203. "brandMode": (pp.brand_mode if pp else None) or None,
  204. "brandId": (pp.brand_id if pp else None) or None,
  205. "brandName": (pp.brand_name if pp else None) or None,
  206. "actualQty": fb.actual_qty,
  207. "brandQtys": brand_qtys or None,
  208. "brandSummary": format_brand_qty_summary(brand_qtys),
  209. }
  210. )
  211. return result