diff --git a/backend/app/crud/production_work_order.py b/backend/app/crud/production_work_order.py new file mode 100644 index 000000000..acdcc6def --- /dev/null +++ b/backend/app/crud/production_work_order.py @@ -0,0 +1,266 @@ +"""入库生产工单 CRUD""" +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple + +from sqlalchemy import func, or_ +from sqlalchemy.orm import Session + +from app.core.utils import generate_uuid +from app.models.models import ( + BizCollectionProtocol, + BizProductionWorkOrder, + BizProtocolSample, + BizSampleReception, +) +from app.models.research_donation import BizResearchDonation, BizResearchDonationSample +from app.schemas.production_work_order import ProductionWorkOrderComplete, ProductionWorkOrderListQuery + +_WORK_ORDER_STATUS_LABELS = { + "dpz": "待配置", + "ypz": "已配置", + "jxz": "执行中", + "ywc": "已完成", + "yzf": "已作废", +} +_RECEPTION_SOURCE_LABELS = { + "cell_storage": "细胞存储", + "research_donation": "科研捐赠", +} + + +def _fmt_dt(t: Optional[datetime]) -> str: + return t.strftime("%Y-%m-%d %H:%M:%S") if t else "" + + +def _generate_work_order_no(db: Session, tenant_no: str) -> str: + today_str = datetime.now().strftime("%Y%m%d") + prefix = f"SC{today_str}" + last = ( + db.query(BizProductionWorkOrder) + .filter( + BizProductionWorkOrder.tenant_no == tenant_no, + BizProductionWorkOrder.work_order_no.like(f"{prefix}%"), + BizProductionWorkOrder.del_flag == 0, + ) + .order_by(BizProductionWorkOrder.work_order_no.desc()) + .first() + ) + seq = 1 + if last: + seq = int(last.work_order_no[-4:]) + 1 + return f"{prefix}{seq:04d}" + + +def _wo_to_dict(wo: BizProductionWorkOrder) -> Dict[str, Any]: + return { + "id": wo.id, + "workOrderNo": wo.work_order_no, + "receptionId": wo.reception_id, + "protocolSampleId": wo.protocol_sample_id, + "collectionProtocolId": wo.collection_protocol_id, + "protocolNo": wo.protocol_no or "", + "sampleCode": wo.sample_code or "", + "receptionSource": getattr(wo, "reception_source", "cell_storage") or "cell_storage", + "receptionSourceLabel": _RECEPTION_SOURCE_LABELS.get( + getattr(wo, "reception_source", "cell_storage") or "cell_storage", + getattr(wo, "reception_source", "cell_storage") or "cell_storage", + ), + "workOrderStatus": wo.work_order_status, + "workOrderStatusLabel": _WORK_ORDER_STATUS_LABELS.get(wo.work_order_status, wo.work_order_status), + "remark": wo.remark or "", + "createTime": _fmt_dt(wo.create_time), + "updateTime": _fmt_dt(wo.update_time), + } + + +def create_production_work_order_from_reception( + db: Session, + reception: BizSampleReception, + tenant_no: str, + user_id: Optional[str] = None, + create_dept_id: str = "", +) -> Optional[BizProductionWorkOrder]: + """接收结论为 js 时创建待配置入库生产工单(flush,不 commit)。""" + if (reception.accept_conclusion or "").strip() != "js": + return None + exists = ( + db.query(BizProductionWorkOrder) + .filter( + BizProductionWorkOrder.reception_id == reception.id, + BizProductionWorkOrder.tenant_no == tenant_no, + BizProductionWorkOrder.del_flag == 0, + ) + .first() + ) + if exists: + return exists + + source = getattr(reception, "reception_source", "cell_storage") or "cell_storage" + + if source == "research_donation": + protocol_no = "" + d = ( + db.query(BizResearchDonation) + .filter( + BizResearchDonation.id == (reception.research_donation_id or ""), + BizResearchDonation.del_flag == 0, + ) + .first() + ) + if d: + protocol_no = d.donation_no or "" + sample_code = "" + ds = ( + db.query(BizResearchDonationSample) + .filter( + BizResearchDonationSample.id == (reception.donation_sample_id or ""), + BizResearchDonationSample.del_flag == 0, + ) + .first() + ) + if ds: + sample_code = ds.sample_code or "" + wo = BizProductionWorkOrder( + id=generate_uuid(), + work_order_no=_generate_work_order_no(db, tenant_no), + reception_id=reception.id, + protocol_sample_id=reception.donation_sample_id or "", + collection_protocol_id=reception.research_donation_id or "", + protocol_no=protocol_no or None, + sample_code=sample_code or None, + reception_source="research_donation", + work_order_status="dpz", + tenant_no=tenant_no, + create_by=user_id, + create_dept_id=create_dept_id or "", + update_by=user_id, + create_time=datetime.now(), + update_time=datetime.now(), + del_flag=0, + ) + else: + protocol_no = "" + p = ( + db.query(BizCollectionProtocol) + .filter( + BizCollectionProtocol.id == reception.collection_protocol_id, + BizCollectionProtocol.del_flag == 0, + ) + .first() + ) + if p: + protocol_no = p.protocol_no or "" + sample_code = "" + s = ( + db.query(BizProtocolSample) + .filter( + BizProtocolSample.id == reception.protocol_sample_id, + BizProtocolSample.del_flag == 0, + ) + .first() + ) + if s: + sample_code = s.sample_code or "" + wo = BizProductionWorkOrder( + id=generate_uuid(), + work_order_no=_generate_work_order_no(db, tenant_no), + reception_id=reception.id, + protocol_sample_id=reception.protocol_sample_id, + collection_protocol_id=reception.collection_protocol_id, + protocol_no=protocol_no or None, + sample_code=sample_code or None, + reception_source="cell_storage", + work_order_status="dpz", + tenant_no=tenant_no, + create_by=user_id, + create_dept_id=create_dept_id or "", + update_by=user_id, + create_time=datetime.now(), + update_time=datetime.now(), + del_flag=0, + ) + db.add(wo) + db.flush() + return wo + + +def list_production_work_orders( + db: Session, + *, + tenant_no: str, + query: Optional[ProductionWorkOrderListQuery] = None, + page: int = 1, + page_size: int = 20, +) -> Dict[str, Any]: + def _filtered(): + q0 = db.query(BizProductionWorkOrder).filter(BizProductionWorkOrder.del_flag == 0) + q0 = q0.filter(BizProductionWorkOrder.tenant_no == tenant_no) + if not query: + return q0 + if query.work_order_no and query.work_order_no.strip(): + like = f"%{query.work_order_no.strip()}%" + q0 = q0.filter(BizProductionWorkOrder.work_order_no.like(like)) + if query.work_order_status and query.work_order_status.strip(): + q0 = q0.filter(BizProductionWorkOrder.work_order_status == query.work_order_status.strip()) + if query.protocol_no and query.protocol_no.strip(): + like = f"%{query.protocol_no.strip()}%" + q0 = q0.filter(BizProductionWorkOrder.protocol_no.like(like)) + if query.sample_code and query.sample_code.strip(): + like = f"%{query.sample_code.strip()}%" + q0 = q0.filter(BizProductionWorkOrder.sample_code.like(like)) + return q0 + + effective_page = query.page if query and query.page else page + effective_size = query.page_size if query and query.page_size else page_size + + total = _filtered().order_by(None).with_entities(func.count(BizProductionWorkOrder.id)).scalar() or 0 + effective_page = max(1, effective_page) + effective_size = min(max(1, effective_size), 100) + rows = ( + _filtered() + .order_by(BizProductionWorkOrder.create_time.desc()) + .offset((effective_page - 1) * effective_size) + .limit(effective_size) + .all() + ) + items = [_wo_to_dict(x) for x in rows] + return {"items": items, "total": int(total)} + + +def get_production_work_order( + db: Session, row_id: str, tenant_no: str +) -> Optional[BizProductionWorkOrder]: + return ( + db.query(BizProductionWorkOrder) + .filter( + BizProductionWorkOrder.id == row_id, + BizProductionWorkOrder.tenant_no == tenant_no, + BizProductionWorkOrder.del_flag == 0, + ) + .first() + ) + + +def complete_production_work_order( + db: Session, + row_id: str, + tenant_no: str, + user_id: str, + data: Optional[ProductionWorkOrderComplete] = None, +) -> BizProductionWorkOrder: + wo = get_production_work_order(db, row_id, tenant_no) + if not wo: + raise ValueError("工单不存在") + if wo.work_order_status == "ywc": + raise ValueError("工单已完成") + wo.work_order_status = "ywc" + wo.update_by = user_id + wo.update_time = datetime.now() + if data and data.remark is not None: + wo.remark = data.remark + db.add(wo) + db.commit() + db.refresh(wo) + return wo