sample_production_reception.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. """生产部门样本接收 CRUD"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from typing import Any, Dict, List, Optional, Set, Tuple
  5. from sqlalchemy.orm import Session
  6. from app.core.utils import generate_uuid
  7. from app.models.models import BizSampleProductionReception, BizSampleReception, SysUser
  8. from app.crud import production_task_step_assignee as step_assignee_svc
  9. PRODUCTION_RECEPTION_HANDLER_ROLE = step_assignee_svc.PRODUCTION_STEP_ASSIGNEE_ROLE
  10. SAMPLE_TYPE_PERINATAL_HSC = "围产期造血干细胞"
  11. SAMPLE_TYPE_UC_MSC = "脐带间充质干细胞"
  12. INFECTIOUS_FOUR_LABELS = {"zc": "正常", "bzc": "不正常"}
  13. # (source, sample_type_name) -> required writable field keys (camelCase API)
  14. FIELD_PROFILES: Dict[Tuple[str, str], List[str]] = {
  15. ("cell_storage", SAMPLE_TYPE_PERINATAL_HSC): [
  16. "originalBloodWeightG",
  17. "hesVolumeMl",
  18. "bloodType",
  19. "infectiousFourTestResult",
  20. "enrichedBloodWeightG",
  21. "cryopreservationVolumeMl",
  22. ],
  23. ("cell_storage", SAMPLE_TYPE_UC_MSC): [
  24. "inoculationAmount",
  25. "cryopreservationTime",
  26. "storageTotalTubes",
  27. "availableCellCount1e7",
  28. ],
  29. ("research_donation", SAMPLE_TYPE_UC_MSC): [
  30. "inoculationAmount",
  31. "cryopreservationTime",
  32. ],
  33. ("research_donation", SAMPLE_TYPE_PERINATAL_HSC): [
  34. "originalBloodVolumeMl",
  35. "nucleatedCellCount1e8",
  36. ],
  37. }
  38. ALL_TYPE_FIELDS = {
  39. "originalBloodWeightG",
  40. "hesVolumeMl",
  41. "bloodType",
  42. "infectiousFourTestResult",
  43. "enrichedBloodWeightG",
  44. "cryopreservationVolumeMl",
  45. "inoculationAmount",
  46. "cryopreservationTime",
  47. "storageTotalTubes",
  48. "availableCellCount1e7",
  49. "originalBloodVolumeMl",
  50. "nucleatedCellCount1e8",
  51. }
  52. def _fmt_dt(t: Optional[datetime]) -> str:
  53. return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
  54. def _dec(v: Any) -> Optional[float]:
  55. if v is None:
  56. return None
  57. return float(v)
  58. def _int_val(v: Any) -> Optional[int]:
  59. if v is None or v == "":
  60. return None
  61. return int(v)
  62. def resolve_field_profile(source: str, sample_type_name: str) -> List[str]:
  63. src = (source or "cell_storage").strip()
  64. name = (sample_type_name or "").strip()
  65. return list(FIELD_PROFILES.get((src, name), []))
  66. def _row_to_dict(row: BizSampleProductionReception) -> Dict[str, Any]:
  67. result = row.infectious_four_test_result or ""
  68. return {
  69. "id": row.id,
  70. "receptionId": row.reception_id,
  71. "sampleCode": row.sample_code or "",
  72. "receptionSource": row.reception_source or "cell_storage",
  73. "sampleTypeName": row.sample_type_name or "",
  74. "handlerUserId": row.handler_user_id or "",
  75. "handlerName": row.handler_name or "",
  76. "processTime": _fmt_dt(row.process_time),
  77. "originalBloodWeightG": _dec(row.original_blood_weight_g),
  78. "hesVolumeMl": _dec(row.hes_volume_ml),
  79. "bloodType": row.blood_type or "",
  80. "infectiousFourTestResult": result,
  81. "infectiousFourTestResultLabel": INFECTIOUS_FOUR_LABELS.get(result, result),
  82. "enrichedBloodWeightG": _dec(row.enriched_blood_weight_g),
  83. "cryopreservationVolumeMl": _dec(row.cryopreservation_volume_ml),
  84. "inoculationAmount": row.inoculation_amount or "",
  85. "cryopreservationTime": _fmt_dt(row.cryopreservation_time),
  86. "storageTotalTubes": row.storage_total_tubes,
  87. "availableCellCount1e7": _dec(row.available_cell_count_1e7),
  88. "originalBloodVolumeMl": _dec(row.original_blood_volume_ml),
  89. "nucleatedCellCount1e8": _dec(row.nucleated_cell_count_1e8),
  90. "remark": row.remark or "",
  91. "createTime": _fmt_dt(row.create_time),
  92. "updateTime": _fmt_dt(row.update_time),
  93. }
  94. def _validate_handler_user_id(
  95. db: Session, handler_user_id: str, tenant_no: str
  96. ) -> str:
  97. uid = (handler_user_id or "").strip()
  98. if not uid:
  99. raise ValueError("请选择处理人")
  100. step_assignee_svc.validate_assignee_user_ids(
  101. db, [uid], tenant_no, required_role_name=PRODUCTION_RECEPTION_HANDLER_ROLE
  102. )
  103. return uid
  104. def _get_qc_reception(
  105. db: Session, *, reception_id: str, tenant_no: str
  106. ) -> BizSampleReception:
  107. rec = (
  108. db.query(BizSampleReception)
  109. .filter(
  110. BizSampleReception.id == reception_id,
  111. BizSampleReception.tenant_no == tenant_no,
  112. BizSampleReception.del_flag == 0,
  113. )
  114. .first()
  115. )
  116. if not rec:
  117. raise ValueError("关联的质检接收记录不存在")
  118. if (rec.accept_conclusion or "").strip() == "bj":
  119. raise ValueError("样本已拒收,无法登记生产接收信息")
  120. return rec
  121. def _validate_payload(
  122. data: Dict[str, Any],
  123. *,
  124. source: str,
  125. sample_type_name: str,
  126. ) -> None:
  127. required = {"handlerUserId", "processTime"} | set(
  128. resolve_field_profile(source, sample_type_name)
  129. )
  130. for key in required:
  131. val = data.get(key)
  132. if val is None or (isinstance(val, str) and not val.strip()):
  133. raise ValueError(f"请填写必填字段:{key}")
  134. infectious = data.get("infectiousFourTestResult")
  135. if infectious is not None and str(infectious).strip():
  136. if str(infectious).strip() not in INFECTIOUS_FOUR_LABELS:
  137. raise ValueError("传染四项检测结果无效")
  138. def _apply_payload_to_row(
  139. row: BizSampleProductionReception,
  140. data: Dict[str, Any],
  141. *,
  142. source: str,
  143. sample_type_name: str,
  144. ) -> None:
  145. profile_keys = set(resolve_field_profile(source, sample_type_name))
  146. allowed = profile_keys | {"handlerUserId", "handlerName", "processTime", "remark"}
  147. row.handler_user_id = str(data.get("handlerUserId") or "").strip() or None
  148. row.handler_name = str(data.get("handlerName") or "").strip() or None
  149. pt = data.get("processTime")
  150. if isinstance(pt, datetime):
  151. row.process_time = pt
  152. row.remark = str(data.get("remark") or "").strip() or None
  153. def _set_decimal(attr: str, key: str) -> None:
  154. if key not in allowed:
  155. setattr(row, attr, None)
  156. return
  157. setattr(row, attr, data.get(key))
  158. row.original_blood_weight_g = data.get("originalBloodWeightG") if "originalBloodWeightG" in allowed else None
  159. row.hes_volume_ml = data.get("hesVolumeMl") if "hesVolumeMl" in allowed else None
  160. row.blood_type = (
  161. str(data.get("bloodType") or "").strip() or None if "bloodType" in allowed else None
  162. )
  163. row.infectious_four_test_result = (
  164. str(data.get("infectiousFourTestResult") or "").strip() or None
  165. if "infectiousFourTestResult" in allowed
  166. else None
  167. )
  168. row.enriched_blood_weight_g = (
  169. data.get("enrichedBloodWeightG") if "enrichedBloodWeightG" in allowed else None
  170. )
  171. row.cryopreservation_volume_ml = (
  172. data.get("cryopreservationVolumeMl") if "cryopreservationVolumeMl" in allowed else None
  173. )
  174. row.inoculation_amount = (
  175. (str(data.get("inoculationAmount") or "").strip() or None)
  176. if "inoculationAmount" in allowed
  177. else None
  178. )
  179. ct = data.get("cryopreservationTime")
  180. row.cryopreservation_time = ct if "cryopreservationTime" in allowed and ct else None
  181. row.storage_total_tubes = (
  182. _int_val(data.get("storageTotalTubes")) if "storageTotalTubes" in allowed else None
  183. )
  184. row.available_cell_count_1e7 = (
  185. data.get("availableCellCount1e7") if "availableCellCount1e7" in allowed else None
  186. )
  187. row.original_blood_volume_ml = (
  188. data.get("originalBloodVolumeMl") if "originalBloodVolumeMl" in allowed else None
  189. )
  190. row.nucleated_cell_count_1e8 = (
  191. data.get("nucleatedCellCount1e8") if "nucleatedCellCount1e8" in allowed else None
  192. )
  193. def get_by_reception_id(
  194. db: Session, reception_id: str, tenant_no: str
  195. ) -> Optional[Dict[str, Any]]:
  196. row = (
  197. db.query(BizSampleProductionReception)
  198. .filter(
  199. BizSampleProductionReception.reception_id == reception_id,
  200. BizSampleProductionReception.tenant_no == tenant_no,
  201. BizSampleProductionReception.del_flag == 0,
  202. )
  203. .first()
  204. )
  205. return _row_to_dict(row) if row else None
  206. def get_map_by_reception_ids(
  207. db: Session, reception_ids: List[str], tenant_no: str
  208. ) -> Dict[str, Dict[str, Any]]:
  209. if not reception_ids:
  210. return {}
  211. rows = (
  212. db.query(BizSampleProductionReception)
  213. .filter(
  214. BizSampleProductionReception.reception_id.in_(reception_ids),
  215. BizSampleProductionReception.tenant_no == tenant_no,
  216. BizSampleProductionReception.del_flag == 0,
  217. )
  218. .all()
  219. )
  220. return {r.reception_id: _row_to_dict(r) for r in rows}
  221. def get_by_id(db: Session, row_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
  222. row = (
  223. db.query(BizSampleProductionReception)
  224. .filter(
  225. BizSampleProductionReception.id == row_id,
  226. BizSampleProductionReception.tenant_no == tenant_no,
  227. BizSampleProductionReception.del_flag == 0,
  228. )
  229. .first()
  230. )
  231. return _row_to_dict(row) if row else None
  232. def create_production_reception(
  233. db: Session,
  234. *,
  235. reception_id: str,
  236. tenant_no: str,
  237. user_id: Optional[str],
  238. create_dept_id: str,
  239. data: Dict[str, Any],
  240. sample_type_name: str = "",
  241. ) -> BizSampleProductionReception:
  242. qc = _get_qc_reception(db, reception_id=reception_id, tenant_no=tenant_no)
  243. exists = (
  244. db.query(BizSampleProductionReception)
  245. .filter(
  246. BizSampleProductionReception.reception_id == reception_id,
  247. BizSampleProductionReception.tenant_no == tenant_no,
  248. BizSampleProductionReception.del_flag == 0,
  249. )
  250. .first()
  251. )
  252. if exists:
  253. raise ValueError("该样本已登记生产接收信息,请使用编辑")
  254. source = (qc.reception_source or "cell_storage").strip()
  255. type_name = (sample_type_name or data.get("sampleTypeName") or "").strip()
  256. _validate_payload(data, source=source, sample_type_name=type_name)
  257. handler_name = str(data.get("handlerName") or "").strip()
  258. handler_user_id = _validate_handler_user_id(
  259. db, str(data.get("handlerUserId") or ""), tenant_no
  260. )
  261. if handler_user_id and not handler_name:
  262. u = db.query(SysUser).filter(SysUser.id == handler_user_id).first()
  263. if u:
  264. handler_name = u.nick_name or ""
  265. row = BizSampleProductionReception(
  266. id=generate_uuid(),
  267. reception_id=reception_id,
  268. sample_code=(qc.sample_code or "").strip() or None,
  269. reception_source=source,
  270. sample_type_name=type_name or None,
  271. tenant_no=tenant_no,
  272. create_by=user_id,
  273. create_dept_id=create_dept_id or "",
  274. update_by=user_id,
  275. )
  276. data = dict(data)
  277. data["handlerUserId"] = handler_user_id
  278. data["handlerName"] = handler_name
  279. _apply_payload_to_row(row, data, source=source, sample_type_name=type_name)
  280. db.add(row)
  281. db.commit()
  282. db.refresh(row)
  283. return row
  284. def update_production_reception(
  285. db: Session,
  286. *,
  287. row_id: str,
  288. tenant_no: str,
  289. user_id: Optional[str],
  290. data: Dict[str, Any],
  291. ) -> BizSampleProductionReception:
  292. row = (
  293. db.query(BizSampleProductionReception)
  294. .filter(
  295. BizSampleProductionReception.id == row_id,
  296. BizSampleProductionReception.tenant_no == tenant_no,
  297. BizSampleProductionReception.del_flag == 0,
  298. )
  299. .first()
  300. )
  301. if not row:
  302. raise ValueError("生产接收记录不存在")
  303. _get_qc_reception(db, reception_id=row.reception_id, tenant_no=tenant_no)
  304. source = (row.reception_source or "cell_storage").strip()
  305. type_name = (row.sample_type_name or "").strip()
  306. _validate_payload(data, source=source, sample_type_name=type_name)
  307. handler_name = str(data.get("handlerName") or "").strip()
  308. handler_user_id = _validate_handler_user_id(
  309. db, str(data.get("handlerUserId") or ""), tenant_no
  310. )
  311. if handler_user_id and not handler_name:
  312. u = db.query(SysUser).filter(SysUser.id == handler_user_id).first()
  313. if u:
  314. handler_name = u.nick_name or ""
  315. data = dict(data)
  316. data["handlerUserId"] = handler_user_id
  317. data["handlerName"] = handler_name
  318. _apply_payload_to_row(row, data, source=source, sample_type_name=type_name)
  319. row.update_by = user_id
  320. row.update_time = datetime.now()
  321. db.add(row)
  322. db.commit()
  323. db.refresh(row)
  324. return row