pilot_application.py 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857
  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, or_
  7. from sqlalchemy.orm import Session
  8. from app.models.models import (
  9. BizChannel,
  10. BizChannelServiceOrder,
  11. BizCustomer,
  12. BizPilotApplication,
  13. BizPilotDelivery,
  14. BizPilotInfusionConfirm,
  15. BizPilotInfusionPlan,
  16. BizPilotInfusionSchedule,
  17. BizPilotInfusionScheduleProduct,
  18. BizPilotPatient,
  19. BizProductionWorkOrder,
  20. BizQcWorkOrder,
  21. )
  22. from app.crud import pilot_checkup
  23. from app.crud.collection_protocol import attachments_to_urls, urls_to_attachment_list
  24. # ────────────────────────────────────────────
  25. # Helpers: _row_to_dict
  26. # ────────────────────────────────────────────
  27. def _normalize_yyyymmdd(value: Any) -> str:
  28. if value is None:
  29. return ""
  30. s = str(value).strip().replace("-", "").replace("/", "")
  31. return s[:8] if len(s) >= 8 else s
  32. def _format_yyyy_mm_dd(value: Any) -> str:
  33. s = _normalize_yyyymmdd(value)
  34. if len(s) == 8:
  35. return f"{s[:4]}-{s[4:6]}-{s[6:8]}"
  36. return str(value or "").strip()
  37. def _parse_infusion_datetime(value: Any) -> Optional[datetime]:
  38. if value is None or value == "":
  39. return None
  40. if isinstance(value, datetime):
  41. return value
  42. s = str(value).strip()
  43. for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
  44. try:
  45. return datetime.strptime(s[:19] if fmt.endswith("%S") else s[:10], fmt)
  46. except ValueError:
  47. continue
  48. compact = _normalize_yyyymmdd(s)
  49. if len(compact) == 8:
  50. try:
  51. return datetime.strptime(compact, "%Y%m%d")
  52. except ValueError:
  53. return None
  54. return None
  55. def _release_application_no_for_soft_delete(application_no: str, row_id: str) -> str:
  56. """软删除时改写申请单号,避免 uk_application_no(application_no, del_flag) 与历史已删记录冲突。"""
  57. base = (application_no or "").strip()
  58. suffix = f"~{row_id.replace('-', '')[:20]}"
  59. max_base_len = max(0, 64 - len(suffix))
  60. return f"{base[:max_base_len]}{suffix}"
  61. def _generate_application_no(db: Session, tenant_no: str, biz_type: str = "pilot_service") -> str:
  62. today_str = datetime.now().strftime("%Y%m%d")
  63. if biz_type == "clinical_application":
  64. prefix = f"LCSQ{today_str}"
  65. elif biz_type == "basic_experiment":
  66. prefix = f"JCSY{today_str}"
  67. else:
  68. prefix = f"ZSSQ{today_str}"
  69. existing = (
  70. db.query(BizPilotApplication.application_no)
  71. .filter(
  72. BizPilotApplication.tenant_no == tenant_no,
  73. BizPilotApplication.application_no.like(f"{prefix}%"),
  74. BizPilotApplication.del_flag == 0,
  75. )
  76. .order_by(BizPilotApplication.application_no.desc())
  77. .first()
  78. )
  79. seq = 1
  80. if existing and existing[0]:
  81. try:
  82. seq = int(existing[0][-4:]) + 1
  83. except ValueError:
  84. seq = 1
  85. return f"{prefix}{seq:04d}"
  86. def _resolve_application_attachment_payload(raw: dict) -> None:
  87. """解析保存载荷:附件列表 → URL 字段。"""
  88. if raw.get("consentAttachments") is not None:
  89. raw["consentFormUrls"] = attachments_to_urls(raw["consentAttachments"]) or None
  90. elif (raw.get("consentFormUrls") or "").strip():
  91. raw["consentFormUrls"] = (raw["consentFormUrls"] or "").strip()
  92. if raw.get("otherAttachments") is not None:
  93. raw["attachmentUrls"] = attachments_to_urls(raw["otherAttachments"]) or None
  94. elif (raw.get("attachmentUrls") or "").strip():
  95. raw["attachmentUrls"] = (raw["attachmentUrls"] or "").strip()
  96. def _row_to_dict_application(row, db=None, tenant_no=None) -> Dict[str, Any]:
  97. tn = tenant_no or row.tenant_no
  98. d: Dict[str, Any] = {
  99. "id": row.id,
  100. "applicationNo": row.application_no or "",
  101. "applicationName": row.application_name or "",
  102. "projectTitle": row.project_title or "",
  103. "customerName": row.customer_name or "",
  104. "customerContact": row.customer_contact or "",
  105. "customerPhone": row.customer_phone or "",
  106. "applicationStatus": row.application_status or "",
  107. "bizType": row.biz_type or "pilot_service",
  108. "channelServiceOrderId": row.channel_service_order_id or "",
  109. "channelId": row.channel_id or "",
  110. "channelName": row.channel_name or "",
  111. "tenantNo": row.tenant_no or "",
  112. "remark": row.remark or "",
  113. "consentFormUrls": row.consent_form_urls or "",
  114. "attachmentUrls": row.attachment_urls or "",
  115. "consentAttachmentFiles": urls_to_attachment_list(row.consent_form_urls, db, tn) if db else [],
  116. "attachmentFiles": urls_to_attachment_list(row.attachment_urls, db, tn) if db else [],
  117. "createBy": row.create_by or "",
  118. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  119. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  120. }
  121. if db and tenant_no:
  122. # patientName
  123. patient = (
  124. db.query(BizPilotPatient.patient_name)
  125. .filter(
  126. BizPilotPatient.pilot_application_id == row.id,
  127. BizPilotPatient.tenant_no == tenant_no,
  128. BizPilotPatient.del_flag == 0,
  129. )
  130. .first()
  131. )
  132. d["patientName"] = patient.patient_name if patient else ""
  133. # infusionScheduleCount:该申请单下全部输注计划条数(跨输注方案汇总)
  134. schedule_count = (
  135. db.query(func.count(BizPilotInfusionSchedule.id))
  136. .join(
  137. BizPilotInfusionPlan,
  138. and_(
  139. BizPilotInfusionSchedule.infusion_plan_id == BizPilotInfusionPlan.id,
  140. BizPilotInfusionPlan.del_flag == 0,
  141. ),
  142. )
  143. .filter(
  144. BizPilotInfusionPlan.pilot_application_id == row.id,
  145. BizPilotInfusionPlan.tenant_no == tenant_no,
  146. BizPilotInfusionSchedule.tenant_no == tenant_no,
  147. BizPilotInfusionSchedule.del_flag == 0,
  148. )
  149. .scalar()
  150. ) or 0
  151. d["infusionScheduleCount"] = schedule_count
  152. # serviceOrderNo + orderType
  153. svc_order = (
  154. db.query(BizChannelServiceOrder)
  155. .filter(BizChannelServiceOrder.id == row.channel_service_order_id)
  156. .first()
  157. )
  158. if svc_order:
  159. name = (svc_order.order_name or "").strip()
  160. if name:
  161. d["serviceOrderNo"] = name
  162. else:
  163. oid = svc_order.id or ""
  164. d["serviceOrderNo"] = oid[-8:] if len(oid) >= 8 else oid
  165. d["orderType"] = svc_order.order_type or ""
  166. else:
  167. d["serviceOrderNo"] = ""
  168. d["orderType"] = ""
  169. return d
  170. def _row_to_dict_patient(row) -> Dict[str, Any]:
  171. return {
  172. "id": row.id,
  173. "pilotApplicationId": row.pilot_application_id or "",
  174. "customerId": row.customer_id or "",
  175. "patientName": row.patient_name or "",
  176. "sex": row.sex or "",
  177. "age": row.age if row.age is not None else None,
  178. "idCardNo": row.id_card_no or "",
  179. "phoneNo": row.phone_no or "",
  180. "emergencyContact": row.emergency_contact or "",
  181. "emergencyPhone": row.emergency_phone or "",
  182. "clinicalDiagnosis": row.clinical_diagnosis or "",
  183. "diagnosisAttachments": row.diagnosis_attachments or "",
  184. "needCheckup": row.need_checkup if row.need_checkup is not None else 1,
  185. "tenantNo": row.tenant_no or "",
  186. "remark": row.remark or "",
  187. "createBy": row.create_by or "",
  188. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  189. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  190. }
  191. def _row_to_dict_infusion_plan(row, db=None, tenant_no=None) -> Dict[str, Any]:
  192. d: Dict[str, Any] = {
  193. "id": row.id,
  194. "pilotApplicationId": row.pilot_application_id or "",
  195. "cellType": row.cell_type or "",
  196. "specText": row.spec_text or "",
  197. "totalCourseCount": row.total_course_count if row.total_course_count is not None else 0,
  198. "courseInterval": row.course_interval or "",
  199. "infusionRoute": row.infusion_route or "",
  200. "infusionStartDate": _format_yyyy_mm_dd(row.infusion_start_date) if row.infusion_start_date else "",
  201. "channelAgreementId": row.channel_agreement_id or "",
  202. "tenantNo": row.tenant_no or "",
  203. "remark": row.remark or "",
  204. "createBy": row.create_by or "",
  205. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  206. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  207. }
  208. if db and tenant_no:
  209. # scheduleCount (all schedules)
  210. schedule_count = (
  211. db.query(func.count(BizPilotInfusionSchedule.id))
  212. .filter(
  213. BizPilotInfusionSchedule.infusion_plan_id == row.id,
  214. BizPilotInfusionSchedule.tenant_no == tenant_no,
  215. BizPilotInfusionSchedule.del_flag == 0,
  216. )
  217. .scalar()
  218. ) or 0
  219. d["scheduleCount"] = schedule_count
  220. # completedCount (schedule_status='ywc')
  221. completed_count = (
  222. db.query(func.count(BizPilotInfusionSchedule.id))
  223. .filter(
  224. BizPilotInfusionSchedule.infusion_plan_id == row.id,
  225. BizPilotInfusionSchedule.tenant_no == tenant_no,
  226. BizPilotInfusionSchedule.del_flag == 0,
  227. BizPilotInfusionSchedule.schedule_status == "ywc",
  228. )
  229. .scalar()
  230. ) or 0
  231. d["completedCount"] = completed_count
  232. return d
  233. def _row_to_dict_schedule_product(row) -> Dict[str, Any]:
  234. return {
  235. "id": row.id,
  236. "infusionScheduleId": row.infusion_schedule_id or "",
  237. "priceId": row.price_id or "",
  238. "projectCode": row.project_code or "",
  239. "projectName": row.project_name or "",
  240. "cellTypeName": row.cell_type_name or "",
  241. "specText": row.spec_text or "",
  242. "agreementPrice": float(row.agreement_price) if row.agreement_price is not None else None,
  243. "actualPrice": float(row.actual_price) if row.actual_price is not None else None,
  244. "quantity": row.quantity if row.quantity is not None else 1,
  245. "status": row.product_status or "",
  246. "remark": row.remark or "",
  247. }
  248. def _row_to_dict_infusion_schedule(row, cell_products: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
  249. infusion_time = ""
  250. if row.infusion_time:
  251. infusion_time = row.infusion_time.strftime("%Y-%m-%d")
  252. return {
  253. "id": row.id,
  254. "infusionPlanId": row.infusion_plan_id or "",
  255. "cellType": row.cell_type or "",
  256. "specText": row.spec_text or "",
  257. "infusionTime": infusion_time,
  258. "infusionLocation": row.infusion_location or "",
  259. "infusionMethod": row.infusion_method or "",
  260. "scheduleStatus": row.schedule_status or "",
  261. "infusionStatus": row.infusion_status or "dtz",
  262. "cancelReason": row.cancel_reason or "",
  263. "qcStatus": row.qc_status or "dzc",
  264. "cellProducts": cell_products if cell_products is not None else [],
  265. "attachments": row.attachments or "",
  266. "amount": float(row.amount) if row.amount else None,
  267. "tenantNo": row.tenant_no or "",
  268. "remark": row.remark or "",
  269. "createBy": row.create_by or "",
  270. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  271. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  272. }
  273. # ────────────────────────────────────────────
  274. # Application CRUD
  275. # ────────────────────────────────────────────
  276. def list_applications(
  277. db: Session,
  278. tenant_no: str,
  279. page: int = 1,
  280. page_size: int = 10,
  281. channel_id: Optional[str] = None,
  282. application_status: Optional[str] = None,
  283. channel_service_order_id: Optional[str] = None,
  284. keyword: Optional[str] = None,
  285. biz_type: Optional[str] = None,
  286. ):
  287. q = db.query(BizPilotApplication).filter(
  288. BizPilotApplication.tenant_no == tenant_no,
  289. BizPilotApplication.del_flag == 0,
  290. )
  291. if channel_id:
  292. q = q.filter(BizPilotApplication.channel_id == channel_id)
  293. if application_status:
  294. statuses = [s.strip() for s in application_status.split(",") if s.strip()]
  295. if statuses:
  296. q = q.filter(BizPilotApplication.application_status.in_(statuses))
  297. if channel_service_order_id:
  298. q = q.filter(BizPilotApplication.channel_service_order_id == channel_service_order_id)
  299. if biz_type:
  300. q = q.filter(BizPilotApplication.biz_type == biz_type)
  301. kw = (keyword or "").strip()
  302. if kw:
  303. like = f"%{kw}%"
  304. patient_app_ids = (
  305. db.query(BizPilotPatient.pilot_application_id)
  306. .filter(
  307. BizPilotPatient.tenant_no == tenant_no,
  308. BizPilotPatient.del_flag == 0,
  309. BizPilotPatient.patient_name.like(like),
  310. )
  311. .distinct()
  312. )
  313. order_ids = (
  314. db.query(BizChannelServiceOrder.id)
  315. .filter(
  316. BizChannelServiceOrder.tenant_no == tenant_no,
  317. BizChannelServiceOrder.del_flag == 0,
  318. BizChannelServiceOrder.order_name.like(like),
  319. )
  320. .distinct()
  321. )
  322. q = q.filter(
  323. or_(
  324. BizPilotApplication.application_no.like(like),
  325. BizPilotApplication.application_name.like(like),
  326. BizPilotApplication.channel_name.like(like),
  327. BizPilotApplication.id.in_(patient_app_ids),
  328. BizPilotApplication.channel_service_order_id.in_(order_ids),
  329. )
  330. )
  331. total = q.order_by(None).with_entities(func.count(BizPilotApplication.id)).scalar() or 0
  332. page = max(1, page)
  333. page_size = min(max(1, page_size), 100)
  334. rows = (
  335. q.order_by(BizPilotApplication.create_time.desc())
  336. .offset((page - 1) * page_size)
  337. .limit(page_size)
  338. .all()
  339. )
  340. items = [_row_to_dict_application(r, db, tenant_no) for r in rows]
  341. return {
  342. "list": items,
  343. "pagination": {"page": page, "pageSize": page_size, "total": total},
  344. }
  345. def get_application(db: Session, app_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
  346. row = (
  347. db.query(BizPilotApplication)
  348. .filter(
  349. BizPilotApplication.id == app_id,
  350. BizPilotApplication.tenant_no == tenant_no,
  351. BizPilotApplication.del_flag == 0,
  352. )
  353. .first()
  354. )
  355. if not row:
  356. return None
  357. result = _row_to_dict_application(row, db, tenant_no)
  358. plans = list_infusion_plans(db, app_id, tenant_no)
  359. if plans:
  360. plan = plans[0]
  361. result["infusionPlan"] = {
  362. "id": plan.get("id"),
  363. "totalCourseCount": plan.get("totalCourseCount"),
  364. "courseInterval": plan.get("courseInterval"),
  365. "infusionStartDate": plan.get("infusionStartDate"),
  366. "channelAgreementId": plan.get("channelAgreementId"),
  367. }
  368. plan_id = plan.get("id")
  369. if plan_id:
  370. result["infusionSchedules"] = list_schedules_by_plan(db, plan_id, tenant_no)
  371. if row.biz_type in ("clinical_application", "basic_experiment") and result["infusionSchedules"]:
  372. from app.crud import clinical_payment
  373. payment_map = clinical_payment.map_payments_by_schedule_id(
  374. db, application_id=app_id, tenant_no=tenant_no
  375. )
  376. for schedule in result["infusionSchedules"]:
  377. schedule_id = schedule.get("id")
  378. if schedule_id and schedule_id in payment_map:
  379. schedule["payment"] = payment_map[schedule_id]
  380. else:
  381. result["infusionPlan"] = None
  382. result["infusionSchedules"] = []
  383. return result
  384. def create_application(
  385. db: Session,
  386. data: dict,
  387. tenant_no: str,
  388. user_id: str,
  389. dept_id: str,
  390. ) -> Dict[str, Any]:
  391. channel_service_order_id = data.get("channelServiceOrderId") or ""
  392. biz_type = data.get("bizType") or "pilot_service"
  393. channel_id = (data.get("channelId") or "").strip()
  394. channel_name = (data.get("channelName") or "").strip()
  395. project_title = ""
  396. customer_name = ""
  397. customer_contact = ""
  398. customer_phone = ""
  399. consent_form_urls = data.get("consentFormUrls") or None
  400. if biz_type == "basic_experiment":
  401. if not (data.get("projectTitle") or "").strip():
  402. raise ValueError("请填写课题名称")
  403. if not (data.get("customerName") or "").strip():
  404. raise ValueError("请填写客户名称")
  405. if not (data.get("customerContact") or "").strip():
  406. raise ValueError("请填写客户联系人")
  407. if not (data.get("customerPhone") or "").strip():
  408. raise ValueError("请填写客户联系方式")
  409. project_title = (data.get("projectTitle") or "").strip()
  410. customer_name = (data.get("customerName") or "").strip()
  411. customer_contact = (data.get("customerContact") or "").strip()
  412. customer_phone = (data.get("customerPhone") or "").strip()
  413. channel_service_order_id = ""
  414. channel_id = ""
  415. channel_name = ""
  416. consent_form_urls = None
  417. elif not channel_service_order_id and biz_type != "clinical_application":
  418. raise ValueError("请选择渠道服务订单")
  419. # Validate service order exists and is active
  420. svc_order = None
  421. if channel_service_order_id:
  422. svc_order = (
  423. db.query(BizChannelServiceOrder)
  424. .filter(
  425. BizChannelServiceOrder.id == channel_service_order_id,
  426. BizChannelServiceOrder.del_flag == 0,
  427. )
  428. .first()
  429. )
  430. if not svc_order:
  431. raise ValueError("渠道服务订单不存在")
  432. if svc_order.order_status != "sxz":
  433. raise ValueError("渠道服务订单未生效,无法创建中试申请")
  434. if biz_type == "clinical_application":
  435. if svc_order:
  436. channel_id = svc_order.channel_id or channel_id
  437. channel_name = svc_order.channel_name or channel_name
  438. if not channel_id:
  439. raise ValueError("请选择渠道")
  440. if not channel_name:
  441. ch = (
  442. db.query(BizChannel)
  443. .filter(
  444. BizChannel.id == channel_id,
  445. BizChannel.tenant_no == tenant_no,
  446. BizChannel.del_flag == 0,
  447. )
  448. .first()
  449. )
  450. if not ch:
  451. raise ValueError("渠道不存在")
  452. channel_name = ch.channel_name or ""
  453. _resolve_application_attachment_payload(data)
  454. now = datetime.now()
  455. application_no = _generate_application_no(db, tenant_no, biz_type=biz_type)
  456. row = BizPilotApplication(
  457. id=str(uuid.uuid4()),
  458. application_no=application_no,
  459. application_name=data.get("applicationName") or "",
  460. project_title=project_title or None,
  461. customer_name=customer_name or None,
  462. customer_contact=customer_contact or None,
  463. customer_phone=customer_phone or None,
  464. application_status="dks",
  465. channel_service_order_id=channel_service_order_id or "",
  466. channel_id=(
  467. (svc_order.channel_id or "" if svc_order else "")
  468. or channel_id
  469. ),
  470. channel_name=(
  471. (svc_order.channel_name or "" if svc_order else "")
  472. or channel_name
  473. ),
  474. tenant_no=tenant_no,
  475. order_num=0,
  476. remark=data.get("remark") or "",
  477. consent_form_urls=consent_form_urls,
  478. attachment_urls=data.get("attachmentUrls") or None,
  479. create_by=user_id,
  480. create_dept_id=dept_id or "",
  481. create_time=now,
  482. update_by=user_id,
  483. update_time=now,
  484. del_flag=0,
  485. biz_type=biz_type,
  486. )
  487. db.add(row)
  488. db.commit()
  489. db.refresh(row)
  490. return _row_to_dict_application(row, db, tenant_no)
  491. def update_application(db: Session, app_id: str, data: dict, tenant_no: str) -> Dict[str, Any]:
  492. row = (
  493. db.query(BizPilotApplication)
  494. .filter(
  495. BizPilotApplication.id == app_id,
  496. BizPilotApplication.tenant_no == tenant_no,
  497. BizPilotApplication.del_flag == 0,
  498. )
  499. .first()
  500. )
  501. if not row:
  502. raise ValueError("申请单不存在")
  503. if row.application_status not in ("dks", "jxz"):
  504. raise ValueError("当前状态不允许编辑")
  505. _resolve_application_attachment_payload(data)
  506. if "remark" in data and data["remark"] is not None:
  507. row.remark = data["remark"]
  508. if "applicationName" in data and data["applicationName"] is not None:
  509. row.application_name = data["applicationName"]
  510. if "projectTitle" in data and data["projectTitle"] is not None:
  511. row.project_title = (data["projectTitle"] or "").strip() or None
  512. if "customerName" in data and data["customerName"] is not None:
  513. row.customer_name = (data["customerName"] or "").strip() or None
  514. if "customerContact" in data and data["customerContact"] is not None:
  515. row.customer_contact = (data["customerContact"] or "").strip() or None
  516. if "customerPhone" in data and data["customerPhone"] is not None:
  517. row.customer_phone = (data["customerPhone"] or "").strip() or None
  518. if "consentFormUrls" in data:
  519. row.consent_form_urls = data.get("consentFormUrls") or None
  520. if "attachmentUrls" in data:
  521. row.attachment_urls = data.get("attachmentUrls") or None
  522. db.commit()
  523. db.refresh(row)
  524. return _row_to_dict_application(row, db, tenant_no)
  525. def soft_delete_application(db: Session, app_id: str, tenant_no: str) -> None:
  526. row = (
  527. db.query(BizPilotApplication)
  528. .filter(
  529. BizPilotApplication.id == app_id,
  530. BizPilotApplication.tenant_no == tenant_no,
  531. BizPilotApplication.del_flag == 0,
  532. )
  533. .first()
  534. )
  535. if not row:
  536. raise ValueError("申请单不存在")
  537. if row.application_status != "dks":
  538. raise ValueError("当前状态不允许删除")
  539. row.application_no = _release_application_no_for_soft_delete(row.application_no or "", row.id)
  540. row.del_flag = 1
  541. row.update_time = datetime.now()
  542. db.commit()
  543. def start_application(db: Session, app_id: str, tenant_no: str, user_id: str) -> Dict[str, Any]:
  544. row = (
  545. db.query(BizPilotApplication)
  546. .filter(
  547. BizPilotApplication.id == app_id,
  548. BizPilotApplication.tenant_no == tenant_no,
  549. BizPilotApplication.del_flag == 0,
  550. )
  551. .first()
  552. )
  553. if not row:
  554. raise ValueError("申请单不存在")
  555. if row.application_status != "dks":
  556. raise ValueError("当前状态不允许开始")
  557. row.application_status = "jxz"
  558. row.update_by = user_id
  559. row.update_time = datetime.now()
  560. db.commit()
  561. db.refresh(row)
  562. return _row_to_dict_application(row, db, tenant_no)
  563. def complete_application(db: Session, app_id: str, tenant_no: str, user_id: str) -> Dict[str, Any]:
  564. row = (
  565. db.query(BizPilotApplication)
  566. .filter(
  567. BizPilotApplication.id == app_id,
  568. BizPilotApplication.tenant_no == tenant_no,
  569. BizPilotApplication.del_flag == 0,
  570. )
  571. .first()
  572. )
  573. if not row:
  574. raise ValueError("申请单不存在")
  575. if row.application_status != "jxz":
  576. raise ValueError("当前状态不允许完成")
  577. row.application_status = "ywc"
  578. row.update_by = user_id
  579. row.update_time = datetime.now()
  580. db.commit()
  581. db.refresh(row)
  582. return _row_to_dict_application(row, db, tenant_no)
  583. def void_application(db: Session, app_id: str, tenant_no: str, user_id: str) -> Dict[str, Any]:
  584. row = (
  585. db.query(BizPilotApplication)
  586. .filter(
  587. BizPilotApplication.id == app_id,
  588. BizPilotApplication.tenant_no == tenant_no,
  589. BizPilotApplication.del_flag == 0,
  590. )
  591. .first()
  592. )
  593. if not row:
  594. raise ValueError("申请单不存在")
  595. if row.application_status not in ("dks", "jxz"):
  596. raise ValueError("当前状态不允许作废")
  597. row.application_status = "yzf"
  598. row.update_by = user_id
  599. row.update_time = datetime.now()
  600. db.commit()
  601. db.refresh(row)
  602. return _row_to_dict_application(row, db, tenant_no)
  603. # ────────────────────────────────────────────
  604. # Patient CRUD
  605. # ────────────────────────────────────────────
  606. def get_patient_by_application(
  607. db: Session, app_id: str, tenant_no: str
  608. ) -> Optional[Dict[str, Any]]:
  609. row = (
  610. db.query(BizPilotPatient)
  611. .filter(
  612. BizPilotPatient.pilot_application_id == app_id,
  613. BizPilotPatient.tenant_no == tenant_no,
  614. BizPilotPatient.del_flag == 0,
  615. )
  616. .first()
  617. )
  618. if not row:
  619. return None
  620. return _row_to_dict_patient(row)
  621. def save_patient(
  622. db: Session,
  623. app_id: str,
  624. data: dict,
  625. tenant_no: str,
  626. user_id: str,
  627. dept_id: str,
  628. ) -> Dict[str, Any]:
  629. now = datetime.now()
  630. customer_id = data.get("customerId") or ""
  631. if customer_id:
  632. customer = (
  633. db.query(BizCustomer)
  634. .filter(BizCustomer.id == customer_id, BizCustomer.del_flag == 0)
  635. .first()
  636. )
  637. if customer:
  638. if data.get("patientName"):
  639. customer.customer_name = data["patientName"]
  640. if data.get("sex"):
  641. customer.sex = data["sex"]
  642. if data.get("age") is not None:
  643. customer.age = data["age"]
  644. if data.get("idCardNo"):
  645. customer.id_card_no = data["idCardNo"]
  646. if data.get("phoneNo"):
  647. customer.phone_no = data["phoneNo"]
  648. customer.update_by = user_id
  649. customer.update_time = now
  650. else:
  651. customer_id = str(uuid.uuid4())
  652. customer = BizCustomer(
  653. id=customer_id,
  654. customer_name=data.get("patientName") or "",
  655. sex=data.get("sex") or "2",
  656. id_card_no=data.get("idCardNo") or "",
  657. age=data.get("age"),
  658. phone_no=data.get("phoneNo") or "",
  659. tenant_no=tenant_no,
  660. order_num=0,
  661. remark="",
  662. create_by=user_id,
  663. create_dept_id=dept_id or "",
  664. create_time=now,
  665. update_by=user_id,
  666. update_time=now,
  667. del_flag=0,
  668. )
  669. db.add(customer)
  670. db.flush()
  671. existing = (
  672. db.query(BizPilotPatient)
  673. .filter(
  674. BizPilotPatient.pilot_application_id == app_id,
  675. BizPilotPatient.tenant_no == tenant_no,
  676. BizPilotPatient.del_flag == 0,
  677. )
  678. .first()
  679. )
  680. if existing:
  681. existing.customer_id = customer_id
  682. existing.patient_name = data.get("patientName") or ""
  683. existing.sex = data.get("sex") or ""
  684. existing.age = data.get("age")
  685. existing.id_card_no = data.get("idCardNo") or ""
  686. existing.phone_no = data.get("phoneNo") or ""
  687. existing.emergency_contact = data.get("emergencyContact") or ""
  688. existing.emergency_phone = data.get("emergencyPhone") or ""
  689. existing.clinical_diagnosis = data.get("clinicalDiagnosis") or ""
  690. existing.diagnosis_attachments = data.get("diagnosisAttachments") or ""
  691. existing.need_checkup = data.get("needCheckup") if data.get("needCheckup") is not None else 1
  692. existing.remark = data.get("remark") or ""
  693. existing.update_by = user_id
  694. existing.update_time = now
  695. else:
  696. existing = BizPilotPatient(
  697. id=str(uuid.uuid4()),
  698. pilot_application_id=app_id,
  699. customer_id=customer_id,
  700. patient_name=data.get("patientName") or "",
  701. sex=data.get("sex") or "",
  702. age=data.get("age"),
  703. id_card_no=data.get("idCardNo") or "",
  704. phone_no=data.get("phoneNo") or "",
  705. emergency_contact=data.get("emergencyContact") or "",
  706. emergency_phone=data.get("emergencyPhone") or "",
  707. clinical_diagnosis=data.get("clinicalDiagnosis") or "",
  708. diagnosis_attachments=data.get("diagnosisAttachments") or "",
  709. need_checkup=data.get("needCheckup") if data.get("needCheckup") is not None else 1,
  710. tenant_no=tenant_no,
  711. order_num=0,
  712. remark=data.get("remark") or "",
  713. create_by=user_id,
  714. create_dept_id=dept_id or "",
  715. create_time=now,
  716. update_by=user_id,
  717. update_time=now,
  718. del_flag=0,
  719. )
  720. db.add(existing)
  721. db.commit()
  722. db.refresh(existing)
  723. need_checkup = data.get("needCheckup") if data.get("needCheckup") is not None else 1
  724. if need_checkup == 1:
  725. pilot_checkup.ensure_checkup_for_patient(
  726. db, patient_id=existing.id,
  727. application_id=app_id, tenant_no=tenant_no, user_id=user_id,
  728. )
  729. else:
  730. pilot_checkup.remove_checkup_for_patient(
  731. db, patient_id=existing.id, tenant_no=tenant_no,
  732. )
  733. db.commit()
  734. return _row_to_dict_patient(existing)
  735. # ────────────────────────────────────────────
  736. # InfusionPlan CRUD
  737. # ────────────────────────────────────────────
  738. def list_infusion_plans(
  739. db: Session, app_id: str, tenant_no: str
  740. ) -> List[Dict[str, Any]]:
  741. rows = (
  742. db.query(BizPilotInfusionPlan)
  743. .filter(
  744. BizPilotInfusionPlan.pilot_application_id == app_id,
  745. BizPilotInfusionPlan.tenant_no == tenant_no,
  746. BizPilotInfusionPlan.del_flag == 0,
  747. )
  748. .order_by(BizPilotInfusionPlan.order_num.asc(), BizPilotInfusionPlan.create_time.asc())
  749. .all()
  750. )
  751. return [_row_to_dict_infusion_plan(r, db, tenant_no) for r in rows]
  752. def create_infusion_plan(
  753. db: Session,
  754. app_id: str,
  755. data: dict,
  756. tenant_no: str,
  757. user_id: str,
  758. dept_id: str,
  759. ) -> Dict[str, Any]:
  760. now = datetime.now()
  761. row = BizPilotInfusionPlan(
  762. id=str(uuid.uuid4()),
  763. pilot_application_id=app_id,
  764. cell_type=data.get("cellType") or "",
  765. spec_text=data.get("specText") or "",
  766. total_course_count=data.get("totalCourseCount") or 0,
  767. course_interval=data.get("courseInterval") or "",
  768. infusion_route=data.get("infusionRoute") or "",
  769. infusion_start_date=_normalize_yyyymmdd(data.get("infusionStartDate")),
  770. channel_agreement_id=data.get("channelAgreementId") or "",
  771. tenant_no=tenant_no,
  772. order_num=data.get("orderNum") or 0,
  773. remark=data.get("remark") or "",
  774. create_by=user_id,
  775. create_dept_id=dept_id or "",
  776. create_time=now,
  777. update_by=user_id,
  778. update_time=now,
  779. del_flag=0,
  780. )
  781. db.add(row)
  782. db.commit()
  783. db.refresh(row)
  784. return _row_to_dict_infusion_plan(row, db, tenant_no)
  785. _INFUSION_STATUS_CN_TO_CODE = {
  786. "待输注": "dtz",
  787. "已用户确认": "yrqr",
  788. "已取消": "yqx",
  789. "已完成": "ywc",
  790. }
  791. def _normalize_infusion_status_code(raw: Any) -> str:
  792. s = (str(raw).strip() if raw is not None else "") or ""
  793. if not s:
  794. return ""
  795. return _INFUSION_STATUS_CN_TO_CODE.get(s, s)
  796. def _schedule_infusion_editable(row: BizPilotInfusionSchedule) -> bool:
  797. return (row.infusion_status or "") in ("dtz", "yrqr")
  798. def _schedule_locked(row: BizPilotInfusionSchedule) -> bool:
  799. return (row.infusion_status or "") in ("ywc", "yqx")
  800. def _schedule_has_blocking_links(
  801. db: Session, schedule_id: str, tenant_no: str
  802. ) -> bool:
  803. delivery = (
  804. db.query(BizPilotDelivery.id)
  805. .filter(
  806. BizPilotDelivery.infusion_schedule_id == schedule_id,
  807. BizPilotDelivery.tenant_no == tenant_no,
  808. BizPilotDelivery.del_flag == 0,
  809. )
  810. .first()
  811. )
  812. if delivery:
  813. return True
  814. confirm = (
  815. db.query(BizPilotInfusionConfirm.id)
  816. .filter(
  817. BizPilotInfusionConfirm.infusion_schedule_id == schedule_id,
  818. BizPilotInfusionConfirm.tenant_no == tenant_no,
  819. BizPilotInfusionConfirm.del_flag == 0,
  820. )
  821. .first()
  822. )
  823. return confirm is not None
  824. def _schedule_deletable(
  825. db: Session, row: BizPilotInfusionSchedule, tenant_no: str
  826. ) -> tuple[bool, str]:
  827. label = f"第{row.order_num or '?'}疗程"
  828. st = row.infusion_status or ""
  829. if st in ("ywc", "yqx"):
  830. return False, f"{label}已完成或已取消,不可删除"
  831. if st != "dtz":
  832. return False, f"{label}仅待输注状态可删除"
  833. if (row.schedule_status or "") != "dsc":
  834. return False, f"{label}生产已开始,不可删除"
  835. wo = (
  836. db.query(BizProductionWorkOrder)
  837. .filter(
  838. BizProductionWorkOrder.infusion_schedule_id == row.id,
  839. BizProductionWorkOrder.tenant_no == tenant_no,
  840. BizProductionWorkOrder.del_flag == 0,
  841. BizProductionWorkOrder.work_order_type == "outbound",
  842. )
  843. .first()
  844. )
  845. if wo and (wo.work_order_status or "") not in ("dpz", "yzf"):
  846. return False, f"{label}关联出库工单进行中,不可删除"
  847. if _schedule_has_blocking_links(db, row.id, tenant_no):
  848. return False, f"{label}已有关联交付或确认记录,不可删除"
  849. return True, ""
  850. def _get_schedule_for_plan(
  851. db: Session,
  852. schedule_id: str,
  853. plan_id: str,
  854. tenant_no: str,
  855. ) -> BizPilotInfusionSchedule:
  856. row = (
  857. db.query(BizPilotInfusionSchedule)
  858. .filter(
  859. BizPilotInfusionSchedule.id == schedule_id,
  860. BizPilotInfusionSchedule.infusion_plan_id == plan_id,
  861. BizPilotInfusionSchedule.tenant_no == tenant_no,
  862. BizPilotInfusionSchedule.del_flag == 0,
  863. )
  864. .first()
  865. )
  866. if not row:
  867. raise ValueError("输注计划不存在或不属于当前申请单")
  868. return row
  869. def _prepare_infusion_plan_payload(
  870. plan_data: Optional[dict],
  871. schedules_data: Optional[List[dict]],
  872. ) -> dict:
  873. plan_payload = dict(plan_data or {})
  874. cell_type = plan_payload.get("cellType") or ""
  875. spec_text = plan_payload.get("specText") or ""
  876. if not cell_type and schedules_data:
  877. for sched in schedules_data:
  878. for cp in sched.get("cellProducts") or []:
  879. if cp.get("cellTypeName"):
  880. cell_type = cp.get("cellTypeName")
  881. spec_text = cp.get("specText") or spec_text
  882. break
  883. if cell_type:
  884. break
  885. plan_payload.setdefault("cellType", cell_type or "-")
  886. plan_payload.setdefault("specText", spec_text)
  887. plan_payload.setdefault(
  888. "totalCourseCount",
  889. plan_payload.get("totalCourseCount") or len(schedules_data or []),
  890. )
  891. return plan_payload
  892. def update_infusion_plan(
  893. db: Session, plan_id: str, data: dict, tenant_no: str, *, commit: bool = True
  894. ) -> Dict[str, Any]:
  895. row = (
  896. db.query(BizPilotInfusionPlan)
  897. .filter(
  898. BizPilotInfusionPlan.id == plan_id,
  899. BizPilotInfusionPlan.tenant_no == tenant_no,
  900. BizPilotInfusionPlan.del_flag == 0,
  901. )
  902. .first()
  903. )
  904. if not row:
  905. raise ValueError("输注方案不存在")
  906. fields = {
  907. "cellType": "cell_type",
  908. "specText": "spec_text",
  909. "totalCourseCount": "total_course_count",
  910. "courseInterval": "course_interval",
  911. "infusionRoute": "infusion_route",
  912. "infusionStartDate": "infusion_start_date",
  913. "channelAgreementId": "channel_agreement_id",
  914. "remark": "remark",
  915. }
  916. for camel, snake in fields.items():
  917. if camel == "infusionStartDate" and data[camel] is not None:
  918. setattr(row, snake, _normalize_yyyymmdd(data[camel]))
  919. continue
  920. if camel in data and data[camel] is not None:
  921. setattr(row, snake, data[camel])
  922. row.update_time = datetime.now()
  923. if commit:
  924. db.commit()
  925. db.refresh(row)
  926. else:
  927. db.flush()
  928. return _row_to_dict_infusion_plan(row, db, tenant_no)
  929. def soft_delete_infusion_plan(db: Session, plan_id: str, tenant_no: str) -> None:
  930. now = datetime.now()
  931. row = (
  932. db.query(BizPilotInfusionPlan)
  933. .filter(
  934. BizPilotInfusionPlan.id == plan_id,
  935. BizPilotInfusionPlan.tenant_no == tenant_no,
  936. BizPilotInfusionPlan.del_flag == 0,
  937. )
  938. .first()
  939. )
  940. if not row:
  941. raise ValueError("输注方案不存在")
  942. row.del_flag = 1
  943. row.update_time = now
  944. schedule_ids = [
  945. r[0]
  946. for r in db.query(BizPilotInfusionSchedule.id)
  947. .filter(
  948. BizPilotInfusionSchedule.infusion_plan_id == plan_id,
  949. BizPilotInfusionSchedule.tenant_no == tenant_no,
  950. BizPilotInfusionSchedule.del_flag == 0,
  951. )
  952. .all()
  953. ]
  954. if schedule_ids:
  955. db.query(BizPilotInfusionSchedule).filter(
  956. BizPilotInfusionSchedule.id.in_(schedule_ids),
  957. ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
  958. db.query(BizPilotInfusionScheduleProduct).filter(
  959. BizPilotInfusionScheduleProduct.infusion_schedule_id.in_(schedule_ids),
  960. BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
  961. BizPilotInfusionScheduleProduct.del_flag == 0,
  962. ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
  963. db.commit()
  964. # ────────────────────────────────────────────
  965. # InfusionSchedule CRUD
  966. # ────────────────────────────────────────────
  967. def list_schedules_by_plan(
  968. db: Session, plan_id: str, tenant_no: str
  969. ) -> List[Dict[str, Any]]:
  970. rows = (
  971. db.query(BizPilotInfusionSchedule)
  972. .filter(
  973. BizPilotInfusionSchedule.infusion_plan_id == plan_id,
  974. BizPilotInfusionSchedule.tenant_no == tenant_no,
  975. BizPilotInfusionSchedule.del_flag == 0,
  976. )
  977. .order_by(BizPilotInfusionSchedule.order_num.asc())
  978. .all()
  979. )
  980. if not rows:
  981. return []
  982. schedule_ids = [r.id for r in rows]
  983. product_rows = (
  984. db.query(BizPilotInfusionScheduleProduct)
  985. .filter(
  986. BizPilotInfusionScheduleProduct.infusion_schedule_id.in_(schedule_ids),
  987. BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
  988. BizPilotInfusionScheduleProduct.del_flag == 0,
  989. )
  990. .order_by(BizPilotInfusionScheduleProduct.order_num.asc())
  991. .all()
  992. )
  993. products_by_schedule: Dict[str, List[Dict[str, Any]]] = {}
  994. for p in product_rows:
  995. products_by_schedule.setdefault(p.infusion_schedule_id, []).append(
  996. _row_to_dict_schedule_product(p)
  997. )
  998. return [
  999. _row_to_dict_infusion_schedule(r, products_by_schedule.get(r.id, []))
  1000. for r in rows
  1001. ]
  1002. def auto_generate_schedules(
  1003. db: Session,
  1004. plan_id: str,
  1005. tenant_no: str,
  1006. user_id: str,
  1007. dept_id: str,
  1008. ) -> Dict[str, Any]:
  1009. plan = (
  1010. db.query(BizPilotInfusionPlan)
  1011. .filter(
  1012. BizPilotInfusionPlan.id == plan_id,
  1013. BizPilotInfusionPlan.tenant_no == tenant_no,
  1014. BizPilotInfusionPlan.del_flag == 0,
  1015. )
  1016. .first()
  1017. )
  1018. if not plan:
  1019. raise ValueError("输注方案不存在")
  1020. # 追溯申请单获取正确的 biz_type
  1021. _app = (
  1022. db.query(BizPilotApplication)
  1023. .filter(
  1024. BizPilotApplication.id == plan.pilot_application_id,
  1025. BizPilotApplication.del_flag == 0,
  1026. )
  1027. .first()
  1028. )
  1029. schedule_biz_type = (_app.biz_type if _app else None) or "pilot_service"
  1030. total = plan.total_course_count or 0
  1031. if total <= 0:
  1032. raise ValueError("总疗程数必须大于0")
  1033. now = datetime.now()
  1034. for i in range(1, total + 1):
  1035. sched = BizPilotInfusionSchedule(
  1036. id=str(uuid.uuid4()),
  1037. infusion_plan_id=plan_id,
  1038. cell_type=plan.cell_type or "",
  1039. spec_text=plan.spec_text or "",
  1040. schedule_status="dsc",
  1041. infusion_status="dtz",
  1042. qc_status="dzc",
  1043. tenant_no=tenant_no,
  1044. order_num=i,
  1045. remark="",
  1046. create_by=user_id,
  1047. create_dept_id=dept_id or "",
  1048. create_time=now,
  1049. update_by=user_id,
  1050. update_time=now,
  1051. del_flag=0,
  1052. )
  1053. db.add(sched)
  1054. from app.crud import production_work_order as pwo_svc
  1055. pwo_svc.create_outbound_production_work_order(
  1056. db,
  1057. schedule_id=sched.id,
  1058. tenant_no=tenant_no,
  1059. user_id=user_id,
  1060. create_dept_id=dept_id,
  1061. biz_type=schedule_biz_type,
  1062. )
  1063. from app.crud import qc_work_order as qc_svc
  1064. qc_svc.create_outbound_qc_work_order(
  1065. db,
  1066. schedule_id=sched.id,
  1067. tenant_no=tenant_no,
  1068. user_id=user_id,
  1069. create_dept_id=dept_id,
  1070. biz_type=schedule_biz_type,
  1071. )
  1072. db.commit()
  1073. return {"generatedCount": total}
  1074. def _create_schedule_products(
  1075. db: Session,
  1076. schedule_id: str,
  1077. products: List[dict],
  1078. tenant_no: str,
  1079. user_id: str,
  1080. dept_id: str,
  1081. now: datetime,
  1082. ) -> List[Dict[str, Any]]:
  1083. result: List[Dict[str, Any]] = []
  1084. for idx, item in enumerate(products or []):
  1085. if not item:
  1086. continue
  1087. row = BizPilotInfusionScheduleProduct(
  1088. id=str(uuid.uuid4()),
  1089. infusion_schedule_id=schedule_id,
  1090. price_id=item.get("priceId") or "",
  1091. project_code=item.get("projectCode") or "",
  1092. project_name=item.get("projectName") or "",
  1093. cell_type_name=item.get("cellTypeName") or "",
  1094. spec_text=item.get("specText") or "",
  1095. agreement_price=item.get("agreementPrice"),
  1096. actual_price=item.get("actualPrice"),
  1097. quantity=item.get("quantity") if item.get("quantity") is not None else 1,
  1098. product_status=item.get("status") or item.get("productStatus") or "dtz",
  1099. tenant_no=tenant_no,
  1100. order_num=item.get("sortNo") or (idx + 1),
  1101. remark=item.get("remark") or "",
  1102. create_by=user_id,
  1103. create_dept_id=dept_id or "",
  1104. create_time=now,
  1105. update_by=user_id,
  1106. update_time=now,
  1107. del_flag=0,
  1108. )
  1109. db.add(row)
  1110. result.append(_row_to_dict_schedule_product(row))
  1111. return result
  1112. def create_schedule(
  1113. db: Session,
  1114. plan_id: str,
  1115. data: dict,
  1116. tenant_no: str,
  1117. user_id: str,
  1118. dept_id: str,
  1119. *,
  1120. commit: bool = True,
  1121. ) -> Dict[str, Any]:
  1122. now = datetime.now()
  1123. schedule_id = str(uuid.uuid4())
  1124. cell_products = data.get("cellProducts") or []
  1125. first_product = cell_products[0] if cell_products else {}
  1126. # 追溯申请单获取正确的 biz_type
  1127. _plan = (
  1128. db.query(BizPilotInfusionPlan)
  1129. .filter(BizPilotInfusionPlan.id == plan_id, BizPilotInfusionPlan.del_flag == 0)
  1130. .first()
  1131. )
  1132. _app = (
  1133. db.query(BizPilotApplication)
  1134. .filter(
  1135. BizPilotApplication.id == (_plan.pilot_application_id if _plan else None),
  1136. BizPilotApplication.del_flag == 0,
  1137. )
  1138. .first()
  1139. ) if _plan else None
  1140. schedule_biz_type = (_app.biz_type if _app else None) or "pilot_service"
  1141. row = BizPilotInfusionSchedule(
  1142. id=schedule_id,
  1143. infusion_plan_id=plan_id,
  1144. cell_type=data.get("cellType") or first_product.get("cellTypeName") or "",
  1145. spec_text=data.get("specText") or first_product.get("specText") or "",
  1146. infusion_time=_parse_infusion_datetime(data.get("infusionTime")),
  1147. infusion_location=data.get("infusionLocation") or "",
  1148. infusion_method=data.get("infusionMethod") or "",
  1149. schedule_status=data.get("scheduleStatus") or "dsc",
  1150. infusion_status=data.get("infusionStatus") or "dtz",
  1151. qc_status=data.get("qcStatus") or "dzc",
  1152. attachments=data.get("attachments") or "",
  1153. amount=data.get("amount") if data.get("amount") else None,
  1154. tenant_no=tenant_no,
  1155. order_num=data.get("sortNo") or data.get("orderNum") or 0,
  1156. remark=data.get("remark") or "",
  1157. create_by=user_id,
  1158. create_dept_id=dept_id or "",
  1159. create_time=now,
  1160. update_by=user_id,
  1161. update_time=now,
  1162. del_flag=0,
  1163. )
  1164. db.add(row)
  1165. products = _create_schedule_products(
  1166. db, schedule_id, cell_products, tenant_no, user_id, dept_id, now
  1167. )
  1168. # 自动创建出库生产工单
  1169. from app.crud import production_work_order as pwo_svc
  1170. pwo_svc.create_outbound_production_work_order(
  1171. db,
  1172. schedule_id=schedule_id,
  1173. tenant_no=tenant_no,
  1174. user_id=user_id,
  1175. create_dept_id=dept_id,
  1176. biz_type=schedule_biz_type,
  1177. )
  1178. # 自动创建出库质检工单
  1179. from app.crud import qc_work_order as qc_svc
  1180. qc_svc.create_outbound_qc_work_order(
  1181. db,
  1182. schedule_id=schedule_id,
  1183. tenant_no=tenant_no,
  1184. user_id=user_id,
  1185. create_dept_id=dept_id,
  1186. biz_type=schedule_biz_type,
  1187. )
  1188. # 临床应用自动创建缴费记录
  1189. plan_row = _plan
  1190. if plan_row:
  1191. application = _app
  1192. if (
  1193. application
  1194. and application.biz_type in ("clinical_application", "basic_experiment")
  1195. and data.get("amount")
  1196. ):
  1197. from app.crud.clinical_payment import auto_create_payment
  1198. auto_create_payment(
  1199. db,
  1200. application_id=application.id,
  1201. schedule_id=schedule_id,
  1202. amount=data["amount"],
  1203. tenant_no=tenant_no,
  1204. user_id=user_id,
  1205. dept_id=dept_id,
  1206. biz_type=application.biz_type,
  1207. )
  1208. if commit:
  1209. db.commit()
  1210. db.refresh(row)
  1211. return _row_to_dict_infusion_schedule(row, products)
  1212. def _update_schedule_editable(
  1213. db: Session,
  1214. row: BizPilotInfusionSchedule,
  1215. data: dict,
  1216. tenant_no: str,
  1217. user_id: str,
  1218. ) -> None:
  1219. cell_products = data.get("cellProducts") or []
  1220. first_product = cell_products[0] if cell_products else {}
  1221. if data.get("infusionTime") is not None:
  1222. row.infusion_time = _parse_infusion_datetime(data.get("infusionTime"))
  1223. if data.get("infusionLocation") is not None:
  1224. row.infusion_location = data.get("infusionLocation") or ""
  1225. if data.get("infusionMethod") is not None:
  1226. row.infusion_method = data.get("infusionMethod") or ""
  1227. if data.get("sortNo") is not None or data.get("orderNum") is not None:
  1228. row.order_num = data.get("sortNo") or data.get("orderNum") or row.order_num
  1229. if cell_products:
  1230. row.cell_type = data.get("cellType") or first_product.get("cellTypeName") or row.cell_type
  1231. row.spec_text = data.get("specText") or first_product.get("specText") or row.spec_text
  1232. if data.get("remark") is not None:
  1233. row.remark = data.get("remark") or ""
  1234. row.update_by = user_id
  1235. row.update_time = datetime.now()
  1236. def _upsert_schedule_products(
  1237. db: Session,
  1238. schedule_row: BizPilotInfusionSchedule,
  1239. products: List[dict],
  1240. tenant_no: str,
  1241. user_id: str,
  1242. dept_id: str,
  1243. ) -> None:
  1244. now = datetime.now()
  1245. schedule_id = schedule_row.id
  1246. existing_rows = (
  1247. db.query(BizPilotInfusionScheduleProduct)
  1248. .filter(
  1249. BizPilotInfusionScheduleProduct.infusion_schedule_id == schedule_id,
  1250. BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
  1251. BizPilotInfusionScheduleProduct.del_flag == 0,
  1252. )
  1253. .all()
  1254. )
  1255. existing_by_id = {r.id: r for r in existing_rows}
  1256. keep_ids: set[str] = set()
  1257. default_status = schedule_row.infusion_status or "dtz"
  1258. for idx, raw in enumerate(products or []):
  1259. if not raw:
  1260. continue
  1261. item = dict(raw)
  1262. pid = (item.get("id") or "").strip()
  1263. if pid and pid in existing_by_id:
  1264. prow = existing_by_id[pid]
  1265. keep_ids.add(pid)
  1266. prow.price_id = item.get("priceId") or ""
  1267. prow.project_code = item.get("projectCode") or ""
  1268. prow.project_name = item.get("projectName") or ""
  1269. prow.cell_type_name = item.get("cellTypeName") or ""
  1270. prow.spec_text = item.get("specText") or ""
  1271. if item.get("agreementPrice") is not None:
  1272. prow.agreement_price = item.get("agreementPrice")
  1273. if item.get("actualPrice") is not None:
  1274. prow.actual_price = item.get("actualPrice")
  1275. if item.get("quantity") is not None:
  1276. prow.quantity = item.get("quantity")
  1277. prow.order_num = item.get("sortNo") or (idx + 1)
  1278. prow.remark = item.get("remark") or ""
  1279. prow.update_by = user_id
  1280. prow.update_time = now
  1281. continue
  1282. status_raw = item.get("status") or item.get("productStatus")
  1283. product_status = _normalize_infusion_status_code(status_raw) or default_status
  1284. row = BizPilotInfusionScheduleProduct(
  1285. id=str(uuid.uuid4()),
  1286. infusion_schedule_id=schedule_id,
  1287. price_id=item.get("priceId") or "",
  1288. project_code=item.get("projectCode") or "",
  1289. project_name=item.get("projectName") or "",
  1290. cell_type_name=item.get("cellTypeName") or "",
  1291. spec_text=item.get("specText") or "",
  1292. agreement_price=item.get("agreementPrice"),
  1293. actual_price=item.get("actualPrice"),
  1294. quantity=item.get("quantity") if item.get("quantity") is not None else 1,
  1295. product_status=product_status,
  1296. tenant_no=tenant_no,
  1297. order_num=item.get("sortNo") or (idx + 1),
  1298. remark=item.get("remark") or "",
  1299. create_by=user_id,
  1300. create_dept_id=dept_id or "",
  1301. create_time=now,
  1302. update_by=user_id,
  1303. update_time=now,
  1304. del_flag=0,
  1305. )
  1306. db.add(row)
  1307. for eid, erow in existing_by_id.items():
  1308. if eid not in keep_ids:
  1309. erow.del_flag = 1
  1310. erow.update_by = user_id
  1311. erow.update_time = now
  1312. def _update_schedule_order_only(
  1313. db: Session,
  1314. row: BizPilotInfusionSchedule,
  1315. data: dict,
  1316. user_id: str,
  1317. ) -> None:
  1318. if data.get("sortNo") is not None or data.get("orderNum") is not None:
  1319. row.order_num = data.get("sortNo") or data.get("orderNum") or row.order_num
  1320. row.update_by = user_id
  1321. row.update_time = datetime.now()
  1322. def upsert_application_infusion(
  1323. db: Session,
  1324. app_id: str,
  1325. plan_data: Optional[dict],
  1326. schedules_data: Optional[List[dict]],
  1327. tenant_no: str,
  1328. user_id: str,
  1329. dept_id: str,
  1330. ) -> None:
  1331. """编辑申请单:增量更新输注方案/计划,保留状态与 schedule id。"""
  1332. if not plan_data and not schedules_data:
  1333. return
  1334. plan_payload = _prepare_infusion_plan_payload(plan_data, schedules_data)
  1335. plans = list_infusion_plans(db, app_id, tenant_no)
  1336. plan_id = (plans[0].get("id") if plans else None) or ""
  1337. if plan_id:
  1338. update_infusion_plan(
  1339. db, plan_id, plan_payload, tenant_no, commit=False
  1340. )
  1341. else:
  1342. plan_result = create_infusion_plan(
  1343. db,
  1344. app_id=app_id,
  1345. data=plan_payload,
  1346. tenant_no=tenant_no,
  1347. user_id=user_id,
  1348. dept_id=dept_id,
  1349. )
  1350. plan_id = plan_result.get("id") or ""
  1351. if not plan_id:
  1352. raise ValueError("创建输注方案失败")
  1353. existing_schedules = list_schedules_by_plan(db, plan_id, tenant_no)
  1354. existing_ids = {s["id"] for s in existing_schedules if s.get("id")}
  1355. payload_ids: set[str] = set()
  1356. for idx, sched in enumerate(schedules_data or []):
  1357. sched_data = dict(sched)
  1358. sched_data["sortNo"] = sched_data.get("sortNo") or (idx + 1)
  1359. sid = (sched_data.get("id") or "").strip()
  1360. label = f"第{idx + 1}疗程"
  1361. if sid:
  1362. if sid not in existing_ids:
  1363. raise ValueError(f"{label}输注计划不存在或不属于当前申请单")
  1364. row = _get_schedule_for_plan(db, sid, plan_id, tenant_no)
  1365. payload_ids.add(sid)
  1366. if _schedule_locked(row):
  1367. _update_schedule_order_only(db, row, sched_data, user_id)
  1368. continue
  1369. if not _schedule_infusion_editable(row):
  1370. raise ValueError(f"{label}当前状态不允许编辑")
  1371. _update_schedule_editable(db, row, sched_data, tenant_no, user_id)
  1372. _upsert_schedule_products(
  1373. db, row, sched_data.get("cellProducts") or [], tenant_no, user_id, dept_id
  1374. )
  1375. continue
  1376. create_schedule(
  1377. db,
  1378. plan_id=plan_id,
  1379. data=sched_data,
  1380. tenant_no=tenant_no,
  1381. user_id=user_id,
  1382. dept_id=dept_id,
  1383. commit=False,
  1384. )
  1385. for sid in existing_ids - payload_ids:
  1386. row = _get_schedule_for_plan(db, sid, plan_id, tenant_no)
  1387. ok, msg = _schedule_deletable(db, row, tenant_no)
  1388. if not ok:
  1389. raise ValueError(msg)
  1390. soft_delete_schedule(db, sid, tenant_no, commit=False)
  1391. plan_row = (
  1392. db.query(BizPilotInfusionPlan)
  1393. .filter(
  1394. BizPilotInfusionPlan.id == plan_id,
  1395. BizPilotInfusionPlan.tenant_no == tenant_no,
  1396. BizPilotInfusionPlan.del_flag == 0,
  1397. )
  1398. .first()
  1399. )
  1400. if plan_row:
  1401. active_count = (
  1402. db.query(func.count(BizPilotInfusionSchedule.id))
  1403. .filter(
  1404. BizPilotInfusionSchedule.infusion_plan_id == plan_id,
  1405. BizPilotInfusionSchedule.tenant_no == tenant_no,
  1406. BizPilotInfusionSchedule.del_flag == 0,
  1407. )
  1408. .scalar()
  1409. ) or 0
  1410. plan_row.total_course_count = int(active_count)
  1411. plan_row.update_by = user_id
  1412. plan_row.update_time = datetime.now()
  1413. db.commit()
  1414. def _get_or_create_infusion_plan(
  1415. db: Session,
  1416. application_id: str,
  1417. tenant_no: str,
  1418. user_id: str,
  1419. dept_id: str,
  1420. ) -> BizPilotInfusionPlan:
  1421. """获取或创建申请单下的输注/售卖方案(基础实验批量创建售卖计划用)。"""
  1422. plans = list_infusion_plans(db, application_id, tenant_no)
  1423. if plans:
  1424. plan_row = (
  1425. db.query(BizPilotInfusionPlan)
  1426. .filter(
  1427. BizPilotInfusionPlan.id == plans[0].get("id"),
  1428. BizPilotInfusionPlan.tenant_no == tenant_no,
  1429. BizPilotInfusionPlan.del_flag == 0,
  1430. )
  1431. .first()
  1432. )
  1433. if plan_row:
  1434. return plan_row
  1435. plan_result = create_infusion_plan(
  1436. db,
  1437. app_id=application_id,
  1438. data={"cellType": "-", "totalCourseCount": 0},
  1439. tenant_no=tenant_no,
  1440. user_id=user_id,
  1441. dept_id=dept_id,
  1442. )
  1443. plan_id = plan_result.get("id") or ""
  1444. plan_row = (
  1445. db.query(BizPilotInfusionPlan)
  1446. .filter(
  1447. BizPilotInfusionPlan.id == plan_id,
  1448. BizPilotInfusionPlan.tenant_no == tenant_no,
  1449. BizPilotInfusionPlan.del_flag == 0,
  1450. )
  1451. .first()
  1452. )
  1453. if not plan_row:
  1454. raise ValueError("创建售卖方案失败")
  1455. return plan_row
  1456. def create_basic_experiment_sale_schedules(
  1457. db: Session,
  1458. application_id: str,
  1459. product_lines: list,
  1460. tenant_no: str,
  1461. user_id: str,
  1462. dept_id: str,
  1463. ) -> List[Dict[str, Any]]:
  1464. """基础实验:每个产品行创建一条售卖计划(一产品一计划)。"""
  1465. app = (
  1466. db.query(BizPilotApplication)
  1467. .filter(
  1468. BizPilotApplication.id == application_id,
  1469. BizPilotApplication.tenant_no == tenant_no,
  1470. BizPilotApplication.del_flag == 0,
  1471. )
  1472. .first()
  1473. )
  1474. if not app:
  1475. raise ValueError("申请单不存在")
  1476. if (app.biz_type or "") != "basic_experiment":
  1477. raise ValueError("仅基础实验申请单可批量创建售卖计划")
  1478. if not product_lines:
  1479. raise ValueError("请至少添加一行售卖产品")
  1480. plan = _get_or_create_infusion_plan(
  1481. db, application_id, tenant_no, user_id, dept_id
  1482. )
  1483. results: List[Dict[str, Any]] = []
  1484. for idx, line in enumerate(product_lines):
  1485. schedule_data = {
  1486. "cellType": line.get("cellTypeName") or "",
  1487. "specText": line.get("specText") or "",
  1488. "infusionTime": line.get("pickupTime"),
  1489. "amount": line.get("amount"),
  1490. "sortNo": idx + 1,
  1491. "cellProducts": [
  1492. {
  1493. "priceId": line.get("priceId"),
  1494. "projectCode": line.get("projectCode"),
  1495. "projectName": line.get("projectName"),
  1496. "cellTypeName": line.get("cellTypeName"),
  1497. "specText": line.get("specText"),
  1498. "agreementPrice": line.get("agreementPrice"),
  1499. "actualPrice": line.get("actualPrice"),
  1500. "quantity": line.get("quantity") or 1,
  1501. }
  1502. ],
  1503. }
  1504. results.append(
  1505. create_schedule(
  1506. db,
  1507. plan.id,
  1508. schedule_data,
  1509. tenant_no,
  1510. user_id,
  1511. dept_id,
  1512. commit=False,
  1513. )
  1514. )
  1515. active_count = (
  1516. db.query(func.count(BizPilotInfusionSchedule.id))
  1517. .filter(
  1518. BizPilotInfusionSchedule.infusion_plan_id == plan.id,
  1519. BizPilotInfusionSchedule.tenant_no == tenant_no,
  1520. BizPilotInfusionSchedule.del_flag == 0,
  1521. )
  1522. .scalar()
  1523. ) or 0
  1524. plan.total_course_count = int(active_count)
  1525. plan.update_by = user_id
  1526. plan.update_time = datetime.now()
  1527. db.commit()
  1528. return results
  1529. def save_application_infusion(
  1530. db: Session,
  1531. app_id: str,
  1532. plan_data: Optional[dict],
  1533. schedules_data: Optional[List[dict]],
  1534. tenant_no: str,
  1535. user_id: str,
  1536. dept_id: str,
  1537. *,
  1538. replace_existing: bool = False,
  1539. ) -> None:
  1540. if replace_existing:
  1541. for plan in list_infusion_plans(db, app_id, tenant_no):
  1542. plan_id = plan.get("id")
  1543. if plan_id:
  1544. soft_delete_infusion_plan(db, plan_id, tenant_no)
  1545. if not plan_data and not schedules_data:
  1546. return
  1547. plan_payload = _prepare_infusion_plan_payload(plan_data, schedules_data)
  1548. plan_result = create_infusion_plan(
  1549. db,
  1550. app_id=app_id,
  1551. data=plan_payload,
  1552. tenant_no=tenant_no,
  1553. user_id=user_id,
  1554. dept_id=dept_id,
  1555. )
  1556. plan_id = plan_result.get("id")
  1557. if not plan_id:
  1558. raise ValueError("创建输注方案失败")
  1559. for idx, sched in enumerate(schedules_data or []):
  1560. sched_data = dict(sched)
  1561. sched_data["sortNo"] = sched_data.get("sortNo") or (idx + 1)
  1562. create_schedule(
  1563. db,
  1564. plan_id=plan_id,
  1565. data=sched_data,
  1566. tenant_no=tenant_no,
  1567. user_id=user_id,
  1568. dept_id=dept_id,
  1569. commit=False,
  1570. )
  1571. db.commit()
  1572. def update_schedule(
  1573. db: Session, schedule_id: str, data: dict, tenant_no: str
  1574. ) -> Dict[str, Any]:
  1575. row = (
  1576. db.query(BizPilotInfusionSchedule)
  1577. .filter(
  1578. BizPilotInfusionSchedule.id == schedule_id,
  1579. BizPilotInfusionSchedule.tenant_no == tenant_no,
  1580. BizPilotInfusionSchedule.del_flag == 0,
  1581. )
  1582. .first()
  1583. )
  1584. if not row:
  1585. raise ValueError("输注计划不存在")
  1586. if "infusionTime" in data and data["infusionTime"] is not None:
  1587. row.infusion_time = data["infusionTime"]
  1588. if "remark" in data and data["remark"] is not None:
  1589. row.remark = data["remark"]
  1590. if "attachments" in data and data["attachments"] is not None:
  1591. row.attachments = data["attachments"]
  1592. row.update_time = datetime.now()
  1593. db.commit()
  1594. db.refresh(row)
  1595. return _row_to_dict_infusion_schedule(row)
  1596. def soft_delete_schedule(
  1597. db: Session, schedule_id: str, tenant_no: str, *, commit: bool = True
  1598. ) -> None:
  1599. row = (
  1600. db.query(BizPilotInfusionSchedule)
  1601. .filter(
  1602. BizPilotInfusionSchedule.id == schedule_id,
  1603. BizPilotInfusionSchedule.tenant_no == tenant_no,
  1604. BizPilotInfusionSchedule.del_flag == 0,
  1605. )
  1606. .first()
  1607. )
  1608. if not row:
  1609. raise ValueError("输注计划不存在")
  1610. now = datetime.now()
  1611. outbound_wo = (
  1612. db.query(BizProductionWorkOrder)
  1613. .filter(
  1614. BizProductionWorkOrder.infusion_schedule_id == schedule_id,
  1615. BizProductionWorkOrder.tenant_no == tenant_no,
  1616. BizProductionWorkOrder.del_flag == 0,
  1617. BizProductionWorkOrder.work_order_type == "outbound",
  1618. )
  1619. .first()
  1620. )
  1621. row.schedule_status = "yqx"
  1622. row.qc_status = "yqx"
  1623. if outbound_wo:
  1624. outbound_wo.work_order_status = "yzf"
  1625. outbound_wo.update_time = now
  1626. from app.crud.pilot_outbound_status_sync import sync_schedule_from_production_work_order
  1627. sync_schedule_from_production_work_order(db, outbound_wo)
  1628. for qc_wo in (
  1629. db.query(BizQcWorkOrder)
  1630. .filter(
  1631. BizQcWorkOrder.infusion_schedule_id == schedule_id,
  1632. BizQcWorkOrder.work_order_status.notin_(["ywc", "yzf"]),
  1633. BizQcWorkOrder.del_flag == 0,
  1634. )
  1635. .all()
  1636. ):
  1637. qc_wo.work_order_status = "yzf"
  1638. qc_wo.update_time = now
  1639. from app.crud.pilot_outbound_status_sync import sync_schedule_from_qc_work_order
  1640. sync_schedule_from_qc_work_order(db, qc_wo)
  1641. db.query(BizPilotInfusionScheduleProduct).filter(
  1642. BizPilotInfusionScheduleProduct.infusion_schedule_id == schedule_id,
  1643. BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
  1644. BizPilotInfusionScheduleProduct.del_flag == 0,
  1645. ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
  1646. row.del_flag = 1
  1647. row.update_time = now
  1648. if commit:
  1649. db.commit()
  1650. else:
  1651. db.flush()
  1652. def mark_schedule_complete(
  1653. db: Session, schedule_id: str, tenant_no: str, user_id: str
  1654. ) -> Dict[str, Any]:
  1655. row = (
  1656. db.query(BizPilotInfusionSchedule)
  1657. .filter(
  1658. BizPilotInfusionSchedule.id == schedule_id,
  1659. BizPilotInfusionSchedule.tenant_no == tenant_no,
  1660. BizPilotInfusionSchedule.del_flag == 0,
  1661. )
  1662. .first()
  1663. )
  1664. if not row:
  1665. raise ValueError("输注计划不存在")
  1666. row.schedule_status = "ysc"
  1667. row.update_by = user_id
  1668. row.update_time = datetime.now()
  1669. from app.crud.pilot_outbound_status_sync import sync_pilot_outbound_from_schedule
  1670. sync_pilot_outbound_from_schedule(db, row, user_id=user_id)
  1671. db.commit()
  1672. db.refresh(row)
  1673. return _row_to_dict_infusion_schedule(row)