qc_inspection_batch.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932
  1. """质检工单检测批次 CRUD"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from decimal import Decimal
  5. from typing import Any, Dict, List, Optional, Set, Tuple
  6. from sqlalchemy.orm import Session
  7. from app.core.utils import generate_uuid
  8. from app.crud import qc_lab_test_item as lab_item_svc
  9. from app.crud import qc_task as task_svc
  10. from app.crud import qc_task_assignee as assignee_svc
  11. from app.crud import qc_third_party_item as third_item_svc
  12. from app.crud.qc_third_party_company import get_company
  13. from app.models.models import (
  14. BizQcBatchAssignee,
  15. BizQcTask,
  16. BizQcWorkOrder,
  17. BizQcWorkOrderInspectionBatch,
  18. BizQcWorkOrderLabItem,
  19. BizQcWorkOrderThirdItem,
  20. SysUser,
  21. )
  22. _CONFIGURABLE_WORK_ORDER_STATUSES = frozenset({"dpz", "ypz", "ywc", "yzf"})
  23. def _is_voided_work_order(wo: BizQcWorkOrder) -> bool:
  24. return (wo.work_order_status or "").strip() == "yzf"
  25. def _dec(v) -> float:
  26. if v is None:
  27. return 0.0
  28. return float(v)
  29. def _unique_ids(ids: List[str]) -> List[str]:
  30. seen: Set[str] = set()
  31. out: List[str] = []
  32. for x in ids:
  33. v = (x or "").strip()
  34. if not v or v in seen:
  35. continue
  36. seen.add(v)
  37. out.append(v)
  38. return out
  39. def _validate_batch_payload(batch: Dict[str, Any], index: int) -> None:
  40. label = f"批次 {index + 1}"
  41. batch_name = (batch.get("batchName") or batch.get("batch_name") or "").strip()
  42. lab_ids = _unique_ids(batch.get("labItemIds") or batch.get("lab_item_ids") or [])
  43. third_ids = _unique_ids(batch.get("thirdPartyItemIds") or batch.get("third_party_item_ids") or [])
  44. assignees = batch.get("assigneeUserIds") or batch.get("assignee_user_ids") or []
  45. if not batch_name:
  46. raise ValueError(f"{label}:请填写批次名称")
  47. if len(lab_ids) != len(set(lab_ids)):
  48. raise ValueError(f"{label}:实验室项目不能重复")
  49. if len(third_ids) != len(set(third_ids)):
  50. raise ValueError(f"{label}:第三方项目不能重复")
  51. if not assignees:
  52. raise ValueError(f"{label}:请指定负责人")
  53. def _order_source_for_wo(wo: BizQcWorkOrder) -> str:
  54. wo_type = getattr(wo, "work_order_type", None) or "inbound"
  55. if wo_type == "temporary":
  56. return "temporary"
  57. if wo_type == "outbound":
  58. return "outbound"
  59. return "inbound"
  60. def _next_batch_no(db: Session, wo: BizQcWorkOrder, tenant_no: str) -> str:
  61. prefix = f"{wo.work_order_no}-B"
  62. rows = (
  63. db.query(BizQcWorkOrderInspectionBatch.batch_no)
  64. .filter(
  65. BizQcWorkOrderInspectionBatch.work_order_id == wo.id,
  66. BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
  67. BizQcWorkOrderInspectionBatch.del_flag == 0,
  68. )
  69. .all()
  70. )
  71. max_seq = 0
  72. for (bn,) in rows:
  73. s = (bn or "")[len(prefix) :]
  74. if s.isdigit():
  75. max_seq = max(max_seq, int(s))
  76. return f"{prefix}{max_seq + 1:02d}"
  77. def _allocate_batch_name(db: Session, wo_id: str, tenant_no: str, day: datetime) -> str:
  78. base = day.strftime("%Y%m%d") + "检测"
  79. rows = (
  80. db.query(BizQcWorkOrderInspectionBatch.batch_name)
  81. .filter(
  82. BizQcWorkOrderInspectionBatch.work_order_id == wo_id,
  83. BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
  84. BizQcWorkOrderInspectionBatch.del_flag == 0,
  85. )
  86. .all()
  87. )
  88. same_day = [n for (n,) in rows if (n or "").startswith(base)]
  89. if not same_day:
  90. return base
  91. if base not in same_day:
  92. return base
  93. suffix = 2
  94. while f"{base}-{suffix}" in same_day:
  95. suffix += 1
  96. return f"{base}-{suffix}"
  97. def sync_batch_assignees(
  98. db: Session,
  99. batch_id: str,
  100. user_ids: List[str],
  101. *,
  102. tenant_no: str,
  103. operator_id: Optional[str],
  104. create_dept_id: str = "",
  105. ) -> List[str]:
  106. normalized = assignee_svc.validate_assignee_user_ids(db, user_ids, tenant_no)
  107. now = datetime.now()
  108. existing = (
  109. db.query(BizQcBatchAssignee)
  110. .filter(
  111. BizQcBatchAssignee.batch_id == batch_id,
  112. BizQcBatchAssignee.tenant_no == tenant_no,
  113. BizQcBatchAssignee.del_flag == 0,
  114. )
  115. .all()
  116. )
  117. keep = set(normalized)
  118. for row in existing:
  119. if row.user_id not in keep:
  120. row.del_flag = 1
  121. row.update_by = operator_id
  122. row.update_time = now
  123. for idx, uid in enumerate(normalized):
  124. hit = next((r for r in existing if r.user_id == uid and r.del_flag == 0), None)
  125. if hit:
  126. continue
  127. db.add(
  128. BizQcBatchAssignee(
  129. id=generate_uuid(),
  130. batch_id=batch_id,
  131. user_id=uid,
  132. tenant_no=tenant_no,
  133. order_num=idx,
  134. create_by=operator_id,
  135. create_dept_id=create_dept_id or "",
  136. create_time=now,
  137. update_by=operator_id,
  138. update_time=now,
  139. del_flag=0,
  140. )
  141. )
  142. db.flush()
  143. return normalized
  144. def _batch_assignee_user_ids(db: Session, batch_id: str, tenant_no: str) -> List[str]:
  145. rows = (
  146. db.query(BizQcBatchAssignee.user_id)
  147. .filter(
  148. BizQcBatchAssignee.batch_id == batch_id,
  149. BizQcBatchAssignee.tenant_no == tenant_no,
  150. BizQcBatchAssignee.del_flag == 0,
  151. )
  152. .order_by(BizQcBatchAssignee.order_num.asc())
  153. .all()
  154. )
  155. return [r[0] for r in rows]
  156. def _assignee_names(db: Session, user_ids: List[str]) -> str:
  157. if not user_ids:
  158. return ""
  159. rows = (
  160. db.query(SysUser.id, SysUser.nick_name, SysUser.user_no)
  161. .filter(SysUser.id.in_(user_ids), SysUser.del_flag == 0)
  162. .all()
  163. )
  164. name_map = {r.id: (r.nick_name or r.user_no or r.id) for r in rows}
  165. return "、".join(name_map.get(uid, uid) for uid in user_ids)
  166. def _apply_assignees_to_batch_tasks(
  167. db: Session,
  168. batch_id: str,
  169. user_ids: List[str],
  170. *,
  171. tenant_no: str,
  172. operator_id: Optional[str],
  173. create_dept_id: str,
  174. ) -> None:
  175. tasks = (
  176. db.query(BizQcTask)
  177. .filter(
  178. BizQcTask.batch_id == batch_id,
  179. BizQcTask.tenant_no == tenant_no,
  180. BizQcTask.del_flag == 0,
  181. )
  182. .all()
  183. )
  184. for task in tasks:
  185. assignee_svc.sync_assignees_for_task(
  186. db,
  187. task.id,
  188. user_ids,
  189. tenant_no=tenant_no,
  190. operator_id=operator_id,
  191. create_dept_id=create_dept_id,
  192. )
  193. def _batch_has_blocking_tasks(db: Session, batch_id: str, tenant_no: str) -> bool:
  194. return (
  195. db.query(BizQcTask)
  196. .filter(
  197. BizQcTask.batch_id == batch_id,
  198. BizQcTask.tenant_no == tenant_no,
  199. BizQcTask.del_flag == 0,
  200. BizQcTask.task_status.in_(["jxz", "ywc"]),
  201. )
  202. .first()
  203. is not None
  204. )
  205. def _soft_delete_batch(
  206. db: Session,
  207. batch: BizQcWorkOrderInspectionBatch,
  208. *,
  209. tenant_no: str,
  210. user_id: Optional[str],
  211. force: bool = False,
  212. ) -> None:
  213. from app.crud.qc_work_order import _soft_delete_tasks_by_source_line_ids
  214. if not force and _batch_has_blocking_tasks(db, batch.id, tenant_no):
  215. raise ValueError(f"批次「{batch.batch_name}」存在进行中或已完成任务,无法删除")
  216. now = datetime.now()
  217. lab_lines = (
  218. db.query(BizQcWorkOrderLabItem)
  219. .filter(
  220. BizQcWorkOrderLabItem.batch_id == batch.id,
  221. BizQcWorkOrderLabItem.del_flag == 0,
  222. )
  223. .all()
  224. )
  225. third_lines = (
  226. db.query(BizQcWorkOrderThirdItem)
  227. .filter(
  228. BizQcWorkOrderThirdItem.batch_id == batch.id,
  229. BizQcWorkOrderThirdItem.del_flag == 0,
  230. )
  231. .all()
  232. )
  233. line_ids = [r.id for r in lab_lines + third_lines]
  234. for line in lab_lines + third_lines:
  235. line.del_flag = 1
  236. line.update_by = user_id
  237. line.update_time = now
  238. _soft_delete_tasks_by_source_line_ids(db, line_ids, tenant_no)
  239. assignees = (
  240. db.query(BizQcBatchAssignee)
  241. .filter(BizQcBatchAssignee.batch_id == batch.id, BizQcBatchAssignee.del_flag == 0)
  242. .all()
  243. )
  244. for a in assignees:
  245. a.del_flag = 1
  246. a.update_by = user_id
  247. a.update_time = now
  248. batch.del_flag = 1
  249. batch.update_by = user_id
  250. batch.update_time = now
  251. def _sync_batch_lab_items(
  252. db: Session,
  253. wo: BizQcWorkOrder,
  254. batch: BizQcWorkOrderInspectionBatch,
  255. *,
  256. order_source: str,
  257. lab_item_ids: List[str],
  258. assignee_user_ids: List[str],
  259. tenant_no: str,
  260. user_id: Optional[str],
  261. create_dept_id: str,
  262. seq_start: int,
  263. ) -> int:
  264. from app.crud.qc_work_order import _get_task_by_source_line_id, _soft_delete_tasks_by_source_line_ids
  265. now = datetime.now()
  266. existing_rows = (
  267. db.query(BizQcWorkOrderLabItem)
  268. .filter(
  269. BizQcWorkOrderLabItem.batch_id == batch.id,
  270. BizQcWorkOrderLabItem.del_flag == 0,
  271. )
  272. .all()
  273. )
  274. existing_by_item_id = {r.lab_test_item_id: r for r in existing_rows}
  275. new_id_set = set(lab_item_ids)
  276. old_id_set = set(existing_by_item_id.keys())
  277. allow_force = _is_voided_work_order(wo)
  278. for item_id in old_id_set - new_id_set:
  279. line = existing_by_item_id[item_id]
  280. task = _get_task_by_source_line_id(db, line.id, tenant_no)
  281. if (
  282. not allow_force
  283. and task
  284. and (task.task_status or "") in ("jxz", "ywc")
  285. ):
  286. raise ValueError(f"项目「{line.item_name}」任务进行中或已完成,无法移除")
  287. line.del_flag = 1
  288. line.update_by = user_id
  289. line.update_time = now
  290. db.add(line)
  291. remove_line_ids = [existing_by_item_id[i].id for i in old_id_set - new_id_set]
  292. _soft_delete_tasks_by_source_line_ids(db, remove_line_ids, tenant_no)
  293. seq = seq_start
  294. for lid in lab_item_ids:
  295. seq += 1
  296. if lid in existing_by_item_id and lid in new_id_set:
  297. line = existing_by_item_id[lid]
  298. line.order_num = seq
  299. line.update_by = user_id
  300. line.update_time = now
  301. db.add(line)
  302. task = _get_task_by_source_line_id(db, line.id, tenant_no)
  303. if task:
  304. task.order_num = seq
  305. task.batch_id = batch.id
  306. task.update_by = user_id
  307. task.update_time = now
  308. db.add(task)
  309. else:
  310. task = task_svc.create_task_for_line(
  311. db,
  312. work_order=wo,
  313. task_type="sysz",
  314. source_line_id=line.id,
  315. item_code=line.item_code,
  316. item_name=line.item_name,
  317. tenant_no=tenant_no,
  318. user_id=user_id,
  319. create_dept_id=create_dept_id,
  320. seq=seq,
  321. order_source=order_source,
  322. batch_id=batch.id,
  323. )
  324. assignee_svc.sync_assignees_for_task(
  325. db,
  326. task.id,
  327. assignee_user_ids,
  328. tenant_no=tenant_no,
  329. operator_id=user_id,
  330. create_dept_id=create_dept_id,
  331. )
  332. continue
  333. item = lab_item_svc.get_lab_item(db, lid, tenant_no=tenant_no)
  334. if not item or item.status != "1":
  335. raise ValueError("实验室项目无效或已停用")
  336. line = BizQcWorkOrderLabItem(
  337. id=generate_uuid(),
  338. work_order_id=wo.id,
  339. order_source=order_source,
  340. batch_id=batch.id,
  341. lab_test_item_id=item.id,
  342. item_code=item.item_code,
  343. item_name=item.item_name,
  344. tenant_no=tenant_no,
  345. order_num=seq,
  346. create_by=user_id,
  347. create_dept_id=create_dept_id or "",
  348. create_time=now,
  349. update_by=user_id,
  350. update_time=now,
  351. del_flag=0,
  352. )
  353. db.add(line)
  354. db.flush()
  355. task = task_svc.create_task_for_line(
  356. db,
  357. work_order=wo,
  358. task_type="sysz",
  359. source_line_id=line.id,
  360. item_code=line.item_code,
  361. item_name=line.item_name,
  362. tenant_no=tenant_no,
  363. user_id=user_id,
  364. create_dept_id=create_dept_id,
  365. seq=seq,
  366. order_source=order_source,
  367. batch_id=batch.id,
  368. )
  369. assignee_svc.sync_assignees_for_task(
  370. db,
  371. task.id,
  372. assignee_user_ids,
  373. tenant_no=tenant_no,
  374. operator_id=user_id,
  375. create_dept_id=create_dept_id,
  376. )
  377. return seq
  378. def _sync_batch_third_items(
  379. db: Session,
  380. wo: BizQcWorkOrder,
  381. batch: BizQcWorkOrderInspectionBatch,
  382. *,
  383. order_source: str,
  384. third_party_item_ids: List[str],
  385. assignee_user_ids: List[str],
  386. tenant_no: str,
  387. user_id: Optional[str],
  388. create_dept_id: str,
  389. seq_start: int,
  390. ) -> Tuple[int, Decimal]:
  391. from app.crud.qc_work_order import _get_task_by_source_line_id, _soft_delete_tasks_by_source_line_ids
  392. now = datetime.now()
  393. existing_rows = (
  394. db.query(BizQcWorkOrderThirdItem)
  395. .filter(
  396. BizQcWorkOrderThirdItem.batch_id == batch.id,
  397. BizQcWorkOrderThirdItem.del_flag == 0,
  398. )
  399. .all()
  400. )
  401. existing_by_item_id = {r.third_party_item_id: r for r in existing_rows}
  402. new_id_set = set(third_party_item_ids)
  403. old_id_set = set(existing_by_item_id.keys())
  404. allow_force = _is_voided_work_order(wo)
  405. for item_id in old_id_set - new_id_set:
  406. line = existing_by_item_id[item_id]
  407. task = _get_task_by_source_line_id(db, line.id, tenant_no)
  408. if (
  409. not allow_force
  410. and task
  411. and (task.task_status or "") in ("jxz", "ywc")
  412. ):
  413. raise ValueError(f"项目「{line.item_name}」任务进行中或已完成,无法移除")
  414. line.del_flag = 1
  415. line.update_by = user_id
  416. line.update_time = now
  417. db.add(line)
  418. remove_line_ids = [existing_by_item_id[i].id for i in old_id_set - new_id_set]
  419. _soft_delete_tasks_by_source_line_ids(db, remove_line_ids, tenant_no)
  420. fee_total = Decimal("0")
  421. seq = seq_start
  422. for tid in third_party_item_ids:
  423. seq += 1
  424. if tid in existing_by_item_id and tid in new_id_set:
  425. line = existing_by_item_id[tid]
  426. line.order_num = seq
  427. line.update_by = user_id
  428. line.update_time = now
  429. db.add(line)
  430. fee_total += line.fee_amount or Decimal("0")
  431. task = _get_task_by_source_line_id(db, line.id, tenant_no)
  432. if task:
  433. task.order_num = seq
  434. task.batch_id = batch.id
  435. task.update_by = user_id
  436. task.update_time = now
  437. db.add(task)
  438. else:
  439. task = task_svc.create_task_for_line(
  440. db,
  441. work_order=wo,
  442. task_type="dsfjc",
  443. source_line_id=line.id,
  444. item_code=line.item_code,
  445. item_name=line.item_name,
  446. tenant_no=tenant_no,
  447. user_id=user_id,
  448. create_dept_id=create_dept_id,
  449. seq=seq,
  450. company_id=line.company_id,
  451. company_name=line.company_name,
  452. fee_amount=line.fee_amount,
  453. order_source=order_source,
  454. batch_id=batch.id,
  455. )
  456. assignee_svc.sync_assignees_for_task(
  457. db,
  458. task.id,
  459. assignee_user_ids,
  460. tenant_no=tenant_no,
  461. operator_id=user_id,
  462. create_dept_id=create_dept_id,
  463. )
  464. continue
  465. item = third_item_svc.get_item(db, tid, tenant_no=tenant_no)
  466. if not item or item.status != "1":
  467. raise ValueError("第三方项目无效或已停用")
  468. comp = get_company(db, item.company_id, tenant_no=tenant_no)
  469. if not comp:
  470. raise ValueError("第三方公司不存在")
  471. fee = item.fee_amount or Decimal("0")
  472. fee_total += fee
  473. line = BizQcWorkOrderThirdItem(
  474. id=generate_uuid(),
  475. work_order_id=wo.id,
  476. order_source=order_source,
  477. batch_id=batch.id,
  478. third_party_item_id=item.id,
  479. company_id=item.company_id,
  480. company_name=comp.company_name,
  481. item_code=item.item_code,
  482. item_name=item.item_name,
  483. fee_amount=fee,
  484. tenant_no=tenant_no,
  485. order_num=seq,
  486. create_by=user_id,
  487. create_dept_id=create_dept_id or "",
  488. create_time=now,
  489. update_by=user_id,
  490. update_time=now,
  491. del_flag=0,
  492. )
  493. db.add(line)
  494. db.flush()
  495. task = task_svc.create_task_for_line(
  496. db,
  497. work_order=wo,
  498. task_type="dsfjc",
  499. source_line_id=line.id,
  500. item_code=line.item_code,
  501. item_name=line.item_name,
  502. tenant_no=tenant_no,
  503. user_id=user_id,
  504. create_dept_id=create_dept_id,
  505. seq=seq,
  506. company_id=line.company_id,
  507. company_name=line.company_name,
  508. fee_amount=line.fee_amount,
  509. order_source=order_source,
  510. batch_id=batch.id,
  511. )
  512. assignee_svc.sync_assignees_for_task(
  513. db,
  514. task.id,
  515. assignee_user_ids,
  516. tenant_no=tenant_no,
  517. operator_id=user_id,
  518. create_dept_id=create_dept_id,
  519. )
  520. return seq, fee_total
  521. def _batch_item_ids(db: Session, batch_id: str) -> Tuple[List[str], List[str]]:
  522. lab_ids = [
  523. r.lab_test_item_id
  524. for r in db.query(BizQcWorkOrderLabItem)
  525. .filter(BizQcWorkOrderLabItem.batch_id == batch_id, BizQcWorkOrderLabItem.del_flag == 0)
  526. .order_by(BizQcWorkOrderLabItem.order_num.asc())
  527. .all()
  528. ]
  529. third_ids = [
  530. r.third_party_item_id
  531. for r in db.query(BizQcWorkOrderThirdItem)
  532. .filter(
  533. BizQcWorkOrderThirdItem.batch_id == batch_id,
  534. BizQcWorkOrderThirdItem.del_flag == 0,
  535. )
  536. .order_by(BizQcWorkOrderThirdItem.order_num.asc())
  537. .all()
  538. ]
  539. return lab_ids, third_ids
  540. def _batch_to_dict(
  541. db: Session,
  542. batch: BizQcWorkOrderInspectionBatch,
  543. *,
  544. tenant_no: str,
  545. tasks: Optional[List[Dict[str, Any]]] = None,
  546. ) -> Dict[str, Any]:
  547. assignee_ids = _batch_assignee_user_ids(db, batch.id, tenant_no)
  548. lab_ids, third_ids = _batch_item_ids(db, batch.id)
  549. task_list = tasks or []
  550. completed = sum(1 for t in task_list if (t.get("taskStatus") or "") == "ywc")
  551. return {
  552. "id": batch.id,
  553. "workOrderId": batch.work_order_id,
  554. "batchNo": batch.batch_no,
  555. "batchName": batch.batch_name,
  556. "orderNum": batch.order_num or 0,
  557. "assigneeUserIds": assignee_ids,
  558. "assigneeUserNames": _assignee_names(db, assignee_ids),
  559. "hasLab": bool(lab_ids),
  560. "hasThirdParty": bool(third_ids),
  561. "labItemIds": lab_ids,
  562. "thirdPartyItemIds": third_ids,
  563. "taskCount": len(task_list),
  564. "completedTaskCount": completed,
  565. "tasks": task_list,
  566. }
  567. def _build_inspection_batch_list(
  568. db: Session,
  569. work_order_id: str,
  570. batches: List[BizQcWorkOrderInspectionBatch],
  571. all_tasks: List[Dict[str, Any]],
  572. tenant_no: str,
  573. ) -> List[Dict[str, Any]]:
  574. tasks_by_batch: Dict[str, List[Dict[str, Any]]] = {}
  575. orphan: List[Dict[str, Any]] = []
  576. batch_ids = {b.id for b in batches}
  577. for t in all_tasks:
  578. bid = t.get("batchId") or ""
  579. if bid and bid in batch_ids:
  580. tasks_by_batch.setdefault(bid, []).append(t)
  581. else:
  582. orphan.append(t)
  583. out = [
  584. _batch_to_dict(db, b, tenant_no=tenant_no, tasks=tasks_by_batch.get(b.id, []))
  585. for b in batches
  586. ]
  587. if orphan and out:
  588. out[0]["tasks"] = orphan + out[0]["tasks"]
  589. out[0]["taskCount"] = len(out[0]["tasks"])
  590. out[0]["completedTaskCount"] = sum(
  591. 1 for t in out[0]["tasks"] if (t.get("taskStatus") or "") == "ywc"
  592. )
  593. elif orphan:
  594. out.append(
  595. {
  596. "id": "",
  597. "workOrderId": work_order_id,
  598. "batchNo": "—",
  599. "batchName": "历史任务",
  600. "orderNum": 0,
  601. "assigneeUserIds": [],
  602. "assigneeUserNames": "",
  603. "taskCount": len(orphan),
  604. "completedTaskCount": sum(1 for t in orphan if (t.get("taskStatus") or "") == "ywc"),
  605. "tasks": orphan,
  606. }
  607. )
  608. return out
  609. def list_inspection_batches(
  610. db: Session, work_order_id: str, tenant_no: str, *, user_info=None
  611. ) -> List[Dict[str, Any]]:
  612. batches = (
  613. db.query(BizQcWorkOrderInspectionBatch)
  614. .filter(
  615. BizQcWorkOrderInspectionBatch.work_order_id == work_order_id,
  616. BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
  617. BizQcWorkOrderInspectionBatch.del_flag == 0,
  618. )
  619. .order_by(BizQcWorkOrderInspectionBatch.order_num.asc())
  620. .all()
  621. )
  622. all_tasks = task_svc.list_tasks_by_work_order(db, work_order_id, tenant_no)
  623. return _build_inspection_batch_list(db, work_order_id, batches, all_tasks, tenant_no)
  624. def list_inspection_batches_by_work_order_ids(
  625. db: Session,
  626. work_order_ids: List[str],
  627. tenant_no: str,
  628. *,
  629. user_info=None,
  630. ) -> Dict[str, List[Dict[str, Any]]]:
  631. """批量返回多个工单的检测批次(含任务),供列表页一次加载。"""
  632. ids = _unique_ids(work_order_ids)
  633. if not ids or not tenant_no:
  634. return {}
  635. ids = ids[:100]
  636. batches = (
  637. db.query(BizQcWorkOrderInspectionBatch)
  638. .filter(
  639. BizQcWorkOrderInspectionBatch.work_order_id.in_(ids),
  640. BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
  641. BizQcWorkOrderInspectionBatch.del_flag == 0,
  642. )
  643. .order_by(
  644. BizQcWorkOrderInspectionBatch.work_order_id.asc(),
  645. BizQcWorkOrderInspectionBatch.order_num.asc(),
  646. )
  647. .all()
  648. )
  649. tasks_by_wo = task_svc.list_tasks_by_work_order_ids(db, ids, tenant_no)
  650. batches_by_wo: Dict[str, List[BizQcWorkOrderInspectionBatch]] = {}
  651. for batch in batches:
  652. batches_by_wo.setdefault(batch.work_order_id, []).append(batch)
  653. out: Dict[str, List[Dict[str, Any]]] = {}
  654. for wo_id in ids:
  655. out[wo_id] = _build_inspection_batch_list(
  656. db,
  657. wo_id,
  658. batches_by_wo.get(wo_id, []),
  659. tasks_by_wo.get(wo_id, []),
  660. tenant_no,
  661. )
  662. return out
  663. def configure_work_order_batches(
  664. db: Session,
  665. wo: BizQcWorkOrder,
  666. batches: List[Dict[str, Any]],
  667. *,
  668. tenant_no: str,
  669. user_id: Optional[str],
  670. create_dept_id: str = "",
  671. ) -> BizQcWorkOrder:
  672. status = (wo.work_order_status or "").strip()
  673. if status not in _CONFIGURABLE_WORK_ORDER_STATUSES:
  674. raise ValueError("当前工单状态不可配置检测")
  675. order_source = _order_source_for_wo(wo)
  676. now = datetime.now()
  677. is_voided = status == "yzf"
  678. is_first = status == "dpz"
  679. for idx, b in enumerate(batches):
  680. _validate_batch_payload(b, idx)
  681. existing_batches = {
  682. r.id: r
  683. for r in db.query(BizQcWorkOrderInspectionBatch)
  684. .filter(
  685. BizQcWorkOrderInspectionBatch.work_order_id == wo.id,
  686. BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
  687. BizQcWorkOrderInspectionBatch.del_flag == 0,
  688. )
  689. .all()
  690. }
  691. keep_batch_ids = {((b.get("id") or "").strip()) for b in batches if (b.get("id") or "").strip()}
  692. # 提交空 batches = 显式清空:强制软删批次及关联任务(含进行中/已完成)
  693. force_clear_all = not batches
  694. for bid, batch_row in existing_batches.items():
  695. if bid not in keep_batch_ids:
  696. _soft_delete_batch(
  697. db,
  698. batch_row,
  699. tenant_no=tenant_no,
  700. user_id=user_id,
  701. force=is_voided or force_clear_all,
  702. )
  703. fee_total = Decimal("0")
  704. has_lab = False
  705. has_third = False
  706. global_seq = 0
  707. for batch_idx, payload in enumerate(batches):
  708. batch_id = (payload.get("id") or "").strip()
  709. lab_ids = _unique_ids(payload.get("labItemIds") or payload.get("lab_item_ids") or [])
  710. third_ids = _unique_ids(payload.get("thirdPartyItemIds") or payload.get("third_party_item_ids") or [])
  711. has_lab_batch = bool(lab_ids) or bool(payload.get("hasLab") or payload.get("has_lab"))
  712. has_third_batch = bool(third_ids) or bool(payload.get("hasThirdParty") or payload.get("has_third_party"))
  713. assignee_ids = payload.get("assigneeUserIds") or payload.get("assignee_user_ids") or []
  714. normalized_assignees = assignee_svc.validate_assignee_user_ids(db, assignee_ids, tenant_no)
  715. batch_name = (payload.get("batchName") or payload.get("batch_name") or "").strip()
  716. if batch_id and batch_id in existing_batches:
  717. batch = existing_batches[batch_id]
  718. batch.order_num = batch_idx + 1
  719. if batch_name:
  720. batch.batch_name = batch_name
  721. batch.update_by = user_id
  722. batch.update_time = now
  723. db.add(batch)
  724. else:
  725. batch = BizQcWorkOrderInspectionBatch(
  726. id=generate_uuid(),
  727. work_order_id=wo.id,
  728. order_source=order_source,
  729. batch_no=_next_batch_no(db, wo, tenant_no),
  730. batch_name=batch_name or _allocate_batch_name(db, wo.id, tenant_no, now),
  731. tenant_no=tenant_no,
  732. order_num=batch_idx + 1,
  733. create_by=user_id,
  734. create_dept_id=create_dept_id or "",
  735. create_time=now,
  736. update_by=user_id,
  737. update_time=now,
  738. del_flag=0,
  739. )
  740. db.add(batch)
  741. db.flush()
  742. sync_batch_assignees(
  743. db,
  744. batch.id,
  745. normalized_assignees,
  746. tenant_no=tenant_no,
  747. operator_id=user_id,
  748. create_dept_id=create_dept_id,
  749. )
  750. if has_lab_batch:
  751. has_lab = True
  752. global_seq = _sync_batch_lab_items(
  753. db,
  754. wo,
  755. batch,
  756. order_source=order_source,
  757. lab_item_ids=lab_ids,
  758. assignee_user_ids=normalized_assignees,
  759. tenant_no=tenant_no,
  760. user_id=user_id,
  761. create_dept_id=create_dept_id,
  762. seq_start=global_seq,
  763. )
  764. else:
  765. global_seq = _sync_batch_lab_items(
  766. db,
  767. wo,
  768. batch,
  769. order_source=order_source,
  770. lab_item_ids=[],
  771. assignee_user_ids=normalized_assignees,
  772. tenant_no=tenant_no,
  773. user_id=user_id,
  774. create_dept_id=create_dept_id,
  775. seq_start=global_seq,
  776. )
  777. if has_third_batch:
  778. has_third = True
  779. global_seq, batch_fee = _sync_batch_third_items(
  780. db,
  781. wo,
  782. batch,
  783. order_source=order_source,
  784. third_party_item_ids=third_ids,
  785. assignee_user_ids=normalized_assignees,
  786. tenant_no=tenant_no,
  787. user_id=user_id,
  788. create_dept_id=create_dept_id,
  789. seq_start=global_seq,
  790. )
  791. fee_total += batch_fee
  792. else:
  793. global_seq, _ = _sync_batch_third_items(
  794. db,
  795. wo,
  796. batch,
  797. order_source=order_source,
  798. third_party_item_ids=[],
  799. assignee_user_ids=normalized_assignees,
  800. tenant_no=tenant_no,
  801. user_id=user_id,
  802. create_dept_id=create_dept_id,
  803. seq_start=global_seq,
  804. )
  805. wo.has_lab = "1" if has_lab else "0"
  806. wo.has_third_party = "1" if has_third else "0"
  807. if not is_voided:
  808. if not batches:
  809. # 空批次列表:清空全部检测批次/任务后回到待配置
  810. wo.work_order_status = "dpz"
  811. else:
  812. wo.work_order_status = "ypz"
  813. if is_first:
  814. wo.configured_time = now
  815. wo.configured_by = user_id
  816. wo.third_party_fee_total = fee_total if has_third else None
  817. wo.update_by = user_id
  818. wo.update_time = now
  819. db.add(wo)
  820. if not is_voided:
  821. from app.crud.pilot_outbound_status_sync import sync_schedule_from_qc_work_order
  822. sync_schedule_from_qc_work_order(db, wo, user_id=user_id)
  823. db.commit()
  824. if not is_voided and batches:
  825. from app.crud.qc_work_order import try_complete_work_order
  826. try_complete_work_order(db, wo.id, tenant_no)
  827. db.refresh(wo)
  828. return wo
  829. def normalize_legacy_configure_to_batches(body: Dict[str, Any]) -> List[Dict[str, Any]]:
  830. """将扁平 labItemIds 配置转为单批次结构。
  831. 注意:`batches: []` 表示显式清空,不得当作「未传 batches」走扁平兼容。
  832. """
  833. if "batches" in body:
  834. batches = body.get("batches")
  835. return list(batches) if batches is not None else []
  836. has_lab = bool(body.get("hasLab") or body.get("has_lab"))
  837. has_third = bool(body.get("hasThirdParty") or body.get("has_third_party"))
  838. lab_ids = body.get("labItemIds") or body.get("lab_item_ids") or []
  839. third_ids = body.get("thirdPartyItemIds") or body.get("third_party_item_ids") or []
  840. lab_assignees = body.get("labItemAssignees") or body.get("lab_item_assignees") or {}
  841. third_assignees = body.get("thirdPartyItemAssignees") or body.get("third_party_item_assignees") or {}
  842. assignee_set: Set[str] = set()
  843. for lid in lab_ids:
  844. for uid in lab_assignees.get(lid) or []:
  845. if uid:
  846. assignee_set.add(uid)
  847. for tid in third_ids:
  848. for uid in third_assignees.get(tid) or []:
  849. if uid:
  850. assignee_set.add(uid)
  851. return [
  852. {
  853. "assigneeUserIds": list(assignee_set),
  854. "hasLab": has_lab,
  855. "hasThirdParty": has_third,
  856. "labItemIds": lab_ids,
  857. "thirdPartyItemIds": third_ids,
  858. }
  859. ]