Files
zkxw/backend/app/crud/cell_product.py
T
2026-05-24 16:58:51 +08:00

948 lines
32 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_cell_product)"""
from __future__ import annotations
import math
import re
from datetime import datetime
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.crud import storage_occupancy as occupancy_svc
from app.models.models import BizStorageSlotOccupancy
from app.models.models import (
BizCellProduct,
BizCellProductSpec,
BizCellProductType,
BizStorageFacility,
BizStorageFacilitySlot,
)
def _row_to_dict(c: BizCellProduct) -> Dict[str, Any]:
return {
"id": c.id,
"productCode": c.product_code,
"productName": c.product_name,
"productTypeId": c.product_type_id or "",
"productTypeName": "",
"specId": c.spec_id or "",
"specName": "",
"batchNo": c.batch_no or "",
"storageSlotId": c.storage_slot_id or "",
"storageSlotName": "",
"passage": c.passage or "",
"freezeDate": c.freeze_date.strftime("%Y-%m-%d %H:%M:%S") if c.freeze_date else "",
"expectedCellCount": float(c.expected_cell_count) if c.expected_cell_count is not None else None,
"viability": float(c.viability) if c.viability is not None else None,
"productStatus": c.product_status or "",
"sourceType": c.source_type or "",
"productionTaskId": c.production_task_id or "",
"quantity": float(c.quantity) if c.quantity is not None else None,
"unit": c.unit or "",
"expiryDate": c.expiry_date.strftime("%Y-%m-%d %H:%M:%S") if c.expiry_date else "",
"productImages": c.product_images or "",
"tenantNo": c.tenant_no,
"orderNum": c.order_num if c.order_num is not None else 0,
"remark": c.remark or "",
"createBy": c.create_by or "",
"createTime": c.create_time.strftime("%Y-%m-%d %H:%M:%S") if c.create_time else "",
"updateBy": c.update_by or "",
"updateTime": c.update_time.strftime("%Y-%m-%d %H:%M:%S") if c.update_time else "",
}
def _build_facility_path_label(node_id: str, by_id: Dict[str, BizStorageFacility]) -> str:
parts: List[str] = []
cur_id = (node_id or "").strip()
visited: set[str] = set()
while cur_id and cur_id != "0" and cur_id not in visited:
visited.add(cur_id)
node = by_id.get(cur_id)
if not node:
break
parts.append(node.node_name or "")
cur_id = (node.parent_id or "").strip()
parts.reverse()
return " / ".join(p for p in parts if p)
def _enrich_product_dicts(
db: Session,
tenant_no: str,
items: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
if not items:
return items
type_ids = {i.get("productTypeId") for i in items if i.get("productTypeId")}
spec_ids = {i.get("specId") for i in items if i.get("specId")}
slot_ids = {i.get("storageSlotId") for i in items if i.get("storageSlotId")}
type_map: Dict[str, str] = {}
if type_ids:
rows = (
db.query(BizCellProductType)
.filter(
BizCellProductType.id.in_(type_ids),
BizCellProductType.del_flag == 0,
)
.all()
)
type_map = {r.id: r.product_type_name or "" for r in rows}
spec_map: Dict[str, str] = {}
if spec_ids:
rows = (
db.query(BizCellProductSpec)
.filter(
BizCellProductSpec.id.in_(spec_ids),
BizCellProductSpec.del_flag == 0,
)
.all()
)
spec_map = {r.id: r.spec_name or "" for r in rows}
slot_map: Dict[str, str] = {}
if slot_ids:
slots = (
db.query(BizStorageFacilitySlot)
.filter(
BizStorageFacilitySlot.id.in_(slot_ids),
BizStorageFacilitySlot.del_flag == 0,
)
.all()
)
facilities = (
db.query(BizStorageFacility)
.filter(
BizStorageFacility.tenant_no == tenant_no,
BizStorageFacility.del_flag == 0,
)
.all()
)
facility_by_id = {f.id: f for f in facilities}
for slot in slots:
node_path = _build_facility_path_label(slot.facility_node_id, facility_by_id)
code = (slot.slot_code or "").strip()
if node_path and code:
slot_map[slot.id] = f"{node_path} / {code}"
else:
slot_map[slot.id] = code or node_path or ""
# --- 来源信息富化 ---
from app.models.models import BizProductionTask, BizProductionWorkOrder
task_ids = {i.get("productionTaskId") for i in items if i.get("productionTaskId")}
task_map: Dict[str, Any] = {}
task_to_wo: Dict[str, str] = {}
wo_map: Dict[str, Any] = {}
if task_ids:
tasks = (
db.query(BizProductionTask)
.filter(
BizProductionTask.id.in_(task_ids),
BizProductionTask.del_flag == 0,
)
.all()
)
for t in tasks:
task_map[t.id] = t
if t.work_order_id:
task_to_wo[t.id] = t.work_order_id
wo_ids = set(task_to_wo.values())
if wo_ids:
wos = (
db.query(BizProductionWorkOrder)
.filter(
BizProductionWorkOrder.id.in_(wo_ids),
BizProductionWorkOrder.del_flag == 0,
)
.all()
)
wo_map = {w.id: w for w in wos}
source_type_labels = {"production": "生产入库", "manual": "手动录入"}
for item in items:
tid = item.get("productTypeId") or ""
sid = item.get("specId") or ""
slot_id = item.get("storageSlotId") or ""
item["productTypeName"] = type_map.get(tid, "") if tid else ""
item["specName"] = spec_map.get(sid, "") if sid else ""
item["storageSlotName"] = slot_map.get(slot_id, "") if slot_id else ""
# 来源信息
src_type = item.get("sourceType", "")
item["sourceTypeLabel"] = source_type_labels.get(src_type, src_type or "")
tid = item.get("productionTaskId") or ""
if tid and tid in task_map:
t_obj = task_map[tid]
item["sourceTaskNo"] = t_obj.task_no or ""
wo_id = task_to_wo.get(tid)
if wo_id and wo_id in wo_map:
wo_obj = wo_map[wo_id]
item["sourceWorkOrderNo"] = wo_obj.work_order_no or ""
item["sourceProtocolNo"] = wo_obj.protocol_no or ""
item["sourceReceptionSource"] = wo_obj.reception_source or ""
else:
item["sourceWorkOrderNo"] = ""
item["sourceProtocolNo"] = ""
item["sourceReceptionSource"] = ""
else:
item["sourceTaskNo"] = ""
item["sourceWorkOrderNo"] = ""
item["sourceProtocolNo"] = ""
item["sourceReceptionSource"] = ""
return items
def product_to_response(db: Session, tenant_no: str, obj: BizCellProduct) -> Dict[str, Any]:
enriched = _enrich_product_dicts(db, tenant_no, [_row_to_dict(obj)])
return enriched[0] if enriched else _row_to_dict(obj)
def list_cell_products(
db: Session,
*,
tenant_no: str,
page: int = 1,
page_size: int = 20,
keyword: Optional[str] = None,
product_code: Optional[str] = None,
product_name: Optional[str] = None,
product_type_id: Optional[str] = None,
product_status: Optional[str] = None,
passage: Optional[str] = None,
source_type: Optional[str] = None,
user_info=None,
) -> Dict[str, Any]:
def _filtered():
q0 = db.query(BizCellProduct).filter(
BizCellProduct.del_flag == 0,
BizCellProduct.tenant_no == tenant_no,
)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q0 = q0.filter(
or_(
BizCellProduct.product_code.ilike(like),
BizCellProduct.product_name.ilike(like),
)
)
pc = (product_code or "").strip()
if pc:
q0 = q0.filter(BizCellProduct.product_code.like(f"%{pc}%"))
pn = (product_name or "").strip()
if pn:
q0 = q0.filter(BizCellProduct.product_name.like(f"%{pn}%"))
if product_type_id:
q0 = q0.filter(BizCellProduct.product_type_id == product_type_id)
if product_status:
q0 = q0.filter(BizCellProduct.product_status == product_status)
if passage:
q0 = q0.filter(BizCellProduct.passage == passage)
if source_type:
q0 = q0.filter(BizCellProduct.source_type == source_type)
if user_info:
from app.core.data_scope import apply_data_scope
q0 = apply_data_scope(q0, BizCellProduct, user_info, db, tenant_no)
return q0
total = _filtered().order_by(None).with_entities(func.count(BizCellProduct.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(BizCellProduct.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
items = _enrich_product_dicts(
db, tenant_no, [_row_to_dict(x) for x in rows]
)
return {"items": items, "total": int(total)}
def get_cell_product(db: Session, id: str, tenant_no: str) -> Optional[BizCellProduct]:
return (
db.query(BizCellProduct)
.filter(
and_(
BizCellProduct.id == id,
BizCellProduct.tenant_no == tenant_no,
BizCellProduct.del_flag == 0,
)
)
.first()
)
def create_cell_product(
db: Session,
*,
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: Optional[str] = None,
data: Any = None,
) -> BizCellProduct:
code = (data.product_code or "").strip()
if not code:
raise ValueError("产品编码不能为空")
existing = db.query(BizCellProduct).filter(
BizCellProduct.tenant_no == tenant_no,
BizCellProduct.product_code == code,
BizCellProduct.del_flag == 0,
).first()
if existing:
raise ValueError(f"产品编码 {code} 已存在")
obj = BizCellProduct(
id=generate_uuid(),
product_code=code,
product_name=(data.product_name or "").strip(),
product_type_id=(data.product_type_id or "").strip() or None,
spec_id=(data.spec_id or "").strip() or None,
batch_no=(data.batch_no or "").strip() or None,
storage_slot_id=(data.storage_slot_id or "").strip() or None,
passage=((data.passage or "").strip() or None) if getattr(data, "passage", None) else None,
freeze_date=data.freeze_date if hasattr(data, "freeze_date") else None,
expected_cell_count=data.expected_cell_count if hasattr(data, "expected_cell_count") else None,
viability=data.viability if hasattr(data, "viability") else None,
product_status=data.product_status or "frozen",
source_type=data.source_type or "manual",
quantity=data.quantity,
unit=(data.unit or "").strip() or None,
expiry_date=data.expiry_date,
product_images=(data.product_images or "").strip() or None,
tenant_no=tenant_no,
order_num=0,
remark=(data.remark or "").strip() or None,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(obj)
db.flush()
if obj.storage_slot_id:
occ = BizStorageSlotOccupancy(
id=generate_uuid(),
slot_id=obj.storage_slot_id,
item_type="cell_product",
item_id=obj.id,
item_code=obj.product_code,
item_name=obj.product_name,
item_source=obj.source_type or "manual",
stored_at=datetime.now(),
occupancy_status="stored",
tenant_no=tenant_no,
order_num=0,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(occ)
db.flush()
occupancy_svc._refresh_slot_cache(db, obj.storage_slot_id)
db.commit()
db.refresh(obj)
return obj
def update_cell_product(
db: Session,
id: str,
tenant_no: str,
user_id: Optional[str] = None,
data: Any = None,
) -> BizCellProduct:
obj = get_cell_product(db, id, tenant_no)
if not obj:
raise ValueError("细胞产品不存在")
if data.product_name is not None:
obj.product_name = (data.product_name or "").strip()
if data.product_type_id is not None:
obj.product_type_id = (data.product_type_id or "").strip() or None
if data.spec_id is not None:
obj.spec_id = (data.spec_id or "").strip() or None
if data.batch_no is not None:
obj.batch_no = (data.batch_no or "").strip() or None
if data.storage_slot_id is not None:
obj.storage_slot_id = (data.storage_slot_id or "").strip() or None
if hasattr(data, "passage") and data.passage is not None:
obj.passage = (data.passage or "").strip() or None
if hasattr(data, "freeze_date") and data.freeze_date is not None:
obj.freeze_date = data.freeze_date
if hasattr(data, "expected_cell_count") and data.expected_cell_count is not None:
obj.expected_cell_count = data.expected_cell_count
if hasattr(data, "viability") and data.viability is not None:
obj.viability = data.viability
if data.product_status is not None and data.product_status != obj.product_status:
allowed = {"frozen": {"sold", "used", "voided"}}
from_status = obj.product_status
to_status = data.product_status
if from_status not in allowed or to_status not in allowed[from_status]:
raise ValueError(f"不允许从状态 {from_status} 变更为 {to_status}")
obj.product_status = data.product_status
if obj.storage_slot_id:
try:
occupancy_svc.remove_by_item(
db, "cell_product", obj.id, user_id=user_id,
)
except Exception:
pass
if data.quantity is not None:
obj.quantity = data.quantity
if data.unit is not None:
obj.unit = (data.unit or "").strip() or None
if data.expiry_date is not None:
obj.expiry_date = data.expiry_date
if data.product_images is not None:
obj.product_images = (data.product_images or "").strip() or None
if data.remark is not None:
obj.remark = (data.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 delete_cell_product(
db: Session,
id: str,
tenant_no: str,
user_id: Optional[str] = None,
) -> None:
obj = get_cell_product(db, id, tenant_no)
if not obj:
raise ValueError("细胞产品不存在")
obj.del_flag = 1
obj.update_by = user_id
obj.update_time = datetime.now()
db.add(obj)
db.commit()
def _next_code_start_seq(db: Session, tenant_no: str, prefix: str) -> int:
last = (
db.query(BizCellProduct)
.filter(
BizCellProduct.tenant_no == tenant_no,
BizCellProduct.product_code.like(f"{prefix}%"),
BizCellProduct.del_flag == 0,
)
.order_by(BizCellProduct.product_code.desc())
.first()
)
seq = 1
if last and last.product_code:
try:
base = normalize_product_code_base(last.product_code)
seq = int(base[-6:]) + 1
except (ValueError, IndexError):
seq = 1
return seq
def get_next_code(db: Session, tenant_no: str) -> str:
today_str = datetime.now().strftime("%Y%m%d")
prefix = f"CP{today_str}"
seq = _next_code_start_seq(db, tenant_no, prefix)
return f"{prefix}{seq:06d}"
_PRODUCT_CODE_SUFFIX_RE = re.compile(r"^(.+)-(\d{2})$")
def normalize_product_code_base(code: str) -> str:
"""去掉末尾 -01/-02 等序号,得到多份入库时的基础编码。"""
c = (code or "").strip()
if not c:
return ""
m = _PRODUCT_CODE_SUFFIX_RE.match(c)
if m:
return m.group(1).strip()
return c
def build_split_product_codes(base: str, count: int) -> List[str]:
"""按基础编码生成 基础-01、基础-02 …"""
if count < 1:
return []
return [f"{base}-{i:02d}" for i in range(1, count + 1)]
def reserve_next_codes(db: Session, tenant_no: str, count: int) -> List[str]:
if count < 1:
return []
count = min(count, 50)
today_str = datetime.now().strftime("%Y%m%d")
prefix = f"CP{today_str}"
start = _next_code_start_seq(db, tenant_no, prefix)
return [f"{prefix}{start + i:06d}" for i in range(count)]
def _validate_batch_items(
db: Session,
tenant_no: str,
items: List[Any],
*,
require_storage_slot: bool = True,
) -> Tuple[List[str], List[Optional[str]]]:
if not items:
raise ValueError("至少需要一份产品明细")
if len(items) > 50:
raise ValueError("单次入库份数不能超过 50")
codes: List[str] = []
slot_ids: List[Optional[str]] = []
for it in items:
code = (getattr(it, "product_code", None) or "").strip()
if not code:
raise ValueError("产品编码不能为空")
if code in codes:
raise ValueError(f"产品编码 {code} 重复")
codes.append(code)
sid = (getattr(it, "storage_slot_id", None) or "").strip() or None
if require_storage_slot and not sid:
raise ValueError("每份产品须选择储物格位置")
slot_ids.append(sid)
existing_codes = (
db.query(BizCellProduct.product_code)
.filter(
BizCellProduct.tenant_no == tenant_no,
BizCellProduct.product_code.in_(codes),
BizCellProduct.del_flag == 0,
)
.all()
)
if existing_codes:
dup = existing_codes[0][0]
raise ValueError(f"产品编码 {dup} 已存在")
return codes, slot_ids
def create_cell_products_by_quantity(
db: Session,
*,
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: Optional[str] = None,
data: Any = None,
count: int,
) -> List[BizCellProduct]:
"""按入库份数拆分:基础编码-01、基础编码-02 …"""
count = max(1, min(int(count), 50))
base = normalize_product_code_base(getattr(data, "product_code", None) or "")
if not base:
raise ValueError("产品编码不能为空")
codes = build_split_product_codes(base, count)
slot_id = (getattr(data, "storage_slot_id", None) or "").strip() or None
from types import SimpleNamespace
items = [
SimpleNamespace(product_code=c, storage_slot_id=slot_id) for c in codes
]
return create_cell_products_batch(
db,
tenant_no=tenant_no,
user_id=user_id,
create_dept_id=create_dept_id,
data=data,
items=items,
require_storage_slot=False,
)
def create_cell_products_batch(
db: Session,
*,
tenant_no: str,
user_id: Optional[str] = None,
create_dept_id: Optional[str] = None,
data: Any = None,
items: List[Any],
require_storage_slot: bool = True,
) -> List[BizCellProduct]:
codes, slot_ids = _validate_batch_items(
db,
tenant_no,
items,
require_storage_slot=require_storage_slot,
)
now = datetime.now()
created: List[BizCellProduct] = []
for code, slot_id in zip(codes, slot_ids):
obj = BizCellProduct(
id=generate_uuid(),
product_code=code,
product_name=(data.product_name or "").strip(),
product_type_id=(data.product_type_id or "").strip() or None,
spec_id=(data.spec_id or "").strip() or None,
batch_no=(data.batch_no or "").strip() or None,
storage_slot_id=slot_id,
passage=((data.passage or "").strip() or None) if getattr(data, "passage", None) else None,
freeze_date=data.freeze_date if hasattr(data, "freeze_date") else None,
expected_cell_count=data.expected_cell_count if hasattr(data, "expected_cell_count") else None,
viability=data.viability if hasattr(data, "viability") else None,
product_status=data.product_status or "frozen",
source_type=data.source_type or "manual",
quantity=1,
unit=(data.unit or "").strip() or None,
expiry_date=data.expiry_date,
product_images=(data.product_images or "").strip() or None,
tenant_no=tenant_no,
order_num=0,
remark=(data.remark or "").strip() or None,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(obj)
created.append(obj)
for obj in created:
if obj.storage_slot_id:
occ = BizStorageSlotOccupancy(
id=generate_uuid(),
slot_id=obj.storage_slot_id,
item_type="cell_product",
item_id=obj.id,
item_code=obj.product_code,
item_name=obj.product_name,
item_source=obj.source_type or "manual",
stored_at=now,
occupancy_status="stored",
tenant_no=tenant_no,
order_num=0,
create_by=user_id,
create_dept_id=create_dept_id or "",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(occ)
db.flush()
slot_ids_seen = {obj.storage_slot_id for obj in created if obj.storage_slot_id}
for sid in slot_ids_seen:
occupancy_svc._refresh_slot_cache(db, sid)
db.commit()
for obj in created:
db.refresh(obj)
return created
def total_pages(total: int, page_size: int) -> int:
if total <= 0:
return 0
return int(math.ceil(total / float(page_size)))
# ---------------------------------------------------------------------------
# 来源链路 API
# ---------------------------------------------------------------------------
_MAX_CHAIN_DEPTH = 10
_SOURCE_TYPE_LABELS = {"production": "生产入库", "manual": "手动录入"}
def _product_node(obj: BizCellProduct, is_current: bool = False) -> Dict[str, Any]:
return {
"id": obj.id,
"productCode": obj.product_code or "",
"productName": obj.product_name or "",
"nodeType": "cell_product",
"productStatus": obj.product_status or "",
"passage": obj.passage or "",
"isCurrent": is_current,
}
def _first_chain_link(chain: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""祖先链单链路段:列表仅含一个节点时取首项,供 parent 嵌套为对象而非数组。"""
return chain[0] if chain else None
def _edge_meta(task: Any, wo: Any = None) -> Dict[str, Any]:
edge: Dict[str, Any] = {
"workOrderNo": "",
"workOrderType": "",
"taskNo": "",
"taskName": "",
"taskId": "",
}
if task:
edge["taskNo"] = task.task_no or ""
edge["taskName"] = task.task_name or ""
edge["taskId"] = task.id or ""
if wo:
edge["workOrderNo"] = wo.work_order_no or ""
edge["workOrderType"] = wo.reception_source or ""
return edge
def _sample_customer_dict(db: Session, protocol_id: str, tenant_no: str) -> Dict[str, Any]:
if not protocol_id:
return {}
from app.models.models import BizProtocolPartyCustomer
pc = (
db.query(BizProtocolPartyCustomer)
.filter(
BizProtocolPartyCustomer.collection_protocol_id == protocol_id,
BizProtocolPartyCustomer.tenant_no == tenant_no,
BizProtocolPartyCustomer.del_flag == 0,
)
.first()
)
if not pc:
return {}
return {
"motherName": pc.mother_name or "",
"motherPhone": pc.mother_phone or "",
"motherIdCard": pc.mother_id_card or "",
"newbornName": pc.newborn_name or "",
}
def _build_ancestor_chain(
db: Session, product: BizCellProduct, depth: int, tenant_no: str
) -> List[Dict[str, Any]]:
if depth >= _MAX_CHAIN_DEPTH:
return []
task_id = product.production_task_id
if not task_id:
return []
from app.models.models import BizProductionTask, BizProductionWorkOrder
from app.models.models import BizProductionTaskInput
task = (
db.query(BizProductionTask)
.filter(BizProductionTask.id == task_id, BizProductionTask.del_flag == 0)
.first()
)
if not task:
return []
wo = None
if task.work_order_id:
wo = (
db.query(BizProductionWorkOrder)
.filter(BizProductionWorkOrder.id == task.work_order_id, BizProductionWorkOrder.del_flag == 0)
.first()
)
edge = _edge_meta(task, wo)
# 检查投入物中是否有父细胞产品(按排序取首个)
parent_input = (
db.query(BizProductionTaskInput)
.filter(
BizProductionTaskInput.task_id == task_id,
BizProductionTaskInput.input_type == "cell_product",
BizProductionTaskInput.del_flag == 0,
)
.order_by(
BizProductionTaskInput.order_num.asc(),
BizProductionTaskInput.create_time.asc(),
)
.first()
)
if parent_input and (parent_input.input_id or parent_input.input_code):
parent_product = None
if parent_input.input_id:
parent_product = (
db.query(BizCellProduct)
.filter(BizCellProduct.id == parent_input.input_id, BizCellProduct.del_flag == 0)
.first()
)
if not parent_product and parent_input.input_code:
parent_product = (
db.query(BizCellProduct)
.filter(BizCellProduct.product_code == parent_input.input_code, BizCellProduct.del_flag == 0)
.first()
)
if parent_product:
parent_chain = _build_ancestor_chain(db, parent_product, depth + 1, tenant_no)
return [
{
"node": _product_node(parent_product),
"edge": edge,
"parent": _first_chain_link(parent_chain),
}
]
# 检查投入物中是否有样本(终止节点)
sample_input = (
db.query(BizProductionTaskInput)
.filter(
BizProductionTaskInput.task_id == task_id,
BizProductionTaskInput.input_type == "sample",
BizProductionTaskInput.del_flag == 0,
)
.first()
)
if sample_input and (sample_input.input_id or sample_input.input_code):
from app.models.models import BizProtocolSample
sample = None
if sample_input.input_id:
sample = (
db.query(BizProtocolSample)
.filter(BizProtocolSample.id == sample_input.input_id, BizProtocolSample.del_flag == 0)
.first()
)
if not sample and sample_input.input_code:
sample = (
db.query(BizProtocolSample)
.filter(BizProtocolSample.sample_code == sample_input.input_code, BizProtocolSample.del_flag == 0)
.first()
)
if sample:
reception_source = wo.reception_source if wo else ""
source_labels = {
"cell_storage": "细胞存储",
"research_donation": "科研捐赠",
}
protocol_id = sample.collection_protocol_id or ""
sample_node = {
"id": sample.id,
"productCode": sample.sample_code or "",
"productName": "",
"nodeType": "sample",
"sampleCode": sample.sample_code or "",
"sampleTypeName": sample_input.input_name or "",
"protocolNo": wo.protocol_no if wo else "",
"collectionProtocolId": protocol_id,
"receptionSource": reception_source,
"receptionSourceLabel": source_labels.get(reception_source, reception_source),
"customer": _sample_customer_dict(db, protocol_id, tenant_no),
"isCurrent": False,
}
return [{"node": sample_node, "edge": edge, "parent": None}]
return []
def _build_descendant_chain(
db: Session, product_id: str, product_code: str, depth: int, tenant_no: str
) -> List[Dict[str, Any]]:
if depth >= _MAX_CHAIN_DEPTH:
return []
from app.models.models import BizProductionTaskInput, BizProductionTaskOutput, BizProductionTask, BizProductionWorkOrder
# 找到引用此产品作为投入物的所有任务(id 或编码)
input_match = [BizProductionTaskInput.input_id == product_id]
if product_code:
input_match.append(BizProductionTaskInput.input_code == product_code)
input_rows = (
db.query(BizProductionTaskInput)
.filter(
BizProductionTaskInput.input_type == "cell_product",
BizProductionTaskInput.del_flag == 0,
or_(*input_match),
)
.all()
)
if not input_rows:
return []
task_ids = list({r.task_id for r in input_rows})
tasks = (
db.query(BizProductionTask)
.filter(BizProductionTask.id.in_(task_ids), BizProductionTask.del_flag == 0)
.all()
)
task_by_id = {t.id: t for t in tasks}
wo_ids = list({t.work_order_id for t in tasks if t.work_order_id})
wo_by_id: Dict[str, Any] = {}
if wo_ids:
wos = (
db.query(BizProductionWorkOrder)
.filter(BizProductionWorkOrder.id.in_(wo_ids), BizProductionWorkOrder.del_flag == 0)
.all()
)
wo_by_id = {w.id: w for w in wos}
# 对每个任务,找产出
result: List[Dict[str, Any]] = []
output_rows = (
db.query(BizProductionTaskOutput)
.filter(
BizProductionTaskOutput.task_id.in_(task_ids),
BizProductionTaskOutput.del_flag == 0,
)
.all()
)
child_product_ids = list({o.cell_product_id for o in output_rows if o.cell_product_id})
child_products = {}
if child_product_ids:
cps = (
db.query(BizCellProduct)
.filter(BizCellProduct.id.in_(child_product_ids), BizCellProduct.del_flag == 0)
.all()
)
child_products = {cp.id: cp for cp in cps}
seen_child_ids: set = set()
for o in output_rows:
cp = child_products.get(o.cell_product_id)
if not cp or cp.id in seen_child_ids:
continue
seen_child_ids.add(cp.id)
t = task_by_id.get(o.task_id)
wo_obj = wo_by_id.get(t.work_order_id) if t and t.work_order_id else None
edge = _edge_meta(t, wo_obj)
result.append({
"node": _product_node(cp),
"edge": edge,
"children": _build_descendant_chain(
db, cp.id, cp.product_code or "", depth + 1, tenant_no
),
})
return result
def get_product_chain(db: Session, product_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
product = get_cell_product(db, product_id, tenant_no)
if not product:
return None
return {
"current": _product_node(product, is_current=True),
"ancestors": _build_ancestor_chain(db, product, 0, tenant_no),
"descendants": _build_descendant_chain(
db, product.id, product.product_code or "", 0, tenant_no
),
}