sample_code.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. """样本编码池 CRUD(biz_sample_code)"""
  2. from __future__ import annotations
  3. import math
  4. from datetime import datetime
  5. from typing import Any, Dict, List, Optional, Tuple
  6. from sqlalchemy import and_, func, or_
  7. from sqlalchemy.orm import Session
  8. from app.core.utils import generate_uuid
  9. from app.models.models import BizSampleCode, SysUser
  10. def _resolve_issued_user_display_name(
  11. db: Session,
  12. user_id: Optional[str],
  13. fallback_name: Optional[str] = None,
  14. ) -> Optional[str]:
  15. name = (fallback_name or "").strip()
  16. if name:
  17. return name
  18. uid = (user_id or "").strip()
  19. if not uid:
  20. return None
  21. u = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
  22. if not u:
  23. return None
  24. return (u.nick_name or u.user_no or "").strip() or None
  25. def _parse_issued_time(value: Optional[str]) -> Optional[datetime]:
  26. raw = (value or "").strip()
  27. if not raw:
  28. return None
  29. for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"):
  30. try:
  31. return datetime.strptime(raw, fmt)
  32. except ValueError:
  33. continue
  34. return None
  35. def _row_to_dict(r: BizSampleCode) -> Dict[str, Any]:
  36. return {
  37. "id": r.id,
  38. "sampleCode": r.sample_code,
  39. "sampleCodeType": r.sample_code_type or "",
  40. "issued": int(r.issued or 0),
  41. "issuedUserId": r.issued_user_id or "",
  42. "issuedUserName": r.issued_user_name or "",
  43. "issuedTime": r.issued_time.strftime("%Y-%m-%d %H:%M:%S") if r.issued_time else "",
  44. "tenantNo": r.tenant_no,
  45. "orderNum": r.order_num if r.order_num is not None else 0,
  46. "remark": r.remark or "",
  47. "createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
  48. "updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
  49. }
  50. def sample_code_exists(db: Session, code: str, tenant_no: str, exclude_id: Optional[str] = None) -> bool:
  51. v = (code or "").strip()
  52. if not v:
  53. return False
  54. q = db.query(BizSampleCode).filter(
  55. and_(BizSampleCode.sample_code == v, BizSampleCode.tenant_no == tenant_no, BizSampleCode.del_flag == 0)
  56. )
  57. if exclude_id:
  58. q = q.filter(BizSampleCode.id != exclude_id)
  59. return q.first() is not None
  60. def list_sample_codes(
  61. db: Session,
  62. *,
  63. page: int = 1,
  64. page_size: int = 10,
  65. keyword: Optional[str] = None,
  66. tenant_no: Optional[str] = None,
  67. ) -> Tuple[List[Dict[str, Any]], int]:
  68. def _filtered():
  69. q0 = db.query(BizSampleCode).filter(BizSampleCode.del_flag == 0)
  70. if tenant_no:
  71. q0 = q0.filter(BizSampleCode.tenant_no == tenant_no)
  72. kw = (keyword or "").strip()
  73. if kw:
  74. like = f"%{kw}%"
  75. kw_conds = [
  76. BizSampleCode.sample_code.like(like),
  77. BizSampleCode.remark.like(like),
  78. ]
  79. if tenant_no:
  80. from app.crud import sample_reception as reception_svc
  81. lab_codes = reception_svc.sample_codes_matching_lab_storage_keyword(
  82. db, tenant_no, kw
  83. )
  84. if lab_codes:
  85. kw_conds.append(BizSampleCode.sample_code.in_(lab_codes))
  86. q0 = q0.filter(or_(*kw_conds))
  87. return q0
  88. total = _filtered().order_by(None).with_entities(func.count(BizSampleCode.id)).scalar() or 0
  89. page = max(1, page)
  90. page_size = min(max(1, page_size), 100)
  91. rows = (
  92. _filtered()
  93. .order_by(BizSampleCode.create_time.desc(), BizSampleCode.sample_code.desc())
  94. .offset((page - 1) * page_size)
  95. .limit(page_size)
  96. .all()
  97. )
  98. items = [_row_to_dict(x) for x in rows]
  99. if items and tenant_no:
  100. from app.crud import sample_reception as reception_svc
  101. codes = [i["sampleCode"] for i in items if i.get("sampleCode")]
  102. code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(db, tenant_no, codes)
  103. for item in items:
  104. sc = (item.get("sampleCode") or "").strip()
  105. item["labStorageCode"] = code_map.get(sc, "") if sc else ""
  106. return items, int(total)
  107. def get_sample_code(db: Session, row_id: str, tenant_no: Optional[str] = None) -> Optional[BizSampleCode]:
  108. q = db.query(BizSampleCode).filter(and_(BizSampleCode.id == row_id, BizSampleCode.del_flag == 0))
  109. if tenant_no:
  110. q = q.filter(BizSampleCode.tenant_no == tenant_no)
  111. return q.first()
  112. def create_sample_code(
  113. db: Session,
  114. *,
  115. sample_code: str,
  116. sample_code_type: Optional[str] = None,
  117. issued: int = 0,
  118. tenant_no: str = "zkxw",
  119. order_num: Optional[int] = None,
  120. remark: Optional[str] = None,
  121. user_id: Optional[str] = None,
  122. issued_user_id: Optional[str] = None,
  123. issued_user_name: Optional[str] = None,
  124. issued_time: Optional[str] = None,
  125. ) -> BizSampleCode:
  126. sc = (sample_code or "").strip()
  127. if not sc:
  128. raise ValueError("样本编码不能为空")
  129. if sample_code_exists(db, sc, tenant_no):
  130. raise ValueError("样本编码已存在")
  131. code_type = (sample_code_type or "").strip() or None
  132. if code_type and code_type not in ("zxbm", "qdbm"):
  133. raise ValueError("样本编码类型无效,仅支持 zxbm/qdbm")
  134. is_issued = 1 if issued else 0
  135. issue_uid: Optional[str] = None
  136. issue_uname: Optional[str] = None
  137. issue_time: Optional[datetime] = None
  138. if is_issued:
  139. issue_uid = (issued_user_id or "").strip() or None
  140. if not issue_uid:
  141. raise ValueError("请选择下发用户")
  142. issue_uname = _resolve_issued_user_display_name(db, issue_uid, issued_user_name)
  143. issue_time = _parse_issued_time(issued_time) or datetime.now()
  144. obj = BizSampleCode(
  145. id=generate_uuid(),
  146. sample_code=sc,
  147. sample_code_type=code_type,
  148. issued=is_issued,
  149. issued_user_id=issue_uid,
  150. issued_user_name=issue_uname,
  151. issued_time=issue_time,
  152. tenant_no=tenant_no,
  153. order_num=order_num if order_num is not None else 0,
  154. remark=(remark or "").strip() or None,
  155. create_by=user_id,
  156. create_dept_id="",
  157. create_time=datetime.now(),
  158. update_by=user_id,
  159. update_time=datetime.now(),
  160. del_flag=0,
  161. )
  162. db.add(obj)
  163. db.commit()
  164. db.refresh(obj)
  165. return obj
  166. def update_sample_code(
  167. db: Session,
  168. row_id: str,
  169. *,
  170. sample_code: Optional[str] = None,
  171. issued: Optional[int] = None,
  172. order_num: Optional[int] = None,
  173. remark: Optional[str] = None,
  174. user_id: Optional[str] = None,
  175. tenant_no: Optional[str] = None,
  176. issued_user_id: Optional[str] = None,
  177. issued_user_name: Optional[str] = None,
  178. issued_time: Optional[str] = None,
  179. ) -> BizSampleCode:
  180. obj = get_sample_code(db, row_id, tenant_no=tenant_no)
  181. if not obj:
  182. raise ValueError("记录不存在")
  183. if sample_code is not None:
  184. sc = sample_code.strip()
  185. if not sc:
  186. raise ValueError("样本编码不能为空")
  187. if sc != obj.sample_code and sample_code_exists(db, sc, obj.tenant_no, exclude_id=row_id):
  188. raise ValueError("样本编码已存在")
  189. obj.sample_code = sc
  190. if issued is not None:
  191. obj.issued = 1 if issued else 0
  192. if not obj.issued:
  193. obj.issued_user_id = None
  194. obj.issued_user_name = None
  195. obj.issued_time = None
  196. if order_num is not None:
  197. obj.order_num = order_num
  198. if remark is not None:
  199. obj.remark = remark.strip() or None
  200. if issued_user_id is not None:
  201. obj.issued_user_id = issued_user_id or None
  202. if issued_user_name is not None:
  203. obj.issued_user_name = issued_user_name or None
  204. if obj.issued == 1 and obj.issued_user_id and not (obj.issued_user_name or "").strip():
  205. obj.issued_user_name = _resolve_issued_user_display_name(db, obj.issued_user_id)
  206. if issued_time is not None:
  207. obj.issued_time = _parse_issued_time(issued_time) if (issued_time or "").strip() else None
  208. if obj.issued == 1 and obj.issued_user_id and not obj.issued_time:
  209. obj.issued_time = datetime.now()
  210. obj.update_by = user_id
  211. obj.update_time = datetime.now()
  212. db.add(obj)
  213. db.commit()
  214. db.refresh(obj)
  215. return obj
  216. def soft_delete_sample_code(db: Session, row_id: str, tenant_no: Optional[str] = None) -> None:
  217. obj = get_sample_code(db, row_id, tenant_no=tenant_no)
  218. if not obj:
  219. raise ValueError("记录不存在")
  220. obj.del_flag = 1
  221. obj.update_time = datetime.now()
  222. db.add(obj)
  223. db.commit()
  224. def batch_generate_sample_codes(
  225. db: Session,
  226. *,
  227. sample_code_type: str,
  228. middle: str,
  229. seq_start: int,
  230. seq_end: int,
  231. digit_count: int = 6,
  232. tenant_no: str = "zkxw",
  233. user_id: Optional[str] = None,
  234. ) -> Dict[str, Any]:
  235. """批量生成样本编码: 类型前缀 + 中间段 + 补零序号"""
  236. t = (sample_code_type or "").strip()
  237. if t not in ("zxbm", "qdbm"):
  238. raise ValueError("样本编码类型无效,仅支持 zxbm/qdbm")
  239. m = (middle or "").strip()
  240. if not m:
  241. raise ValueError("中间段不能为空")
  242. if seq_start < 0 or seq_end < 0:
  243. raise ValueError("序号不能为负数")
  244. if seq_start > seq_end:
  245. raise ValueError("开始序号不能大于结束序号")
  246. if digit_count < 1:
  247. raise ValueError("序号位数不能小于1")
  248. count = seq_end - seq_start + 1
  249. if count > 10000:
  250. raise ValueError("单次批量生成不能超过10000条")
  251. prefix_map = {"zxbm": "ZKZX", "qdbm": "ZKQD"}
  252. prefix = prefix_map[t]
  253. generated_codes = []
  254. for i in range(seq_start, seq_end + 1):
  255. seq_str = str(i).zfill(digit_count)
  256. full_code = f"{prefix}{m}{seq_str}"
  257. generated_codes.append(full_code)
  258. existing = set(
  259. row.sample_code
  260. for row in db.query(BizSampleCode.sample_code)
  261. .filter(
  262. BizSampleCode.tenant_no == tenant_no,
  263. BizSampleCode.del_flag == 0,
  264. BizSampleCode.sample_code.in_(generated_codes),
  265. )
  266. .all()
  267. )
  268. now = datetime.now()
  269. created = []
  270. skipped = []
  271. for code in generated_codes:
  272. if code in existing:
  273. skipped.append(code)
  274. continue
  275. obj = BizSampleCode(
  276. id=generate_uuid(),
  277. sample_code=code,
  278. sample_code_type=t,
  279. issued=0,
  280. tenant_no=tenant_no,
  281. order_num=0,
  282. remark=None,
  283. create_by=user_id,
  284. create_dept_id="",
  285. create_time=now,
  286. update_by=user_id,
  287. update_time=now,
  288. del_flag=0,
  289. )
  290. created.append(obj)
  291. if created:
  292. db.bulk_save_objects(created)
  293. db.commit()
  294. return {
  295. "created": len(created),
  296. "skipped": len(skipped),
  297. "skippedCodes": skipped,
  298. }
  299. def batch_issue_sample_codes(
  300. db: Session,
  301. *,
  302. ids: List[str],
  303. issued_user_id: str,
  304. issued_user_name: Optional[str] = None,
  305. tenant_no: Optional[str] = None,
  306. user_id: Optional[str] = None,
  307. ) -> Dict[str, Any]:
  308. """批量下发样本编码"""
  309. if not ids:
  310. raise ValueError("请选择要下发的样本编码")
  311. uid = (issued_user_id or "").strip()
  312. if not uid:
  313. raise ValueError("请选择下发用户")
  314. now = datetime.now()
  315. issued_count = 0
  316. already_issued = []
  317. for row_id in ids:
  318. obj = get_sample_code(db, row_id, tenant_no=tenant_no)
  319. if not obj:
  320. continue
  321. if obj.issued == 1:
  322. already_issued.append(obj.sample_code)
  323. continue
  324. obj.issued = 1
  325. obj.issued_user_id = uid
  326. obj.issued_user_name = (issued_user_name or "").strip() or None
  327. obj.issued_time = now
  328. obj.update_by = user_id
  329. obj.update_time = now
  330. db.add(obj)
  331. issued_count += 1
  332. if issued_count > 0:
  333. db.commit()
  334. return {
  335. "issuedCount": issued_count,
  336. "alreadyIssued": already_issued,
  337. }
  338. def batch_revoke_sample_codes(
  339. db: Session,
  340. *,
  341. ids: List[str],
  342. tenant_no: Optional[str] = None,
  343. user_id: Optional[str] = None,
  344. ) -> Dict[str, Any]:
  345. if not ids:
  346. raise ValueError("请选择要收回的样本编码")
  347. now = datetime.now()
  348. revoked_count = 0
  349. not_issued = []
  350. for row_id in ids:
  351. obj = get_sample_code(db, row_id, tenant_no=tenant_no)
  352. if not obj:
  353. continue
  354. if obj.issued != 1:
  355. not_issued.append(obj.sample_code)
  356. continue
  357. obj.issued = 0
  358. obj.issued_user_id = None
  359. obj.issued_user_name = None
  360. obj.issued_time = None
  361. obj.update_by = user_id
  362. obj.update_time = now
  363. db.add(obj)
  364. revoked_count += 1
  365. if revoked_count > 0:
  366. db.commit()
  367. return {
  368. "revokedCount": revoked_count,
  369. "notIssued": not_issued,
  370. }
  371. def total_pages(total: int, page_size: int) -> int:
  372. if total <= 0:
  373. return 0
  374. return int(math.ceil(total / float(page_size)))