sample_production_reception.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  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": _dec(row.inoculation_amount),
  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 = data.get("inoculationAmount") if "inoculationAmount" in allowed else None
  175. ct = data.get("cryopreservationTime")
  176. row.cryopreservation_time = ct if "cryopreservationTime" in allowed and ct else None
  177. row.storage_total_tubes = (
  178. _int_val(data.get("storageTotalTubes")) if "storageTotalTubes" in allowed else None
  179. )
  180. row.available_cell_count_1e7 = (
  181. data.get("availableCellCount1e7") if "availableCellCount1e7" in allowed else None
  182. )
  183. row.original_blood_volume_ml = (
  184. data.get("originalBloodVolumeMl") if "originalBloodVolumeMl" in allowed else None
  185. )
  186. row.nucleated_cell_count_1e8 = (
  187. data.get("nucleatedCellCount1e8") if "nucleatedCellCount1e8" in allowed else None
  188. )
  189. def get_by_reception_id(
  190. db: Session, reception_id: str, tenant_no: str
  191. ) -> Optional[Dict[str, Any]]:
  192. row = (
  193. db.query(BizSampleProductionReception)
  194. .filter(
  195. BizSampleProductionReception.reception_id == reception_id,
  196. BizSampleProductionReception.tenant_no == tenant_no,
  197. BizSampleProductionReception.del_flag == 0,
  198. )
  199. .first()
  200. )
  201. return _row_to_dict(row) if row else None
  202. def get_map_by_reception_ids(
  203. db: Session, reception_ids: List[str], tenant_no: str
  204. ) -> Dict[str, Dict[str, Any]]:
  205. if not reception_ids:
  206. return {}
  207. rows = (
  208. db.query(BizSampleProductionReception)
  209. .filter(
  210. BizSampleProductionReception.reception_id.in_(reception_ids),
  211. BizSampleProductionReception.tenant_no == tenant_no,
  212. BizSampleProductionReception.del_flag == 0,
  213. )
  214. .all()
  215. )
  216. return {r.reception_id: _row_to_dict(r) for r in rows}
  217. def get_by_id(db: Session, row_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
  218. row = (
  219. db.query(BizSampleProductionReception)
  220. .filter(
  221. BizSampleProductionReception.id == row_id,
  222. BizSampleProductionReception.tenant_no == tenant_no,
  223. BizSampleProductionReception.del_flag == 0,
  224. )
  225. .first()
  226. )
  227. return _row_to_dict(row) if row else None
  228. def create_production_reception(
  229. db: Session,
  230. *,
  231. reception_id: str,
  232. tenant_no: str,
  233. user_id: Optional[str],
  234. create_dept_id: str,
  235. data: Dict[str, Any],
  236. sample_type_name: str = "",
  237. ) -> BizSampleProductionReception:
  238. qc = _get_qc_reception(db, reception_id=reception_id, tenant_no=tenant_no)
  239. exists = (
  240. db.query(BizSampleProductionReception)
  241. .filter(
  242. BizSampleProductionReception.reception_id == reception_id,
  243. BizSampleProductionReception.tenant_no == tenant_no,
  244. BizSampleProductionReception.del_flag == 0,
  245. )
  246. .first()
  247. )
  248. if exists:
  249. raise ValueError("该样本已登记生产接收信息,请使用编辑")
  250. source = (qc.reception_source or "cell_storage").strip()
  251. type_name = (sample_type_name or data.get("sampleTypeName") or "").strip()
  252. _validate_payload(data, source=source, sample_type_name=type_name)
  253. handler_name = str(data.get("handlerName") or "").strip()
  254. handler_user_id = _validate_handler_user_id(
  255. db, str(data.get("handlerUserId") or ""), tenant_no
  256. )
  257. if handler_user_id and not handler_name:
  258. u = db.query(SysUser).filter(SysUser.id == handler_user_id).first()
  259. if u:
  260. handler_name = u.nick_name or ""
  261. row = BizSampleProductionReception(
  262. id=generate_uuid(),
  263. reception_id=reception_id,
  264. sample_code=(qc.sample_code or "").strip() or None,
  265. reception_source=source,
  266. sample_type_name=type_name or None,
  267. tenant_no=tenant_no,
  268. create_by=user_id,
  269. create_dept_id=create_dept_id or "",
  270. update_by=user_id,
  271. )
  272. data = dict(data)
  273. data["handlerUserId"] = handler_user_id
  274. data["handlerName"] = handler_name
  275. _apply_payload_to_row(row, data, source=source, sample_type_name=type_name)
  276. db.add(row)
  277. db.commit()
  278. db.refresh(row)
  279. return row
  280. def update_production_reception(
  281. db: Session,
  282. *,
  283. row_id: str,
  284. tenant_no: str,
  285. user_id: Optional[str],
  286. data: Dict[str, Any],
  287. ) -> BizSampleProductionReception:
  288. row = (
  289. db.query(BizSampleProductionReception)
  290. .filter(
  291. BizSampleProductionReception.id == row_id,
  292. BizSampleProductionReception.tenant_no == tenant_no,
  293. BizSampleProductionReception.del_flag == 0,
  294. )
  295. .first()
  296. )
  297. if not row:
  298. raise ValueError("生产接收记录不存在")
  299. _get_qc_reception(db, reception_id=row.reception_id, tenant_no=tenant_no)
  300. source = (row.reception_source or "cell_storage").strip()
  301. type_name = (row.sample_type_name or "").strip()
  302. _validate_payload(data, source=source, sample_type_name=type_name)
  303. handler_name = str(data.get("handlerName") or "").strip()
  304. handler_user_id = _validate_handler_user_id(
  305. db, str(data.get("handlerUserId") or ""), tenant_no
  306. )
  307. if handler_user_id and not handler_name:
  308. u = db.query(SysUser).filter(SysUser.id == handler_user_id).first()
  309. if u:
  310. handler_name = u.nick_name or ""
  311. data = dict(data)
  312. data["handlerUserId"] = handler_user_id
  313. data["handlerName"] = handler_name
  314. _apply_payload_to_row(row, data, source=source, sample_type_name=type_name)
  315. row.update_by = user_id
  316. row.update_time = datetime.now()
  317. db.add(row)
  318. db.commit()
  319. db.refresh(row)
  320. return row