| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932 |
- """质检工单检测批次 CRUD"""
- from __future__ import annotations
- from datetime import datetime
- from decimal import Decimal
- from typing import Any, Dict, List, Optional, Set, Tuple
- from sqlalchemy.orm import Session
- from app.core.utils import generate_uuid
- from app.crud import qc_lab_test_item as lab_item_svc
- from app.crud import qc_task as task_svc
- from app.crud import qc_task_assignee as assignee_svc
- from app.crud import qc_third_party_item as third_item_svc
- from app.crud.qc_third_party_company import get_company
- from app.models.models import (
- BizQcBatchAssignee,
- BizQcTask,
- BizQcWorkOrder,
- BizQcWorkOrderInspectionBatch,
- BizQcWorkOrderLabItem,
- BizQcWorkOrderThirdItem,
- SysUser,
- )
- _CONFIGURABLE_WORK_ORDER_STATUSES = frozenset({"dpz", "ypz", "ywc", "yzf"})
- def _is_voided_work_order(wo: BizQcWorkOrder) -> bool:
- return (wo.work_order_status or "").strip() == "yzf"
- def _dec(v) -> float:
- if v is None:
- return 0.0
- return float(v)
- def _unique_ids(ids: List[str]) -> List[str]:
- seen: Set[str] = set()
- out: List[str] = []
- for x in ids:
- v = (x or "").strip()
- if not v or v in seen:
- continue
- seen.add(v)
- out.append(v)
- return out
- def _validate_batch_payload(batch: Dict[str, Any], index: int) -> None:
- label = f"批次 {index + 1}"
- batch_name = (batch.get("batchName") or batch.get("batch_name") or "").strip()
- lab_ids = _unique_ids(batch.get("labItemIds") or batch.get("lab_item_ids") or [])
- third_ids = _unique_ids(batch.get("thirdPartyItemIds") or batch.get("third_party_item_ids") or [])
- assignees = batch.get("assigneeUserIds") or batch.get("assignee_user_ids") or []
- if not batch_name:
- raise ValueError(f"{label}:请填写批次名称")
- if len(lab_ids) != len(set(lab_ids)):
- raise ValueError(f"{label}:实验室项目不能重复")
- if len(third_ids) != len(set(third_ids)):
- raise ValueError(f"{label}:第三方项目不能重复")
- if not assignees:
- raise ValueError(f"{label}:请指定负责人")
- def _order_source_for_wo(wo: BizQcWorkOrder) -> str:
- wo_type = getattr(wo, "work_order_type", None) or "inbound"
- if wo_type == "temporary":
- return "temporary"
- if wo_type == "outbound":
- return "outbound"
- return "inbound"
- def _next_batch_no(db: Session, wo: BizQcWorkOrder, tenant_no: str) -> str:
- prefix = f"{wo.work_order_no}-B"
- rows = (
- db.query(BizQcWorkOrderInspectionBatch.batch_no)
- .filter(
- BizQcWorkOrderInspectionBatch.work_order_id == wo.id,
- BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
- BizQcWorkOrderInspectionBatch.del_flag == 0,
- )
- .all()
- )
- max_seq = 0
- for (bn,) in rows:
- s = (bn or "")[len(prefix) :]
- if s.isdigit():
- max_seq = max(max_seq, int(s))
- return f"{prefix}{max_seq + 1:02d}"
- def _allocate_batch_name(db: Session, wo_id: str, tenant_no: str, day: datetime) -> str:
- base = day.strftime("%Y%m%d") + "检测"
- rows = (
- db.query(BizQcWorkOrderInspectionBatch.batch_name)
- .filter(
- BizQcWorkOrderInspectionBatch.work_order_id == wo_id,
- BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
- BizQcWorkOrderInspectionBatch.del_flag == 0,
- )
- .all()
- )
- same_day = [n for (n,) in rows if (n or "").startswith(base)]
- if not same_day:
- return base
- if base not in same_day:
- return base
- suffix = 2
- while f"{base}-{suffix}" in same_day:
- suffix += 1
- return f"{base}-{suffix}"
- def sync_batch_assignees(
- db: Session,
- batch_id: str,
- user_ids: List[str],
- *,
- tenant_no: str,
- operator_id: Optional[str],
- create_dept_id: str = "",
- ) -> List[str]:
- normalized = assignee_svc.validate_assignee_user_ids(db, user_ids, tenant_no)
- now = datetime.now()
- existing = (
- db.query(BizQcBatchAssignee)
- .filter(
- BizQcBatchAssignee.batch_id == batch_id,
- BizQcBatchAssignee.tenant_no == tenant_no,
- BizQcBatchAssignee.del_flag == 0,
- )
- .all()
- )
- keep = set(normalized)
- for row in existing:
- if row.user_id not in keep:
- row.del_flag = 1
- row.update_by = operator_id
- row.update_time = now
- for idx, uid in enumerate(normalized):
- hit = next((r for r in existing if r.user_id == uid and r.del_flag == 0), None)
- if hit:
- continue
- db.add(
- BizQcBatchAssignee(
- id=generate_uuid(),
- batch_id=batch_id,
- user_id=uid,
- tenant_no=tenant_no,
- order_num=idx,
- create_by=operator_id,
- create_dept_id=create_dept_id or "",
- create_time=now,
- update_by=operator_id,
- update_time=now,
- del_flag=0,
- )
- )
- db.flush()
- return normalized
- def _batch_assignee_user_ids(db: Session, batch_id: str, tenant_no: str) -> List[str]:
- rows = (
- db.query(BizQcBatchAssignee.user_id)
- .filter(
- BizQcBatchAssignee.batch_id == batch_id,
- BizQcBatchAssignee.tenant_no == tenant_no,
- BizQcBatchAssignee.del_flag == 0,
- )
- .order_by(BizQcBatchAssignee.order_num.asc())
- .all()
- )
- return [r[0] for r in rows]
- def _assignee_names(db: Session, user_ids: List[str]) -> str:
- if not user_ids:
- return ""
- rows = (
- db.query(SysUser.id, SysUser.nick_name, SysUser.user_no)
- .filter(SysUser.id.in_(user_ids), SysUser.del_flag == 0)
- .all()
- )
- name_map = {r.id: (r.nick_name or r.user_no or r.id) for r in rows}
- return "、".join(name_map.get(uid, uid) for uid in user_ids)
- def _apply_assignees_to_batch_tasks(
- db: Session,
- batch_id: str,
- user_ids: List[str],
- *,
- tenant_no: str,
- operator_id: Optional[str],
- create_dept_id: str,
- ) -> None:
- tasks = (
- db.query(BizQcTask)
- .filter(
- BizQcTask.batch_id == batch_id,
- BizQcTask.tenant_no == tenant_no,
- BizQcTask.del_flag == 0,
- )
- .all()
- )
- for task in tasks:
- assignee_svc.sync_assignees_for_task(
- db,
- task.id,
- user_ids,
- tenant_no=tenant_no,
- operator_id=operator_id,
- create_dept_id=create_dept_id,
- )
- def _batch_has_blocking_tasks(db: Session, batch_id: str, tenant_no: str) -> bool:
- return (
- db.query(BizQcTask)
- .filter(
- BizQcTask.batch_id == batch_id,
- BizQcTask.tenant_no == tenant_no,
- BizQcTask.del_flag == 0,
- BizQcTask.task_status.in_(["jxz", "ywc"]),
- )
- .first()
- is not None
- )
- def _soft_delete_batch(
- db: Session,
- batch: BizQcWorkOrderInspectionBatch,
- *,
- tenant_no: str,
- user_id: Optional[str],
- force: bool = False,
- ) -> None:
- from app.crud.qc_work_order import _soft_delete_tasks_by_source_line_ids
- if not force and _batch_has_blocking_tasks(db, batch.id, tenant_no):
- raise ValueError(f"批次「{batch.batch_name}」存在进行中或已完成任务,无法删除")
- now = datetime.now()
- lab_lines = (
- db.query(BizQcWorkOrderLabItem)
- .filter(
- BizQcWorkOrderLabItem.batch_id == batch.id,
- BizQcWorkOrderLabItem.del_flag == 0,
- )
- .all()
- )
- third_lines = (
- db.query(BizQcWorkOrderThirdItem)
- .filter(
- BizQcWorkOrderThirdItem.batch_id == batch.id,
- BizQcWorkOrderThirdItem.del_flag == 0,
- )
- .all()
- )
- line_ids = [r.id for r in lab_lines + third_lines]
- for line in lab_lines + third_lines:
- line.del_flag = 1
- line.update_by = user_id
- line.update_time = now
- _soft_delete_tasks_by_source_line_ids(db, line_ids, tenant_no)
- assignees = (
- db.query(BizQcBatchAssignee)
- .filter(BizQcBatchAssignee.batch_id == batch.id, BizQcBatchAssignee.del_flag == 0)
- .all()
- )
- for a in assignees:
- a.del_flag = 1
- a.update_by = user_id
- a.update_time = now
- batch.del_flag = 1
- batch.update_by = user_id
- batch.update_time = now
- def _sync_batch_lab_items(
- db: Session,
- wo: BizQcWorkOrder,
- batch: BizQcWorkOrderInspectionBatch,
- *,
- order_source: str,
- lab_item_ids: List[str],
- assignee_user_ids: List[str],
- tenant_no: str,
- user_id: Optional[str],
- create_dept_id: str,
- seq_start: int,
- ) -> int:
- from app.crud.qc_work_order import _get_task_by_source_line_id, _soft_delete_tasks_by_source_line_ids
- now = datetime.now()
- existing_rows = (
- db.query(BizQcWorkOrderLabItem)
- .filter(
- BizQcWorkOrderLabItem.batch_id == batch.id,
- BizQcWorkOrderLabItem.del_flag == 0,
- )
- .all()
- )
- existing_by_item_id = {r.lab_test_item_id: r for r in existing_rows}
- new_id_set = set(lab_item_ids)
- old_id_set = set(existing_by_item_id.keys())
- allow_force = _is_voided_work_order(wo)
- for item_id in old_id_set - new_id_set:
- line = existing_by_item_id[item_id]
- task = _get_task_by_source_line_id(db, line.id, tenant_no)
- if (
- not allow_force
- and task
- and (task.task_status or "") in ("jxz", "ywc")
- ):
- raise ValueError(f"项目「{line.item_name}」任务进行中或已完成,无法移除")
- line.del_flag = 1
- line.update_by = user_id
- line.update_time = now
- db.add(line)
- remove_line_ids = [existing_by_item_id[i].id for i in old_id_set - new_id_set]
- _soft_delete_tasks_by_source_line_ids(db, remove_line_ids, tenant_no)
- seq = seq_start
- for lid in lab_item_ids:
- seq += 1
- if lid in existing_by_item_id and lid in new_id_set:
- line = existing_by_item_id[lid]
- line.order_num = seq
- line.update_by = user_id
- line.update_time = now
- db.add(line)
- task = _get_task_by_source_line_id(db, line.id, tenant_no)
- if task:
- task.order_num = seq
- task.batch_id = batch.id
- task.update_by = user_id
- task.update_time = now
- db.add(task)
- else:
- task = task_svc.create_task_for_line(
- db,
- work_order=wo,
- task_type="sysz",
- source_line_id=line.id,
- item_code=line.item_code,
- item_name=line.item_name,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- seq=seq,
- order_source=order_source,
- batch_id=batch.id,
- )
- assignee_svc.sync_assignees_for_task(
- db,
- task.id,
- assignee_user_ids,
- tenant_no=tenant_no,
- operator_id=user_id,
- create_dept_id=create_dept_id,
- )
- continue
- item = lab_item_svc.get_lab_item(db, lid, tenant_no=tenant_no)
- if not item or item.status != "1":
- raise ValueError("实验室项目无效或已停用")
- line = BizQcWorkOrderLabItem(
- id=generate_uuid(),
- work_order_id=wo.id,
- order_source=order_source,
- batch_id=batch.id,
- lab_test_item_id=item.id,
- item_code=item.item_code,
- item_name=item.item_name,
- tenant_no=tenant_no,
- order_num=seq,
- 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(line)
- db.flush()
- task = task_svc.create_task_for_line(
- db,
- work_order=wo,
- task_type="sysz",
- source_line_id=line.id,
- item_code=line.item_code,
- item_name=line.item_name,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- seq=seq,
- order_source=order_source,
- batch_id=batch.id,
- )
- assignee_svc.sync_assignees_for_task(
- db,
- task.id,
- assignee_user_ids,
- tenant_no=tenant_no,
- operator_id=user_id,
- create_dept_id=create_dept_id,
- )
- return seq
- def _sync_batch_third_items(
- db: Session,
- wo: BizQcWorkOrder,
- batch: BizQcWorkOrderInspectionBatch,
- *,
- order_source: str,
- third_party_item_ids: List[str],
- assignee_user_ids: List[str],
- tenant_no: str,
- user_id: Optional[str],
- create_dept_id: str,
- seq_start: int,
- ) -> Tuple[int, Decimal]:
- from app.crud.qc_work_order import _get_task_by_source_line_id, _soft_delete_tasks_by_source_line_ids
- now = datetime.now()
- existing_rows = (
- db.query(BizQcWorkOrderThirdItem)
- .filter(
- BizQcWorkOrderThirdItem.batch_id == batch.id,
- BizQcWorkOrderThirdItem.del_flag == 0,
- )
- .all()
- )
- existing_by_item_id = {r.third_party_item_id: r for r in existing_rows}
- new_id_set = set(third_party_item_ids)
- old_id_set = set(existing_by_item_id.keys())
- allow_force = _is_voided_work_order(wo)
- for item_id in old_id_set - new_id_set:
- line = existing_by_item_id[item_id]
- task = _get_task_by_source_line_id(db, line.id, tenant_no)
- if (
- not allow_force
- and task
- and (task.task_status or "") in ("jxz", "ywc")
- ):
- raise ValueError(f"项目「{line.item_name}」任务进行中或已完成,无法移除")
- line.del_flag = 1
- line.update_by = user_id
- line.update_time = now
- db.add(line)
- remove_line_ids = [existing_by_item_id[i].id for i in old_id_set - new_id_set]
- _soft_delete_tasks_by_source_line_ids(db, remove_line_ids, tenant_no)
- fee_total = Decimal("0")
- seq = seq_start
- for tid in third_party_item_ids:
- seq += 1
- if tid in existing_by_item_id and tid in new_id_set:
- line = existing_by_item_id[tid]
- line.order_num = seq
- line.update_by = user_id
- line.update_time = now
- db.add(line)
- fee_total += line.fee_amount or Decimal("0")
- task = _get_task_by_source_line_id(db, line.id, tenant_no)
- if task:
- task.order_num = seq
- task.batch_id = batch.id
- task.update_by = user_id
- task.update_time = now
- db.add(task)
- else:
- task = task_svc.create_task_for_line(
- db,
- work_order=wo,
- task_type="dsfjc",
- source_line_id=line.id,
- item_code=line.item_code,
- item_name=line.item_name,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- seq=seq,
- company_id=line.company_id,
- company_name=line.company_name,
- fee_amount=line.fee_amount,
- order_source=order_source,
- batch_id=batch.id,
- )
- assignee_svc.sync_assignees_for_task(
- db,
- task.id,
- assignee_user_ids,
- tenant_no=tenant_no,
- operator_id=user_id,
- create_dept_id=create_dept_id,
- )
- continue
- item = third_item_svc.get_item(db, tid, tenant_no=tenant_no)
- if not item or item.status != "1":
- raise ValueError("第三方项目无效或已停用")
- comp = get_company(db, item.company_id, tenant_no=tenant_no)
- if not comp:
- raise ValueError("第三方公司不存在")
- fee = item.fee_amount or Decimal("0")
- fee_total += fee
- line = BizQcWorkOrderThirdItem(
- id=generate_uuid(),
- work_order_id=wo.id,
- order_source=order_source,
- batch_id=batch.id,
- third_party_item_id=item.id,
- company_id=item.company_id,
- company_name=comp.company_name,
- item_code=item.item_code,
- item_name=item.item_name,
- fee_amount=fee,
- tenant_no=tenant_no,
- order_num=seq,
- 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(line)
- db.flush()
- task = task_svc.create_task_for_line(
- db,
- work_order=wo,
- task_type="dsfjc",
- source_line_id=line.id,
- item_code=line.item_code,
- item_name=line.item_name,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- seq=seq,
- company_id=line.company_id,
- company_name=line.company_name,
- fee_amount=line.fee_amount,
- order_source=order_source,
- batch_id=batch.id,
- )
- assignee_svc.sync_assignees_for_task(
- db,
- task.id,
- assignee_user_ids,
- tenant_no=tenant_no,
- operator_id=user_id,
- create_dept_id=create_dept_id,
- )
- return seq, fee_total
- def _batch_item_ids(db: Session, batch_id: str) -> Tuple[List[str], List[str]]:
- lab_ids = [
- r.lab_test_item_id
- for r in db.query(BizQcWorkOrderLabItem)
- .filter(BizQcWorkOrderLabItem.batch_id == batch_id, BizQcWorkOrderLabItem.del_flag == 0)
- .order_by(BizQcWorkOrderLabItem.order_num.asc())
- .all()
- ]
- third_ids = [
- r.third_party_item_id
- for r in db.query(BizQcWorkOrderThirdItem)
- .filter(
- BizQcWorkOrderThirdItem.batch_id == batch_id,
- BizQcWorkOrderThirdItem.del_flag == 0,
- )
- .order_by(BizQcWorkOrderThirdItem.order_num.asc())
- .all()
- ]
- return lab_ids, third_ids
- def _batch_to_dict(
- db: Session,
- batch: BizQcWorkOrderInspectionBatch,
- *,
- tenant_no: str,
- tasks: Optional[List[Dict[str, Any]]] = None,
- ) -> Dict[str, Any]:
- assignee_ids = _batch_assignee_user_ids(db, batch.id, tenant_no)
- lab_ids, third_ids = _batch_item_ids(db, batch.id)
- task_list = tasks or []
- completed = sum(1 for t in task_list if (t.get("taskStatus") or "") == "ywc")
- return {
- "id": batch.id,
- "workOrderId": batch.work_order_id,
- "batchNo": batch.batch_no,
- "batchName": batch.batch_name,
- "orderNum": batch.order_num or 0,
- "assigneeUserIds": assignee_ids,
- "assigneeUserNames": _assignee_names(db, assignee_ids),
- "hasLab": bool(lab_ids),
- "hasThirdParty": bool(third_ids),
- "labItemIds": lab_ids,
- "thirdPartyItemIds": third_ids,
- "taskCount": len(task_list),
- "completedTaskCount": completed,
- "tasks": task_list,
- }
- def _build_inspection_batch_list(
- db: Session,
- work_order_id: str,
- batches: List[BizQcWorkOrderInspectionBatch],
- all_tasks: List[Dict[str, Any]],
- tenant_no: str,
- ) -> List[Dict[str, Any]]:
- tasks_by_batch: Dict[str, List[Dict[str, Any]]] = {}
- orphan: List[Dict[str, Any]] = []
- batch_ids = {b.id for b in batches}
- for t in all_tasks:
- bid = t.get("batchId") or ""
- if bid and bid in batch_ids:
- tasks_by_batch.setdefault(bid, []).append(t)
- else:
- orphan.append(t)
- out = [
- _batch_to_dict(db, b, tenant_no=tenant_no, tasks=tasks_by_batch.get(b.id, []))
- for b in batches
- ]
- if orphan and out:
- out[0]["tasks"] = orphan + out[0]["tasks"]
- out[0]["taskCount"] = len(out[0]["tasks"])
- out[0]["completedTaskCount"] = sum(
- 1 for t in out[0]["tasks"] if (t.get("taskStatus") or "") == "ywc"
- )
- elif orphan:
- out.append(
- {
- "id": "",
- "workOrderId": work_order_id,
- "batchNo": "—",
- "batchName": "历史任务",
- "orderNum": 0,
- "assigneeUserIds": [],
- "assigneeUserNames": "",
- "taskCount": len(orphan),
- "completedTaskCount": sum(1 for t in orphan if (t.get("taskStatus") or "") == "ywc"),
- "tasks": orphan,
- }
- )
- return out
- def list_inspection_batches(
- db: Session, work_order_id: str, tenant_no: str, *, user_info=None
- ) -> List[Dict[str, Any]]:
- batches = (
- db.query(BizQcWorkOrderInspectionBatch)
- .filter(
- BizQcWorkOrderInspectionBatch.work_order_id == work_order_id,
- BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
- BizQcWorkOrderInspectionBatch.del_flag == 0,
- )
- .order_by(BizQcWorkOrderInspectionBatch.order_num.asc())
- .all()
- )
- all_tasks = task_svc.list_tasks_by_work_order(db, work_order_id, tenant_no)
- return _build_inspection_batch_list(db, work_order_id, batches, all_tasks, tenant_no)
- def list_inspection_batches_by_work_order_ids(
- db: Session,
- work_order_ids: List[str],
- tenant_no: str,
- *,
- user_info=None,
- ) -> Dict[str, List[Dict[str, Any]]]:
- """批量返回多个工单的检测批次(含任务),供列表页一次加载。"""
- ids = _unique_ids(work_order_ids)
- if not ids or not tenant_no:
- return {}
- ids = ids[:100]
- batches = (
- db.query(BizQcWorkOrderInspectionBatch)
- .filter(
- BizQcWorkOrderInspectionBatch.work_order_id.in_(ids),
- BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
- BizQcWorkOrderInspectionBatch.del_flag == 0,
- )
- .order_by(
- BizQcWorkOrderInspectionBatch.work_order_id.asc(),
- BizQcWorkOrderInspectionBatch.order_num.asc(),
- )
- .all()
- )
- tasks_by_wo = task_svc.list_tasks_by_work_order_ids(db, ids, tenant_no)
- batches_by_wo: Dict[str, List[BizQcWorkOrderInspectionBatch]] = {}
- for batch in batches:
- batches_by_wo.setdefault(batch.work_order_id, []).append(batch)
- out: Dict[str, List[Dict[str, Any]]] = {}
- for wo_id in ids:
- out[wo_id] = _build_inspection_batch_list(
- db,
- wo_id,
- batches_by_wo.get(wo_id, []),
- tasks_by_wo.get(wo_id, []),
- tenant_no,
- )
- return out
- def configure_work_order_batches(
- db: Session,
- wo: BizQcWorkOrder,
- batches: List[Dict[str, Any]],
- *,
- tenant_no: str,
- user_id: Optional[str],
- create_dept_id: str = "",
- ) -> BizQcWorkOrder:
- status = (wo.work_order_status or "").strip()
- if status not in _CONFIGURABLE_WORK_ORDER_STATUSES:
- raise ValueError("当前工单状态不可配置检测")
- order_source = _order_source_for_wo(wo)
- now = datetime.now()
- is_voided = status == "yzf"
- is_first = status == "dpz"
- for idx, b in enumerate(batches):
- _validate_batch_payload(b, idx)
- existing_batches = {
- r.id: r
- for r in db.query(BizQcWorkOrderInspectionBatch)
- .filter(
- BizQcWorkOrderInspectionBatch.work_order_id == wo.id,
- BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
- BizQcWorkOrderInspectionBatch.del_flag == 0,
- )
- .all()
- }
- keep_batch_ids = {((b.get("id") or "").strip()) for b in batches if (b.get("id") or "").strip()}
- # 提交空 batches = 显式清空:强制软删批次及关联任务(含进行中/已完成)
- force_clear_all = not batches
- for bid, batch_row in existing_batches.items():
- if bid not in keep_batch_ids:
- _soft_delete_batch(
- db,
- batch_row,
- tenant_no=tenant_no,
- user_id=user_id,
- force=is_voided or force_clear_all,
- )
- fee_total = Decimal("0")
- has_lab = False
- has_third = False
- global_seq = 0
- for batch_idx, payload in enumerate(batches):
- batch_id = (payload.get("id") or "").strip()
- lab_ids = _unique_ids(payload.get("labItemIds") or payload.get("lab_item_ids") or [])
- third_ids = _unique_ids(payload.get("thirdPartyItemIds") or payload.get("third_party_item_ids") or [])
- has_lab_batch = bool(lab_ids) or bool(payload.get("hasLab") or payload.get("has_lab"))
- has_third_batch = bool(third_ids) or bool(payload.get("hasThirdParty") or payload.get("has_third_party"))
- assignee_ids = payload.get("assigneeUserIds") or payload.get("assignee_user_ids") or []
- normalized_assignees = assignee_svc.validate_assignee_user_ids(db, assignee_ids, tenant_no)
- batch_name = (payload.get("batchName") or payload.get("batch_name") or "").strip()
- if batch_id and batch_id in existing_batches:
- batch = existing_batches[batch_id]
- batch.order_num = batch_idx + 1
- if batch_name:
- batch.batch_name = batch_name
- batch.update_by = user_id
- batch.update_time = now
- db.add(batch)
- else:
- batch = BizQcWorkOrderInspectionBatch(
- id=generate_uuid(),
- work_order_id=wo.id,
- order_source=order_source,
- batch_no=_next_batch_no(db, wo, tenant_no),
- batch_name=batch_name or _allocate_batch_name(db, wo.id, tenant_no, now),
- tenant_no=tenant_no,
- order_num=batch_idx + 1,
- 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(batch)
- db.flush()
- sync_batch_assignees(
- db,
- batch.id,
- normalized_assignees,
- tenant_no=tenant_no,
- operator_id=user_id,
- create_dept_id=create_dept_id,
- )
- if has_lab_batch:
- has_lab = True
- global_seq = _sync_batch_lab_items(
- db,
- wo,
- batch,
- order_source=order_source,
- lab_item_ids=lab_ids,
- assignee_user_ids=normalized_assignees,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- seq_start=global_seq,
- )
- else:
- global_seq = _sync_batch_lab_items(
- db,
- wo,
- batch,
- order_source=order_source,
- lab_item_ids=[],
- assignee_user_ids=normalized_assignees,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- seq_start=global_seq,
- )
- if has_third_batch:
- has_third = True
- global_seq, batch_fee = _sync_batch_third_items(
- db,
- wo,
- batch,
- order_source=order_source,
- third_party_item_ids=third_ids,
- assignee_user_ids=normalized_assignees,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- seq_start=global_seq,
- )
- fee_total += batch_fee
- else:
- global_seq, _ = _sync_batch_third_items(
- db,
- wo,
- batch,
- order_source=order_source,
- third_party_item_ids=[],
- assignee_user_ids=normalized_assignees,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- seq_start=global_seq,
- )
- wo.has_lab = "1" if has_lab else "0"
- wo.has_third_party = "1" if has_third else "0"
- if not is_voided:
- if not batches:
- # 空批次列表:清空全部检测批次/任务后回到待配置
- wo.work_order_status = "dpz"
- else:
- wo.work_order_status = "ypz"
- if is_first:
- wo.configured_time = now
- wo.configured_by = user_id
- wo.third_party_fee_total = fee_total if has_third else None
- wo.update_by = user_id
- wo.update_time = now
- db.add(wo)
- if not is_voided:
- from app.crud.pilot_outbound_status_sync import sync_schedule_from_qc_work_order
- sync_schedule_from_qc_work_order(db, wo, user_id=user_id)
- db.commit()
- if not is_voided and batches:
- from app.crud.qc_work_order import try_complete_work_order
- try_complete_work_order(db, wo.id, tenant_no)
- db.refresh(wo)
- return wo
- def normalize_legacy_configure_to_batches(body: Dict[str, Any]) -> List[Dict[str, Any]]:
- """将扁平 labItemIds 配置转为单批次结构。
- 注意:`batches: []` 表示显式清空,不得当作「未传 batches」走扁平兼容。
- """
- if "batches" in body:
- batches = body.get("batches")
- return list(batches) if batches is not None else []
- has_lab = bool(body.get("hasLab") or body.get("has_lab"))
- has_third = bool(body.get("hasThirdParty") or body.get("has_third_party"))
- lab_ids = body.get("labItemIds") or body.get("lab_item_ids") or []
- third_ids = body.get("thirdPartyItemIds") or body.get("third_party_item_ids") or []
- lab_assignees = body.get("labItemAssignees") or body.get("lab_item_assignees") or {}
- third_assignees = body.get("thirdPartyItemAssignees") or body.get("third_party_item_assignees") or {}
- assignee_set: Set[str] = set()
- for lid in lab_ids:
- for uid in lab_assignees.get(lid) or []:
- if uid:
- assignee_set.add(uid)
- for tid in third_ids:
- for uid in third_assignees.get(tid) or []:
- if uid:
- assignee_set.add(uid)
- return [
- {
- "assigneeUserIds": list(assignee_set),
- "hasLab": has_lab,
- "hasThirdParty": has_third,
- "labItemIds": lab_ids,
- "thirdPartyItemIds": third_ids,
- }
- ]
|