qc_work_order.py 129 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712
  1. """质检工单 CRUD"""
  2. from __future__ import annotations
  3. from collections import defaultdict
  4. from datetime import datetime
  5. from decimal import Decimal
  6. from typing import Any, Dict, List, Optional, Tuple
  7. from sqlalchemy import and_, func, or_
  8. from sqlalchemy.orm import Session
  9. from app.core.db_collation import eq_unicode, like_unicode, unicode_ci
  10. from app.core.utils import generate_uuid
  11. from app.crud import qc_lab_test_item as lab_item_svc
  12. from app.crud import qc_task as task_svc
  13. from app.crud import qc_task_assignee as assignee_svc
  14. from app.crud import qc_third_party_item as third_item_svc
  15. from app.crud.qc_third_party_company import get_company
  16. from app.crud.qc_inbound_lab_storage_sort import (
  17. inbound_lab_storage_order_by_clauses,
  18. uses_inbound_lab_storage_list_sort,
  19. )
  20. from app.crud.collection_protocol import urls_to_attachment_list
  21. from app.utils.sample_special import apply_special_to_item_from_samples
  22. from app.crud import sample_reception as sample_reception_svc
  23. from app.crud.temp_qc_target import resolve_temp_qc_target
  24. from app.crud.sample_reception import _donation_sample_id_col
  25. from app.crud import sample_reception_check_result as check_result_svc
  26. from app.crud.cell_product import _enrich_product_dicts as _cp_enrich, _row_to_dict as _cp_row_to_dict, get_cell_product
  27. from app.services.qc_work_order_fee_summary import enrich_work_order_fee_summaries
  28. from app.services.qc_work_order_conclusion import (
  29. build_qc_conclusion_fields,
  30. work_order_qc_conclusion_label,
  31. )
  32. from app.models.models import (
  33. BizCellProduct,
  34. BizCellSampleType,
  35. BizCollectionProtocol,
  36. BizPilotApplication,
  37. BizPilotInfusionPlan,
  38. BizPilotInfusionSchedule,
  39. BizPilotInfusionScheduleProduct,
  40. BizPilotPatient,
  41. BizProtocolPartyCustomer,
  42. BizProtocolSample,
  43. BizProductionTask,
  44. BizProductionTaskStep,
  45. BizProductionTaskStepOutput,
  46. BizProductionWorkOrder,
  47. BizOutputType,
  48. BizQcLabTestItem,
  49. BizQcTask,
  50. BizQcTaskAssignee,
  51. BizQcThirdPartyItem,
  52. BizQcWorkOrder,
  53. BizQcWorkOrderLabItem,
  54. BizQcWorkOrderThirdItem,
  55. BizSampleReception,
  56. SysUser,
  57. )
  58. from app.models.qc_param import BizQcTaskResult
  59. from app.models.research_donation import (
  60. BizResearchDonation,
  61. BizResearchDonationParty,
  62. BizResearchDonationSample,
  63. )
  64. _RECEPTION_SOURCE_LABELS = {
  65. "cell_storage": "细胞存储",
  66. "research_donation": "科研捐赠",
  67. "outbound": "出库",
  68. }
  69. _QC_BIZ_TYPE_LABELS = {
  70. "inbound": "入库质检",
  71. "outbound": "出库质检",
  72. "temporary": "临时质检",
  73. }
  74. _TRIGGER_SOURCE_LABELS = {
  75. "reception": "样本接收",
  76. "schedule": "计划质检",
  77. "production": "生产过程",
  78. "manual": "手动创建",
  79. }
  80. _ACCEPT_CONCLUSION_LABELS = {"js": "接收", "bj": "拒收"}
  81. _WORK_ORDER_STATUS_LABELS = {"dpz": "待配置", "ypz": "已配置", "ywc": "已完成", "yzf": "已作废"}
  82. _VALID_QC_WORK_ORDER_STATUSES = frozenset(_WORK_ORDER_STATUS_LABELS.keys())
  83. _VALID_QC_WORK_ORDER_CONCLUSIONS = frozenset({"tg", "btg"})
  84. _VALID_QC_DISPATCH_OUTPUT_TYPE_NAME = "有效物"
  85. _TASK_TYPE_LABELS = {"sysz": "实验室自检", "dsfjc": "第三方检测"}
  86. _TASK_STATUS_LABELS = {"dzx": "待执行", "jxz": "进行中", "ywc": "已完成"}
  87. _PROTOCOL_STATUS_LABELS = {
  88. "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zky": "转科研"
  89. }
  90. _DONATION_STATUS_LABELS = {
  91. "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zckc": "转细胞存储"
  92. }
  93. _CONCLUSION_LABELS = {"tg": "通过", "btg": "不通过"}
  94. _OUTBOUND_BIZ_TYPE_LABELS = {
  95. "pilot_service": "中试服务",
  96. "clinical_application": "临床应用",
  97. "basic_experiment": "基础实验",
  98. }
  99. _OUTBOUND_BIZ_TYPES = tuple(_OUTBOUND_BIZ_TYPE_LABELS.keys())
  100. def resolve_outbound_biz_type_for_schedule(
  101. db: Session, schedule_id: str, tenant_no: str
  102. ) -> str:
  103. """按输注计划追溯申请单,得到出库生产/质检工单应使用的 biz_type。"""
  104. if not schedule_id:
  105. return "pilot_service"
  106. sched = (
  107. db.query(BizPilotInfusionSchedule)
  108. .filter(
  109. BizPilotInfusionSchedule.id == schedule_id,
  110. BizPilotInfusionSchedule.tenant_no == tenant_no,
  111. BizPilotInfusionSchedule.del_flag == 0,
  112. )
  113. .first()
  114. )
  115. if not sched or not sched.infusion_plan_id:
  116. return "pilot_service"
  117. plan = (
  118. db.query(BizPilotInfusionPlan)
  119. .filter(
  120. BizPilotInfusionPlan.id == sched.infusion_plan_id,
  121. BizPilotInfusionPlan.tenant_no == tenant_no,
  122. BizPilotInfusionPlan.del_flag == 0,
  123. )
  124. .first()
  125. )
  126. if not plan or not plan.pilot_application_id:
  127. return "pilot_service"
  128. app = (
  129. db.query(BizPilotApplication)
  130. .filter(
  131. BizPilotApplication.id == plan.pilot_application_id,
  132. BizPilotApplication.tenant_no == tenant_no,
  133. BizPilotApplication.del_flag == 0,
  134. )
  135. .first()
  136. )
  137. bt = ((app.biz_type if app else None) or "").strip()
  138. return bt if bt in _OUTBOUND_BIZ_TYPE_LABELS else "pilot_service"
  139. def _dec(v: Any) -> Optional[float]:
  140. if v is None:
  141. return None
  142. return float(v)
  143. def _fmt_dt(t: Optional[datetime]) -> str:
  144. return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
  145. def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
  146. val = (value or "").strip()
  147. if len(val) == 10:
  148. return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
  149. return val
  150. def _reception_source_label(source: Optional[str]) -> str:
  151. key = (source or "").strip() or "cell_storage"
  152. return _RECEPTION_SOURCE_LABELS.get(key, key)
  153. def _qc_biz_type_label(biz_type: Optional[str]) -> str:
  154. key = (biz_type or "").strip() or "inbound"
  155. return _QC_BIZ_TYPE_LABELS.get(key, key)
  156. def _work_order_qc_biz_type(wo: BizQcWorkOrder) -> str:
  157. wo_type = getattr(wo, "work_order_type", None) or "inbound"
  158. if wo_type == "temporary":
  159. return "temporary"
  160. return getattr(wo, "qc_biz_type", None) or "inbound"
  161. def _validate_cell_product_id(
  162. db: Session, cell_product_id: Optional[str], tenant_no: str
  163. ) -> str:
  164. cp_id = (cell_product_id or "").strip()
  165. if not cp_id:
  166. raise ValueError("请选择质检对象(细胞产品)")
  167. cp = get_cell_product(db, cp_id, tenant_no)
  168. if not cp:
  169. raise ValueError("细胞产品不存在")
  170. return cp_id
  171. def _enrich_work_order_cell_products(
  172. db: Session, tenant_no: str, items: List[Dict[str, Any]]
  173. ) -> List[Dict[str, Any]]:
  174. if not items:
  175. return items
  176. cp_ids = list({x.get("cellProductId") for x in items if x.get("cellProductId")})
  177. if not cp_ids:
  178. return items
  179. products = (
  180. db.query(BizCellProduct)
  181. .filter(
  182. BizCellProduct.id.in_(cp_ids),
  183. BizCellProduct.tenant_no == tenant_no,
  184. BizCellProduct.del_flag == 0,
  185. )
  186. .all()
  187. )
  188. product_dicts = _cp_enrich(db, tenant_no, [_cp_row_to_dict(p) for p in products])
  189. product_map = {d["id"]: d for d in product_dicts}
  190. for item in items:
  191. cp = product_map.get(item.get("cellProductId") or "")
  192. item["cellProductCode"] = cp.get("productCode", "") if cp else ""
  193. item["cellProductName"] = cp.get("productName", "") if cp else ""
  194. item["cellProductTypeName"] = cp.get("productTypeName", "") if cp else ""
  195. item["cellProductPassage"] = cp.get("passage", "") if cp else ""
  196. item["cellProductSpecName"] = cp.get("specName", "") if cp else ""
  197. return items
  198. def _enrich_work_order_step_outputs(
  199. db: Session, tenant_no: str, items: List[Dict[str, Any]]
  200. ) -> List[Dict[str, Any]]:
  201. """步骤产出物触发的质检:无细胞产品时用工单条码/名称展示质检对象。"""
  202. if not items:
  203. return items
  204. step_output_ids = list(
  205. {
  206. x.get("productionStepOutputId")
  207. for x in items
  208. if (x.get("productionStepOutputId") or "").strip()
  209. and not (x.get("cellProductId") or "").strip()
  210. }
  211. )
  212. if not step_output_ids:
  213. return items
  214. rows = (
  215. db.query(BizProductionTaskStepOutput)
  216. .filter(
  217. BizProductionTaskStepOutput.id.in_(step_output_ids),
  218. BizProductionTaskStepOutput.tenant_no == tenant_no,
  219. BizProductionTaskStepOutput.del_flag == 0,
  220. )
  221. .all()
  222. )
  223. row_map = {r.id: r for r in rows}
  224. for item in items:
  225. if (item.get("cellProductId") or "").strip():
  226. continue
  227. so = row_map.get(item.get("productionStepOutputId") or "")
  228. if not so:
  229. continue
  230. if not (item.get("cellProductCode") or "").strip():
  231. item["cellProductCode"] = (so.output_barcode or "").strip()
  232. if not (item.get("cellProductName") or "").strip():
  233. item["cellProductName"] = (so.output_name or "").strip()
  234. return items
  235. def _enrich_work_order_qc_targets(
  236. db: Session, tenant_no: str, items: List[Dict[str, Any]]
  237. ) -> List[Dict[str, Any]]:
  238. items = _enrich_work_order_cell_products(db, tenant_no, items)
  239. return _enrich_work_order_step_outputs(db, tenant_no, items)
  240. def _enrich_outbound_work_orders(
  241. db: Session, tenant_no: str, items: List[Dict[str, Any]]
  242. ) -> List[Dict[str, Any]]:
  243. """为 outbound 类型工单批量补充申请单/用户姓名/疗程序号/输注计划信息。"""
  244. from app.crud.pilot_infusion_schedule_mgmt import (
  245. _get_plan_schedule_course_meta,
  246. _resolve_course_display,
  247. )
  248. outbound = [x for x in items if x.get("workOrderType") == "outbound"]
  249. if not outbound:
  250. return items
  251. schedule_ids = list({x.get("infusionScheduleId") for x in outbound if x.get("infusionScheduleId")})
  252. if not schedule_ids:
  253. return items
  254. products_map: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
  255. for product in (
  256. db.query(BizPilotInfusionScheduleProduct)
  257. .filter(
  258. BizPilotInfusionScheduleProduct.infusion_schedule_id.in_(schedule_ids),
  259. BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
  260. BizPilotInfusionScheduleProduct.del_flag == 0,
  261. )
  262. .order_by(BizPilotInfusionScheduleProduct.order_num.asc())
  263. .all()
  264. ):
  265. products_map[product.infusion_schedule_id].append(
  266. {
  267. "projectCode": product.project_code or "",
  268. "projectName": product.project_name or "",
  269. "cellTypeName": product.cell_type_name or "",
  270. "specText": product.spec_text or "",
  271. "agreementPrice": _dec(product.agreement_price),
  272. "actualPrice": _dec(product.actual_price),
  273. "quantity": product.quantity,
  274. "productStatus": product.product_status or "",
  275. }
  276. )
  277. # 查输注计划 → 输注方案 → 申请单 → 患者
  278. schedules = {
  279. s.id: s
  280. for s in db.query(BizPilotInfusionSchedule)
  281. .filter(
  282. BizPilotInfusionSchedule.id.in_(schedule_ids),
  283. BizPilotInfusionSchedule.tenant_no == tenant_no,
  284. BizPilotInfusionSchedule.del_flag == 0,
  285. )
  286. .all()
  287. }
  288. plan_ids = list({s.infusion_plan_id for s in schedules.values() if s.infusion_plan_id})
  289. plans = {
  290. p.id: p
  291. for p in db.query(BizPilotInfusionPlan)
  292. .filter(
  293. BizPilotInfusionPlan.id.in_(plan_ids),
  294. BizPilotInfusionPlan.tenant_no == tenant_no,
  295. BizPilotInfusionPlan.del_flag == 0,
  296. )
  297. .all()
  298. } if plan_ids else {}
  299. app_ids = list({p.pilot_application_id for p in plans.values() if p.pilot_application_id})
  300. apps = {
  301. a.id: a
  302. for a in db.query(BizPilotApplication)
  303. .filter(
  304. BizPilotApplication.id.in_(app_ids),
  305. BizPilotApplication.tenant_no == tenant_no,
  306. BizPilotApplication.del_flag == 0,
  307. )
  308. .all()
  309. } if app_ids else {}
  310. patient_map: Dict[str, BizPilotPatient] = {}
  311. if app_ids:
  312. patients = (
  313. db.query(BizPilotPatient)
  314. .filter(
  315. BizPilotPatient.pilot_application_id.in_(app_ids),
  316. BizPilotPatient.tenant_no == tenant_no,
  317. BizPilotPatient.del_flag == 0,
  318. )
  319. .all()
  320. )
  321. patient_map = {p.pilot_application_id: p for p in patients}
  322. plan_course_meta = _get_plan_schedule_course_meta(db, plan_ids, tenant_no)
  323. for item in outbound:
  324. sid = item.get("infusionScheduleId") or ""
  325. sched = schedules.get(sid)
  326. if not sched:
  327. continue
  328. plan = plans.get(sched.infusion_plan_id or "")
  329. app = apps.get(plan.pilot_application_id or "") if plan else None
  330. patient = (
  331. patient_map.get(plan.pilot_application_id)
  332. if plan and plan.pilot_application_id
  333. else None
  334. )
  335. plan_meta = (
  336. plan_course_meta.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
  337. )
  338. course = _resolve_course_display(sched, plan, plan_meta)
  339. item["applicationNo"] = app.application_no or "" if app else ""
  340. item["applicationName"] = app.application_name or "" if app else ""
  341. item["channelName"] = app.channel_name or "" if app else ""
  342. if app:
  343. item["projectTitle"] = app.project_title or ""
  344. item["customerName"] = app.customer_name or ""
  345. item["patientName"] = patient.patient_name if patient else ""
  346. item["sortNo"] = course.get("sortNo")
  347. item["totalCourseCount"] = course.get("totalCourseCount")
  348. item["cellType"] = sched.cell_type or ""
  349. item["specText"] = sched.spec_text or ""
  350. item["infusionTime"] = _fmt_dt(sched.infusion_time)
  351. item["products"] = products_map.get(sid, [])
  352. return items
  353. def _outbound_qc_work_order_ids_by_keyword(
  354. db: Session, tenant_no: str, keyword: str
  355. ) -> List[str]:
  356. """出库质检工单关键词:按关联申请单号、申请单名称匹配。"""
  357. kw = (keyword or "").strip()
  358. if not kw:
  359. return []
  360. like = f"%{kw}%"
  361. rows = (
  362. db.query(BizQcWorkOrder.id)
  363. .join(
  364. BizPilotInfusionSchedule,
  365. eq_unicode(BizQcWorkOrder.infusion_schedule_id, BizPilotInfusionSchedule.id),
  366. )
  367. .join(
  368. BizPilotInfusionPlan,
  369. eq_unicode(
  370. BizPilotInfusionSchedule.infusion_plan_id,
  371. BizPilotInfusionPlan.id,
  372. ),
  373. )
  374. .join(
  375. BizPilotApplication,
  376. eq_unicode(
  377. BizPilotInfusionPlan.pilot_application_id,
  378. BizPilotApplication.id,
  379. ),
  380. )
  381. .filter(
  382. BizQcWorkOrder.del_flag == 0,
  383. BizQcWorkOrder.tenant_no == tenant_no,
  384. BizQcWorkOrder.work_order_type == "outbound",
  385. BizPilotInfusionSchedule.del_flag == 0,
  386. BizPilotInfusionPlan.del_flag == 0,
  387. BizPilotApplication.del_flag == 0,
  388. or_(
  389. like_unicode(BizPilotApplication.application_no, like),
  390. like_unicode(BizPilotApplication.application_name, like),
  391. like_unicode(BizPilotApplication.project_title, like),
  392. like_unicode(BizPilotApplication.customer_name, like),
  393. ),
  394. )
  395. .all()
  396. )
  397. return [r[0] for r in rows]
  398. def _inbound_qc_work_order_ids_by_mother_name(
  399. db: Session,
  400. tenant_no: str,
  401. mother_name: str,
  402. *,
  403. reception_source: Optional[str] = None,
  404. ) -> List[str]:
  405. """入库质检工单:按母亲姓名匹配(细胞存储协议 / 科研捐赠单)。"""
  406. mn = (mother_name or "").strip()
  407. if not mn or not tenant_no:
  408. return []
  409. like = f"%{mn}%"
  410. ids: set = set()
  411. rs = (reception_source or "").strip()
  412. if not rs or rs == "cell_storage":
  413. for (wid,) in (
  414. db.query(BizQcWorkOrder.id)
  415. .join(
  416. BizProtocolPartyCustomer,
  417. eq_unicode(
  418. BizProtocolPartyCustomer.collection_protocol_id,
  419. BizQcWorkOrder.collection_protocol_id,
  420. ),
  421. )
  422. .filter(
  423. BizQcWorkOrder.del_flag == 0,
  424. BizQcWorkOrder.tenant_no == tenant_no,
  425. BizQcWorkOrder.work_order_type == "inbound",
  426. or_(
  427. BizQcWorkOrder.reception_source == "cell_storage",
  428. BizQcWorkOrder.reception_source.is_(None),
  429. BizQcWorkOrder.reception_source == "",
  430. ),
  431. BizProtocolPartyCustomer.del_flag == 0,
  432. like_unicode(BizProtocolPartyCustomer.mother_name, like),
  433. )
  434. .all()
  435. ):
  436. ids.add(wid)
  437. if not rs or rs == "research_donation":
  438. for (wid,) in (
  439. db.query(BizQcWorkOrder.id)
  440. .join(
  441. BizResearchDonationParty,
  442. eq_unicode(
  443. BizResearchDonationParty.research_donation_id,
  444. BizQcWorkOrder.collection_protocol_id,
  445. ),
  446. )
  447. .filter(
  448. BizQcWorkOrder.del_flag == 0,
  449. BizQcWorkOrder.tenant_no == tenant_no,
  450. BizQcWorkOrder.work_order_type == "inbound",
  451. BizQcWorkOrder.reception_source == "research_donation",
  452. BizResearchDonationParty.del_flag == 0,
  453. like_unicode(BizResearchDonationParty.mother_name, like),
  454. )
  455. .all()
  456. ):
  457. ids.add(wid)
  458. return list(ids)
  459. def _accept_conclusion_label(v: Optional[str]) -> str:
  460. key = (v or "").strip()
  461. return _ACCEPT_CONCLUSION_LABELS.get(key, key)
  462. def _load_sample_type_name_map(db: Session, type_ids: List[str]) -> Dict[str, str]:
  463. ids = [x for x in type_ids if x]
  464. if not ids:
  465. return {}
  466. return {
  467. t.id: t.type_name or ""
  468. for t in db.query(BizCellSampleType)
  469. .filter(BizCellSampleType.id.in_(ids), BizCellSampleType.del_flag == 0)
  470. .all()
  471. }
  472. def _effective_reception_source(item: Dict[str, Any], rec: Optional[BizSampleReception]) -> str:
  473. """列表展示以工单 receptionSource 为准,接收记录作兜底。"""
  474. return (
  475. (item.get("receptionSource") or "").strip()
  476. or (getattr(rec, "reception_source", None) if rec else None)
  477. or "cell_storage"
  478. )
  479. def _effective_donation_sample_id(item: Dict[str, Any], rec: Optional[BizSampleReception]) -> str:
  480. """科研捐赠:接收记录 donation_sample_id;工单 protocol_sample_id 存捐赠样本 id。"""
  481. if _effective_reception_source(item, rec) != "research_donation":
  482. return ""
  483. return (
  484. (getattr(rec, "donation_sample_id", None) if rec else None) or ""
  485. ).strip() or (item.get("protocolSampleId") or "").strip()
  486. def _effective_protocol_sample_id(item: Dict[str, Any], rec: Optional[BizSampleReception]) -> str:
  487. if _effective_reception_source(item, rec) == "research_donation":
  488. return ""
  489. return (
  490. (getattr(rec, "protocol_sample_id", None) if rec else None) or ""
  491. ).strip() or (item.get("protocolSampleId") or "").strip()
  492. def _sample_type_name_for_item(
  493. *,
  494. source: str,
  495. donation_sample_id: str,
  496. protocol_sample_id: str,
  497. type_map: Dict[str, str],
  498. protocol_samples: Dict[str, BizProtocolSample],
  499. donation_samples: Dict[str, BizResearchDonationSample],
  500. ) -> str:
  501. if source == "research_donation":
  502. sample = donation_samples.get(donation_sample_id or "")
  503. tid = sample.sample_type_id if sample else ""
  504. else:
  505. sample = protocol_samples.get(protocol_sample_id or "")
  506. tid = sample.sample_type_id if sample else ""
  507. return type_map.get(tid or "", "")
  508. def _sample_type_name_for_reception(
  509. db: Session,
  510. rec: BizSampleReception,
  511. type_map: Dict[str, str],
  512. *,
  513. protocol_samples: Dict[str, BizProtocolSample],
  514. donation_samples: Dict[str, BizResearchDonationSample],
  515. item: Optional[Dict[str, Any]] = None,
  516. ) -> str:
  517. """按接收记录解析样本类型;传入 item 时与工单列表 enrich 规则一致。"""
  518. _ = db
  519. ctx = item if item is not None else {}
  520. source = _effective_reception_source(ctx, rec)
  521. return _sample_type_name_for_item(
  522. source=source,
  523. donation_sample_id=_effective_donation_sample_id(ctx, rec),
  524. protocol_sample_id=_effective_protocol_sample_id(ctx, rec),
  525. type_map=type_map,
  526. protocol_samples=protocol_samples,
  527. donation_samples=donation_samples,
  528. )
  529. def _enrich_work_order_list_items(
  530. db: Session, items: List[Dict[str, Any]]
  531. ) -> List[Dict[str, Any]]:
  532. if not items:
  533. return items
  534. rec_ids = [x["receptionId"] for x in items if x.get("receptionId")]
  535. sample_codes_no_rec = list(
  536. {
  537. (x.get("sampleCode") or "").strip()
  538. for x in items
  539. if not (x.get("receptionId") or "").strip() and (x.get("sampleCode") or "").strip()
  540. }
  541. )
  542. rec_map = {}
  543. if rec_ids:
  544. rec_map = {
  545. r.id: r
  546. for r in db.query(BizSampleReception)
  547. .filter(BizSampleReception.id.in_(rec_ids), BizSampleReception.del_flag == 0)
  548. .all()
  549. }
  550. sample_code_rec_map: Dict[str, BizSampleReception] = {}
  551. if sample_codes_no_rec:
  552. for r in (
  553. db.query(BizSampleReception)
  554. .filter(
  555. BizSampleReception.sample_code.in_(sample_codes_no_rec),
  556. BizSampleReception.del_flag == 0,
  557. )
  558. .order_by(BizSampleReception.create_time.desc())
  559. .all()
  560. ):
  561. code = (r.sample_code or "").strip()
  562. if code and code not in sample_code_rec_map:
  563. sample_code_rec_map[code] = r
  564. if not rec_map and not sample_code_rec_map:
  565. return items
  566. def _resolve_reception(item: Dict[str, Any]) -> Optional[BizSampleReception]:
  567. rec = rec_map.get(item.get("receptionId") or "")
  568. if rec:
  569. return rec
  570. code = (item.get("sampleCode") or "").strip()
  571. return sample_code_rec_map.get(code) if code else None
  572. cell_sample_ids: List[str] = []
  573. donation_sample_ids: List[str] = []
  574. cell_proto_ids: List[str] = []
  575. donation_ids: List[str] = []
  576. for item in items:
  577. rec = _resolve_reception(item)
  578. source = _effective_reception_source(item, rec)
  579. if source == "research_donation":
  580. dsid = _effective_donation_sample_id(item, rec)
  581. if dsid:
  582. donation_sample_ids.append(dsid)
  583. did = (
  584. (getattr(rec, "research_donation_id", None) if rec else None) or ""
  585. ).strip() or (item.get("collectionProtocolId") or "").strip()
  586. if did:
  587. donation_ids.append(did)
  588. else:
  589. psid = _effective_protocol_sample_id(item, rec)
  590. if psid:
  591. cell_sample_ids.append(psid)
  592. pid = (
  593. (getattr(rec, "collection_protocol_id", None) if rec else None) or ""
  594. ).strip() or (item.get("collectionProtocolId") or "").strip()
  595. if pid:
  596. cell_proto_ids.append(pid)
  597. donation_sample_ids = list(dict.fromkeys(donation_sample_ids))
  598. cell_sample_ids = list(dict.fromkeys(cell_sample_ids))
  599. donation_ids = list(dict.fromkeys(donation_ids))
  600. cell_proto_ids = list(dict.fromkeys(cell_proto_ids))
  601. protocol_samples: Dict[str, BizProtocolSample] = {}
  602. if cell_sample_ids:
  603. protocol_samples = {
  604. s.id: s
  605. for s in db.query(BizProtocolSample)
  606. .filter(BizProtocolSample.id.in_(cell_sample_ids), BizProtocolSample.del_flag == 0)
  607. .all()
  608. }
  609. donation_samples: Dict[str, BizResearchDonationSample] = {}
  610. if donation_sample_ids:
  611. donation_samples = {}
  612. for s in (
  613. db.query(BizResearchDonationSample)
  614. .filter(
  615. _donation_sample_id_col(BizResearchDonationSample.id).in_(donation_sample_ids),
  616. BizResearchDonationSample.del_flag == 0,
  617. )
  618. .all()
  619. ):
  620. donation_samples[s.id] = s
  621. donation_samples[(s.id or "").strip()] = s
  622. type_ids = list(
  623. {
  624. s.sample_type_id
  625. for s in list(protocol_samples.values()) + list(donation_samples.values())
  626. if s.sample_type_id
  627. }
  628. )
  629. type_map = _load_sample_type_name_map(db, type_ids)
  630. proto_status_map: Dict[str, str] = {}
  631. proto_void_type_map: Dict[str, str] = {}
  632. tenant_no_for_status = ""
  633. if cell_proto_ids:
  634. for p in (
  635. db.query(
  636. BizCollectionProtocol.id,
  637. BizCollectionProtocol.protocol_status,
  638. BizCollectionProtocol.void_type,
  639. BizCollectionProtocol.tenant_no,
  640. )
  641. .filter(BizCollectionProtocol.id.in_(cell_proto_ids), BizCollectionProtocol.del_flag == 0)
  642. .all()
  643. ):
  644. proto_status_map[p.id] = p.protocol_status or ""
  645. proto_void_type_map[p.id] = p.void_type or ""
  646. if not tenant_no_for_status and (p.tenant_no or "").strip():
  647. tenant_no_for_status = (p.tenant_no or "").strip()
  648. donation_status_map: Dict[str, str] = {}
  649. donation_void_type_map: Dict[str, str] = {}
  650. if donation_ids:
  651. for d in (
  652. db.query(
  653. BizResearchDonation.id,
  654. BizResearchDonation.donation_status,
  655. BizResearchDonation.void_type,
  656. BizResearchDonation.tenant_no,
  657. )
  658. .filter(BizResearchDonation.id.in_(donation_ids), BizResearchDonation.del_flag == 0)
  659. .all()
  660. ):
  661. donation_status_map[d.id] = d.donation_status or ""
  662. donation_void_type_map[d.id] = d.void_type or ""
  663. if not tenant_no_for_status and (d.tenant_no or "").strip():
  664. tenant_no_for_status = (d.tenant_no or "").strip()
  665. cell_party_map: Dict[str, Tuple[str, str]] = {}
  666. if cell_proto_ids:
  667. for party in (
  668. db.query(BizProtocolPartyCustomer)
  669. .filter(
  670. BizProtocolPartyCustomer.collection_protocol_id.in_(cell_proto_ids),
  671. BizProtocolPartyCustomer.del_flag == 0,
  672. )
  673. .all()
  674. ):
  675. cell_party_map[party.collection_protocol_id] = (
  676. party.mother_name or "",
  677. party.newborn_name or "",
  678. )
  679. donation_party_map: Dict[str, Tuple[str, str]] = {}
  680. if donation_ids:
  681. for party in (
  682. db.query(BizResearchDonationParty)
  683. .filter(
  684. BizResearchDonationParty.research_donation_id.in_(donation_ids),
  685. BizResearchDonationParty.del_flag == 0,
  686. )
  687. .all()
  688. ):
  689. donation_party_map[party.research_donation_id] = (
  690. party.mother_name or "",
  691. party.newborn_name or "",
  692. )
  693. for item in items:
  694. if item.get("workOrderType") == "temporary":
  695. item["receptionSource"] = ""
  696. item["receptionSourceLabel"] = ""
  697. item["motherName"] = ""
  698. item["newbornName"] = ""
  699. continue
  700. rec = _resolve_reception(item)
  701. source = _effective_reception_source(item, rec)
  702. item["receptionSource"] = source
  703. item["receptionSourceLabel"] = _reception_source_label(source)
  704. if source == "research_donation":
  705. _did = (
  706. (getattr(rec, "research_donation_id", None) if rec else None) or ""
  707. ).strip() or (item.get("collectionProtocolId") or "").strip()
  708. _mother, _newborn = donation_party_map.get(_did or "", ("", ""))
  709. item["motherName"] = _mother
  710. item["newbornName"] = _newborn
  711. else:
  712. _pid = (
  713. (getattr(rec, "collection_protocol_id", None) if rec else None) or ""
  714. ).strip() or (item.get("collectionProtocolId") or "").strip()
  715. _mother, _newborn = cell_party_map.get(_pid or "", ("", ""))
  716. item["motherName"] = _mother
  717. item["newbornName"] = _newborn
  718. if not rec:
  719. item.setdefault("receptionTime", "")
  720. item.setdefault("handlerName", "")
  721. item.setdefault("sampleTypeName", "")
  722. item.setdefault("labStorageCode", "")
  723. continue
  724. item["receptionTime"] = _fmt_dt(rec.reception_time)
  725. item["handlerName"] = rec.handler_name or ""
  726. item["labStorageCode"] = rec.lab_storage_code or ""
  727. item["sampleTypeName"] = _sample_type_name_for_item(
  728. source=source,
  729. donation_sample_id=_effective_donation_sample_id(item, rec),
  730. protocol_sample_id=_effective_protocol_sample_id(item, rec),
  731. type_map=type_map,
  732. protocol_samples=protocol_samples,
  733. donation_samples=donation_samples,
  734. )
  735. apply_special_to_item_from_samples(
  736. item,
  737. source=source,
  738. protocol_sample_id=_effective_protocol_sample_id(item, rec),
  739. donation_sample_id=_effective_donation_sample_id(item, rec),
  740. protocol_samples=protocol_samples,
  741. donation_samples=donation_samples,
  742. )
  743. if source == "research_donation":
  744. _did = (
  745. (getattr(rec, "research_donation_id", None) or "").strip()
  746. or (item.get("collectionProtocolId") or "").strip()
  747. )
  748. _st = donation_status_map.get(_did or "", "")
  749. _vt = donation_void_type_map.get(_did or "", "")
  750. item["protocolStatus"] = _st
  751. item["voidType"] = _vt
  752. from app.core.protocol_status_display import format_protocol_status_label
  753. item["protocolStatusLabel"] = format_protocol_status_label(
  754. _st, _vt, kind="donation", db=db, tenant_no=tenant_no_for_status or None
  755. )
  756. else:
  757. _pid = (
  758. (getattr(rec, "collection_protocol_id", None) or "").strip()
  759. or (item.get("collectionProtocolId") or "").strip()
  760. )
  761. _st = proto_status_map.get(_pid or "", "")
  762. _vt = proto_void_type_map.get(_pid or "", "")
  763. item["protocolStatus"] = _st
  764. item["voidType"] = _vt
  765. from app.core.protocol_status_display import format_protocol_status_label
  766. item["protocolStatusLabel"] = format_protocol_status_label(
  767. _st, _vt, kind="protocol", db=db, tenant_no=tenant_no_for_status or None
  768. )
  769. return _enrich_protocol_sibling_samples(db, items)
  770. def _protocol_group_key_for_item(item: Dict[str, Any]) -> str:
  771. wo_type = (item.get("workOrderType") or "inbound").strip() or "inbound"
  772. if wo_type != "inbound":
  773. return ""
  774. source = (item.get("receptionSource") or "cell_storage").strip() or "cell_storage"
  775. proto_id = (item.get("collectionProtocolId") or "").strip()
  776. if proto_id:
  777. if source == "research_donation":
  778. return f"donation:{proto_id}"
  779. return f"protocol:{proto_id}"
  780. protocol_no = (item.get("protocolNo") or "").strip()
  781. if protocol_no:
  782. return f"protocol-no:{protocol_no}"
  783. return ""
  784. def _enrich_protocol_sibling_samples(
  785. db: Session, items: List[Dict[str, Any]]
  786. ) -> List[Dict[str, Any]]:
  787. """入库列表:补充同协议下全部样本(供卡片标题栏「同协议样本」展示)。"""
  788. if not items:
  789. return items
  790. cell_proto_ids: List[str] = []
  791. donation_ids: List[str] = []
  792. for item in items:
  793. if (item.get("workOrderType") or "inbound").strip() not in ("", "inbound"):
  794. continue
  795. pid = (item.get("collectionProtocolId") or "").strip()
  796. if not pid:
  797. continue
  798. source = (item.get("receptionSource") or "cell_storage").strip() or "cell_storage"
  799. if source == "research_donation":
  800. if pid not in donation_ids:
  801. donation_ids.append(pid)
  802. elif pid not in cell_proto_ids:
  803. cell_proto_ids.append(pid)
  804. siblings_by_key: Dict[str, List[Dict[str, str]]] = {}
  805. if cell_proto_ids:
  806. proto_samples = (
  807. db.query(BizProtocolSample)
  808. .filter(
  809. BizProtocolSample.collection_protocol_id.in_(cell_proto_ids),
  810. BizProtocolSample.del_flag == 0,
  811. )
  812. .all()
  813. )
  814. by_proto: Dict[str, List[BizProtocolSample]] = defaultdict(list)
  815. type_ids: List[str] = []
  816. tenant_no = ""
  817. for sample in proto_samples:
  818. pid = (sample.collection_protocol_id or "").strip()
  819. if pid:
  820. by_proto[pid].append(sample)
  821. if sample.sample_type_id and sample.sample_type_id not in type_ids:
  822. type_ids.append(sample.sample_type_id)
  823. if not tenant_no and (sample.tenant_no or "").strip():
  824. tenant_no = (sample.tenant_no or "").strip()
  825. type_map = _load_sample_type_name_map(db, type_ids)
  826. sample_codes = list(
  827. {
  828. (s.sample_code or "").strip()
  829. for samples in by_proto.values()
  830. for s in samples
  831. if (s.sample_code or "").strip()
  832. }
  833. )
  834. lab_map = (
  835. sample_reception_svc.get_lab_storage_code_map_by_sample_codes(
  836. db, tenant_no, sample_codes, reception_source="cell_storage"
  837. )
  838. if tenant_no and sample_codes
  839. else {}
  840. )
  841. for pid, samples in by_proto.items():
  842. entries: List[Dict[str, str]] = []
  843. for sample in sorted(samples, key=lambda row: (row.sample_code or "")):
  844. code = (sample.sample_code or "").strip()
  845. if not code:
  846. continue
  847. entries.append(
  848. {
  849. "sampleCode": code,
  850. "labStorageCode": lab_map.get(code, ""),
  851. "sampleTypeName": type_map.get(sample.sample_type_id or "", ""),
  852. }
  853. )
  854. if len(entries) > 1:
  855. siblings_by_key[f"protocol:{pid}"] = entries
  856. if donation_ids:
  857. donation_samples = (
  858. db.query(BizResearchDonationSample)
  859. .filter(
  860. BizResearchDonationSample.research_donation_id.in_(donation_ids),
  861. BizResearchDonationSample.del_flag == 0,
  862. )
  863. .all()
  864. )
  865. by_donation: Dict[str, List[BizResearchDonationSample]] = defaultdict(list)
  866. type_ids = []
  867. tenant_no = ""
  868. for sample in donation_samples:
  869. did = (sample.research_donation_id or "").strip()
  870. if did:
  871. by_donation[did].append(sample)
  872. if sample.sample_type_id and sample.sample_type_id not in type_ids:
  873. type_ids.append(sample.sample_type_id)
  874. if not tenant_no and (sample.tenant_no or "").strip():
  875. tenant_no = (sample.tenant_no or "").strip()
  876. type_map = _load_sample_type_name_map(db, type_ids)
  877. sample_codes = list(
  878. {
  879. (s.sample_code or "").strip()
  880. for samples in by_donation.values()
  881. for s in samples
  882. if (s.sample_code or "").strip()
  883. }
  884. )
  885. lab_map = (
  886. sample_reception_svc.get_lab_storage_code_map_by_sample_codes(
  887. db, tenant_no, sample_codes, reception_source="research_donation"
  888. )
  889. if tenant_no and sample_codes
  890. else {}
  891. )
  892. for did, samples in by_donation.items():
  893. entries = []
  894. for sample in sorted(samples, key=lambda row: (row.sample_code or "")):
  895. code = (sample.sample_code or "").strip()
  896. if not code:
  897. continue
  898. entries.append(
  899. {
  900. "sampleCode": code,
  901. "labStorageCode": lab_map.get(code, ""),
  902. "sampleTypeName": type_map.get(sample.sample_type_id or "", ""),
  903. }
  904. )
  905. if len(entries) > 1:
  906. siblings_by_key[f"donation:{did}"] = entries
  907. for item in items:
  908. key = _protocol_group_key_for_item(item)
  909. siblings = siblings_by_key.get(key)
  910. if siblings:
  911. item["protocolSiblingSamples"] = siblings
  912. return items
  913. def _enrich_production_work_order_refs(
  914. db: Session, items: List[Dict[str, Any]]
  915. ) -> List[Dict[str, Any]]:
  916. if not items:
  917. return items
  918. pwo_ids = list(
  919. {x.get("productionWorkOrderId") for x in items if (x.get("productionWorkOrderId") or "").strip()}
  920. )
  921. pwo_map: Dict[str, str] = {}
  922. if pwo_ids:
  923. rows = (
  924. db.query(BizProductionWorkOrder.id, BizProductionWorkOrder.work_order_no)
  925. .filter(
  926. BizProductionWorkOrder.id.in_(pwo_ids),
  927. BizProductionWorkOrder.del_flag == 0,
  928. )
  929. .all()
  930. )
  931. pwo_map = {r.id: r.work_order_no or "" for r in rows}
  932. for item in items:
  933. pid = (item.get("productionWorkOrderId") or "").strip()
  934. item["productionWorkOrderNo"] = pwo_map.get(pid, "") if pid else ""
  935. return items
  936. def _gen_daily_no(db: Session, tenant_no: str, prefix: str) -> str:
  937. day = datetime.now().strftime("%Y%m%d")
  938. like = f"{prefix}{day}%"
  939. cnt = (
  940. db.query(BizQcWorkOrder)
  941. .filter(
  942. BizQcWorkOrder.tenant_no == tenant_no,
  943. BizQcWorkOrder.work_order_no.like(like),
  944. )
  945. .count()
  946. )
  947. return f"{prefix}{day}{cnt + 1:04d}"
  948. def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
  949. ids = list({x for x in user_ids if x})
  950. if not ids:
  951. return {}
  952. rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
  953. return {u.id: (u.nick_name or "").strip() for u in rows}
  954. def _wo_to_dict(
  955. wo: BizQcWorkOrder,
  956. *,
  957. outbound_info: Optional[Dict[str, Any]] = None,
  958. configured_by_name: str = "",
  959. ) -> Dict[str, Any]:
  960. wo_type = getattr(wo, "work_order_type", None) or "inbound"
  961. recv_source_raw = getattr(wo, "reception_source", None)
  962. if wo_type == "temporary":
  963. recv_source = (recv_source_raw or "").strip()
  964. else:
  965. recv_source = (recv_source_raw or "cell_storage").strip() or "cell_storage"
  966. biz_type = getattr(wo, "biz_type", None) or ""
  967. is_outbound = wo_type == "outbound"
  968. ob = outbound_info or {}
  969. return {
  970. "id": wo.id,
  971. "workOrderNo": wo.work_order_no,
  972. "workOrderType": wo_type,
  973. "receptionId": wo.reception_id,
  974. "protocolSampleId": wo.protocol_sample_id,
  975. "collectionProtocolId": wo.collection_protocol_id,
  976. "protocolNo": wo.protocol_no or "",
  977. "sampleCode": wo.sample_code or "",
  978. "receptionSource": recv_source,
  979. "receptionSourceLabel": _reception_source_label(recv_source) if recv_source else "",
  980. "qcBizType": _work_order_qc_biz_type(wo),
  981. "qcBizTypeLabel": _qc_biz_type_label(_work_order_qc_biz_type(wo)),
  982. "infusionScheduleId": getattr(wo, "infusion_schedule_id", None) or None,
  983. "bizType": biz_type if is_outbound else "",
  984. "bizTypeLabel": _OUTBOUND_BIZ_TYPE_LABELS.get(biz_type, "") if is_outbound else "",
  985. "applicationNo": ob.get("applicationNo", "") if is_outbound else "",
  986. "applicationName": ob.get("applicationName", "") if is_outbound else "",
  987. "channelName": ob.get("channelName", "") if is_outbound else "",
  988. "infusionTime": ob.get("infusionTime", "") if is_outbound else "",
  989. "cellProductId": getattr(wo, "cell_product_id", None) or "",
  990. "externalTargetCode": getattr(wo, "external_target_code", None) or "",
  991. "cellProductCode": "",
  992. "cellProductName": "",
  993. "cellProductTypeName": "",
  994. "cellProductPassage": "",
  995. "cellProductSpecName": "",
  996. "triggerSource": getattr(wo, "trigger_source", None) or "reception",
  997. "triggerSourceLabel": _TRIGGER_SOURCE_LABELS.get(
  998. getattr(wo, "trigger_source", None) or "reception", ""
  999. ),
  1000. "productionWorkOrderId": getattr(wo, "production_work_order_id", None) or "",
  1001. "productionWorkOrderNo": "",
  1002. "productionTaskId": getattr(wo, "production_task_id", None) or "",
  1003. "productionStepOutputId": getattr(wo, "production_step_output_id", None) or "",
  1004. "receptionTime": "",
  1005. "handlerName": "",
  1006. "sampleTypeName": "",
  1007. "workOrderStatus": wo.work_order_status,
  1008. "workOrderStatusLabel": _WORK_ORDER_STATUS_LABELS.get(
  1009. wo.work_order_status or "", wo.work_order_status or ""
  1010. ),
  1011. "qcConclusion": (
  1012. (getattr(wo, "conclusion", None) or "").strip()
  1013. if (getattr(wo, "conclusion", None) or "").strip() in _VALID_QC_WORK_ORDER_CONCLUSIONS
  1014. else ""
  1015. ),
  1016. "qcConclusionLabel": work_order_qc_conclusion_label(getattr(wo, "conclusion", None) or ""),
  1017. "hasLab": wo.has_lab == "1",
  1018. "hasThirdParty": wo.has_third_party == "1",
  1019. "configuredTime": _fmt_dt(wo.configured_time),
  1020. "configuredBy": wo.configured_by or "",
  1021. "configuredByName": configured_by_name or wo.configured_by or "",
  1022. "thirdPartyFeeTotal": _dec(wo.third_party_fee_total),
  1023. "remark": wo.remark or "",
  1024. "createTime": _fmt_dt(wo.create_time),
  1025. "updateTime": _fmt_dt(wo.update_time),
  1026. }
  1027. def create_work_order_from_reception(
  1028. db: Session,
  1029. *,
  1030. reception,
  1031. tenant_no: str,
  1032. user_id: Optional[str] = None,
  1033. create_dept_id: str = "",
  1034. ) -> Optional[BizQcWorkOrder]:
  1035. """接收结论为 js 时创建待配置工单(flush,不 commit)。"""
  1036. if (reception.accept_conclusion or "").strip() != "js":
  1037. return None
  1038. exists = (
  1039. db.query(BizQcWorkOrder)
  1040. .filter(
  1041. BizQcWorkOrder.reception_id == reception.id,
  1042. BizQcWorkOrder.tenant_no == tenant_no,
  1043. BizQcWorkOrder.del_flag == 0,
  1044. )
  1045. .first()
  1046. )
  1047. if exists:
  1048. return exists
  1049. source = getattr(reception, "reception_source", "cell_storage") or "cell_storage"
  1050. if source == "research_donation":
  1051. protocol_no = ""
  1052. d = (
  1053. db.query(BizResearchDonation)
  1054. .filter(
  1055. BizResearchDonation.id == (reception.research_donation_id or ""),
  1056. BizResearchDonation.del_flag == 0,
  1057. )
  1058. .first()
  1059. )
  1060. if d:
  1061. protocol_no = d.donation_no or ""
  1062. sample_code = ""
  1063. ds = (
  1064. db.query(BizResearchDonationSample)
  1065. .filter(
  1066. BizResearchDonationSample.id == (reception.donation_sample_id or ""),
  1067. BizResearchDonationSample.del_flag == 0,
  1068. )
  1069. .first()
  1070. )
  1071. if ds:
  1072. sample_code = ds.sample_code or ""
  1073. wo = BizQcWorkOrder(
  1074. id=generate_uuid(),
  1075. work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
  1076. reception_id=reception.id,
  1077. protocol_sample_id=reception.donation_sample_id or "",
  1078. collection_protocol_id=reception.research_donation_id or "",
  1079. protocol_no=protocol_no or None,
  1080. sample_code=sample_code or None,
  1081. reception_source="research_donation",
  1082. qc_biz_type="inbound",
  1083. trigger_source="reception",
  1084. work_order_status="dpz",
  1085. has_lab="0",
  1086. has_third_party="0",
  1087. tenant_no=tenant_no,
  1088. create_by=user_id,
  1089. create_dept_id=create_dept_id or "",
  1090. update_by=user_id,
  1091. create_time=datetime.now(),
  1092. update_time=datetime.now(),
  1093. del_flag=0,
  1094. )
  1095. else:
  1096. protocol_no = ""
  1097. p = (
  1098. db.query(BizCollectionProtocol)
  1099. .filter(
  1100. BizCollectionProtocol.id == reception.collection_protocol_id,
  1101. BizCollectionProtocol.del_flag == 0,
  1102. )
  1103. .first()
  1104. )
  1105. if p:
  1106. protocol_no = p.protocol_no or ""
  1107. sample_code = ""
  1108. s = (
  1109. db.query(BizProtocolSample)
  1110. .filter(BizProtocolSample.id == reception.protocol_sample_id, BizProtocolSample.del_flag == 0)
  1111. .first()
  1112. )
  1113. if s:
  1114. sample_code = s.sample_code or ""
  1115. wo = BizQcWorkOrder(
  1116. id=generate_uuid(),
  1117. work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
  1118. reception_id=reception.id,
  1119. protocol_sample_id=reception.protocol_sample_id,
  1120. collection_protocol_id=reception.collection_protocol_id,
  1121. protocol_no=protocol_no or None,
  1122. sample_code=sample_code or None,
  1123. reception_source="cell_storage",
  1124. qc_biz_type="inbound",
  1125. trigger_source="reception",
  1126. work_order_status="dpz",
  1127. has_lab="0",
  1128. has_third_party="0",
  1129. tenant_no=tenant_no,
  1130. create_by=user_id,
  1131. create_dept_id=create_dept_id or "",
  1132. update_by=user_id,
  1133. create_time=datetime.now(),
  1134. update_time=datetime.now(),
  1135. del_flag=0,
  1136. )
  1137. db.add(wo)
  1138. db.flush()
  1139. return wo
  1140. def _valid_qc_dispatch_output_type_ids(db: Session, tenant_no: str) -> set:
  1141. rows = (
  1142. db.query(BizOutputType)
  1143. .filter(
  1144. BizOutputType.tenant_no == tenant_no,
  1145. BizOutputType.del_flag == 0,
  1146. BizOutputType.type_name == _VALID_QC_DISPATCH_OUTPUT_TYPE_NAME,
  1147. )
  1148. .all()
  1149. )
  1150. return {r.id for r in rows}
  1151. def _resolve_cell_product_id_by_barcode(
  1152. db: Session, barcode: Optional[str], tenant_no: str
  1153. ) -> Optional[str]:
  1154. code = (barcode or "").strip()
  1155. if not code:
  1156. return None
  1157. cp = (
  1158. db.query(BizCellProduct)
  1159. .filter(
  1160. BizCellProduct.product_code == code,
  1161. BizCellProduct.tenant_no == tenant_no,
  1162. BizCellProduct.del_flag == 0,
  1163. )
  1164. .first()
  1165. )
  1166. return cp.id if cp else None
  1167. def create_qc_work_order_from_production(
  1168. db: Session,
  1169. *,
  1170. production_work_order_id: str,
  1171. production_step_output_id: str,
  1172. remark: str = "",
  1173. tenant_no: str,
  1174. user_id: Optional[str] = None,
  1175. create_dept_id: str = "",
  1176. ) -> BizQcWorkOrder:
  1177. """生产过程派发质检工单(flush,不 commit)。"""
  1178. pwo = (
  1179. db.query(BizProductionWorkOrder)
  1180. .filter(
  1181. BizProductionWorkOrder.id == production_work_order_id,
  1182. BizProductionWorkOrder.tenant_no == tenant_no,
  1183. BizProductionWorkOrder.del_flag == 0,
  1184. )
  1185. .first()
  1186. )
  1187. if not pwo:
  1188. raise ValueError("生产工单不存在")
  1189. wo_type = (getattr(pwo, "work_order_type", None) or "inbound").strip()
  1190. if wo_type not in ("inbound", "outbound", "temporary"):
  1191. raise ValueError("仅入库、出库或临时生产工单可派发质检")
  1192. status = (pwo.work_order_status or "").strip()
  1193. if status == "yzf":
  1194. raise ValueError("已作废的生产工单不可派发质检")
  1195. if status not in ("jxz", "ywc"):
  1196. raise ValueError("仅进行中或已完成的生产工单可派发质检")
  1197. step_output_id = (production_step_output_id or "").strip()
  1198. if not step_output_id:
  1199. raise ValueError("请选择步骤产出物")
  1200. options = list_step_output_dispatch_options_for_production_work_order(
  1201. db, production_work_order_id=production_work_order_id, tenant_no=tenant_no
  1202. )
  1203. option_map = {o["id"]: o for o in options}
  1204. selected = option_map.get(step_output_id)
  1205. if not selected:
  1206. raise ValueError("所选步骤产出物不在该生产工单的可派发列表中")
  1207. step_output = (
  1208. db.query(BizProductionTaskStepOutput)
  1209. .filter(
  1210. BizProductionTaskStepOutput.id == step_output_id,
  1211. BizProductionTaskStepOutput.tenant_no == tenant_no,
  1212. BizProductionTaskStepOutput.del_flag == 0,
  1213. )
  1214. .first()
  1215. )
  1216. if not step_output:
  1217. raise ValueError("步骤产出物不存在")
  1218. step = (
  1219. db.query(BizProductionTaskStep)
  1220. .filter(
  1221. BizProductionTaskStep.id == step_output.step_id,
  1222. BizProductionTaskStep.tenant_no == tenant_no,
  1223. BizProductionTaskStep.del_flag == 0,
  1224. )
  1225. .first()
  1226. )
  1227. if not step:
  1228. raise ValueError("关联工艺步骤不存在")
  1229. task = (
  1230. db.query(BizProductionTask)
  1231. .filter(
  1232. BizProductionTask.id == step.task_id,
  1233. BizProductionTask.work_order_id == pwo.id,
  1234. BizProductionTask.tenant_no == tenant_no,
  1235. BizProductionTask.del_flag == 0,
  1236. )
  1237. .first()
  1238. )
  1239. if not task:
  1240. raise ValueError("关联生产任务不存在")
  1241. cp_id = _resolve_cell_product_id_by_barcode(db, step_output.output_barcode, tenant_no)
  1242. task_id = task.id
  1243. common_fields = dict(
  1244. trigger_source="production",
  1245. cell_product_id=cp_id,
  1246. production_work_order_id=pwo.id,
  1247. production_task_id=task_id,
  1248. production_step_output_id=step_output_id,
  1249. work_order_status="dpz",
  1250. has_lab="0",
  1251. has_third_party="0",
  1252. tenant_no=tenant_no,
  1253. remark=(remark or "").strip() or None,
  1254. create_by=user_id,
  1255. create_dept_id=create_dept_id or "",
  1256. update_by=user_id,
  1257. create_time=datetime.now(),
  1258. update_time=datetime.now(),
  1259. del_flag=0,
  1260. )
  1261. if wo_type == "outbound":
  1262. schedule_id = (getattr(pwo, "infusion_schedule_id", None) or "").strip()
  1263. if not schedule_id:
  1264. raise ValueError("出库生产工单缺少输注计划关联")
  1265. biz_type = (getattr(pwo, "biz_type", None) or "pilot_service").strip() or "pilot_service"
  1266. if biz_type not in _OUTBOUND_BIZ_TYPE_LABELS:
  1267. biz_type = "pilot_service"
  1268. wo = BizQcWorkOrder(
  1269. id=generate_uuid(),
  1270. work_order_no=_gen_daily_no(db, tenant_no, "CKZJ"),
  1271. work_order_type="outbound",
  1272. reception_id=None,
  1273. protocol_sample_id=None,
  1274. collection_protocol_id=None,
  1275. protocol_no=None,
  1276. sample_code=None,
  1277. reception_source="outbound",
  1278. qc_biz_type="outbound",
  1279. biz_type=biz_type,
  1280. infusion_schedule_id=schedule_id,
  1281. **common_fields,
  1282. )
  1283. elif wo_type == "temporary":
  1284. wo = BizQcWorkOrder(
  1285. id=generate_uuid(),
  1286. work_order_no=_gen_daily_no(db, tenant_no, "LSZJ"),
  1287. work_order_type="temporary",
  1288. reception_id=None,
  1289. protocol_sample_id=None,
  1290. collection_protocol_id=None,
  1291. protocol_no=None,
  1292. sample_code=None,
  1293. reception_source=None,
  1294. qc_biz_type="temporary",
  1295. **common_fields,
  1296. )
  1297. else:
  1298. wo = BizQcWorkOrder(
  1299. id=generate_uuid(),
  1300. work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
  1301. work_order_type="inbound",
  1302. reception_id=None,
  1303. protocol_sample_id=pwo.protocol_sample_id,
  1304. collection_protocol_id=pwo.collection_protocol_id,
  1305. protocol_no=pwo.protocol_no or None,
  1306. sample_code=pwo.sample_code or None,
  1307. reception_source=getattr(pwo, "reception_source", "cell_storage") or "cell_storage",
  1308. qc_biz_type="inbound",
  1309. **common_fields,
  1310. )
  1311. db.add(wo)
  1312. db.flush()
  1313. return wo
  1314. def list_step_output_dispatch_options_for_production_work_order(
  1315. db: Session,
  1316. *,
  1317. production_work_order_id: str,
  1318. tenant_no: str,
  1319. ) -> List[Dict[str, Any]]:
  1320. """汇总生产工单下可派发的步骤产出物:类型为有效物且有条码。"""
  1321. pwo = (
  1322. db.query(BizProductionWorkOrder)
  1323. .filter(
  1324. BizProductionWorkOrder.id == production_work_order_id,
  1325. BizProductionWorkOrder.tenant_no == tenant_no,
  1326. BizProductionWorkOrder.del_flag == 0,
  1327. )
  1328. .first()
  1329. )
  1330. if not pwo:
  1331. raise ValueError("生产工单不存在")
  1332. valid_type_ids = _valid_qc_dispatch_output_type_ids(db, tenant_no)
  1333. if not valid_type_ids:
  1334. return []
  1335. tasks = (
  1336. db.query(BizProductionTask)
  1337. .filter(
  1338. BizProductionTask.work_order_id == pwo.id,
  1339. BizProductionTask.tenant_no == tenant_no,
  1340. BizProductionTask.del_flag == 0,
  1341. )
  1342. .all()
  1343. )
  1344. if not tasks:
  1345. return []
  1346. task_map = {t.id: t for t in tasks}
  1347. task_ids = list(task_map.keys())
  1348. steps = (
  1349. db.query(BizProductionTaskStep)
  1350. .filter(
  1351. BizProductionTaskStep.task_id.in_(task_ids),
  1352. BizProductionTaskStep.tenant_no == tenant_no,
  1353. BizProductionTaskStep.del_flag == 0,
  1354. )
  1355. .all()
  1356. )
  1357. if not steps:
  1358. return []
  1359. step_map = {s.id: s for s in steps}
  1360. step_ids = list(step_map.keys())
  1361. type_rows = (
  1362. db.query(BizOutputType)
  1363. .filter(
  1364. BizOutputType.id.in_(list(valid_type_ids)),
  1365. BizOutputType.del_flag == 0,
  1366. )
  1367. .all()
  1368. )
  1369. type_name_map = {t.id: t.type_name or "" for t in type_rows}
  1370. outputs = (
  1371. db.query(BizProductionTaskStepOutput)
  1372. .filter(
  1373. BizProductionTaskStepOutput.step_id.in_(step_ids),
  1374. BizProductionTaskStepOutput.tenant_no == tenant_no,
  1375. BizProductionTaskStepOutput.del_flag == 0,
  1376. BizProductionTaskStepOutput.output_type_id.in_(list(valid_type_ids)),
  1377. )
  1378. .order_by(
  1379. BizProductionTaskStepOutput.order_num.asc(),
  1380. BizProductionTaskStepOutput.create_time.asc(),
  1381. )
  1382. .all()
  1383. )
  1384. items: List[Dict[str, Any]] = []
  1385. for out in outputs:
  1386. barcode = (out.output_barcode or "").strip()
  1387. if not barcode:
  1388. continue
  1389. step = step_map.get(out.step_id)
  1390. if not step:
  1391. continue
  1392. task = task_map.get(step.task_id)
  1393. if not task:
  1394. continue
  1395. type_id = (out.output_type_id or "").strip()
  1396. cell_product_id = _resolve_cell_product_id_by_barcode(db, barcode, tenant_no)
  1397. items.append(
  1398. {
  1399. "id": out.id,
  1400. "stepOutputId": out.id,
  1401. "taskId": task.id,
  1402. "taskNo": task.task_no or "",
  1403. "taskName": task.task_name or "",
  1404. "stepId": step.id,
  1405. "processName": step.process_name or "",
  1406. "outputTypeId": type_id,
  1407. "outputTypeName": type_name_map.get(type_id, _VALID_QC_DISPATCH_OUTPUT_TYPE_NAME),
  1408. "outputName": (out.output_name or "").strip(),
  1409. "outputBarcode": barcode,
  1410. "cellProductId": cell_product_id or "",
  1411. }
  1412. )
  1413. return items
  1414. def list_cell_product_options_for_production_work_order(
  1415. db: Session,
  1416. *,
  1417. production_work_order_id: str,
  1418. tenant_no: str,
  1419. ) -> List[Dict[str, Any]]:
  1420. """兼容旧接口名:返回步骤产出物派发选项。"""
  1421. return list_step_output_dispatch_options_for_production_work_order(
  1422. db,
  1423. production_work_order_id=production_work_order_id,
  1424. tenant_no=tenant_no,
  1425. )
  1426. def _protocol_sample_ids_for_sample_type(
  1427. db: Session,
  1428. tenant_no: str,
  1429. sample_type_id: str,
  1430. reception_source: Optional[str] = None,
  1431. ) -> List[str]:
  1432. stid = (sample_type_id or "").strip()
  1433. if not stid or not tenant_no:
  1434. return []
  1435. ids: List[str] = []
  1436. rs = (reception_source or "").strip()
  1437. if not rs or rs == "cell_storage":
  1438. cell_rows = (
  1439. db.query(BizProtocolSample.id)
  1440. .filter(
  1441. BizProtocolSample.tenant_no == tenant_no,
  1442. BizProtocolSample.del_flag == 0,
  1443. BizProtocolSample.sample_type_id == stid,
  1444. )
  1445. .all()
  1446. )
  1447. ids.extend([r[0] for r in cell_rows if r[0]])
  1448. if not rs or rs == "research_donation":
  1449. donation_rows = (
  1450. db.query(BizResearchDonationSample.id)
  1451. .filter(
  1452. BizResearchDonationSample.tenant_no == tenant_no,
  1453. BizResearchDonationSample.del_flag == 0,
  1454. BizResearchDonationSample.sample_type_id == stid,
  1455. )
  1456. .all()
  1457. )
  1458. ids.extend([r[0] for r in donation_rows if r[0]])
  1459. return ids
  1460. def list_work_orders(
  1461. db: Session,
  1462. *,
  1463. page: int = 1,
  1464. page_size: int = 10,
  1465. keyword: Optional[str] = None,
  1466. work_order_status: Optional[str] = None,
  1467. work_order_type: Optional[str] = None,
  1468. biz_type: Optional[str] = None,
  1469. reception_source: Optional[str] = None,
  1470. trigger_source: Optional[str] = None,
  1471. sample_type_id: Optional[str] = None,
  1472. create_time_start: Optional[str] = None,
  1473. create_time_end: Optional[str] = None,
  1474. infusion_time_start: Optional[str] = None,
  1475. infusion_time_end: Optional[str] = None,
  1476. tenant_no: Optional[str] = None,
  1477. user_info=None,
  1478. ) -> Tuple[List[Dict[str, Any]], int]:
  1479. def _filtered():
  1480. q0 = db.query(BizQcWorkOrder).filter(BizQcWorkOrder.del_flag == 0)
  1481. if tenant_no:
  1482. q0 = q0.filter(BizQcWorkOrder.tenant_no == tenant_no)
  1483. wt = (work_order_type or "").strip()
  1484. if wt:
  1485. q0 = q0.filter(BizQcWorkOrder.work_order_type == wt)
  1486. else:
  1487. q0 = q0.filter(
  1488. ~and_(
  1489. BizQcWorkOrder.work_order_type == "outbound",
  1490. BizQcWorkOrder.biz_type == "clinical_application",
  1491. )
  1492. )
  1493. bt = (biz_type or "").strip()
  1494. if bt:
  1495. q0 = q0.filter(BizQcWorkOrder.biz_type == bt)
  1496. rs = (reception_source or "").strip()
  1497. if rs:
  1498. q0 = q0.filter(BizQcWorkOrder.reception_source == rs)
  1499. ts = (trigger_source or "").strip()
  1500. if ts:
  1501. if ts == "temp_manual":
  1502. q0 = q0.filter(
  1503. BizQcWorkOrder.work_order_type == "temporary",
  1504. BizQcWorkOrder.trigger_source != "production",
  1505. )
  1506. elif ts == "temp_production":
  1507. q0 = q0.filter(
  1508. BizQcWorkOrder.work_order_type == "temporary",
  1509. BizQcWorkOrder.trigger_source == "production",
  1510. )
  1511. elif ts == "manual" and wt == "temporary":
  1512. q0 = q0.filter(BizQcWorkOrder.trigger_source != "production")
  1513. elif ts == "production" and not wt:
  1514. q0 = q0.filter(
  1515. BizQcWorkOrder.trigger_source == "production",
  1516. BizQcWorkOrder.work_order_type != "temporary",
  1517. )
  1518. else:
  1519. q0 = q0.filter(BizQcWorkOrder.trigger_source == ts)
  1520. kw = (keyword or "").strip()
  1521. if kw:
  1522. like = f"%{kw}%"
  1523. keyword_conditions = [
  1524. BizQcWorkOrder.work_order_no.like(like),
  1525. BizQcWorkOrder.sample_code.like(like),
  1526. BizQcWorkOrder.protocol_no.like(like),
  1527. BizQcWorkOrder.external_target_code.like(like),
  1528. ]
  1529. if tenant_no:
  1530. from app.crud import sample_reception as reception_svc
  1531. rec_ids = reception_svc.reception_ids_matching_lab_storage_keyword(
  1532. db, tenant_no, kw
  1533. )
  1534. if rec_ids:
  1535. keyword_conditions.append(BizQcWorkOrder.reception_id.in_(rec_ids))
  1536. lab_codes = reception_svc.sample_codes_matching_lab_storage_keyword(
  1537. db, tenant_no, kw
  1538. )
  1539. if lab_codes:
  1540. keyword_conditions.append(BizQcWorkOrder.sample_code.in_(lab_codes))
  1541. if tenant_no and (wt == "outbound" or not wt):
  1542. outbound_ids = _outbound_qc_work_order_ids_by_keyword(db, tenant_no, kw)
  1543. if outbound_ids:
  1544. keyword_conditions.append(BizQcWorkOrder.id.in_(outbound_ids))
  1545. if tenant_no and (not wt or wt == "inbound"):
  1546. inbound_mother_ids = _inbound_qc_work_order_ids_by_mother_name(
  1547. db, tenant_no, kw, reception_source=rs or None
  1548. )
  1549. if inbound_mother_ids:
  1550. keyword_conditions.append(BizQcWorkOrder.id.in_(inbound_mother_ids))
  1551. q0 = q0.filter(or_(*keyword_conditions))
  1552. st_raw = (work_order_status or "").strip()
  1553. if st_raw:
  1554. statuses = [s.strip() for s in st_raw.split(",") if s.strip()]
  1555. if statuses:
  1556. q0 = q0.filter(BizQcWorkOrder.work_order_status.in_(statuses))
  1557. use_infusion_time = wt == "outbound" and (
  1558. (infusion_time_start or "").strip() or (infusion_time_end or "").strip()
  1559. )
  1560. if use_infusion_time:
  1561. q0 = q0.join(
  1562. BizPilotInfusionSchedule,
  1563. eq_unicode(BizQcWorkOrder.infusion_schedule_id, BizPilotInfusionSchedule.id),
  1564. ).filter(
  1565. BizPilotInfusionSchedule.del_flag == 0,
  1566. BizPilotInfusionSchedule.tenant_no == tenant_no,
  1567. )
  1568. if (infusion_time_start or "").strip():
  1569. q0 = q0.filter(
  1570. BizPilotInfusionSchedule.infusion_time
  1571. >= _normalize_date_bound(infusion_time_start.strip())
  1572. )
  1573. if (infusion_time_end or "").strip():
  1574. q0 = q0.filter(
  1575. BizPilotInfusionSchedule.infusion_time
  1576. <= _normalize_date_bound(infusion_time_end.strip(), end_of_day=True)
  1577. )
  1578. else:
  1579. if create_time_start:
  1580. q0 = q0.filter(
  1581. BizQcWorkOrder.create_time >= _normalize_date_bound(create_time_start)
  1582. )
  1583. if create_time_end:
  1584. q0 = q0.filter(
  1585. BizQcWorkOrder.create_time
  1586. <= _normalize_date_bound(create_time_end, end_of_day=True)
  1587. )
  1588. stid = (sample_type_id or "").strip()
  1589. if stid and tenant_no:
  1590. ps_ids = _protocol_sample_ids_for_sample_type(db, tenant_no, stid, rs or None)
  1591. if not ps_ids:
  1592. q0 = q0.filter(BizQcWorkOrder.id.in_([]))
  1593. else:
  1594. q0 = q0.filter(
  1595. BizQcWorkOrder.work_order_type == "inbound",
  1596. BizQcWorkOrder.protocol_sample_id.in_(ps_ids),
  1597. )
  1598. if user_info:
  1599. from app.core.data_scope import apply_data_scope
  1600. q0 = apply_data_scope(q0, BizQcWorkOrder, user_info, db, tenant_no)
  1601. return q0
  1602. total = _filtered().order_by(None).with_entities(func.count(BizQcWorkOrder.id)).scalar() or 0
  1603. page = max(1, page)
  1604. page_size = min(max(1, page_size), 100)
  1605. q_page = _filtered()
  1606. if uses_inbound_lab_storage_list_sort(reception_source):
  1607. q_page = q_page.order_by(*inbound_lab_storage_order_by_clauses())
  1608. else:
  1609. q_page = q_page.order_by(BizQcWorkOrder.create_time.desc())
  1610. rows = (
  1611. q_page
  1612. .offset((page - 1) * page_size)
  1613. .limit(page_size)
  1614. .all()
  1615. )
  1616. items = [_wo_to_dict(x) for x in rows]
  1617. items = _enrich_work_order_list_items(db, items)
  1618. if tenant_no:
  1619. items = _enrich_work_order_qc_targets(db, tenant_no, items)
  1620. items = _enrich_production_work_order_refs(db, items)
  1621. items = _enrich_outbound_work_orders(db, tenant_no, items)
  1622. items = enrich_work_order_fee_summaries(db, items, tenant_no)
  1623. items = _enrich_unfinished_badge_counts(db, items)
  1624. items = _enrich_work_order_qc_conclusions(db, items)
  1625. return items, int(total)
  1626. def list_work_orders_by_sample(
  1627. db: Session,
  1628. *,
  1629. sample_code: str,
  1630. reception_source: str,
  1631. tenant_no: Optional[str],
  1632. user_info: Optional[Dict[str, Any]] = None,
  1633. ) -> List[Dict[str, Any]]:
  1634. """按样本编码精确查询入库质检工单(同协议样本跳转插入卡)。"""
  1635. code = (sample_code or "").strip()
  1636. rs = (reception_source or "").strip()
  1637. if not code or not rs:
  1638. return []
  1639. q = db.query(BizQcWorkOrder).filter(
  1640. BizQcWorkOrder.del_flag == 0,
  1641. BizQcWorkOrder.work_order_type == "inbound",
  1642. BizQcWorkOrder.sample_code == code,
  1643. BizQcWorkOrder.reception_source == rs,
  1644. )
  1645. if tenant_no:
  1646. q = q.filter(BizQcWorkOrder.tenant_no == tenant_no)
  1647. if user_info:
  1648. from app.core.data_scope import apply_data_scope
  1649. q = apply_data_scope(q, BizQcWorkOrder, user_info, db, tenant_no)
  1650. rows = q.order_by(BizQcWorkOrder.create_time.desc()).all()
  1651. items = [_wo_to_dict(x) for x in rows]
  1652. items = _enrich_work_order_list_items(db, items)
  1653. if tenant_no:
  1654. items = _enrich_work_order_qc_targets(db, tenant_no, items)
  1655. items = _enrich_production_work_order_refs(db, items)
  1656. items = _enrich_outbound_work_orders(db, tenant_no, items)
  1657. items = enrich_work_order_fee_summaries(db, items, tenant_no)
  1658. items = _enrich_unfinished_badge_counts(db, items)
  1659. items = _enrich_work_order_qc_conclusions(db, items)
  1660. return items
  1661. def build_work_order_list_summaries(
  1662. db: Session,
  1663. work_orders: List[BizQcWorkOrder],
  1664. tenant_no: Optional[str],
  1665. ) -> Dict[str, Dict[str, Any]]:
  1666. """批量构建列表级工单摘要(与 list_work_orders 单项字段一致,非详情接口)。"""
  1667. if not work_orders:
  1668. return {}
  1669. unique: Dict[str, BizQcWorkOrder] = {}
  1670. for wo in work_orders:
  1671. wid = (wo.id or "").strip()
  1672. if wid and wid not in unique:
  1673. unique[wid] = wo
  1674. items = [_wo_to_dict(wo) for wo in unique.values()]
  1675. items = _enrich_work_order_list_items(db, items)
  1676. if tenant_no:
  1677. items = _enrich_work_order_qc_targets(db, tenant_no, items)
  1678. items = _enrich_production_work_order_refs(db, items)
  1679. items = _enrich_outbound_work_orders(db, tenant_no, items)
  1680. items = enrich_work_order_fee_summaries(db, items, tenant_no)
  1681. items = _enrich_unfinished_badge_counts(db, items)
  1682. items = _enrich_work_order_qc_conclusions(db, items)
  1683. return {item["id"]: item for item in items if item.get("id")}
  1684. _QC_TAB_COUNT_KEYS = (
  1685. "all",
  1686. "inbound_cell_storage",
  1687. "inbound_research_donation",
  1688. "outbound_pilot_service",
  1689. "outbound_basic_experiment",
  1690. "temporary",
  1691. )
  1692. def _empty_qc_tab_counts() -> Dict[str, int]:
  1693. return {k: 0 for k in _QC_TAB_COUNT_KEYS}
  1694. def _resolve_wo_tab_key(
  1695. wo_type: Optional[str],
  1696. reception_source: Optional[str],
  1697. biz_type: Optional[str],
  1698. ) -> str:
  1699. wt = (wo_type or "inbound").strip() or "inbound"
  1700. if wt == "temporary":
  1701. return "temporary"
  1702. if wt == "outbound":
  1703. bt = (biz_type or "pilot_service").strip() or "pilot_service"
  1704. if bt == "basic_experiment":
  1705. return "outbound_basic_experiment"
  1706. if bt == "clinical_application":
  1707. return "outbound_clinical_application"
  1708. return "outbound_pilot_service"
  1709. rs = (reception_source or "cell_storage").strip() or "cell_storage"
  1710. return "inbound_research_donation" if rs == "research_donation" else "inbound_cell_storage"
  1711. def _unfinished_work_order_filter(q):
  1712. return q.filter(
  1713. BizQcWorkOrder.work_order_status.in_(("dpz", "ypz")),
  1714. ~and_(
  1715. BizQcWorkOrder.work_order_type == "outbound",
  1716. BizQcWorkOrder.biz_type == "clinical_application",
  1717. ),
  1718. )
  1719. def _enrich_unfinished_badge_counts(
  1720. db: Session, items: List[Dict[str, Any]]
  1721. ) -> List[Dict[str, Any]]:
  1722. """列表项补充未完成卡片角标:待配置=1,已配置=未完成质检任务数(至少 1)。"""
  1723. if not items:
  1724. return items
  1725. unfinished_ids = [
  1726. x["id"]
  1727. for x in items
  1728. if (x.get("id") or "").strip() and x.get("workOrderStatus") in ("dpz", "ypz")
  1729. ]
  1730. task_counts: Dict[str, int] = {}
  1731. if unfinished_ids:
  1732. rows = (
  1733. db.query(BizQcTask.work_order_id, func.count(BizQcTask.id))
  1734. .filter(
  1735. BizQcTask.del_flag == 0,
  1736. BizQcTask.work_order_id.in_(unfinished_ids),
  1737. BizQcTask.task_status.notin_(("ywc", "yzf")),
  1738. )
  1739. .group_by(BizQcTask.work_order_id)
  1740. .all()
  1741. )
  1742. task_counts = {wo_id: int(cnt or 0) for wo_id, cnt in rows}
  1743. for item in items:
  1744. status = item.get("workOrderStatus") or ""
  1745. if status == "dpz":
  1746. item["unfinishedBadgeCount"] = 1
  1747. elif status == "ypz":
  1748. pending = task_counts.get(item.get("id") or "", 0)
  1749. item["unfinishedBadgeCount"] = max(pending, 1)
  1750. else:
  1751. item["unfinishedBadgeCount"] = 0
  1752. return items
  1753. def _enrich_work_order_qc_conclusions(
  1754. db: Session, items: List[Dict[str, Any]]
  1755. ) -> List[Dict[str, Any]]:
  1756. """列表项补充工单级质检结论:优先落库 conclusion,否则由任务结论派生。"""
  1757. if not items:
  1758. return items
  1759. need_derive_ids = [
  1760. (x.get("id") or "").strip()
  1761. for x in items
  1762. if (x.get("id") or "").strip()
  1763. and (x.get("qcConclusion") or "").strip() not in _VALID_QC_WORK_ORDER_CONCLUSIONS
  1764. ]
  1765. conclusions_by_wo: Dict[str, List[Optional[str]]] = defaultdict(list)
  1766. if need_derive_ids:
  1767. rows = (
  1768. db.query(BizQcTask.work_order_id, BizQcTask.conclusion)
  1769. .filter(
  1770. BizQcTask.del_flag == 0,
  1771. BizQcTask.work_order_id.in_(need_derive_ids),
  1772. )
  1773. .all()
  1774. )
  1775. for wo_id, conclusion in rows:
  1776. wid = (wo_id or "").strip()
  1777. if wid:
  1778. conclusions_by_wo[wid].append(conclusion)
  1779. for item in items:
  1780. stored = (item.get("qcConclusion") or "").strip()
  1781. if stored in _VALID_QC_WORK_ORDER_CONCLUSIONS:
  1782. item["qcConclusion"] = stored
  1783. item["qcConclusionLabel"] = work_order_qc_conclusion_label(stored)
  1784. continue
  1785. wid = (item.get("id") or "").strip()
  1786. fields = build_qc_conclusion_fields(conclusions_by_wo.get(wid, []))
  1787. item["qcConclusion"] = fields["qcConclusion"]
  1788. item["qcConclusionLabel"] = fields["qcConclusionLabel"]
  1789. return items
  1790. def _aggregate_qc_tab_counts(rows) -> Dict[str, int]:
  1791. counts = _empty_qc_tab_counts()
  1792. for wo_type, rs, bt, cnt in rows:
  1793. tab_key = _resolve_wo_tab_key(wo_type, rs, bt)
  1794. if tab_key == "outbound_clinical_application":
  1795. continue
  1796. n = int(cnt or 0)
  1797. counts[tab_key] = counts.get(tab_key, 0) + n
  1798. counts["all"] += n
  1799. return counts
  1800. def count_unfinished_work_orders_by_tab(
  1801. db: Session,
  1802. *,
  1803. tenant_no: str,
  1804. user_info=None,
  1805. ) -> Dict[str, int]:
  1806. """按 Tab 统计未完成质检工单数(待配置 + 已配置)。"""
  1807. q = db.query(BizQcWorkOrder).filter(
  1808. BizQcWorkOrder.del_flag == 0,
  1809. BizQcWorkOrder.tenant_no == tenant_no,
  1810. )
  1811. q = _unfinished_work_order_filter(q)
  1812. if user_info:
  1813. from app.core.data_scope import apply_data_scope
  1814. q = apply_data_scope(q, BizQcWorkOrder, user_info, db, tenant_no)
  1815. rows = (
  1816. q.with_entities(
  1817. BizQcWorkOrder.work_order_type,
  1818. BizQcWorkOrder.reception_source,
  1819. BizQcWorkOrder.biz_type,
  1820. func.count(BizQcWorkOrder.id),
  1821. )
  1822. .group_by(
  1823. BizQcWorkOrder.work_order_type,
  1824. BizQcWorkOrder.reception_source,
  1825. BizQcWorkOrder.biz_type,
  1826. )
  1827. .all()
  1828. )
  1829. return _aggregate_qc_tab_counts(rows)
  1830. def count_mine_unfinished_work_orders_by_tab(
  1831. db: Session,
  1832. *,
  1833. tenant_no: str,
  1834. assignee_user_id: str,
  1835. ) -> Dict[str, int]:
  1836. """我的质检任务:按 Tab 统计当前用户负责且未完成的质检工单数。"""
  1837. q = (
  1838. db.query(
  1839. BizQcWorkOrder.work_order_type,
  1840. BizQcWorkOrder.reception_source,
  1841. BizQcWorkOrder.biz_type,
  1842. func.count(func.distinct(BizQcWorkOrder.id)),
  1843. )
  1844. .join(
  1845. BizQcTask,
  1846. and_(
  1847. BizQcTask.work_order_id == BizQcWorkOrder.id,
  1848. BizQcTask.del_flag == 0,
  1849. ),
  1850. )
  1851. .join(
  1852. BizQcTaskAssignee,
  1853. and_(
  1854. eq_unicode(BizQcTaskAssignee.task_id, BizQcTask.id),
  1855. unicode_ci(BizQcTaskAssignee.user_id) == assignee_user_id,
  1856. BizQcTaskAssignee.del_flag == 0,
  1857. ),
  1858. )
  1859. .filter(
  1860. BizQcWorkOrder.del_flag == 0,
  1861. BizQcWorkOrder.tenant_no == tenant_no,
  1862. )
  1863. )
  1864. q = _unfinished_work_order_filter(q)
  1865. rows = (
  1866. q.group_by(
  1867. BizQcWorkOrder.work_order_type,
  1868. BizQcWorkOrder.reception_source,
  1869. BizQcWorkOrder.biz_type,
  1870. )
  1871. .all()
  1872. )
  1873. return _aggregate_qc_tab_counts(rows)
  1874. def get_work_order(
  1875. db: Session, row_id: str, tenant_no: Optional[str] = None
  1876. ) -> Optional[BizQcWorkOrder]:
  1877. q = db.query(BizQcWorkOrder).filter(BizQcWorkOrder.id == row_id, BizQcWorkOrder.del_flag == 0)
  1878. if tenant_no:
  1879. q = q.filter(BizQcWorkOrder.tenant_no == tenant_no)
  1880. return q.first()
  1881. def _lab_lines(db: Session, work_order_id: str) -> List[Dict[str, Any]]:
  1882. rows = (
  1883. db.query(BizQcWorkOrderLabItem)
  1884. .filter(
  1885. BizQcWorkOrderLabItem.work_order_id == work_order_id,
  1886. BizQcWorkOrderLabItem.del_flag == 0,
  1887. )
  1888. .order_by(BizQcWorkOrderLabItem.order_num.asc())
  1889. .all()
  1890. )
  1891. return [
  1892. {
  1893. "id": r.id,
  1894. "labTestItemId": r.lab_test_item_id,
  1895. "itemCode": r.item_code,
  1896. "itemName": r.item_name,
  1897. }
  1898. for r in rows
  1899. ]
  1900. def _third_lines(db: Session, work_order_id: str) -> List[Dict[str, Any]]:
  1901. rows = (
  1902. db.query(BizQcWorkOrderThirdItem)
  1903. .filter(
  1904. BizQcWorkOrderThirdItem.work_order_id == work_order_id,
  1905. BizQcWorkOrderThirdItem.del_flag == 0,
  1906. )
  1907. .order_by(BizQcWorkOrderThirdItem.order_num.asc())
  1908. .all()
  1909. )
  1910. return [
  1911. {
  1912. "id": r.id,
  1913. "thirdPartyItemId": r.third_party_item_id,
  1914. "companyId": r.company_id,
  1915. "companyName": r.company_name,
  1916. "itemCode": r.item_code,
  1917. "itemName": r.item_name,
  1918. "feeAmount": _dec(r.fee_amount),
  1919. }
  1920. for r in rows
  1921. ]
  1922. def _qc_info_item(db: Session, wo: BizQcWorkOrder, tenant_no: str) -> Dict[str, Any]:
  1923. from app.crud import qc_inspection_batch as batch_svc
  1924. from app.crud import qc_task_result as result_svc
  1925. tasks = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
  1926. task_ids = [str(t.get("id") or "").strip() for t in tasks if t.get("id")]
  1927. results_map = (
  1928. result_svc.list_saved_results_by_task_ids(db, task_ids, tenant_no) if task_ids else {}
  1929. )
  1930. for t in tasks:
  1931. tt = (t.get("taskType") or "").strip()
  1932. ts = (t.get("taskStatus") or "").strip()
  1933. tc = (t.get("conclusion") or "").strip()
  1934. t["taskTypeLabel"] = _TASK_TYPE_LABELS.get(tt, tt)
  1935. t["taskStatusLabel"] = _TASK_STATUS_LABELS.get(ts, ts)
  1936. t["conclusionLabel"] = t.get("conclusionLabel") or _CONCLUSION_LABELS.get(tc, tc or "—")
  1937. tid = str(t.get("id") or "").strip()
  1938. t["results"] = results_map.get(tid, []) if tid else []
  1939. wo_status = (wo.work_order_status or "").strip()
  1940. biz = _work_order_qc_biz_type(wo)
  1941. return {
  1942. **_wo_to_dict(wo),
  1943. "workOrderStatusLabel": _WORK_ORDER_STATUS_LABELS.get(wo_status, wo_status),
  1944. "qcBizTypeLabel": _qc_biz_type_label(biz),
  1945. "labItems": _lab_lines(db, wo.id),
  1946. "thirdPartyItems": _third_lines(db, wo.id),
  1947. "inspectionBatches": batch_svc.list_inspection_batches(db, wo.id, tenant_no),
  1948. "tasks": tasks,
  1949. }
  1950. def get_qc_info_map_for_samples(
  1951. db: Session,
  1952. sample_ids: List[str],
  1953. tenant_no: str,
  1954. *,
  1955. sample_code_by_id: Optional[Dict[str, str]] = None,
  1956. ) -> Dict[str, Dict[str, Any]]:
  1957. """按样本批量返回门禁质检工单摘要(trigger_source=reception),供协议/捐赠查看弹窗使用。
  1958. 细胞存储:传入 sample_code_by_id 时,按租户 + 样本编码关联。
  1959. 科研捐赠等:仅传 sample_ids 时,仍按 protocol_sample_id(存捐赠样本 id)关联。
  1960. """
  1961. if not tenant_no:
  1962. return {}
  1963. if sample_code_by_id:
  1964. codes = list({(c or "").strip() for c in sample_code_by_id.values() if (c or "").strip()})
  1965. if not codes:
  1966. return {}
  1967. wos = (
  1968. db.query(BizQcWorkOrder)
  1969. .filter(
  1970. BizQcWorkOrder.tenant_no == tenant_no,
  1971. BizQcWorkOrder.del_flag == 0,
  1972. BizQcWorkOrder.reception_source == "cell_storage",
  1973. BizQcWorkOrder.trigger_source == "reception",
  1974. BizQcWorkOrder.sample_code.in_(codes),
  1975. )
  1976. .all()
  1977. )
  1978. code_to_wo: Dict[str, BizQcWorkOrder] = {}
  1979. for wo in wos:
  1980. code = (wo.sample_code or "").strip()
  1981. if code and code not in code_to_wo:
  1982. code_to_wo[code] = wo
  1983. out: Dict[str, Dict[str, Any]] = {}
  1984. for sid, raw_code in sample_code_by_id.items():
  1985. code = (raw_code or "").strip()
  1986. if not code:
  1987. continue
  1988. wo = code_to_wo.get(code)
  1989. if wo:
  1990. out[sid] = _qc_info_item(db, wo, tenant_no)
  1991. return out
  1992. if not sample_ids:
  1993. return {}
  1994. wos = (
  1995. db.query(BizQcWorkOrder)
  1996. .filter(
  1997. BizQcWorkOrder.protocol_sample_id.in_(sample_ids),
  1998. BizQcWorkOrder.tenant_no == tenant_no,
  1999. BizQcWorkOrder.del_flag == 0,
  2000. BizQcWorkOrder.trigger_source == "reception",
  2001. )
  2002. .all()
  2003. )
  2004. out = {}
  2005. for wo in wos:
  2006. sid = (wo.protocol_sample_id or "").strip()
  2007. if sid and sid not in out:
  2008. out[sid] = _qc_info_item(db, wo, tenant_no)
  2009. return out
  2010. def _process_qc_info_items_from_wos(
  2011. db: Session, wos: List[BizQcWorkOrder], tenant_no: str
  2012. ) -> List[Dict[str, Any]]:
  2013. if not wos:
  2014. return []
  2015. items = [_qc_info_item(db, wo, tenant_no) for wo in wos]
  2016. items = _enrich_work_order_list_items(db, items)
  2017. items = _enrich_work_order_qc_targets(db, tenant_no, items)
  2018. items = _enrich_production_work_order_refs(db, items)
  2019. return items
  2020. def get_process_qc_info_map_for_samples(
  2021. db: Session,
  2022. sample_ids: List[str],
  2023. tenant_no: str,
  2024. *,
  2025. sample_code_by_id: Optional[Dict[str, str]] = None,
  2026. ) -> Dict[str, List[Dict[str, Any]]]:
  2027. """按样本批量返回生产过程触发的入库质检工单摘要。"""
  2028. if not tenant_no:
  2029. return {}
  2030. if sample_code_by_id:
  2031. codes = list({(c or "").strip() for c in sample_code_by_id.values() if (c or "").strip()})
  2032. if not codes:
  2033. return {}
  2034. wos = (
  2035. db.query(BizQcWorkOrder)
  2036. .filter(
  2037. BizQcWorkOrder.tenant_no == tenant_no,
  2038. BizQcWorkOrder.del_flag == 0,
  2039. BizQcWorkOrder.work_order_type == "inbound",
  2040. BizQcWorkOrder.trigger_source == "production",
  2041. BizQcWorkOrder.sample_code.in_(codes),
  2042. )
  2043. .order_by(BizQcWorkOrder.create_time.asc())
  2044. .all()
  2045. )
  2046. code_to_wos: Dict[str, List[BizQcWorkOrder]] = {}
  2047. for wo in wos:
  2048. code = (wo.sample_code or "").strip()
  2049. if code:
  2050. code_to_wos.setdefault(code, []).append(wo)
  2051. out: Dict[str, List[Dict[str, Any]]] = {}
  2052. for sid, raw_code in sample_code_by_id.items():
  2053. code = (raw_code or "").strip()
  2054. if not code:
  2055. continue
  2056. batch = code_to_wos.get(code)
  2057. if batch:
  2058. out[sid] = _process_qc_info_items_from_wos(db, batch, tenant_no)
  2059. return out
  2060. if not sample_ids:
  2061. return {}
  2062. wos = (
  2063. db.query(BizQcWorkOrder)
  2064. .filter(
  2065. BizQcWorkOrder.protocol_sample_id.in_(sample_ids),
  2066. BizQcWorkOrder.tenant_no == tenant_no,
  2067. BizQcWorkOrder.del_flag == 0,
  2068. BizQcWorkOrder.work_order_type == "inbound",
  2069. BizQcWorkOrder.trigger_source == "production",
  2070. )
  2071. .order_by(BizQcWorkOrder.create_time.asc())
  2072. .all()
  2073. )
  2074. sid_to_wos: Dict[str, List[BizQcWorkOrder]] = {}
  2075. for wo in wos:
  2076. sid = (wo.protocol_sample_id or "").strip()
  2077. if sid:
  2078. sid_to_wos.setdefault(sid, []).append(wo)
  2079. out: Dict[str, List[Dict[str, Any]]] = {}
  2080. for sid, batch in sid_to_wos.items():
  2081. out[sid] = _process_qc_info_items_from_wos(db, batch, tenant_no)
  2082. return out
  2083. def list_process_qc_info_for_sample(
  2084. db: Session,
  2085. *,
  2086. sample_code: str,
  2087. tenant_no: str,
  2088. ) -> List[Dict[str, Any]]:
  2089. """按样本编码返回生产过程触发的入库质检工单摘要(供入库放行查看等)。"""
  2090. code = (sample_code or "").strip()
  2091. if not code or not tenant_no:
  2092. return []
  2093. wos = (
  2094. db.query(BizQcWorkOrder)
  2095. .filter(
  2096. BizQcWorkOrder.tenant_no == tenant_no,
  2097. BizQcWorkOrder.del_flag == 0,
  2098. BizQcWorkOrder.work_order_type == "inbound",
  2099. BizQcWorkOrder.trigger_source == "production",
  2100. BizQcWorkOrder.sample_code == code,
  2101. )
  2102. .order_by(BizQcWorkOrder.create_time.asc())
  2103. .all()
  2104. )
  2105. return _process_qc_info_items_from_wos(db, wos, tenant_no)
  2106. def get_schedule_gate_qc_info(
  2107. db: Session,
  2108. *,
  2109. schedule_id: str,
  2110. tenant_no: str,
  2111. ) -> Optional[Dict[str, Any]]:
  2112. """出库计划自动质检(trigger_source=schedule),供输注计划/出库放行查看。"""
  2113. sid = (schedule_id or "").strip()
  2114. if not sid or not tenant_no:
  2115. return None
  2116. wo = (
  2117. db.query(BizQcWorkOrder)
  2118. .filter(
  2119. BizQcWorkOrder.tenant_no == tenant_no,
  2120. BizQcWorkOrder.del_flag == 0,
  2121. BizQcWorkOrder.work_order_type == "outbound",
  2122. BizQcWorkOrder.infusion_schedule_id == sid,
  2123. BizQcWorkOrder.trigger_source == "schedule",
  2124. )
  2125. .order_by(BizQcWorkOrder.create_time.asc())
  2126. .first()
  2127. )
  2128. if not wo:
  2129. return None
  2130. return _qc_info_item(db, wo, tenant_no)
  2131. def list_process_qc_info_for_schedule(
  2132. db: Session,
  2133. *,
  2134. schedule_id: str,
  2135. tenant_no: str,
  2136. ) -> List[Dict[str, Any]]:
  2137. """按输注计划返回生产过程触发的出库质检工单摘要。"""
  2138. sid = (schedule_id or "").strip()
  2139. if not sid or not tenant_no:
  2140. return []
  2141. wos = (
  2142. db.query(BizQcWorkOrder)
  2143. .filter(
  2144. BizQcWorkOrder.tenant_no == tenant_no,
  2145. BizQcWorkOrder.del_flag == 0,
  2146. BizQcWorkOrder.work_order_type == "outbound",
  2147. BizQcWorkOrder.infusion_schedule_id == sid,
  2148. BizQcWorkOrder.trigger_source == "production",
  2149. )
  2150. .order_by(BizQcWorkOrder.create_time.asc())
  2151. .all()
  2152. )
  2153. return _process_qc_info_items_from_wos(db, wos, tenant_no)
  2154. def list_process_qc_info_for_production_work_order(
  2155. db: Session,
  2156. *,
  2157. production_work_order_id: str,
  2158. tenant_no: str,
  2159. ) -> List[Dict[str, Any]]:
  2160. """按生产工单返回生产过程派发的质检工单摘要。"""
  2161. pid = (production_work_order_id or "").strip()
  2162. if not pid or not tenant_no:
  2163. return []
  2164. wos = (
  2165. db.query(BizQcWorkOrder)
  2166. .filter(
  2167. BizQcWorkOrder.tenant_no == tenant_no,
  2168. BizQcWorkOrder.del_flag == 0,
  2169. BizQcWorkOrder.production_work_order_id == pid,
  2170. BizQcWorkOrder.trigger_source == "production",
  2171. )
  2172. .order_by(BizQcWorkOrder.create_time.asc())
  2173. .all()
  2174. )
  2175. return _process_qc_info_items_from_wos(db, wos, tenant_no)
  2176. def _gate_qc_info_for_inbound_production_work_order(
  2177. db: Session, pwo, tenant_no: str
  2178. ) -> Optional[Dict[str, Any]]:
  2179. """入库生产工单关联的样本接收质检(trigger_source=reception)。"""
  2180. from app.models.models import BizProductionWorkOrder
  2181. if not isinstance(pwo, BizProductionWorkOrder):
  2182. return None
  2183. source = (getattr(pwo, "reception_source", None) or "cell_storage").strip()
  2184. if source not in ("cell_storage", "research_donation"):
  2185. return None
  2186. q = db.query(BizQcWorkOrder).filter(
  2187. BizQcWorkOrder.tenant_no == tenant_no,
  2188. BizQcWorkOrder.del_flag == 0,
  2189. BizQcWorkOrder.trigger_source == "reception",
  2190. )
  2191. wo = None
  2192. if source == "cell_storage":
  2193. code = (pwo.sample_code or "").strip()
  2194. if code:
  2195. wo = q.filter(
  2196. BizQcWorkOrder.reception_source == "cell_storage",
  2197. BizQcWorkOrder.sample_code == code,
  2198. ).first()
  2199. else:
  2200. psid = (pwo.protocol_sample_id or "").strip()
  2201. if psid:
  2202. wo = q.filter(BizQcWorkOrder.protocol_sample_id == psid).first()
  2203. if not wo:
  2204. code = (pwo.sample_code or "").strip()
  2205. if code:
  2206. wo = q.filter(BizQcWorkOrder.sample_code == code).first()
  2207. if not wo and pwo.reception_id:
  2208. wo = q.filter(BizQcWorkOrder.reception_id == pwo.reception_id).first()
  2209. if not wo:
  2210. return None
  2211. return _qc_info_item(db, wo, tenant_no)
  2212. def get_production_work_order_qc_info(
  2213. db: Session,
  2214. *,
  2215. production_work_order_id: str,
  2216. tenant_no: str,
  2217. ) -> Dict[str, Any]:
  2218. """生产工单质检只读摘要:入库含样本接收质检 + 本工单派发的生产过程质检。"""
  2219. from app.models.models import BizProductionWorkOrder, BizSampleReception
  2220. pid = (production_work_order_id or "").strip()
  2221. if not pid or not tenant_no:
  2222. raise ValueError("生产工单不存在")
  2223. pwo = (
  2224. db.query(BizProductionWorkOrder)
  2225. .filter(
  2226. BizProductionWorkOrder.id == pid,
  2227. BizProductionWorkOrder.tenant_no == tenant_no,
  2228. BizProductionWorkOrder.del_flag == 0,
  2229. )
  2230. .first()
  2231. )
  2232. if not pwo:
  2233. raise ValueError("生产工单不存在")
  2234. wo_type = (getattr(pwo, "work_order_type", None) or "inbound").strip()
  2235. reception_source = (getattr(pwo, "reception_source", None) or "cell_storage").strip()
  2236. show_gate_section = (
  2237. wo_type == "inbound"
  2238. and reception_source in ("cell_storage", "research_donation")
  2239. )
  2240. gate_qc_info = None
  2241. sample_reception = None
  2242. if show_gate_section:
  2243. gate_qc_info = _gate_qc_info_for_inbound_production_work_order(db, pwo, tenant_no)
  2244. rec = None
  2245. if pwo.reception_id:
  2246. rec = (
  2247. db.query(BizSampleReception)
  2248. .filter(
  2249. BizSampleReception.id == pwo.reception_id,
  2250. BizSampleReception.del_flag == 0,
  2251. )
  2252. .first()
  2253. )
  2254. if rec:
  2255. sample_reception = {"acceptConclusion": rec.accept_conclusion or ""}
  2256. process_qc_info_list = list_process_qc_info_for_production_work_order(
  2257. db, production_work_order_id=pwo.id, tenant_no=tenant_no
  2258. )
  2259. return {
  2260. "showGateSection": show_gate_section,
  2261. "gateQcInfo": gate_qc_info,
  2262. "processQcInfoList": process_qc_info_list,
  2263. "sampleReception": sample_reception,
  2264. }
  2265. def _resolve_reception_id_for_work_order_detail(
  2266. db: Session, wo: BizQcWorkOrder, tenant_no: str
  2267. ) -> Optional[str]:
  2268. """详情接口解析接收记录 ID:优先 reception_id,生产过程工单按 sample_code 回查。"""
  2269. rid = (wo.reception_id or "").strip()
  2270. if rid:
  2271. return rid
  2272. code = (wo.sample_code or "").strip()
  2273. if not code:
  2274. return None
  2275. q = db.query(BizSampleReception).filter(
  2276. BizSampleReception.tenant_no == tenant_no,
  2277. BizSampleReception.del_flag == 0,
  2278. BizSampleReception.sample_code == code,
  2279. )
  2280. source = (getattr(wo, "reception_source", None) or "").strip()
  2281. if source:
  2282. q = q.filter(BizSampleReception.reception_source == source)
  2283. rec = q.order_by(BizSampleReception.reception_time.desc()).first()
  2284. return rec.id if rec else None
  2285. def get_work_order_detail(
  2286. db: Session, row_id: str, tenant_no: str
  2287. ) -> Dict[str, Any]:
  2288. wo = get_work_order(db, row_id, tenant_no=tenant_no)
  2289. if not wo:
  2290. raise ValueError("工单不存在")
  2291. out = _wo_to_dict(wo)
  2292. enriched_list = _enrich_work_order_list_items(db, [out])
  2293. if tenant_no:
  2294. enriched_list = _enrich_work_order_qc_targets(db, tenant_no, enriched_list)
  2295. enriched_list = _enrich_work_order_qc_conclusions(db, enriched_list)
  2296. enriched = enriched_list[0]
  2297. out.update(
  2298. {
  2299. "receptionSource": enriched.get("receptionSource", out.get("receptionSource", "")),
  2300. "receptionTime": enriched.get("receptionTime", ""),
  2301. "handlerName": enriched.get("handlerName", ""),
  2302. "labStorageCode": enriched.get("labStorageCode", ""),
  2303. "sampleTypeName": enriched.get("sampleTypeName", ""),
  2304. "receptionSourceLabel": enriched.get("receptionSourceLabel", ""),
  2305. "motherName": enriched.get("motherName", ""),
  2306. "newbornName": enriched.get("newbornName", ""),
  2307. "protocolStatus": enriched.get("protocolStatus", ""),
  2308. "protocolStatusLabel": enriched.get("protocolStatusLabel", ""),
  2309. "cellProductCode": enriched.get("cellProductCode", ""),
  2310. "cellProductName": enriched.get("cellProductName", ""),
  2311. "cellProductTypeName": enriched.get("cellProductTypeName", ""),
  2312. "cellProductPassage": enriched.get("cellProductPassage", ""),
  2313. "cellProductSpecName": enriched.get("cellProductSpecName", ""),
  2314. "qcConclusion": enriched.get("qcConclusion", ""),
  2315. "qcConclusionLabel": enriched.get("qcConclusionLabel", ""),
  2316. }
  2317. )
  2318. resolved_reception_id = _resolve_reception_id_for_work_order_detail(db, wo, tenant_no)
  2319. reception_detail = (
  2320. sample_reception_svc.get_reception_by_id(db, resolved_reception_id, tenant_no)
  2321. if resolved_reception_id
  2322. else None
  2323. )
  2324. if reception_detail:
  2325. out["reception"] = reception_detail
  2326. out["productionReception"] = reception_detail.get("productionReception")
  2327. else:
  2328. rec = (
  2329. db.query(BizSampleReception)
  2330. .filter(
  2331. BizSampleReception.id == resolved_reception_id,
  2332. BizSampleReception.del_flag == 0,
  2333. )
  2334. .first()
  2335. if resolved_reception_id
  2336. else None
  2337. )
  2338. cr = (
  2339. check_result_svc.get_check_results_for_reception(db, rec, tenant_no) if rec else []
  2340. )
  2341. out["reception"] = {
  2342. "receptionTime": _fmt_dt(rec.reception_time) if rec else "",
  2343. "handlerName": (rec.handler_name if rec else "") or "",
  2344. "labStorageCode": (rec.lab_storage_code if rec else "") or "",
  2345. "acceptConclusion": (rec.accept_conclusion if rec else "") or "",
  2346. "acceptConclusionLabel": _accept_conclusion_label(
  2347. rec.accept_conclusion if rec else ""
  2348. ),
  2349. "sourceType": wo.reception_source or "cell_storage",
  2350. "sourceTypeLabel": _reception_source_label(wo.reception_source),
  2351. "sampleTypeName": enriched.get("sampleTypeName", ""),
  2352. "checkResults": cr,
  2353. "receiveAttachments": (rec.receive_attachments if rec else "") or "",
  2354. "receiveAttachmentFiles": urls_to_attachment_list(
  2355. rec.receive_attachments if rec else None, db, tenant_no
  2356. ),
  2357. }
  2358. out["labItems"] = _lab_lines(db, wo.id)
  2359. out["thirdPartyItems"] = _third_lines(db, wo.id)
  2360. out["tasks"] = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
  2361. from app.crud import qc_inspection_batch as batch_svc
  2362. out["inspectionBatches"] = batch_svc.list_inspection_batches(db, wo.id, tenant_no)
  2363. enriched_fee = enrich_work_order_fee_summaries(db, [out], tenant_no)
  2364. if enriched_fee:
  2365. out["paymentSummary"] = enriched_fee[0].get("paymentSummary")
  2366. out["feeSummary"] = enriched_fee[0].get("feeSummary")
  2367. if tenant_no:
  2368. _enrich_production_work_order_refs(db, [out])
  2369. return out
  2370. def _soft_delete_tasks_by_source_line_ids(
  2371. db: Session, source_line_ids: List[str], tenant_no: str
  2372. ) -> None:
  2373. """软删指定明细行关联的质检任务及其检测结果。"""
  2374. if not source_line_ids:
  2375. return
  2376. tasks = (
  2377. db.query(BizQcTask)
  2378. .filter(
  2379. BizQcTask.source_line_id.in_(source_line_ids),
  2380. BizQcTask.tenant_no == tenant_no,
  2381. BizQcTask.del_flag == 0,
  2382. )
  2383. .all()
  2384. )
  2385. if not tasks:
  2386. return
  2387. now = datetime.now()
  2388. task_ids = [t.id for t in tasks]
  2389. assignee_svc.soft_delete_assignees_by_task_ids(db, task_ids, tenant_no)
  2390. db.query(BizQcTaskResult).filter(
  2391. BizQcTaskResult.task_id.in_(task_ids),
  2392. BizQcTaskResult.tenant_no == tenant_no,
  2393. BizQcTaskResult.del_flag == 0,
  2394. ).update(
  2395. {"del_flag": 1, "update_time": now},
  2396. synchronize_session="fetch",
  2397. )
  2398. for t in tasks:
  2399. t.del_flag = 1
  2400. t.update_time = now
  2401. db.add(t)
  2402. def _get_task_by_source_line_id(
  2403. db: Session, source_line_id: str, tenant_no: str
  2404. ) -> Optional[BizQcTask]:
  2405. return (
  2406. db.query(BizQcTask)
  2407. .filter(
  2408. BizQcTask.source_line_id == source_line_id,
  2409. BizQcTask.tenant_no == tenant_no,
  2410. BizQcTask.del_flag == 0,
  2411. )
  2412. .first()
  2413. )
  2414. def _apply_item_assignees(
  2415. db: Session,
  2416. task: Optional[BizQcTask],
  2417. item_master_id: str,
  2418. assignees_map: Optional[Dict[str, List[str]]],
  2419. *,
  2420. tenant_no: str,
  2421. user_id: Optional[str],
  2422. create_dept_id: str,
  2423. item_label: str,
  2424. ) -> None:
  2425. if not task:
  2426. return
  2427. user_ids = (assignees_map or {}).get(item_master_id) or []
  2428. if not user_ids:
  2429. raise ValueError(f"请为{item_label}指定负责人")
  2430. assignee_svc.sync_assignees_for_task(
  2431. db,
  2432. task.id,
  2433. user_ids,
  2434. tenant_no=tenant_no,
  2435. operator_id=user_id,
  2436. create_dept_id=create_dept_id,
  2437. )
  2438. def _sync_work_order_lab_items(
  2439. db: Session,
  2440. wo: BizQcWorkOrder,
  2441. *,
  2442. order_source: str,
  2443. lab_item_ids: List[str],
  2444. lab_item_assignees: Optional[Dict[str, List[str]]] = None,
  2445. tenant_no: str,
  2446. user_id: Optional[str],
  2447. create_dept_id: str,
  2448. seq_start: int = 0,
  2449. ) -> int:
  2450. """比对同步实验室项目明细:保留已有任务数据,仅增删变更项。"""
  2451. now = datetime.now()
  2452. existing_rows = (
  2453. db.query(BizQcWorkOrderLabItem)
  2454. .filter(
  2455. BizQcWorkOrderLabItem.work_order_id == wo.id,
  2456. BizQcWorkOrderLabItem.order_source == order_source,
  2457. BizQcWorkOrderLabItem.del_flag == 0,
  2458. )
  2459. .all()
  2460. )
  2461. existing_by_item_id = {r.lab_test_item_id: r for r in existing_rows}
  2462. new_id_set = set(lab_item_ids)
  2463. old_id_set = set(existing_by_item_id.keys())
  2464. remove_line_ids = [existing_by_item_id[i].id for i in old_id_set - new_id_set]
  2465. for item_id in old_id_set - new_id_set:
  2466. line = existing_by_item_id[item_id]
  2467. line.del_flag = 1
  2468. line.update_by = user_id
  2469. line.update_time = now
  2470. db.add(line)
  2471. _soft_delete_tasks_by_source_line_ids(db, remove_line_ids, tenant_no)
  2472. seq = seq_start
  2473. for lid in lab_item_ids:
  2474. seq += 1
  2475. if lid in existing_by_item_id and lid in new_id_set:
  2476. line = existing_by_item_id[lid]
  2477. line.order_num = seq
  2478. line.update_by = user_id
  2479. line.update_time = now
  2480. db.add(line)
  2481. task = _get_task_by_source_line_id(db, line.id, tenant_no)
  2482. if task:
  2483. task.order_num = seq
  2484. task.update_by = user_id
  2485. task.update_time = now
  2486. db.add(task)
  2487. else:
  2488. task = task_svc.create_task_for_line(
  2489. db,
  2490. work_order=wo,
  2491. task_type="sysz",
  2492. source_line_id=line.id,
  2493. item_code=line.item_code,
  2494. item_name=line.item_name,
  2495. tenant_no=tenant_no,
  2496. user_id=user_id,
  2497. create_dept_id=create_dept_id,
  2498. seq=seq,
  2499. order_source=order_source,
  2500. )
  2501. _apply_item_assignees(
  2502. db,
  2503. task,
  2504. lid,
  2505. lab_item_assignees,
  2506. tenant_no=tenant_no,
  2507. user_id=user_id,
  2508. create_dept_id=create_dept_id,
  2509. item_label="实验室自检项目",
  2510. )
  2511. continue
  2512. item = lab_item_svc.get_lab_item(db, lid, tenant_no=tenant_no)
  2513. if not item or item.status != "1":
  2514. raise ValueError("实验室项目无效或已停用")
  2515. line = BizQcWorkOrderLabItem(
  2516. id=generate_uuid(),
  2517. work_order_id=wo.id,
  2518. order_source=order_source,
  2519. lab_test_item_id=item.id,
  2520. item_code=item.item_code,
  2521. item_name=item.item_name,
  2522. tenant_no=tenant_no,
  2523. order_num=seq,
  2524. create_by=user_id,
  2525. create_dept_id=create_dept_id or "",
  2526. create_time=now,
  2527. update_by=user_id,
  2528. update_time=now,
  2529. del_flag=0,
  2530. )
  2531. db.add(line)
  2532. db.flush()
  2533. task = task_svc.create_task_for_line(
  2534. db,
  2535. work_order=wo,
  2536. task_type="sysz",
  2537. source_line_id=line.id,
  2538. item_code=line.item_code,
  2539. item_name=line.item_name,
  2540. tenant_no=tenant_no,
  2541. user_id=user_id,
  2542. create_dept_id=create_dept_id,
  2543. seq=seq,
  2544. order_source=order_source,
  2545. )
  2546. _apply_item_assignees(
  2547. db,
  2548. task,
  2549. lid,
  2550. lab_item_assignees,
  2551. tenant_no=tenant_no,
  2552. user_id=user_id,
  2553. create_dept_id=create_dept_id,
  2554. item_label="实验室自检项目",
  2555. )
  2556. return seq
  2557. def _sync_work_order_third_party_items(
  2558. db: Session,
  2559. wo: BizQcWorkOrder,
  2560. *,
  2561. order_source: str,
  2562. third_party_item_ids: List[str],
  2563. third_party_item_assignees: Optional[Dict[str, List[str]]] = None,
  2564. tenant_no: str,
  2565. user_id: Optional[str],
  2566. create_dept_id: str,
  2567. seq_start: int = 0,
  2568. ) -> Tuple[int, Decimal]:
  2569. """比对同步第三方项目明细:保留已有任务数据,仅增删变更项。"""
  2570. now = datetime.now()
  2571. existing_rows = (
  2572. db.query(BizQcWorkOrderThirdItem)
  2573. .filter(
  2574. BizQcWorkOrderThirdItem.work_order_id == wo.id,
  2575. BizQcWorkOrderThirdItem.order_source == order_source,
  2576. BizQcWorkOrderThirdItem.del_flag == 0,
  2577. )
  2578. .all()
  2579. )
  2580. existing_by_item_id = {r.third_party_item_id: r for r in existing_rows}
  2581. new_id_set = set(third_party_item_ids)
  2582. old_id_set = set(existing_by_item_id.keys())
  2583. remove_line_ids = [existing_by_item_id[i].id for i in old_id_set - new_id_set]
  2584. for item_id in old_id_set - new_id_set:
  2585. line = existing_by_item_id[item_id]
  2586. line.del_flag = 1
  2587. line.update_by = user_id
  2588. line.update_time = now
  2589. db.add(line)
  2590. _soft_delete_tasks_by_source_line_ids(db, remove_line_ids, tenant_no)
  2591. fee_total = Decimal("0")
  2592. seq = seq_start
  2593. for tid in third_party_item_ids:
  2594. seq += 1
  2595. if tid in existing_by_item_id and tid in new_id_set:
  2596. line = existing_by_item_id[tid]
  2597. line.order_num = seq
  2598. line.update_by = user_id
  2599. line.update_time = now
  2600. db.add(line)
  2601. fee_total += line.fee_amount or Decimal("0")
  2602. task = _get_task_by_source_line_id(db, line.id, tenant_no)
  2603. if task:
  2604. task.order_num = seq
  2605. task.update_by = user_id
  2606. task.update_time = now
  2607. db.add(task)
  2608. else:
  2609. task = task_svc.create_task_for_line(
  2610. db,
  2611. work_order=wo,
  2612. task_type="dsfjc",
  2613. source_line_id=line.id,
  2614. item_code=line.item_code,
  2615. item_name=line.item_name,
  2616. tenant_no=tenant_no,
  2617. user_id=user_id,
  2618. create_dept_id=create_dept_id,
  2619. seq=seq,
  2620. company_id=line.company_id,
  2621. company_name=line.company_name,
  2622. fee_amount=line.fee_amount,
  2623. order_source=order_source,
  2624. )
  2625. _apply_item_assignees(
  2626. db,
  2627. task,
  2628. tid,
  2629. third_party_item_assignees,
  2630. tenant_no=tenant_no,
  2631. user_id=user_id,
  2632. create_dept_id=create_dept_id,
  2633. item_label="第三方检测项目",
  2634. )
  2635. continue
  2636. item = third_item_svc.get_item(db, tid, tenant_no=tenant_no)
  2637. if not item or item.status != "1":
  2638. raise ValueError("第三方项目无效或已停用")
  2639. comp = get_company(db, item.company_id, tenant_no=tenant_no)
  2640. if not comp:
  2641. raise ValueError("第三方公司不存在")
  2642. fee = item.fee_amount or Decimal("0")
  2643. fee_total += fee
  2644. line = BizQcWorkOrderThirdItem(
  2645. id=generate_uuid(),
  2646. work_order_id=wo.id,
  2647. order_source=order_source,
  2648. third_party_item_id=item.id,
  2649. company_id=comp.id,
  2650. company_name=comp.company_name,
  2651. item_code=item.item_code,
  2652. item_name=item.item_name,
  2653. fee_amount=fee,
  2654. tenant_no=tenant_no,
  2655. order_num=seq,
  2656. create_by=user_id,
  2657. create_dept_id=create_dept_id or "",
  2658. create_time=now,
  2659. update_by=user_id,
  2660. update_time=now,
  2661. del_flag=0,
  2662. )
  2663. db.add(line)
  2664. db.flush()
  2665. task = task_svc.create_task_for_line(
  2666. db,
  2667. work_order=wo,
  2668. task_type="dsfjc",
  2669. source_line_id=line.id,
  2670. item_code=line.item_code,
  2671. item_name=line.item_name,
  2672. tenant_no=tenant_no,
  2673. user_id=user_id,
  2674. create_dept_id=create_dept_id,
  2675. seq=seq,
  2676. company_id=comp.id,
  2677. company_name=comp.company_name,
  2678. fee_amount=fee,
  2679. order_source=order_source,
  2680. )
  2681. _apply_item_assignees(
  2682. db,
  2683. task,
  2684. tid,
  2685. third_party_item_assignees,
  2686. tenant_no=tenant_no,
  2687. user_id=user_id,
  2688. create_dept_id=create_dept_id,
  2689. item_label="第三方检测项目",
  2690. )
  2691. return seq, fee_total
  2692. def configure_work_order(
  2693. db: Session,
  2694. work_order_id: str,
  2695. *,
  2696. has_lab: bool,
  2697. has_third_party: bool,
  2698. lab_item_ids: List[str],
  2699. third_party_item_ids: List[str],
  2700. lab_item_assignees: Optional[Dict[str, List[str]]] = None,
  2701. third_party_item_assignees: Optional[Dict[str, List[str]]] = None,
  2702. batches: Optional[List[Dict[str, Any]]] = None,
  2703. tenant_no: str,
  2704. user_id: Optional[str] = None,
  2705. create_dept_id: str = "",
  2706. ) -> BizQcWorkOrder:
  2707. from app.crud import qc_inspection_batch as batch_svc
  2708. wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
  2709. if not wo:
  2710. raise ValueError("工单不存在")
  2711. if batches is not None:
  2712. payload_batches = batches
  2713. else:
  2714. payload_batches = batch_svc.normalize_legacy_configure_to_batches(
  2715. {
  2716. "hasLab": has_lab,
  2717. "hasThirdParty": has_third_party,
  2718. "labItemIds": lab_item_ids,
  2719. "thirdPartyItemIds": third_party_item_ids,
  2720. "labItemAssignees": lab_item_assignees,
  2721. "thirdPartyItemAssignees": third_party_item_assignees,
  2722. }
  2723. )
  2724. return batch_svc.configure_work_order_batches(
  2725. db,
  2726. wo,
  2727. payload_batches,
  2728. tenant_no=tenant_no,
  2729. user_id=user_id,
  2730. create_dept_id=create_dept_id,
  2731. )
  2732. def try_complete_work_order(db: Session, work_order_id: str, tenant_no: str) -> None:
  2733. pending = (
  2734. db.query(BizQcTask)
  2735. .filter(
  2736. BizQcTask.work_order_id == work_order_id,
  2737. BizQcTask.tenant_no == tenant_no,
  2738. BizQcTask.del_flag == 0,
  2739. BizQcTask.task_status != "ywc",
  2740. )
  2741. .count()
  2742. )
  2743. if pending:
  2744. return
  2745. wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
  2746. if wo and wo.work_order_status == "ypz":
  2747. wo.work_order_status = "ywc"
  2748. task_conclusions = [
  2749. c
  2750. for (c,) in db.query(BizQcTask.conclusion)
  2751. .filter(
  2752. BizQcTask.work_order_id == work_order_id,
  2753. BizQcTask.tenant_no == tenant_no,
  2754. BizQcTask.del_flag == 0,
  2755. )
  2756. .all()
  2757. ]
  2758. derived = build_qc_conclusion_fields(task_conclusions)["qcConclusion"]
  2759. wo.conclusion = derived or None
  2760. wo.update_time = datetime.now()
  2761. db.add(wo)
  2762. from app.crud.pilot_outbound_status_sync import sync_schedule_from_qc_work_order
  2763. sync_schedule_from_qc_work_order(db, wo)
  2764. db.commit()
  2765. def try_reopen_work_order(db: Session, work_order_id: str, tenant_no: str) -> None:
  2766. """任务从已完成回退时,若工单已完成则恢复为已配置。"""
  2767. wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
  2768. if not wo or (wo.work_order_status or "").strip() != "ywc":
  2769. return
  2770. wo.work_order_status = "ypz"
  2771. wo.conclusion = None
  2772. wo.update_time = datetime.now()
  2773. db.add(wo)
  2774. from app.crud.pilot_outbound_status_sync import sync_schedule_from_qc_work_order
  2775. sync_schedule_from_qc_work_order(db, wo)
  2776. db.commit()
  2777. # ---------- 临时质检工单 ----------
  2778. def _temp_lab_lines(db: Session, work_order_id: str) -> List[Dict[str, Any]]:
  2779. rows = (
  2780. db.query(BizQcWorkOrderLabItem)
  2781. .filter(
  2782. BizQcWorkOrderLabItem.work_order_id == work_order_id,
  2783. BizQcWorkOrderLabItem.order_source == "temporary",
  2784. BizQcWorkOrderLabItem.del_flag == 0,
  2785. )
  2786. .order_by(BizQcWorkOrderLabItem.order_num.asc())
  2787. .all()
  2788. )
  2789. return [
  2790. {
  2791. "id": r.id,
  2792. "labTestItemId": r.lab_test_item_id,
  2793. "itemCode": r.item_code,
  2794. "itemName": r.item_name,
  2795. }
  2796. for r in rows
  2797. ]
  2798. def _temp_third_lines(db: Session, work_order_id: str) -> List[Dict[str, Any]]:
  2799. rows = (
  2800. db.query(BizQcWorkOrderThirdItem)
  2801. .filter(
  2802. BizQcWorkOrderThirdItem.work_order_id == work_order_id,
  2803. BizQcWorkOrderThirdItem.order_source == "temporary",
  2804. BizQcWorkOrderThirdItem.del_flag == 0,
  2805. )
  2806. .order_by(BizQcWorkOrderThirdItem.order_num.asc())
  2807. .all()
  2808. )
  2809. return [
  2810. {
  2811. "id": r.id,
  2812. "thirdPartyItemId": r.third_party_item_id,
  2813. "companyId": r.company_id,
  2814. "companyName": r.company_name,
  2815. "itemCode": r.item_code,
  2816. "itemName": r.item_name,
  2817. "feeAmount": _dec(r.fee_amount),
  2818. }
  2819. for r in rows
  2820. ]
  2821. def _temp_tasks(db: Session, work_order_id: str, tenant_no: str) -> List[Dict[str, Any]]:
  2822. rows = (
  2823. db.query(BizQcTask)
  2824. .filter(
  2825. BizQcTask.work_order_id == work_order_id,
  2826. BizQcTask.order_source == "temporary",
  2827. BizQcTask.tenant_no == tenant_no,
  2828. BizQcTask.del_flag == 0,
  2829. )
  2830. .order_by(BizQcTask.order_num.asc())
  2831. .all()
  2832. )
  2833. items = []
  2834. for t in rows:
  2835. d = task_svc._task_to_dict(t, "", db, tenant_no)
  2836. tt = (t.task_type or "").strip()
  2837. ts = (t.task_status or "").strip()
  2838. tc = (t.conclusion or "").strip()
  2839. d["taskTypeLabel"] = _TASK_TYPE_LABELS.get(tt, tt)
  2840. d["taskStatusLabel"] = _TASK_STATUS_LABELS.get(ts, ts)
  2841. d["conclusionLabel"] = _CONCLUSION_LABELS.get(tc, tc or "—")
  2842. items.append(d)
  2843. return items
  2844. def create_temp_qc_work_order(
  2845. db: Session,
  2846. *,
  2847. tenant_no: str,
  2848. user_id: Optional[str] = None,
  2849. create_dept_id: str = "",
  2850. cell_product_id: str = "",
  2851. external_target_code: str = "",
  2852. remark: str = "",
  2853. ) -> BizQcWorkOrder:
  2854. """创建临时质检工单(work_order_type='temporary',LSZJ前缀编号)"""
  2855. cp_id, ext_code = resolve_temp_qc_target(
  2856. cell_product_id=cell_product_id,
  2857. external_target_code=external_target_code,
  2858. )
  2859. if cp_id:
  2860. cp_id = _validate_cell_product_id(db, cp_id, tenant_no)
  2861. wo = BizQcWorkOrder(
  2862. id=generate_uuid(),
  2863. work_order_no=_gen_daily_no(db, tenant_no, "LSZJ"),
  2864. work_order_type="temporary",
  2865. reception_id=None,
  2866. protocol_sample_id=None,
  2867. collection_protocol_id=None,
  2868. protocol_no=None,
  2869. sample_code=None,
  2870. reception_source=None,
  2871. qc_biz_type="temporary",
  2872. trigger_source="manual",
  2873. cell_product_id=cp_id,
  2874. external_target_code=ext_code,
  2875. work_order_status="dpz",
  2876. has_lab="0",
  2877. has_third_party="0",
  2878. tenant_no=tenant_no,
  2879. remark=remark,
  2880. create_by=user_id,
  2881. create_dept_id=create_dept_id or "",
  2882. update_by=user_id,
  2883. create_time=datetime.now(),
  2884. update_time=datetime.now(),
  2885. del_flag=0,
  2886. )
  2887. db.add(wo)
  2888. db.flush()
  2889. return wo
  2890. def update_qc_work_order(
  2891. db: Session,
  2892. work_order_id: str,
  2893. *,
  2894. tenant_no: str,
  2895. user_id: Optional[str] = None,
  2896. cell_product_id: Optional[str] = None,
  2897. external_target_code: Optional[str] = None,
  2898. work_order_status: Optional[str] = None,
  2899. qc_conclusion: Optional[str] = None,
  2900. remark: Optional[str] = None,
  2901. ) -> BizQcWorkOrder:
  2902. """编辑质检工单:入库/出库可改备注、状态与质检结论;临时可改备注与质检对象"""
  2903. wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
  2904. if not wo:
  2905. raise ValueError("工单不存在")
  2906. wo_type = (getattr(wo, "work_order_type", None) or "inbound").strip()
  2907. if wo_type == "temporary":
  2908. # 编辑弹窗同时传两侧(一侧为空串);备注-only 更新两侧均为 None,不改对象
  2909. if cell_product_id is not None and external_target_code is not None:
  2910. cp_id, ext_code = resolve_temp_qc_target(
  2911. cell_product_id=cell_product_id,
  2912. external_target_code=external_target_code,
  2913. )
  2914. if cp_id:
  2915. cp_id = _validate_cell_product_id(db, cp_id, tenant_no)
  2916. wo.cell_product_id = cp_id
  2917. wo.external_target_code = ext_code
  2918. elif cell_product_id is not None:
  2919. pass
  2920. if work_order_status is not None:
  2921. st = (work_order_status or "").strip()
  2922. if st not in _VALID_QC_WORK_ORDER_STATUSES:
  2923. raise ValueError("工单状态无效")
  2924. wo.work_order_status = st
  2925. if qc_conclusion is not None:
  2926. conc = (qc_conclusion or "").strip()
  2927. if conc and conc not in _VALID_QC_WORK_ORDER_CONCLUSIONS:
  2928. raise ValueError("质检结论无效")
  2929. wo.conclusion = conc or None
  2930. if remark is not None:
  2931. wo.remark = (remark or "").strip() or None
  2932. wo.update_by = user_id
  2933. wo.update_time = datetime.now()
  2934. db.add(wo)
  2935. db.flush()
  2936. return wo
  2937. def update_temp_qc_work_order(
  2938. db: Session,
  2939. work_order_id: str,
  2940. *,
  2941. tenant_no: str,
  2942. user_id: Optional[str] = None,
  2943. cell_product_id: Optional[str] = None,
  2944. work_order_status: Optional[str] = None,
  2945. qc_conclusion: Optional[str] = None,
  2946. remark: Optional[str] = None,
  2947. ) -> BizQcWorkOrder:
  2948. """编辑临时质检工单(备注、质检对象、状态、质检结论)"""
  2949. return update_qc_work_order(
  2950. db,
  2951. work_order_id,
  2952. tenant_no=tenant_no,
  2953. user_id=user_id,
  2954. cell_product_id=cell_product_id,
  2955. work_order_status=work_order_status,
  2956. qc_conclusion=qc_conclusion,
  2957. remark=remark,
  2958. )
  2959. def list_temp_qc_work_orders(
  2960. db: Session,
  2961. *,
  2962. page: int = 1,
  2963. page_size: int = 10,
  2964. keyword: Optional[str] = None,
  2965. work_order_status: Optional[str] = None,
  2966. tenant_no: Optional[str] = None,
  2967. user_info=None,
  2968. ) -> Tuple[List[Dict[str, Any]], int]:
  2969. def _filtered():
  2970. q0 = db.query(BizQcWorkOrder).filter(
  2971. BizQcWorkOrder.del_flag == 0,
  2972. BizQcWorkOrder.work_order_type == "temporary",
  2973. )
  2974. if tenant_no:
  2975. q0 = q0.filter(BizQcWorkOrder.tenant_no == tenant_no)
  2976. kw = (keyword or "").strip()
  2977. if kw:
  2978. like = f"%{kw}%"
  2979. q0 = q0.filter(
  2980. or_(
  2981. BizQcWorkOrder.work_order_no.like(like),
  2982. BizQcWorkOrder.external_target_code.like(like),
  2983. )
  2984. )
  2985. st = (work_order_status or "").strip()
  2986. if st:
  2987. q0 = q0.filter(BizQcWorkOrder.work_order_status == st)
  2988. if user_info:
  2989. from app.core.data_scope import apply_data_scope
  2990. q0 = apply_data_scope(q0, BizQcWorkOrder, user_info, db, tenant_no)
  2991. return q0
  2992. total = _filtered().order_by(None).with_entities(func.count(BizQcWorkOrder.id)).scalar() or 0
  2993. page = max(1, page)
  2994. page_size = min(max(1, page_size), 100)
  2995. rows = (
  2996. _filtered()
  2997. .order_by(BizQcWorkOrder.create_time.desc())
  2998. .offset((page - 1) * page_size)
  2999. .limit(page_size)
  3000. .all()
  3001. )
  3002. items = [_wo_to_dict(x) for x in rows]
  3003. if tenant_no:
  3004. items = _enrich_work_order_qc_targets(db, tenant_no, items)
  3005. items = _enrich_work_order_qc_conclusions(db, items)
  3006. return items, int(total)
  3007. def get_temp_qc_work_order(
  3008. db: Session, row_id: str, tenant_no: str
  3009. ) -> Optional[BizQcWorkOrder]:
  3010. q = db.query(BizQcWorkOrder).filter(
  3011. BizQcWorkOrder.id == row_id,
  3012. BizQcWorkOrder.del_flag == 0,
  3013. BizQcWorkOrder.work_order_type == "temporary",
  3014. )
  3015. if tenant_no:
  3016. q = q.filter(BizQcWorkOrder.tenant_no == tenant_no)
  3017. return q.first()
  3018. def get_temp_qc_work_order_detail(
  3019. db: Session, row_id: str, tenant_no: str
  3020. ) -> Optional[Dict[str, Any]]:
  3021. wo = get_temp_qc_work_order(db, row_id, tenant_no)
  3022. if not wo:
  3023. return None
  3024. out = _wo_to_dict(wo)
  3025. enriched_list = _enrich_work_order_qc_targets(db, tenant_no, [out])
  3026. out = enriched_list[0]
  3027. out["labItems"] = _temp_lab_lines(db, wo.id)
  3028. out["thirdPartyItems"] = _temp_third_lines(db, wo.id)
  3029. out["tasks"] = _temp_tasks(db, wo.id, tenant_no)
  3030. return out
  3031. def configure_temp_qc_work_order(
  3032. db: Session,
  3033. work_order_id: str,
  3034. *,
  3035. has_lab: bool,
  3036. has_third_party: bool,
  3037. lab_item_ids: List[str],
  3038. third_party_item_ids: List[str],
  3039. batches: Optional[List[Dict[str, Any]]] = None,
  3040. tenant_no: str,
  3041. user_id: Optional[str] = None,
  3042. create_dept_id: str = "",
  3043. ) -> BizQcWorkOrder:
  3044. wo = get_temp_qc_work_order(db, work_order_id, tenant_no)
  3045. if not wo:
  3046. raise ValueError("工单不存在")
  3047. from app.crud import qc_inspection_batch as batch_svc
  3048. if batches is not None:
  3049. payload_batches = batches
  3050. else:
  3051. payload_batches = batch_svc.normalize_legacy_configure_to_batches(
  3052. {
  3053. "hasLab": has_lab,
  3054. "hasThirdParty": has_third_party,
  3055. "labItemIds": lab_item_ids,
  3056. "thirdPartyItemIds": third_party_item_ids,
  3057. }
  3058. )
  3059. return batch_svc.configure_work_order_batches(
  3060. db,
  3061. wo,
  3062. payload_batches,
  3063. tenant_no=tenant_no,
  3064. user_id=user_id,
  3065. create_dept_id=create_dept_id,
  3066. )
  3067. def try_complete_temp_qc_work_order(
  3068. db: Session, work_order_id: str, tenant_no: str
  3069. ) -> None:
  3070. pending = (
  3071. db.query(BizQcTask)
  3072. .filter(
  3073. BizQcTask.work_order_id == work_order_id,
  3074. BizQcTask.order_source == "temporary",
  3075. BizQcTask.tenant_no == tenant_no,
  3076. BizQcTask.del_flag == 0,
  3077. BizQcTask.task_status != "ywc",
  3078. )
  3079. .count()
  3080. )
  3081. if pending:
  3082. return
  3083. wo = get_temp_qc_work_order(db, work_order_id, tenant_no)
  3084. if wo and wo.work_order_status == "ypz":
  3085. wo.work_order_status = "ywc"
  3086. wo.update_time = datetime.now()
  3087. db.add(wo)
  3088. db.commit()
  3089. def delete_temp_qc_work_order(
  3090. db: Session, work_order_id: str, tenant_no: str, user_id: Optional[str]
  3091. ) -> None:
  3092. wo = get_temp_qc_work_order(db, work_order_id, tenant_no)
  3093. if not wo:
  3094. raise ValueError("工单不存在")
  3095. if wo.work_order_status == "ywc":
  3096. raise ValueError("已完成的工单不能删除")
  3097. now = datetime.now()
  3098. tasks = (
  3099. db.query(BizQcTask)
  3100. .filter(
  3101. BizQcTask.work_order_id == work_order_id,
  3102. BizQcTask.order_source == "temporary",
  3103. BizQcTask.del_flag == 0,
  3104. )
  3105. .all()
  3106. )
  3107. for task in tasks:
  3108. results = (
  3109. db.query(BizQcTaskResult)
  3110. .filter(
  3111. BizQcTaskResult.task_id == task.id,
  3112. BizQcTaskResult.del_flag == 0,
  3113. )
  3114. .all()
  3115. )
  3116. for r in results:
  3117. r.del_flag = 1
  3118. r.update_by = user_id
  3119. r.update_time = now
  3120. task.del_flag = 1
  3121. task.update_by = user_id
  3122. task.update_time = now
  3123. lab_items = (
  3124. db.query(BizQcWorkOrderLabItem)
  3125. .filter(
  3126. BizQcWorkOrderLabItem.work_order_id == work_order_id,
  3127. BizQcWorkOrderLabItem.order_source == "temporary",
  3128. BizQcWorkOrderLabItem.del_flag == 0,
  3129. )
  3130. .all()
  3131. )
  3132. for item in lab_items:
  3133. item.del_flag = 1
  3134. item.update_by = user_id
  3135. item.update_time = now
  3136. third_items = (
  3137. db.query(BizQcWorkOrderThirdItem)
  3138. .filter(
  3139. BizQcWorkOrderThirdItem.work_order_id == work_order_id,
  3140. BizQcWorkOrderThirdItem.order_source == "temporary",
  3141. BizQcWorkOrderThirdItem.del_flag == 0,
  3142. )
  3143. .all()
  3144. )
  3145. for item in third_items:
  3146. item.del_flag = 1
  3147. item.update_by = user_id
  3148. item.update_time = now
  3149. wo.del_flag = 1
  3150. wo.update_by = user_id
  3151. wo.update_time = now
  3152. db.commit()
  3153. def build_outbound_pilot_context_by_schedule(
  3154. db: Session, schedule_id: str, tenant_no: str
  3155. ) -> Dict[str, Any]:
  3156. """按输注计划 ID 组装出库中试服务上下文(申请单/患者/方案/计划/产品),供质检与生产工单详情复用。"""
  3157. out: Dict[str, Any] = {
  3158. "application": None,
  3159. "patient": None,
  3160. "infusionPlan": None,
  3161. "infusionSchedule": None,
  3162. "products": [],
  3163. }
  3164. if not schedule_id:
  3165. return out
  3166. sched = (
  3167. db.query(BizPilotInfusionSchedule)
  3168. .filter(
  3169. BizPilotInfusionSchedule.id == schedule_id,
  3170. BizPilotInfusionSchedule.tenant_no == tenant_no,
  3171. BizPilotInfusionSchedule.del_flag == 0,
  3172. )
  3173. .first()
  3174. )
  3175. if sched:
  3176. out["infusionSchedule"] = {
  3177. "infusionTime": _fmt_dt(sched.infusion_time),
  3178. "infusionLocation": sched.infusion_location or "",
  3179. "infusionMethod": sched.infusion_method or "",
  3180. "cellType": sched.cell_type or "",
  3181. "specText": sched.spec_text or "",
  3182. "scheduleStatus": sched.schedule_status or "",
  3183. "infusionStatus": sched.infusion_status or "",
  3184. "qcStatus": sched.qc_status or "",
  3185. "cancelReason": sched.cancel_reason or "",
  3186. "amount": _dec(sched.amount) if sched.amount is not None else None,
  3187. }
  3188. plan = (
  3189. db.query(BizPilotInfusionPlan)
  3190. .filter(
  3191. BizPilotInfusionPlan.id == (sched.infusion_plan_id or ""),
  3192. BizPilotInfusionPlan.tenant_no == tenant_no,
  3193. BizPilotInfusionPlan.del_flag == 0,
  3194. )
  3195. .first()
  3196. )
  3197. if plan:
  3198. out["infusionPlan"] = {
  3199. "cellType": plan.cell_type or "",
  3200. "specText": plan.spec_text or "",
  3201. "totalCourseCount": plan.total_course_count,
  3202. "courseInterval": plan.course_interval or "",
  3203. "infusionRoute": plan.infusion_route or "",
  3204. "infusionStartDate": plan.infusion_start_date or "",
  3205. }
  3206. app = (
  3207. db.query(BizPilotApplication)
  3208. .filter(
  3209. BizPilotApplication.id == (plan.pilot_application_id or ""),
  3210. BizPilotApplication.tenant_no == tenant_no,
  3211. BizPilotApplication.del_flag == 0,
  3212. )
  3213. .first()
  3214. )
  3215. if app:
  3216. app_biz = (app.biz_type or "").strip() or "pilot_service"
  3217. out["application"] = {
  3218. "applicationNo": app.application_no or "",
  3219. "applicationName": app.application_name or "",
  3220. "applicationStatus": app.application_status or "",
  3221. "channelName": app.channel_name or "",
  3222. "bizType": app_biz,
  3223. "bizTypeLabel": _OUTBOUND_BIZ_TYPE_LABELS.get(app_biz, app_biz),
  3224. }
  3225. if app_biz == "basic_experiment":
  3226. out["application"]["projectTitle"] = app.project_title or ""
  3227. out["application"]["customerName"] = app.customer_name or ""
  3228. out["application"]["customerContact"] = app.customer_contact or ""
  3229. out["application"]["customerPhone"] = app.customer_phone or ""
  3230. else:
  3231. patient = (
  3232. db.query(BizPilotPatient)
  3233. .filter(
  3234. BizPilotPatient.pilot_application_id == app.id,
  3235. BizPilotPatient.tenant_no == tenant_no,
  3236. BizPilotPatient.del_flag == 0,
  3237. )
  3238. .first()
  3239. )
  3240. if patient:
  3241. out["patient"] = {
  3242. "patientName": patient.patient_name or "",
  3243. "sex": patient.sex or "",
  3244. "age": str(patient.age) if patient.age is not None else "",
  3245. "phoneNo": patient.phone_no or "",
  3246. "idCardNo": patient.id_card_no or "",
  3247. "emergencyContact": patient.emergency_contact or "",
  3248. "emergencyPhone": patient.emergency_phone or "",
  3249. "clinicalDiagnosis": patient.clinical_diagnosis or "",
  3250. "diagnosisAttachments": patient.diagnosis_attachments or "",
  3251. }
  3252. products = (
  3253. db.query(BizPilotInfusionScheduleProduct)
  3254. .filter(
  3255. BizPilotInfusionScheduleProduct.infusion_schedule_id == schedule_id,
  3256. BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
  3257. BizPilotInfusionScheduleProduct.del_flag == 0,
  3258. )
  3259. .all()
  3260. )
  3261. out["products"] = [
  3262. {
  3263. "projectCode": p.project_code or "",
  3264. "projectName": p.project_name or "",
  3265. "cellTypeName": p.cell_type_name or "",
  3266. "specText": p.spec_text or "",
  3267. "agreementPrice": _dec(p.agreement_price),
  3268. "actualPrice": _dec(p.actual_price),
  3269. "quantity": p.quantity,
  3270. "productStatus": p.product_status or "",
  3271. }
  3272. for p in products
  3273. ]
  3274. return out
  3275. def get_outbound_qc_detail(
  3276. db: Session, work_order_id: str, tenant_no: str
  3277. ) -> Dict[str, Any]:
  3278. wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
  3279. if not wo:
  3280. raise ValueError("工单不存在")
  3281. wo_type = getattr(wo, "work_order_type", None) or "inbound"
  3282. if wo_type != "outbound":
  3283. raise ValueError("该工单不是出库质检类型")
  3284. configured_by = wo.configured_by or ""
  3285. configured_by_name = ""
  3286. if configured_by:
  3287. configured_by_name = _user_nick_name_map(db, [configured_by]).get(configured_by, "")
  3288. schedule_id = getattr(wo, "infusion_schedule_id", None) or ""
  3289. pilot_ctx = build_outbound_pilot_context_by_schedule(db, schedule_id, tenant_no)
  3290. out: Dict[str, Any] = {
  3291. "workOrder": _wo_to_dict(wo, configured_by_name=configured_by_name),
  3292. **pilot_ctx,
  3293. "tasks": [],
  3294. }
  3295. from app.crud import qc_inspection_batch as batch_svc
  3296. out["tasks"] = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
  3297. out["inspectionBatches"] = batch_svc.list_inspection_batches(db, wo.id, tenant_no)
  3298. enriched_fee = enrich_work_order_fee_summaries(db, [out["workOrder"]], tenant_no)
  3299. if enriched_fee:
  3300. out["workOrder"]["feeSummary"] = enriched_fee[0].get("feeSummary")
  3301. out["feeSummary"] = enriched_fee[0].get("feeSummary")
  3302. return out
  3303. def create_qc_work_order_manual(
  3304. db: Session,
  3305. *,
  3306. tenant_no: str,
  3307. user_id: Optional[str] = None,
  3308. create_dept_id: str = "",
  3309. protocol_no: str = "",
  3310. sample_code: str = "",
  3311. reception_source: str = "cell_storage",
  3312. qc_biz_type: str = "inbound",
  3313. remark: str = "",
  3314. ) -> BizQcWorkOrder:
  3315. """手动创建质检工单(不关联接收记录)"""
  3316. wo = BizQcWorkOrder(
  3317. id=generate_uuid(),
  3318. work_order_no=_gen_daily_no(db, tenant_no, "ZJ"),
  3319. work_order_type="inbound",
  3320. reception_id=None,
  3321. protocol_sample_id=None,
  3322. collection_protocol_id=None,
  3323. protocol_no=protocol_no or None,
  3324. sample_code=sample_code or None,
  3325. reception_source=reception_source,
  3326. qc_biz_type=qc_biz_type,
  3327. work_order_status="dpz",
  3328. has_lab="0",
  3329. has_third_party="0",
  3330. tenant_no=tenant_no,
  3331. remark=remark,
  3332. create_by=user_id,
  3333. create_dept_id=create_dept_id,
  3334. )
  3335. db.add(wo)
  3336. db.flush()
  3337. return wo
  3338. def create_outbound_qc_work_order(
  3339. db: Session,
  3340. *,
  3341. schedule_id: str,
  3342. cell_product_id: str = "",
  3343. tenant_no: str = "",
  3344. user_id: Optional[str] = None,
  3345. create_dept_id: str = "",
  3346. remark: str = "",
  3347. biz_type: Optional[str] = None,
  3348. ) -> BizQcWorkOrder:
  3349. resolved_biz_type = (biz_type or "").strip() or resolve_outbound_biz_type_for_schedule(
  3350. db, schedule_id, tenant_no
  3351. )
  3352. if resolved_biz_type not in _OUTBOUND_BIZ_TYPE_LABELS:
  3353. resolved_biz_type = "pilot_service"
  3354. existing = (
  3355. db.query(BizQcWorkOrder)
  3356. .filter(
  3357. BizQcWorkOrder.infusion_schedule_id == schedule_id,
  3358. BizQcWorkOrder.tenant_no == tenant_no,
  3359. BizQcWorkOrder.del_flag == 0,
  3360. BizQcWorkOrder.trigger_source == "schedule",
  3361. )
  3362. .first()
  3363. )
  3364. if existing:
  3365. if (existing.biz_type or "") != resolved_biz_type:
  3366. existing.biz_type = resolved_biz_type
  3367. db.flush()
  3368. return existing
  3369. wo = BizQcWorkOrder(
  3370. id=generate_uuid(),
  3371. work_order_no=_gen_daily_no(db, tenant_no, "CKZJ"),
  3372. work_order_type="outbound",
  3373. reception_id=None,
  3374. protocol_sample_id=None,
  3375. collection_protocol_id=None,
  3376. protocol_no=None,
  3377. sample_code=None,
  3378. reception_source="outbound",
  3379. qc_biz_type="outbound",
  3380. biz_type=resolved_biz_type,
  3381. infusion_schedule_id=schedule_id,
  3382. trigger_source="schedule",
  3383. cell_product_id=cell_product_id or None,
  3384. work_order_status="dpz",
  3385. has_lab="0",
  3386. has_third_party="0",
  3387. tenant_no=tenant_no,
  3388. remark=remark,
  3389. create_by=user_id,
  3390. create_dept_id=create_dept_id,
  3391. )
  3392. db.add(wo)
  3393. db.flush()
  3394. return wo
  3395. def delete_qc_work_order(db: Session, work_order_id: str, tenant_no: str, user_id: Optional[str]) -> None:
  3396. """软删除质检工单及其关联的任务、任务结果、检测项目和第三方检测项目"""
  3397. wo = db.query(BizQcWorkOrder).filter(
  3398. and_(
  3399. BizQcWorkOrder.id == work_order_id,
  3400. BizQcWorkOrder.tenant_no == tenant_no,
  3401. BizQcWorkOrder.del_flag == 0,
  3402. )
  3403. ).first()
  3404. if not wo:
  3405. raise ValueError("工单不存在")
  3406. if wo.work_order_status == "ywc":
  3407. raise ValueError("已完成的工单不能删除")
  3408. now = datetime.now()
  3409. # 1. 软删除所有关联任务及其结果
  3410. tasks = db.query(BizQcTask).filter(
  3411. and_(BizQcTask.work_order_id == work_order_id, BizQcTask.del_flag == 0)
  3412. ).all()
  3413. from app.models.qc_param import BizQcTaskResult
  3414. for task in tasks:
  3415. results = db.query(BizQcTaskResult).filter(
  3416. and_(BizQcTaskResult.task_id == task.id, BizQcTaskResult.del_flag == 0)
  3417. ).all()
  3418. for r in results:
  3419. r.del_flag = 1
  3420. r.update_by = user_id
  3421. r.update_time = now
  3422. task.del_flag = 1
  3423. task.update_by = user_id
  3424. task.update_time = now
  3425. # 2. 软删除检测项目
  3426. lab_items = db.query(BizQcWorkOrderLabItem).filter(
  3427. and_(BizQcWorkOrderLabItem.work_order_id == work_order_id, BizQcWorkOrderLabItem.del_flag == 0)
  3428. ).all()
  3429. for item in lab_items:
  3430. item.del_flag = 1
  3431. item.update_by = user_id
  3432. item.update_time = now
  3433. # 3. 软删除第三方检测项目
  3434. third_items = db.query(BizQcWorkOrderThirdItem).filter(
  3435. and_(BizQcWorkOrderThirdItem.work_order_id == work_order_id, BizQcWorkOrderThirdItem.del_flag == 0)
  3436. ).all()
  3437. for item in third_items:
  3438. item.del_flag = 1
  3439. item.update_by = user_id
  3440. item.update_time = now
  3441. # 4. 软删除工单本体
  3442. wo.del_flag = 1
  3443. wo.update_by = user_id
  3444. wo.update_time = now
  3445. db.commit()