协议样本、生产产出和盘点明细按行更新并保留原 id;补齐存储证明质检预填、离职人员显示名,以及样本采集通知模板字段。 Co-authored-by: Cursor <cursoragent@cursor.com>
183 lines
6.4 KiB
Python
183 lines
6.4 KiB
Python
"""配置试剂配方 BOM CRUD"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from sqlalchemy import and_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.crud.child_row_sync import sync_child_rows
|
|
from app.crud.production_material import _dec, _fmt_dt
|
|
from app.models.production_material import BizProductionMaterial, BizProductionMaterialFormula
|
|
|
|
COMPONENT_TYPES = ("reagent", "consumable")
|
|
|
|
|
|
def _line_to_dict(r: BizProductionMaterialFormula) -> Dict[str, Any]:
|
|
return {
|
|
"id": r.id,
|
|
"configuredMaterialId": r.configured_material_id,
|
|
"componentMaterialId": r.component_material_id,
|
|
"componentMaterialType": r.component_material_type or "",
|
|
"componentMaterialCode": r.component_material_code or "",
|
|
"componentMaterialName": r.component_material_name or "",
|
|
"standardQty": _dec(r.standard_qty),
|
|
"usageUnit": r.usage_unit or "",
|
|
"orderNum": r.order_num if r.order_num is not None else 0,
|
|
"remark": r.remark or "",
|
|
}
|
|
|
|
|
|
def _get_configured_material(
|
|
db: Session, material_id: str, tenant_no: str
|
|
) -> BizProductionMaterial:
|
|
row = (
|
|
db.query(BizProductionMaterial)
|
|
.filter(
|
|
BizProductionMaterial.id == material_id,
|
|
BizProductionMaterial.tenant_no == tenant_no,
|
|
BizProductionMaterial.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not row:
|
|
raise ValueError("配置试剂不存在")
|
|
if (row.material_type or "").strip() != "configured_reagent":
|
|
raise ValueError("仅配置试剂目录可维护配方")
|
|
return row
|
|
|
|
|
|
def get_formula(
|
|
db: Session, configured_material_id: str, tenant_no: str
|
|
) -> List[Dict[str, Any]]:
|
|
_get_configured_material(db, configured_material_id, tenant_no)
|
|
rows = (
|
|
db.query(BizProductionMaterialFormula)
|
|
.filter(
|
|
BizProductionMaterialFormula.configured_material_id == configured_material_id,
|
|
BizProductionMaterialFormula.tenant_no == tenant_no,
|
|
BizProductionMaterialFormula.del_flag == 0,
|
|
)
|
|
.order_by(
|
|
BizProductionMaterialFormula.order_num.asc(),
|
|
BizProductionMaterialFormula.create_time.asc(),
|
|
)
|
|
.all()
|
|
)
|
|
return [_line_to_dict(r) for r in rows]
|
|
|
|
|
|
def replace_formula(
|
|
db: Session,
|
|
configured_material_id: str,
|
|
*,
|
|
lines: List[Dict[str, Any]],
|
|
tenant_no: str,
|
|
user_id: Optional[str] = None,
|
|
dept_id: str = "",
|
|
) -> List[Dict[str, Any]]:
|
|
configured = _get_configured_material(db, configured_material_id, tenant_no)
|
|
now = datetime.now()
|
|
if not lines:
|
|
raise ValueError("请至少添加一条配方明细")
|
|
|
|
seen_components: set = set()
|
|
normalized: List[Dict[str, Any]] = []
|
|
|
|
for idx, it in enumerate(lines):
|
|
comp_id = (
|
|
it.get("componentMaterialId") or it.get("component_material_id") or ""
|
|
).strip()
|
|
if not comp_id:
|
|
raise ValueError("请选择子物料")
|
|
if comp_id in seen_components:
|
|
raise ValueError("配方中存在重复的子物料")
|
|
seen_components.add(comp_id)
|
|
|
|
comp_type = (
|
|
it.get("componentMaterialType") or it.get("component_material_type") or ""
|
|
).strip().lower()
|
|
if comp_type not in COMPONENT_TYPES:
|
|
raise ValueError("子物料类型无效,应为试剂或耗材")
|
|
|
|
comp = (
|
|
db.query(BizProductionMaterial)
|
|
.filter(
|
|
BizProductionMaterial.id == comp_id,
|
|
BizProductionMaterial.tenant_no == tenant_no,
|
|
BizProductionMaterial.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not comp:
|
|
raise ValueError("子物料不存在")
|
|
if (comp.material_type or "").strip() != comp_type:
|
|
raise ValueError(f"子物料「{comp.material_name}」类型与所选不一致")
|
|
|
|
qty_raw = it.get("standardQty") if it.get("standardQty") is not None else it.get("standard_qty")
|
|
if qty_raw is None or Decimal(str(qty_raw)) <= 0:
|
|
raise ValueError("标准用量必须大于0")
|
|
standard_qty = Decimal(str(qty_raw))
|
|
|
|
usage_unit = (
|
|
it.get("usageUnit") or it.get("usage_unit") or comp.unit or ""
|
|
).strip()[:32]
|
|
|
|
normalized.append(
|
|
{
|
|
"id": (it.get("id") or "").strip(),
|
|
"component_material_id": comp_id,
|
|
"component_material_type": comp_type,
|
|
"component_material_code": comp.material_code,
|
|
"component_material_name": comp.material_name,
|
|
"standard_qty": standard_qty,
|
|
"usage_unit": usage_unit or None,
|
|
"order_num": idx,
|
|
"remark": (it.get("remark") or "").strip()[:512] or None,
|
|
}
|
|
)
|
|
|
|
dept = dept_id or configured.create_dept_id or ""
|
|
|
|
def build_row(row_id: str) -> BizProductionMaterialFormula:
|
|
return BizProductionMaterialFormula(
|
|
id=row_id,
|
|
configured_material_id=configured_material_id,
|
|
tenant_no=tenant_no,
|
|
create_by=user_id,
|
|
create_dept_id=dept,
|
|
create_time=now,
|
|
)
|
|
|
|
def apply_row(row: BizProductionMaterialFormula, it: Dict[str, Any], idx: int, _now: datetime) -> None:
|
|
row.component_material_id = it["component_material_id"]
|
|
row.component_material_type = it["component_material_type"]
|
|
row.component_material_code = it["component_material_code"]
|
|
row.component_material_name = it["component_material_name"]
|
|
row.standard_qty = it["standard_qty"]
|
|
row.usage_unit = it["usage_unit"]
|
|
row.order_num = idx
|
|
row.remark = it["remark"]
|
|
|
|
sync_child_rows(
|
|
db,
|
|
BizProductionMaterialFormula,
|
|
parent_attr="configured_material_id",
|
|
parent_id=configured_material_id,
|
|
tenant_no=tenant_no,
|
|
items=normalized,
|
|
user_id=user_id,
|
|
now=now,
|
|
build_row=build_row,
|
|
apply_fields=apply_row,
|
|
natural_key=lambda it: (it.get("component_material_id") or "").strip(),
|
|
row_natural_key=lambda row: (row.component_material_id or "").strip(),
|
|
duplicate_message="配方明细编号重复",
|
|
foreign_message="配方明细不属于当前配置试剂",
|
|
)
|
|
|
|
db.commit()
|
|
return get_formula(db, configured_material_id, tenant_no)
|