pilot_application.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. """中试服务申请单管理 API(申请单、患者信息、输注方案、输注计划、出库联动)"""
  2. from typing import Optional
  3. from fastapi import APIRouter, Depends, Query
  4. from pydantic import BaseModel, ConfigDict, Field
  5. from sqlalchemy.orm import Session
  6. from app.core.database import get_db
  7. from app.core.menu_guard import require_admin_or_super_admin
  8. from app.core.response import error_response, success_response
  9. from app.crud import pilot_application
  10. from app.models.models import SysUser
  11. router = APIRouter(prefix="/web/admin", tags=["中试服务申请单管理"])
  12. def _get_tenant_no(db: Session, user: dict) -> str:
  13. user_id = user.get("user_id")
  14. if not user_id:
  15. raise PermissionError("用户身份信息缺失,无法确定租户")
  16. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  17. if not sys_user or not sys_user.tenant_no:
  18. raise PermissionError("用户租户信息缺失")
  19. return sys_user.tenant_no
  20. # ---------------------------------------------------------------------------
  21. # Pydantic Schemas
  22. # ---------------------------------------------------------------------------
  23. class IdBody(BaseModel):
  24. model_config = ConfigDict(populate_by_name=True)
  25. id: str
  26. class PlanIdBody(BaseModel):
  27. model_config = ConfigDict(populate_by_name=True)
  28. plan_id: str = Field(..., alias="infusionPlanId")
  29. class ApplicationCreateBody(BaseModel):
  30. model_config = ConfigDict(populate_by_name=True)
  31. channel_service_order_id: Optional[str] = Field(None, alias="channelServiceOrderId")
  32. remark: Optional[str] = None
  33. biz_type: Optional[str] = Field(None, alias="bizType")
  34. project_title: Optional[str] = Field(None, alias="projectTitle")
  35. customer_name: Optional[str] = Field(None, alias="customerName")
  36. customer_contact: Optional[str] = Field(None, alias="customerContact")
  37. customer_phone: Optional[str] = Field(None, alias="customerPhone")
  38. class ApplicationUpdateBody(BaseModel):
  39. model_config = ConfigDict(populate_by_name=True)
  40. id: str
  41. remark: Optional[str] = None
  42. project_title: Optional[str] = Field(None, alias="projectTitle")
  43. customer_name: Optional[str] = Field(None, alias="customerName")
  44. customer_contact: Optional[str] = Field(None, alias="customerContact")
  45. customer_phone: Optional[str] = Field(None, alias="customerPhone")
  46. class PatientSaveBody(BaseModel):
  47. model_config = ConfigDict(populate_by_name=True)
  48. application_id: str = Field(..., alias="pilotApplicationId")
  49. customer_id: Optional[str] = Field(None, alias="customerId")
  50. patient_name: str = Field(..., alias="patientName")
  51. sex: Optional[str] = None
  52. age: Optional[int] = None
  53. id_card_no: Optional[str] = Field(None, alias="idCardNo")
  54. phone_no: Optional[str] = Field(None, alias="phoneNo")
  55. emergency_contact: Optional[str] = Field(None, alias="emergencyContact")
  56. emergency_phone: Optional[str] = Field(None, alias="emergencyPhone")
  57. clinical_diagnosis: Optional[str] = Field(None, alias="clinicalDiagnosis")
  58. diagnosis_attachments: Optional[str] = Field(None, alias="diagnosisAttachments")
  59. need_checkup: int = Field(1, alias="needCheckup")
  60. class InfusionPlanCreateBody(BaseModel):
  61. model_config = ConfigDict(populate_by_name=True)
  62. pilot_application_id: str = Field(..., alias="pilotApplicationId")
  63. cell_type: str = Field(..., alias="cellType")
  64. spec_text: Optional[str] = Field(None, alias="specText")
  65. total_course_count: int = Field(..., alias="totalCourseCount")
  66. course_interval: Optional[str] = Field(None, alias="courseInterval")
  67. infusion_route: Optional[str] = Field(None, alias="infusionRoute")
  68. infusion_start_date: Optional[str] = Field(None, alias="infusionStartDate")
  69. class InfusionPlanUpdateBody(BaseModel):
  70. model_config = ConfigDict(populate_by_name=True)
  71. id: str
  72. cell_type: Optional[str] = Field(None, alias="cellType")
  73. spec_text: Optional[str] = Field(None, alias="specText")
  74. total_course_count: Optional[int] = Field(None, alias="totalCourseCount")
  75. course_interval: Optional[str] = Field(None, alias="courseInterval")
  76. infusion_route: Optional[str] = Field(None, alias="infusionRoute")
  77. infusion_start_date: Optional[str] = Field(None, alias="infusionStartDate")
  78. class InfusionScheduleUpdateBody(BaseModel):
  79. model_config = ConfigDict(populate_by_name=True)
  80. id: str
  81. infusion_time: Optional[str] = Field(None, alias="infusionTime")
  82. remark: Optional[str] = None
  83. attachments: Optional[str] = None
  84. class ScheduleCreateBody(BaseModel):
  85. model_config = ConfigDict(populate_by_name=True)
  86. plan_id: str = Field(..., alias="infusionPlanId")
  87. infusion_time: Optional[str] = Field(None, alias="infusionTime")
  88. remark: Optional[str] = None
  89. amount: Optional[float] = None
  90. # --- 复合保存 Schema(参考采集协议单一次性保存模式)---
  91. class PatientNestedBody(BaseModel):
  92. model_config = ConfigDict(populate_by_name=True)
  93. customer_id: Optional[str] = Field(None, alias="customerId")
  94. patient_name: Optional[str] = Field(None, alias="patientName")
  95. sex: Optional[str] = None
  96. age: Optional[int] = None
  97. id_card_no: Optional[str] = Field(None, alias="idCardNo")
  98. phone_no: Optional[str] = Field(None, alias="phoneNo")
  99. emergency_contact: Optional[str] = Field(None, alias="emergencyContact")
  100. emergency_phone: Optional[str] = Field(None, alias="emergencyPhone")
  101. clinical_diagnosis: Optional[str] = Field(None, alias="clinicalDiagnosis")
  102. diagnosis_attachments: Optional[str] = Field(None, alias="diagnosisAttachments")
  103. need_checkup: int = Field(1, alias="needCheckup")
  104. class ScheduleProductNestedBody(BaseModel):
  105. model_config = ConfigDict(populate_by_name=True)
  106. id: Optional[str] = None
  107. price_id: Optional[str] = Field(None, alias="priceId")
  108. project_code: Optional[str] = Field(None, alias="projectCode")
  109. project_name: Optional[str] = Field(None, alias="projectName")
  110. cell_type_name: Optional[str] = Field(None, alias="cellTypeName")
  111. spec_text: Optional[str] = Field(None, alias="specText")
  112. agreement_price: Optional[float] = Field(None, alias="agreementPrice")
  113. actual_price: Optional[float] = Field(None, alias="actualPrice")
  114. quantity: Optional[int] = 1
  115. status: Optional[str] = None
  116. remark: Optional[str] = None
  117. class InfusionScheduleNestedBody(BaseModel):
  118. model_config = ConfigDict(populate_by_name=True)
  119. id: Optional[str] = None
  120. sort_no: Optional[int] = Field(None, alias="sortNo")
  121. infusion_time: Optional[str] = Field(None, alias="infusionTime")
  122. infusion_location: Optional[str] = Field(None, alias="infusionLocation")
  123. infusion_method: Optional[str] = Field(None, alias="infusionMethod")
  124. amount: Optional[float] = None
  125. cell_products: Optional[list[ScheduleProductNestedBody]] = Field(None, alias="cellProducts")
  126. class InfusionPlanNestedBody(BaseModel):
  127. model_config = ConfigDict(populate_by_name=True)
  128. id: Optional[str] = None
  129. cell_type: Optional[str] = Field(None, alias="cellType")
  130. spec_text: Optional[str] = Field(None, alias="specText")
  131. total_course_count: Optional[int] = Field(None, alias="totalCourseCount")
  132. course_interval: Optional[str] = Field(None, alias="courseInterval")
  133. infusion_route: Optional[str] = Field(None, alias="infusionRoute")
  134. infusion_start_date: Optional[str] = Field(None, alias="infusionStartDate")
  135. channel_agreement_id: Optional[str] = Field(None, alias="channelAgreementId")
  136. class ApplicationSaveBody(BaseModel):
  137. """一揽子保存:申请单 + 患者信息 + 输注方案 + 输注计划(含细胞产品)"""
  138. model_config = ConfigDict(populate_by_name=True)
  139. id: Optional[str] = None
  140. application_name: Optional[str] = Field(None, alias="applicationName")
  141. channel_service_order_id: Optional[str] = Field(None, alias="channelServiceOrderId")
  142. channel_id: Optional[str] = Field(None, alias="channelId")
  143. biz_type: Optional[str] = Field(None, alias="bizType")
  144. project_title: Optional[str] = Field(None, alias="projectTitle")
  145. customer_name: Optional[str] = Field(None, alias="customerName")
  146. customer_contact: Optional[str] = Field(None, alias="customerContact")
  147. customer_phone: Optional[str] = Field(None, alias="customerPhone")
  148. remark: Optional[str] = None
  149. consent_form_urls: Optional[str] = Field(None, alias="consentFormUrls")
  150. consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
  151. attachment_urls: Optional[str] = Field(None, alias="attachmentUrls")
  152. other_attachments: Optional[list] = Field(None, alias="otherAttachments")
  153. patient: Optional[PatientNestedBody] = None
  154. infusion_plan: Optional[InfusionPlanNestedBody] = Field(None, alias="infusionPlan")
  155. infusion_schedules: Optional[list[InfusionScheduleNestedBody]] = Field(
  156. None, alias="infusionSchedules"
  157. )
  158. infusion_plans: Optional[list[InfusionPlanNestedBody]] = Field(None, alias="infusionPlans")
  159. # ---------------------------------------------------------------------------
  160. # 1. 申请单 CRUD + 状态变更
  161. # ---------------------------------------------------------------------------
  162. @router.get("/pilot-application/list")
  163. async def api_list_applications(
  164. page: int = Query(1, ge=1),
  165. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  166. keyword: Optional[str] = Query(None, alias="keyword"),
  167. channelId: Optional[str] = Query(None, alias="channelId"),
  168. applicationStatus: Optional[str] = Query(None, alias="applicationStatus"),
  169. channelServiceOrderId: Optional[str] = Query(None, alias="channelServiceOrderId"),
  170. bizType: Optional[str] = Query(None, alias="bizType"),
  171. user: dict = Depends(require_admin_or_super_admin),
  172. db: Session = Depends(get_db),
  173. ):
  174. tenant_no = _get_tenant_no(db, user)
  175. result = pilot_application.list_applications(
  176. db,
  177. tenant_no=tenant_no,
  178. page=page,
  179. page_size=pageSize,
  180. channel_id=channelId,
  181. application_status=applicationStatus,
  182. channel_service_order_id=channelServiceOrderId,
  183. keyword=keyword,
  184. biz_type=bizType,
  185. )
  186. return success_response(result)
  187. @router.get("/pilot-application/detail")
  188. async def api_get_application(
  189. id: str = Query(...),
  190. user: dict = Depends(require_admin_or_super_admin),
  191. db: Session = Depends(get_db),
  192. ):
  193. tenant_no = _get_tenant_no(db, user)
  194. result = pilot_application.get_application(db, app_id=id, tenant_no=tenant_no)
  195. if result is None:
  196. return error_response(404, "申请单不存在")
  197. return success_response(result)
  198. @router.post("/pilot-application/create")
  199. async def api_create_application(
  200. body: ApplicationCreateBody,
  201. user: dict = Depends(require_admin_or_super_admin),
  202. db: Session = Depends(get_db),
  203. ):
  204. try:
  205. tenant_no = _get_tenant_no(db, user)
  206. data = {
  207. "channelServiceOrderId": body.channel_service_order_id or "",
  208. "remark": body.remark,
  209. "bizType": body.biz_type or "pilot_service",
  210. "projectTitle": body.project_title,
  211. "customerName": body.customer_name,
  212. "customerContact": body.customer_contact,
  213. "customerPhone": body.customer_phone,
  214. }
  215. result = pilot_application.create_application(
  216. db,
  217. data=data,
  218. tenant_no=tenant_no,
  219. user_id=user.get("user_id"),
  220. dept_id=user.get("region_id", ""),
  221. )
  222. return success_response(result, message="创建成功")
  223. except ValueError as e:
  224. return error_response(400, str(e))
  225. @router.put("/pilot-application/update")
  226. async def api_update_application(
  227. body: ApplicationUpdateBody,
  228. user: dict = Depends(require_admin_or_super_admin),
  229. db: Session = Depends(get_db),
  230. ):
  231. try:
  232. tenant_no = _get_tenant_no(db, user)
  233. data = {}
  234. if body.remark is not None:
  235. data["remark"] = body.remark
  236. if body.project_title is not None:
  237. data["projectTitle"] = body.project_title
  238. if body.customer_name is not None:
  239. data["customerName"] = body.customer_name
  240. if body.customer_contact is not None:
  241. data["customerContact"] = body.customer_contact
  242. if body.customer_phone is not None:
  243. data["customerPhone"] = body.customer_phone
  244. result = pilot_application.update_application(
  245. db, app_id=body.id, data=data, tenant_no=tenant_no
  246. )
  247. return success_response(result, message="更新成功")
  248. except ValueError as e:
  249. return error_response(400, str(e))
  250. @router.post("/pilot-application/save")
  251. async def api_save_application(
  252. body: ApplicationSaveBody,
  253. user: dict = Depends(require_admin_or_super_admin),
  254. db: Session = Depends(get_db),
  255. ):
  256. try:
  257. tenant_no = _get_tenant_no(db, user)
  258. user_id = user.get("user_id")
  259. dept_id = user.get("region_id", "")
  260. is_edit = bool(body.id)
  261. biz_type = body.biz_type or "pilot_service"
  262. app_data: dict = {
  263. "channelServiceOrderId": body.channel_service_order_id or "",
  264. "channelId": body.channel_id or "",
  265. "bizType": biz_type,
  266. "remark": body.remark,
  267. }
  268. if body.application_name is not None:
  269. app_data["applicationName"] = body.application_name
  270. if body.project_title is not None:
  271. app_data["projectTitle"] = body.project_title
  272. if body.customer_name is not None:
  273. app_data["customerName"] = body.customer_name
  274. if body.customer_contact is not None:
  275. app_data["customerContact"] = body.customer_contact
  276. if body.customer_phone is not None:
  277. app_data["customerPhone"] = body.customer_phone
  278. if body.consent_attachments is not None:
  279. app_data["consentAttachments"] = body.consent_attachments
  280. elif body.consent_form_urls is not None:
  281. app_data["consentFormUrls"] = body.consent_form_urls
  282. if body.other_attachments is not None:
  283. app_data["otherAttachments"] = body.other_attachments
  284. elif body.attachment_urls is not None:
  285. app_data["attachmentUrls"] = body.attachment_urls
  286. if (
  287. not is_edit
  288. and biz_type not in ("clinical_application", "basic_experiment")
  289. and not app_data["channelServiceOrderId"]
  290. ):
  291. return error_response(400, "请选择渠道服务订单")
  292. if not is_edit and biz_type == "clinical_application" and not app_data["channelId"]:
  293. return error_response(400, "请选择渠道")
  294. if is_edit:
  295. app_result = pilot_application.update_application(
  296. db,
  297. app_id=body.id,
  298. data=app_data,
  299. tenant_no=tenant_no,
  300. )
  301. app_id = body.id
  302. else:
  303. app_result = pilot_application.create_application(
  304. db,
  305. data=app_data,
  306. tenant_no=tenant_no,
  307. user_id=user_id,
  308. dept_id=dept_id,
  309. )
  310. app_id = app_result.get("id")
  311. if not app_id:
  312. return error_response(500, "创建申请单失败,未获取到ID")
  313. if body.patient:
  314. pilot_application.save_patient(
  315. db,
  316. app_id=app_id,
  317. data=body.patient.model_dump(by_alias=True),
  318. tenant_no=tenant_no,
  319. user_id=user_id,
  320. dept_id=dept_id,
  321. )
  322. if body.infusion_plan is not None or body.infusion_schedules:
  323. plan_dict = (
  324. body.infusion_plan.model_dump(by_alias=True) if body.infusion_plan else {}
  325. )
  326. schedules = [
  327. s.model_dump(by_alias=True) for s in (body.infusion_schedules or [])
  328. ]
  329. if is_edit:
  330. pilot_application.upsert_application_infusion(
  331. db,
  332. app_id=app_id,
  333. plan_data=plan_dict,
  334. schedules_data=schedules,
  335. tenant_no=tenant_no,
  336. user_id=user_id,
  337. dept_id=dept_id,
  338. )
  339. else:
  340. pilot_application.save_application_infusion(
  341. db,
  342. app_id=app_id,
  343. plan_data=plan_dict,
  344. schedules_data=schedules,
  345. tenant_no=tenant_no,
  346. user_id=user_id,
  347. dept_id=dept_id,
  348. replace_existing=False,
  349. )
  350. elif body.infusion_plans:
  351. for plan_data in body.infusion_plans:
  352. plan_result = pilot_application.create_infusion_plan(
  353. db,
  354. app_id=app_id,
  355. data=plan_data.model_dump(by_alias=True),
  356. tenant_no=tenant_no,
  357. user_id=user_id,
  358. dept_id=dept_id,
  359. )
  360. plan_id = plan_result.get("id")
  361. if plan_id and (plan_data.total_course_count or 0) > 0:
  362. pilot_application.auto_generate_schedules(
  363. db,
  364. plan_id=plan_id,
  365. tenant_no=tenant_no,
  366. user_id=user_id,
  367. dept_id=dept_id,
  368. )
  369. app_result = pilot_application.get_application(db, app_id=app_id, tenant_no=tenant_no)
  370. return success_response(app_result, message="保存成功")
  371. except ValueError as e:
  372. return error_response(400, str(e))
  373. except Exception as e:
  374. return error_response(500, str(e))
  375. @router.delete("/pilot-application/delete")
  376. async def api_delete_application(
  377. body: IdBody,
  378. user: dict = Depends(require_admin_or_super_admin),
  379. db: Session = Depends(get_db),
  380. ):
  381. try:
  382. tenant_no = _get_tenant_no(db, user)
  383. pilot_application.soft_delete_application(db, app_id=body.id, tenant_no=tenant_no)
  384. return success_response(None, message="删除成功")
  385. except ValueError as e:
  386. return error_response(400, str(e))
  387. @router.post("/pilot-application/start")
  388. async def api_start_application(
  389. body: IdBody,
  390. user: dict = Depends(require_admin_or_super_admin),
  391. db: Session = Depends(get_db),
  392. ):
  393. try:
  394. tenant_no = _get_tenant_no(db, user)
  395. result = pilot_application.start_application(
  396. db, app_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
  397. )
  398. return success_response(result, message="操作成功")
  399. except ValueError as e:
  400. return error_response(400, str(e))
  401. @router.post("/pilot-application/complete")
  402. async def api_complete_application(
  403. body: IdBody,
  404. user: dict = Depends(require_admin_or_super_admin),
  405. db: Session = Depends(get_db),
  406. ):
  407. try:
  408. tenant_no = _get_tenant_no(db, user)
  409. result = pilot_application.complete_application(
  410. db, app_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
  411. )
  412. return success_response(result, message="操作成功")
  413. except ValueError as e:
  414. return error_response(400, str(e))
  415. class SaleScheduleProductLineBody(BaseModel):
  416. model_config = ConfigDict(populate_by_name=True)
  417. price_id: Optional[str] = Field(None, alias="priceId")
  418. project_code: Optional[str] = Field(None, alias="projectCode")
  419. project_name: Optional[str] = Field(None, alias="projectName")
  420. cell_type_name: Optional[str] = Field(None, alias="cellTypeName")
  421. spec_text: Optional[str] = Field(None, alias="specText")
  422. agreement_price: Optional[float] = Field(None, alias="agreementPrice")
  423. actual_price: Optional[float] = Field(None, alias="actualPrice")
  424. quantity: Optional[int] = 1
  425. pickup_time: Optional[str] = Field(None, alias="pickupTime")
  426. amount: Optional[float] = None
  427. class SaleSchedulesCreateBody(BaseModel):
  428. model_config = ConfigDict(populate_by_name=True)
  429. product_lines: list[SaleScheduleProductLineBody] = Field(..., alias="productLines")
  430. @router.post("/pilot-application/{application_id}/sale-schedules")
  431. async def api_create_sale_schedules(
  432. application_id: str,
  433. body: SaleSchedulesCreateBody,
  434. user: dict = Depends(require_admin_or_super_admin),
  435. db: Session = Depends(get_db),
  436. ):
  437. try:
  438. tenant_no = _get_tenant_no(db, user)
  439. product_lines = [line.model_dump(by_alias=True) for line in body.product_lines]
  440. results = pilot_application.create_basic_experiment_sale_schedules(
  441. db,
  442. application_id=application_id,
  443. product_lines=product_lines,
  444. tenant_no=tenant_no,
  445. user_id=user.get("user_id"),
  446. dept_id=user.get("region_id", ""),
  447. )
  448. return success_response(results, message="售卖计划创建成功")
  449. except ValueError as e:
  450. return error_response(400, str(e))
  451. @router.post("/pilot-application/void")
  452. async def api_void_application(
  453. body: IdBody,
  454. user: dict = Depends(require_admin_or_super_admin),
  455. db: Session = Depends(get_db),
  456. ):
  457. try:
  458. tenant_no = _get_tenant_no(db, user)
  459. result = pilot_application.void_application(
  460. db, app_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
  461. )
  462. return success_response(result, message="操作成功")
  463. except ValueError as e:
  464. return error_response(400, str(e))
  465. # ---------------------------------------------------------------------------
  466. # 2. 患者信息
  467. # ---------------------------------------------------------------------------
  468. @router.get("/pilot-patient/detail")
  469. async def api_get_patient(
  470. application_id: str = Query(..., alias="pilotApplicationId"),
  471. user: dict = Depends(require_admin_or_super_admin),
  472. db: Session = Depends(get_db),
  473. ):
  474. tenant_no = _get_tenant_no(db, user)
  475. result = pilot_application.get_patient_by_application(
  476. db, app_id=application_id, tenant_no=tenant_no
  477. )
  478. if result is None:
  479. return success_response(None)
  480. return success_response(result)
  481. @router.post("/pilot-patient/save")
  482. async def api_save_patient(
  483. body: PatientSaveBody,
  484. user: dict = Depends(require_admin_or_super_admin),
  485. db: Session = Depends(get_db),
  486. ):
  487. try:
  488. tenant_no = _get_tenant_no(db, user)
  489. data = {
  490. "customerId": body.customer_id,
  491. "patientName": body.patient_name,
  492. "sex": body.sex,
  493. "age": body.age,
  494. "idCardNo": body.id_card_no,
  495. "phoneNo": body.phone_no,
  496. "emergencyContact": body.emergency_contact,
  497. "emergencyPhone": body.emergency_phone,
  498. "clinicalDiagnosis": body.clinical_diagnosis,
  499. "diagnosisAttachments": body.diagnosis_attachments,
  500. "needCheckup": body.need_checkup,
  501. }
  502. result = pilot_application.save_patient(
  503. db,
  504. app_id=body.application_id,
  505. data=data,
  506. tenant_no=tenant_no,
  507. user_id=user.get("user_id"),
  508. dept_id=user.get("region_id", ""),
  509. )
  510. return success_response(result, message="保存成功")
  511. except ValueError as e:
  512. return error_response(400, str(e))
  513. # ---------------------------------------------------------------------------
  514. # 3. 输注方案
  515. # ---------------------------------------------------------------------------
  516. @router.get("/pilot-infusion-plan/list")
  517. async def api_list_plans(
  518. application_id: str = Query(..., alias="pilotApplicationId"),
  519. user: dict = Depends(require_admin_or_super_admin),
  520. db: Session = Depends(get_db),
  521. ):
  522. tenant_no = _get_tenant_no(db, user)
  523. result = pilot_application.list_infusion_plans(
  524. db, app_id=application_id, tenant_no=tenant_no
  525. )
  526. return success_response(result)
  527. @router.post("/pilot-infusion-plan/create")
  528. async def api_create_plan(
  529. body: InfusionPlanCreateBody,
  530. user: dict = Depends(require_admin_or_super_admin),
  531. db: Session = Depends(get_db),
  532. ):
  533. try:
  534. tenant_no = _get_tenant_no(db, user)
  535. data = {
  536. "cellType": body.cell_type,
  537. "specText": body.spec_text,
  538. "totalCourseCount": body.total_course_count,
  539. "courseInterval": body.course_interval,
  540. "infusionRoute": body.infusion_route,
  541. "infusionStartDate": body.infusion_start_date,
  542. }
  543. result = pilot_application.create_infusion_plan(
  544. db,
  545. app_id=body.pilot_application_id,
  546. data=data,
  547. tenant_no=tenant_no,
  548. user_id=user.get("user_id"),
  549. dept_id=user.get("region_id", ""),
  550. )
  551. return success_response(result, message="创建成功")
  552. except ValueError as e:
  553. return error_response(400, str(e))
  554. @router.put("/pilot-infusion-plan/update")
  555. async def api_update_plan(
  556. body: InfusionPlanUpdateBody,
  557. user: dict = Depends(require_admin_or_super_admin),
  558. db: Session = Depends(get_db),
  559. ):
  560. try:
  561. tenant_no = _get_tenant_no(db, user)
  562. data = {}
  563. if body.cell_type is not None:
  564. data["cellType"] = body.cell_type
  565. if body.spec_text is not None:
  566. data["specText"] = body.spec_text
  567. if body.total_course_count is not None:
  568. data["totalCourseCount"] = body.total_course_count
  569. if body.course_interval is not None:
  570. data["courseInterval"] = body.course_interval
  571. if body.infusion_route is not None:
  572. data["infusionRoute"] = body.infusion_route
  573. if body.infusion_start_date is not None:
  574. data["infusionStartDate"] = body.infusion_start_date
  575. result = pilot_application.update_infusion_plan(
  576. db, plan_id=body.id, data=data, tenant_no=tenant_no
  577. )
  578. return success_response(result, message="更新成功")
  579. except ValueError as e:
  580. return error_response(400, str(e))
  581. @router.delete("/pilot-infusion-plan/delete")
  582. async def api_delete_plan(
  583. body: IdBody,
  584. user: dict = Depends(require_admin_or_super_admin),
  585. db: Session = Depends(get_db),
  586. ):
  587. try:
  588. tenant_no = _get_tenant_no(db, user)
  589. pilot_application.soft_delete_infusion_plan(
  590. db, plan_id=body.id, tenant_no=tenant_no
  591. )
  592. return success_response(None, message="删除成功")
  593. except ValueError as e:
  594. return error_response(400, str(e))
  595. # ---------------------------------------------------------------------------
  596. # 4. 输注计划
  597. # ---------------------------------------------------------------------------
  598. @router.get("/pilot-infusion-schedule/list")
  599. async def api_list_schedules(
  600. plan_id: str = Query(..., alias="infusionPlanId"),
  601. user: dict = Depends(require_admin_or_super_admin),
  602. db: Session = Depends(get_db),
  603. ):
  604. tenant_no = _get_tenant_no(db, user)
  605. result = pilot_application.list_schedules_by_plan(
  606. db, plan_id=plan_id, tenant_no=tenant_no
  607. )
  608. return success_response(result)
  609. @router.post("/pilot-infusion-schedule/auto-generate")
  610. async def api_auto_generate(
  611. body: PlanIdBody,
  612. user: dict = Depends(require_admin_or_super_admin),
  613. db: Session = Depends(get_db),
  614. ):
  615. try:
  616. tenant_no = _get_tenant_no(db, user)
  617. result = pilot_application.auto_generate_schedules(
  618. db,
  619. plan_id=body.plan_id,
  620. tenant_no=tenant_no,
  621. user_id=user.get("user_id"),
  622. dept_id=user.get("region_id", ""),
  623. )
  624. return success_response(result, message="生成成功")
  625. except ValueError as e:
  626. return error_response(400, str(e))
  627. @router.post("/pilot-infusion-schedule/create")
  628. async def api_create_schedule(
  629. body: ScheduleCreateBody,
  630. user: dict = Depends(require_admin_or_super_admin),
  631. db: Session = Depends(get_db),
  632. ):
  633. try:
  634. tenant_no = _get_tenant_no(db, user)
  635. data = {
  636. "infusionTime": body.infusion_time,
  637. "remark": body.remark,
  638. "amount": body.amount,
  639. }
  640. result = pilot_application.create_schedule(
  641. db,
  642. plan_id=body.plan_id,
  643. data=data,
  644. tenant_no=tenant_no,
  645. user_id=user.get("user_id"),
  646. dept_id=user.get("region_id", ""),
  647. )
  648. return success_response(result, message="创建成功")
  649. except ValueError as e:
  650. return error_response(400, str(e))
  651. @router.put("/pilot-infusion-schedule/update")
  652. async def api_update_schedule(
  653. body: InfusionScheduleUpdateBody,
  654. user: dict = Depends(require_admin_or_super_admin),
  655. db: Session = Depends(get_db),
  656. ):
  657. try:
  658. tenant_no = _get_tenant_no(db, user)
  659. data = {}
  660. if body.infusion_time is not None:
  661. data["infusionTime"] = body.infusion_time
  662. if body.remark is not None:
  663. data["remark"] = body.remark
  664. if body.attachments is not None:
  665. data["attachments"] = body.attachments
  666. result = pilot_application.update_schedule(
  667. db, schedule_id=body.id, data=data, tenant_no=tenant_no
  668. )
  669. return success_response(result, message="更新成功")
  670. except ValueError as e:
  671. return error_response(400, str(e))
  672. @router.delete("/pilot-infusion-schedule/delete")
  673. async def api_delete_schedule(
  674. body: IdBody,
  675. user: dict = Depends(require_admin_or_super_admin),
  676. db: Session = Depends(get_db),
  677. ):
  678. try:
  679. tenant_no = _get_tenant_no(db, user)
  680. pilot_application.soft_delete_schedule(
  681. db, schedule_id=body.id, tenant_no=tenant_no
  682. )
  683. return success_response(None, message="删除成功")
  684. except ValueError as e:
  685. return error_response(400, str(e))
  686. @router.post("/pilot-infusion-schedule/mark-complete")
  687. async def api_mark_complete(
  688. body: IdBody,
  689. user: dict = Depends(require_admin_or_super_admin),
  690. db: Session = Depends(get_db),
  691. ):
  692. try:
  693. tenant_no = _get_tenant_no(db, user)
  694. result = pilot_application.mark_schedule_complete(
  695. db, schedule_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
  696. )
  697. return success_response(result, message="操作成功")
  698. except ValueError as e:
  699. return error_response(400, str(e))
  700. # ---------------------------------------------------------------------------
  701. # 出库生产工单 / 出库质检工单 联动
  702. # ---------------------------------------------------------------------------
  703. class ScheduleIdBody(BaseModel):
  704. scheduleId: str
  705. @router.post("/pilot-infusion-schedule/create-production-work-order")
  706. async def api_create_outbound_production(
  707. body: ScheduleIdBody,
  708. user: dict = Depends(require_admin_or_super_admin),
  709. db: Session = Depends(get_db),
  710. ):
  711. try:
  712. tenant_no = _get_tenant_no(db, user)
  713. from app.crud import production_work_order as pwo_svc
  714. from app.crud.qc_work_order import resolve_outbound_biz_type_for_schedule
  715. outbound_biz_type = resolve_outbound_biz_type_for_schedule(
  716. db, body.scheduleId, tenant_no
  717. )
  718. wo = pwo_svc.create_outbound_production_work_order(
  719. db,
  720. schedule_id=body.scheduleId,
  721. tenant_no=tenant_no,
  722. user_id=user.get("user_id"),
  723. create_dept_id=user.get("dept_id", ""),
  724. biz_type=outbound_biz_type,
  725. )
  726. db.commit()
  727. db.refresh(wo)
  728. return success_response(pwo_svc._wo_to_dict(wo), message="创建出库生产工单成功")
  729. except ValueError as e:
  730. return error_response(400, str(e))
  731. except Exception as e:
  732. db.rollback()
  733. return error_response(400, str(e))
  734. class OutboundQcBody(BaseModel):
  735. scheduleId: str
  736. cellProductId: str = ""
  737. @router.post("/pilot-infusion-schedule/create-qc-work-order")
  738. async def api_create_outbound_qc(
  739. body: OutboundQcBody,
  740. user: dict = Depends(require_admin_or_super_admin),
  741. db: Session = Depends(get_db),
  742. ):
  743. try:
  744. tenant_no = _get_tenant_no(db, user)
  745. from app.crud import qc_work_order as qc_svc
  746. from app.crud.qc_work_order import resolve_outbound_biz_type_for_schedule
  747. outbound_biz_type = resolve_outbound_biz_type_for_schedule(
  748. db, body.scheduleId, tenant_no
  749. )
  750. wo = qc_svc.create_outbound_qc_work_order(
  751. db,
  752. schedule_id=body.scheduleId,
  753. cell_product_id=body.cellProductId,
  754. tenant_no=tenant_no,
  755. user_id=user.get("user_id"),
  756. create_dept_id=user.get("dept_id", ""),
  757. biz_type=outbound_biz_type,
  758. )
  759. return success_response(qc_svc._wo_to_dict(wo), message="创建出库质检工单成功")
  760. except ValueError as e:
  761. return error_response(400, str(e))
  762. except Exception as e:
  763. db.rollback()
  764. return error_response(400, str(e))