933 lines
30 KiB
Python
933 lines
30 KiB
Python
"""质检工单检测批次 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,
|
|
}
|
|
]
|