mappers.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. """地磅 ORM → API Read 模型映射。"""
  2. from __future__ import annotations
  3. import json
  4. from datetime import date, datetime, time
  5. from typing import Iterable, Optional
  6. from sqlalchemy.orm import Session
  7. from app.core.utils import format_datetime
  8. from app.models.models import SysUser
  9. # 三方预生成出入库单「日期未填」占位(原系统 date="",DDL 为 NOT NULL DATE)
  10. EMPTY_INVENTORY_DATE = date(1970, 1, 1)
  11. from app.models.db_models import DbInboundOutbound, DbPurchasePlan, DbWeighingSlip
  12. from app.schemas.db.inventory import InboundOutboundRead
  13. from app.schemas.db.plans import PurchasePlanRead, PurchasePlanWithFillLink
  14. from app.schemas.db.weighing import WeighingSlipRead
  15. def fmt_date(value: date | datetime | str | None) -> str | None:
  16. if value is None:
  17. return None
  18. if isinstance(value, datetime):
  19. return value.strftime("%Y-%m-%d")
  20. if isinstance(value, date):
  21. return value.strftime("%Y-%m-%d")
  22. return str(value)
  23. def fmt_datetime(value: datetime | str | None) -> str | None:
  24. if value is None:
  25. return None
  26. if isinstance(value, datetime):
  27. return format_datetime(value)
  28. return str(value)
  29. def load_user_display_name_map(
  30. db: Session, user_ids: Iterable[Optional[str]]
  31. ) -> dict[str, str]:
  32. """批量解析用户展示名(昵称优先,否则账号)。"""
  33. ids = {(uid or "").strip() for uid in user_ids if (uid or "").strip()}
  34. if not ids:
  35. return {}
  36. rows = (
  37. db.query(SysUser.id, SysUser.nick_name, SysUser.user_no)
  38. .filter(SysUser.id.in_(ids), SysUser.del_flag == 0)
  39. .all()
  40. )
  41. return {u.id: (u.nick_name or u.user_no or "").strip() for u in rows}
  42. def _create_by_name(
  43. create_by: Optional[str], user_name_map: dict[str, str] | None
  44. ) -> Optional[str]:
  45. uid = (create_by or "").strip()
  46. if not uid or user_name_map is None:
  47. return None
  48. name = (user_name_map.get(uid) or "").strip()
  49. return name or None
  50. def plan_to_read(
  51. row: DbPurchasePlan,
  52. *,
  53. executed_quantity: float | None = None,
  54. user_name_map: dict[str, str] | None = None,
  55. db: Session | None = None,
  56. ) -> PurchasePlanRead:
  57. name_map = user_name_map
  58. if name_map is None and db is not None:
  59. name_map = load_user_display_name_map(db, [row.create_by])
  60. return PurchasePlanRead(
  61. id=row.id,
  62. planNo=row.plan_no,
  63. planDate=fmt_date(row.plan_date) or "",
  64. startDate=fmt_date(row.start_date),
  65. endDate=fmt_date(row.end_date),
  66. businessType=row.business_type,
  67. productId=row.product_id,
  68. product=row.product,
  69. specification=row.specification,
  70. plannedQuantity=row.planned_quantity,
  71. vehicleNo=row.vehicle_no or "",
  72. customerName=row.customer_name,
  73. remarks=row.remark,
  74. executionStatus=row.execution_status,
  75. executedAt=fmt_datetime(row.executed_at),
  76. lastWeighingSlipId=row.last_weighing_slip_id,
  77. executedQuantity=(
  78. executed_quantity if executed_quantity is not None else (row.executed_quantity or 0)
  79. ),
  80. autoGenerateEnabled=row.auto_generate_enabled or 0,
  81. autoGenerateEndMonth=row.auto_generate_end_month,
  82. autoGenerateLastMonth=row.auto_generate_last_month,
  83. isDeleted=row.del_flag,
  84. createdAt=format_datetime(row.create_time) or "",
  85. createBy=row.create_by,
  86. createByName=_create_by_name(row.create_by, name_map),
  87. )
  88. def plan_to_read_with_fill_link(
  89. row: DbPurchasePlan,
  90. *,
  91. executed_quantity: float | None = None,
  92. fill_link_inventory_id: str | None = None,
  93. user_name_map: dict[str, str] | None = None,
  94. db: Session | None = None,
  95. ) -> PurchasePlanWithFillLink:
  96. base = plan_to_read(
  97. row, executed_quantity=executed_quantity, user_name_map=user_name_map, db=db
  98. )
  99. return PurchasePlanWithFillLink(
  100. **base.model_dump(by_alias=False),
  101. fill_link_inventory_id=fill_link_inventory_id,
  102. )
  103. def is_empty_inventory_date(value: date | None) -> bool:
  104. return value is None or value == EMPTY_INVENTORY_DATE
  105. def inventory_date_to_api(value: date | None) -> str:
  106. if is_empty_inventory_date(value):
  107. return ""
  108. return fmt_date(value) or ""
  109. def _parse_site_feedback_images(raw: str | None) -> list[str] | None:
  110. if not raw:
  111. return None
  112. try:
  113. parsed = json.loads(raw)
  114. if isinstance(parsed, list):
  115. items = [str(x) for x in parsed if x]
  116. return items or None
  117. except (TypeError, json.JSONDecodeError):
  118. pass
  119. items = [part.strip() for part in raw.split(",") if part.strip()]
  120. return items or None
  121. def inventory_to_read(
  122. row: DbInboundOutbound,
  123. *,
  124. weighing: DbWeighingSlip | None = None,
  125. user_name_map: dict[str, str] | None = None,
  126. db: Session | None = None,
  127. ) -> InboundOutboundRead:
  128. gross_weight = None
  129. tare_weight = None
  130. deduction = None
  131. net_weight = None
  132. gross_weight_time = None
  133. tare_weight_time = None
  134. has_weighing_slip = False
  135. if weighing:
  136. gross_weight = weighing.gross_weight
  137. tare_weight = weighing.tare_weight
  138. deduction = weighing.deduction
  139. net_weight = weighing.net_weight
  140. gross_weight_time = fmt_datetime(weighing.gross_weight_time)
  141. tare_time = fmt_datetime(weighing.tare_weight_time)
  142. tare_weight_time = tare_time if tare_time and tare_time.strip() else None
  143. has_weighing_slip = True
  144. name_map = user_name_map
  145. if name_map is None and db is not None:
  146. name_map = load_user_display_name_map(db, [row.create_by])
  147. return InboundOutboundRead(
  148. id=row.id,
  149. date=inventory_date_to_api(row.date),
  150. partnerCompany=row.partner_company,
  151. businessType=row.business_type,
  152. productId=row.product_id,
  153. product=row.product,
  154. specification=row.specification,
  155. quantity=row.quantity,
  156. planNo=row.plan_no,
  157. batchNo=row.batch_no,
  158. vehicleNo=row.vehicle_no,
  159. status=row.status,
  160. remarks=row.remark,
  161. purchasePlanId=row.purchase_plan_id,
  162. isDeleted=row.del_flag,
  163. createdAt=format_datetime(row.create_time) or "",
  164. createBy=row.create_by,
  165. createByName=_create_by_name(row.create_by, name_map),
  166. grossWeight=gross_weight,
  167. tareWeight=tare_weight,
  168. deduction=deduction,
  169. netWeight=net_weight,
  170. grossWeightTime=gross_weight_time,
  171. tareWeightTime=tare_weight_time,
  172. hasWeighingSlip=has_weighing_slip,
  173. siteFeedbackRemark=row.site_feedback_remark,
  174. siteFeedbackImages=_parse_site_feedback_images(row.site_feedback_images),
  175. siteFeedbackTime=format_datetime(row.site_feedback_time) or None,
  176. )
  177. def weighing_to_read(
  178. row: DbWeighingSlip,
  179. *,
  180. inventory_date: str | None = None,
  181. user_name_map: dict[str, str] | None = None,
  182. db: Session | None = None,
  183. ) -> WeighingSlipRead:
  184. name_map = user_name_map
  185. if name_map is None and db is not None:
  186. name_map = load_user_display_name_map(db, [row.create_by])
  187. return WeighingSlipRead(
  188. id=row.id,
  189. vehicleNo=row.vehicle_no,
  190. planNo=row.plan_no,
  191. productId=row.product_id,
  192. cargoName=row.cargo_name,
  193. specification=row.specification,
  194. businessType=row.business_type,
  195. batchNo=row.batch_no,
  196. supplier=row.supplier,
  197. consignee=row.consignee,
  198. grossWeight=row.gross_weight,
  199. grossWeightTime=fmt_datetime(row.gross_weight_time) or "",
  200. tareWeight=row.tare_weight,
  201. tareWeightTime=fmt_datetime(row.tare_weight_time) or "",
  202. deduction=row.deduction,
  203. netWeight=row.net_weight,
  204. plannedQuantity=row.planned_quantity,
  205. executedQuantity=row.executed_quantity,
  206. purchasePlanId=row.purchase_plan_id,
  207. inboundOutboundId=row.inbound_outbound_id,
  208. weighingPoint=row.weighing_point,
  209. remarks=row.remark,
  210. currentQuantity=row.current_quantity,
  211. isVoided=row.is_voided,
  212. createdAt=format_datetime(row.create_time) or "",
  213. createBy=row.create_by,
  214. createByName=_create_by_name(row.create_by, name_map),
  215. inventoryDate=inventory_date,
  216. )
  217. def datetime_end_of_day(value: date) -> datetime:
  218. return datetime.combine(value, time(23, 59, 59))
  219. def parse_date_str(value: str | None) -> date | None:
  220. if not value:
  221. return None
  222. try:
  223. return datetime.strptime(value[:10], "%Y-%m-%d").date()
  224. except ValueError:
  225. return None
  226. def parse_datetime_str(value: str | None) -> datetime | None:
  227. if not value:
  228. return None
  229. for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"):
  230. try:
  231. return datetime.strptime(value[:19] if fmt != "%Y-%m-%d" else value[:10], fmt)
  232. except ValueError:
  233. continue
  234. return None