reagent.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. """试剂 CRUD(biz_reagent)"""
  2. from __future__ import annotations
  3. import math
  4. from datetime import datetime
  5. from decimal import Decimal
  6. from typing import Any, Dict, List, Optional, Tuple
  7. from sqlalchemy import and_, func, or_
  8. from sqlalchemy.orm import Session
  9. from app.core.utils import generate_uuid
  10. from app.models.models import BizReagent, BizReagentStockLog
  11. def _dec(v: Any) -> float:
  12. if v is None:
  13. return 0.0
  14. return float(v)
  15. def _norm_status(v: Any) -> str:
  16. if v is None:
  17. return "1"
  18. if isinstance(v, bool):
  19. return "1" if v else "0"
  20. if isinstance(v, (int, float)):
  21. if int(v) == 0:
  22. return "0"
  23. if int(v) == 1:
  24. return "1"
  25. s = str(v).strip()
  26. if s in ("0", "1"):
  27. return s
  28. return "1"
  29. def _is_enabled_status(v: Any) -> bool:
  30. return _norm_status(v) == "1"
  31. def _row_to_dict(r: BizReagent) -> Dict[str, Any]:
  32. return {
  33. "id": r.id,
  34. "reagentCode": r.reagent_code,
  35. "reagentName": r.reagent_name,
  36. "spec": r.spec or "",
  37. "standardUsage": r.standard_usage or "",
  38. "unit": r.unit or "",
  39. "description": r.description or "",
  40. "stockQty": _dec(r.stock_qty),
  41. "warningQty": _dec(r.warning_qty),
  42. "status": _norm_status(r.status),
  43. "tenantNo": r.tenant_no,
  44. "orderNum": r.order_num if r.order_num is not None else 0,
  45. "remark": r.remark or "",
  46. "createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
  47. "updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
  48. }
  49. def list_reagents(
  50. db: Session,
  51. *,
  52. page: int = 1,
  53. page_size: int = 10,
  54. keyword: Optional[str] = None,
  55. tenant_no: Optional[str] = None,
  56. user_info=None,
  57. ) -> Tuple[List[Dict[str, Any]], int]:
  58. def _filtered():
  59. q0 = db.query(BizReagent).filter(BizReagent.del_flag == 0)
  60. if tenant_no:
  61. q0 = q0.filter(BizReagent.tenant_no == tenant_no)
  62. kw = (keyword or "").strip()
  63. if kw:
  64. like = f"%{kw}%"
  65. q0 = q0.filter(
  66. or_(
  67. BizReagent.reagent_code.like(like),
  68. BizReagent.reagent_name.like(like),
  69. )
  70. )
  71. if user_info:
  72. from app.core.data_scope import apply_data_scope
  73. q0 = apply_data_scope(q0, BizReagent, user_info, db, tenant_no)
  74. return q0
  75. total = _filtered().order_by(None).with_entities(func.count(BizReagent.id)).scalar() or 0
  76. page = max(1, page)
  77. page_size = min(max(1, page_size), 100)
  78. rows = (
  79. _filtered()
  80. .order_by(BizReagent.create_time.desc())
  81. .offset((page - 1) * page_size)
  82. .limit(page_size)
  83. .all()
  84. )
  85. return [_row_to_dict(x) for x in rows], int(total)
  86. def get_reagent(db: Session, reagent_id: str) -> Optional[BizReagent]:
  87. return (
  88. db.query(BizReagent)
  89. .filter(and_(BizReagent.id == reagent_id, BizReagent.del_flag == 0))
  90. .first()
  91. )
  92. def reagent_code_exists(db: Session, code: str, tenant_no: str, exclude_id: Optional[str] = None) -> bool:
  93. q = db.query(BizReagent).filter(
  94. and_(BizReagent.reagent_code == code.strip(), BizReagent.tenant_no == tenant_no, BizReagent.del_flag == 0)
  95. )
  96. if exclude_id:
  97. q = q.filter(BizReagent.id != exclude_id)
  98. return q.first() is not None
  99. def create_reagent(
  100. db: Session,
  101. *,
  102. reagent_code: str,
  103. reagent_name: str,
  104. spec: Optional[str] = None,
  105. standard_usage: Optional[str] = None,
  106. unit: Optional[str] = None,
  107. description: Optional[str] = None,
  108. tenant_no: str = "zkxw",
  109. warning_qty: Optional[float] = None,
  110. status: Optional[str] = None,
  111. order_num: Optional[int] = None,
  112. remark: Optional[str] = None,
  113. user_id: Optional[str] = None,
  114. ) -> BizReagent:
  115. rc = (reagent_code or "").strip()
  116. if not rc:
  117. raise ValueError("试剂编码不能为空")
  118. if reagent_code_exists(db, rc, tenant_no):
  119. raise ValueError("试剂编码已存在")
  120. obj = BizReagent(
  121. id=generate_uuid(),
  122. reagent_code=rc,
  123. reagent_name=(reagent_name or "").strip() or rc,
  124. spec=(spec or "").strip() or None,
  125. standard_usage=(standard_usage or "").strip() or None,
  126. unit=(unit or "").strip() or None,
  127. description=(description or "").strip() or None,
  128. tenant_no=tenant_no,
  129. warning_qty=warning_qty if warning_qty is not None else 0,
  130. status=_norm_status(status),
  131. order_num=order_num if order_num is not None else 0,
  132. remark=(remark or "").strip() or None,
  133. create_by=user_id,
  134. create_dept_id="",
  135. create_time=datetime.now(),
  136. update_by=user_id,
  137. update_time=datetime.now(),
  138. del_flag=0,
  139. )
  140. db.add(obj)
  141. db.commit()
  142. db.refresh(obj)
  143. return obj
  144. def update_reagent(
  145. db: Session,
  146. reagent_id: str,
  147. *,
  148. reagent_code: Optional[str] = None,
  149. reagent_name: Optional[str] = None,
  150. spec: Optional[str] = None,
  151. standard_usage: Optional[str] = None,
  152. unit: Optional[str] = None,
  153. description: Optional[str] = None,
  154. warning_qty: Optional[float] = None,
  155. status: Optional[str] = None,
  156. order_num: Optional[int] = None,
  157. remark: Optional[str] = None,
  158. user_id: Optional[str] = None,
  159. ) -> BizReagent:
  160. obj = get_reagent(db, reagent_id)
  161. if not obj:
  162. raise ValueError("试剂不存在")
  163. if reagent_code is not None:
  164. new_code = reagent_code.strip()
  165. if not new_code:
  166. raise ValueError("试剂编码不能为空")
  167. if new_code != obj.reagent_code and reagent_code_exists(db, new_code, obj.tenant_no, exclude_id=reagent_id):
  168. raise ValueError("试剂编码已存在")
  169. obj.reagent_code = new_code
  170. if reagent_name is not None:
  171. obj.reagent_name = (reagent_name or "").strip() or obj.reagent_name
  172. if spec is not None:
  173. obj.spec = (spec or "").strip() or None
  174. if standard_usage is not None:
  175. obj.standard_usage = (standard_usage or "").strip() or None
  176. if unit is not None:
  177. obj.unit = (unit or "").strip() or None
  178. if description is not None:
  179. obj.description = (description or "").strip() or None
  180. if warning_qty is not None:
  181. obj.warning_qty = warning_qty
  182. if status is not None:
  183. obj.status = _norm_status(status)
  184. if order_num is not None:
  185. obj.order_num = order_num
  186. if remark is not None:
  187. obj.remark = (remark or "").strip() or None
  188. obj.update_by = user_id
  189. obj.update_time = datetime.now()
  190. db.add(obj)
  191. db.commit()
  192. db.refresh(obj)
  193. return obj
  194. def soft_delete_reagent(db: Session, reagent_id: str) -> None:
  195. obj = get_reagent(db, reagent_id)
  196. if not obj:
  197. raise ValueError("试剂不存在")
  198. if obj.stock_qty is not None and _dec(obj.stock_qty) > 0:
  199. raise ValueError("该试剂仍有库存,无法删除")
  200. obj.del_flag = 1
  201. obj.update_time = datetime.now()
  202. db.add(obj)
  203. db.commit()
  204. def stock_in(
  205. db: Session,
  206. row_id: str,
  207. *,
  208. qty: Decimal,
  209. remark: Optional[str] = None,
  210. user_id: Optional[str] = None,
  211. tenant_no: Optional[str] = None,
  212. operate_time: Optional[datetime] = None,
  213. ) -> BizReagent:
  214. if qty is None or float(qty) <= 0:
  215. raise ValueError("入库数量必须大于0")
  216. obj = (
  217. db.query(BizReagent)
  218. .filter(BizReagent.id == row_id, BizReagent.del_flag == 0)
  219. .with_for_update()
  220. .first()
  221. )
  222. if not obj:
  223. raise ValueError("试剂不存在")
  224. if tenant_no and obj.tenant_no != tenant_no:
  225. raise ValueError("试剂不存在")
  226. if not _is_enabled_status(obj.status):
  227. raise ValueError(f"试剂「{obj.reagent_name}」已停用,无法入库")
  228. op_time = operate_time or datetime.now()
  229. old_stock = obj.stock_qty or 0
  230. new_stock = old_stock + qty
  231. obj.stock_qty = new_stock
  232. obj.update_by = user_id
  233. obj.update_time = datetime.now()
  234. db.add(obj)
  235. log = BizReagentStockLog(
  236. id=generate_uuid(),
  237. material_id=obj.id,
  238. material_code=obj.reagent_code,
  239. material_name=obj.reagent_name,
  240. change_type="in",
  241. change_qty=qty,
  242. stock_before=old_stock,
  243. stock_after=new_stock,
  244. source_type="manual_in",
  245. tenant_no=obj.tenant_no,
  246. remark=(remark or "").strip() or None,
  247. create_by=user_id,
  248. create_dept_id=obj.create_dept_id or "",
  249. create_time=op_time,
  250. update_by=user_id,
  251. update_time=op_time,
  252. del_flag=0,
  253. )
  254. db.add(log)
  255. db.commit()
  256. db.refresh(obj)
  257. return obj
  258. def batch_stock_in(
  259. db: Session,
  260. *,
  261. items: List[Dict[str, Any]],
  262. remark: Optional[str] = None,
  263. tenant_no: str,
  264. user_id: Optional[str] = None,
  265. operate_time: Optional[datetime] = None,
  266. ) -> int:
  267. if not items:
  268. raise ValueError("请至少选择一条试剂")
  269. op_time = operate_time or datetime.now()
  270. shared_remark = (remark or "").strip() or None
  271. try:
  272. for it in items:
  273. material_id = (it.get("material_id") or it.get("materialId") or "").strip()
  274. qty = it.get("qty")
  275. if not material_id:
  276. raise ValueError("试剂ID不能为空")
  277. if qty is None or float(qty) <= 0:
  278. raise ValueError("入库数量必须大于0")
  279. qty_dec = Decimal(str(qty))
  280. line_remark = (it.get("remark") or "").strip()
  281. log_remark = line_remark or shared_remark or None
  282. obj = (
  283. db.query(BizReagent)
  284. .filter(
  285. BizReagent.id == material_id,
  286. BizReagent.tenant_no == tenant_no,
  287. BizReagent.del_flag == 0,
  288. )
  289. .with_for_update()
  290. .first()
  291. )
  292. if not obj:
  293. raise ValueError("试剂不存在")
  294. if not _is_enabled_status(obj.status):
  295. raise ValueError(f"试剂「{obj.reagent_name}」已停用,无法入库")
  296. old_stock = obj.stock_qty or 0
  297. new_stock = old_stock + qty_dec
  298. obj.stock_qty = new_stock
  299. obj.update_by = user_id
  300. obj.update_time = datetime.now()
  301. db.add(obj)
  302. log = BizReagentStockLog(
  303. id=generate_uuid(),
  304. material_id=obj.id,
  305. material_code=obj.reagent_code,
  306. material_name=obj.reagent_name,
  307. change_type="in",
  308. change_qty=qty_dec,
  309. stock_before=old_stock,
  310. stock_after=new_stock,
  311. source_type="manual_in",
  312. tenant_no=obj.tenant_no,
  313. remark=log_remark,
  314. create_by=user_id,
  315. create_dept_id=obj.create_dept_id or "",
  316. create_time=op_time,
  317. update_by=user_id,
  318. update_time=op_time,
  319. del_flag=0,
  320. )
  321. db.add(log)
  322. db.commit()
  323. return len(items)
  324. except Exception:
  325. db.rollback()
  326. raise
  327. def stock_out(
  328. db: Session,
  329. row_id: str,
  330. *,
  331. qty: Decimal,
  332. receiver: Optional[str] = None,
  333. purpose: Optional[str] = None,
  334. remark: Optional[str] = None,
  335. user_id: Optional[str] = None,
  336. tenant_no: Optional[str] = None,
  337. operate_time: Optional[datetime] = None,
  338. ) -> BizReagent:
  339. if qty is None or float(qty) <= 0:
  340. raise ValueError("出库数量必须大于0")
  341. qty_dec = Decimal(str(qty))
  342. if not (receiver or "").strip():
  343. raise ValueError("请选择领用人")
  344. obj = (
  345. db.query(BizReagent)
  346. .filter(BizReagent.id == row_id, BizReagent.del_flag == 0)
  347. .with_for_update()
  348. .first()
  349. )
  350. if not obj:
  351. raise ValueError("试剂不存在")
  352. if tenant_no and obj.tenant_no != tenant_no:
  353. raise ValueError("试剂不存在")
  354. if not _is_enabled_status(obj.status):
  355. raise ValueError(f"试剂「{obj.reagent_name}」已停用,无法出库")
  356. stock_before = obj.stock_qty if obj.stock_qty is not None else Decimal("0")
  357. if stock_before < qty_dec:
  358. raise ValueError(
  359. f"试剂「{obj.reagent_name}」库存不足,当前库存 {stock_before},需出库 {qty_dec}"
  360. )
  361. op_time = operate_time or datetime.now()
  362. new_stock = stock_before - qty_dec
  363. obj.stock_qty = new_stock
  364. obj.update_by = user_id
  365. obj.update_time = datetime.now()
  366. db.add(obj)
  367. log = BizReagentStockLog(
  368. id=generate_uuid(),
  369. material_id=obj.id,
  370. material_code=obj.reagent_code,
  371. material_name=obj.reagent_name,
  372. change_type="use",
  373. change_qty=-qty_dec,
  374. stock_before=stock_before,
  375. stock_after=new_stock,
  376. source_type="manual_out",
  377. tenant_no=obj.tenant_no,
  378. receiver=(receiver or "").strip() or None,
  379. purpose=(purpose or "").strip() or None,
  380. remark=(remark or "").strip() or None,
  381. create_by=user_id,
  382. create_dept_id=obj.create_dept_id or "",
  383. create_time=op_time,
  384. update_by=user_id,
  385. update_time=op_time,
  386. del_flag=0,
  387. )
  388. db.add(log)
  389. db.commit()
  390. db.refresh(obj)
  391. return obj
  392. def batch_stock_out(
  393. db: Session,
  394. *,
  395. items: List[Dict[str, Any]],
  396. receiver: Optional[str] = None,
  397. purpose: Optional[str] = None,
  398. remark: Optional[str] = None,
  399. tenant_no: str,
  400. user_id: Optional[str] = None,
  401. operate_time: Optional[datetime] = None,
  402. ) -> int:
  403. if not items:
  404. raise ValueError("请至少选择一条试剂")
  405. if not (receiver or "").strip():
  406. raise ValueError("请选择领用人")
  407. op_time = operate_time or datetime.now()
  408. recv = (receiver or "").strip() or None
  409. purp = (purpose or "").strip() or None
  410. shared_remark = (remark or "").strip() or None
  411. try:
  412. for it in items:
  413. material_id = (it.get("material_id") or it.get("materialId") or "").strip()
  414. qty = it.get("qty")
  415. if not material_id:
  416. raise ValueError("试剂ID不能为空")
  417. if qty is None or float(qty) <= 0:
  418. raise ValueError("出库数量必须大于0")
  419. qty_dec = Decimal(str(qty))
  420. line_remark = (it.get("remark") or "").strip() or shared_remark
  421. obj = (
  422. db.query(BizReagent)
  423. .filter(
  424. BizReagent.id == material_id,
  425. BizReagent.tenant_no == tenant_no,
  426. BizReagent.del_flag == 0,
  427. )
  428. .with_for_update()
  429. .first()
  430. )
  431. if not obj:
  432. raise ValueError("试剂不存在")
  433. if not _is_enabled_status(obj.status):
  434. raise ValueError(f"试剂「{obj.reagent_name}」已停用,无法出库")
  435. stock_before = obj.stock_qty if obj.stock_qty is not None else Decimal("0")
  436. if stock_before < qty_dec:
  437. raise ValueError(
  438. f"试剂「{obj.reagent_name}」库存不足,当前库存 {stock_before},需出库 {qty_dec}"
  439. )
  440. new_stock = stock_before - qty_dec
  441. obj.stock_qty = new_stock
  442. obj.update_by = user_id
  443. obj.update_time = datetime.now()
  444. db.add(obj)
  445. log = BizReagentStockLog(
  446. id=generate_uuid(),
  447. material_id=obj.id,
  448. material_code=obj.reagent_code,
  449. material_name=obj.reagent_name,
  450. change_type="use",
  451. change_qty=-qty_dec,
  452. stock_before=stock_before,
  453. stock_after=new_stock,
  454. source_type="manual_out",
  455. tenant_no=obj.tenant_no,
  456. receiver=recv,
  457. purpose=purp,
  458. remark=line_remark,
  459. create_by=user_id,
  460. create_dept_id=obj.create_dept_id or "",
  461. create_time=op_time,
  462. update_by=user_id,
  463. update_time=op_time,
  464. del_flag=0,
  465. )
  466. db.add(log)
  467. db.commit()
  468. return len(items)
  469. except Exception:
  470. db.rollback()
  471. raise
  472. def total_pages(total: int, page_size: int) -> int:
  473. if total <= 0:
  474. return 0
  475. return int(math.ceil(total / float(page_size)))