"""质检任务 CRUD""" from __future__ import annotations from datetime import datetime from decimal import Decimal from typing import Any, Dict, List, Optional, Tuple from sqlalchemy import and_, func, or_ from sqlalchemy.orm import Session from app.core.utils import generate_uuid from app.crud.collection_protocol import urls_to_attachment_list from app.crud import sample_reception as sample_reception_svc from app.crud.qc_work_order import ( _load_sample_type_name_map, _qc_biz_type_label, _reception_source_label, _resolve_reception_id_for_work_order_detail, _sample_type_name_for_reception, _work_order_qc_biz_type, _OUTBOUND_BIZ_TYPE_LABELS, build_work_order_list_summaries, ) from app.core.db_collation import eq_unicode, unicode_ci from app.utils.sample_special import apply_special_to_item_from_samples from app.crud import qc_task_assignee as assignee_svc from app.models.models import ( BizCollectionProtocol, BizProtocolSample, BizQcTask, BizQcTaskAssignee, BizQcWorkOrder, BizQcWorkOrderLabItem, BizQcWorkOrderThirdItem, BizSampleReception, ) from app.models.research_donation import BizResearchDonation, BizResearchDonationSample def _dec(v: Any) -> Optional[float]: if v is None: return None return float(v) def _fmt_dt(t: Optional[datetime]) -> str: return t.strftime("%Y-%m-%d %H:%M:%S") if t else "" def _gen_task_no(db: Session, work_order, seq: int) -> str: return f"{work_order.work_order_no}-{seq:02d}" _CONCLUSION_LABELS = {"tg": "通过", "btg": "不通过"} _PROTOCOL_STATUS_LABELS = { "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zky": "转科研" } _DONATION_STATUS_LABELS = { "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zckc": "转细胞存储" } def _conclusion_label(v: Optional[str]) -> str: key = (v or "").strip() return _CONCLUSION_LABELS.get(key, key) def _task_to_dict( t: BizQcTask, work_order_no: str = "", db: Optional[Session] = None, tenant_no: Optional[str] = None, ) -> Dict[str, Any]: tn = tenant_no or t.tenant_no return { "id": t.id, "workOrderId": t.work_order_id, "batchId": getattr(t, "batch_id", None) or "", "orderSource": getattr(t, "order_source", "inbound") or "inbound", "workOrderNo": work_order_no, "taskNo": t.task_no, "taskType": t.task_type, "sourceLineId": t.source_line_id, "itemCode": t.item_code, "itemName": t.item_name, "companyId": t.company_id or "", "companyName": t.company_name or "", "feeAmount": _dec(t.fee_amount), "taskStatus": t.task_status, "conclusion": t.conclusion or "", "conclusionLabel": _conclusion_label(t.conclusion), "thirdPartyAttachments": t.third_party_attachments or "", "attachmentFiles": urls_to_attachment_list(t.third_party_attachments, db, tn) if db else [], "completedTime": _fmt_dt(t.completed_time), "completedBy": t.completed_by or "", "remark": t.remark or "", "createTime": _fmt_dt(t.create_time), "updateTime": _fmt_dt(t.update_time), } def _task_work_order_context(wo: BizQcWorkOrder, t: BizQcTask) -> Dict[str, Any]: biz = _work_order_qc_biz_type(wo) wo_type = getattr(wo, "work_order_type", None) or "inbound" if wo_type == "temporary": order_source = "temporary" recv_source = "" recv_label = "" elif wo_type == "outbound": order_source = "outbound" recv_source = getattr(wo, "biz_type", "") or "" recv_label = _OUTBOUND_BIZ_TYPE_LABELS.get(recv_source, recv_source) if recv_source else "" else: order_source = getattr(t, "order_source", "inbound") or "inbound" recv_source = getattr(wo, "reception_source", "cell_storage") or "cell_storage" recv_label = _reception_source_label(recv_source) return { "orderSource": order_source, "receptionId": wo.reception_id, "protocolNo": wo.protocol_no or "", "sampleCode": wo.sample_code or t.sample_code or "", "qcBizType": biz, "qcBizTypeLabel": _qc_biz_type_label(biz), "receptionSource": recv_source, "receptionSourceLabel": recv_label, "cellProductId": getattr(wo, "cell_product_id", None) or "", "cellProductCode": "", "cellProductName": "", "cellProductTypeName": "", "cellProductPassage": "", "cellProductSpecName": "", "receptionTime": "", "handlerName": "", "sampleTypeName": "", "workOrderStatus": wo.work_order_status or "", } _WORK_ORDER_SUMMARY_COPY_KEYS = ( "triggerSource", "triggerSourceLabel", "hasLab", "hasThirdParty", "labStorageCode", "protocolSampleId", "collectionProtocolId", "applicationNo", "applicationName", "projectTitle", "customerName", "channelName", "patientName", "infusionTime", "sortNo", "totalCourseCount", "products", "cellType", "specText", "productionWorkOrderId", "productionWorkOrderNo", "cellProductCode", "cellProductName", "cellProductTypeName", "cellProductPassage", "cellProductSpecName", "paymentSummary", "feeSummary", "bizType", "bizTypeLabel", "motherName", "newbornName", "isSpecial", "specialRemark", "unfinishedBadgeCount", "protocolSiblingSamples", ) def _merge_work_order_summary_into_task(item: Dict[str, Any], summary: Dict[str, Any]) -> None: """将列表级工单摘要合并进任务行,供「我的质检任务」前端聚合展示。""" for key in _WORK_ORDER_SUMMARY_COPY_KEYS: if key in summary: item[key] = summary[key] if summary.get("createTime"): item["workOrderCreateTime"] = summary["createTime"] if "remark" in summary: item["workOrderRemark"] = summary.get("remark") or "" for key in ( "receptionTime", "handlerName", "sampleTypeName", "protocolStatus", "protocolStatusLabel", "motherName", "newbornName", "isSpecial", "specialRemark", ): if not item.get(key) and summary.get(key): item[key] = summary[key] def _enrich_mine_tasks_with_work_order_summaries( db: Session, rows: List[Tuple[BizQcTask, BizQcWorkOrder]], items: List[Dict[str, Any]], tenant_no: Optional[str], ) -> List[Dict[str, Any]]: if not rows or not items: return items summaries = build_work_order_list_summaries( db, [wo for _, wo in rows], tenant_no ) if not summaries: return items for item in items: summary = summaries.get(item.get("workOrderId") or "") if summary: _merge_work_order_summary_into_task(item, summary) return items def _enrich_tasks_reception_fields( db: Session, items: List[Dict[str, Any]] ) -> List[Dict[str, Any]]: if not items: return items rec_ids = [x.get("receptionId") for x in items if x.get("receptionId")] if not rec_ids: return items rec_map = { r.id: r for r in db.query(BizSampleReception) .filter(BizSampleReception.id.in_(rec_ids), BizSampleReception.del_flag == 0) .all() } cell_sample_ids = [ r.protocol_sample_id for r in rec_map.values() if (r.reception_source or "cell_storage") != "research_donation" and r.protocol_sample_id ] donation_sample_ids = [ r.donation_sample_id for r in rec_map.values() if (r.reception_source or "") == "research_donation" and r.donation_sample_id ] protocol_samples: Dict[str, BizProtocolSample] = {} if cell_sample_ids: protocol_samples = { s.id: s for s in db.query(BizProtocolSample) .filter(BizProtocolSample.id.in_(cell_sample_ids), BizProtocolSample.del_flag == 0) .all() } donation_samples: Dict[str, BizResearchDonationSample] = {} if donation_sample_ids: donation_samples = { s.id: s for s in db.query(BizResearchDonationSample) .filter( BizResearchDonationSample.id.in_(donation_sample_ids), BizResearchDonationSample.del_flag == 0, ) .all() } type_ids = list( { s.sample_type_id for s in list(protocol_samples.values()) + list(donation_samples.values()) if s.sample_type_id } ) type_map = _load_sample_type_name_map(db, type_ids) cell_proto_ids = [ rec.collection_protocol_id for rec in rec_map.values() if (rec.reception_source or "cell_storage") != "research_donation" and rec.collection_protocol_id ] donation_ids = [ rec.research_donation_id for rec in rec_map.values() if (rec.reception_source or "") == "research_donation" and rec.research_donation_id ] proto_status_map: Dict[str, str] = {} proto_void_type_map: Dict[str, str] = {} tenant_no_for_status = "" if cell_proto_ids: for p in ( db.query( BizCollectionProtocol.id, BizCollectionProtocol.protocol_status, BizCollectionProtocol.void_type, BizCollectionProtocol.tenant_no, ) .filter(BizCollectionProtocol.id.in_(cell_proto_ids), BizCollectionProtocol.del_flag == 0) .all() ): proto_status_map[p.id] = p.protocol_status or "" proto_void_type_map[p.id] = p.void_type or "" if not tenant_no_for_status and (p.tenant_no or "").strip(): tenant_no_for_status = (p.tenant_no or "").strip() donation_status_map: Dict[str, str] = {} donation_void_type_map: Dict[str, str] = {} if donation_ids: for d in ( db.query( BizResearchDonation.id, BizResearchDonation.donation_status, BizResearchDonation.void_type, BizResearchDonation.tenant_no, ) .filter(BizResearchDonation.id.in_(donation_ids), BizResearchDonation.del_flag == 0) .all() ): donation_status_map[d.id] = d.donation_status or "" donation_void_type_map[d.id] = d.void_type or "" if not tenant_no_for_status and (d.tenant_no or "").strip(): tenant_no_for_status = (d.tenant_no or "").strip() for item in items: if item.get("orderSource") in ("temporary", "outbound"): item.setdefault("receptionSource", "") item.setdefault("receptionSourceLabel", "") continue rec = rec_map.get(item.get("receptionId") or "") if not rec: continue item["receptionTime"] = _fmt_dt(rec.reception_time) item["handlerName"] = rec.handler_name or "" item["sampleTypeName"] = _sample_type_name_for_reception( db, rec, type_map, protocol_samples=protocol_samples, donation_samples=donation_samples, ) source = item.get("receptionSource") or (rec.reception_source if rec else "cell_storage") if source == "research_donation": _did = rec.research_donation_id if rec else "" _st = donation_status_map.get(_did or "", "") _vt = donation_void_type_map.get(_did or "", "") item["protocolStatus"] = _st item["voidType"] = _vt from app.core.protocol_status_display import format_protocol_status_label item["protocolStatusLabel"] = format_protocol_status_label( _st, _vt, kind="donation", db=db, tenant_no=tenant_no_for_status or None ) else: _pid = rec.collection_protocol_id if rec else "" _st = proto_status_map.get(_pid or "", "") _vt = proto_void_type_map.get(_pid or "", "") item["protocolStatus"] = _st item["voidType"] = _vt from app.core.protocol_status_display import format_protocol_status_label item["protocolStatusLabel"] = format_protocol_status_label( _st, _vt, kind="protocol", db=db, tenant_no=tenant_no_for_status or None ) apply_special_to_item_from_samples( item, source=source, protocol_sample_id=rec.protocol_sample_id or "", donation_sample_id=rec.donation_sample_id or "", protocol_samples=protocol_samples, donation_samples=donation_samples, ) return items def _task_row_to_dict( t: BizQcTask, wo: BizQcWorkOrder, db: Session, tenant_no: Optional[str], ) -> Dict[str, Any]: out = _task_to_dict(t, wo.work_order_no or "", db, tenant_no) out.update(_task_work_order_context(wo, t)) return out def create_task_for_line( db: Session, *, work_order: BizQcWorkOrder, task_type: str, source_line_id: str, item_code: str, item_name: str, tenant_no: str, user_id: Optional[str] = None, create_dept_id: str = "", seq: int = 1, company_id: Optional[str] = None, company_name: Optional[str] = None, fee_amount: Optional[Decimal] = None, order_source: str = "inbound", batch_id: Optional[str] = None, ) -> BizQcTask: obj = BizQcTask( id=generate_uuid(), work_order_id=work_order.id, batch_id=batch_id, order_source=order_source, task_no=_gen_task_no(db, work_order, seq), task_type=task_type, source_line_id=source_line_id, sample_code=getattr(work_order, "sample_code", None) or None, item_code=item_code, item_name=item_name, company_id=company_id, company_name=company_name, fee_amount=fee_amount, task_status="dzx", tenant_no=tenant_no, order_num=seq, 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() return obj def list_tasks( db: Session, *, page: int = 1, page_size: int = 10, keyword: Optional[str] = None, task_type: Optional[str] = None, task_status: Optional[str] = None, work_order_id: Optional[str] = None, qc_biz_type: Optional[str] = None, tenant_no: Optional[str] = None, user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: def _filtered(): q0 = ( db.query(BizQcTask, BizQcWorkOrder) .join( BizQcWorkOrder, and_( BizQcWorkOrder.id == BizQcTask.work_order_id, BizQcWorkOrder.del_flag == 0, ), ) .filter(BizQcTask.del_flag == 0) ) if tenant_no: q0 = q0.filter(BizQcTask.tenant_no == tenant_no) if work_order_id: q0 = q0.filter(BizQcTask.work_order_id == work_order_id) kw = (keyword or "").strip() if kw: like = f"%{kw}%" q0 = q0.filter( or_( BizQcTask.task_no.like(like), BizQcTask.item_name.like(like), BizQcWorkOrder.work_order_no.like(like), BizQcWorkOrder.sample_code.like(like), ) ) tt = (task_type or "").strip() if tt: q0 = q0.filter(BizQcTask.task_type == tt) st = (task_status or "").strip() if st: q0 = q0.filter(BizQcTask.task_status == st) bt = (qc_biz_type or "").strip() if bt: if bt == "temporary": q0 = q0.filter( or_( BizQcWorkOrder.qc_biz_type == "temporary", BizQcWorkOrder.work_order_type == "temporary", ) ) else: q0 = q0.filter(BizQcWorkOrder.qc_biz_type == bt) if user_info: from app.core.data_scope import apply_data_scope q0 = apply_data_scope(q0, BizQcTask, user_info, db, tenant_no) return q0 total = _filtered().order_by(None).with_entities(func.count(BizQcTask.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) rows = ( _filtered() .order_by( BizQcTask.create_time.desc(), BizQcWorkOrder.work_order_no.desc(), BizQcTask.task_no.asc(), ) .offset((page - 1) * page_size) .limit(page_size) .all() ) items = [_task_row_to_dict(t, wo, db, tenant_no) for t, wo in rows] items = _enrich_tasks_reception_fields(db, items) return assignee_svc.enrich_tasks_with_assignees(db, items, tenant_no), int(total) def list_mine_tasks( db: Session, *, assignee_user_id: str, page: int = 1, page_size: int = 10, keyword: Optional[str] = None, task_type: Optional[str] = None, task_status: Optional[str] = None, qc_biz_type: Optional[str] = None, reception_source: Optional[str] = None, biz_type: Optional[str] = None, tenant_no: Optional[str] = None, ) -> Tuple[List[Dict[str, Any]], int]: def _filtered(): q0 = ( db.query(BizQcTask, BizQcWorkOrder) .join( BizQcWorkOrder, and_( BizQcWorkOrder.id == BizQcTask.work_order_id, BizQcWorkOrder.del_flag == 0, ), ) .join( BizQcTaskAssignee, and_( eq_unicode(BizQcTaskAssignee.task_id, BizQcTask.id), unicode_ci(BizQcTaskAssignee.user_id) == assignee_user_id, BizQcTaskAssignee.del_flag == 0, ), ) .filter(BizQcTask.del_flag == 0) ) if tenant_no: q0 = q0.filter(BizQcTask.tenant_no == tenant_no) kw = (keyword or "").strip() if kw: like = f"%{kw}%" q0 = q0.filter( or_( BizQcTask.task_no.like(like), BizQcTask.item_name.like(like), BizQcWorkOrder.work_order_no.like(like), BizQcWorkOrder.sample_code.like(like), ) ) tt = (task_type or "").strip() if tt: q0 = q0.filter(BizQcTask.task_type == tt) st = (task_status or "").strip() if st: q0 = q0.filter(BizQcTask.task_status == st) bt = (qc_biz_type or "").strip() if bt: if bt == "temporary": q0 = q0.filter( or_( BizQcWorkOrder.qc_biz_type == "temporary", BizQcWorkOrder.work_order_type == "temporary", ) ) else: q0 = q0.filter(BizQcWorkOrder.qc_biz_type == bt) rs = (reception_source or "").strip() if rs: q0 = q0.filter(BizQcWorkOrder.reception_source == rs) bt_out = (biz_type or "").strip() if bt_out: q0 = q0.filter(BizQcWorkOrder.biz_type == bt_out) return q0 total = _filtered().order_by(None).with_entities(func.count(BizQcTask.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) rows = ( _filtered() .order_by( BizQcTask.create_time.desc(), BizQcWorkOrder.work_order_no.desc(), BizQcTask.task_no.asc(), ) .offset((page - 1) * page_size) .limit(page_size) .all() ) items = [_task_row_to_dict(t, wo, db, tenant_no) for t, wo in rows] items = _enrich_tasks_reception_fields(db, items) items = _enrich_mine_tasks_with_work_order_summaries(db, rows, items, tenant_no) return assignee_svc.enrich_tasks_with_assignees(db, items, tenant_no), int(total) def assert_task_assignee( db: Session, task_id: str, user_id: str, tenant_no: str ) -> None: if not assignee_svc.is_user_assignee(db, task_id, user_id, tenant_no): raise PermissionError("您不是该任务的负责人,无权操作") def list_tasks_by_work_order(db: Session, work_order_id: str, tenant_no: str) -> List[Dict[str, Any]]: wo_id = (work_order_id or "").strip() if not wo_id: return [] return list_tasks_by_work_order_ids(db, [wo_id], tenant_no).get(wo_id, []) def list_tasks_by_work_order_ids( db: Session, work_order_ids: List[str], tenant_no: str ) -> Dict[str, List[Dict[str, Any]]]: ids = list({(x or "").strip() for x in work_order_ids if (x or "").strip()}) if not ids or not tenant_no: return {} ids = ids[:100] rows = ( db.query(BizQcTask, BizQcWorkOrder) .join(BizQcWorkOrder, BizQcWorkOrder.id == BizQcTask.work_order_id) .filter( BizQcTask.work_order_id.in_(ids), BizQcTask.tenant_no == tenant_no, BizQcTask.del_flag == 0, BizQcWorkOrder.del_flag == 0, ) .order_by(BizQcTask.work_order_id.asc(), BizQcTask.order_num.asc()) .all() ) items = [_task_row_to_dict(t, wo, db, tenant_no) for t, wo in rows] items = _enrich_tasks_reception_fields(db, items) items = assignee_svc.enrich_tasks_with_assignees(db, items, tenant_no) grouped: Dict[str, List[Dict[str, Any]]] = {wo_id: [] for wo_id in ids} for item in items: wo_id = (item.get("workOrderId") or "").strip() if wo_id in grouped: grouped[wo_id].append(item) return grouped def get_task( db: Session, row_id: str, tenant_no: Optional[str] = None ) -> Optional[BizQcTask]: q = db.query(BizQcTask).filter(BizQcTask.id == row_id, BizQcTask.del_flag == 0) if tenant_no: q = q.filter(BizQcTask.tenant_no == tenant_no) return q.first() def get_task_detail(db: Session, row_id: str, tenant_no: str) -> Dict[str, Any]: row = ( db.query(BizQcTask, BizQcWorkOrder) .join(BizQcWorkOrder, BizQcWorkOrder.id == BizQcTask.work_order_id) .filter( BizQcTask.id == row_id, BizQcTask.tenant_no == tenant_no, BizQcTask.del_flag == 0, BizQcWorkOrder.del_flag == 0, ) .first() ) if not row: raise ValueError("任务不存在") t, wo = row out = _task_row_to_dict(t, wo, db, tenant_no) out = _enrich_tasks_reception_fields(db, [out])[0] out = assignee_svc.enrich_tasks_with_assignees(db, [out], tenant_no)[0] from app.crud.qc_work_order import _enrich_work_order_qc_targets out = _enrich_work_order_qc_targets(db, tenant_no, [out])[0] if (out.get("qcBizType") or "") != "temporary": resolved_reception_id = _resolve_reception_id_for_work_order_detail(db, wo, tenant_no) reception_detail = ( sample_reception_svc.get_reception_by_id(db, resolved_reception_id, tenant_no) if resolved_reception_id else None ) if reception_detail: out["reception"] = reception_detail return out def start_task(db: Session, row_id: str, *, tenant_no: str, user_id: Optional[str]) -> BizQcTask: t = get_task(db, row_id, tenant_no=tenant_no) if not t: raise ValueError("任务不存在") if t.task_type != "sysz": raise ValueError("仅实验室自检任务可开始") if t.task_status != "dzx": raise ValueError("当前状态不可开始") t.task_status = "jxz" t.update_by = user_id t.update_time = datetime.now() db.add(t) db.commit() db.refresh(t) return t def _set_task_remark(t: BizQcTask, remark: Optional[str]) -> None: t.remark = (remark or "").strip() or None def _require_dsfjc_attachments(t: BizQcTask, *, for_status: str = "ywc") -> None: """第三方检测且任务状态为已完成时,须至少有一个检测材料附件。""" if (t.task_type or "").strip() != "dsfjc": return if (for_status or "").strip() != "ywc": return if not (t.third_party_attachments or "").strip(): raise ValueError("第三方检测须上传检测材料附件") def complete_task( db: Session, row_id: str, *, tenant_no: str, user_id: Optional[str], conclusion: Optional[str] = None, remark: Optional[str] = None, commit: bool = True, ) -> BizQcTask: t = get_task(db, row_id, tenant_no=tenant_no) if not t: raise ValueError("任务不存在") concl = (conclusion or "").strip() if conclusion is not None else "" if concl and concl not in ("tg", "btg"): raise ValueError("检测结论无效") if t.task_type == "sysz": if t.task_status not in ("dzx", "jxz"): raise ValueError("当前状态不可完成") elif t.task_type != "dsfjc": raise ValueError("任务类型无效") _require_dsfjc_attachments(t) t.task_status = "ywc" if concl: t.conclusion = concl _set_task_remark(t, remark) t.completed_time = datetime.now() t.completed_by = user_id t.update_by = user_id t.update_time = datetime.now() db.add(t) if commit: db.commit() db.refresh(t) from app.crud import qc_work_order as wo_svc wo_svc.try_complete_work_order(db, t.work_order_id, tenant_no) else: db.flush() return t _QC_TASK_RESULT_STATUSES = frozenset({"dzx", "jxz", "ywc"}) def resolve_task_result_conclusion( conclusion: Optional[str], target_status: str, ) -> str: """Normalize conclusion for apply_task_result. Returns '' | 'tg' | 'btg'. Empty allowed only when target is dzx/jxz. """ concl = (conclusion or "").strip() if concl and concl not in ("tg", "btg"): raise ValueError("检测结论无效") if target_status == "ywc" and concl not in ("tg", "btg"): raise ValueError("请选择检测结论(通过/不通过)") return concl def apply_task_result( db: Session, row_id: str, *, tenant_no: str, user_id: Optional[str], conclusion: Optional[str] = None, remark: Optional[str] = None, material_usages: Optional[List[Dict[str, Any]]] = None, task_status: Optional[str] = None, commit: bool = True, ) -> BizQcTask: """保存检测结果与结论,并按指定任务状态落库(默认已完成)。""" from app.crud import qc_material_usage as mu_svc from app.crud import qc_work_order as wo_svc t = get_task(db, row_id, tenant_no=tenant_no) if not t: raise ValueError("任务不存在") target = (task_status or "").strip() or "ywc" if target not in _QC_TASK_RESULT_STATUSES: raise ValueError("任务状态无效") concl = resolve_task_result_conclusion(conclusion, target) _require_dsfjc_attachments(t, for_status=target) if material_usages is not None: mu_svc.deduct_for_task( db, row_id, task_no=t.task_no or "", material_usages=material_usages, tenant_no=tenant_no, user_id=user_id, ) was_done = (t.task_status or "").strip() == "ywc" t.conclusion = concl or None _set_task_remark(t, remark if remark is not None else "") t.task_status = target t.update_by = user_id t.update_time = datetime.now() if target == "ywc": t.completed_time = datetime.now() t.completed_by = user_id else: t.completed_time = None t.completed_by = None db.add(t) if commit: db.commit() db.refresh(t) if target == "ywc": wo_svc.try_complete_work_order(db, t.work_order_id, tenant_no) elif was_done: wo_svc.try_reopen_work_order(db, t.work_order_id, tenant_no) else: db.flush() return t def update_task_attachments( db: Session, row_id: str, *, attachments: str, tenant_no: str, user_id: Optional[str], commit: bool = True, ) -> BizQcTask: t = get_task(db, row_id, tenant_no=tenant_no) if not t: raise ValueError("任务不存在") t.third_party_attachments = (attachments or "").strip() or None if ( t.task_type == "dsfjc" and t.task_status == "dzx" and t.third_party_attachments ): t.task_status = "jxz" t.update_by = user_id t.update_time = datetime.now() db.add(t) if commit: db.commit() db.refresh(t) else: db.flush() return t def create_qc_task_manual( db: Session, *, tenant_no: str, user_id: Optional[str] = None, create_dept_id: str = "", task_type: str = "sysz", item_code: str = "", item_name: str = "", company_id: Optional[str] = None, company_name: Optional[str] = None, fee_amount: Optional[float] = None, sample_code: str = "", work_order_id: str = "", remark: str = "", ) -> BizQcTask: """手动创建质检任务(可独立于工单)""" # 生成任务号:ZJRW + 日期 + 序号 day = datetime.now().strftime("%Y%m%d") like = f"ZJRW{day}%" cnt = db.query(BizQcTask).filter( BizQcTask.tenant_no == tenant_no, BizQcTask.task_no.like(like), ).count() task_no = f"ZJRW{day}{cnt + 1:04d}" task = BizQcTask( id=generate_uuid(), work_order_id=work_order_id or "", task_no=task_no, task_type=task_type, source_line_id="", sample_code=sample_code, item_code=item_code, item_name=item_name, company_id=company_id, company_name=company_name, fee_amount=fee_amount, task_status="dzx", tenant_no=tenant_no, remark=remark, create_by=user_id, create_dept_id=create_dept_id, ) db.add(task) db.flush() return task def _soft_delete_source_line_for_task( db: Session, task: BizQcTask, *, tenant_no: str, user_id: Optional[str], now: datetime, ) -> None: """删除任务时同步软删批次下对应 lab/third 明细行,避免配置弹窗仍勾选该项目。""" sid = (task.source_line_id or "").strip() if not sid: return task_type = (task.task_type or "").strip() line = None if task_type == "sysz": line = ( db.query(BizQcWorkOrderLabItem) .filter( BizQcWorkOrderLabItem.id == sid, BizQcWorkOrderLabItem.tenant_no == tenant_no, BizQcWorkOrderLabItem.del_flag == 0, ) .first() ) elif task_type == "dsfjc": line = ( db.query(BizQcWorkOrderThirdItem) .filter( BizQcWorkOrderThirdItem.id == sid, BizQcWorkOrderThirdItem.tenant_no == tenant_no, BizQcWorkOrderThirdItem.del_flag == 0, ) .first() ) else: line = ( db.query(BizQcWorkOrderLabItem) .filter( BizQcWorkOrderLabItem.id == sid, BizQcWorkOrderLabItem.tenant_no == tenant_no, BizQcWorkOrderLabItem.del_flag == 0, ) .first() ) if not line: line = ( db.query(BizQcWorkOrderThirdItem) .filter( BizQcWorkOrderThirdItem.id == sid, BizQcWorkOrderThirdItem.tenant_no == tenant_no, BizQcWorkOrderThirdItem.del_flag == 0, ) .first() ) if not line: return line.del_flag = 1 line.update_by = user_id line.update_time = now db.add(line) def _refresh_work_order_item_flags( db: Session, wo: BizQcWorkOrder, *, tenant_no: str, user_id: Optional[str], now: datetime, ) -> None: """按未删除明细行回写工单 has_lab / has_third_party / 第三方费用合计。""" lab_count = ( db.query(BizQcWorkOrderLabItem) .filter( BizQcWorkOrderLabItem.work_order_id == wo.id, BizQcWorkOrderLabItem.tenant_no == tenant_no, BizQcWorkOrderLabItem.del_flag == 0, ) .count() ) third_rows = ( db.query(BizQcWorkOrderThirdItem) .filter( BizQcWorkOrderThirdItem.work_order_id == wo.id, BizQcWorkOrderThirdItem.tenant_no == tenant_no, BizQcWorkOrderThirdItem.del_flag == 0, ) .all() ) wo.has_lab = "1" if lab_count else "0" wo.has_third_party = "1" if third_rows else "0" if third_rows: fee_total = sum((r.fee_amount or Decimal("0")) for r in third_rows) wo.third_party_fee_total = fee_total else: wo.third_party_fee_total = None wo.update_by = user_id wo.update_time = now db.add(wo) def delete_qc_task(db: Session, task_id: str, tenant_no: str, user_id: Optional[str]) -> None: """软删除质检任务、检测结果,并同步解除批次中的项目关联。""" task = db.query(BizQcTask).filter( and_( BizQcTask.id == task_id, BizQcTask.tenant_no == tenant_no, BizQcTask.del_flag == 0, ) ).first() if not task: raise ValueError("任务不存在") work_order_id = task.work_order_id now = datetime.now() # 软删除任务结果 from app.models.qc_param import BizQcTaskResult results = db.query(BizQcTaskResult).filter( and_(BizQcTaskResult.task_id == task_id, BizQcTaskResult.del_flag == 0) ).all() for r in results: r.del_flag = 1 r.update_by = user_id r.update_time = now # 同步软删批次明细行(配置弹窗勾选来源) _soft_delete_source_line_for_task( db, task, tenant_no=tenant_no, user_id=user_id, now=now ) # 软删除任务负责人 assignee_svc.soft_delete_assignees_by_task_ids(db, [task_id], tenant_no) # 软删除任务本体 task.del_flag = 1 task.update_by = user_id task.update_time = now db.flush() from app.crud.qc_work_order import get_work_order, try_complete_work_order wo = get_work_order(db, work_order_id, tenant_no=tenant_no) if wo: _refresh_work_order_item_flags( db, wo, tenant_no=tenant_no, user_id=user_id, now=now ) if (wo.work_order_status or "").strip() == "ywc": pending = ( db.query(BizQcTask) .filter( BizQcTask.work_order_id == work_order_id, BizQcTask.tenant_no == tenant_no, BizQcTask.del_flag == 0, BizQcTask.task_status != "ywc", ) .count() ) if pending: wo.work_order_status = "ypz" wo.update_by = user_id wo.update_time = now db.add(wo) db.commit() try_complete_work_order(db, work_order_id, tenant_no)