qc_task.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083
  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, Tuple
  6. from sqlalchemy import and_, func, or_
  7. from sqlalchemy.orm import Session
  8. from app.core.utils import generate_uuid
  9. from app.crud.collection_protocol import urls_to_attachment_list
  10. from app.crud import sample_reception as sample_reception_svc
  11. from app.crud.qc_work_order import (
  12. _load_sample_type_name_map,
  13. _qc_biz_type_label,
  14. _reception_source_label,
  15. _resolve_reception_id_for_work_order_detail,
  16. _sample_type_name_for_reception,
  17. _work_order_qc_biz_type,
  18. _OUTBOUND_BIZ_TYPE_LABELS,
  19. build_work_order_list_summaries,
  20. )
  21. from app.core.db_collation import eq_unicode, unicode_ci
  22. from app.utils.sample_special import apply_special_to_item_from_samples
  23. from app.crud import qc_task_assignee as assignee_svc
  24. from app.models.models import (
  25. BizCollectionProtocol,
  26. BizProtocolSample,
  27. BizQcTask,
  28. BizQcTaskAssignee,
  29. BizQcWorkOrder,
  30. BizQcWorkOrderLabItem,
  31. BizQcWorkOrderThirdItem,
  32. BizSampleReception,
  33. )
  34. from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
  35. def _dec(v: Any) -> Optional[float]:
  36. if v is None:
  37. return None
  38. return float(v)
  39. def _fmt_dt(t: Optional[datetime]) -> str:
  40. return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
  41. def _gen_task_no(db: Session, work_order, seq: int) -> str:
  42. return f"{work_order.work_order_no}-{seq:02d}"
  43. _CONCLUSION_LABELS = {"tg": "通过", "btg": "不通过"}
  44. _PROTOCOL_STATUS_LABELS = {
  45. "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zky": "转科研"
  46. }
  47. _DONATION_STATUS_LABELS = {
  48. "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zckc": "转细胞存储"
  49. }
  50. def _conclusion_label(v: Optional[str]) -> str:
  51. key = (v or "").strip()
  52. return _CONCLUSION_LABELS.get(key, key)
  53. def _task_to_dict(
  54. t: BizQcTask,
  55. work_order_no: str = "",
  56. db: Optional[Session] = None,
  57. tenant_no: Optional[str] = None,
  58. ) -> Dict[str, Any]:
  59. tn = tenant_no or t.tenant_no
  60. return {
  61. "id": t.id,
  62. "workOrderId": t.work_order_id,
  63. "batchId": getattr(t, "batch_id", None) or "",
  64. "orderSource": getattr(t, "order_source", "inbound") or "inbound",
  65. "workOrderNo": work_order_no,
  66. "taskNo": t.task_no,
  67. "taskType": t.task_type,
  68. "sourceLineId": t.source_line_id,
  69. "itemCode": t.item_code,
  70. "itemName": t.item_name,
  71. "companyId": t.company_id or "",
  72. "companyName": t.company_name or "",
  73. "feeAmount": _dec(t.fee_amount),
  74. "taskStatus": t.task_status,
  75. "conclusion": t.conclusion or "",
  76. "conclusionLabel": _conclusion_label(t.conclusion),
  77. "thirdPartyAttachments": t.third_party_attachments or "",
  78. "attachmentFiles": urls_to_attachment_list(t.third_party_attachments, db, tn) if db else [],
  79. "completedTime": _fmt_dt(t.completed_time),
  80. "completedBy": t.completed_by or "",
  81. "remark": t.remark or "",
  82. "createTime": _fmt_dt(t.create_time),
  83. "updateTime": _fmt_dt(t.update_time),
  84. }
  85. def _task_work_order_context(wo: BizQcWorkOrder, t: BizQcTask) -> Dict[str, Any]:
  86. biz = _work_order_qc_biz_type(wo)
  87. wo_type = getattr(wo, "work_order_type", None) or "inbound"
  88. if wo_type == "temporary":
  89. order_source = "temporary"
  90. recv_source = ""
  91. recv_label = ""
  92. elif wo_type == "outbound":
  93. order_source = "outbound"
  94. recv_source = getattr(wo, "biz_type", "") or ""
  95. recv_label = _OUTBOUND_BIZ_TYPE_LABELS.get(recv_source, recv_source) if recv_source else ""
  96. else:
  97. order_source = getattr(t, "order_source", "inbound") or "inbound"
  98. recv_source = getattr(wo, "reception_source", "cell_storage") or "cell_storage"
  99. recv_label = _reception_source_label(recv_source)
  100. return {
  101. "orderSource": order_source,
  102. "receptionId": wo.reception_id,
  103. "protocolNo": wo.protocol_no or "",
  104. "sampleCode": wo.sample_code or t.sample_code or "",
  105. "qcBizType": biz,
  106. "qcBizTypeLabel": _qc_biz_type_label(biz),
  107. "receptionSource": recv_source,
  108. "receptionSourceLabel": recv_label,
  109. "cellProductId": getattr(wo, "cell_product_id", None) or "",
  110. "cellProductCode": "",
  111. "cellProductName": "",
  112. "cellProductTypeName": "",
  113. "cellProductPassage": "",
  114. "cellProductSpecName": "",
  115. "receptionTime": "",
  116. "handlerName": "",
  117. "sampleTypeName": "",
  118. "workOrderStatus": wo.work_order_status or "",
  119. }
  120. _WORK_ORDER_SUMMARY_COPY_KEYS = (
  121. "triggerSource",
  122. "triggerSourceLabel",
  123. "hasLab",
  124. "hasThirdParty",
  125. "labStorageCode",
  126. "protocolSampleId",
  127. "collectionProtocolId",
  128. "applicationNo",
  129. "applicationName",
  130. "projectTitle",
  131. "customerName",
  132. "channelName",
  133. "patientName",
  134. "infusionTime",
  135. "sortNo",
  136. "totalCourseCount",
  137. "products",
  138. "cellType",
  139. "specText",
  140. "productionWorkOrderId",
  141. "productionWorkOrderNo",
  142. "cellProductCode",
  143. "cellProductName",
  144. "cellProductTypeName",
  145. "cellProductPassage",
  146. "cellProductSpecName",
  147. "paymentSummary",
  148. "feeSummary",
  149. "bizType",
  150. "bizTypeLabel",
  151. "motherName",
  152. "newbornName",
  153. "isSpecial",
  154. "specialRemark",
  155. "unfinishedBadgeCount",
  156. "protocolSiblingSamples",
  157. )
  158. def _merge_work_order_summary_into_task(item: Dict[str, Any], summary: Dict[str, Any]) -> None:
  159. """将列表级工单摘要合并进任务行,供「我的质检任务」前端聚合展示。"""
  160. for key in _WORK_ORDER_SUMMARY_COPY_KEYS:
  161. if key in summary:
  162. item[key] = summary[key]
  163. if summary.get("createTime"):
  164. item["workOrderCreateTime"] = summary["createTime"]
  165. if "remark" in summary:
  166. item["workOrderRemark"] = summary.get("remark") or ""
  167. for key in (
  168. "receptionTime",
  169. "handlerName",
  170. "sampleTypeName",
  171. "protocolStatus",
  172. "protocolStatusLabel",
  173. "motherName",
  174. "newbornName",
  175. "isSpecial",
  176. "specialRemark",
  177. ):
  178. if not item.get(key) and summary.get(key):
  179. item[key] = summary[key]
  180. def _enrich_mine_tasks_with_work_order_summaries(
  181. db: Session,
  182. rows: List[Tuple[BizQcTask, BizQcWorkOrder]],
  183. items: List[Dict[str, Any]],
  184. tenant_no: Optional[str],
  185. ) -> List[Dict[str, Any]]:
  186. if not rows or not items:
  187. return items
  188. summaries = build_work_order_list_summaries(
  189. db, [wo for _, wo in rows], tenant_no
  190. )
  191. if not summaries:
  192. return items
  193. for item in items:
  194. summary = summaries.get(item.get("workOrderId") or "")
  195. if summary:
  196. _merge_work_order_summary_into_task(item, summary)
  197. return items
  198. def _enrich_tasks_reception_fields(
  199. db: Session, items: List[Dict[str, Any]]
  200. ) -> List[Dict[str, Any]]:
  201. if not items:
  202. return items
  203. rec_ids = [x.get("receptionId") for x in items if x.get("receptionId")]
  204. if not rec_ids:
  205. return items
  206. rec_map = {
  207. r.id: r
  208. for r in db.query(BizSampleReception)
  209. .filter(BizSampleReception.id.in_(rec_ids), BizSampleReception.del_flag == 0)
  210. .all()
  211. }
  212. cell_sample_ids = [
  213. r.protocol_sample_id
  214. for r in rec_map.values()
  215. if (r.reception_source or "cell_storage") != "research_donation" and r.protocol_sample_id
  216. ]
  217. donation_sample_ids = [
  218. r.donation_sample_id
  219. for r in rec_map.values()
  220. if (r.reception_source or "") == "research_donation" and r.donation_sample_id
  221. ]
  222. protocol_samples: Dict[str, BizProtocolSample] = {}
  223. if cell_sample_ids:
  224. protocol_samples = {
  225. s.id: s
  226. for s in db.query(BizProtocolSample)
  227. .filter(BizProtocolSample.id.in_(cell_sample_ids), BizProtocolSample.del_flag == 0)
  228. .all()
  229. }
  230. donation_samples: Dict[str, BizResearchDonationSample] = {}
  231. if donation_sample_ids:
  232. donation_samples = {
  233. s.id: s
  234. for s in db.query(BizResearchDonationSample)
  235. .filter(
  236. BizResearchDonationSample.id.in_(donation_sample_ids),
  237. BizResearchDonationSample.del_flag == 0,
  238. )
  239. .all()
  240. }
  241. type_ids = list(
  242. {
  243. s.sample_type_id
  244. for s in list(protocol_samples.values()) + list(donation_samples.values())
  245. if s.sample_type_id
  246. }
  247. )
  248. type_map = _load_sample_type_name_map(db, type_ids)
  249. cell_proto_ids = [
  250. rec.collection_protocol_id
  251. for rec in rec_map.values()
  252. if (rec.reception_source or "cell_storage") != "research_donation" and rec.collection_protocol_id
  253. ]
  254. donation_ids = [
  255. rec.research_donation_id
  256. for rec in rec_map.values()
  257. if (rec.reception_source or "") == "research_donation" and rec.research_donation_id
  258. ]
  259. proto_status_map: Dict[str, str] = {}
  260. proto_void_type_map: Dict[str, str] = {}
  261. tenant_no_for_status = ""
  262. if cell_proto_ids:
  263. for p in (
  264. db.query(
  265. BizCollectionProtocol.id,
  266. BizCollectionProtocol.protocol_status,
  267. BizCollectionProtocol.void_type,
  268. BizCollectionProtocol.tenant_no,
  269. )
  270. .filter(BizCollectionProtocol.id.in_(cell_proto_ids), BizCollectionProtocol.del_flag == 0)
  271. .all()
  272. ):
  273. proto_status_map[p.id] = p.protocol_status or ""
  274. proto_void_type_map[p.id] = p.void_type or ""
  275. if not tenant_no_for_status and (p.tenant_no or "").strip():
  276. tenant_no_for_status = (p.tenant_no or "").strip()
  277. donation_status_map: Dict[str, str] = {}
  278. donation_void_type_map: Dict[str, str] = {}
  279. if donation_ids:
  280. for d in (
  281. db.query(
  282. BizResearchDonation.id,
  283. BizResearchDonation.donation_status,
  284. BizResearchDonation.void_type,
  285. BizResearchDonation.tenant_no,
  286. )
  287. .filter(BizResearchDonation.id.in_(donation_ids), BizResearchDonation.del_flag == 0)
  288. .all()
  289. ):
  290. donation_status_map[d.id] = d.donation_status or ""
  291. donation_void_type_map[d.id] = d.void_type or ""
  292. if not tenant_no_for_status and (d.tenant_no or "").strip():
  293. tenant_no_for_status = (d.tenant_no or "").strip()
  294. for item in items:
  295. if item.get("orderSource") in ("temporary", "outbound"):
  296. item.setdefault("receptionSource", "")
  297. item.setdefault("receptionSourceLabel", "")
  298. continue
  299. rec = rec_map.get(item.get("receptionId") or "")
  300. if not rec:
  301. continue
  302. item["receptionTime"] = _fmt_dt(rec.reception_time)
  303. item["handlerName"] = rec.handler_name or ""
  304. item["sampleTypeName"] = _sample_type_name_for_reception(
  305. db,
  306. rec,
  307. type_map,
  308. protocol_samples=protocol_samples,
  309. donation_samples=donation_samples,
  310. )
  311. source = item.get("receptionSource") or (rec.reception_source if rec else "cell_storage")
  312. if source == "research_donation":
  313. _did = rec.research_donation_id if rec else ""
  314. _st = donation_status_map.get(_did or "", "")
  315. _vt = donation_void_type_map.get(_did or "", "")
  316. item["protocolStatus"] = _st
  317. item["voidType"] = _vt
  318. from app.core.protocol_status_display import format_protocol_status_label
  319. item["protocolStatusLabel"] = format_protocol_status_label(
  320. _st, _vt, kind="donation", db=db, tenant_no=tenant_no_for_status or None
  321. )
  322. else:
  323. _pid = rec.collection_protocol_id if rec else ""
  324. _st = proto_status_map.get(_pid or "", "")
  325. _vt = proto_void_type_map.get(_pid or "", "")
  326. item["protocolStatus"] = _st
  327. item["voidType"] = _vt
  328. from app.core.protocol_status_display import format_protocol_status_label
  329. item["protocolStatusLabel"] = format_protocol_status_label(
  330. _st, _vt, kind="protocol", db=db, tenant_no=tenant_no_for_status or None
  331. )
  332. apply_special_to_item_from_samples(
  333. item,
  334. source=source,
  335. protocol_sample_id=rec.protocol_sample_id or "",
  336. donation_sample_id=rec.donation_sample_id or "",
  337. protocol_samples=protocol_samples,
  338. donation_samples=donation_samples,
  339. )
  340. return items
  341. def _task_row_to_dict(
  342. t: BizQcTask,
  343. wo: BizQcWorkOrder,
  344. db: Session,
  345. tenant_no: Optional[str],
  346. ) -> Dict[str, Any]:
  347. out = _task_to_dict(t, wo.work_order_no or "", db, tenant_no)
  348. out.update(_task_work_order_context(wo, t))
  349. return out
  350. def create_task_for_line(
  351. db: Session,
  352. *,
  353. work_order: BizQcWorkOrder,
  354. task_type: str,
  355. source_line_id: str,
  356. item_code: str,
  357. item_name: str,
  358. tenant_no: str,
  359. user_id: Optional[str] = None,
  360. create_dept_id: str = "",
  361. seq: int = 1,
  362. company_id: Optional[str] = None,
  363. company_name: Optional[str] = None,
  364. fee_amount: Optional[Decimal] = None,
  365. order_source: str = "inbound",
  366. batch_id: Optional[str] = None,
  367. ) -> BizQcTask:
  368. obj = BizQcTask(
  369. id=generate_uuid(),
  370. work_order_id=work_order.id,
  371. batch_id=batch_id,
  372. order_source=order_source,
  373. task_no=_gen_task_no(db, work_order, seq),
  374. task_type=task_type,
  375. source_line_id=source_line_id,
  376. sample_code=getattr(work_order, "sample_code", None) or None,
  377. item_code=item_code,
  378. item_name=item_name,
  379. company_id=company_id,
  380. company_name=company_name,
  381. fee_amount=fee_amount,
  382. task_status="dzx",
  383. tenant_no=tenant_no,
  384. order_num=seq,
  385. create_by=user_id,
  386. create_dept_id=create_dept_id or "",
  387. create_time=datetime.now(),
  388. update_by=user_id,
  389. update_time=datetime.now(),
  390. del_flag=0,
  391. )
  392. db.add(obj)
  393. db.flush()
  394. return obj
  395. def list_tasks(
  396. db: Session,
  397. *,
  398. page: int = 1,
  399. page_size: int = 10,
  400. keyword: Optional[str] = None,
  401. task_type: Optional[str] = None,
  402. task_status: Optional[str] = None,
  403. work_order_id: Optional[str] = None,
  404. qc_biz_type: Optional[str] = None,
  405. tenant_no: Optional[str] = None,
  406. user_info=None,
  407. ) -> Tuple[List[Dict[str, Any]], int]:
  408. def _filtered():
  409. q0 = (
  410. db.query(BizQcTask, BizQcWorkOrder)
  411. .join(
  412. BizQcWorkOrder,
  413. and_(
  414. BizQcWorkOrder.id == BizQcTask.work_order_id,
  415. BizQcWorkOrder.del_flag == 0,
  416. ),
  417. )
  418. .filter(BizQcTask.del_flag == 0)
  419. )
  420. if tenant_no:
  421. q0 = q0.filter(BizQcTask.tenant_no == tenant_no)
  422. if work_order_id:
  423. q0 = q0.filter(BizQcTask.work_order_id == work_order_id)
  424. kw = (keyword or "").strip()
  425. if kw:
  426. like = f"%{kw}%"
  427. q0 = q0.filter(
  428. or_(
  429. BizQcTask.task_no.like(like),
  430. BizQcTask.item_name.like(like),
  431. BizQcWorkOrder.work_order_no.like(like),
  432. BizQcWorkOrder.sample_code.like(like),
  433. )
  434. )
  435. tt = (task_type or "").strip()
  436. if tt:
  437. q0 = q0.filter(BizQcTask.task_type == tt)
  438. st = (task_status or "").strip()
  439. if st:
  440. q0 = q0.filter(BizQcTask.task_status == st)
  441. bt = (qc_biz_type or "").strip()
  442. if bt:
  443. if bt == "temporary":
  444. q0 = q0.filter(
  445. or_(
  446. BizQcWorkOrder.qc_biz_type == "temporary",
  447. BizQcWorkOrder.work_order_type == "temporary",
  448. )
  449. )
  450. else:
  451. q0 = q0.filter(BizQcWorkOrder.qc_biz_type == bt)
  452. if user_info:
  453. from app.core.data_scope import apply_data_scope
  454. q0 = apply_data_scope(q0, BizQcTask, user_info, db, tenant_no)
  455. return q0
  456. total = _filtered().order_by(None).with_entities(func.count(BizQcTask.id)).scalar() or 0
  457. page = max(1, page)
  458. page_size = min(max(1, page_size), 100)
  459. rows = (
  460. _filtered()
  461. .order_by(
  462. BizQcTask.create_time.desc(),
  463. BizQcWorkOrder.work_order_no.desc(),
  464. BizQcTask.task_no.asc(),
  465. )
  466. .offset((page - 1) * page_size)
  467. .limit(page_size)
  468. .all()
  469. )
  470. items = [_task_row_to_dict(t, wo, db, tenant_no) for t, wo in rows]
  471. items = _enrich_tasks_reception_fields(db, items)
  472. return assignee_svc.enrich_tasks_with_assignees(db, items, tenant_no), int(total)
  473. def list_mine_tasks(
  474. db: Session,
  475. *,
  476. assignee_user_id: str,
  477. page: int = 1,
  478. page_size: int = 10,
  479. keyword: Optional[str] = None,
  480. task_type: Optional[str] = None,
  481. task_status: Optional[str] = None,
  482. qc_biz_type: Optional[str] = None,
  483. reception_source: Optional[str] = None,
  484. biz_type: Optional[str] = None,
  485. tenant_no: Optional[str] = None,
  486. ) -> Tuple[List[Dict[str, Any]], int]:
  487. def _filtered():
  488. q0 = (
  489. db.query(BizQcTask, BizQcWorkOrder)
  490. .join(
  491. BizQcWorkOrder,
  492. and_(
  493. BizQcWorkOrder.id == BizQcTask.work_order_id,
  494. BizQcWorkOrder.del_flag == 0,
  495. ),
  496. )
  497. .join(
  498. BizQcTaskAssignee,
  499. and_(
  500. eq_unicode(BizQcTaskAssignee.task_id, BizQcTask.id),
  501. unicode_ci(BizQcTaskAssignee.user_id) == assignee_user_id,
  502. BizQcTaskAssignee.del_flag == 0,
  503. ),
  504. )
  505. .filter(BizQcTask.del_flag == 0)
  506. )
  507. if tenant_no:
  508. q0 = q0.filter(BizQcTask.tenant_no == tenant_no)
  509. kw = (keyword or "").strip()
  510. if kw:
  511. like = f"%{kw}%"
  512. q0 = q0.filter(
  513. or_(
  514. BizQcTask.task_no.like(like),
  515. BizQcTask.item_name.like(like),
  516. BizQcWorkOrder.work_order_no.like(like),
  517. BizQcWorkOrder.sample_code.like(like),
  518. )
  519. )
  520. tt = (task_type or "").strip()
  521. if tt:
  522. q0 = q0.filter(BizQcTask.task_type == tt)
  523. st = (task_status or "").strip()
  524. if st:
  525. q0 = q0.filter(BizQcTask.task_status == st)
  526. bt = (qc_biz_type or "").strip()
  527. if bt:
  528. if bt == "temporary":
  529. q0 = q0.filter(
  530. or_(
  531. BizQcWorkOrder.qc_biz_type == "temporary",
  532. BizQcWorkOrder.work_order_type == "temporary",
  533. )
  534. )
  535. else:
  536. q0 = q0.filter(BizQcWorkOrder.qc_biz_type == bt)
  537. rs = (reception_source or "").strip()
  538. if rs:
  539. q0 = q0.filter(BizQcWorkOrder.reception_source == rs)
  540. bt_out = (biz_type or "").strip()
  541. if bt_out:
  542. q0 = q0.filter(BizQcWorkOrder.biz_type == bt_out)
  543. return q0
  544. total = _filtered().order_by(None).with_entities(func.count(BizQcTask.id)).scalar() or 0
  545. page = max(1, page)
  546. page_size = min(max(1, page_size), 100)
  547. rows = (
  548. _filtered()
  549. .order_by(
  550. BizQcTask.create_time.desc(),
  551. BizQcWorkOrder.work_order_no.desc(),
  552. BizQcTask.task_no.asc(),
  553. )
  554. .offset((page - 1) * page_size)
  555. .limit(page_size)
  556. .all()
  557. )
  558. items = [_task_row_to_dict(t, wo, db, tenant_no) for t, wo in rows]
  559. items = _enrich_tasks_reception_fields(db, items)
  560. items = _enrich_mine_tasks_with_work_order_summaries(db, rows, items, tenant_no)
  561. return assignee_svc.enrich_tasks_with_assignees(db, items, tenant_no), int(total)
  562. def assert_task_assignee(
  563. db: Session, task_id: str, user_id: str, tenant_no: str
  564. ) -> None:
  565. if not assignee_svc.is_user_assignee(db, task_id, user_id, tenant_no):
  566. raise PermissionError("您不是该任务的负责人,无权操作")
  567. def list_tasks_by_work_order(db: Session, work_order_id: str, tenant_no: str) -> List[Dict[str, Any]]:
  568. wo_id = (work_order_id or "").strip()
  569. if not wo_id:
  570. return []
  571. return list_tasks_by_work_order_ids(db, [wo_id], tenant_no).get(wo_id, [])
  572. def list_tasks_by_work_order_ids(
  573. db: Session, work_order_ids: List[str], tenant_no: str
  574. ) -> Dict[str, List[Dict[str, Any]]]:
  575. ids = list({(x or "").strip() for x in work_order_ids if (x or "").strip()})
  576. if not ids or not tenant_no:
  577. return {}
  578. ids = ids[:100]
  579. rows = (
  580. db.query(BizQcTask, BizQcWorkOrder)
  581. .join(BizQcWorkOrder, BizQcWorkOrder.id == BizQcTask.work_order_id)
  582. .filter(
  583. BizQcTask.work_order_id.in_(ids),
  584. BizQcTask.tenant_no == tenant_no,
  585. BizQcTask.del_flag == 0,
  586. BizQcWorkOrder.del_flag == 0,
  587. )
  588. .order_by(BizQcTask.work_order_id.asc(), BizQcTask.order_num.asc())
  589. .all()
  590. )
  591. items = [_task_row_to_dict(t, wo, db, tenant_no) for t, wo in rows]
  592. items = _enrich_tasks_reception_fields(db, items)
  593. items = assignee_svc.enrich_tasks_with_assignees(db, items, tenant_no)
  594. grouped: Dict[str, List[Dict[str, Any]]] = {wo_id: [] for wo_id in ids}
  595. for item in items:
  596. wo_id = (item.get("workOrderId") or "").strip()
  597. if wo_id in grouped:
  598. grouped[wo_id].append(item)
  599. return grouped
  600. def get_task(
  601. db: Session, row_id: str, tenant_no: Optional[str] = None
  602. ) -> Optional[BizQcTask]:
  603. q = db.query(BizQcTask).filter(BizQcTask.id == row_id, BizQcTask.del_flag == 0)
  604. if tenant_no:
  605. q = q.filter(BizQcTask.tenant_no == tenant_no)
  606. return q.first()
  607. def get_task_detail(db: Session, row_id: str, tenant_no: str) -> Dict[str, Any]:
  608. row = (
  609. db.query(BizQcTask, BizQcWorkOrder)
  610. .join(BizQcWorkOrder, BizQcWorkOrder.id == BizQcTask.work_order_id)
  611. .filter(
  612. BizQcTask.id == row_id,
  613. BizQcTask.tenant_no == tenant_no,
  614. BizQcTask.del_flag == 0,
  615. BizQcWorkOrder.del_flag == 0,
  616. )
  617. .first()
  618. )
  619. if not row:
  620. raise ValueError("任务不存在")
  621. t, wo = row
  622. out = _task_row_to_dict(t, wo, db, tenant_no)
  623. out = _enrich_tasks_reception_fields(db, [out])[0]
  624. out = assignee_svc.enrich_tasks_with_assignees(db, [out], tenant_no)[0]
  625. from app.crud.qc_work_order import _enrich_work_order_qc_targets
  626. out = _enrich_work_order_qc_targets(db, tenant_no, [out])[0]
  627. if (out.get("qcBizType") or "") != "temporary":
  628. resolved_reception_id = _resolve_reception_id_for_work_order_detail(db, wo, tenant_no)
  629. reception_detail = (
  630. sample_reception_svc.get_reception_by_id(db, resolved_reception_id, tenant_no)
  631. if resolved_reception_id
  632. else None
  633. )
  634. if reception_detail:
  635. out["reception"] = reception_detail
  636. return out
  637. def start_task(db: Session, row_id: str, *, tenant_no: str, user_id: Optional[str]) -> BizQcTask:
  638. t = get_task(db, row_id, tenant_no=tenant_no)
  639. if not t:
  640. raise ValueError("任务不存在")
  641. if t.task_type != "sysz":
  642. raise ValueError("仅实验室自检任务可开始")
  643. if t.task_status != "dzx":
  644. raise ValueError("当前状态不可开始")
  645. t.task_status = "jxz"
  646. t.update_by = user_id
  647. t.update_time = datetime.now()
  648. db.add(t)
  649. db.commit()
  650. db.refresh(t)
  651. return t
  652. def _set_task_remark(t: BizQcTask, remark: Optional[str]) -> None:
  653. t.remark = (remark or "").strip() or None
  654. def _require_dsfjc_attachments(t: BizQcTask, *, for_status: str = "ywc") -> None:
  655. """第三方检测且任务状态为已完成时,须至少有一个检测材料附件。"""
  656. if (t.task_type or "").strip() != "dsfjc":
  657. return
  658. if (for_status or "").strip() != "ywc":
  659. return
  660. if not (t.third_party_attachments or "").strip():
  661. raise ValueError("第三方检测须上传检测材料附件")
  662. def complete_task(
  663. db: Session,
  664. row_id: str,
  665. *,
  666. tenant_no: str,
  667. user_id: Optional[str],
  668. conclusion: Optional[str] = None,
  669. remark: Optional[str] = None,
  670. commit: bool = True,
  671. ) -> BizQcTask:
  672. t = get_task(db, row_id, tenant_no=tenant_no)
  673. if not t:
  674. raise ValueError("任务不存在")
  675. concl = (conclusion or "").strip() if conclusion is not None else ""
  676. if concl and concl not in ("tg", "btg"):
  677. raise ValueError("检测结论无效")
  678. if t.task_type == "sysz":
  679. if t.task_status not in ("dzx", "jxz"):
  680. raise ValueError("当前状态不可完成")
  681. elif t.task_type != "dsfjc":
  682. raise ValueError("任务类型无效")
  683. _require_dsfjc_attachments(t)
  684. t.task_status = "ywc"
  685. if concl:
  686. t.conclusion = concl
  687. _set_task_remark(t, remark)
  688. t.completed_time = datetime.now()
  689. t.completed_by = user_id
  690. t.update_by = user_id
  691. t.update_time = datetime.now()
  692. db.add(t)
  693. if commit:
  694. db.commit()
  695. db.refresh(t)
  696. from app.crud import qc_work_order as wo_svc
  697. wo_svc.try_complete_work_order(db, t.work_order_id, tenant_no)
  698. else:
  699. db.flush()
  700. return t
  701. _QC_TASK_RESULT_STATUSES = frozenset({"dzx", "jxz", "ywc"})
  702. def resolve_task_result_conclusion(
  703. conclusion: Optional[str],
  704. target_status: str,
  705. ) -> str:
  706. """Normalize conclusion for apply_task_result.
  707. Returns '' | 'tg' | 'btg'. Empty allowed only when target is dzx/jxz.
  708. """
  709. concl = (conclusion or "").strip()
  710. if concl and concl not in ("tg", "btg"):
  711. raise ValueError("检测结论无效")
  712. if target_status == "ywc" and concl not in ("tg", "btg"):
  713. raise ValueError("请选择检测结论(通过/不通过)")
  714. return concl
  715. def apply_task_result(
  716. db: Session,
  717. row_id: str,
  718. *,
  719. tenant_no: str,
  720. user_id: Optional[str],
  721. conclusion: Optional[str] = None,
  722. remark: Optional[str] = None,
  723. material_usages: Optional[List[Dict[str, Any]]] = None,
  724. task_status: Optional[str] = None,
  725. commit: bool = True,
  726. ) -> BizQcTask:
  727. """保存检测结果与结论,并按指定任务状态落库(默认已完成)。"""
  728. from app.crud import qc_material_usage as mu_svc
  729. from app.crud import qc_work_order as wo_svc
  730. t = get_task(db, row_id, tenant_no=tenant_no)
  731. if not t:
  732. raise ValueError("任务不存在")
  733. target = (task_status or "").strip() or "ywc"
  734. if target not in _QC_TASK_RESULT_STATUSES:
  735. raise ValueError("任务状态无效")
  736. concl = resolve_task_result_conclusion(conclusion, target)
  737. _require_dsfjc_attachments(t, for_status=target)
  738. if material_usages is not None:
  739. mu_svc.deduct_for_task(
  740. db,
  741. row_id,
  742. task_no=t.task_no or "",
  743. material_usages=material_usages,
  744. tenant_no=tenant_no,
  745. user_id=user_id,
  746. )
  747. was_done = (t.task_status or "").strip() == "ywc"
  748. t.conclusion = concl or None
  749. _set_task_remark(t, remark if remark is not None else "")
  750. t.task_status = target
  751. t.update_by = user_id
  752. t.update_time = datetime.now()
  753. if target == "ywc":
  754. t.completed_time = datetime.now()
  755. t.completed_by = user_id
  756. else:
  757. t.completed_time = None
  758. t.completed_by = None
  759. db.add(t)
  760. if commit:
  761. db.commit()
  762. db.refresh(t)
  763. if target == "ywc":
  764. wo_svc.try_complete_work_order(db, t.work_order_id, tenant_no)
  765. elif was_done:
  766. wo_svc.try_reopen_work_order(db, t.work_order_id, tenant_no)
  767. else:
  768. db.flush()
  769. return t
  770. def update_task_attachments(
  771. db: Session,
  772. row_id: str,
  773. *,
  774. attachments: str,
  775. tenant_no: str,
  776. user_id: Optional[str],
  777. commit: bool = True,
  778. ) -> BizQcTask:
  779. t = get_task(db, row_id, tenant_no=tenant_no)
  780. if not t:
  781. raise ValueError("任务不存在")
  782. t.third_party_attachments = (attachments or "").strip() or None
  783. if (
  784. t.task_type == "dsfjc"
  785. and t.task_status == "dzx"
  786. and t.third_party_attachments
  787. ):
  788. t.task_status = "jxz"
  789. t.update_by = user_id
  790. t.update_time = datetime.now()
  791. db.add(t)
  792. if commit:
  793. db.commit()
  794. db.refresh(t)
  795. else:
  796. db.flush()
  797. return t
  798. def create_qc_task_manual(
  799. db: Session,
  800. *,
  801. tenant_no: str,
  802. user_id: Optional[str] = None,
  803. create_dept_id: str = "",
  804. task_type: str = "sysz",
  805. item_code: str = "",
  806. item_name: str = "",
  807. company_id: Optional[str] = None,
  808. company_name: Optional[str] = None,
  809. fee_amount: Optional[float] = None,
  810. sample_code: str = "",
  811. work_order_id: str = "",
  812. remark: str = "",
  813. ) -> BizQcTask:
  814. """手动创建质检任务(可独立于工单)"""
  815. # 生成任务号:ZJRW + 日期 + 序号
  816. day = datetime.now().strftime("%Y%m%d")
  817. like = f"ZJRW{day}%"
  818. cnt = db.query(BizQcTask).filter(
  819. BizQcTask.tenant_no == tenant_no,
  820. BizQcTask.task_no.like(like),
  821. ).count()
  822. task_no = f"ZJRW{day}{cnt + 1:04d}"
  823. task = BizQcTask(
  824. id=generate_uuid(),
  825. work_order_id=work_order_id or "",
  826. task_no=task_no,
  827. task_type=task_type,
  828. source_line_id="",
  829. sample_code=sample_code,
  830. item_code=item_code,
  831. item_name=item_name,
  832. company_id=company_id,
  833. company_name=company_name,
  834. fee_amount=fee_amount,
  835. task_status="dzx",
  836. tenant_no=tenant_no,
  837. remark=remark,
  838. create_by=user_id,
  839. create_dept_id=create_dept_id,
  840. )
  841. db.add(task)
  842. db.flush()
  843. return task
  844. def _soft_delete_source_line_for_task(
  845. db: Session,
  846. task: BizQcTask,
  847. *,
  848. tenant_no: str,
  849. user_id: Optional[str],
  850. now: datetime,
  851. ) -> None:
  852. """删除任务时同步软删批次下对应 lab/third 明细行,避免配置弹窗仍勾选该项目。"""
  853. sid = (task.source_line_id or "").strip()
  854. if not sid:
  855. return
  856. task_type = (task.task_type or "").strip()
  857. line = None
  858. if task_type == "sysz":
  859. line = (
  860. db.query(BizQcWorkOrderLabItem)
  861. .filter(
  862. BizQcWorkOrderLabItem.id == sid,
  863. BizQcWorkOrderLabItem.tenant_no == tenant_no,
  864. BizQcWorkOrderLabItem.del_flag == 0,
  865. )
  866. .first()
  867. )
  868. elif task_type == "dsfjc":
  869. line = (
  870. db.query(BizQcWorkOrderThirdItem)
  871. .filter(
  872. BizQcWorkOrderThirdItem.id == sid,
  873. BizQcWorkOrderThirdItem.tenant_no == tenant_no,
  874. BizQcWorkOrderThirdItem.del_flag == 0,
  875. )
  876. .first()
  877. )
  878. else:
  879. line = (
  880. db.query(BizQcWorkOrderLabItem)
  881. .filter(
  882. BizQcWorkOrderLabItem.id == sid,
  883. BizQcWorkOrderLabItem.tenant_no == tenant_no,
  884. BizQcWorkOrderLabItem.del_flag == 0,
  885. )
  886. .first()
  887. )
  888. if not line:
  889. line = (
  890. db.query(BizQcWorkOrderThirdItem)
  891. .filter(
  892. BizQcWorkOrderThirdItem.id == sid,
  893. BizQcWorkOrderThirdItem.tenant_no == tenant_no,
  894. BizQcWorkOrderThirdItem.del_flag == 0,
  895. )
  896. .first()
  897. )
  898. if not line:
  899. return
  900. line.del_flag = 1
  901. line.update_by = user_id
  902. line.update_time = now
  903. db.add(line)
  904. def _refresh_work_order_item_flags(
  905. db: Session,
  906. wo: BizQcWorkOrder,
  907. *,
  908. tenant_no: str,
  909. user_id: Optional[str],
  910. now: datetime,
  911. ) -> None:
  912. """按未删除明细行回写工单 has_lab / has_third_party / 第三方费用合计。"""
  913. lab_count = (
  914. db.query(BizQcWorkOrderLabItem)
  915. .filter(
  916. BizQcWorkOrderLabItem.work_order_id == wo.id,
  917. BizQcWorkOrderLabItem.tenant_no == tenant_no,
  918. BizQcWorkOrderLabItem.del_flag == 0,
  919. )
  920. .count()
  921. )
  922. third_rows = (
  923. db.query(BizQcWorkOrderThirdItem)
  924. .filter(
  925. BizQcWorkOrderThirdItem.work_order_id == wo.id,
  926. BizQcWorkOrderThirdItem.tenant_no == tenant_no,
  927. BizQcWorkOrderThirdItem.del_flag == 0,
  928. )
  929. .all()
  930. )
  931. wo.has_lab = "1" if lab_count else "0"
  932. wo.has_third_party = "1" if third_rows else "0"
  933. if third_rows:
  934. fee_total = sum((r.fee_amount or Decimal("0")) for r in third_rows)
  935. wo.third_party_fee_total = fee_total
  936. else:
  937. wo.third_party_fee_total = None
  938. wo.update_by = user_id
  939. wo.update_time = now
  940. db.add(wo)
  941. def delete_qc_task(db: Session, task_id: str, tenant_no: str, user_id: Optional[str]) -> None:
  942. """软删除质检任务、检测结果,并同步解除批次中的项目关联。"""
  943. task = db.query(BizQcTask).filter(
  944. and_(
  945. BizQcTask.id == task_id,
  946. BizQcTask.tenant_no == tenant_no,
  947. BizQcTask.del_flag == 0,
  948. )
  949. ).first()
  950. if not task:
  951. raise ValueError("任务不存在")
  952. work_order_id = task.work_order_id
  953. now = datetime.now()
  954. # 软删除任务结果
  955. from app.models.qc_param import BizQcTaskResult
  956. results = db.query(BizQcTaskResult).filter(
  957. and_(BizQcTaskResult.task_id == task_id, BizQcTaskResult.del_flag == 0)
  958. ).all()
  959. for r in results:
  960. r.del_flag = 1
  961. r.update_by = user_id
  962. r.update_time = now
  963. # 同步软删批次明细行(配置弹窗勾选来源)
  964. _soft_delete_source_line_for_task(
  965. db, task, tenant_no=tenant_no, user_id=user_id, now=now
  966. )
  967. # 软删除任务负责人
  968. assignee_svc.soft_delete_assignees_by_task_ids(db, [task_id], tenant_no)
  969. # 软删除任务本体
  970. task.del_flag = 1
  971. task.update_by = user_id
  972. task.update_time = now
  973. db.flush()
  974. from app.crud.qc_work_order import get_work_order, try_complete_work_order
  975. wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
  976. if wo:
  977. _refresh_work_order_item_flags(
  978. db, wo, tenant_no=tenant_no, user_id=user_id, now=now
  979. )
  980. if (wo.work_order_status or "").strip() == "ywc":
  981. pending = (
  982. db.query(BizQcTask)
  983. .filter(
  984. BizQcTask.work_order_id == work_order_id,
  985. BizQcTask.tenant_no == tenant_no,
  986. BizQcTask.del_flag == 0,
  987. BizQcTask.task_status != "ywc",
  988. )
  989. .count()
  990. )
  991. if pending:
  992. wo.work_order_status = "ypz"
  993. wo.update_by = user_id
  994. wo.update_time = now
  995. db.add(wo)
  996. db.commit()
  997. try_complete_work_order(db, work_order_id, tenant_no)