pilot_delivery.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. """中试交付管理 CRUD(交付信息、交付物料、交付方式 + 费用扣减)"""
  2. from __future__ import annotations
  3. import uuid
  4. from datetime import datetime
  5. from typing import Any, Dict, List, Optional
  6. from sqlalchemy import and_, func
  7. from sqlalchemy.orm import Session
  8. from app.models.models import (
  9. BizChannelAccount,
  10. BizChannelAgreementProductPrice,
  11. BizChannelProductQuota,
  12. BizChannelServiceOrder,
  13. BizChannelServiceOrderProduct,
  14. BizExpressCompany,
  15. BizItemUnit,
  16. BizPilotApplication,
  17. BizPilotDelivery,
  18. BizPilotDeliveryMaterial,
  19. BizPilotDeliveryMethod,
  20. BizPilotInfusionPlan,
  21. BizPilotInfusionSchedule,
  22. )
  23. from app.crud.pilot_service import (
  24. _get_or_create_channel_account,
  25. _reduce_channel_product_quota,
  26. )
  27. # ────────────────────────────────────────────
  28. # Helpers: _row_to_dict
  29. # ────────────────────────────────────────────
  30. def _generate_delivery_no(db: Session, tenant_no: str) -> str:
  31. today_str = datetime.now().strftime("%Y%m%d")
  32. prefix = f"ZSJF{today_str}"
  33. existing = (
  34. db.query(BizPilotDelivery.delivery_no)
  35. .filter(
  36. BizPilotDelivery.tenant_no == tenant_no,
  37. BizPilotDelivery.delivery_no.like(f"{prefix}%"),
  38. BizPilotDelivery.del_flag == 0,
  39. )
  40. .order_by(BizPilotDelivery.delivery_no.desc())
  41. .first()
  42. )
  43. seq = 1
  44. if existing and existing[0]:
  45. try:
  46. seq = int(existing[0][-4:]) + 1
  47. except ValueError:
  48. seq = 1
  49. return f"{prefix}{seq:04d}"
  50. def _row_to_dict_delivery(row, db=None, tenant_no=None) -> Dict[str, Any]:
  51. d: Dict[str, Any] = {
  52. "id": row.id,
  53. "infusionScheduleId": row.infusion_schedule_id or "",
  54. "deliveryNo": row.delivery_no or "",
  55. "tenantNo": row.tenant_no or "",
  56. "remark": row.remark or "",
  57. "createBy": row.create_by or "",
  58. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  59. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  60. }
  61. if db and tenant_no:
  62. # materialCount
  63. material_count = (
  64. db.query(func.count(BizPilotDeliveryMaterial.id))
  65. .filter(
  66. BizPilotDeliveryMaterial.delivery_id == row.id,
  67. BizPilotDeliveryMaterial.tenant_no == tenant_no,
  68. BizPilotDeliveryMaterial.del_flag == 0,
  69. )
  70. .scalar()
  71. ) or 0
  72. d["materialCount"] = material_count
  73. # hasMethod
  74. method = (
  75. db.query(BizPilotDeliveryMethod.id)
  76. .filter(
  77. BizPilotDeliveryMethod.delivery_id == row.id,
  78. BizPilotDeliveryMethod.tenant_no == tenant_no,
  79. BizPilotDeliveryMethod.del_flag == 0,
  80. )
  81. .first()
  82. )
  83. d["hasMethod"] = method is not None
  84. return d
  85. def _row_to_dict_delivery_material(row, db=None) -> Dict[str, Any]:
  86. d: Dict[str, Any] = {
  87. "id": row.id,
  88. "deliveryId": row.delivery_id or "",
  89. "materialName": row.material_name or "",
  90. "qty": float(row.qty) if row.qty is not None else None,
  91. "unitId": row.unit_id or "",
  92. "tenantNo": row.tenant_no or "",
  93. "remark": row.remark or "",
  94. "createBy": row.create_by or "",
  95. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  96. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  97. }
  98. # unitName lookup
  99. if db and row.unit_id:
  100. unit = (
  101. db.query(BizItemUnit.unit_name)
  102. .filter(
  103. BizItemUnit.id == row.unit_id,
  104. BizItemUnit.del_flag == 0,
  105. )
  106. .first()
  107. )
  108. d["unitName"] = unit.unit_name if unit else ""
  109. else:
  110. d["unitName"] = ""
  111. return d
  112. def _row_to_dict_delivery_method(row, db=None) -> Dict[str, Any]:
  113. d: Dict[str, Any] = {
  114. "id": row.id,
  115. "deliveryId": row.delivery_id or "",
  116. "deliveryMode": row.delivery_mode or "",
  117. "deliveryAttachments": row.delivery_attachments or "",
  118. "shipTime": row.ship_time.strftime("%Y-%m-%d %H:%M:%S") if row.ship_time else "",
  119. "handoverTemp": row.handover_temp or "",
  120. "pickupPerson": row.pickup_person or "",
  121. "pickupPhone": row.pickup_phone or "",
  122. "senderName": row.sender_name or "",
  123. "expressCompanyId": row.express_company_id or "",
  124. "waybillNo": row.waybill_no or "",
  125. "expressFee": float(row.express_fee) if row.express_fee is not None else None,
  126. "etaTime": row.eta_time.strftime("%Y-%m-%d %H:%M:%S") if row.eta_time else "",
  127. "tenantNo": row.tenant_no or "",
  128. "remark": row.remark or "",
  129. "createBy": row.create_by or "",
  130. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  131. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  132. }
  133. # expressCompanyName lookup
  134. if db and row.express_company_id:
  135. company = (
  136. db.query(BizExpressCompany.express_name)
  137. .filter(
  138. BizExpressCompany.id == row.express_company_id,
  139. BizExpressCompany.del_flag == 0,
  140. )
  141. .first()
  142. )
  143. d["expressCompanyName"] = company.express_name if company else ""
  144. else:
  145. d["expressCompanyName"] = ""
  146. return d
  147. # ────────────────────────────────────────────
  148. # Fee Deduction Logic
  149. # ────────────────────────────────────────────
  150. def _deduct_fee_on_delivery(db: Session, schedule_id: str, tenant_no: str, user_id: str):
  151. """
  152. 创建交付时扣减费用。
  153. 流程:
  154. 1. schedule → plan → application
  155. 2. application.channel_service_order_id → service order
  156. 3. service_order.order_type (yfk/gmcp)
  157. 4. schedule.cell_type, schedule.spec_text
  158. 5. yfk(预付款型): 查协议价 → 扣渠道账户余额 → 更新订单余额
  159. 6. gmcp(购买型): 扣份数 + 更新订单产品持仓
  160. """
  161. # 1. Find schedule
  162. schedule = (
  163. db.query(BizPilotInfusionSchedule)
  164. .filter(
  165. BizPilotInfusionSchedule.id == schedule_id,
  166. BizPilotInfusionSchedule.tenant_no == tenant_no,
  167. BizPilotInfusionSchedule.del_flag == 0,
  168. )
  169. .first()
  170. )
  171. if not schedule:
  172. raise ValueError("输注计划不存在")
  173. # 2. Find plan → application
  174. plan = (
  175. db.query(BizPilotInfusionPlan)
  176. .filter(
  177. BizPilotInfusionPlan.id == schedule.infusion_plan_id,
  178. BizPilotInfusionPlan.del_flag == 0,
  179. )
  180. .first()
  181. )
  182. if not plan:
  183. raise ValueError("输注方案不存在")
  184. application = (
  185. db.query(BizPilotApplication)
  186. .filter(
  187. BizPilotApplication.id == plan.pilot_application_id,
  188. BizPilotApplication.tenant_no == tenant_no,
  189. BizPilotApplication.del_flag == 0,
  190. )
  191. .first()
  192. )
  193. if not application:
  194. raise ValueError("中试申请单不存在")
  195. # 临床应用走 clinical_payment 模块缴费,不走服务订单扣费
  196. if (application.biz_type or "") == "clinical_application":
  197. return
  198. # 3. Get service order
  199. if not application.channel_service_order_id:
  200. raise ValueError("申请单未关联渠道服务订单,无法扣费")
  201. order = (
  202. db.query(BizChannelServiceOrder)
  203. .filter(
  204. BizChannelServiceOrder.id == application.channel_service_order_id,
  205. BizChannelServiceOrder.del_flag == 0,
  206. )
  207. .first()
  208. )
  209. if not order:
  210. raise ValueError("渠道服务订单不存在")
  211. order_type = order.order_type or ""
  212. # 4. yfk: deduct from channel account balance
  213. if order_type == "yfk":
  214. # Find agreement price
  215. agreement_id = order.channel_agreement_id
  216. if not agreement_id:
  217. raise ValueError("服务订单未关联渠道协议,无法确定扣费金额")
  218. agreement_product = (
  219. db.query(BizChannelAgreementProductPrice)
  220. .filter(
  221. BizChannelAgreementProductPrice.channel_agreement_id == agreement_id,
  222. BizChannelAgreementProductPrice.cell_type_code == (schedule.cell_type or ""),
  223. BizChannelAgreementProductPrice.spec_text == (schedule.spec_text or ""),
  224. BizChannelAgreementProductPrice.del_flag == 0,
  225. )
  226. .first()
  227. )
  228. deduction_amount = float(agreement_product.agreement_price) if agreement_product else 0
  229. if deduction_amount <= 0:
  230. # No price found, skip deduction
  231. return
  232. # Get channel account with row lock
  233. account = _get_or_create_channel_account(
  234. db, order.channel_id, tenant_no, user_id
  235. )
  236. balance_before = float(account.balance_amount) if account.balance_amount else 0
  237. if balance_before < deduction_amount:
  238. raise ValueError(
  239. f"渠道账户余额不足,当前余额 {balance_before} 元,需扣减 {deduction_amount} 元"
  240. )
  241. # Deduct balance
  242. account.balance_amount = balance_before - deduction_amount
  243. balance_after = float(account.balance_amount)
  244. account.update_by = user_id
  245. account.update_time = datetime.now()
  246. db.add(account)
  247. db.flush()
  248. # Update order current_balance
  249. order.current_balance = float(order.current_balance or 0) - deduction_amount
  250. order.balance_before = balance_before
  251. order.balance_after = balance_after
  252. order.update_by = user_id
  253. order.update_time = datetime.now()
  254. db.add(order)
  255. db.flush()
  256. # 5. gmcp: deduct quota and update order product
  257. elif order_type == "gmcp":
  258. # Find matching service order product for project_code
  259. order_product = (
  260. db.query(BizChannelServiceOrderProduct)
  261. .filter(
  262. BizChannelServiceOrderProduct.service_order_id == order.id,
  263. BizChannelServiceOrderProduct.cell_type == (schedule.cell_type or ""),
  264. BizChannelServiceOrderProduct.spec_text == (schedule.spec_text or ""),
  265. BizChannelServiceOrderProduct.del_flag == 0,
  266. )
  267. .first()
  268. )
  269. project_code = order_product.project_code if order_product else ""
  270. # Reduce channel product quota
  271. _reduce_channel_product_quota(
  272. db,
  273. channel_id=order.channel_id,
  274. project_code=project_code,
  275. cell_type_code=schedule.cell_type or "",
  276. spec_text=schedule.spec_text or "",
  277. reduce_qty=1,
  278. user_id=user_id,
  279. )
  280. # Update BizChannelServiceOrderProduct.current_qty
  281. if order_product:
  282. current_qty = float(order_product.current_qty or 0)
  283. if current_qty < 1:
  284. raise ValueError("订单产品持仓数量不足")
  285. order_product.current_qty = current_qty - 1
  286. order_product.update_by = user_id
  287. order_product.update_time = datetime.now()
  288. db.add(order_product)
  289. db.flush()
  290. # ────────────────────────────────────────────
  291. # Delivery CRUD
  292. # ────────────────────────────────────────────
  293. def get_delivery_by_schedule(
  294. db: Session, schedule_id: str, tenant_no: str
  295. ) -> Optional[Dict[str, Any]]:
  296. row = (
  297. db.query(BizPilotDelivery)
  298. .filter(
  299. BizPilotDelivery.infusion_schedule_id == schedule_id,
  300. BizPilotDelivery.tenant_no == tenant_no,
  301. BizPilotDelivery.del_flag == 0,
  302. )
  303. .first()
  304. )
  305. if not row:
  306. return None
  307. return _row_to_dict_delivery(row, db, tenant_no)
  308. def list_deliveries_by_application(
  309. db: Session, application_id: str, tenant_no: str
  310. ) -> List[Dict[str, Any]]:
  311. """List all deliveries for an application (through plan → schedule → delivery)."""
  312. # Find all plan IDs for this application
  313. plan_ids = [
  314. r[0]
  315. for r in db.query(BizPilotInfusionPlan.id)
  316. .filter(
  317. BizPilotInfusionPlan.pilot_application_id == application_id,
  318. BizPilotInfusionPlan.tenant_no == tenant_no,
  319. BizPilotInfusionPlan.del_flag == 0,
  320. )
  321. .all()
  322. ]
  323. if not plan_ids:
  324. return []
  325. # Find all schedule IDs for these plans
  326. schedule_ids = [
  327. r[0]
  328. for r in db.query(BizPilotInfusionSchedule.id)
  329. .filter(
  330. BizPilotInfusionSchedule.infusion_plan_id.in_(plan_ids),
  331. BizPilotInfusionSchedule.tenant_no == tenant_no,
  332. BizPilotInfusionSchedule.del_flag == 0,
  333. )
  334. .all()
  335. ]
  336. if not schedule_ids:
  337. return []
  338. # Find all deliveries for these schedules
  339. rows = (
  340. db.query(BizPilotDelivery)
  341. .filter(
  342. BizPilotDelivery.infusion_schedule_id.in_(schedule_ids),
  343. BizPilotDelivery.tenant_no == tenant_no,
  344. BizPilotDelivery.del_flag == 0,
  345. )
  346. .order_by(BizPilotDelivery.create_time.desc())
  347. .all()
  348. )
  349. return [_row_to_dict_delivery(r, db, tenant_no) for r in rows]
  350. def create_delivery(
  351. db: Session,
  352. schedule_id: str,
  353. data: dict,
  354. tenant_no: str,
  355. user_id: str,
  356. dept_id: str,
  357. ) -> Dict[str, Any]:
  358. # Validate schedule exists and belongs to tenant
  359. schedule = (
  360. db.query(BizPilotInfusionSchedule)
  361. .filter(
  362. BizPilotInfusionSchedule.id == schedule_id,
  363. BizPilotInfusionSchedule.tenant_no == tenant_no,
  364. BizPilotInfusionSchedule.del_flag == 0,
  365. )
  366. .first()
  367. )
  368. if not schedule:
  369. raise ValueError("输注计划不存在")
  370. # Check if delivery already exists for this schedule
  371. existing = (
  372. db.query(BizPilotDelivery)
  373. .filter(
  374. BizPilotDelivery.infusion_schedule_id == schedule_id,
  375. BizPilotDelivery.tenant_no == tenant_no,
  376. BizPilotDelivery.del_flag == 0,
  377. )
  378. .first()
  379. )
  380. if existing:
  381. raise ValueError("该输注计划已创建交付信息")
  382. now = datetime.now()
  383. delivery_no = _generate_delivery_no(db, tenant_no)
  384. row = BizPilotDelivery(
  385. id=str(uuid.uuid4()),
  386. infusion_schedule_id=schedule_id,
  387. delivery_no=delivery_no,
  388. tenant_no=tenant_no,
  389. order_num=0,
  390. remark=data.get("remark") or "",
  391. create_by=user_id,
  392. create_dept_id=dept_id or "",
  393. create_time=now,
  394. update_by=user_id,
  395. update_time=now,
  396. del_flag=0,
  397. )
  398. db.add(row)
  399. db.flush()
  400. # Trigger fee deduction
  401. try:
  402. _deduct_fee_on_delivery(db, schedule_id, tenant_no, user_id)
  403. except ValueError:
  404. db.rollback()
  405. raise
  406. # Update schedule status to yjf (已交付)
  407. schedule.schedule_status = "yjf"
  408. schedule.update_by = user_id
  409. schedule.update_time = now
  410. from app.crud.pilot_outbound_status_sync import sync_pilot_outbound_from_schedule
  411. sync_pilot_outbound_from_schedule(db, schedule, user_id=user_id)
  412. db.commit()
  413. db.refresh(row)
  414. return _row_to_dict_delivery(row, db, tenant_no)
  415. def soft_delete_delivery(db: Session, delivery_id: str, tenant_no: str) -> None:
  416. now = datetime.now()
  417. row = (
  418. db.query(BizPilotDelivery)
  419. .filter(
  420. BizPilotDelivery.id == delivery_id,
  421. BizPilotDelivery.tenant_no == tenant_no,
  422. BizPilotDelivery.del_flag == 0,
  423. )
  424. .first()
  425. )
  426. if not row:
  427. raise ValueError("交付信息不存在")
  428. row.del_flag = 1
  429. row.update_time = now
  430. # Cascade soft-delete to materials
  431. db.query(BizPilotDeliveryMaterial).filter(
  432. BizPilotDeliveryMaterial.delivery_id == delivery_id,
  433. BizPilotDeliveryMaterial.tenant_no == tenant_no,
  434. BizPilotDeliveryMaterial.del_flag == 0,
  435. ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
  436. # Cascade soft-delete to method
  437. db.query(BizPilotDeliveryMethod).filter(
  438. BizPilotDeliveryMethod.delivery_id == delivery_id,
  439. BizPilotDeliveryMethod.tenant_no == tenant_no,
  440. BizPilotDeliveryMethod.del_flag == 0,
  441. ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
  442. db.commit()
  443. # ────────────────────────────────────────────
  444. # Delivery Material CRUD
  445. # ────────────────────────────────────────────
  446. def list_delivery_materials(
  447. db: Session, delivery_id: str, tenant_no: str
  448. ) -> List[Dict[str, Any]]:
  449. rows = (
  450. db.query(BizPilotDeliveryMaterial)
  451. .filter(
  452. BizPilotDeliveryMaterial.delivery_id == delivery_id,
  453. BizPilotDeliveryMaterial.tenant_no == tenant_no,
  454. BizPilotDeliveryMaterial.del_flag == 0,
  455. )
  456. .order_by(
  457. BizPilotDeliveryMaterial.order_num.asc(),
  458. BizPilotDeliveryMaterial.create_time.asc(),
  459. )
  460. .all()
  461. )
  462. return [_row_to_dict_delivery_material(r, db) for r in rows]
  463. def save_delivery_materials(
  464. db: Session,
  465. delivery_id: str,
  466. items_json: list,
  467. tenant_no: str,
  468. user_id: str,
  469. ) -> List[Dict[str, Any]]:
  470. """Bulk save: soft-delete existing + insert new."""
  471. now = datetime.now()
  472. # Validate delivery exists
  473. delivery = (
  474. db.query(BizPilotDelivery)
  475. .filter(
  476. BizPilotDelivery.id == delivery_id,
  477. BizPilotDelivery.tenant_no == tenant_no,
  478. BizPilotDelivery.del_flag == 0,
  479. )
  480. .first()
  481. )
  482. if not delivery:
  483. raise ValueError("交付信息不存在")
  484. # Soft-delete existing materials
  485. db.query(BizPilotDeliveryMaterial).filter(
  486. BizPilotDeliveryMaterial.delivery_id == delivery_id,
  487. BizPilotDeliveryMaterial.tenant_no == tenant_no,
  488. BizPilotDeliveryMaterial.del_flag == 0,
  489. ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
  490. db.flush()
  491. # Insert new
  492. created = []
  493. for idx, item in enumerate(items_json):
  494. material = BizPilotDeliveryMaterial(
  495. id=str(uuid.uuid4()),
  496. delivery_id=delivery_id,
  497. material_name=item.get("material_name") or "",
  498. qty=item.get("qty"),
  499. unit_id=item.get("unit_id") or "",
  500. tenant_no=tenant_no,
  501. order_num=idx + 1,
  502. remark=item.get("remark") or "",
  503. create_by=user_id,
  504. create_dept_id="",
  505. create_time=now,
  506. update_by=user_id,
  507. update_time=now,
  508. del_flag=0,
  509. )
  510. db.add(material)
  511. created.append(material)
  512. db.commit()
  513. for m in created:
  514. db.refresh(m)
  515. return [_row_to_dict_delivery_material(m, db) for m in created]
  516. def delete_delivery_material(
  517. db: Session, material_id: str, tenant_no: str
  518. ) -> None:
  519. row = (
  520. db.query(BizPilotDeliveryMaterial)
  521. .filter(
  522. BizPilotDeliveryMaterial.id == material_id,
  523. BizPilotDeliveryMaterial.tenant_no == tenant_no,
  524. BizPilotDeliveryMaterial.del_flag == 0,
  525. )
  526. .first()
  527. )
  528. if not row:
  529. raise ValueError("交付物料不存在")
  530. row.del_flag = 1
  531. row.update_time = datetime.now()
  532. db.commit()
  533. # ────────────────────────────────────────────
  534. # Delivery Method CRUD
  535. # ────────────────────────────────────────────
  536. def get_delivery_method(
  537. db: Session, delivery_id: str, tenant_no: str
  538. ) -> Optional[Dict[str, Any]]:
  539. row = (
  540. db.query(BizPilotDeliveryMethod)
  541. .filter(
  542. BizPilotDeliveryMethod.delivery_id == delivery_id,
  543. BizPilotDeliveryMethod.tenant_no == tenant_no,
  544. BizPilotDeliveryMethod.del_flag == 0,
  545. )
  546. .first()
  547. )
  548. if not row:
  549. return None
  550. return _row_to_dict_delivery_method(row, db)
  551. def save_delivery_method(
  552. db: Session,
  553. delivery_id: str,
  554. data: dict,
  555. tenant_no: str,
  556. user_id: str,
  557. ) -> Dict[str, Any]:
  558. """Create or update delivery method (upsert pattern)."""
  559. now = datetime.now()
  560. # Validate delivery exists
  561. delivery = (
  562. db.query(BizPilotDelivery)
  563. .filter(
  564. BizPilotDelivery.id == delivery_id,
  565. BizPilotDelivery.tenant_no == tenant_no,
  566. BizPilotDelivery.del_flag == 0,
  567. )
  568. .first()
  569. )
  570. if not delivery:
  571. raise ValueError("交付信息不存在")
  572. existing = (
  573. db.query(BizPilotDeliveryMethod)
  574. .filter(
  575. BizPilotDeliveryMethod.delivery_id == delivery_id,
  576. BizPilotDeliveryMethod.tenant_no == tenant_no,
  577. BizPilotDeliveryMethod.del_flag == 0,
  578. )
  579. .first()
  580. )
  581. # Parse datetime fields
  582. ship_time = data.get("ship_time")
  583. eta_time = data.get("eta_time")
  584. if existing:
  585. # Update
  586. existing.delivery_mode = data.get("delivery_mode") or existing.delivery_mode
  587. existing.delivery_attachments = data.get("delivery_attachments") if data.get("delivery_attachments") is not None else existing.delivery_attachments
  588. existing.ship_time = ship_time if ship_time is not None else existing.ship_time
  589. existing.handover_temp = data.get("handover_temp") if data.get("handover_temp") is not None else existing.handover_temp
  590. existing.pickup_person = data.get("pickup_person") if data.get("pickup_person") is not None else existing.pickup_person
  591. existing.pickup_phone = data.get("pickup_phone") if data.get("pickup_phone") is not None else existing.pickup_phone
  592. existing.sender_name = data.get("sender_name") if data.get("sender_name") is not None else existing.sender_name
  593. existing.express_company_id = data.get("express_company_id") if data.get("express_company_id") is not None else existing.express_company_id
  594. existing.waybill_no = data.get("waybill_no") if data.get("waybill_no") is not None else existing.waybill_no
  595. existing.express_fee = data.get("express_fee") if data.get("express_fee") is not None else existing.express_fee
  596. existing.eta_time = eta_time if eta_time is not None else existing.eta_time
  597. existing.remark = data.get("remark") if data.get("remark") is not None else existing.remark
  598. existing.update_by = user_id
  599. existing.update_time = now
  600. db.commit()
  601. db.refresh(existing)
  602. return _row_to_dict_delivery_method(existing, db)
  603. # Create
  604. row = BizPilotDeliveryMethod(
  605. id=str(uuid.uuid4()),
  606. delivery_id=delivery_id,
  607. delivery_mode=data.get("delivery_mode") or "",
  608. delivery_attachments=data.get("delivery_attachments") or "",
  609. ship_time=ship_time,
  610. handover_temp=data.get("handover_temp") or "",
  611. pickup_person=data.get("pickup_person") or "",
  612. pickup_phone=data.get("pickup_phone") or "",
  613. sender_name=data.get("sender_name") or "",
  614. express_company_id=data.get("express_company_id") or "",
  615. waybill_no=data.get("waybill_no") or "",
  616. express_fee=data.get("express_fee"),
  617. eta_time=eta_time,
  618. tenant_no=tenant_no,
  619. order_num=0,
  620. remark=data.get("remark") or "",
  621. create_by=user_id,
  622. create_dept_id="",
  623. create_time=now,
  624. update_by=user_id,
  625. update_time=now,
  626. del_flag=0,
  627. )
  628. db.add(row)
  629. db.commit()
  630. db.refresh(row)
  631. return _row_to_dict_delivery_method(row, db)