sample_reception.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. """质量检测 — 样本接收 API"""
  2. from typing import List, Optional
  3. from fastapi import APIRouter, Depends, Query
  4. from pydantic import BaseModel, ConfigDict, Field
  5. from sqlalchemy import or_
  6. from sqlalchemy.orm import Session
  7. from app.core.data_scope import apply_data_scope
  8. from app.core.database import get_db
  9. from app.core.menu_guard import require_admin_or_super_admin, require_any_menu_code, require_menu_code
  10. from app.core.response import error_response, success_response
  11. from app.core.utils import parse_datetime
  12. from app.crud import sample_reception as sr_svc
  13. from app.crud import sample_reception_check_item as check_item_svc
  14. from app.crud.collection_protocol import total_pages
  15. from app.models.models import BizSampleReception, BizSampleReceptionCheckItem, SysUser
  16. router = APIRouter(prefix="/web/admin", tags=["质量检测"])
  17. SAMPLE_RECEPTION_READ_MENU_CODES = (
  18. "biz:sampleReception:list",
  19. "production:sample-reception-completion:list",
  20. )
  21. def _get_tenant_no(db: Session, user: dict) -> str:
  22. user_id = user.get("user_id")
  23. if not user_id:
  24. raise PermissionError("用户身份信息缺失,无法确定租户")
  25. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  26. if not sys_user or not sys_user.tenant_no:
  27. raise PermissionError("用户租户信息缺失")
  28. return sys_user.tenant_no
  29. def _get_current_user_id(user: dict) -> str:
  30. user_id = user.get("user_id")
  31. if not user_id:
  32. raise PermissionError("用户身份信息缺失")
  33. return str(user_id)
  34. def _page_payload(items: list, total: int, page: int, page_size: int) -> dict:
  35. return {
  36. "list": items,
  37. "total": total,
  38. "page": page,
  39. "pageSize": page_size,
  40. "totalPages": total_pages(total, page_size),
  41. }
  42. # ---------- 检查项配置 ----------
  43. class CheckItemBody(BaseModel):
  44. model_config = ConfigDict(populate_by_name=True)
  45. item_code: str = Field(..., alias="itemCode")
  46. item_name: str = Field(..., alias="itemName")
  47. status: str = Field("1", alias="status")
  48. order_num: Optional[int] = Field(None, alias="orderNum")
  49. remark: Optional[str] = None
  50. class CheckItemUpdateBody(BaseModel):
  51. model_config = ConfigDict(populate_by_name=True)
  52. item_code: Optional[str] = Field(None, alias="itemCode")
  53. item_name: Optional[str] = Field(None, alias="itemName")
  54. status: Optional[str] = None
  55. order_num: Optional[int] = Field(None, alias="orderNum")
  56. remark: Optional[str] = None
  57. @router.get("/sample-reception-check-items")
  58. async def list_check_items(
  59. page: int = Query(1, ge=1),
  60. pageSize: int = Query(10, ge=1, le=100),
  61. keyword: Optional[str] = None,
  62. status: Optional[str] = None,
  63. db: Session = Depends(get_db),
  64. user: dict = Depends(require_menu_code("biz:sampleReception:list")),
  65. ):
  66. try:
  67. tenant_no = _get_tenant_no(db, user)
  68. query = db.query(BizSampleReceptionCheckItem).filter(
  69. BizSampleReceptionCheckItem.del_flag == 0,
  70. BizSampleReceptionCheckItem.tenant_no == tenant_no,
  71. )
  72. kw = (keyword or "").strip()
  73. if kw:
  74. like = f"%{kw}%"
  75. query = query.filter(
  76. or_(
  77. BizSampleReceptionCheckItem.item_code.like(like),
  78. BizSampleReceptionCheckItem.item_name.like(like),
  79. )
  80. )
  81. if status:
  82. query = query.filter(BizSampleReceptionCheckItem.status == status)
  83. query = apply_data_scope(query, BizSampleReceptionCheckItem, user, db, tenant_no)
  84. total = query.count()
  85. items = query.order_by(BizSampleReceptionCheckItem.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
  86. result_list = [check_item_svc._row_to_dict(r) for r in items]
  87. return success_response(_page_payload(result_list, total, page, pageSize))
  88. except PermissionError as e:
  89. return error_response(403, str(e))
  90. except Exception as e:
  91. return error_response(400, str(e))
  92. @router.get("/sample-reception-check-items/active")
  93. async def list_active_check_items(
  94. db: Session = Depends(get_db),
  95. user: dict = Depends(require_any_menu_code(*SAMPLE_RECEPTION_READ_MENU_CODES)),
  96. ):
  97. try:
  98. tenant_no = _get_tenant_no(db, user)
  99. items = check_item_svc.list_active_check_items(db, tenant_no)
  100. return success_response(items)
  101. except PermissionError as e:
  102. return error_response(403, str(e))
  103. except Exception as e:
  104. return error_response(400, str(e))
  105. @router.post("/sample-reception-check-items")
  106. async def create_check_item(
  107. body: CheckItemBody,
  108. db: Session = Depends(get_db),
  109. user: dict = Depends(require_admin_or_super_admin),
  110. ):
  111. try:
  112. tenant_no = _get_tenant_no(db, user)
  113. user_id = _get_current_user_id(user)
  114. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  115. dept_id = (sys_user.dept_id or "") if sys_user else ""
  116. obj = check_item_svc.create_check_item(
  117. db,
  118. item_code=body.item_code,
  119. item_name=body.item_name,
  120. status=body.status,
  121. tenant_no=tenant_no,
  122. order_num=body.order_num,
  123. remark=body.remark,
  124. user_id=user_id,
  125. create_dept_id=dept_id,
  126. )
  127. return success_response(check_item_svc._row_to_dict(obj))
  128. except PermissionError as e:
  129. return error_response(403, str(e))
  130. except ValueError as e:
  131. return error_response(400, str(e))
  132. except Exception as e:
  133. return error_response(400, str(e))
  134. @router.put("/sample-reception-check-items/{row_id}")
  135. async def update_check_item(
  136. row_id: str,
  137. body: CheckItemUpdateBody,
  138. db: Session = Depends(get_db),
  139. user: dict = Depends(require_admin_or_super_admin),
  140. ):
  141. try:
  142. tenant_no = _get_tenant_no(db, user)
  143. user_id = _get_current_user_id(user)
  144. obj = check_item_svc.update_check_item(
  145. db,
  146. row_id,
  147. item_code=body.item_code,
  148. item_name=body.item_name,
  149. status=body.status,
  150. order_num=body.order_num,
  151. remark=body.remark,
  152. user_id=user_id,
  153. tenant_no=tenant_no,
  154. )
  155. return success_response(check_item_svc._row_to_dict(obj))
  156. except PermissionError as e:
  157. return error_response(403, str(e))
  158. except ValueError as e:
  159. return error_response(400, str(e))
  160. except Exception as e:
  161. return error_response(400, str(e))
  162. @router.delete("/sample-reception-check-items/{row_id}")
  163. async def delete_check_item(
  164. row_id: str,
  165. db: Session = Depends(get_db),
  166. user: dict = Depends(require_admin_or_super_admin),
  167. ):
  168. try:
  169. tenant_no = _get_tenant_no(db, user)
  170. check_item_svc.soft_delete_check_item(db, row_id, tenant_no=tenant_no)
  171. return success_response(None)
  172. except PermissionError as e:
  173. return error_response(403, str(e))
  174. except ValueError as e:
  175. return error_response(400, str(e))
  176. except Exception as e:
  177. return error_response(400, str(e))
  178. # ---------- 样本接收 ----------
  179. @router.get("/sample-receptions/pending")
  180. async def list_pending_samples(
  181. page: int = Query(1, ge=1),
  182. pageSize: int = Query(10, ge=1, le=100),
  183. keyword: Optional[str] = None,
  184. sourceType: Optional[str] = None,
  185. etaTimeStart: Optional[str] = Query(None, alias="etaTimeStart"),
  186. etaTimeEnd: Optional[str] = Query(None, alias="etaTimeEnd"),
  187. db: Session = Depends(get_db),
  188. user: dict = Depends(require_menu_code("biz:sampleReception:list")),
  189. ):
  190. try:
  191. tenant_no = _get_tenant_no(db, user)
  192. items, total = sr_svc.list_pending_protocol_samples(
  193. db, page=page, page_size=pageSize, tenant_no=tenant_no, keyword=keyword,
  194. source_type=sourceType,
  195. eta_time_start=etaTimeStart,
  196. eta_time_end=etaTimeEnd,
  197. user_info=user,
  198. )
  199. return success_response(_page_payload(items, total, page, pageSize))
  200. except PermissionError as e:
  201. return error_response(403, str(e))
  202. except Exception as e:
  203. return error_response(400, str(e))
  204. @router.get("/sample-receptions/received")
  205. async def list_received_samples(
  206. page: int = Query(1, ge=1),
  207. pageSize: int = Query(10, ge=1, le=100),
  208. keyword: Optional[str] = None,
  209. sourceType: Optional[str] = None,
  210. receptionTimeStart: Optional[str] = Query(None, alias="receptionTimeStart"),
  211. receptionTimeEnd: Optional[str] = Query(None, alias="receptionTimeEnd"),
  212. db: Session = Depends(get_db),
  213. user: dict = Depends(require_any_menu_code(*SAMPLE_RECEPTION_READ_MENU_CODES)),
  214. ):
  215. try:
  216. tenant_no = _get_tenant_no(db, user)
  217. items, total = sr_svc.list_received_protocol_samples(
  218. db, page=page, page_size=pageSize, tenant_no=tenant_no, keyword=keyword,
  219. source_type=sourceType,
  220. reception_time_start=receptionTimeStart,
  221. reception_time_end=receptionTimeEnd,
  222. user_info=user,
  223. )
  224. return success_response(_page_payload(items, total, page, pageSize))
  225. except PermissionError as e:
  226. return error_response(403, str(e))
  227. except Exception as e:
  228. return error_response(400, str(e))
  229. @router.get("/sample-receptions/by-sample-code")
  230. async def get_sample_reception_by_sample_code(
  231. sampleCode: str = Query(..., alias="sampleCode"),
  232. sourceType: Optional[str] = Query(None, alias="sourceType"),
  233. db: Session = Depends(get_db),
  234. user: dict = Depends(require_any_menu_code(*SAMPLE_RECEPTION_READ_MENU_CODES)),
  235. ):
  236. """根据样本编码查询样本接收记录:有则返回 receptionId + sampleInfo;无则仅返回 sampleInfo(用于新建)。"""
  237. try:
  238. tenant_no = _get_tenant_no(db, user)
  239. result = sr_svc.get_reception_by_sample_code(
  240. db, sample_code=sampleCode, source_type=sourceType, tenant_no=tenant_no
  241. )
  242. return success_response(result)
  243. except PermissionError as e:
  244. return error_response(403, str(e))
  245. except Exception as e:
  246. return error_response(400, str(e))
  247. @router.get("/sample-receptions/{reception_id}")
  248. async def get_sample_reception_detail(
  249. reception_id: str,
  250. db: Session = Depends(get_db),
  251. user: dict = Depends(require_any_menu_code(*SAMPLE_RECEPTION_READ_MENU_CODES)),
  252. ):
  253. try:
  254. tenant_no = _get_tenant_no(db, user)
  255. item = sr_svc.get_reception_by_id(db, reception_id, tenant_no)
  256. if not item:
  257. return error_response(404, "接收记录不存在")
  258. return success_response(item)
  259. except PermissionError as e:
  260. return error_response(403, str(e))
  261. except Exception as e:
  262. return error_response(400, str(e))
  263. class ReceptionCheckSubmit(BaseModel):
  264. model_config = ConfigDict(populate_by_name=True)
  265. check_item_id: str = Field(..., alias="checkItemId")
  266. checked: bool = False
  267. class SampleReceptionUpdateBody(BaseModel):
  268. model_config = ConfigDict(populate_by_name=True)
  269. reception_time: str = Field(..., alias="receptionTime")
  270. handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
  271. handler_name: Optional[str] = Field(None, alias="handlerName")
  272. receive_temperature: Optional[str] = Field(None, alias="receiveTemperature")
  273. checks: List[ReceptionCheckSubmit] = Field(default_factory=list)
  274. accept_conclusion: str = Field(..., alias="acceptConclusion")
  275. receive_attachments: Optional[str] = Field(None, alias="receiveAttachments")
  276. remark: Optional[str] = None
  277. lab_storage_code: Optional[str] = Field(None, alias="labStorageCode")
  278. class SampleReceptionCreateBody(BaseModel):
  279. model_config = ConfigDict(populate_by_name=True)
  280. protocol_sample_id: Optional[str] = Field(None, alias="protocolSampleId")
  281. reception_time: str = Field(..., alias="receptionTime")
  282. handler_user_id: Optional[str] = Field(None, alias="handlerUserId")
  283. handler_name: Optional[str] = Field(None, alias="handlerName")
  284. receive_temperature: Optional[str] = Field(None, alias="receiveTemperature")
  285. checks: List[ReceptionCheckSubmit] = Field(default_factory=list)
  286. accept_conclusion: str = Field(..., alias="acceptConclusion")
  287. receive_attachments: Optional[str] = Field(None, alias="receiveAttachments")
  288. remark: Optional[str] = None
  289. source_type: Optional[str] = Field("cell_storage", alias="sourceType")
  290. donation_sample_id: Optional[str] = Field(None, alias="donationSampleId")
  291. lab_storage_code: Optional[str] = Field(None, alias="labStorageCode")
  292. class AllocateLabStorageCodesBody(BaseModel):
  293. model_config = ConfigDict(populate_by_name=True)
  294. reception_date: str = Field(..., alias="receptionDate")
  295. count: int = Field(1, ge=1, le=50)
  296. @router.post("/sample-receptions/allocate-lab-storage-codes")
  297. async def allocate_lab_storage_codes(
  298. body: AllocateLabStorageCodesBody,
  299. db: Session = Depends(get_db),
  300. user: dict = Depends(require_admin_or_super_admin),
  301. ):
  302. try:
  303. tenant_no = _get_tenant_no(db, user)
  304. user_id = _get_current_user_id(user)
  305. from app.crud import sample_reception_lab_code as lab_code_svc
  306. codes = lab_code_svc.allocate_lab_storage_codes(
  307. db,
  308. tenant_no=tenant_no,
  309. reception_date=body.reception_date,
  310. count=body.count,
  311. user_id=user_id,
  312. )
  313. return success_response({"codes": codes})
  314. except PermissionError as e:
  315. return error_response(403, str(e))
  316. except ValueError as e:
  317. return error_response(400, str(e))
  318. except Exception as e:
  319. return error_response(400, str(e))
  320. @router.post("/sample-receptions")
  321. async def post_sample_reception(
  322. body: SampleReceptionCreateBody,
  323. db: Session = Depends(get_db),
  324. user: dict = Depends(require_admin_or_super_admin),
  325. ):
  326. try:
  327. tenant_no = _get_tenant_no(db, user)
  328. user_id = _get_current_user_id(user)
  329. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  330. dept_id = (sys_user.dept_id or "") if sys_user else ""
  331. rt = parse_datetime(body.reception_time)
  332. if not rt:
  333. return error_response(400, "样本接收时间格式无效")
  334. checks_payload = [
  335. {"checkItemId": c.check_item_id, "checked": c.checked} for c in body.checks
  336. ]
  337. obj = sr_svc.create_sample_reception(
  338. db,
  339. reception_source=body.source_type or "cell_storage",
  340. protocol_sample_id=body.protocol_sample_id if body.source_type != "research_donation" else None,
  341. donation_sample_id=body.donation_sample_id if body.source_type == "research_donation" else None,
  342. tenant_no=tenant_no,
  343. user_id=user_id,
  344. create_dept_id=dept_id,
  345. reception_time=rt,
  346. handler_user_id=body.handler_user_id or user_id,
  347. handler_name=body.handler_name,
  348. receive_temperature=body.receive_temperature,
  349. checks=checks_payload,
  350. accept_conclusion=body.accept_conclusion,
  351. receive_attachments=body.receive_attachments,
  352. remark=body.remark,
  353. lab_storage_code=body.lab_storage_code,
  354. )
  355. detail = sr_svc.get_reception_by_id(db, obj.id, tenant_no)
  356. return success_response(detail or {"id": obj.id})
  357. except PermissionError as e:
  358. return error_response(403, str(e))
  359. except ValueError as e:
  360. return error_response(400, str(e))
  361. except Exception as e:
  362. return error_response(400, str(e))
  363. @router.put("/sample-receptions/{reception_id}")
  364. async def put_sample_reception(
  365. reception_id: str,
  366. body: SampleReceptionUpdateBody,
  367. db: Session = Depends(get_db),
  368. user: dict = Depends(require_admin_or_super_admin),
  369. ):
  370. try:
  371. tenant_no = _get_tenant_no(db, user)
  372. user_id = _get_current_user_id(user)
  373. rt = parse_datetime(body.reception_time)
  374. if not rt:
  375. return error_response(400, "样本接收时间格式无效")
  376. checks_payload = [
  377. {"checkItemId": c.check_item_id, "checked": c.checked} for c in body.checks
  378. ]
  379. obj = sr_svc.update_sample_reception(
  380. db,
  381. reception_id=reception_id,
  382. tenant_no=tenant_no,
  383. user_id=user_id,
  384. reception_time=rt,
  385. handler_user_id=body.handler_user_id or user_id,
  386. handler_name=body.handler_name,
  387. receive_temperature=body.receive_temperature,
  388. checks=checks_payload,
  389. accept_conclusion=body.accept_conclusion,
  390. receive_attachments=body.receive_attachments,
  391. remark=body.remark,
  392. lab_storage_code=body.lab_storage_code,
  393. )
  394. detail = sr_svc.get_reception_by_id(db, obj.id, tenant_no)
  395. return success_response(detail or {"id": obj.id})
  396. except PermissionError as e:
  397. return error_response(403, str(e))
  398. except ValueError as e:
  399. return error_response(400, str(e))
  400. except Exception as e:
  401. return error_response(400, str(e))