Files
zkxw/backend/app/crud/reagent.py
T
2026-05-23 12:51:22 +08:00

514 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""试剂 CRUD(biz_reagent)"""
from __future__ import annotations
import math
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.models import BizReagent, BizReagentStockLog
def _dec(v: Any) -> float:
if v is None:
return 0.0
return float(v)
def _norm_status(v: Any) -> str:
if v is None:
return "1"
if isinstance(v, bool):
return "1" if v else "0"
if isinstance(v, (int, float)):
if int(v) == 0:
return "0"
if int(v) == 1:
return "1"
s = str(v).strip()
if s in ("0", "1"):
return s
return "1"
def _is_enabled_status(v: Any) -> bool:
return _norm_status(v) == "1"
def _row_to_dict(r: BizReagent) -> Dict[str, Any]:
return {
"id": r.id,
"reagentCode": r.reagent_code,
"reagentName": r.reagent_name,
"spec": r.spec or "",
"standardUsage": r.standard_usage or "",
"unit": r.unit or "",
"description": r.description or "",
"stockQty": _dec(r.stock_qty),
"warningQty": _dec(r.warning_qty),
"status": _norm_status(r.status),
"tenantNo": r.tenant_no,
"orderNum": r.order_num if r.order_num is not None else 0,
"remark": r.remark or "",
"createTime": r.create_time.strftime("%Y-%m-%d %H:%M:%S") if r.create_time else "",
"updateTime": r.update_time.strftime("%Y-%m-%d %H:%M:%S") if r.update_time else "",
}
def list_reagents(
db: Session,
*,
page: int = 1,
page_size: int = 10,
keyword: Optional[str] = None,
tenant_no: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
def _filtered():
q0 = db.query(BizReagent).filter(BizReagent.del_flag == 0)
if tenant_no:
q0 = q0.filter(BizReagent.tenant_no == tenant_no)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(
or_(
BizReagent.reagent_code.like(like),
BizReagent.reagent_name.like(like),
)
)
if user_info:
from app.core.data_scope import apply_data_scope
q0 = apply_data_scope(q0, BizReagent, user_info, db, tenant_no)
return q0
total = _filtered().order_by(None).with_entities(func.count(BizReagent.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(BizReagent.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return [_row_to_dict(x) for x in rows], int(total)
def get_reagent(db: Session, reagent_id: str) -> Optional[BizReagent]:
return (
db.query(BizReagent)
.filter(and_(BizReagent.id == reagent_id, BizReagent.del_flag == 0))
.first()
)
def reagent_code_exists(db: Session, code: str, tenant_no: str, exclude_id: Optional[str] = None) -> bool:
q = db.query(BizReagent).filter(
and_(BizReagent.reagent_code == code.strip(), BizReagent.tenant_no == tenant_no, BizReagent.del_flag == 0)
)
if exclude_id:
q = q.filter(BizReagent.id != exclude_id)
return q.first() is not None
def create_reagent(
db: Session,
*,
reagent_code: str,
reagent_name: str,
spec: Optional[str] = None,
standard_usage: Optional[str] = None,
unit: Optional[str] = None,
description: Optional[str] = None,
tenant_no: str = "zkxw",
order_num: Optional[int] = None,
remark: Optional[str] = None,
user_id: Optional[str] = None,
) -> BizReagent:
rc = (reagent_code or "").strip()
if not rc:
raise ValueError("试剂编码不能为空")
if reagent_code_exists(db, rc, tenant_no):
raise ValueError("试剂编码已存在")
obj = BizReagent(
id=generate_uuid(),
reagent_code=rc,
reagent_name=(reagent_name or "").strip() or rc,
spec=(spec or "").strip() or None,
standard_usage=(standard_usage or "").strip() or None,
unit=(unit or "").strip() or None,
description=(description or "").strip() or None,
tenant_no=tenant_no,
order_num=order_num if order_num is not None else 0,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def update_reagent(
db: Session,
reagent_id: str,
*,
reagent_code: Optional[str] = None,
reagent_name: Optional[str] = None,
spec: Optional[str] = None,
standard_usage: Optional[str] = None,
unit: Optional[str] = None,
description: Optional[str] = None,
order_num: Optional[int] = None,
remark: Optional[str] = None,
user_id: Optional[str] = None,
) -> BizReagent:
obj = get_reagent(db, reagent_id)
if not obj:
raise ValueError("试剂不存在")
if reagent_code is not None:
new_code = reagent_code.strip()
if not new_code:
raise ValueError("试剂编码不能为空")
if new_code != obj.reagent_code and reagent_code_exists(db, new_code, obj.tenant_no, exclude_id=reagent_id):
raise ValueError("试剂编码已存在")
obj.reagent_code = new_code
if reagent_name is not None:
obj.reagent_name = (reagent_name or "").strip() or obj.reagent_name
if spec is not None:
obj.spec = (spec or "").strip() or None
if standard_usage is not None:
obj.standard_usage = (standard_usage or "").strip() or None
if unit is not None:
obj.unit = (unit or "").strip() or None
if description is not None:
obj.description = (description or "").strip() or None
if order_num is not None:
obj.order_num = order_num
if remark is not None:
obj.remark = (remark or "").strip() or None
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def soft_delete_reagent(db: Session, reagent_id: str) -> None:
obj = get_reagent(db, reagent_id)
if not obj:
raise ValueError("试剂不存在")
if obj.stock_qty is not None and _dec(obj.stock_qty) > 0:
raise ValueError("该试剂仍有库存,无法删除")
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
db.commit()
def stock_in(
db: Session,
row_id: str,
*,
qty: Decimal,
remark: Optional[str] = None,
user_id: Optional[str] = None,
tenant_no: Optional[str] = None,
operate_time: Optional[datetime] = None,
) -> BizReagent:
if qty is None or float(qty) <= 0:
raise ValueError("入库数量必须大于0")
obj = (
db.query(BizReagent)
.filter(BizReagent.id == row_id, BizReagent.del_flag == 0)
.with_for_update()
.first()
)
if not obj:
raise ValueError("试剂不存在")
if tenant_no and obj.tenant_no != tenant_no:
raise ValueError("试剂不存在")
if not _is_enabled_status(obj.status):
raise ValueError(f"试剂「{obj.reagent_name}」已停用,无法入库")
op_time = operate_time or datetime.now()
old_stock = obj.stock_qty or 0
new_stock = old_stock + qty
obj.stock_qty = new_stock
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
log = BizReagentStockLog(
id=generate_uuid(),
material_id=obj.id,
material_code=obj.reagent_code,
material_name=obj.reagent_name,
change_type="in",
change_qty=qty,
stock_before=old_stock,
stock_after=new_stock,
source_type="manual_in",
tenant_no=obj.tenant_no,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id=obj.create_dept_id or "",
create_time=op_time,
update_by=user_id,
update_time=op_time,
del_flag=0,
)
db.add(log)
db.commit()
db.refresh(obj)
return obj
def batch_stock_in(
db: Session,
*,
items: List[Dict[str, Any]],
remark: Optional[str] = None,
tenant_no: str,
user_id: Optional[str] = None,
operate_time: Optional[datetime] = None,
) -> int:
if not items:
raise ValueError("请至少选择一条试剂")
op_time = operate_time or datetime.now()
shared_remark = (remark or "").strip() or None
try:
for it in items:
material_id = (it.get("material_id") or it.get("materialId") or "").strip()
qty = it.get("qty")
if not material_id:
raise ValueError("试剂ID不能为空")
if qty is None or float(qty) <= 0:
raise ValueError("入库数量必须大于0")
qty_dec = Decimal(str(qty))
line_remark = (it.get("remark") or "").strip()
log_remark = line_remark or shared_remark or None
obj = (
db.query(BizReagent)
.filter(
BizReagent.id == material_id,
BizReagent.tenant_no == tenant_no,
BizReagent.del_flag == 0,
)
.with_for_update()
.first()
)
if not obj:
raise ValueError("试剂不存在")
if not _is_enabled_status(obj.status):
raise ValueError(f"试剂「{obj.reagent_name}」已停用,无法入库")
old_stock = obj.stock_qty or 0
new_stock = old_stock + qty_dec
obj.stock_qty = new_stock
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
log = BizReagentStockLog(
id=generate_uuid(),
material_id=obj.id,
material_code=obj.reagent_code,
material_name=obj.reagent_name,
change_type="in",
change_qty=qty_dec,
stock_before=old_stock,
stock_after=new_stock,
source_type="manual_in",
tenant_no=obj.tenant_no,
remark=log_remark,
create_by=user_id,
create_dept_id=obj.create_dept_id or "",
create_time=op_time,
update_by=user_id,
update_time=op_time,
del_flag=0,
)
db.add(log)
db.commit()
return len(items)
except Exception:
db.rollback()
raise
def stock_out(
db: Session,
row_id: str,
*,
qty: Decimal,
receiver: Optional[str] = None,
purpose: Optional[str] = None,
remark: Optional[str] = None,
user_id: Optional[str] = None,
tenant_no: Optional[str] = None,
operate_time: Optional[datetime] = None,
) -> BizReagent:
if qty is None or float(qty) <= 0:
raise ValueError("出库数量必须大于0")
qty_dec = Decimal(str(qty))
if not (receiver or "").strip():
raise ValueError("请选择领用人")
obj = (
db.query(BizReagent)
.filter(BizReagent.id == row_id, BizReagent.del_flag == 0)
.with_for_update()
.first()
)
if not obj:
raise ValueError("试剂不存在")
if tenant_no and obj.tenant_no != tenant_no:
raise ValueError("试剂不存在")
if not _is_enabled_status(obj.status):
raise ValueError(f"试剂「{obj.reagent_name}」已停用,无法出库")
stock_before = obj.stock_qty if obj.stock_qty is not None else Decimal("0")
if stock_before < qty_dec:
raise ValueError(
f"试剂「{obj.reagent_name}」库存不足,当前库存 {stock_before},需出库 {qty_dec}"
)
op_time = operate_time or datetime.now()
new_stock = stock_before - qty_dec
obj.stock_qty = new_stock
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
log = BizReagentStockLog(
id=generate_uuid(),
material_id=obj.id,
material_code=obj.reagent_code,
material_name=obj.reagent_name,
change_type="use",
change_qty=-qty_dec,
stock_before=stock_before,
stock_after=new_stock,
source_type="manual_out",
tenant_no=obj.tenant_no,
receiver=(receiver or "").strip() or None,
purpose=(purpose or "").strip() or None,
remark=(remark or "").strip() or None,
create_by=user_id,
create_dept_id=obj.create_dept_id or "",
create_time=op_time,
update_by=user_id,
update_time=op_time,
del_flag=0,
)
db.add(log)
db.commit()
db.refresh(obj)
return obj
def batch_stock_out(
db: Session,
*,
items: List[Dict[str, Any]],
receiver: Optional[str] = None,
purpose: Optional[str] = None,
remark: Optional[str] = None,
tenant_no: str,
user_id: Optional[str] = None,
operate_time: Optional[datetime] = None,
) -> int:
if not items:
raise ValueError("请至少选择一条试剂")
if not (receiver or "").strip():
raise ValueError("请选择领用人")
op_time = operate_time or datetime.now()
recv = (receiver or "").strip() or None
purp = (purpose or "").strip() or None
shared_remark = (remark or "").strip() or None
try:
for it in items:
material_id = (it.get("material_id") or it.get("materialId") or "").strip()
qty = it.get("qty")
if not material_id:
raise ValueError("试剂ID不能为空")
if qty is None or float(qty) <= 0:
raise ValueError("出库数量必须大于0")
qty_dec = Decimal(str(qty))
line_remark = (it.get("remark") or "").strip() or shared_remark
obj = (
db.query(BizReagent)
.filter(
BizReagent.id == material_id,
BizReagent.tenant_no == tenant_no,
BizReagent.del_flag == 0,
)
.with_for_update()
.first()
)
if not obj:
raise ValueError("试剂不存在")
if not _is_enabled_status(obj.status):
raise ValueError(f"试剂「{obj.reagent_name}」已停用,无法出库")
stock_before = obj.stock_qty if obj.stock_qty is not None else Decimal("0")
if stock_before < qty_dec:
raise ValueError(
f"试剂「{obj.reagent_name}」库存不足,当前库存 {stock_before},需出库 {qty_dec}"
)
new_stock = stock_before - qty_dec
obj.stock_qty = new_stock
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
log = BizReagentStockLog(
id=generate_uuid(),
material_id=obj.id,
material_code=obj.reagent_code,
material_name=obj.reagent_name,
change_type="use",
change_qty=-qty_dec,
stock_before=stock_before,
stock_after=new_stock,
source_type="manual_out",
tenant_no=obj.tenant_no,
receiver=recv,
purpose=purp,
remark=line_remark,
create_by=user_id,
create_dept_id=obj.create_dept_id or "",
create_time=op_time,
update_by=user_id,
update_time=op_time,
del_flag=0,
)
db.add(log)
db.commit()
return len(items)
except Exception:
db.rollback()
raise
def total_pages(total: int, page_size: int) -> int:
if total <= 0:
return 0
return int(math.ceil(total / float(page_size)))