sample_reception.py 16 KB

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