qc_inspection_batch.py 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028
  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 _prefetch_batch_side_data(
  167. db: Session,
  168. batches: List[BizQcWorkOrderInspectionBatch],
  169. tenant_no: str,
  170. ) -> Dict[str, Any]:
  171. """一次预取多批次的负责人 / 实验室项 / 第三方项 / 用户名。"""
  172. batch_ids = [b.id for b in batches if b and b.id]
  173. assignees_by_batch: Dict[str, List[str]] = {bid: [] for bid in batch_ids}
  174. lab_by_batch: Dict[str, List[str]] = {bid: [] for bid in batch_ids}
  175. third_by_batch: Dict[str, List[str]] = {bid: [] for bid in batch_ids}
  176. name_map: Dict[str, str] = {}
  177. if not batch_ids:
  178. return {
  179. "assignees_by_batch": assignees_by_batch,
  180. "lab_by_batch": lab_by_batch,
  181. "third_by_batch": third_by_batch,
  182. "name_map": name_map,
  183. }
  184. for batch_id, user_id in (
  185. db.query(BizQcBatchAssignee.batch_id, BizQcBatchAssignee.user_id)
  186. .filter(
  187. BizQcBatchAssignee.batch_id.in_(batch_ids),
  188. BizQcBatchAssignee.tenant_no == tenant_no,
  189. BizQcBatchAssignee.del_flag == 0,
  190. )
  191. .order_by(BizQcBatchAssignee.order_num.asc())
  192. .all()
  193. ):
  194. assignees_by_batch.setdefault(batch_id, []).append(user_id)
  195. for batch_id, lab_id in (
  196. db.query(BizQcWorkOrderLabItem.batch_id, BizQcWorkOrderLabItem.lab_test_item_id)
  197. .filter(
  198. BizQcWorkOrderLabItem.batch_id.in_(batch_ids),
  199. BizQcWorkOrderLabItem.del_flag == 0,
  200. )
  201. .order_by(BizQcWorkOrderLabItem.order_num.asc())
  202. .all()
  203. ):
  204. if lab_id:
  205. lab_by_batch.setdefault(batch_id, []).append(lab_id)
  206. for batch_id, third_id in (
  207. db.query(BizQcWorkOrderThirdItem.batch_id, BizQcWorkOrderThirdItem.third_party_item_id)
  208. .filter(
  209. BizQcWorkOrderThirdItem.batch_id.in_(batch_ids),
  210. BizQcWorkOrderThirdItem.del_flag == 0,
  211. )
  212. .order_by(BizQcWorkOrderThirdItem.order_num.asc())
  213. .all()
  214. ):
  215. if third_id:
  216. third_by_batch.setdefault(batch_id, []).append(third_id)
  217. all_user_ids = _unique_ids(
  218. [uid for ids in assignees_by_batch.values() for uid in ids]
  219. )
  220. if all_user_ids:
  221. for row in (
  222. db.query(SysUser.id, SysUser.nick_name, SysUser.user_no)
  223. .filter(SysUser.id.in_(all_user_ids), SysUser.del_flag == 0)
  224. .all()
  225. ):
  226. name_map[row.id] = row.nick_name or row.user_no or row.id
  227. return {
  228. "assignees_by_batch": assignees_by_batch,
  229. "lab_by_batch": lab_by_batch,
  230. "third_by_batch": third_by_batch,
  231. "name_map": name_map,
  232. }
  233. def _apply_assignees_to_batch_tasks(
  234. db: Session,
  235. batch_id: str,
  236. user_ids: List[str],
  237. *,
  238. tenant_no: str,
  239. operator_id: Optional[str],
  240. create_dept_id: str,
  241. ) -> None:
  242. tasks = (
  243. db.query(BizQcTask)
  244. .filter(
  245. BizQcTask.batch_id == batch_id,
  246. BizQcTask.tenant_no == tenant_no,
  247. BizQcTask.del_flag == 0,
  248. )
  249. .all()
  250. )
  251. for task in tasks:
  252. assignee_svc.sync_assignees_for_task(
  253. db,
  254. task.id,
  255. user_ids,
  256. tenant_no=tenant_no,
  257. operator_id=operator_id,
  258. create_dept_id=create_dept_id,
  259. )
  260. def _batch_has_blocking_tasks(db: Session, batch_id: str, tenant_no: str) -> bool:
  261. return (
  262. db.query(BizQcTask)
  263. .filter(
  264. BizQcTask.batch_id == batch_id,
  265. BizQcTask.tenant_no == tenant_no,
  266. BizQcTask.del_flag == 0,
  267. BizQcTask.task_status.in_(["jxz", "ywc"]),
  268. )
  269. .first()
  270. is not None
  271. )
  272. def _soft_delete_batch(
  273. db: Session,
  274. batch: BizQcWorkOrderInspectionBatch,
  275. *,
  276. tenant_no: str,
  277. user_id: Optional[str],
  278. force: bool = False,
  279. ) -> None:
  280. from app.crud.qc_work_order import _soft_delete_tasks_by_source_line_ids
  281. if not force and _batch_has_blocking_tasks(db, batch.id, tenant_no):
  282. raise ValueError(f"批次「{batch.batch_name}」存在进行中或已完成任务,无法删除")
  283. now = datetime.now()
  284. lab_lines = (
  285. db.query(BizQcWorkOrderLabItem)
  286. .filter(
  287. BizQcWorkOrderLabItem.batch_id == batch.id,
  288. BizQcWorkOrderLabItem.del_flag == 0,
  289. )
  290. .all()
  291. )
  292. third_lines = (
  293. db.query(BizQcWorkOrderThirdItem)
  294. .filter(
  295. BizQcWorkOrderThirdItem.batch_id == batch.id,
  296. BizQcWorkOrderThirdItem.del_flag == 0,
  297. )
  298. .all()
  299. )
  300. line_ids = [r.id for r in lab_lines + third_lines]
  301. for line in lab_lines + third_lines:
  302. line.del_flag = 1
  303. line.update_by = user_id
  304. line.update_time = now
  305. _soft_delete_tasks_by_source_line_ids(db, line_ids, tenant_no)
  306. assignees = (
  307. db.query(BizQcBatchAssignee)
  308. .filter(BizQcBatchAssignee.batch_id == batch.id, BizQcBatchAssignee.del_flag == 0)
  309. .all()
  310. )
  311. for a in assignees:
  312. a.del_flag = 1
  313. a.update_by = user_id
  314. a.update_time = now
  315. batch.del_flag = 1
  316. batch.update_by = user_id
  317. batch.update_time = now
  318. def _sync_batch_lab_items(
  319. db: Session,
  320. wo: BizQcWorkOrder,
  321. batch: BizQcWorkOrderInspectionBatch,
  322. *,
  323. order_source: str,
  324. lab_item_ids: List[str],
  325. assignee_user_ids: List[str],
  326. tenant_no: str,
  327. user_id: Optional[str],
  328. create_dept_id: str,
  329. seq_start: int,
  330. ) -> int:
  331. from app.crud.qc_work_order import _get_task_by_source_line_id, _soft_delete_tasks_by_source_line_ids
  332. now = datetime.now()
  333. existing_rows = (
  334. db.query(BizQcWorkOrderLabItem)
  335. .filter(
  336. BizQcWorkOrderLabItem.batch_id == batch.id,
  337. BizQcWorkOrderLabItem.del_flag == 0,
  338. )
  339. .all()
  340. )
  341. existing_by_item_id = {r.lab_test_item_id: r for r in existing_rows}
  342. new_id_set = set(lab_item_ids)
  343. old_id_set = set(existing_by_item_id.keys())
  344. allow_force = _is_voided_work_order(wo)
  345. for item_id in old_id_set - new_id_set:
  346. line = existing_by_item_id[item_id]
  347. task = _get_task_by_source_line_id(db, line.id, tenant_no)
  348. if (
  349. not allow_force
  350. and task
  351. and (task.task_status or "") in ("jxz", "ywc")
  352. ):
  353. raise ValueError(f"项目「{line.item_name}」任务进行中或已完成,无法移除")
  354. line.del_flag = 1
  355. line.update_by = user_id
  356. line.update_time = now
  357. db.add(line)
  358. remove_line_ids = [existing_by_item_id[i].id for i in old_id_set - new_id_set]
  359. _soft_delete_tasks_by_source_line_ids(db, remove_line_ids, tenant_no)
  360. seq = seq_start
  361. for lid in lab_item_ids:
  362. seq += 1
  363. if lid in existing_by_item_id and lid in new_id_set:
  364. line = existing_by_item_id[lid]
  365. line.order_num = seq
  366. line.update_by = user_id
  367. line.update_time = now
  368. db.add(line)
  369. task = _get_task_by_source_line_id(db, line.id, tenant_no)
  370. if task:
  371. task.order_num = seq
  372. task.batch_id = batch.id
  373. task.update_by = user_id
  374. task.update_time = now
  375. db.add(task)
  376. else:
  377. task = task_svc.create_task_for_line(
  378. db,
  379. work_order=wo,
  380. task_type="sysz",
  381. source_line_id=line.id,
  382. item_code=line.item_code,
  383. item_name=line.item_name,
  384. tenant_no=tenant_no,
  385. user_id=user_id,
  386. create_dept_id=create_dept_id,
  387. seq=seq,
  388. order_source=order_source,
  389. batch_id=batch.id,
  390. )
  391. assignee_svc.sync_assignees_for_task(
  392. db,
  393. task.id,
  394. assignee_user_ids,
  395. tenant_no=tenant_no,
  396. operator_id=user_id,
  397. create_dept_id=create_dept_id,
  398. )
  399. continue
  400. item = lab_item_svc.get_lab_item(db, lid, tenant_no=tenant_no)
  401. if not item or item.status != "1":
  402. raise ValueError("实验室项目无效或已停用")
  403. line = BizQcWorkOrderLabItem(
  404. id=generate_uuid(),
  405. work_order_id=wo.id,
  406. order_source=order_source,
  407. batch_id=batch.id,
  408. lab_test_item_id=item.id,
  409. item_code=item.item_code,
  410. item_name=item.item_name,
  411. tenant_no=tenant_no,
  412. order_num=seq,
  413. create_by=user_id,
  414. create_dept_id=create_dept_id or "",
  415. create_time=now,
  416. update_by=user_id,
  417. update_time=now,
  418. del_flag=0,
  419. )
  420. db.add(line)
  421. db.flush()
  422. task = task_svc.create_task_for_line(
  423. db,
  424. work_order=wo,
  425. task_type="sysz",
  426. source_line_id=line.id,
  427. item_code=line.item_code,
  428. item_name=line.item_name,
  429. tenant_no=tenant_no,
  430. user_id=user_id,
  431. create_dept_id=create_dept_id,
  432. seq=seq,
  433. order_source=order_source,
  434. batch_id=batch.id,
  435. )
  436. assignee_svc.sync_assignees_for_task(
  437. db,
  438. task.id,
  439. assignee_user_ids,
  440. tenant_no=tenant_no,
  441. operator_id=user_id,
  442. create_dept_id=create_dept_id,
  443. )
  444. return seq
  445. def _sync_batch_third_items(
  446. db: Session,
  447. wo: BizQcWorkOrder,
  448. batch: BizQcWorkOrderInspectionBatch,
  449. *,
  450. order_source: str,
  451. third_party_item_ids: List[str],
  452. assignee_user_ids: List[str],
  453. tenant_no: str,
  454. user_id: Optional[str],
  455. create_dept_id: str,
  456. seq_start: int,
  457. ) -> Tuple[int, Decimal]:
  458. from app.crud.qc_work_order import _get_task_by_source_line_id, _soft_delete_tasks_by_source_line_ids
  459. now = datetime.now()
  460. existing_rows = (
  461. db.query(BizQcWorkOrderThirdItem)
  462. .filter(
  463. BizQcWorkOrderThirdItem.batch_id == batch.id,
  464. BizQcWorkOrderThirdItem.del_flag == 0,
  465. )
  466. .all()
  467. )
  468. existing_by_item_id = {r.third_party_item_id: r for r in existing_rows}
  469. new_id_set = set(third_party_item_ids)
  470. old_id_set = set(existing_by_item_id.keys())
  471. allow_force = _is_voided_work_order(wo)
  472. for item_id in old_id_set - new_id_set:
  473. line = existing_by_item_id[item_id]
  474. task = _get_task_by_source_line_id(db, line.id, tenant_no)
  475. if (
  476. not allow_force
  477. and task
  478. and (task.task_status or "") in ("jxz", "ywc")
  479. ):
  480. raise ValueError(f"项目「{line.item_name}」任务进行中或已完成,无法移除")
  481. line.del_flag = 1
  482. line.update_by = user_id
  483. line.update_time = now
  484. db.add(line)
  485. remove_line_ids = [existing_by_item_id[i].id for i in old_id_set - new_id_set]
  486. _soft_delete_tasks_by_source_line_ids(db, remove_line_ids, tenant_no)
  487. fee_total = Decimal("0")
  488. seq = seq_start
  489. for tid in third_party_item_ids:
  490. seq += 1
  491. if tid in existing_by_item_id and tid in new_id_set:
  492. line = existing_by_item_id[tid]
  493. line.order_num = seq
  494. line.update_by = user_id
  495. line.update_time = now
  496. db.add(line)
  497. fee_total += line.fee_amount or Decimal("0")
  498. task = _get_task_by_source_line_id(db, line.id, tenant_no)
  499. if task:
  500. task.order_num = seq
  501. task.batch_id = batch.id
  502. task.update_by = user_id
  503. task.update_time = now
  504. db.add(task)
  505. else:
  506. task = task_svc.create_task_for_line(
  507. db,
  508. work_order=wo,
  509. task_type="dsfjc",
  510. source_line_id=line.id,
  511. item_code=line.item_code,
  512. item_name=line.item_name,
  513. tenant_no=tenant_no,
  514. user_id=user_id,
  515. create_dept_id=create_dept_id,
  516. seq=seq,
  517. company_id=line.company_id,
  518. company_name=line.company_name,
  519. fee_amount=line.fee_amount,
  520. order_source=order_source,
  521. batch_id=batch.id,
  522. )
  523. assignee_svc.sync_assignees_for_task(
  524. db,
  525. task.id,
  526. assignee_user_ids,
  527. tenant_no=tenant_no,
  528. operator_id=user_id,
  529. create_dept_id=create_dept_id,
  530. )
  531. continue
  532. item = third_item_svc.get_item(db, tid, tenant_no=tenant_no)
  533. if not item or item.status != "1":
  534. raise ValueError("第三方项目无效或已停用")
  535. comp = get_company(db, item.company_id, tenant_no=tenant_no)
  536. if not comp:
  537. raise ValueError("第三方公司不存在")
  538. fee = item.fee_amount or Decimal("0")
  539. fee_total += fee
  540. line = BizQcWorkOrderThirdItem(
  541. id=generate_uuid(),
  542. work_order_id=wo.id,
  543. order_source=order_source,
  544. batch_id=batch.id,
  545. third_party_item_id=item.id,
  546. company_id=item.company_id,
  547. company_name=comp.company_name,
  548. item_code=item.item_code,
  549. item_name=item.item_name,
  550. fee_amount=fee,
  551. tenant_no=tenant_no,
  552. order_num=seq,
  553. create_by=user_id,
  554. create_dept_id=create_dept_id or "",
  555. create_time=now,
  556. update_by=user_id,
  557. update_time=now,
  558. del_flag=0,
  559. )
  560. db.add(line)
  561. db.flush()
  562. task = task_svc.create_task_for_line(
  563. db,
  564. work_order=wo,
  565. task_type="dsfjc",
  566. source_line_id=line.id,
  567. item_code=line.item_code,
  568. item_name=line.item_name,
  569. tenant_no=tenant_no,
  570. user_id=user_id,
  571. create_dept_id=create_dept_id,
  572. seq=seq,
  573. company_id=line.company_id,
  574. company_name=line.company_name,
  575. fee_amount=line.fee_amount,
  576. order_source=order_source,
  577. batch_id=batch.id,
  578. )
  579. assignee_svc.sync_assignees_for_task(
  580. db,
  581. task.id,
  582. assignee_user_ids,
  583. tenant_no=tenant_no,
  584. operator_id=user_id,
  585. create_dept_id=create_dept_id,
  586. )
  587. return seq, fee_total
  588. def _batch_item_ids(db: Session, batch_id: str) -> Tuple[List[str], List[str]]:
  589. lab_ids = [
  590. r.lab_test_item_id
  591. for r in db.query(BizQcWorkOrderLabItem)
  592. .filter(BizQcWorkOrderLabItem.batch_id == batch_id, BizQcWorkOrderLabItem.del_flag == 0)
  593. .order_by(BizQcWorkOrderLabItem.order_num.asc())
  594. .all()
  595. ]
  596. third_ids = [
  597. r.third_party_item_id
  598. for r in db.query(BizQcWorkOrderThirdItem)
  599. .filter(
  600. BizQcWorkOrderThirdItem.batch_id == batch_id,
  601. BizQcWorkOrderThirdItem.del_flag == 0,
  602. )
  603. .order_by(BizQcWorkOrderThirdItem.order_num.asc())
  604. .all()
  605. ]
  606. return lab_ids, third_ids
  607. def _batch_to_dict(
  608. db: Session,
  609. batch: BizQcWorkOrderInspectionBatch,
  610. *,
  611. tenant_no: str,
  612. tasks: Optional[List[Dict[str, Any]]] = None,
  613. side_data: Optional[Dict[str, Any]] = None,
  614. ) -> Dict[str, Any]:
  615. if side_data is not None:
  616. assignee_ids = list(side_data["assignees_by_batch"].get(batch.id, []))
  617. lab_ids = list(side_data["lab_by_batch"].get(batch.id, []))
  618. third_ids = list(side_data["third_by_batch"].get(batch.id, []))
  619. name_map = side_data.get("name_map") or {}
  620. assignee_names = "、".join(name_map.get(uid, uid) for uid in assignee_ids)
  621. else:
  622. assignee_ids = _batch_assignee_user_ids(db, batch.id, tenant_no)
  623. lab_ids, third_ids = _batch_item_ids(db, batch.id)
  624. assignee_names = _assignee_names(db, assignee_ids)
  625. task_list = tasks or []
  626. completed = sum(1 for t in task_list if (t.get("taskStatus") or "") == "ywc")
  627. return {
  628. "id": batch.id,
  629. "workOrderId": batch.work_order_id,
  630. "batchNo": batch.batch_no,
  631. "batchName": batch.batch_name,
  632. "orderNum": batch.order_num or 0,
  633. "assigneeUserIds": assignee_ids,
  634. "assigneeUserNames": assignee_names,
  635. "hasLab": bool(lab_ids),
  636. "hasThirdParty": bool(third_ids),
  637. "labItemIds": lab_ids,
  638. "thirdPartyItemIds": third_ids,
  639. "taskCount": len(task_list),
  640. "completedTaskCount": completed,
  641. "tasks": task_list,
  642. }
  643. def _build_inspection_batch_list(
  644. db: Session,
  645. work_order_id: str,
  646. batches: List[BizQcWorkOrderInspectionBatch],
  647. all_tasks: List[Dict[str, Any]],
  648. tenant_no: str,
  649. *,
  650. side_data: Optional[Dict[str, Any]] = None,
  651. ) -> List[Dict[str, Any]]:
  652. tasks_by_batch: Dict[str, List[Dict[str, Any]]] = {}
  653. orphan: List[Dict[str, Any]] = []
  654. batch_ids = {b.id for b in batches}
  655. for t in all_tasks:
  656. bid = t.get("batchId") or ""
  657. if bid and bid in batch_ids:
  658. tasks_by_batch.setdefault(bid, []).append(t)
  659. else:
  660. orphan.append(t)
  661. resolved_side = side_data
  662. if resolved_side is None and batches:
  663. resolved_side = _prefetch_batch_side_data(db, batches, tenant_no)
  664. out = [
  665. _batch_to_dict(
  666. db,
  667. b,
  668. tenant_no=tenant_no,
  669. tasks=tasks_by_batch.get(b.id, []),
  670. side_data=resolved_side,
  671. )
  672. for b in batches
  673. ]
  674. if orphan and out:
  675. out[0]["tasks"] = orphan + out[0]["tasks"]
  676. out[0]["taskCount"] = len(out[0]["tasks"])
  677. out[0]["completedTaskCount"] = sum(
  678. 1 for t in out[0]["tasks"] if (t.get("taskStatus") or "") == "ywc"
  679. )
  680. elif orphan:
  681. out.append(
  682. {
  683. "id": "",
  684. "workOrderId": work_order_id,
  685. "batchNo": "—",
  686. "batchName": "历史任务",
  687. "orderNum": 0,
  688. "assigneeUserIds": [],
  689. "assigneeUserNames": "",
  690. "taskCount": len(orphan),
  691. "completedTaskCount": sum(1 for t in orphan if (t.get("taskStatus") or "") == "ywc"),
  692. "tasks": orphan,
  693. }
  694. )
  695. return out
  696. def list_inspection_batches(
  697. db: Session, work_order_id: str, tenant_no: str, *, user_info=None
  698. ) -> List[Dict[str, Any]]:
  699. batches = (
  700. db.query(BizQcWorkOrderInspectionBatch)
  701. .filter(
  702. BizQcWorkOrderInspectionBatch.work_order_id == work_order_id,
  703. BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
  704. BizQcWorkOrderInspectionBatch.del_flag == 0,
  705. )
  706. .order_by(BizQcWorkOrderInspectionBatch.order_num.asc())
  707. .all()
  708. )
  709. all_tasks = task_svc.list_tasks_by_work_order(db, work_order_id, tenant_no)
  710. return _build_inspection_batch_list(db, work_order_id, batches, all_tasks, tenant_no)
  711. def list_inspection_batches_by_work_order_ids(
  712. db: Session,
  713. work_order_ids: List[str],
  714. tenant_no: str,
  715. *,
  716. user_info=None,
  717. ) -> Dict[str, List[Dict[str, Any]]]:
  718. """批量返回多个工单的检测批次(含任务),供列表页一次加载。"""
  719. ids = _unique_ids(work_order_ids)
  720. if not ids or not tenant_no:
  721. return {}
  722. ids = ids[:100]
  723. batches = (
  724. db.query(BizQcWorkOrderInspectionBatch)
  725. .filter(
  726. BizQcWorkOrderInspectionBatch.work_order_id.in_(ids),
  727. BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
  728. BizQcWorkOrderInspectionBatch.del_flag == 0,
  729. )
  730. .order_by(
  731. BizQcWorkOrderInspectionBatch.work_order_id.asc(),
  732. BizQcWorkOrderInspectionBatch.order_num.asc(),
  733. )
  734. .all()
  735. )
  736. tasks_by_wo = task_svc.list_tasks_by_work_order_ids(db, ids, tenant_no)
  737. side_data = _prefetch_batch_side_data(db, batches, tenant_no)
  738. batches_by_wo: Dict[str, List[BizQcWorkOrderInspectionBatch]] = {}
  739. for batch in batches:
  740. batches_by_wo.setdefault(batch.work_order_id, []).append(batch)
  741. out: Dict[str, List[Dict[str, Any]]] = {}
  742. for wo_id in ids:
  743. out[wo_id] = _build_inspection_batch_list(
  744. db,
  745. wo_id,
  746. batches_by_wo.get(wo_id, []),
  747. tasks_by_wo.get(wo_id, []),
  748. tenant_no,
  749. side_data=side_data,
  750. )
  751. return out
  752. def configure_work_order_batches(
  753. db: Session,
  754. wo: BizQcWorkOrder,
  755. batches: List[Dict[str, Any]],
  756. *,
  757. tenant_no: str,
  758. user_id: Optional[str],
  759. create_dept_id: str = "",
  760. ) -> BizQcWorkOrder:
  761. status = (wo.work_order_status or "").strip()
  762. if status not in _CONFIGURABLE_WORK_ORDER_STATUSES:
  763. raise ValueError("当前工单状态不可配置检测")
  764. order_source = _order_source_for_wo(wo)
  765. now = datetime.now()
  766. is_voided = status == "yzf"
  767. is_first = status == "dpz"
  768. for idx, b in enumerate(batches):
  769. _validate_batch_payload(b, idx)
  770. existing_batches = {
  771. r.id: r
  772. for r in db.query(BizQcWorkOrderInspectionBatch)
  773. .filter(
  774. BizQcWorkOrderInspectionBatch.work_order_id == wo.id,
  775. BizQcWorkOrderInspectionBatch.tenant_no == tenant_no,
  776. BizQcWorkOrderInspectionBatch.del_flag == 0,
  777. )
  778. .all()
  779. }
  780. keep_batch_ids = {((b.get("id") or "").strip()) for b in batches if (b.get("id") or "").strip()}
  781. # 提交空 batches = 显式清空:强制软删批次及关联任务(含进行中/已完成)
  782. force_clear_all = not batches
  783. for bid, batch_row in existing_batches.items():
  784. if bid not in keep_batch_ids:
  785. _soft_delete_batch(
  786. db,
  787. batch_row,
  788. tenant_no=tenant_no,
  789. user_id=user_id,
  790. force=is_voided or force_clear_all,
  791. )
  792. fee_total = Decimal("0")
  793. has_lab = False
  794. has_third = False
  795. global_seq = 0
  796. for batch_idx, payload in enumerate(batches):
  797. batch_id = (payload.get("id") or "").strip()
  798. lab_ids = _unique_ids(payload.get("labItemIds") or payload.get("lab_item_ids") or [])
  799. third_ids = _unique_ids(payload.get("thirdPartyItemIds") or payload.get("third_party_item_ids") or [])
  800. has_lab_batch = bool(lab_ids) or bool(payload.get("hasLab") or payload.get("has_lab"))
  801. has_third_batch = bool(third_ids) or bool(payload.get("hasThirdParty") or payload.get("has_third_party"))
  802. assignee_ids = payload.get("assigneeUserIds") or payload.get("assignee_user_ids") or []
  803. normalized_assignees = assignee_svc.validate_assignee_user_ids(db, assignee_ids, tenant_no)
  804. batch_name = (payload.get("batchName") or payload.get("batch_name") or "").strip()
  805. if batch_id and batch_id in existing_batches:
  806. batch = existing_batches[batch_id]
  807. batch.order_num = batch_idx + 1
  808. if batch_name:
  809. batch.batch_name = batch_name
  810. batch.update_by = user_id
  811. batch.update_time = now
  812. db.add(batch)
  813. else:
  814. batch = BizQcWorkOrderInspectionBatch(
  815. id=generate_uuid(),
  816. work_order_id=wo.id,
  817. order_source=order_source,
  818. batch_no=_next_batch_no(db, wo, tenant_no),
  819. batch_name=batch_name or _allocate_batch_name(db, wo.id, tenant_no, now),
  820. tenant_no=tenant_no,
  821. order_num=batch_idx + 1,
  822. create_by=user_id,
  823. create_dept_id=create_dept_id or "",
  824. create_time=now,
  825. update_by=user_id,
  826. update_time=now,
  827. del_flag=0,
  828. )
  829. db.add(batch)
  830. db.flush()
  831. sync_batch_assignees(
  832. db,
  833. batch.id,
  834. normalized_assignees,
  835. tenant_no=tenant_no,
  836. operator_id=user_id,
  837. create_dept_id=create_dept_id,
  838. )
  839. if has_lab_batch:
  840. has_lab = True
  841. global_seq = _sync_batch_lab_items(
  842. db,
  843. wo,
  844. batch,
  845. order_source=order_source,
  846. lab_item_ids=lab_ids,
  847. assignee_user_ids=normalized_assignees,
  848. tenant_no=tenant_no,
  849. user_id=user_id,
  850. create_dept_id=create_dept_id,
  851. seq_start=global_seq,
  852. )
  853. else:
  854. global_seq = _sync_batch_lab_items(
  855. db,
  856. wo,
  857. batch,
  858. order_source=order_source,
  859. lab_item_ids=[],
  860. assignee_user_ids=normalized_assignees,
  861. tenant_no=tenant_no,
  862. user_id=user_id,
  863. create_dept_id=create_dept_id,
  864. seq_start=global_seq,
  865. )
  866. if has_third_batch:
  867. has_third = True
  868. global_seq, batch_fee = _sync_batch_third_items(
  869. db,
  870. wo,
  871. batch,
  872. order_source=order_source,
  873. third_party_item_ids=third_ids,
  874. assignee_user_ids=normalized_assignees,
  875. tenant_no=tenant_no,
  876. user_id=user_id,
  877. create_dept_id=create_dept_id,
  878. seq_start=global_seq,
  879. )
  880. fee_total += batch_fee
  881. else:
  882. global_seq, _ = _sync_batch_third_items(
  883. db,
  884. wo,
  885. batch,
  886. order_source=order_source,
  887. third_party_item_ids=[],
  888. assignee_user_ids=normalized_assignees,
  889. tenant_no=tenant_no,
  890. user_id=user_id,
  891. create_dept_id=create_dept_id,
  892. seq_start=global_seq,
  893. )
  894. wo.has_lab = "1" if has_lab else "0"
  895. wo.has_third_party = "1" if has_third else "0"
  896. if not is_voided:
  897. if not batches:
  898. # 空批次列表:清空全部检测批次/任务后回到待配置
  899. wo.work_order_status = "dpz"
  900. else:
  901. wo.work_order_status = "ypz"
  902. if is_first:
  903. wo.configured_time = now
  904. wo.configured_by = user_id
  905. wo.third_party_fee_total = fee_total if has_third else None
  906. wo.update_by = user_id
  907. wo.update_time = now
  908. db.add(wo)
  909. if not is_voided:
  910. from app.crud.pilot_outbound_status_sync import sync_schedule_from_qc_work_order
  911. sync_schedule_from_qc_work_order(db, wo, user_id=user_id)
  912. db.commit()
  913. if not is_voided and batches:
  914. from app.crud.qc_work_order import try_complete_work_order
  915. try_complete_work_order(db, wo.id, tenant_no)
  916. db.refresh(wo)
  917. return wo
  918. def normalize_legacy_configure_to_batches(body: Dict[str, Any]) -> List[Dict[str, Any]]:
  919. """将扁平 labItemIds 配置转为单批次结构。
  920. 注意:`batches: []` 表示显式清空,不得当作「未传 batches」走扁平兼容。
  921. """
  922. if "batches" in body:
  923. batches = body.get("batches")
  924. return list(batches) if batches is not None else []
  925. has_lab = bool(body.get("hasLab") or body.get("has_lab"))
  926. has_third = bool(body.get("hasThirdParty") or body.get("has_third_party"))
  927. lab_ids = body.get("labItemIds") or body.get("lab_item_ids") or []
  928. third_ids = body.get("thirdPartyItemIds") or body.get("third_party_item_ids") or []
  929. lab_assignees = body.get("labItemAssignees") or body.get("lab_item_assignees") or {}
  930. third_assignees = body.get("thirdPartyItemAssignees") or body.get("third_party_item_assignees") or {}
  931. assignee_set: Set[str] = set()
  932. for lid in lab_ids:
  933. for uid in lab_assignees.get(lid) or []:
  934. if uid:
  935. assignee_set.add(uid)
  936. for tid in third_ids:
  937. for uid in third_assignees.get(tid) or []:
  938. if uid:
  939. assignee_set.add(uid)
  940. return [
  941. {
  942. "assigneeUserIds": list(assignee_set),
  943. "hasLab": has_lab,
  944. "hasThirdParty": has_third,
  945. "labItemIds": lab_ids,
  946. "thirdPartyItemIds": third_ids,
  947. }
  948. ]