Files
shanxia2.0/backend/app/crud/production_task.py
T
2026-05-24 10:03:00 +08:00

1574 lines
66 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_production_task + 步骤 + 子表 + 投入/产出)"""
from __future__ import annotations
import math
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, parse_datetime
from app.models.models import (
BizCellProduct,
BizCellSampleType,
BizCollectionProtocol,
BizProcess,
BizProcessConsumable,
BizProcessDataCollection,
BizProcessOutput,
BizProcessReagent,
BizProcessRoute,
BizProcessRouteStep,
BizProcessRouteStepConsumable,
BizProcessRouteStepDataCollection,
BizProcessRouteStepOutput,
BizProcessRouteStepReagent,
BizProductionTask,
BizProductionTaskInput,
BizProductionTaskOutput,
BizProductionTaskStep,
BizProductionTaskStepConsumable,
BizProductionTaskStepDataCollection,
BizProductionTaskStepOutput,
BizProductionTaskStepReagent,
BizProductionWorkOrder,
BizProtocolSample,
BizSampleReception,
BizStorageSlotOccupancy,
)
from app.crud import storage_occupancy as occupancy_svc
from app.crud.cell_product import _row_to_dict as _cp_row_to_dict, _enrich_product_dicts as _cp_enrich
from app.models.research_donation import BizResearchDonation
_PROTOCOL_STATUS_LABELS = {
"dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "zky": "转科研"
}
_DONATION_STATUS_LABELS = {
"dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "zckc": "转细胞存储"
}
def _fmt_dt(t: Optional[datetime]) -> str:
return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
def _generate_task_no(db: Session, tenant_no: str) -> str:
day = datetime.now().strftime("%Y%m%d")
like = f"RW{day}%"
cnt = (
db.query(func.count(BizProductionTask.id))
.filter(
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.task_no.like(like),
)
.scalar()
)
return f"RW{day}{(cnt or 0) + 1:04d}"
def _task_to_dict(t: BizProductionTask, *, work_order_no: str = "") -> Dict[str, Any]:
return {
"id": t.id,
"taskNo": t.task_no,
"workOrderId": t.work_order_id,
"workOrderNo": work_order_no,
"taskType": t.task_type or "cell_storage",
"taskName": t.task_name or "",
"description": t.description or "",
"routeId": t.route_id or "",
"taskStatus": t.task_status,
"startedTime": _fmt_dt(t.started_time),
"completedTime": _fmt_dt(t.completed_time),
"remark": t.remark or "",
"attachments": t.attachments or "",
"tenantNo": t.tenant_no,
"orderNum": t.order_num if t.order_num is not None else 0,
"createTime": _fmt_dt(t.create_time),
"updateTime": _fmt_dt(t.update_time),
}
def _step_to_dict(s: BizProductionTaskStep) -> Dict[str, Any]:
return {
"id": s.id,
"taskId": s.task_id,
"stepOrder": s.step_order if s.step_order is not None else 0,
"processCode": s.process_code or "",
"categoryId": s.category_id or "",
"processName": s.process_name or "",
"description": s.description or "",
"equipmentName": s.equipment_name or "",
"estimatedDuration": s.estimated_duration or "",
"actualDuration": s.actual_duration or "",
"sopIds": s.sop_ids or "",
"stepStatus": s.step_status or "dzx",
"startedTime": _fmt_dt(s.started_time),
"completedTime": _fmt_dt(s.completed_time),
"remark": s.remark or "",
}
def _step_reagent_to_dict(r: BizProductionTaskStepReagent) -> Dict[str, Any]:
return {
"id": r.id,
"stepId": r.step_id,
"reagentCode": r.reagent_code or "",
"reagentName": r.reagent_name or "",
"spec": r.spec or "",
"standardUsage": r.standard_usage or "",
"actualUsage": r.actual_usage or "",
"unit": r.unit or "",
"isKeyMaterial": r.is_key_material if r.is_key_material is not None else "0",
"orderNum": r.order_num if r.order_num is not None else 0,
"remark": r.remark or "",
}
def _step_consumable_to_dict(c: BizProductionTaskStepConsumable) -> Dict[str, Any]:
return {
"id": c.id,
"stepId": c.step_id,
"consumableCode": c.consumable_code or "",
"consumableName": c.consumable_name or "",
"specModel": c.spec_model or "",
"standardUsage": c.standard_usage or "",
"actualUsage": c.actual_usage or "",
"unit": c.unit or "",
"isRequired": c.is_required if c.is_required is not None else "1",
"orderNum": c.order_num if c.order_num is not None else 0,
"remark": c.remark or "",
}
def _step_output_to_dict(o: BizProductionTaskStepOutput) -> Dict[str, Any]:
return {
"id": o.id,
"stepId": o.step_id,
"outputTypeId": o.output_type_id or "",
"outputName": o.output_name or "",
"outputBarcode": o.output_barcode or "",
"orderNum": o.order_num if o.order_num is not None else 0,
"remark": o.remark or "",
}
def _step_data_collection_to_dict(d: BizProductionTaskStepDataCollection) -> Dict[str, Any]:
return {
"id": d.id,
"stepId": d.step_id,
"collectionName": d.collection_name or "",
"unit": d.unit or "",
"collectionValue": d.collection_value or "",
"orderNum": d.order_num if d.order_num is not None else 0,
"remark": d.remark or "",
}
def _task_input_to_dict(i: BizProductionTaskInput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
result = {
"id": i.id,
"taskId": i.task_id,
"inputType": i.input_type or "",
"inputId": i.input_id or "",
"inputCode": i.input_code or "",
"inputName": i.input_name or "",
"remark": i.remark or "",
}
if product_dict:
for key in [
"productCode", "productName", "productTypeId", "productTypeName",
"specId", "specName", "passage", "batchNo", "storageSlotName",
"expectedCellCount", "viability", "freezeDate", "expiryDate",
"productStatus", "productImages", "storageSlotId",
]:
if key in product_dict:
result[key] = product_dict[key]
return result
def _task_output_to_dict(o: BizProductionTaskOutput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
result = {
"id": o.id,
"taskId": o.task_id,
"cellProductId": o.cell_product_id or "",
"storageSlotId": o.storage_slot_id or "",
"outputName": o.output_name or "",
"outputBarcode": o.output_barcode or "",
"quantity": float(o.quantity) if o.quantity is not None else None,
"unit": o.unit or "",
"remark": o.remark or "",
"attachments": o.attachments or "",
}
if product_dict:
for key in [
"productCode", "productName", "productTypeId", "productTypeName",
"specId", "specName", "passage", "batchNo", "storageSlotName",
"expectedCellCount", "viability", "freezeDate", "expiryDate",
"productStatus", "productImages",
]:
if key in product_dict:
result[key] = product_dict[key]
return result
def _soft_delete_sub(db: Session, model_class, parent_field: str, parent_id: str, now: datetime) -> None:
rows = db.query(model_class).filter(
and_(
getattr(model_class, parent_field) == parent_id,
model_class.del_flag == 0,
)
).all()
for r in rows:
r.del_flag = 1
r.update_time = now
db.add(r)
def _resolve_sub_table_order_num(order_num: Optional[int], idx: int, rows: list) -> int:
"""复制/克隆子表排序号:保留源 order_num;若源记录均为 0 则按遍历顺序写入 idx。"""
if rows and len(rows) > 1 and all((getattr(r, "order_num", None) or 0) == 0 for r in rows):
return idx
if order_num is not None:
return order_num
return idx
def _step_sub_tables_to_dict(db: Session, step_id: str) -> Dict[str, Any]:
"""加载步骤四张子表并序列化(与 get_production_task_detail 一致)。"""
reagents = db.query(BizProductionTaskStepReagent).filter(
and_(BizProductionTaskStepReagent.step_id == step_id, BizProductionTaskStepReagent.del_flag == 0)
).order_by(BizProductionTaskStepReagent.order_num.asc(), BizProductionTaskStepReagent.create_time.asc()).all()
consumables = db.query(BizProductionTaskStepConsumable).filter(
and_(BizProductionTaskStepConsumable.step_id == step_id, BizProductionTaskStepConsumable.del_flag == 0)
).order_by(BizProductionTaskStepConsumable.order_num.asc(), BizProductionTaskStepConsumable.create_time.asc()).all()
outputs = db.query(BizProductionTaskStepOutput).filter(
and_(BizProductionTaskStepOutput.step_id == step_id, BizProductionTaskStepOutput.del_flag == 0)
).order_by(BizProductionTaskStepOutput.order_num.asc(), BizProductionTaskStepOutput.create_time.asc()).all()
data_collections = db.query(BizProductionTaskStepDataCollection).filter(
and_(BizProductionTaskStepDataCollection.step_id == step_id, BizProductionTaskStepDataCollection.del_flag == 0)
).order_by(BizProductionTaskStepDataCollection.order_num.asc(), BizProductionTaskStepDataCollection.create_time.asc()).all()
return {
"reagents": [_step_reagent_to_dict(r) for r in reagents],
"consumables": [_step_consumable_to_dict(c) for c in consumables],
"outputs": [_step_output_to_dict(o) for o in outputs],
"dataCollections": [_step_data_collection_to_dict(d) for d in data_collections],
}
def _clone_step_reagents(
db: Session, src_step_id: str, new_step_id: str,
tenant_no: str, user_id: Optional[str], now: datetime,
) -> None:
rows = db.query(BizProcessRouteStepReagent).filter(
and_(BizProcessRouteStepReagent.step_id == src_step_id, BizProcessRouteStepReagent.del_flag == 0)
).order_by(
BizProcessRouteStepReagent.order_num.asc(),
BizProcessRouteStepReagent.create_time.asc(),
).all()
for idx, r in enumerate(rows):
db.add(BizProductionTaskStepReagent(
id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
reagent_code=r.reagent_code, reagent_name=r.reagent_name,
spec=r.spec, standard_usage=r.standard_usage,
unit=r.unit, is_key_material=r.is_key_material,
remark=r.remark,
order_num=_resolve_sub_table_order_num(r.order_num, idx, rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _clone_step_consumables(
db: Session, src_step_id: str, new_step_id: str,
tenant_no: str, user_id: Optional[str], now: datetime,
) -> None:
rows = db.query(BizProcessRouteStepConsumable).filter(
and_(BizProcessRouteStepConsumable.step_id == src_step_id, BizProcessRouteStepConsumable.del_flag == 0)
).order_by(
BizProcessRouteStepConsumable.order_num.asc(),
BizProcessRouteStepConsumable.create_time.asc(),
).all()
for idx, c in enumerate(rows):
db.add(BizProductionTaskStepConsumable(
id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
consumable_code=c.consumable_code, consumable_name=c.consumable_name,
spec_model=c.spec_model, standard_usage=c.standard_usage,
unit=c.unit, is_required=c.is_required,
remark=c.remark,
order_num=_resolve_sub_table_order_num(c.order_num, idx, rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _clone_step_outputs(
db: Session, src_step_id: str, new_step_id: str,
tenant_no: str, user_id: Optional[str], now: datetime,
) -> None:
rows = db.query(BizProcessRouteStepOutput).filter(
and_(BizProcessRouteStepOutput.step_id == src_step_id, BizProcessRouteStepOutput.del_flag == 0)
).order_by(
BizProcessRouteStepOutput.order_num.asc(),
BizProcessRouteStepOutput.create_time.asc(),
).all()
for idx, o in enumerate(rows):
db.add(BizProductionTaskStepOutput(
id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
output_type_id=o.output_type_id, output_name=o.output_name,
output_barcode=o.output_barcode,
remark=o.remark,
order_num=_resolve_sub_table_order_num(o.order_num, idx, rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _clone_step_data_collections(
db: Session, src_step_id: str, new_step_id: str,
tenant_no: str, user_id: Optional[str], now: datetime,
) -> None:
rows = db.query(BizProcessRouteStepDataCollection).filter(
and_(BizProcessRouteStepDataCollection.step_id == src_step_id, BizProcessRouteStepDataCollection.del_flag == 0)
).order_by(
BizProcessRouteStepDataCollection.order_num.asc(),
BizProcessRouteStepDataCollection.create_time.asc(),
).all()
for idx, d in enumerate(rows):
db.add(BizProductionTaskStepDataCollection(
id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
collection_name=d.collection_name, unit=d.unit,
remark=d.remark,
order_num=_resolve_sub_table_order_num(d.order_num, idx, rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
def _clone_route_to_task(
db: Session, task_id: str, route_id: str,
tenant_no: str, user_id: Optional[str], create_dept_id: str,
) -> None:
route_steps = (
db.query(BizProcessRouteStep)
.filter(and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0))
.order_by(BizProcessRouteStep.step_order.asc(), BizProcessRouteStep.create_time.asc())
.all()
)
now = datetime.now()
for rs in route_steps:
new_step = BizProductionTaskStep(
id=generate_uuid(), task_id=task_id,
step_order=rs.step_order,
process_code=rs.process_code, category_id=rs.category_id,
process_name=rs.process_name, description=rs.description,
equipment_name=rs.equipment_name, estimated_duration=rs.estimated_duration,
sop_ids=rs.sop_ids,
step_status="dzx",
tenant_no=tenant_no, order_num=0,
remark=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(new_step)
db.flush()
_clone_step_reagents(db, rs.id, new_step.id, tenant_no, user_id, now)
_clone_step_consumables(db, rs.id, new_step.id, tenant_no, user_id, now)
_clone_step_outputs(db, rs.id, new_step.id, tenant_no, user_id, now)
_clone_step_data_collections(db, rs.id, new_step.id, tenant_no, user_id, now)
def create_production_task(
db: Session, *, tenant_no: str, user_id: Optional[str] = None,
create_dept_id: str = "", data: Dict[str, Any],
) -> BizProductionTask:
task_no = _generate_task_no(db, tenant_no)
now = datetime.now()
task = BizProductionTask(
id=generate_uuid(),
task_no=task_no,
work_order_id=data.get("work_order_id", ""),
task_type=data.get("task_type") or "cell_storage",
task_name=(data.get("task_name") or "").strip() or None,
description=(data.get("description") or "").strip() or None,
route_id=data.get("route_id") or None,
task_status="dpz",
remark=(data.get("remark") or "").strip() or None,
attachments=(data.get("attachments") or "").strip() or None,
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(task)
db.flush()
route_id = data.get("route_id")
if route_id:
_clone_route_to_task(db, task.id, route_id, tenant_no, user_id, create_dept_id)
from app.crud import production_work_order as production_work_order_svc
production_work_order_svc.sync_production_work_order_status_from_tasks(
db, task.work_order_id or "", tenant_no, user_id
)
db.flush()
return task
def list_production_tasks(
db: Session, *, tenant_no: str,
page: int = 1, page_size: int = 10,
keyword: Optional[str] = None,
work_order_id: Optional[str] = None,
task_no: Optional[str] = None,
task_status: Optional[str] = None,
user_info=None,
) -> Tuple[List[Dict[str, Any]], int]:
def _filtered():
q = db.query(BizProductionTask).filter(
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
kw = (keyword or "").strip()
if kw:
like = f"%{kw}%"
q = q.outerjoin(
BizProductionWorkOrder,
and_(
BizProductionWorkOrder.id == BizProductionTask.work_order_id,
BizProductionWorkOrder.del_flag == 0,
),
).filter(
or_(
BizProductionTask.task_no.like(like),
BizProductionWorkOrder.work_order_no.like(like),
BizProductionWorkOrder.protocol_no.like(like),
BizProductionWorkOrder.sample_code.like(like),
)
)
elif task_no and task_no.strip():
q = q.filter(BizProductionTask.task_no.like(f"%{task_no.strip()}%"))
if work_order_id and work_order_id.strip():
q = q.filter(BizProductionTask.work_order_id == work_order_id.strip())
if task_status and task_status.strip():
q = q.filter(BizProductionTask.task_status == task_status.strip())
if user_info:
from app.core.data_scope import apply_data_scope
q = apply_data_scope(q, BizProductionTask, user_info, db, tenant_no)
return q
total = _filtered().order_by(None).with_entities(func.count(BizProductionTask.id)).scalar() or 0
page = max(1, page)
page_size = min(max(1, page_size), 100)
rows = (
_filtered()
.order_by(BizProductionTask.create_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
items = [_task_to_dict(x) for x in rows]
# 批量补充路线名称
route_ids = [x.route_id for x in rows if x.route_id]
if route_ids:
route_map = {
r.id: r.route_name
for r in db.query(BizProcessRoute.id, BizProcessRoute.route_name)
.filter(BizProcessRoute.id.in_(route_ids))
.all()
}
for item, row in zip(items, rows):
item["routeName"] = route_map.get(row.route_id, "")
# 批量补充工单号 + 协议号 + 样本编码
wo_ids = [x.work_order_id for x in rows if x.work_order_id]
wo_map: Dict[str, Any] = {}
if wo_ids:
wo_rows = (
db.query(
BizProductionWorkOrder.id,
BizProductionWorkOrder.work_order_no,
BizProductionWorkOrder.protocol_no,
BizProductionWorkOrder.sample_code,
BizProductionWorkOrder.reception_id,
BizProductionWorkOrder.protocol_sample_id,
BizProductionWorkOrder.collection_protocol_id,
BizProductionWorkOrder.reception_source,
)
.filter(BizProductionWorkOrder.id.in_(wo_ids))
.all()
)
for w in wo_rows:
wo_map[w.id] = w
cell_wo_ids = [w.collection_protocol_id for w in wo_rows if (w.reception_source or "cell_storage") != "research_donation" and w.collection_protocol_id]
donation_wo_ids = [w.collection_protocol_id for w in wo_rows if (w.reception_source or "") == "research_donation" and w.collection_protocol_id]
proto_status_map: Dict[str, str] = {}
if cell_wo_ids:
for p in (
db.query(BizCollectionProtocol.id, BizCollectionProtocol.protocol_status)
.filter(BizCollectionProtocol.id.in_(cell_wo_ids), BizCollectionProtocol.del_flag == 0)
.all()
):
proto_status_map[p.id] = p.protocol_status or ""
donation_status_map: Dict[str, str] = {}
if donation_wo_ids:
for d in (
db.query(BizResearchDonation.id, BizResearchDonation.donation_status)
.filter(BizResearchDonation.id.in_(donation_wo_ids), BizResearchDonation.del_flag == 0)
.all()
):
donation_status_map[d.id] = d.donation_status or ""
for item, row in zip(items, rows):
wo = wo_map.get(row.work_order_id)
if wo:
item["workOrderNo"] = wo.work_order_no or ""
item["protocolNo"] = wo.protocol_no or ""
item["sampleCode"] = wo.sample_code or ""
_source = wo.reception_source or "cell_storage"
if _source == "research_donation":
_st = donation_status_map.get(wo.collection_protocol_id, "")
item["protocolStatus"] = _st
item["protocolStatusLabel"] = _DONATION_STATUS_LABELS.get(_st, _st)
else:
_st = proto_status_map.get(wo.collection_protocol_id, "")
item["protocolStatus"] = _st
item["protocolStatusLabel"] = _PROTOCOL_STATUS_LABELS.get(_st, _st)
# 批量补充接收人、接收时间
reception_ids = [wo.reception_id for wo in wo_map.values() if getattr(wo, "reception_id", None)]
reception_map: Dict[str, Any] = {}
if reception_ids:
for r in (
db.query(
BizSampleReception.id,
BizSampleReception.handler_name,
BizSampleReception.reception_time,
)
.filter(BizSampleReception.id.in_(reception_ids))
.all()
):
reception_map[r.id] = r
# 批量补充样本类型名称
protocol_sample_ids = [
wo.protocol_sample_id for wo in wo_map.values() if getattr(wo, "protocol_sample_id", None)
]
sample_type_map: Dict[str, str] = {}
if protocol_sample_ids:
ps_rows = (
db.query(BizProtocolSample.id, BizProtocolSample.sample_type_id)
.filter(BizProtocolSample.id.in_(protocol_sample_ids))
.all()
)
st_ids = [ps.sample_type_id for ps in ps_rows if ps.sample_type_id]
if st_ids:
st_name_map = {
st.id: st.type_name
for st in db.query(BizCellSampleType.id, BizCellSampleType.type_name)
.filter(BizCellSampleType.id.in_(st_ids))
.all()
}
for ps in ps_rows:
sample_type_map[ps.id] = st_name_map.get(ps.sample_type_id, "") if ps.sample_type_id else ""
# 将补充字段写入 items
for item, row in zip(items, rows):
wo = wo_map.get(row.work_order_id)
if wo:
item["protocolNo"] = wo.protocol_no or ""
item["sampleCode"] = wo.sample_code or ""
# 接收人、接收时间
reception = reception_map.get(wo.reception_id) if wo.reception_id else None
item["receiverName"] = reception.handler_name or "" if reception else ""
item["receptionTime"] = _fmt_dt(reception.reception_time) if reception else ""
# 样本类型
item["sampleTypeName"] = sample_type_map.get(wo.protocol_sample_id, "") if wo.protocol_sample_id else ""
return items, int(total)
def get_production_task_detail(db: Session, task_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
return None
wo = db.query(BizProductionWorkOrder).filter(BizProductionWorkOrder.id == task.work_order_id).first()
result = _task_to_dict(task, work_order_no=wo.work_order_no if wo else "")
if wo:
result["sampleCode"] = wo.sample_code or ""
result["protocolNo"] = wo.protocol_no or ""
_src = getattr(wo, "reception_source", "cell_storage") or "cell_storage"
_cpid = wo.collection_protocol_id or ""
if _src == "research_donation" and _cpid:
d = db.query(BizResearchDonation.donation_status).filter(
BizResearchDonation.id == _cpid, BizResearchDonation.del_flag == 0
).first()
_st = d.donation_status or "" if d else ""
result["protocolStatus"] = _st
result["protocolStatusLabel"] = _DONATION_STATUS_LABELS.get(_st, _st)
elif _cpid:
p = db.query(BizCollectionProtocol.protocol_status).filter(
BizCollectionProtocol.id == _cpid, BizCollectionProtocol.del_flag == 0
).first()
_st = p.protocol_status or "" if p else ""
result["protocolStatus"] = _st
result["protocolStatusLabel"] = _PROTOCOL_STATUS_LABELS.get(_st, _st)
else:
result["protocolStatus"] = ""
result["protocolStatusLabel"] = ""
steps = (
db.query(BizProductionTaskStep)
.filter(and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0))
.order_by(BizProductionTaskStep.step_order.asc(), BizProductionTaskStep.create_time.asc())
.all()
)
step_list = []
for s in steps:
sd = _step_to_dict(s)
reagents = db.query(BizProductionTaskStepReagent).filter(
and_(BizProductionTaskStepReagent.step_id == s.id, BizProductionTaskStepReagent.del_flag == 0)
).order_by(BizProductionTaskStepReagent.order_num.asc(), BizProductionTaskStepReagent.create_time.asc()).all()
consumables = db.query(BizProductionTaskStepConsumable).filter(
and_(BizProductionTaskStepConsumable.step_id == s.id, BizProductionTaskStepConsumable.del_flag == 0)
).order_by(BizProductionTaskStepConsumable.order_num.asc(), BizProductionTaskStepConsumable.create_time.asc()).all()
outputs = db.query(BizProductionTaskStepOutput).filter(
and_(BizProductionTaskStepOutput.step_id == s.id, BizProductionTaskStepOutput.del_flag == 0)
).order_by(BizProductionTaskStepOutput.order_num.asc(), BizProductionTaskStepOutput.create_time.asc()).all()
data_collections = db.query(BizProductionTaskStepDataCollection).filter(
and_(BizProductionTaskStepDataCollection.step_id == s.id, BizProductionTaskStepDataCollection.del_flag == 0)
).order_by(BizProductionTaskStepDataCollection.order_num.asc(), BizProductionTaskStepDataCollection.create_time.asc()).all()
sd["reagents"] = [_step_reagent_to_dict(r) for r in reagents]
sd["consumables"] = [_step_consumable_to_dict(c) for c in consumables]
sd["outputs"] = [_step_output_to_dict(o) for o in outputs]
sd["dataCollections"] = [_step_data_collection_to_dict(d) for d in data_collections]
step_list.append(sd)
inputs = db.query(BizProductionTaskInput).filter(
and_(BizProductionTaskInput.task_id == task_id, BizProductionTaskInput.del_flag == 0)
).order_by(BizProductionTaskInput.order_num.asc(), BizProductionTaskInput.create_time.asc()).all()
outputs = db.query(BizProductionTaskOutput).filter(
and_(BizProductionTaskOutput.task_id == task_id, BizProductionTaskOutput.del_flag == 0)
).order_by(BizProductionTaskOutput.order_num.asc(), BizProductionTaskOutput.create_time.asc()).all()
result["steps"] = step_list
# --- enrich inputs with cell product info ---
input_dicts = [_task_input_to_dict(i) for i in inputs]
cp_input_ids = [i.input_id for i in inputs if i.input_type == "cell_product" and i.input_id]
input_product_map: Dict[str, Dict[str, Any]] = {}
if cp_input_ids:
cp_rows = db.query(BizCellProduct).filter(
BizCellProduct.id.in_(set(cp_input_ids)), BizCellProduct.del_flag == 0
).all()
cp_dicts = [_cp_row_to_dict(c) for c in cp_rows]
cp_dicts = _cp_enrich(db, tenant_no, cp_dicts)
input_product_map = {d["id"]: d for d in cp_dicts}
for idx, inp in enumerate(inputs):
if inp.input_type == "cell_product" and inp.input_id and inp.input_id in input_product_map:
input_dicts[idx] = _task_input_to_dict(inp, product_dict=input_product_map[inp.input_id])
result["inputs"] = input_dicts
# --- enrich outputs with cell product info ---
output_dicts = [_task_output_to_dict(o) for o in outputs]
cp_output_ids = [o.cell_product_id for o in outputs if o.cell_product_id]
output_product_map: Dict[str, Dict[str, Any]] = {}
if cp_output_ids:
cp_rows = db.query(BizCellProduct).filter(
BizCellProduct.id.in_(set(cp_output_ids)), BizCellProduct.del_flag == 0
).all()
cp_dicts = [_cp_row_to_dict(c) for c in cp_rows]
cp_dicts = _cp_enrich(db, tenant_no, cp_dicts)
output_product_map = {d["id"]: d for d in cp_dicts}
for idx, out in enumerate(outputs):
if out.cell_product_id and out.cell_product_id in output_product_map:
output_dicts[idx] = _task_output_to_dict(out, product_dict=output_product_map[out.cell_product_id])
result["outputs"] = output_dicts
return result
def update_production_task(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], data: Dict[str, Any],
) -> BizProductionTask:
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
raise ValueError("任务不存在")
for field in ["task_name", "description", "remark", "attachments"]:
val = data.get(field)
if val is not None:
setattr(task, field, (val or "").strip() or None)
task.update_by = user_id
task.update_time = datetime.now()
db.add(task)
db.commit()
db.refresh(task)
return task
def update_task_status(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], status: str,
) -> BizProductionTask:
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
raise ValueError("任务不存在")
now = datetime.now()
task.task_status = status
if status == "jxz" and not task.started_time:
task.started_time = now
if status == "ywc" and not task.completed_time:
task.completed_time = now
task.update_by = user_id
task.update_time = now
db.add(task)
from app.crud import production_work_order as production_work_order_svc
production_work_order_svc.sync_production_work_order_status_from_tasks(
db, task.work_order_id or "", tenant_no, user_id
)
db.commit()
db.refresh(task)
return task
def add_task_step(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], data: Dict[str, Any],
) -> Dict[str, Any]:
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
raise ValueError("任务不存在")
now = datetime.now()
step_order = data.get("step_order")
if step_order is None:
max_order = db.query(func.max(BizProductionTaskStep.step_order)).filter(
and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0)
).scalar()
step_order = (max_order or 0) + 1
# 如果传入了 process_id,从工艺主表深拷贝基本信息和子表数据
process_id = data.get("process_id")
if process_id:
proc = db.query(BizProcess).filter(
and_(BizProcess.id == process_id, BizProcess.del_flag == 0)
).first()
if not proc:
raise ValueError("工艺不存在")
step = BizProductionTaskStep(
id=generate_uuid(), task_id=task_id,
step_order=step_order,
process_code=proc.process_code,
category_id=proc.category_id,
process_name=proc.process_name,
description=proc.description,
equipment_name=proc.equipment_name,
estimated_duration=proc.estimated_duration,
sop_ids=proc.sop_ids,
step_status="dzx",
tenant_no=tenant_no, order_num=0,
remark=None,
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(step)
db.flush()
# 深拷贝工艺子表 → 任务步骤子表
reagent_rows = db.query(BizProcessReagent).filter(
and_(BizProcessReagent.process_id == process_id, BizProcessReagent.del_flag == 0)
).order_by(BizProcessReagent.order_num.asc(), BizProcessReagent.create_time.asc()).all()
for idx, r in enumerate(reagent_rows):
db.add(BizProductionTaskStepReagent(
id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
reagent_code=r.reagent_code, reagent_name=r.reagent_name,
spec=r.spec, standard_usage=r.standard_usage,
unit=r.unit, is_key_material=r.is_key_material,
remark=r.remark,
order_num=_resolve_sub_table_order_num(r.order_num, idx, reagent_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
consumable_rows = db.query(BizProcessConsumable).filter(
and_(BizProcessConsumable.process_id == process_id, BizProcessConsumable.del_flag == 0)
).order_by(BizProcessConsumable.order_num.asc(), BizProcessConsumable.create_time.asc()).all()
for idx, c in enumerate(consumable_rows):
db.add(BizProductionTaskStepConsumable(
id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
consumable_code=c.consumable_code, consumable_name=c.consumable_name,
spec_model=c.spec_model, standard_usage=c.standard_usage,
unit=c.unit, is_required=c.is_required,
remark=c.remark,
order_num=_resolve_sub_table_order_num(c.order_num, idx, consumable_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
output_rows = db.query(BizProcessOutput).filter(
and_(BizProcessOutput.process_id == process_id, BizProcessOutput.del_flag == 0)
).order_by(BizProcessOutput.order_num.asc(), BizProcessOutput.create_time.asc()).all()
for idx, o in enumerate(output_rows):
db.add(BizProductionTaskStepOutput(
id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
output_type_id=o.output_type_id, output_name=o.output_name,
output_barcode=o.output_barcode,
remark=o.remark,
order_num=_resolve_sub_table_order_num(o.order_num, idx, output_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
collection_rows = db.query(BizProcessDataCollection).filter(
and_(BizProcessDataCollection.process_id == process_id, BizProcessDataCollection.del_flag == 0)
).order_by(BizProcessDataCollection.order_num.asc(), BizProcessDataCollection.create_time.asc()).all()
for idx, d in enumerate(collection_rows):
db.add(BizProductionTaskStepDataCollection(
id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
collection_name=d.collection_name, unit=d.unit,
remark=d.remark,
order_num=_resolve_sub_table_order_num(d.order_num, idx, collection_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.commit()
else:
# 无 process_id 时,使用前端传来的字段直接创建(兼容旧逻辑)
step = BizProductionTaskStep(
id=generate_uuid(), task_id=task_id,
step_order=step_order,
process_code=(data.get("process_code") or "").strip() or None,
category_id=(data.get("category_id") or "").strip() or None,
process_name=(data.get("process_name") or "").strip() or None,
description=(data.get("description") or "").strip() or None,
equipment_name=(data.get("equipment_name") or "").strip() or None,
estimated_duration=(data.get("estimated_duration") or "").strip() or None,
actual_duration=(data.get("actual_duration") or "").strip() or None,
sop_ids=(data.get("sop_ids") or "").strip() or None,
step_status=(data.get("step_status") or "dzx"),
tenant_no=tenant_no, order_num=0,
remark=(data.get("remark") or "").strip() or None,
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(step)
db.commit()
# 返回带子表的完整步骤信息(与 get_production_task_detail 格式一致)
result = _step_to_dict(step)
reagents = db.query(BizProductionTaskStepReagent).filter(
and_(BizProductionTaskStepReagent.step_id == step.id, BizProductionTaskStepReagent.del_flag == 0)
).order_by(BizProductionTaskStepReagent.order_num.asc(), BizProductionTaskStepReagent.create_time.asc()).all()
consumables = db.query(BizProductionTaskStepConsumable).filter(
and_(BizProductionTaskStepConsumable.step_id == step.id, BizProductionTaskStepConsumable.del_flag == 0)
).order_by(BizProductionTaskStepConsumable.order_num.asc(), BizProductionTaskStepConsumable.create_time.asc()).all()
outputs = db.query(BizProductionTaskStepOutput).filter(
and_(BizProductionTaskStepOutput.step_id == step.id, BizProductionTaskStepOutput.del_flag == 0)
).order_by(BizProductionTaskStepOutput.order_num.asc(), BizProductionTaskStepOutput.create_time.asc()).all()
data_collections = db.query(BizProductionTaskStepDataCollection).filter(
and_(BizProductionTaskStepDataCollection.step_id == step.id, BizProductionTaskStepDataCollection.del_flag == 0)
).order_by(BizProductionTaskStepDataCollection.order_num.asc(), BizProductionTaskStepDataCollection.create_time.asc()).all()
result["reagents"] = [_step_reagent_to_dict(r) for r in reagents]
result["consumables"] = [_step_consumable_to_dict(c) for c in consumables]
result["outputs"] = [_step_output_to_dict(o) for o in outputs]
result["dataCollections"] = [_step_data_collection_to_dict(d) for d in data_collections]
return result
def update_task_step(
db: Session, step_id: str, tenant_no: str,
user_id: Optional[str], data: Dict[str, Any],
) -> BizProductionTaskStep:
step = db.query(BizProductionTaskStep).filter(
and_(
BizProductionTaskStep.id == step_id,
BizProductionTaskStep.tenant_no == tenant_no,
BizProductionTaskStep.del_flag == 0,
)
).first()
if not step:
raise ValueError("步骤不存在")
for field in [
"step_order", "process_code", "category_id", "process_name",
"description", "equipment_name", "estimated_duration", "actual_duration",
"sop_ids", "step_status", "remark",
]:
val = data.get(field)
if val is not None:
if field == "step_order":
setattr(step, field, int(val))
else:
setattr(step, field, (val or "").strip() or None)
if data.get("started_time"):
from dateutil.parser import parse as _parse_dt
step.started_time = _parse_dt(data["started_time"])
if data.get("completed_time"):
from dateutil.parser import parse as _parse_dt
step.completed_time = _parse_dt(data["completed_time"])
step.update_by = user_id
step.update_time = datetime.now()
db.add(step)
db.commit()
db.refresh(step)
return step
def delete_task_step(db: Session, step_id: str, tenant_no: str, user_id: Optional[str]) -> None:
step = db.query(BizProductionTaskStep).filter(
and_(
BizProductionTaskStep.id == step_id,
BizProductionTaskStep.tenant_no == tenant_no,
BizProductionTaskStep.del_flag == 0,
)
).first()
if not step:
raise ValueError("步骤不存在")
now = datetime.now()
step.del_flag = 1
step.update_by = user_id
step.update_time = now
db.add(step)
_soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step_id, now)
_soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step_id, now)
_soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step_id, now)
_soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step_id, now)
db.commit()
def replace_step_reagents(
db: Session, step_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# Look up task info from step_id for stock log
step_row = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
).first()
task_id = ""
task_no = ""
step_name = ""
if step_row:
step_name = step_row.process_name or ""
task_row = db.query(BizProductionTask).filter(
and_(BizProductionTask.id == step_row.task_id, BizProductionTask.del_flag == 0)
).first()
if task_row:
task_id = task_row.id
task_no = task_row.task_no or ""
try:
_soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step_id, now)
# Revert previous step_save stock deductions
from app.crud import reagent_stock_log as reagent_stock_log_crud
reagent_stock_log_crud.revert_step_stock_changes(db, step_id, tenant_no, user_id)
for idx, it in enumerate(items):
db.add(BizProductionTaskStepReagent(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
reagent_code=(it.get("reagent_code") or "").strip() or None,
reagent_name=(it.get("reagent_name") or "").strip() or None,
spec=(it.get("spec") or "").strip() or None,
standard_usage=(it.get("standard_usage") or "").strip() or None,
actual_usage=(it.get("actual_usage") or "").strip() or None,
unit=(it.get("unit") or "").strip() or None,
is_key_material=it.get("is_key_material", "0"),
remark=(it.get("remark") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# Deduct stock based on actual_usage
reagent_stock_log_crud.deduct_for_step(
db, step_id, step_name, task_id, task_no, items, tenant_no, user_id, "",
)
db.commit()
except Exception:
db.rollback()
raise
def replace_step_consumables(
db: Session, step_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# Look up task info from step_id for stock log
step_row = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
).first()
task_id = ""
task_no = ""
step_name = ""
if step_row:
step_name = step_row.process_name or ""
task_row = db.query(BizProductionTask).filter(
and_(BizProductionTask.id == step_row.task_id, BizProductionTask.del_flag == 0)
).first()
if task_row:
task_id = task_row.id
task_no = task_row.task_no or ""
try:
_soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step_id, now)
# Revert previous step_save stock deductions
from app.crud import consumable_stock_log as consumable_stock_log_crud
consumable_stock_log_crud.revert_step_stock_changes(db, step_id, tenant_no, user_id)
for idx, it in enumerate(items):
db.add(BizProductionTaskStepConsumable(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
consumable_code=(it.get("consumable_code") or "").strip() or None,
consumable_name=(it.get("consumable_name") or "").strip() or None,
spec_model=(it.get("spec_model") or "").strip() or None,
standard_usage=(it.get("standard_usage") or "").strip() or None,
actual_usage=(it.get("actual_usage") or "").strip() or None,
unit=(it.get("unit") or "").strip() or None,
is_required=it.get("is_required", "1"),
remark=(it.get("remark") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# Deduct stock based on actual_usage
consumable_stock_log_crud.deduct_for_step(
db, step_id, step_name, task_id, task_no, items, tenant_no, user_id, "",
)
db.commit()
except Exception:
db.rollback()
raise
def replace_step_outputs(
db: Session, step_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
_soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step_id, now)
for idx, it in enumerate(items):
db.add(BizProductionTaskStepOutput(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
output_type_id=(it.get("output_type_id") or "").strip() or None,
output_name=(it.get("output_name") or "").strip() or None,
output_barcode=(it.get("output_barcode") or "").strip() or None,
remark=(it.get("remark") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.commit()
def replace_step_data_collections(
db: Session, step_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
_soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step_id, now)
for idx, it in enumerate(items):
db.add(BizProductionTaskStepDataCollection(
id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
collection_name=(it.get("collection_name") or "").strip() or None,
unit=(it.get("unit") or "").strip() or None,
collection_value=(it.get("collection_value") or "").strip() or None,
remark=(it.get("remark") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.commit()
def set_task_inputs(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# --- 1. 加载旧 inputs 并按 cell_product 恢复状态 ---
old_rows = db.query(BizProductionTaskInput).filter(
and_(
BizProductionTaskInput.task_id == task_id,
BizProductionTaskInput.del_flag == 0,
)
).all()
for old_row in old_rows:
# 如果是细胞产品,恢复状态
if old_row.input_type == "cell_product" and old_row.input_id:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == old_row.input_id, BizCellProduct.del_flag == 0
).first()
if product and product.product_status == "in_production":
product.product_status = "frozen"
product.update_by = user_id
product.update_time = now
db.add(product)
# --- 2. 全量软删除旧行 ---
_soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
# --- 3. 按传入顺序批量新建 ---
for idx, it in enumerate(items):
inp_type = (it.get("input_type") or it.get("inputType") or "").strip()
inp_id = (it.get("input_id") or it.get("inputId") or "").strip()
# 如果是细胞产品,设置状态为 in_production
if inp_type == "cell_product" and inp_id:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == inp_id, BizCellProduct.del_flag == 0
).first()
if product and product.product_status in ("frozen", "thawed"):
product.product_status = "in_production"
product.update_by = user_id
product.update_time = now
db.add(product)
db.add(BizProductionTaskInput(
id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
input_type=inp_type or None,
input_id=inp_id or None,
input_code=(it.get("input_code") or it.get("inputCode") or "").strip() or None,
input_name=(it.get("input_name") or it.get("inputName") or "").strip() or None,
remark=(it.get("remark") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.flush()
db.commit()
def _parse_optional_date(val):
"""解析可选日期字符串(兼容 YYYY-MM-DD 与 ISO 8601)"""
if not val:
return None
if isinstance(val, datetime):
return val
return parse_datetime(str(val).strip())
def _create_occupancy_for_product(db: Session, product: BizCellProduct, tenant_no: str, user_id: Optional[str]):
"""为细胞产品创建库位占用记录"""
if not product.storage_slot_id:
return
now = datetime.now()
occ = BizStorageSlotOccupancy(
id=generate_uuid(),
slot_id=product.storage_slot_id,
item_type="cell_product",
item_id=product.id,
item_code=product.product_code or "",
item_name=product.product_name or "",
item_source="production",
stored_at=now,
occupancy_status="stored",
tenant_no=tenant_no,
order_num=0,
remark=None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(occ)
db.flush()
occupancy_svc._refresh_slot_cache(db, product.storage_slot_id)
def _update_product_from_output(product: BizCellProduct, it: Dict[str, Any], user_id, now):
"""从 output item 更新细胞产品字段"""
field_map = [
("product_name", "product_name", "productName"),
("product_type_id", "product_type_id", "productTypeId"),
("spec_id", "spec_id", "specId"),
("batch_no", "batch_no", "batchNo"),
("storage_slot_id", "storage_slot_id", "storageSlotId"),
("unit", "unit", "unit"),
("product_images", "product_images", "productImages"),
("remark", "remark", "remark"),
]
for db_field, snake_key, camel_key in field_map:
val = it.get(snake_key) if it.get(snake_key) is not None else it.get(camel_key)
if val is not None:
setattr(product, db_field, (str(val) or "").strip() or None)
for num_field, snake_key, camel_key in [
("expected_cell_count", "expected_cell_count", "expectedCellCount"),
("viability", "viability", "viability"),
]:
val = it.get(snake_key)
if val is None:
val = it.get(camel_key)
if val is not None:
setattr(product, num_field, val)
for date_field, snake_key, camel_key in [
("freeze_date", "freeze_date", "freezeDate"),
("expiry_date", "expiry_date", "expiryDate"),
]:
val = it.get(snake_key) or it.get(camel_key)
if val is not None:
setattr(product, date_field, _parse_optional_date(val))
passage = it.get("passage")
if passage is not None:
product.passage = (str(passage) or "").strip() or None
quantity = it.get("quantity")
if quantity is not None:
product.quantity = quantity
product.update_by = user_id
product.update_time = now
def set_task_outputs(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# --- 1. 加载旧 outputs ---
old_rows = db.query(BizProductionTaskOutput).filter(
and_(
BizProductionTaskOutput.task_id == task_id,
BizProductionTaskOutput.del_flag == 0,
)
).all()
old_cp_map: Dict[str, BizProductionTaskOutput] = {}
for o in old_rows:
if o.cell_product_id:
old_cp_map[o.cell_product_id] = o
# --- 2. 构建新的 cp_id 集合 ---
new_cp_ids: set = set()
for it in items:
cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
if cp_id:
new_cp_ids.add(cp_id)
# --- 3. 处理删除(旧有新无) ---
for cp_id, old_row in old_cp_map.items():
if cp_id not in new_cp_ids:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
).first()
if product:
product.product_status = "voided"
product.update_by = user_id
product.update_time = now
db.add(product)
if product.storage_slot_id:
occupancy_svc.remove_by_item(db, "cell_product", cp_id, user_id=user_id)
old_row.del_flag = 1
old_row.update_by = user_id
old_row.update_time = now
db.add(old_row)
# --- 4. 处理新增和修改 ---
for idx, it in enumerate(items):
cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
if cp_id and cp_id in old_cp_map:
# === 修改 ===
product = db.query(BizCellProduct).filter(
BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
).first()
if product:
old_slot_id = product.storage_slot_id
_update_product_from_output(product, it, user_id, now)
new_slot_id = product.storage_slot_id
db.add(product)
if old_slot_id != new_slot_id:
if old_slot_id:
occupancy_svc.remove_by_item(db, "cell_product", cp_id, user_id=user_id)
if new_slot_id:
_create_occupancy_for_product(db, product, tenant_no, user_id)
old_row = old_cp_map[cp_id]
old_row.output_name = product.product_name if product else ""
old_row.output_barcode = product.product_code if product else ""
old_row.storage_slot_id = product.storage_slot_id if product else None
old_row.quantity = it.get("quantity")
old_row.unit = (it.get("unit") or "").strip() or None
old_row.remark = (it.get("remark") or "").strip() or None
old_row.order_num = idx
old_row.update_by = user_id
old_row.update_time = now
db.add(old_row)
else:
# === 新增 ===
product = BizCellProduct(
id=generate_uuid(),
product_code=(it.get("product_code") or it.get("productCode") or "").strip(),
product_name=(it.get("product_name") or it.get("productName") or "").strip(),
product_type_id=(it.get("product_type_id") or it.get("productTypeId") or "").strip() or None,
spec_id=(it.get("spec_id") or it.get("specId") or "").strip() or None,
passage=(it.get("passage") or "").strip() or None,
batch_no=(it.get("batch_no") or it.get("batchNo") or "").strip() or None,
storage_slot_id=(it.get("storage_slot_id") or it.get("storageSlotId") or "").strip() or None,
expected_cell_count=it.get("expected_cell_count") if it.get("expected_cell_count") is not None else it.get("expectedCellCount"),
viability=it.get("viability"),
freeze_date=_parse_optional_date(it.get("freeze_date") or it.get("freezeDate")),
product_status=it.get("product_status") or it.get("productStatus") or "frozen",
source_type="production",
production_task_id=task_id,
quantity=it.get("quantity"),
unit=(it.get("unit") or "").strip() or None,
expiry_date=_parse_optional_date(it.get("expiry_date") or it.get("expiryDate")),
product_images=(it.get("product_images") or it.get("productImages") or "").strip() or None,
tenant_no=tenant_no,
order_num=idx,
remark=(it.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(product)
db.flush()
if product.storage_slot_id:
_create_occupancy_for_product(db, product, tenant_no, user_id)
output_row = BizProductionTaskOutput(
id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
cell_product_id=product.id,
storage_slot_id=product.storage_slot_id,
output_name=product.product_name or "",
output_barcode=product.product_code or "",
quantity=product.quantity,
unit=product.unit,
remark=product.remark,
attachments=(it.get("attachments") or "").strip() or None,
order_num=idx, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(output_row)
db.flush()
db.commit()
def reorder_task_steps(db: Session, task_id: str, step_ids: List[str]) -> None:
now = datetime.now()
for idx, sid in enumerate(step_ids):
step = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.id == sid, BizProductionTaskStep.task_id == task_id,
BizProductionTaskStep.del_flag == 0)
).first()
if step:
step.step_order = idx + 1
step.update_time = now
db.add(step)
db.commit()
def copy_task_step(db: Session, step_id: str, *, tenant_no: str = "zkxw",
user_id: Optional[str] = None) -> Dict[str, Any]:
"""复制生产任务步骤,插入到原步骤之后,名称追加(复制)"""
src = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
).first()
if not src:
raise ValueError("步骤不存在")
now = datetime.now()
new_order = src.step_order + 1
# 后续步骤 step_order +1
later_steps = db.query(BizProductionTaskStep).filter(
and_(
BizProductionTaskStep.task_id == src.task_id,
BizProductionTaskStep.del_flag == 0,
BizProductionTaskStep.step_order > src.step_order,
)
).order_by(BizProductionTaskStep.step_order.asc()).all()
for ls in later_steps:
ls.step_order += 1
ls.update_time = now
db.add(ls)
# 创建新步骤
new_name = (src.process_name or "").strip()
if not new_name.endswith("(复制)"):
new_name += "(复制)"
new_step = BizProductionTaskStep(
id=generate_uuid(), task_id=src.task_id,
step_order=new_order,
process_code=src.process_code, category_id=src.category_id,
process_name=new_name, description=src.description,
equipment_name=src.equipment_name, estimated_duration=src.estimated_duration,
actual_duration=src.actual_duration, sop_ids=src.sop_ids,
step_status="dzx",
tenant_no=tenant_no, order_num=0,
remark=src.remark, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(new_step)
db.flush()
# 复制子表:试剂(从生产任务步骤试剂表复制)
reagent_rows = db.query(BizProductionTaskStepReagent).filter(
and_(BizProductionTaskStepReagent.step_id == step_id, BizProductionTaskStepReagent.del_flag == 0)
).order_by(
BizProductionTaskStepReagent.order_num.asc(),
BizProductionTaskStepReagent.create_time.asc(),
).all()
for idx, r in enumerate(reagent_rows):
db.add(BizProductionTaskStepReagent(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
reagent_code=r.reagent_code, reagent_name=r.reagent_name,
spec=r.spec, standard_usage=r.standard_usage,
actual_usage=r.actual_usage,
unit=r.unit, is_key_material=r.is_key_material,
remark=r.remark,
order_num=_resolve_sub_table_order_num(r.order_num, idx, reagent_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# 复制子表:耗材
consumable_rows = db.query(BizProductionTaskStepConsumable).filter(
and_(BizProductionTaskStepConsumable.step_id == step_id, BizProductionTaskStepConsumable.del_flag == 0)
).order_by(
BizProductionTaskStepConsumable.order_num.asc(),
BizProductionTaskStepConsumable.create_time.asc(),
).all()
for idx, c in enumerate(consumable_rows):
db.add(BizProductionTaskStepConsumable(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
consumable_code=c.consumable_code, consumable_name=c.consumable_name,
spec_model=c.spec_model, standard_usage=c.standard_usage,
actual_usage=c.actual_usage,
unit=c.unit, is_required=c.is_required,
remark=c.remark,
order_num=_resolve_sub_table_order_num(c.order_num, idx, consumable_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# 复制子表:产出
output_rows = db.query(BizProductionTaskStepOutput).filter(
and_(BizProductionTaskStepOutput.step_id == step_id, BizProductionTaskStepOutput.del_flag == 0)
).order_by(
BizProductionTaskStepOutput.order_num.asc(),
BizProductionTaskStepOutput.create_time.asc(),
).all()
for idx, o in enumerate(output_rows):
db.add(BizProductionTaskStepOutput(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
output_type_id=o.output_type_id, output_name=o.output_name,
output_barcode=o.output_barcode,
remark=o.remark,
order_num=_resolve_sub_table_order_num(o.order_num, idx, output_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
# 复制子表:数据采集
collection_rows = db.query(BizProductionTaskStepDataCollection).filter(
and_(BizProductionTaskStepDataCollection.step_id == step_id, BizProductionTaskStepDataCollection.del_flag == 0)
).order_by(
BizProductionTaskStepDataCollection.order_num.asc(),
BizProductionTaskStepDataCollection.create_time.asc(),
).all()
for idx, d in enumerate(collection_rows):
db.add(BizProductionTaskStepDataCollection(
id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
collection_name=d.collection_name, unit=d.unit,
collection_value=d.collection_value,
remark=d.remark,
order_num=_resolve_sub_table_order_num(d.order_num, idx, collection_rows),
create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.commit()
result = _step_to_dict(new_step)
result.update(_step_sub_tables_to_dict(db, new_step.id))
return result
def delete_production_task(db: Session, task_id: str, tenant_no: str, user_id: Optional[str]) -> None:
"""软删除生产任务及其所有子表数据(步骤 + 投入 + 产出)"""
task = db.query(BizProductionTask).filter(
and_(
BizProductionTask.id == task_id,
BizProductionTask.tenant_no == tenant_no,
BizProductionTask.del_flag == 0,
)
).first()
if not task:
raise ValueError("任务不存在")
now = datetime.now()
# 1. 软删除所有步骤及其子表
steps = db.query(BizProductionTaskStep).filter(
and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0)
).all()
for step in steps:
step.del_flag = 1
step.update_by = user_id
step.update_time = now
db.add(step)
_soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step.id, now)
_soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step.id, now)
_soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step.id, now)
_soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step.id, now)
# 2. 软删除投入
_soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
# 3. 软删除产出
_soft_delete_sub(db, BizProductionTaskOutput, "task_id", task_id, now)
# 4. 软删除任务本体
task.del_flag = 1
task.update_by = user_id
task.update_time = now
db.add(task)
work_order_id = task.work_order_id or ""
from app.crud import production_work_order as production_work_order_svc
production_work_order_svc.sync_production_work_order_status_from_tasks(
db, work_order_id, tenant_no, user_id
)
db.commit()
def total_pages(total: int, page_size: int) -> int:
if total <= 0:
return 0
return int(math.ceil(total / float(page_size)))