production_task.py 126 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286
  1. """生产任务 CRUD(biz_production_task + 步骤 + 子表 + 投入/产出)"""
  2. from __future__ import annotations
  3. import math
  4. from datetime import datetime
  5. from typing import Any, Dict, List, Optional, Tuple
  6. from sqlalchemy import and_, func, or_
  7. from sqlalchemy.orm import Session
  8. from app.core.utils import format_date, generate_uuid, parse_datetime
  9. from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot
  10. from app.models.models import (
  11. BizCellProduct,
  12. BizCollectionProtocol,
  13. BizProcess,
  14. BizProcessConsumable,
  15. BizProcessDataCollection,
  16. BizProcessOutput,
  17. BizProcessReagent,
  18. BizProcessRoute,
  19. BizProcessRouteStep,
  20. BizProcessRouteStepConsumable,
  21. BizProcessRouteStepDataCollection,
  22. BizProcessRouteStepOutput,
  23. BizProcessRouteStepReagent,
  24. BizProductionTask,
  25. BizProductionTaskHandover,
  26. BizProductionTaskInput,
  27. BizProductionTaskOutput,
  28. BizProductionTaskStep,
  29. BizProductionTaskStepAssignee,
  30. BizProductionTaskStepConsumable,
  31. BizProductionTaskStepConfiguredReagent,
  32. BizProductionTaskStepDataCollection,
  33. BizProductionTaskStepOutput,
  34. BizProductionTaskStepReagent,
  35. BizProductionWorkOrder,
  36. BizProtocolSample,
  37. BizSampleReception,
  38. BizStorageSlotOccupancy,
  39. SysUser,
  40. )
  41. from app.crud import storage_occupancy as occupancy_svc
  42. from app.crud.cell_product import (
  43. _row_to_dict as _cp_row_to_dict,
  44. _enrich_product_dicts as _cp_enrich,
  45. product_code_exists as _cp_product_code_exists,
  46. )
  47. from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
  48. def _require_requisition_for_catalog_material(
  49. db: Session,
  50. tenant_no: str,
  51. material_code: Optional[str],
  52. material_type: str,
  53. actual_usage: Optional[str],
  54. requisition_usage_id: Optional[str],
  55. ) -> None:
  56. code = (material_code or "").strip()
  57. actual = (actual_usage or "").strip()
  58. req_id = (requisition_usage_id or "").strip()
  59. if not code or not actual or req_id:
  60. return
  61. from app.models.production_material import BizProductionMaterial
  62. catalog_row = db.query(BizProductionMaterial).filter(
  63. and_(
  64. BizProductionMaterial.material_code == code,
  65. BizProductionMaterial.material_type == material_type,
  66. BizProductionMaterial.tenant_no == tenant_no,
  67. BizProductionMaterial.del_flag == 0,
  68. )
  69. ).first()
  70. if catalog_row:
  71. raise ValueError(f"物料「{code}」需选择领用记录")
  72. _PROTOCOL_STATUS_LABELS = {
  73. "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zky": "转科研"
  74. }
  75. _DONATION_STATUS_LABELS = {
  76. "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zckc": "转细胞存储"
  77. }
  78. _RECEPTION_SOURCE_LABELS = {
  79. "cell_storage": "细胞存储",
  80. "research_donation": "科研捐赠",
  81. }
  82. _OUTBOUND_BIZ_TYPE_LABELS = {
  83. "pilot_service": "中试服务",
  84. "clinical_application": "临床应用",
  85. }
  86. _APPLICATION_STATUS_LABELS = {
  87. "dks": "待开始",
  88. "jxz": "进行中",
  89. "ywc": "已完成",
  90. "yzf": "已作废",
  91. }
  92. _PRODUCTION_CONCLUSION_LABELS = {
  93. "cg": "成功",
  94. "sb": "失败",
  95. }
  96. def _production_conclusion_label(v: Optional[str]) -> str:
  97. code = (v or "").strip()
  98. if not code:
  99. return ""
  100. return _PRODUCTION_CONCLUSION_LABELS.get(code, code)
  101. def _fmt_dt(t: Optional[datetime]) -> str:
  102. return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
  103. def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
  104. val = (value or "").strip()
  105. if len(val) == 10:
  106. return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
  107. return val
  108. def _enrich_outbound_task_items(
  109. db: Session,
  110. tenant_no: str,
  111. items: List[Dict[str, Any]],
  112. wo_ctx_map: Dict[str, Dict[str, Any]],
  113. ) -> None:
  114. """出库生产任务:来源类型=业务类型,协议号=申请单号,协议状态=申请单状态。"""
  115. from app.crud.pilot_infusion_schedule_mgmt import (
  116. _get_plan_schedule_course_meta,
  117. _resolve_course_display,
  118. )
  119. from app.models.models import (
  120. BizPilotApplication,
  121. BizPilotInfusionPlan,
  122. BizPilotInfusionSchedule,
  123. BizPilotPatient,
  124. )
  125. schedule_ids = [
  126. ctx.get("infusionScheduleId")
  127. for ctx in wo_ctx_map.values()
  128. if ctx.get("workOrderType") == "outbound" and ctx.get("infusionScheduleId")
  129. ]
  130. if not schedule_ids:
  131. return
  132. schedules = {
  133. s.id: s
  134. for s in db.query(BizPilotInfusionSchedule)
  135. .filter(
  136. BizPilotInfusionSchedule.id.in_(schedule_ids),
  137. BizPilotInfusionSchedule.tenant_no == tenant_no,
  138. BizPilotInfusionSchedule.del_flag == 0,
  139. )
  140. .all()
  141. }
  142. plan_ids = list({s.infusion_plan_id for s in schedules.values() if s.infusion_plan_id})
  143. plan_map = {}
  144. if plan_ids:
  145. plan_map = {
  146. p.id: p
  147. for p in db.query(BizPilotInfusionPlan)
  148. .filter(
  149. BizPilotInfusionPlan.id.in_(plan_ids),
  150. BizPilotInfusionPlan.tenant_no == tenant_no,
  151. BizPilotInfusionPlan.del_flag == 0,
  152. )
  153. .all()
  154. }
  155. app_ids = list({p.pilot_application_id for p in plan_map.values() if p.pilot_application_id})
  156. app_map = {}
  157. patient_map: Dict[str, BizPilotPatient] = {}
  158. if app_ids:
  159. app_map = {
  160. a.id: a
  161. for a in db.query(BizPilotApplication)
  162. .filter(
  163. BizPilotApplication.id.in_(app_ids),
  164. BizPilotApplication.tenant_no == tenant_no,
  165. BizPilotApplication.del_flag == 0,
  166. )
  167. .all()
  168. }
  169. patients = (
  170. db.query(BizPilotPatient)
  171. .filter(
  172. BizPilotPatient.pilot_application_id.in_(app_ids),
  173. BizPilotPatient.tenant_no == tenant_no,
  174. BizPilotPatient.del_flag == 0,
  175. )
  176. .all()
  177. )
  178. patient_map = {p.pilot_application_id: p for p in patients}
  179. plan_course_meta = _get_plan_schedule_course_meta(db, plan_ids, tenant_no)
  180. schedule_app_map: Dict[str, BizPilotApplication] = {}
  181. for sid, sched in schedules.items():
  182. plan = plan_map.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
  183. app = app_map.get(plan.pilot_application_id) if plan and plan.pilot_application_id else None
  184. if app:
  185. schedule_app_map[sid] = app
  186. for item in items:
  187. ctx = wo_ctx_map.get(item.get("workOrderId") or "")
  188. if not ctx or ctx.get("workOrderType") != "outbound":
  189. continue
  190. biz_type = ctx.get("bizType") or ""
  191. biz_label = _OUTBOUND_BIZ_TYPE_LABELS.get(biz_type, biz_type)
  192. item["bizType"] = biz_type
  193. item["bizTypeLabel"] = biz_label
  194. item["receptionSource"] = biz_type
  195. item["receptionSourceLabel"] = biz_label
  196. sched_id = ctx.get("infusionScheduleId") or ""
  197. sched = schedules.get(sched_id)
  198. app = schedule_app_map.get(sched_id)
  199. if app:
  200. app_status = app.application_status or ""
  201. item["protocolNo"] = app.application_no or ""
  202. item["protocolStatus"] = app_status
  203. item["protocolStatusLabel"] = _APPLICATION_STATUS_LABELS.get(app_status, app_status)
  204. item["applicationNo"] = app.application_no or ""
  205. item["applicationName"] = app.application_name or ""
  206. item["channelName"] = app.channel_name or ""
  207. item["projectTitle"] = app.project_title or ""
  208. item["customerName"] = app.customer_name or ""
  209. else:
  210. item["protocolNo"] = ""
  211. item["protocolStatus"] = ""
  212. item["protocolStatusLabel"] = ""
  213. item["applicationNo"] = ""
  214. item["applicationName"] = ""
  215. item["channelName"] = ""
  216. item["projectTitle"] = ""
  217. item["customerName"] = ""
  218. if sched:
  219. plan = plan_map.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
  220. patient = (
  221. patient_map.get(plan.pilot_application_id)
  222. if plan and plan.pilot_application_id
  223. else None
  224. )
  225. plan_meta = (
  226. plan_course_meta.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
  227. )
  228. course = _resolve_course_display(sched, plan, plan_meta)
  229. item["patientName"] = patient.patient_name if patient else ""
  230. item["sortNo"] = course.get("sortNo")
  231. item["totalCourseCount"] = course.get("totalCourseCount")
  232. item["infusionTime"] = _fmt_dt(sched.infusion_time) if sched.infusion_time else ""
  233. def _generate_task_no(db: Session, tenant_no: str) -> str:
  234. day = datetime.now().strftime("%Y%m%d")
  235. like = f"RW{day}%"
  236. cnt = (
  237. db.query(func.count(BizProductionTask.id))
  238. .filter(
  239. BizProductionTask.tenant_no == tenant_no,
  240. BizProductionTask.task_no.like(like),
  241. )
  242. .scalar()
  243. )
  244. return f"RW{day}{(cnt or 0) + 1:04d}"
  245. def _task_to_dict(t: BizProductionTask, *, work_order_no: str = "") -> Dict[str, Any]:
  246. return {
  247. "id": t.id,
  248. "taskNo": t.task_no,
  249. "workOrderId": t.work_order_id,
  250. "orderSource": getattr(t, "order_source", "inbound") or "inbound",
  251. "workOrderNo": work_order_no,
  252. "taskType": t.task_type or "cell_storage",
  253. "taskName": t.task_name or "",
  254. "description": t.description or "",
  255. "routeId": t.route_id or "",
  256. "taskStatus": t.task_status,
  257. "currentAssigneeUserId": t.current_assignee_user_id or "",
  258. "currentAssigneeUserName": "",
  259. "startedTime": _fmt_dt(t.started_time),
  260. "completedTime": _fmt_dt(t.completed_time),
  261. "conclusion": (getattr(t, "conclusion", None) or "").strip(),
  262. "conclusionLabel": _production_conclusion_label(getattr(t, "conclusion", None)),
  263. "remark": t.remark or "",
  264. "attachments": t.attachments or "",
  265. "equipmentIds": parse_equipment_ids(getattr(t, "equipment_ids", None)),
  266. "equipmentName": getattr(t, "equipment_name", None) or "",
  267. "tenantNo": t.tenant_no,
  268. "orderNum": t.order_num if t.order_num is not None else 0,
  269. "createTime": _fmt_dt(t.create_time),
  270. "updateTime": _fmt_dt(t.update_time),
  271. }
  272. def _step_to_dict(s: BizProductionTaskStep) -> Dict[str, Any]:
  273. return {
  274. "id": s.id,
  275. "taskId": s.task_id,
  276. "stepOrder": s.step_order if s.step_order is not None else 0,
  277. "processCode": s.process_code or "",
  278. "categoryId": s.category_id or "",
  279. "processName": s.process_name or "",
  280. "description": s.description or "",
  281. "estimatedDuration": s.estimated_duration or "",
  282. "actualDuration": s.actual_duration or "",
  283. "sopIds": s.sop_ids or "",
  284. "sopContent": s.sop_content or "",
  285. "stepStatus": s.step_status or "dzx",
  286. "plannedStartTime": format_date(getattr(s, "planned_start_time", None)) or "",
  287. "startedTime": _fmt_dt(s.started_time),
  288. "completedTime": _fmt_dt(s.completed_time),
  289. "completedBy": getattr(s, "completed_by", None) or "",
  290. "completedByName": "",
  291. "assigneeUserIds": [],
  292. "assigneeNames": [],
  293. "assignees": [],
  294. "remark": s.remark or "",
  295. }
  296. def _step_reagent_to_dict(r: BizProductionTaskStepReagent) -> Dict[str, Any]:
  297. return {
  298. "id": r.id,
  299. "stepId": r.step_id,
  300. "reagentCode": r.reagent_code or "",
  301. "reagentName": r.reagent_name or "",
  302. "spec": r.spec or "",
  303. "standardUsage": r.standard_usage or "",
  304. "actualUsage": r.actual_usage or "",
  305. "unit": r.unit or "",
  306. "requisitionUsageId": (r.requisition_usage_id or "").strip(),
  307. "isKeyMaterial": r.is_key_material if r.is_key_material is not None else "0",
  308. "orderNum": r.order_num if r.order_num is not None else 0,
  309. "remark": r.remark or "",
  310. }
  311. def _step_configured_reagent_to_dict(r: BizProductionTaskStepConfiguredReagent) -> Dict[str, Any]:
  312. return {
  313. "id": r.id,
  314. "stepId": r.step_id,
  315. "configuredReagentCode": r.configured_reagent_code or "",
  316. "configuredReagentName": r.configured_reagent_name or "",
  317. "spec": r.spec or "",
  318. "standardUsage": r.standard_usage or "",
  319. "actualUsage": r.actual_usage or "",
  320. "unit": r.unit or "",
  321. "requisitionUsageId": (r.requisition_usage_id or "").strip(),
  322. "orderNum": r.order_num if r.order_num is not None else 0,
  323. "remark": r.remark or "",
  324. }
  325. def _step_consumable_to_dict(c: BizProductionTaskStepConsumable) -> Dict[str, Any]:
  326. return {
  327. "id": c.id,
  328. "stepId": c.step_id,
  329. "consumableCode": c.consumable_code or "",
  330. "consumableName": c.consumable_name or "",
  331. "specModel": c.spec_model or "",
  332. "standardUsage": c.standard_usage or "",
  333. "actualUsage": c.actual_usage or "",
  334. "unit": c.unit or "",
  335. "requisitionUsageId": (c.requisition_usage_id or "").strip(),
  336. "isRequired": c.is_required if c.is_required is not None else "1",
  337. "orderNum": c.order_num if c.order_num is not None else 0,
  338. "remark": c.remark or "",
  339. }
  340. def _step_output_to_dict(o: BizProductionTaskStepOutput) -> Dict[str, Any]:
  341. return {
  342. "id": o.id,
  343. "stepId": o.step_id,
  344. "outputTypeId": o.output_type_id or "",
  345. "outputName": o.output_name or "",
  346. "outputBarcode": o.output_barcode or "",
  347. "orderNum": o.order_num if o.order_num is not None else 0,
  348. "remark": o.remark or "",
  349. }
  350. def _step_data_collection_to_dict(d: BizProductionTaskStepDataCollection) -> Dict[str, Any]:
  351. return {
  352. "id": d.id,
  353. "stepId": d.step_id,
  354. "collectionName": d.collection_name or "",
  355. "unit": d.unit or "",
  356. "collectionValue": d.collection_value or "",
  357. "orderNum": d.order_num if d.order_num is not None else 0,
  358. "remark": d.remark or "",
  359. }
  360. def _task_input_to_dict(i: BizProductionTaskInput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
  361. result = {
  362. "id": i.id,
  363. "taskId": i.task_id,
  364. "inputType": i.input_type or "",
  365. "inputId": i.input_id or "",
  366. "inputCode": i.input_code or "",
  367. "inputName": i.input_name or "",
  368. "remark": i.remark or "",
  369. }
  370. if product_dict:
  371. for key in [
  372. "productCode", "productName", "productTypeId", "productTypeName",
  373. "specId", "specName", "specText", "passage", "batchNo", "storageSlotName",
  374. "expectedCellCount", "viability", "freezeDate", "expiryDate",
  375. "productStatus", "productImages", "storageSlotId",
  376. ]:
  377. if key in product_dict:
  378. result[key] = product_dict[key]
  379. return result
  380. def _task_output_to_dict(o: BizProductionTaskOutput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
  381. result = {
  382. "id": o.id,
  383. "taskId": o.task_id,
  384. "cellProductId": o.cell_product_id or "",
  385. "storageSlotId": o.storage_slot_id or "",
  386. "outputName": o.output_name or "",
  387. "outputBarcode": o.output_barcode or "",
  388. "quantity": float(o.quantity) if o.quantity is not None else None,
  389. "unit": o.unit or "",
  390. "remark": o.remark or "",
  391. "attachments": o.attachments or "",
  392. }
  393. if product_dict:
  394. for key in [
  395. "productCode", "productName", "productTypeId", "productTypeName",
  396. "specId", "specName", "specText", "passage", "batchNo", "storageSlotName",
  397. "expectedCellCount", "viability", "freezeDate", "expiryDate",
  398. "productStatus", "productImages",
  399. ]:
  400. if key in product_dict:
  401. result[key] = product_dict[key]
  402. return result
  403. def _soft_delete_sub(db: Session, model_class, parent_field: str, parent_id: str, now: datetime) -> None:
  404. rows = db.query(model_class).filter(
  405. and_(
  406. getattr(model_class, parent_field) == parent_id,
  407. model_class.del_flag == 0,
  408. )
  409. ).all()
  410. for r in rows:
  411. r.del_flag = 1
  412. r.update_time = now
  413. db.add(r)
  414. def _resolve_sub_table_order_num(order_num: Optional[int], idx: int, rows: list) -> int:
  415. """复制/克隆子表排序号:保留源 order_num;若源记录均为 0 则按遍历顺序写入 idx。"""
  416. if rows and len(rows) > 1 and all((getattr(r, "order_num", None) or 0) == 0 for r in rows):
  417. return idx
  418. if order_num is not None:
  419. return order_num
  420. return idx
  421. def _empty_step_sub_tables() -> Dict[str, Any]:
  422. return {
  423. "reagents": [],
  424. "consumables": [],
  425. "configuredReagents": [],
  426. "outputs": [],
  427. "dataCollections": [],
  428. }
  429. def _batch_load_step_sub_tables(db: Session, step_ids: List[str]) -> Dict[str, Dict[str, Any]]:
  430. """批量加载步骤子表,避免 get_production_task_detail 逐步骤 N+1 查询。"""
  431. ids = [sid for sid in step_ids if (sid or "").strip()]
  432. if not ids:
  433. return {}
  434. result: Dict[str, Dict[str, Any]] = {sid: _empty_step_sub_tables() for sid in ids}
  435. reagents = (
  436. db.query(BizProductionTaskStepReagent)
  437. .filter(
  438. and_(
  439. BizProductionTaskStepReagent.step_id.in_(ids),
  440. BizProductionTaskStepReagent.del_flag == 0,
  441. )
  442. )
  443. .order_by(
  444. BizProductionTaskStepReagent.step_id.asc(),
  445. BizProductionTaskStepReagent.order_num.asc(),
  446. BizProductionTaskStepReagent.create_time.asc(),
  447. )
  448. .all()
  449. )
  450. for row in reagents:
  451. result[row.step_id]["reagents"].append(_step_reagent_to_dict(row))
  452. consumables = (
  453. db.query(BizProductionTaskStepConsumable)
  454. .filter(
  455. and_(
  456. BizProductionTaskStepConsumable.step_id.in_(ids),
  457. BizProductionTaskStepConsumable.del_flag == 0,
  458. )
  459. )
  460. .order_by(
  461. BizProductionTaskStepConsumable.step_id.asc(),
  462. BizProductionTaskStepConsumable.order_num.asc(),
  463. BizProductionTaskStepConsumable.create_time.asc(),
  464. )
  465. .all()
  466. )
  467. for row in consumables:
  468. result[row.step_id]["consumables"].append(_step_consumable_to_dict(row))
  469. outputs = (
  470. db.query(BizProductionTaskStepOutput)
  471. .filter(
  472. and_(
  473. BizProductionTaskStepOutput.step_id.in_(ids),
  474. BizProductionTaskStepOutput.del_flag == 0,
  475. )
  476. )
  477. .order_by(
  478. BizProductionTaskStepOutput.step_id.asc(),
  479. BizProductionTaskStepOutput.order_num.asc(),
  480. BizProductionTaskStepOutput.create_time.asc(),
  481. )
  482. .all()
  483. )
  484. for row in outputs:
  485. result[row.step_id]["outputs"].append(_step_output_to_dict(row))
  486. data_collections = (
  487. db.query(BizProductionTaskStepDataCollection)
  488. .filter(
  489. and_(
  490. BizProductionTaskStepDataCollection.step_id.in_(ids),
  491. BizProductionTaskStepDataCollection.del_flag == 0,
  492. )
  493. )
  494. .order_by(
  495. BizProductionTaskStepDataCollection.step_id.asc(),
  496. BizProductionTaskStepDataCollection.order_num.asc(),
  497. BizProductionTaskStepDataCollection.create_time.asc(),
  498. )
  499. .all()
  500. )
  501. for row in data_collections:
  502. result[row.step_id]["dataCollections"].append(_step_data_collection_to_dict(row))
  503. configured_reagents = (
  504. db.query(BizProductionTaskStepConfiguredReagent)
  505. .filter(
  506. and_(
  507. BizProductionTaskStepConfiguredReagent.step_id.in_(ids),
  508. BizProductionTaskStepConfiguredReagent.del_flag == 0,
  509. )
  510. )
  511. .order_by(
  512. BizProductionTaskStepConfiguredReagent.step_id.asc(),
  513. BizProductionTaskStepConfiguredReagent.order_num.asc(),
  514. BizProductionTaskStepConfiguredReagent.create_time.asc(),
  515. )
  516. .all()
  517. )
  518. for row in configured_reagents:
  519. result[row.step_id]["configuredReagents"].append(_step_configured_reagent_to_dict(row))
  520. return result
  521. def _step_sub_tables_to_dict(db: Session, step_id: str) -> Dict[str, Any]:
  522. """加载步骤四张子表并序列化(与 get_production_task_detail 一致)。"""
  523. return _batch_load_step_sub_tables(db, [step_id]).get(step_id) or _empty_step_sub_tables()
  524. def _clone_step_reagents(
  525. db: Session, src_step_id: str, new_step_id: str,
  526. tenant_no: str, user_id: Optional[str], now: datetime,
  527. ) -> None:
  528. rows = db.query(BizProcessRouteStepReagent).filter(
  529. and_(BizProcessRouteStepReagent.step_id == src_step_id, BizProcessRouteStepReagent.del_flag == 0)
  530. ).order_by(
  531. BizProcessRouteStepReagent.order_num.asc(),
  532. BizProcessRouteStepReagent.create_time.asc(),
  533. ).all()
  534. for idx, r in enumerate(rows):
  535. db.add(BizProductionTaskStepReagent(
  536. id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
  537. reagent_code=r.reagent_code, reagent_name=r.reagent_name,
  538. spec=r.spec, standard_usage=r.standard_usage,
  539. unit=r.unit, is_key_material=r.is_key_material,
  540. remark=r.remark,
  541. order_num=_resolve_sub_table_order_num(r.order_num, idx, rows),
  542. create_by=user_id, create_dept_id="",
  543. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  544. ))
  545. def _clone_step_consumables(
  546. db: Session, src_step_id: str, new_step_id: str,
  547. tenant_no: str, user_id: Optional[str], now: datetime,
  548. ) -> None:
  549. rows = db.query(BizProcessRouteStepConsumable).filter(
  550. and_(BizProcessRouteStepConsumable.step_id == src_step_id, BizProcessRouteStepConsumable.del_flag == 0)
  551. ).order_by(
  552. BizProcessRouteStepConsumable.order_num.asc(),
  553. BizProcessRouteStepConsumable.create_time.asc(),
  554. ).all()
  555. for idx, c in enumerate(rows):
  556. db.add(BizProductionTaskStepConsumable(
  557. id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
  558. consumable_code=c.consumable_code, consumable_name=c.consumable_name,
  559. spec_model=c.spec_model, standard_usage=c.standard_usage,
  560. unit=c.unit, is_required=c.is_required,
  561. remark=c.remark,
  562. order_num=_resolve_sub_table_order_num(c.order_num, idx, rows),
  563. create_by=user_id, create_dept_id="",
  564. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  565. ))
  566. def _clone_step_outputs(
  567. db: Session, src_step_id: str, new_step_id: str,
  568. tenant_no: str, user_id: Optional[str], now: datetime,
  569. ) -> None:
  570. rows = db.query(BizProcessRouteStepOutput).filter(
  571. and_(BizProcessRouteStepOutput.step_id == src_step_id, BizProcessRouteStepOutput.del_flag == 0)
  572. ).order_by(
  573. BizProcessRouteStepOutput.order_num.asc(),
  574. BizProcessRouteStepOutput.create_time.asc(),
  575. ).all()
  576. for idx, o in enumerate(rows):
  577. db.add(BizProductionTaskStepOutput(
  578. id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
  579. output_type_id=o.output_type_id, output_name=o.output_name,
  580. output_barcode=o.output_barcode,
  581. remark=o.remark,
  582. order_num=_resolve_sub_table_order_num(o.order_num, idx, rows),
  583. create_by=user_id, create_dept_id="",
  584. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  585. ))
  586. def _clone_step_data_collections(
  587. db: Session, src_step_id: str, new_step_id: str,
  588. tenant_no: str, user_id: Optional[str], now: datetime,
  589. ) -> None:
  590. rows = db.query(BizProcessRouteStepDataCollection).filter(
  591. and_(BizProcessRouteStepDataCollection.step_id == src_step_id, BizProcessRouteStepDataCollection.del_flag == 0)
  592. ).order_by(
  593. BizProcessRouteStepDataCollection.order_num.asc(),
  594. BizProcessRouteStepDataCollection.create_time.asc(),
  595. ).all()
  596. for idx, d in enumerate(rows):
  597. db.add(BizProductionTaskStepDataCollection(
  598. id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
  599. collection_name=d.collection_name, unit=d.unit,
  600. remark=d.remark,
  601. order_num=_resolve_sub_table_order_num(d.order_num, idx, rows),
  602. create_by=user_id, create_dept_id="",
  603. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  604. ))
  605. def _clone_route_to_task(
  606. db: Session, task_id: str, route_id: str,
  607. tenant_no: str, user_id: Optional[str], create_dept_id: str,
  608. ) -> None:
  609. route_steps = (
  610. db.query(BizProcessRouteStep)
  611. .filter(and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0))
  612. .order_by(BizProcessRouteStep.step_order.asc(), BizProcessRouteStep.create_time.asc())
  613. .all()
  614. )
  615. now = datetime.now()
  616. for rs in route_steps:
  617. new_step = BizProductionTaskStep(
  618. id=generate_uuid(), task_id=task_id,
  619. step_order=rs.step_order,
  620. process_code=rs.process_code, category_id=rs.category_id,
  621. process_name=rs.process_name, description=rs.description,
  622. estimated_duration=rs.estimated_duration,
  623. sop_ids=rs.sop_ids,
  624. sop_content=rs.sop_content,
  625. step_status="dzx",
  626. tenant_no=tenant_no, order_num=0,
  627. remark=None, create_by=user_id, create_dept_id=create_dept_id or "",
  628. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  629. )
  630. db.add(new_step)
  631. db.flush()
  632. _clone_step_reagents(db, rs.id, new_step.id, tenant_no, user_id, now)
  633. _clone_step_consumables(db, rs.id, new_step.id, tenant_no, user_id, now)
  634. _clone_step_outputs(db, rs.id, new_step.id, tenant_no, user_id, now)
  635. _clone_step_data_collections(db, rs.id, new_step.id, tenant_no, user_id, now)
  636. def _validate_assignee_user_id(db: Session, user_id: str, tenant_no: str) -> str:
  637. uid = (user_id or "").strip()
  638. if not uid:
  639. raise ValueError("请选择负责人")
  640. row = (
  641. db.query(SysUser.id)
  642. .filter(
  643. SysUser.id == uid,
  644. SysUser.tenant_no == tenant_no,
  645. SysUser.del_flag == 0,
  646. SysUser.status == "1",
  647. )
  648. .first()
  649. )
  650. if not row:
  651. raise ValueError("负责人用户无效或已停用")
  652. return uid
  653. def _enrich_assignee_names(db: Session, items: List[Dict[str, Any]]) -> None:
  654. user_ids = list({(x.get("currentAssigneeUserId") or "").strip() for x in items if (x.get("currentAssigneeUserId") or "").strip()})
  655. if not user_ids:
  656. return
  657. rows = (
  658. db.query(SysUser.id, SysUser.nick_name)
  659. .filter(SysUser.id.in_(user_ids), SysUser.del_flag == 0)
  660. .all()
  661. )
  662. name_map = {r.id: (r.nick_name or "").strip() for r in rows}
  663. for item in items:
  664. uid = (item.get("currentAssigneeUserId") or "").strip()
  665. if uid:
  666. item["currentAssigneeUserName"] = name_map.get(uid, "")
  667. def _enrich_handover_times(
  668. db: Session,
  669. items: List[Dict[str, Any]],
  670. *,
  671. from_user_id: str,
  672. tenant_no: str,
  673. ) -> None:
  674. task_ids = [x.get("id") for x in items if x.get("id")]
  675. if not task_ids:
  676. return
  677. rows = (
  678. db.query(
  679. BizProductionTaskHandover.task_id,
  680. func.max(BizProductionTaskHandover.handover_time),
  681. )
  682. .filter(
  683. BizProductionTaskHandover.task_id.in_(task_ids),
  684. BizProductionTaskHandover.from_user_id == from_user_id,
  685. BizProductionTaskHandover.tenant_no == tenant_no,
  686. BizProductionTaskHandover.del_flag == 0,
  687. )
  688. .group_by(BizProductionTaskHandover.task_id)
  689. .all()
  690. )
  691. time_map = {r[0]: _fmt_dt(r[1]) for r in rows}
  692. for item in items:
  693. item["handoverTime"] = time_map.get(item.get("id"), "")
  694. def get_production_task_row(
  695. db: Session, task_id: str, tenant_no: str
  696. ) -> Optional[BizProductionTask]:
  697. return (
  698. db.query(BizProductionTask)
  699. .filter(
  700. BizProductionTask.id == task_id,
  701. BizProductionTask.tenant_no == tenant_no,
  702. BizProductionTask.del_flag == 0,
  703. )
  704. .first()
  705. )
  706. def assert_task_assignee(
  707. db: Session, task_id: str, user_id: str, tenant_no: str
  708. ) -> BizProductionTask:
  709. """Deprecated:优先使用 assert_task_step_assignee_or_legacy。"""
  710. return assert_task_step_assignee_or_legacy(db, task_id, user_id, tenant_no)
  711. def assert_task_step_assignee_or_legacy(
  712. db: Session, task_id: str, user_id: str, tenant_no: str
  713. ) -> BizProductionTask:
  714. from app.crud import production_task_step_assignee as step_assignee_svc
  715. task = get_production_task_row(db, task_id, tenant_no)
  716. if not task:
  717. raise ValueError("任务不存在")
  718. if step_assignee_svc.user_is_any_step_assignee_of_task(db, task_id, user_id, tenant_no):
  719. return task
  720. if (task.current_assignee_user_id or "") == (user_id or ""):
  721. return task
  722. raise PermissionError("非任务步骤负责人,无权操作")
  723. def assert_step_assignee_for_write(
  724. db: Session,
  725. step_id: str,
  726. user_id: str,
  727. tenant_no: str,
  728. *,
  729. manage: bool = False,
  730. ) -> BizProductionTaskStep:
  731. if manage:
  732. step = db.query(BizProductionTaskStep).filter(
  733. BizProductionTaskStep.id == step_id,
  734. BizProductionTaskStep.tenant_no == tenant_no,
  735. BizProductionTaskStep.del_flag == 0,
  736. ).first()
  737. if not step:
  738. raise ValueError("步骤不存在")
  739. return step
  740. from app.crud import production_task_step_assignee as step_assignee_svc
  741. step = db.query(BizProductionTaskStep).filter(
  742. BizProductionTaskStep.id == step_id,
  743. BizProductionTaskStep.tenant_no == tenant_no,
  744. BizProductionTaskStep.del_flag == 0,
  745. ).first()
  746. if not step:
  747. raise ValueError("步骤不存在")
  748. if not step_assignee_svc.user_is_step_assignee(db, step_id, user_id, tenant_no):
  749. raise PermissionError("非当前步骤负责人,无权操作")
  750. return step
  751. def _record_task_handover(
  752. db: Session,
  753. *,
  754. task_id: str,
  755. from_user_id: str,
  756. to_user_id: str,
  757. tenant_no: str,
  758. operator_id: Optional[str],
  759. create_dept_id: str,
  760. remark: Optional[str] = None,
  761. ) -> None:
  762. now = datetime.now()
  763. db.add(
  764. BizProductionTaskHandover(
  765. id=generate_uuid(),
  766. task_id=task_id,
  767. from_user_id=from_user_id,
  768. to_user_id=to_user_id,
  769. handover_time=now,
  770. remark=(remark or "").strip() or None,
  771. tenant_no=tenant_no,
  772. order_num=0,
  773. create_by=operator_id,
  774. create_dept_id=create_dept_id or "",
  775. create_time=now,
  776. update_by=operator_id,
  777. update_time=now,
  778. del_flag=0,
  779. )
  780. )
  781. def forward_production_task(
  782. db: Session,
  783. task_id: str,
  784. *,
  785. from_user_id: str,
  786. to_user_id: str,
  787. tenant_no: str,
  788. operator_id: Optional[str] = None,
  789. create_dept_id: str = "",
  790. remark: Optional[str] = None,
  791. require_assignee: bool = True,
  792. ) -> BizProductionTask:
  793. to_uid = _validate_assignee_user_id(db, to_user_id, tenant_no)
  794. from_uid = (from_user_id or "").strip()
  795. if not from_uid:
  796. raise ValueError("转出用户无效")
  797. if from_uid == to_uid:
  798. raise ValueError("不能转发给自己")
  799. if require_assignee:
  800. task = assert_task_assignee(db, task_id, from_uid, tenant_no)
  801. else:
  802. task = get_production_task_row(db, task_id, tenant_no)
  803. if not task:
  804. raise ValueError("任务不存在")
  805. handover_from = (task.current_assignee_user_id or from_uid).strip() or from_uid
  806. _record_task_handover(
  807. db,
  808. task_id=task_id,
  809. from_user_id=handover_from,
  810. to_user_id=to_uid,
  811. tenant_no=tenant_no,
  812. operator_id=operator_id,
  813. create_dept_id=create_dept_id,
  814. remark=remark,
  815. )
  816. task.current_assignee_user_id = to_uid
  817. task.update_by = operator_id
  818. task.update_time = datetime.now()
  819. db.add(task)
  820. db.flush()
  821. return task
  822. def reassign_production_task(
  823. db: Session,
  824. task_id: str,
  825. *,
  826. assignee_user_id: str,
  827. tenant_no: str,
  828. operator_id: Optional[str] = None,
  829. create_dept_id: str = "",
  830. remark: Optional[str] = None,
  831. ) -> BizProductionTask:
  832. task = get_production_task_row(db, task_id, tenant_no)
  833. if not task:
  834. raise ValueError("任务不存在")
  835. to_uid = _validate_assignee_user_id(db, assignee_user_id, tenant_no)
  836. from_uid = (task.current_assignee_user_id or operator_id or "").strip()
  837. if from_uid and from_uid != to_uid:
  838. _record_task_handover(
  839. db,
  840. task_id=task_id,
  841. from_user_id=from_uid,
  842. to_user_id=to_uid,
  843. tenant_no=tenant_no,
  844. operator_id=operator_id,
  845. create_dept_id=create_dept_id,
  846. remark=(remark or "").strip() or "管理员改派",
  847. )
  848. elif not from_uid:
  849. _record_task_handover(
  850. db,
  851. task_id=task_id,
  852. from_user_id=operator_id or to_uid,
  853. to_user_id=to_uid,
  854. tenant_no=tenant_no,
  855. operator_id=operator_id,
  856. create_dept_id=create_dept_id,
  857. remark=(remark or "").strip() or "管理员改派",
  858. )
  859. task.current_assignee_user_id = to_uid
  860. task.update_by = operator_id
  861. task.update_time = datetime.now()
  862. db.add(task)
  863. db.flush()
  864. return task
  865. def create_production_task(
  866. db: Session, *, tenant_no: str, user_id: Optional[str] = None,
  867. create_dept_id: str = "", data: Dict[str, Any],
  868. order_source: str = "inbound",
  869. ) -> BizProductionTask:
  870. work_order_id = (data.get("work_order_id") or "").strip()
  871. wo = None
  872. if work_order_id:
  873. wo = (
  874. db.query(BizProductionWorkOrder)
  875. .filter(
  876. BizProductionWorkOrder.id == work_order_id,
  877. BizProductionWorkOrder.tenant_no == tenant_no,
  878. BizProductionWorkOrder.del_flag == 0,
  879. )
  880. .first()
  881. )
  882. if wo:
  883. wo_type = getattr(wo, "work_order_type", None) or "inbound"
  884. if wo_type == "temporary":
  885. order_source = "temporary"
  886. elif wo_type == "outbound":
  887. order_source = "outbound"
  888. task_type = (data.get("task_type") or "").strip() or "cell_storage"
  889. if wo and (getattr(wo, "work_order_type", None) or "inbound") == "outbound":
  890. if not (data.get("task_type") or "").strip():
  891. task_type = "outbound"
  892. assignee_raw = (data.get("assignee_user_id") or data.get("assigneeUserId") or "").strip()
  893. assignee_user_id = None
  894. if assignee_raw:
  895. assignee_user_id = _validate_assignee_user_id(db, assignee_raw, tenant_no)
  896. task_no = _generate_task_no(db, tenant_no)
  897. now = datetime.now()
  898. task = BizProductionTask(
  899. id=generate_uuid(),
  900. task_no=task_no,
  901. work_order_id=data.get("work_order_id", ""),
  902. order_source=order_source,
  903. task_type=task_type,
  904. task_name=(data.get("task_name") or "").strip() or None,
  905. description=(data.get("description") or "").strip() or None,
  906. route_id=data.get("route_id") or None,
  907. task_status="dpz",
  908. current_assignee_user_id=assignee_user_id,
  909. remark=(data.get("remark") or "").strip() or None,
  910. attachments=(data.get("attachments") or "").strip() or None,
  911. tenant_no=tenant_no,
  912. order_num=0,
  913. create_by=user_id,
  914. create_dept_id=create_dept_id or "",
  915. create_time=now,
  916. update_by=user_id,
  917. update_time=now,
  918. del_flag=0,
  919. )
  920. db.add(task)
  921. db.flush()
  922. route_id = data.get("route_id")
  923. if route_id:
  924. _clone_route_to_task(db, task.id, route_id, tenant_no, user_id, create_dept_id)
  925. route = (
  926. db.query(BizProcessRoute)
  927. .filter(
  928. BizProcessRoute.id == route_id,
  929. BizProcessRoute.tenant_no == tenant_no,
  930. BizProcessRoute.del_flag == 0,
  931. )
  932. .first()
  933. )
  934. if route and (getattr(route, "equipment_ids", None) or getattr(route, "equipment_name", None)):
  935. task.equipment_ids = route.equipment_ids
  936. task.equipment_name = route.equipment_name
  937. db.add(task)
  938. from app.crud import production_work_order as production_work_order_svc
  939. production_work_order_svc.sync_production_work_order_status_from_tasks(
  940. db, task.work_order_id or "", tenant_no, user_id
  941. )
  942. if wo and (getattr(wo, "work_order_type", None) or "inbound") == "temporary":
  943. if not getattr(wo, "configured_time", None):
  944. wo.configured_time = now
  945. wo.configured_by = user_id
  946. wo.update_by = user_id
  947. wo.update_time = now
  948. db.add(wo)
  949. db.flush()
  950. return task
  951. def dispatch_task_steps(
  952. db: Session,
  953. task_id: str,
  954. *,
  955. tenant_no: str,
  956. steps_payload: List[Dict[str, Any]],
  957. operator_id: Optional[str] = None,
  958. create_dept_id: str = "",
  959. ) -> Dict[str, Any]:
  960. from app.crud import production_task_step_assignee as step_assignee_svc
  961. task = get_production_task_row(db, task_id, tenant_no)
  962. if not task:
  963. raise ValueError("任务不存在")
  964. if not steps_payload:
  965. raise ValueError("请至少指定一个步骤的负责人")
  966. step_rows = {
  967. s.id: s
  968. for s in db.query(BizProductionTaskStep).filter(
  969. BizProductionTaskStep.task_id == task_id,
  970. BizProductionTaskStep.tenant_no == tenant_no,
  971. BizProductionTaskStep.del_flag == 0,
  972. ).all()
  973. }
  974. for item in steps_payload:
  975. sid = (item.get("step_id") or item.get("stepId") or "").strip()
  976. if sid not in step_rows:
  977. raise ValueError(f"步骤不存在或不属于该任务: {sid}")
  978. user_ids = item.get("assignee_user_ids") or item.get("assigneeUserIds") or []
  979. step_assignee_svc.sync_assignees_for_step(
  980. db,
  981. sid,
  982. user_ids,
  983. tenant_no=tenant_no,
  984. operator_id=operator_id,
  985. create_dept_id=create_dept_id,
  986. )
  987. step = step_rows[sid]
  988. if "planned_start_time" in item or "plannedStartTime" in item:
  989. pst = item.get("planned_start_time")
  990. if pst is None and "plannedStartTime" in item:
  991. pst = item.get("plannedStartTime")
  992. if pst is None or pst == "":
  993. step.planned_start_time = None
  994. else:
  995. if isinstance(pst, datetime):
  996. step.planned_start_time = pst
  997. else:
  998. parsed = parse_datetime(str(pst).strip())
  999. if parsed is None:
  1000. raise ValueError("计划开始时间格式无效")
  1001. step.planned_start_time = parsed
  1002. step.update_by = operator_id
  1003. step.update_time = datetime.now()
  1004. db.add(step)
  1005. db.flush()
  1006. task_dict = _task_to_dict(task)
  1007. _enrich_tasks_with_step_summaries(db, [task_dict], tenant_no=tenant_no)
  1008. return {"taskId": task_id, "steps": task_dict.get("steps") or []}
  1009. def _enrich_tasks_with_step_summaries(
  1010. db: Session, items: List[Dict[str, Any]], *, tenant_no: str
  1011. ) -> None:
  1012. """批量附加工艺步骤摘要(无试剂/耗材/产出等子表),供列表卡片展示。"""
  1013. if not items:
  1014. return
  1015. task_ids = [it["id"] for it in items if (it.get("id") or "").strip()]
  1016. if not task_ids:
  1017. return
  1018. rows = (
  1019. db.query(BizProductionTaskStep)
  1020. .filter(
  1021. BizProductionTaskStep.task_id.in_(task_ids),
  1022. BizProductionTaskStep.tenant_no == tenant_no,
  1023. BizProductionTaskStep.del_flag == 0,
  1024. )
  1025. .order_by(
  1026. BizProductionTaskStep.task_id.asc(),
  1027. BizProductionTaskStep.step_order.asc(),
  1028. BizProductionTaskStep.create_time.asc(),
  1029. )
  1030. .all()
  1031. )
  1032. step_dicts = [_step_to_dict(s) for s in rows]
  1033. if step_dicts:
  1034. from app.crud.production_task_step_assignee import enrich_step_assignees
  1035. enrich_step_assignees(db, step_dicts, tenant_no=tenant_no)
  1036. by_task: Dict[str, List[Dict[str, Any]]] = {tid: [] for tid in task_ids}
  1037. for sd in step_dicts:
  1038. tid = (sd.get("taskId") or "").strip()
  1039. if tid in by_task:
  1040. by_task[tid].append(sd)
  1041. for item in items:
  1042. tid = (item.get("id") or "").strip()
  1043. item["steps"] = by_task.get(tid, [])
  1044. def list_production_tasks(
  1045. db: Session, *, tenant_no: str,
  1046. page: int = 1, page_size: int = 10,
  1047. keyword: Optional[str] = None,
  1048. work_order_id: Optional[str] = None,
  1049. work_order_ids: Optional[List[str]] = None,
  1050. task_no: Optional[str] = None,
  1051. task_status: Optional[str] = None,
  1052. started_time_start: Optional[str] = None,
  1053. started_time_end: Optional[str] = None,
  1054. planned_start_time_start: Optional[str] = None,
  1055. planned_start_time_end: Optional[str] = None,
  1056. planned_start_time_mine_user_id: Optional[str] = None,
  1057. current_assignee_user_id: Optional[str] = None,
  1058. step_assignee_user_id: Optional[str] = None,
  1059. mine_history_user_id: Optional[str] = None,
  1060. work_order_type: Optional[str] = None,
  1061. reception_source: Optional[str] = None,
  1062. biz_type: Optional[str] = None,
  1063. user_info=None,
  1064. include_step_summary: bool = False,
  1065. exclude_void_protocol: bool = True,
  1066. ) -> Tuple[List[Dict[str, Any]], int]:
  1067. def _filtered():
  1068. q = db.query(BizProductionTask).filter(
  1069. BizProductionTask.tenant_no == tenant_no,
  1070. BizProductionTask.del_flag == 0,
  1071. )
  1072. joined_wo = False
  1073. kw = (keyword or "").strip()
  1074. if kw:
  1075. like = f"%{kw}%"
  1076. wo_type = (work_order_type or "").strip()
  1077. if wo_type == "temporary":
  1078. keyword_conditions = [BizProductionWorkOrder.remark.like(like)]
  1079. else:
  1080. keyword_conditions = [
  1081. BizProductionTask.task_no.like(like),
  1082. BizProductionWorkOrder.work_order_no.like(like),
  1083. BizProductionWorkOrder.sample_code.like(like),
  1084. ]
  1085. if tenant_no:
  1086. from app.crud.production_work_order import _outbound_work_order_ids_by_keyword
  1087. outbound_ids = _outbound_work_order_ids_by_keyword(db, tenant_no, kw)
  1088. if outbound_ids:
  1089. keyword_conditions.append(BizProductionWorkOrder.id.in_(outbound_ids))
  1090. q = q.outerjoin(
  1091. BizProductionWorkOrder,
  1092. and_(
  1093. BizProductionWorkOrder.id == BizProductionTask.work_order_id,
  1094. BizProductionWorkOrder.del_flag == 0,
  1095. ),
  1096. ).filter(or_(*keyword_conditions))
  1097. joined_wo = True
  1098. elif task_no and task_no.strip():
  1099. q = q.filter(BizProductionTask.task_no.like(f"%{task_no.strip()}%"))
  1100. wo_ids = [x.strip() for x in (work_order_ids or []) if x and str(x).strip()]
  1101. if wo_ids:
  1102. q = q.filter(BizProductionTask.work_order_id.in_(wo_ids))
  1103. elif work_order_id and work_order_id.strip():
  1104. q = q.filter(BizProductionTask.work_order_id == work_order_id.strip())
  1105. if task_status and task_status.strip():
  1106. q = q.filter(BizProductionTask.task_status == task_status.strip())
  1107. if started_time_start and started_time_start.strip():
  1108. q = q.filter(
  1109. BizProductionTask.started_time
  1110. >= _normalize_date_bound(started_time_start.strip())
  1111. )
  1112. if started_time_end and started_time_end.strip():
  1113. q = q.filter(
  1114. BizProductionTask.started_time
  1115. <= _normalize_date_bound(started_time_end.strip(), end_of_day=True)
  1116. )
  1117. pst_start = (planned_start_time_start or "").strip()
  1118. pst_end = (planned_start_time_end or "").strip()
  1119. if pst_start or pst_end:
  1120. if not joined_wo:
  1121. q = q.join(
  1122. BizProductionWorkOrder,
  1123. and_(
  1124. BizProductionWorkOrder.id == BizProductionTask.work_order_id,
  1125. BizProductionWorkOrder.del_flag == 0,
  1126. ),
  1127. )
  1128. joined_wo = True
  1129. from app.crud.production_work_order import apply_planned_start_time_step_filter
  1130. q = apply_planned_start_time_step_filter(
  1131. q,
  1132. tenant_no=tenant_no,
  1133. work_order_id_column=BizProductionTask.work_order_id,
  1134. planned_start_time_start=planned_start_time_start,
  1135. planned_start_time_end=planned_start_time_end,
  1136. mine_user_id=planned_start_time_mine_user_id,
  1137. )
  1138. ca_uid = (current_assignee_user_id or "").strip()
  1139. if ca_uid:
  1140. q = q.filter(BizProductionTask.current_assignee_user_id == ca_uid)
  1141. sa_uid = (step_assignee_user_id or "").strip()
  1142. if sa_uid:
  1143. step_task_ids = (
  1144. db.query(BizProductionTaskStep.task_id)
  1145. .join(
  1146. BizProductionTaskStepAssignee,
  1147. BizProductionTaskStepAssignee.step_id == BizProductionTaskStep.id,
  1148. )
  1149. .filter(
  1150. BizProductionTaskStepAssignee.user_id == sa_uid,
  1151. BizProductionTaskStepAssignee.tenant_no == tenant_no,
  1152. BizProductionTaskStepAssignee.del_flag == 0,
  1153. BizProductionTaskStep.del_flag == 0,
  1154. )
  1155. .distinct()
  1156. .subquery()
  1157. )
  1158. q = q.filter(
  1159. or_(
  1160. BizProductionTask.id.in_(step_task_ids),
  1161. BizProductionTask.current_assignee_user_id == sa_uid,
  1162. )
  1163. )
  1164. mh_uid = (mine_history_user_id or "").strip()
  1165. if mh_uid:
  1166. handover_task_ids = (
  1167. db.query(BizProductionTaskHandover.task_id)
  1168. .filter(
  1169. BizProductionTaskHandover.from_user_id == mh_uid,
  1170. BizProductionTaskHandover.tenant_no == tenant_no,
  1171. BizProductionTaskHandover.del_flag == 0,
  1172. )
  1173. .distinct()
  1174. .subquery()
  1175. )
  1176. q = q.filter(
  1177. BizProductionTask.current_assignee_user_id != mh_uid,
  1178. BizProductionTask.id.in_(handover_task_ids),
  1179. )
  1180. wo_type = (work_order_type or "").strip()
  1181. if wo_type:
  1182. if not joined_wo:
  1183. q = q.join(
  1184. BizProductionWorkOrder,
  1185. and_(
  1186. BizProductionWorkOrder.id == BizProductionTask.work_order_id,
  1187. BizProductionWorkOrder.del_flag == 0,
  1188. ),
  1189. )
  1190. joined_wo = True
  1191. q = q.filter(BizProductionWorkOrder.work_order_type == wo_type)
  1192. rs = (reception_source or "").strip()
  1193. if rs:
  1194. if not joined_wo:
  1195. q = q.join(
  1196. BizProductionWorkOrder,
  1197. and_(
  1198. BizProductionWorkOrder.id == BizProductionTask.work_order_id,
  1199. BizProductionWorkOrder.del_flag == 0,
  1200. ),
  1201. )
  1202. joined_wo = True
  1203. q = q.filter(BizProductionWorkOrder.reception_source == rs)
  1204. bt = (biz_type or "").strip()
  1205. if bt:
  1206. if not joined_wo:
  1207. q = q.join(
  1208. BizProductionWorkOrder,
  1209. and_(
  1210. BizProductionWorkOrder.id == BizProductionTask.work_order_id,
  1211. BizProductionWorkOrder.del_flag == 0,
  1212. ),
  1213. )
  1214. joined_wo = True
  1215. q = q.filter(BizProductionWorkOrder.biz_type == bt)
  1216. if user_info:
  1217. from app.core.data_scope import apply_data_scope
  1218. q = apply_data_scope(q, BizProductionTask, user_info, db, tenant_no)
  1219. if exclude_void_protocol:
  1220. if not joined_wo:
  1221. q = q.join(
  1222. BizProductionWorkOrder,
  1223. and_(
  1224. BizProductionWorkOrder.id == BizProductionTask.work_order_id,
  1225. BizProductionWorkOrder.del_flag == 0,
  1226. ),
  1227. )
  1228. from app.crud.production_work_order import apply_exclude_inactive_protocol_filter
  1229. q = apply_exclude_inactive_protocol_filter(q, exclude=True)
  1230. return q
  1231. total = _filtered().order_by(None).with_entities(func.count(BizProductionTask.id)).scalar() or 0
  1232. page = max(1, page)
  1233. page_size = min(max(1, page_size), 100)
  1234. rows = (
  1235. _filtered()
  1236. .order_by(BizProductionTask.create_time.desc())
  1237. .offset((page - 1) * page_size)
  1238. .limit(page_size)
  1239. .all()
  1240. )
  1241. items = [_task_to_dict(x) for x in rows]
  1242. # 批量补充路线名称
  1243. route_ids = [x.route_id for x in rows if x.route_id]
  1244. if route_ids:
  1245. route_map = {
  1246. r.id: r.route_name
  1247. for r in db.query(BizProcessRoute.id, BizProcessRoute.route_name)
  1248. .filter(BizProcessRoute.id.in_(route_ids))
  1249. .all()
  1250. }
  1251. for item, row in zip(items, rows):
  1252. item["routeName"] = route_map.get(row.route_id, "")
  1253. # 批量补充工单号 + 协议号 + 样本编码
  1254. wo_ids = [x.work_order_id for x in rows if x.work_order_id]
  1255. wo_map: Dict[str, Any] = {}
  1256. wo_ctx_map: Dict[str, Dict[str, Any]] = {}
  1257. if wo_ids:
  1258. wo_rows = (
  1259. db.query(
  1260. BizProductionWorkOrder.id,
  1261. BizProductionWorkOrder.work_order_no,
  1262. BizProductionWorkOrder.work_order_type,
  1263. BizProductionWorkOrder.biz_type,
  1264. BizProductionWorkOrder.infusion_schedule_id,
  1265. BizProductionWorkOrder.protocol_no,
  1266. BizProductionWorkOrder.sample_code,
  1267. BizProductionWorkOrder.reception_id,
  1268. BizProductionWorkOrder.protocol_sample_id,
  1269. BizProductionWorkOrder.collection_protocol_id,
  1270. BizProductionWorkOrder.reception_source,
  1271. )
  1272. .filter(BizProductionWorkOrder.id.in_(wo_ids))
  1273. .all()
  1274. )
  1275. for w in wo_rows:
  1276. wo_map[w.id] = w
  1277. wo_type = getattr(w, "work_order_type", None) or "inbound"
  1278. wo_ctx_map[w.id] = {
  1279. "workOrderType": wo_type,
  1280. "bizType": getattr(w, "biz_type", None) or "",
  1281. "infusionScheduleId": getattr(w, "infusion_schedule_id", None) or "",
  1282. }
  1283. cell_wo_ids = [
  1284. w.collection_protocol_id
  1285. for w in wo_rows
  1286. if (w.reception_source or "cell_storage") != "research_donation"
  1287. and (getattr(w, "work_order_type", None) or "inbound") != "outbound"
  1288. and w.collection_protocol_id
  1289. ]
  1290. donation_wo_ids = [
  1291. w.collection_protocol_id
  1292. for w in wo_rows
  1293. if (w.reception_source or "") == "research_donation"
  1294. and (getattr(w, "work_order_type", None) or "inbound") != "outbound"
  1295. and w.collection_protocol_id
  1296. ]
  1297. proto_status_map: Dict[str, str] = {}
  1298. proto_void_type_map: Dict[str, str] = {}
  1299. if cell_wo_ids:
  1300. for p in (
  1301. db.query(
  1302. BizCollectionProtocol.id,
  1303. BizCollectionProtocol.protocol_status,
  1304. BizCollectionProtocol.void_type,
  1305. )
  1306. .filter(BizCollectionProtocol.id.in_(cell_wo_ids), BizCollectionProtocol.del_flag == 0)
  1307. .all()
  1308. ):
  1309. proto_status_map[p.id] = p.protocol_status or ""
  1310. proto_void_type_map[p.id] = p.void_type or ""
  1311. donation_status_map: Dict[str, str] = {}
  1312. donation_void_type_map: Dict[str, str] = {}
  1313. if donation_wo_ids:
  1314. for d in (
  1315. db.query(
  1316. BizResearchDonation.id,
  1317. BizResearchDonation.donation_status,
  1318. BizResearchDonation.void_type,
  1319. )
  1320. .filter(BizResearchDonation.id.in_(donation_wo_ids), BizResearchDonation.del_flag == 0)
  1321. .all()
  1322. ):
  1323. donation_status_map[d.id] = d.donation_status or ""
  1324. donation_void_type_map[d.id] = d.void_type or ""
  1325. from app.core.protocol_status_display import format_protocol_status_label
  1326. for item, row in zip(items, rows):
  1327. wo = wo_map.get(row.work_order_id)
  1328. if wo:
  1329. item["workOrderNo"] = wo.work_order_no or ""
  1330. wo_type = getattr(wo, "work_order_type", None) or "inbound"
  1331. if wo_type != "outbound":
  1332. item["protocolNo"] = wo.protocol_no or ""
  1333. item["sampleCode"] = wo.sample_code or ""
  1334. _source = wo.reception_source or "cell_storage"
  1335. if _source == "research_donation":
  1336. _st = donation_status_map.get(wo.collection_protocol_id, "")
  1337. _vt = donation_void_type_map.get(wo.collection_protocol_id, "")
  1338. item["protocolStatus"] = _st
  1339. item["voidType"] = _vt
  1340. item["protocolStatusLabel"] = format_protocol_status_label(
  1341. _st, _vt, kind="donation", db=db, tenant_no=tenant_no
  1342. )
  1343. else:
  1344. _st = proto_status_map.get(wo.collection_protocol_id, "")
  1345. _vt = proto_void_type_map.get(wo.collection_protocol_id, "")
  1346. item["protocolStatus"] = _st
  1347. item["voidType"] = _vt
  1348. item["protocolStatusLabel"] = format_protocol_status_label(
  1349. _st, _vt, kind="protocol", db=db, tenant_no=tenant_no
  1350. )
  1351. else:
  1352. item["sampleCode"] = wo.sample_code or ""
  1353. _enrich_outbound_task_items(db, tenant_no, items, wo_ctx_map)
  1354. # 批量补充接收人、接收时间、样本类型(与质检任务/工单列表一致:优先接收记录样本 id)
  1355. from app.crud.qc_work_order import (
  1356. _effective_donation_sample_id,
  1357. _effective_protocol_sample_id,
  1358. _effective_reception_source,
  1359. _load_sample_type_name_map,
  1360. _sample_type_name_for_item,
  1361. )
  1362. from app.crud.sample_reception import _donation_sample_id_col
  1363. reception_ids = [wo.reception_id for wo in wo_map.values() if getattr(wo, "reception_id", None)]
  1364. reception_map: Dict[str, BizSampleReception] = {}
  1365. if reception_ids:
  1366. reception_map = {
  1367. r.id: r
  1368. for r in db.query(BizSampleReception)
  1369. .filter(BizSampleReception.id.in_(reception_ids), BizSampleReception.del_flag == 0)
  1370. .all()
  1371. }
  1372. wo_item_ctx: Dict[str, Dict[str, Any]] = {}
  1373. cell_sample_ids: List[str] = []
  1374. donation_sample_ids: List[str] = []
  1375. for wo in wo_map.values():
  1376. rec = reception_map.get(wo.reception_id) if wo.reception_id else None
  1377. ctx = {
  1378. "receptionSource": getattr(wo, "reception_source", "cell_storage") or "cell_storage",
  1379. "protocolSampleId": wo.protocol_sample_id or "",
  1380. "collectionProtocolId": wo.collection_protocol_id or "",
  1381. }
  1382. wo_item_ctx[wo.id] = ctx
  1383. source = _effective_reception_source(ctx, rec)
  1384. if source == "research_donation":
  1385. dsid = _effective_donation_sample_id(ctx, rec)
  1386. if dsid:
  1387. donation_sample_ids.append(dsid)
  1388. else:
  1389. psid = _effective_protocol_sample_id(ctx, rec)
  1390. if psid:
  1391. cell_sample_ids.append(psid)
  1392. cell_sample_ids = list(dict.fromkeys(cell_sample_ids))
  1393. donation_sample_ids = list(dict.fromkeys(donation_sample_ids))
  1394. protocol_samples: Dict[str, BizProtocolSample] = {}
  1395. if cell_sample_ids:
  1396. protocol_samples = {
  1397. s.id: s
  1398. for s in db.query(BizProtocolSample)
  1399. .filter(BizProtocolSample.id.in_(cell_sample_ids), BizProtocolSample.del_flag == 0)
  1400. .all()
  1401. }
  1402. donation_samples: Dict[str, BizResearchDonationSample] = {}
  1403. if donation_sample_ids:
  1404. for s in (
  1405. db.query(BizResearchDonationSample)
  1406. .filter(
  1407. _donation_sample_id_col(BizResearchDonationSample.id).in_(donation_sample_ids),
  1408. BizResearchDonationSample.del_flag == 0,
  1409. )
  1410. .all()
  1411. ):
  1412. donation_samples[s.id] = s
  1413. donation_samples[(s.id or "").strip()] = s
  1414. type_ids = list(
  1415. {
  1416. s.sample_type_id
  1417. for s in list(protocol_samples.values()) + list(donation_samples.values())
  1418. if s.sample_type_id
  1419. }
  1420. )
  1421. type_map = _load_sample_type_name_map(db, type_ids)
  1422. # 将补充字段写入 items
  1423. for item, row in zip(items, rows):
  1424. wo = wo_map.get(row.work_order_id)
  1425. if not wo:
  1426. continue
  1427. wo_type = getattr(wo, "work_order_type", None) or "inbound"
  1428. if wo_type == "outbound":
  1429. continue
  1430. item["protocolNo"] = wo.protocol_no or ""
  1431. item["sampleCode"] = wo.sample_code or ""
  1432. rec = reception_map.get(wo.reception_id) if wo.reception_id else None
  1433. item["receiverName"] = rec.handler_name or "" if rec else ""
  1434. item["receptionTime"] = _fmt_dt(rec.reception_time) if rec else ""
  1435. ctx = wo_item_ctx.get(wo.id, {})
  1436. source = _effective_reception_source(ctx, rec)
  1437. item["sampleTypeName"] = _sample_type_name_for_item(
  1438. source=source,
  1439. donation_sample_id=_effective_donation_sample_id(ctx, rec),
  1440. protocol_sample_id=_effective_protocol_sample_id(ctx, rec),
  1441. type_map=type_map,
  1442. protocol_samples=protocol_samples,
  1443. donation_samples=donation_samples,
  1444. )
  1445. _rs = source
  1446. item["receptionSource"] = _rs
  1447. item["receptionSourceLabel"] = _RECEPTION_SOURCE_LABELS.get(_rs, _rs)
  1448. _enrich_assignee_names(db, items)
  1449. if include_step_summary and items:
  1450. _enrich_tasks_with_step_summaries(db, items, tenant_no=tenant_no)
  1451. return items, int(total)
  1452. def list_mine_production_tasks(
  1453. db: Session,
  1454. *,
  1455. assignee_user_id: str,
  1456. tenant_no: str,
  1457. scope: str = "current",
  1458. page: int = 1,
  1459. page_size: int = 10,
  1460. keyword: Optional[str] = None,
  1461. task_status: Optional[str] = None,
  1462. work_order_type: Optional[str] = None,
  1463. reception_source: Optional[str] = None,
  1464. biz_type: Optional[str] = None,
  1465. include_step_summary: bool = False,
  1466. exclude_void_protocol: bool = True,
  1467. planned_start_time_start: Optional[str] = None,
  1468. planned_start_time_end: Optional[str] = None,
  1469. ) -> Tuple[List[Dict[str, Any]], int]:
  1470. uid = (assignee_user_id or "").strip()
  1471. if not uid:
  1472. return [], 0
  1473. pst_start = (planned_start_time_start or "").strip()
  1474. pst_end = (planned_start_time_end or "").strip()
  1475. mine_planned_user_id = uid if (pst_start or pst_end) else None
  1476. sc = (scope or "current").strip().lower()
  1477. if sc == "history":
  1478. items, total = list_production_tasks(
  1479. db,
  1480. tenant_no=tenant_no,
  1481. page=page,
  1482. page_size=page_size,
  1483. keyword=keyword,
  1484. task_status=task_status,
  1485. mine_history_user_id=uid,
  1486. work_order_type=work_order_type,
  1487. reception_source=reception_source,
  1488. biz_type=biz_type,
  1489. include_step_summary=include_step_summary,
  1490. exclude_void_protocol=exclude_void_protocol,
  1491. planned_start_time_start=planned_start_time_start,
  1492. planned_start_time_end=planned_start_time_end,
  1493. planned_start_time_mine_user_id=mine_planned_user_id,
  1494. )
  1495. _enrich_handover_times(db, items, from_user_id=uid, tenant_no=tenant_no)
  1496. return items, total
  1497. return list_production_tasks(
  1498. db,
  1499. tenant_no=tenant_no,
  1500. page=page,
  1501. page_size=page_size,
  1502. keyword=keyword,
  1503. task_status=task_status,
  1504. step_assignee_user_id=uid,
  1505. work_order_type=work_order_type,
  1506. reception_source=reception_source,
  1507. biz_type=biz_type,
  1508. include_step_summary=include_step_summary,
  1509. exclude_void_protocol=exclude_void_protocol,
  1510. planned_start_time_start=planned_start_time_start,
  1511. planned_start_time_end=planned_start_time_end,
  1512. planned_start_time_mine_user_id=mine_planned_user_id,
  1513. )
  1514. def get_production_task_detail(
  1515. db: Session,
  1516. task_id: str,
  1517. tenant_no: str,
  1518. *,
  1519. include_step_sub_tables: bool = True,
  1520. ) -> Optional[Dict[str, Any]]:
  1521. task = db.query(BizProductionTask).filter(
  1522. and_(
  1523. BizProductionTask.id == task_id,
  1524. BizProductionTask.tenant_no == tenant_no,
  1525. BizProductionTask.del_flag == 0,
  1526. )
  1527. ).first()
  1528. if not task:
  1529. return None
  1530. wo = db.query(BizProductionWorkOrder).filter(BizProductionWorkOrder.id == task.work_order_id).first()
  1531. result = _task_to_dict(task, work_order_no=wo.work_order_no if wo else "")
  1532. if wo:
  1533. wo_type = getattr(wo, "work_order_type", None) or "inbound"
  1534. result["workOrderType"] = wo_type
  1535. if wo_type == "outbound":
  1536. result["bizType"] = getattr(wo, "biz_type", None) or ""
  1537. elif wo_type == "inbound":
  1538. result["receptionSource"] = (
  1539. getattr(wo, "reception_source", "cell_storage") or "cell_storage"
  1540. )
  1541. result["sampleCode"] = wo.sample_code or ""
  1542. if wo.reception_id:
  1543. rec = (
  1544. db.query(BizSampleReception)
  1545. .filter(
  1546. BizSampleReception.id == wo.reception_id,
  1547. BizSampleReception.tenant_no == tenant_no,
  1548. BizSampleReception.del_flag == 0,
  1549. )
  1550. .first()
  1551. )
  1552. if rec and (rec.lab_storage_code or "").strip():
  1553. result["labStorageCode"] = (rec.lab_storage_code or "").strip()
  1554. if wo_type == "outbound":
  1555. wo_ctx = {
  1556. wo.id: {
  1557. "workOrderType": wo_type,
  1558. "bizType": getattr(wo, "biz_type", None) or "",
  1559. "infusionScheduleId": getattr(wo, "infusion_schedule_id", None) or "",
  1560. }
  1561. }
  1562. _enrich_outbound_task_items(db, tenant_no, [result], wo_ctx)
  1563. else:
  1564. result["protocolNo"] = wo.protocol_no or ""
  1565. _src = getattr(wo, "reception_source", "cell_storage") or "cell_storage"
  1566. _cpid = wo.collection_protocol_id or ""
  1567. if _src == "research_donation" and _cpid:
  1568. d = (
  1569. db.query(BizResearchDonation.donation_status, BizResearchDonation.void_type)
  1570. .filter(BizResearchDonation.id == _cpid, BizResearchDonation.del_flag == 0)
  1571. .first()
  1572. )
  1573. _st = d.donation_status or "" if d else ""
  1574. _vt = d.void_type or "" if d else ""
  1575. result["protocolStatus"] = _st
  1576. result["voidType"] = _vt
  1577. from app.core.protocol_status_display import format_protocol_status_label
  1578. result["protocolStatusLabel"] = format_protocol_status_label(
  1579. _st, _vt, kind="donation", db=db, tenant_no=tenant_no
  1580. )
  1581. elif _cpid:
  1582. p = (
  1583. db.query(BizCollectionProtocol.protocol_status, BizCollectionProtocol.void_type)
  1584. .filter(BizCollectionProtocol.id == _cpid, BizCollectionProtocol.del_flag == 0)
  1585. .first()
  1586. )
  1587. _st = p.protocol_status or "" if p else ""
  1588. _vt = p.void_type or "" if p else ""
  1589. result["protocolStatus"] = _st
  1590. result["voidType"] = _vt
  1591. from app.core.protocol_status_display import format_protocol_status_label
  1592. result["protocolStatusLabel"] = format_protocol_status_label(
  1593. _st, _vt, kind="protocol", db=db, tenant_no=tenant_no
  1594. )
  1595. else:
  1596. result["protocolStatus"] = ""
  1597. result["protocolStatusLabel"] = ""
  1598. steps = (
  1599. db.query(BizProductionTaskStep)
  1600. .filter(and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0))
  1601. .order_by(BizProductionTaskStep.step_order.asc(), BizProductionTaskStep.create_time.asc())
  1602. .all()
  1603. )
  1604. step_ids = [s.id for s in steps]
  1605. sub_tables_by_step: Dict[str, Dict[str, Any]] = {}
  1606. if include_step_sub_tables and step_ids:
  1607. sub_tables_by_step = _batch_load_step_sub_tables(db, step_ids)
  1608. step_list = []
  1609. for s in steps:
  1610. sd = _step_to_dict(s)
  1611. sd.update(sub_tables_by_step.get(s.id) or _empty_step_sub_tables())
  1612. step_list.append(sd)
  1613. from app.crud.production_task_step_assignee import enrich_step_assignees, enrich_step_completed_by_names
  1614. enrich_step_assignees(db, step_list, tenant_no=tenant_no)
  1615. enrich_step_completed_by_names(db, step_list)
  1616. inputs = db.query(BizProductionTaskInput).filter(
  1617. and_(BizProductionTaskInput.task_id == task_id, BizProductionTaskInput.del_flag == 0)
  1618. ).order_by(BizProductionTaskInput.order_num.asc(), BizProductionTaskInput.create_time.asc()).all()
  1619. outputs = db.query(BizProductionTaskOutput).filter(
  1620. and_(BizProductionTaskOutput.task_id == task_id, BizProductionTaskOutput.del_flag == 0)
  1621. ).order_by(BizProductionTaskOutput.order_num.asc(), BizProductionTaskOutput.create_time.asc()).all()
  1622. result["steps"] = step_list
  1623. # --- enrich inputs with cell product info ---
  1624. input_dicts = [_task_input_to_dict(i) for i in inputs]
  1625. cp_input_ids = [i.input_id for i in inputs if i.input_type == "cell_product" and i.input_id]
  1626. input_product_map: Dict[str, Dict[str, Any]] = {}
  1627. if cp_input_ids:
  1628. cp_rows = db.query(BizCellProduct).filter(
  1629. BizCellProduct.id.in_(set(cp_input_ids)), BizCellProduct.del_flag == 0
  1630. ).all()
  1631. cp_dicts = [_cp_row_to_dict(c) for c in cp_rows]
  1632. cp_dicts = _cp_enrich(db, tenant_no, cp_dicts)
  1633. input_product_map = {d["id"]: d for d in cp_dicts}
  1634. for idx, inp in enumerate(inputs):
  1635. if inp.input_type == "cell_product" and inp.input_id and inp.input_id in input_product_map:
  1636. input_dicts[idx] = _task_input_to_dict(inp, product_dict=input_product_map[inp.input_id])
  1637. sample_codes: List[str] = []
  1638. task_sample_code = (result.get("sampleCode") or "").strip()
  1639. if task_sample_code:
  1640. sample_codes.append(task_sample_code)
  1641. for item in input_dicts:
  1642. if item.get("inputType") == "sample":
  1643. sc = (item.get("inputCode") or "").strip()
  1644. if sc:
  1645. sample_codes.append(sc)
  1646. if sample_codes:
  1647. from app.crud import sample_reception as reception_svc
  1648. code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(
  1649. db, tenant_no, sample_codes
  1650. )
  1651. if task_sample_code and code_map.get(task_sample_code) and not result.get("labStorageCode"):
  1652. result["labStorageCode"] = code_map[task_sample_code]
  1653. for item in input_dicts:
  1654. if item.get("inputType") == "sample":
  1655. sc = (item.get("inputCode") or "").strip()
  1656. item["labStorageCode"] = code_map.get(sc, "")
  1657. result["inputs"] = input_dicts
  1658. # --- enrich outputs with cell product info ---
  1659. output_dicts = [_task_output_to_dict(o) for o in outputs]
  1660. cp_output_ids = [o.cell_product_id for o in outputs if o.cell_product_id]
  1661. output_product_map: Dict[str, Dict[str, Any]] = {}
  1662. if cp_output_ids:
  1663. cp_rows = db.query(BizCellProduct).filter(
  1664. BizCellProduct.id.in_(set(cp_output_ids)), BizCellProduct.del_flag == 0
  1665. ).all()
  1666. cp_dicts = [_cp_row_to_dict(c) for c in cp_rows]
  1667. cp_dicts = _cp_enrich(db, tenant_no, cp_dicts)
  1668. output_product_map = {d["id"]: d for d in cp_dicts}
  1669. for idx, out in enumerate(outputs):
  1670. if out.cell_product_id and out.cell_product_id in output_product_map:
  1671. output_dicts[idx] = _task_output_to_dict(out, product_dict=output_product_map[out.cell_product_id])
  1672. result["outputs"] = output_dicts
  1673. return result
  1674. def _build_process_library_for_editor(
  1675. processes: List[Dict[str, Any]],
  1676. categories: List[Dict[str, Any]],
  1677. ) -> List[Dict[str, Any]]:
  1678. """按分类分组工艺库,结构与前端 route-config 左栏一致。"""
  1679. cat_map: Dict[str, Dict[str, Any]] = {}
  1680. u_key = "__uncategorized__"
  1681. cat_map[u_key] = {"categoryId": u_key, "categoryName": "未分类", "processes": []}
  1682. for cat in categories or []:
  1683. cat_id = (cat.get("id") or "").strip()
  1684. if not cat_id:
  1685. continue
  1686. cat_map[cat_id] = {
  1687. "categoryId": cat_id,
  1688. "categoryName": cat.get("categoryName") or cat.get("name") or "未命名分类",
  1689. "processes": [],
  1690. }
  1691. for proc in processes or []:
  1692. cat_id = (proc.get("categoryId") or "").strip() or u_key
  1693. if cat_id not in cat_map:
  1694. cat_map[cat_id] = {"categoryId": cat_id, "categoryName": "未知分类", "processes": []}
  1695. cat_map[cat_id]["processes"].append(proc)
  1696. return list(cat_map.values())
  1697. def get_production_task_editor_init(
  1698. db: Session,
  1699. task_id: str,
  1700. tenant_no: str,
  1701. *,
  1702. user_info=None,
  1703. include_reference: bool = True,
  1704. ) -> Optional[Dict[str, Any]]:
  1705. """编辑生产任务弹窗初始化:任务详情 + 工艺库 + 步骤配置参考数据(单次聚合)。"""
  1706. task = get_production_task_detail(
  1707. db, task_id, tenant_no, include_step_sub_tables=True
  1708. )
  1709. if not task:
  1710. return None
  1711. result: Dict[str, Any] = {"task": task}
  1712. if not include_reference:
  1713. return result
  1714. from app.crud import consumable as consumable_svc
  1715. from app.crud import equipment as equipment_svc
  1716. from app.crud import item_unit as item_unit_svc
  1717. from app.crud import output_type as output_type_svc
  1718. from app.crud import process as process_svc
  1719. from app.crud import process_category as process_category_svc
  1720. from app.crud import reagent as reagent_svc
  1721. from app.crud import sop_document as sop_document_svc
  1722. processes = process_svc.list_all_processes(db, tenant_no, user_info=user_info)
  1723. categories, _ = process_category_svc.list_process_categories(
  1724. db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
  1725. )
  1726. sops, _ = sop_document_svc.list_sop_documents(
  1727. db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
  1728. )
  1729. output_types, _ = output_type_svc.list_output_types(
  1730. db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
  1731. )
  1732. equipments, _ = equipment_svc.list_equipments(
  1733. db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
  1734. )
  1735. reagents, _ = reagent_svc.list_reagents(
  1736. db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
  1737. )
  1738. consumables, _ = consumable_svc.list_consumables(
  1739. db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
  1740. )
  1741. item_units, _ = item_unit_svc.list_item_units(
  1742. db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
  1743. )
  1744. result["processLibrary"] = _build_process_library_for_editor(processes, categories)
  1745. result["options"] = {
  1746. "processCategories": categories,
  1747. "sops": sops,
  1748. "outputTypes": output_types,
  1749. "equipments": equipments,
  1750. "reagents": reagents,
  1751. "consumables": consumables,
  1752. "itemUnits": item_units,
  1753. }
  1754. return result
  1755. def update_production_task(
  1756. db: Session, task_id: str, tenant_no: str,
  1757. user_id: Optional[str], data: Dict[str, Any],
  1758. ) -> BizProductionTask:
  1759. task = db.query(BizProductionTask).filter(
  1760. and_(
  1761. BizProductionTask.id == task_id,
  1762. BizProductionTask.tenant_no == tenant_no,
  1763. BizProductionTask.del_flag == 0,
  1764. )
  1765. ).first()
  1766. if not task:
  1767. raise ValueError("任务不存在")
  1768. for field in ["task_name", "description", "remark", "attachments"]:
  1769. val = data.get(field)
  1770. if val is not None:
  1771. setattr(task, field, (val or "").strip() or None)
  1772. if "equipment_ids" in data:
  1773. eids, enames = resolve_equipment_list_snapshot(db, data.get("equipment_ids"), tenant_no)
  1774. task.equipment_ids = eids
  1775. task.equipment_name = enames
  1776. task.update_by = user_id
  1777. task.update_time = datetime.now()
  1778. db.add(task)
  1779. db.commit()
  1780. db.refresh(task)
  1781. return task
  1782. def _consume_task_input_cell_products(
  1783. db: Session,
  1784. task_id: str,
  1785. user_id: Optional[str],
  1786. now: datetime,
  1787. ) -> None:
  1788. """任务首次完成时,将原材料中的细胞产品标记为已使用并释放库位占用。"""
  1789. input_rows = db.query(BizProductionTaskInput).filter(
  1790. and_(
  1791. BizProductionTaskInput.task_id == task_id,
  1792. BizProductionTaskInput.del_flag == 0,
  1793. BizProductionTaskInput.input_type == "cell_product",
  1794. BizProductionTaskInput.input_id.isnot(None),
  1795. BizProductionTaskInput.input_id != "",
  1796. )
  1797. ).all()
  1798. seen_ids: set = set()
  1799. consumable_from = frozenset({"frozen", "thawed", "in_production"})
  1800. for row in input_rows:
  1801. cp_id = (row.input_id or "").strip()
  1802. if not cp_id or cp_id in seen_ids:
  1803. continue
  1804. seen_ids.add(cp_id)
  1805. product = db.query(BizCellProduct).filter(
  1806. BizCellProduct.id == cp_id,
  1807. BizCellProduct.del_flag == 0,
  1808. ).first()
  1809. if not product:
  1810. continue
  1811. current = (product.product_status or "").strip()
  1812. if current in ("used", "voided", "sold"):
  1813. continue
  1814. if current not in consumable_from:
  1815. continue
  1816. product.product_status = "used"
  1817. product.update_by = user_id
  1818. product.update_time = now
  1819. db.add(product)
  1820. if product.storage_slot_id:
  1821. try:
  1822. occupancy_svc.remove_by_item(
  1823. db, "cell_product", product.id, user_id=user_id,
  1824. )
  1825. except Exception:
  1826. pass
  1827. def _force_complete_task_steps(
  1828. db: Session,
  1829. task_id: str,
  1830. tenant_no: str,
  1831. user_id: Optional[str],
  1832. now: datetime,
  1833. ) -> int:
  1834. """将任务下未完成步骤置为 ywc;不 commit。返回更新条数。"""
  1835. steps = (
  1836. db.query(BizProductionTaskStep)
  1837. .filter(
  1838. and_(
  1839. BizProductionTaskStep.task_id == task_id,
  1840. BizProductionTaskStep.tenant_no == tenant_no,
  1841. BizProductionTaskStep.del_flag == 0,
  1842. BizProductionTaskStep.step_status != "ywc",
  1843. )
  1844. )
  1845. .all()
  1846. )
  1847. for step in steps:
  1848. step.step_status = "ywc"
  1849. if user_id:
  1850. step.completed_by = user_id
  1851. step.completed_time = now
  1852. step.update_by = user_id
  1853. step.update_time = now
  1854. db.add(step)
  1855. return len(steps)
  1856. def update_task_status(
  1857. db: Session,
  1858. task_id: str,
  1859. tenant_no: str,
  1860. user_id: Optional[str],
  1861. status: str,
  1862. *,
  1863. conclusion: Optional[str] = None,
  1864. remark: Optional[str] = None,
  1865. ) -> BizProductionTask:
  1866. task = db.query(BizProductionTask).filter(
  1867. and_(
  1868. BizProductionTask.id == task_id,
  1869. BizProductionTask.tenant_no == tenant_no,
  1870. BizProductionTask.del_flag == 0,
  1871. )
  1872. ).first()
  1873. if not task:
  1874. raise ValueError("任务不存在")
  1875. if task.task_status == "ywc" and status != "ywc":
  1876. raise ValueError("任务已完成,不可变更状态")
  1877. now = datetime.now()
  1878. previous_status = task.task_status
  1879. task.task_status = status
  1880. if status == "jxz" and not task.started_time:
  1881. task.started_time = now
  1882. if status == "ywc":
  1883. concl = (conclusion or "").strip()
  1884. if concl not in ("cg", "sb"):
  1885. raise ValueError("须选择生产结论(成功或失败)")
  1886. if previous_status != "ywc":
  1887. _force_complete_task_steps(db, task_id, tenant_no, user_id, now)
  1888. task.conclusion = concl
  1889. if remark is not None:
  1890. task.remark = (remark or "").strip() or None
  1891. if previous_status != "ywc" and not task.completed_time:
  1892. task.completed_time = now
  1893. if previous_status != "ywc":
  1894. _consume_task_input_cell_products(db, task.id, user_id, now)
  1895. task.update_by = user_id
  1896. task.update_time = now
  1897. db.add(task)
  1898. from app.crud import production_work_order as production_work_order_svc
  1899. production_work_order_svc.sync_production_work_order_status_from_tasks(
  1900. db, task.work_order_id or "", tenant_no, user_id
  1901. )
  1902. db.commit()
  1903. db.refresh(task)
  1904. return task
  1905. def add_task_step(
  1906. db: Session, task_id: str, tenant_no: str,
  1907. user_id: Optional[str], data: Dict[str, Any],
  1908. ) -> Dict[str, Any]:
  1909. task = db.query(BizProductionTask).filter(
  1910. and_(
  1911. BizProductionTask.id == task_id,
  1912. BizProductionTask.tenant_no == tenant_no,
  1913. BizProductionTask.del_flag == 0,
  1914. )
  1915. ).first()
  1916. if not task:
  1917. raise ValueError("任务不存在")
  1918. now = datetime.now()
  1919. step_order = data.get("step_order")
  1920. if step_order is None:
  1921. max_order = db.query(func.max(BizProductionTaskStep.step_order)).filter(
  1922. and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0)
  1923. ).scalar()
  1924. step_order = (max_order or 0) + 1
  1925. # 如果传入了 process_id,从工艺主表深拷贝基本信息和子表数据
  1926. process_id = data.get("process_id")
  1927. if process_id:
  1928. proc = db.query(BizProcess).filter(
  1929. and_(BizProcess.id == process_id, BizProcess.del_flag == 0)
  1930. ).first()
  1931. if not proc:
  1932. raise ValueError("工艺不存在")
  1933. step = BizProductionTaskStep(
  1934. id=generate_uuid(), task_id=task_id,
  1935. step_order=step_order,
  1936. process_code=proc.process_code,
  1937. category_id=proc.category_id,
  1938. process_name=proc.process_name,
  1939. description=proc.description,
  1940. estimated_duration=proc.estimated_duration,
  1941. sop_ids=proc.sop_ids,
  1942. sop_content=proc.sop_content,
  1943. step_status="dzx",
  1944. tenant_no=tenant_no, order_num=0,
  1945. remark=None,
  1946. create_by=user_id, create_dept_id="",
  1947. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  1948. )
  1949. db.add(step)
  1950. db.flush()
  1951. # 深拷贝工艺子表 → 任务步骤子表
  1952. reagent_rows = db.query(BizProcessReagent).filter(
  1953. and_(BizProcessReagent.process_id == process_id, BizProcessReagent.del_flag == 0)
  1954. ).order_by(BizProcessReagent.order_num.asc(), BizProcessReagent.create_time.asc()).all()
  1955. for idx, r in enumerate(reagent_rows):
  1956. db.add(BizProductionTaskStepReagent(
  1957. id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
  1958. reagent_code=r.reagent_code, reagent_name=r.reagent_name,
  1959. spec=r.spec, standard_usage=r.standard_usage,
  1960. unit=r.unit, is_key_material=r.is_key_material,
  1961. remark=r.remark,
  1962. order_num=_resolve_sub_table_order_num(r.order_num, idx, reagent_rows),
  1963. create_by=user_id, create_dept_id="",
  1964. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  1965. ))
  1966. consumable_rows = db.query(BizProcessConsumable).filter(
  1967. and_(BizProcessConsumable.process_id == process_id, BizProcessConsumable.del_flag == 0)
  1968. ).order_by(BizProcessConsumable.order_num.asc(), BizProcessConsumable.create_time.asc()).all()
  1969. for idx, c in enumerate(consumable_rows):
  1970. db.add(BizProductionTaskStepConsumable(
  1971. id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
  1972. consumable_code=c.consumable_code, consumable_name=c.consumable_name,
  1973. spec_model=c.spec_model, standard_usage=c.standard_usage,
  1974. unit=c.unit, is_required=c.is_required,
  1975. remark=c.remark,
  1976. order_num=_resolve_sub_table_order_num(c.order_num, idx, consumable_rows),
  1977. create_by=user_id, create_dept_id="",
  1978. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  1979. ))
  1980. output_rows = db.query(BizProcessOutput).filter(
  1981. and_(BizProcessOutput.process_id == process_id, BizProcessOutput.del_flag == 0)
  1982. ).order_by(BizProcessOutput.order_num.asc(), BizProcessOutput.create_time.asc()).all()
  1983. for idx, o in enumerate(output_rows):
  1984. db.add(BizProductionTaskStepOutput(
  1985. id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
  1986. output_type_id=o.output_type_id, output_name=o.output_name,
  1987. output_barcode=o.output_barcode,
  1988. remark=o.remark,
  1989. order_num=_resolve_sub_table_order_num(o.order_num, idx, output_rows),
  1990. create_by=user_id, create_dept_id="",
  1991. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  1992. ))
  1993. collection_rows = db.query(BizProcessDataCollection).filter(
  1994. and_(BizProcessDataCollection.process_id == process_id, BizProcessDataCollection.del_flag == 0)
  1995. ).order_by(BizProcessDataCollection.order_num.asc(), BizProcessDataCollection.create_time.asc()).all()
  1996. for idx, d in enumerate(collection_rows):
  1997. db.add(BizProductionTaskStepDataCollection(
  1998. id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
  1999. collection_name=d.collection_name, unit=d.unit,
  2000. remark=d.remark,
  2001. order_num=_resolve_sub_table_order_num(d.order_num, idx, collection_rows),
  2002. create_by=user_id, create_dept_id="",
  2003. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2004. ))
  2005. db.commit()
  2006. else:
  2007. # 无 process_id 时,使用前端传来的字段直接创建(兼容旧逻辑)
  2008. step = BizProductionTaskStep(
  2009. id=generate_uuid(), task_id=task_id,
  2010. step_order=step_order,
  2011. process_code=(data.get("process_code") or "").strip() or None,
  2012. category_id=(data.get("category_id") or "").strip() or None,
  2013. process_name=(data.get("process_name") or "").strip() or None,
  2014. description=(data.get("description") or "").strip() or None,
  2015. estimated_duration=(data.get("estimated_duration") or "").strip() or None,
  2016. actual_duration=(data.get("actual_duration") or "").strip() or None,
  2017. sop_ids=(data.get("sop_ids") or "").strip() or None,
  2018. sop_content=(data.get("sop_content") if str(data.get("sop_content") or "").strip() else None),
  2019. step_status=(data.get("step_status") or "dzx"),
  2020. tenant_no=tenant_no, order_num=0,
  2021. remark=(data.get("remark") or "").strip() or None,
  2022. create_by=user_id, create_dept_id="",
  2023. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2024. )
  2025. db.add(step)
  2026. db.commit()
  2027. # 返回带子表的完整步骤信息(与 get_production_task_detail 格式一致)
  2028. result = _step_to_dict(step)
  2029. reagents = db.query(BizProductionTaskStepReagent).filter(
  2030. and_(BizProductionTaskStepReagent.step_id == step.id, BizProductionTaskStepReagent.del_flag == 0)
  2031. ).order_by(BizProductionTaskStepReagent.order_num.asc(), BizProductionTaskStepReagent.create_time.asc()).all()
  2032. consumables = db.query(BizProductionTaskStepConsumable).filter(
  2033. and_(BizProductionTaskStepConsumable.step_id == step.id, BizProductionTaskStepConsumable.del_flag == 0)
  2034. ).order_by(BizProductionTaskStepConsumable.order_num.asc(), BizProductionTaskStepConsumable.create_time.asc()).all()
  2035. outputs = db.query(BizProductionTaskStepOutput).filter(
  2036. and_(BizProductionTaskStepOutput.step_id == step.id, BizProductionTaskStepOutput.del_flag == 0)
  2037. ).order_by(BizProductionTaskStepOutput.order_num.asc(), BizProductionTaskStepOutput.create_time.asc()).all()
  2038. data_collections = db.query(BizProductionTaskStepDataCollection).filter(
  2039. and_(BizProductionTaskStepDataCollection.step_id == step.id, BizProductionTaskStepDataCollection.del_flag == 0)
  2040. ).order_by(BizProductionTaskStepDataCollection.order_num.asc(), BizProductionTaskStepDataCollection.create_time.asc()).all()
  2041. configured_reagents = db.query(BizProductionTaskStepConfiguredReagent).filter(
  2042. and_(BizProductionTaskStepConfiguredReagent.step_id == step.id, BizProductionTaskStepConfiguredReagent.del_flag == 0)
  2043. ).order_by(BizProductionTaskStepConfiguredReagent.order_num.asc(), BizProductionTaskStepConfiguredReagent.create_time.asc()).all()
  2044. result["reagents"] = [_step_reagent_to_dict(r) for r in reagents]
  2045. result["consumables"] = [_step_consumable_to_dict(c) for c in consumables]
  2046. result["configuredReagents"] = [_step_configured_reagent_to_dict(r) for r in configured_reagents]
  2047. result["outputs"] = [_step_output_to_dict(o) for o in outputs]
  2048. result["dataCollections"] = [_step_data_collection_to_dict(d) for d in data_collections]
  2049. return result
  2050. def update_task_step(
  2051. db: Session, step_id: str, tenant_no: str,
  2052. user_id: Optional[str], data: Dict[str, Any],
  2053. ) -> BizProductionTaskStep:
  2054. step = db.query(BizProductionTaskStep).filter(
  2055. and_(
  2056. BizProductionTaskStep.id == step_id,
  2057. BizProductionTaskStep.tenant_no == tenant_no,
  2058. BizProductionTaskStep.del_flag == 0,
  2059. )
  2060. ).first()
  2061. if not step:
  2062. raise ValueError("步骤不存在")
  2063. for field in [
  2064. "step_order", "process_code", "category_id", "process_name",
  2065. "description", "estimated_duration", "actual_duration",
  2066. "sop_ids", "sop_content", "step_status", "remark",
  2067. ]:
  2068. val = data.get(field)
  2069. if val is not None:
  2070. if field == "step_order":
  2071. setattr(step, field, int(val))
  2072. elif field == "sop_content":
  2073. setattr(step, field, val if str(val).strip() else None)
  2074. else:
  2075. setattr(step, field, (val or "").strip() or None)
  2076. if data.get("started_time"):
  2077. parsed = parse_datetime(str(data["started_time"]).strip())
  2078. if parsed is None:
  2079. raise ValueError("开始时间格式无效")
  2080. step.started_time = parsed
  2081. if data.get("completed_time"):
  2082. parsed = parse_datetime(str(data["completed_time"]).strip())
  2083. if parsed is None:
  2084. raise ValueError("完成时间格式无效")
  2085. step.completed_time = parsed
  2086. new_status = (data.get("step_status") or "").strip()
  2087. if new_status == "ywc" and user_id:
  2088. step.completed_by = user_id
  2089. step.update_by = user_id
  2090. step.update_time = datetime.now()
  2091. db.add(step)
  2092. db.commit()
  2093. db.refresh(step)
  2094. return step
  2095. def delete_task_step(db: Session, step_id: str, tenant_no: str, user_id: Optional[str]) -> None:
  2096. step = db.query(BizProductionTaskStep).filter(
  2097. and_(
  2098. BizProductionTaskStep.id == step_id,
  2099. BizProductionTaskStep.tenant_no == tenant_no,
  2100. BizProductionTaskStep.del_flag == 0,
  2101. )
  2102. ).first()
  2103. if not step:
  2104. raise ValueError("步骤不存在")
  2105. now = datetime.now()
  2106. step.del_flag = 1
  2107. step.update_by = user_id
  2108. step.update_time = now
  2109. db.add(step)
  2110. _soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step_id, now)
  2111. _soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step_id, now)
  2112. _soft_delete_sub(db, BizProductionTaskStepConfiguredReagent, "step_id", step_id, now)
  2113. _soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step_id, now)
  2114. _soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step_id, now)
  2115. db.commit()
  2116. def replace_step_reagents(
  2117. db: Session, step_id: str, tenant_no: str,
  2118. user_id: Optional[str], items: List[Dict[str, Any]],
  2119. ) -> None:
  2120. now = datetime.now()
  2121. # Look up task info from step_id for stock log
  2122. step_row = db.query(BizProductionTaskStep).filter(
  2123. and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
  2124. ).first()
  2125. task_id = ""
  2126. task_no = ""
  2127. step_name = ""
  2128. if step_row:
  2129. step_name = step_row.process_name or ""
  2130. task_row = db.query(BizProductionTask).filter(
  2131. and_(BizProductionTask.id == step_row.task_id, BizProductionTask.del_flag == 0)
  2132. ).first()
  2133. if task_row:
  2134. task_id = task_row.id
  2135. task_no = task_row.task_no or ""
  2136. try:
  2137. _soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step_id, now)
  2138. usage_items: List[Dict[str, Any]] = []
  2139. for idx, it in enumerate(items):
  2140. req_id = (
  2141. (it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
  2142. )
  2143. _require_requisition_for_catalog_material(
  2144. db,
  2145. tenant_no,
  2146. it.get("reagent_code"),
  2147. "reagent",
  2148. it.get("actual_usage"),
  2149. req_id,
  2150. )
  2151. db.add(BizProductionTaskStepReagent(
  2152. id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
  2153. reagent_code=(it.get("reagent_code") or "").strip() or None,
  2154. reagent_name=(it.get("reagent_name") or "").strip() or None,
  2155. spec=(it.get("spec") or "").strip() or None,
  2156. standard_usage=(it.get("standard_usage") or "").strip() or None,
  2157. actual_usage=(it.get("actual_usage") or "").strip() or None,
  2158. unit=(it.get("unit") or "").strip() or None,
  2159. requisition_usage_id=req_id or None,
  2160. is_key_material=it.get("is_key_material", "0"),
  2161. remark=(it.get("remark") or "").strip() or None,
  2162. order_num=idx, create_by=user_id, create_dept_id="",
  2163. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2164. ))
  2165. if req_id:
  2166. usage_items.append({
  2167. "requisitionUsageId": req_id,
  2168. "qty": it.get("actual_usage"),
  2169. "usageUnit": it.get("unit"),
  2170. "remark": it.get("remark"),
  2171. })
  2172. from app.crud import production_material_usage as pm_usage_crud
  2173. pm_usage_crud.register_step_usage_from_requisition(
  2174. db, step_id, step_name, task_id, task_no, usage_items, tenant_no, user_id, "",
  2175. )
  2176. db.commit()
  2177. except Exception:
  2178. db.rollback()
  2179. raise
  2180. def replace_step_consumables(
  2181. db: Session, step_id: str, tenant_no: str,
  2182. user_id: Optional[str], items: List[Dict[str, Any]],
  2183. ) -> None:
  2184. now = datetime.now()
  2185. # Look up task info from step_id for stock log
  2186. step_row = db.query(BizProductionTaskStep).filter(
  2187. and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
  2188. ).first()
  2189. task_id = ""
  2190. task_no = ""
  2191. step_name = ""
  2192. if step_row:
  2193. step_name = step_row.process_name or ""
  2194. task_row = db.query(BizProductionTask).filter(
  2195. and_(BizProductionTask.id == step_row.task_id, BizProductionTask.del_flag == 0)
  2196. ).first()
  2197. if task_row:
  2198. task_id = task_row.id
  2199. task_no = task_row.task_no or ""
  2200. try:
  2201. _soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step_id, now)
  2202. usage_items: List[Dict[str, Any]] = []
  2203. for idx, it in enumerate(items):
  2204. req_id = (
  2205. (it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
  2206. )
  2207. _require_requisition_for_catalog_material(
  2208. db,
  2209. tenant_no,
  2210. it.get("consumable_code"),
  2211. "consumable",
  2212. it.get("actual_usage"),
  2213. req_id,
  2214. )
  2215. db.add(BizProductionTaskStepConsumable(
  2216. id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
  2217. consumable_code=(it.get("consumable_code") or "").strip() or None,
  2218. consumable_name=(it.get("consumable_name") or "").strip() or None,
  2219. spec_model=(it.get("spec_model") or "").strip() or None,
  2220. standard_usage=(it.get("standard_usage") or "").strip() or None,
  2221. actual_usage=(it.get("actual_usage") or "").strip() or None,
  2222. unit=(it.get("unit") or "").strip() or None,
  2223. requisition_usage_id=req_id or None,
  2224. is_required=it.get("is_required", "1"),
  2225. remark=(it.get("remark") or "").strip() or None,
  2226. order_num=idx, create_by=user_id, create_dept_id="",
  2227. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2228. ))
  2229. if req_id:
  2230. usage_items.append({
  2231. "requisitionUsageId": req_id,
  2232. "qty": it.get("actual_usage"),
  2233. "usageUnit": it.get("unit"),
  2234. "remark": it.get("remark"),
  2235. })
  2236. from app.crud import production_material_usage as pm_usage_crud
  2237. pm_usage_crud.register_step_usage_from_requisition(
  2238. db, step_id, step_name, task_id, task_no, usage_items, tenant_no, user_id, "",
  2239. )
  2240. db.commit()
  2241. except Exception:
  2242. db.rollback()
  2243. raise
  2244. def replace_step_configured_reagents(
  2245. db: Session, step_id: str, tenant_no: str,
  2246. user_id: Optional[str], items: List[Dict[str, Any]],
  2247. ) -> None:
  2248. now = datetime.now()
  2249. step_row = db.query(BizProductionTaskStep).filter(
  2250. and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
  2251. ).first()
  2252. task_id = ""
  2253. task_no = ""
  2254. step_name = ""
  2255. if step_row:
  2256. step_name = step_row.process_name or ""
  2257. task_row = db.query(BizProductionTask).filter(
  2258. and_(BizProductionTask.id == step_row.task_id, BizProductionTask.del_flag == 0)
  2259. ).first()
  2260. if task_row:
  2261. task_id = task_row.id
  2262. task_no = task_row.task_no or ""
  2263. try:
  2264. _soft_delete_sub(db, BizProductionTaskStepConfiguredReagent, "step_id", step_id, now)
  2265. usage_items: List[Dict[str, Any]] = []
  2266. for idx, it in enumerate(items):
  2267. req_id = (
  2268. (it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
  2269. )
  2270. _require_requisition_for_catalog_material(
  2271. db,
  2272. tenant_no,
  2273. it.get("configured_reagent_code"),
  2274. "configured_reagent",
  2275. it.get("actual_usage"),
  2276. req_id,
  2277. )
  2278. db.add(BizProductionTaskStepConfiguredReagent(
  2279. id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
  2280. configured_reagent_code=(it.get("configured_reagent_code") or "").strip() or None,
  2281. configured_reagent_name=(it.get("configured_reagent_name") or "").strip() or None,
  2282. spec=(it.get("spec") or "").strip() or None,
  2283. standard_usage=(it.get("standard_usage") or "").strip() or None,
  2284. actual_usage=(it.get("actual_usage") or "").strip() or None,
  2285. unit=(it.get("unit") or "").strip() or None,
  2286. requisition_usage_id=req_id or None,
  2287. remark=(it.get("remark") or "").strip() or None,
  2288. order_num=idx, create_by=user_id, create_dept_id="",
  2289. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2290. ))
  2291. if req_id:
  2292. usage_items.append({
  2293. "requisitionUsageId": req_id,
  2294. "qty": it.get("actual_usage"),
  2295. "usageUnit": it.get("unit"),
  2296. "remark": it.get("remark"),
  2297. })
  2298. from app.crud import production_material_usage as pm_usage_crud
  2299. pm_usage_crud.register_step_usage_from_requisition(
  2300. db, step_id, step_name, task_id, task_no, usage_items, tenant_no, user_id, "",
  2301. )
  2302. db.commit()
  2303. except Exception:
  2304. db.rollback()
  2305. raise
  2306. def replace_step_outputs(
  2307. db: Session, step_id: str, tenant_no: str,
  2308. user_id: Optional[str], items: List[Dict[str, Any]],
  2309. ) -> None:
  2310. now = datetime.now()
  2311. _soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step_id, now)
  2312. for idx, it in enumerate(items):
  2313. db.add(BizProductionTaskStepOutput(
  2314. id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
  2315. output_type_id=(it.get("output_type_id") or "").strip() or None,
  2316. output_name=(it.get("output_name") or "").strip() or None,
  2317. output_barcode=(it.get("output_barcode") or "").strip() or None,
  2318. remark=(it.get("remark") or "").strip() or None,
  2319. order_num=idx, create_by=user_id, create_dept_id="",
  2320. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2321. ))
  2322. db.commit()
  2323. def replace_step_data_collections(
  2324. db: Session, step_id: str, tenant_no: str,
  2325. user_id: Optional[str], items: List[Dict[str, Any]],
  2326. ) -> None:
  2327. now = datetime.now()
  2328. _soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step_id, now)
  2329. for idx, it in enumerate(items):
  2330. db.add(BizProductionTaskStepDataCollection(
  2331. id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
  2332. collection_name=(it.get("collection_name") or "").strip() or None,
  2333. unit=(it.get("unit") or "").strip() or None,
  2334. collection_value=(it.get("collection_value") or "").strip() or None,
  2335. remark=(it.get("remark") or "").strip() or None,
  2336. order_num=idx, create_by=user_id, create_dept_id="",
  2337. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2338. ))
  2339. db.commit()
  2340. def spawn_temporary_work_order_from_task(
  2341. db: Session,
  2342. source_task_id: str,
  2343. *,
  2344. tenant_no: str,
  2345. user_id: Optional[str] = None,
  2346. create_dept_id: str = "",
  2347. remark: str = "",
  2348. ) -> Dict[str, Any]:
  2349. """从生产任务创建临时生产工单,并返回待复制到新任务的原材料载荷(不含产出)。"""
  2350. task = (
  2351. db.query(BizProductionTask)
  2352. .filter(
  2353. and_(
  2354. BizProductionTask.id == source_task_id,
  2355. BizProductionTask.tenant_no == tenant_no,
  2356. BizProductionTask.del_flag == 0,
  2357. )
  2358. )
  2359. .first()
  2360. )
  2361. if not task:
  2362. raise ValueError("任务不存在")
  2363. inputs = (
  2364. db.query(BizProductionTaskInput)
  2365. .filter(
  2366. and_(
  2367. BizProductionTaskInput.task_id == source_task_id,
  2368. BizProductionTaskInput.del_flag == 0,
  2369. )
  2370. )
  2371. .order_by(BizProductionTaskInput.order_num.asc(), BizProductionTaskInput.create_time.asc())
  2372. .all()
  2373. )
  2374. if not inputs:
  2375. raise ValueError("请先在原材料中填写并保存后再派发临时工单")
  2376. copy_items: List[Dict[str, Any]] = []
  2377. for inp in inputs:
  2378. inp_type = (inp.input_type or "").strip()
  2379. inp_id = (inp.input_id or "").strip()
  2380. inp_code = (inp.input_code or "").strip()
  2381. inp_name = (inp.input_name or "").strip()
  2382. if not inp_type or (not inp_id and not inp_code and not inp_name):
  2383. raise ValueError("原材料数据不完整,请保存后再派发")
  2384. copy_items.append({
  2385. "inputType": inp_type,
  2386. "inputId": inp_id or None,
  2387. "inputCode": inp_code or None,
  2388. "inputName": inp_name or None,
  2389. "remark": (inp.remark or "").strip() or None,
  2390. })
  2391. user_remark = (remark or "").strip()
  2392. full_remark = user_remark[:512] if user_remark else ""
  2393. from app.crud import production_work_order as production_work_order_svc
  2394. new_wo = production_work_order_svc.create_temp_production_work_order(
  2395. db,
  2396. tenant_no=tenant_no,
  2397. user_id=user_id,
  2398. create_dept_id=create_dept_id,
  2399. remark=full_remark,
  2400. )
  2401. return {
  2402. "workOrder": production_work_order_svc._temp_wo_to_dict(new_wo),
  2403. "sourceTaskId": task.id,
  2404. "sourceTaskNo": task.task_no or "",
  2405. "inputItems": copy_items,
  2406. }
  2407. def set_task_inputs(
  2408. db: Session, task_id: str, tenant_no: str,
  2409. user_id: Optional[str], items: List[Dict[str, Any]],
  2410. ) -> None:
  2411. now = datetime.now()
  2412. # --- 1. 加载旧 inputs 并按 cell_product 恢复状态 ---
  2413. old_rows = db.query(BizProductionTaskInput).filter(
  2414. and_(
  2415. BizProductionTaskInput.task_id == task_id,
  2416. BizProductionTaskInput.del_flag == 0,
  2417. )
  2418. ).all()
  2419. for old_row in old_rows:
  2420. # 如果是细胞产品,恢复状态
  2421. if old_row.input_type == "cell_product" and old_row.input_id:
  2422. product = db.query(BizCellProduct).filter(
  2423. BizCellProduct.id == old_row.input_id, BizCellProduct.del_flag == 0
  2424. ).first()
  2425. if product and product.product_status == "in_production":
  2426. product.product_status = "frozen"
  2427. product.update_by = user_id
  2428. product.update_time = now
  2429. db.add(product)
  2430. # --- 2. 全量软删除旧行 ---
  2431. _soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
  2432. # --- 3. 按传入顺序批量新建 ---
  2433. for idx, it in enumerate(items):
  2434. inp_type = (it.get("input_type") or it.get("inputType") or "").strip()
  2435. inp_id = (it.get("input_id") or it.get("inputId") or "").strip()
  2436. # 如果是细胞产品,设置状态为 in_production
  2437. if inp_type == "cell_product" and inp_id:
  2438. product = db.query(BizCellProduct).filter(
  2439. BizCellProduct.id == inp_id, BizCellProduct.del_flag == 0
  2440. ).first()
  2441. if product and product.product_status in ("frozen", "thawed"):
  2442. product.product_status = "in_production"
  2443. product.update_by = user_id
  2444. product.update_time = now
  2445. db.add(product)
  2446. db.add(BizProductionTaskInput(
  2447. id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
  2448. input_type=inp_type or None,
  2449. input_id=inp_id or None,
  2450. input_code=(it.get("input_code") or it.get("inputCode") or "").strip() or None,
  2451. input_name=(it.get("input_name") or it.get("inputName") or "").strip() or None,
  2452. remark=(it.get("remark") or "").strip() or None,
  2453. order_num=idx, create_by=user_id, create_dept_id="",
  2454. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2455. ))
  2456. db.flush()
  2457. db.commit()
  2458. def _parse_optional_date(val):
  2459. """解析可选日期字符串(兼容 YYYY-MM-DD 与 ISO 8601)"""
  2460. if not val:
  2461. return None
  2462. if isinstance(val, datetime):
  2463. return val
  2464. return parse_datetime(str(val).strip())
  2465. def _create_occupancy_for_product(db: Session, product: BizCellProduct, tenant_no: str, user_id: Optional[str]):
  2466. """为细胞产品创建库位占用记录"""
  2467. slot_id = (product.storage_slot_id or "").strip()
  2468. if not slot_id:
  2469. return
  2470. occupancy_svc.assert_slot_vacant(
  2471. db, slot_id, tenant_no=tenant_no, exclude_product_id=product.id,
  2472. )
  2473. occupancy_svc._add_stored_occupancy_no_commit(
  2474. db,
  2475. slot_id=slot_id,
  2476. item_type="cell_product",
  2477. item_id=product.id,
  2478. item_code=product.product_code or "",
  2479. item_name=product.product_name or "",
  2480. item_source="production",
  2481. tenant_no=tenant_no,
  2482. user_id=user_id,
  2483. create_dept_id=product.create_dept_id or "",
  2484. )
  2485. def _output_item_has_spec(it: Dict[str, Any]) -> bool:
  2486. return any(k in it for k in ("spec_text", "specText", "spec_name", "specName"))
  2487. def _output_item_spec_text(it: Dict[str, Any]) -> Optional[str]:
  2488. for key in ("spec_text", "specText", "spec_name", "specName"):
  2489. if key not in it:
  2490. continue
  2491. val = it[key]
  2492. if val is None:
  2493. return None
  2494. s = str(val).strip()
  2495. return s or None
  2496. val = it.get("spec_text") or it.get("specText") or it.get("spec_name") or it.get("specName")
  2497. if val is None:
  2498. return None
  2499. s = str(val).strip()
  2500. return s or None
  2501. def _update_product_from_output(
  2502. db: Session,
  2503. product: BizCellProduct,
  2504. it: Dict[str, Any],
  2505. tenant_no: str,
  2506. user_id,
  2507. now,
  2508. ):
  2509. """从 output item 更新细胞产品字段"""
  2510. new_code = it.get("product_code") if it.get("product_code") is not None else it.get("productCode")
  2511. if new_code is not None:
  2512. code = (str(new_code) or "").strip()
  2513. if code and code != (product.product_code or "").strip():
  2514. if _cp_product_code_exists(db, tenant_no, code, exclude_id=product.id):
  2515. raise ValueError(f"产品编码 {code} 已存在")
  2516. product.product_code = code
  2517. field_map = [
  2518. ("product_name", "product_name", "productName"),
  2519. ("product_type_id", "product_type_id", "productTypeId"),
  2520. ("spec_id", "spec_id", "specId"),
  2521. ("spec_text", "spec_text", "specText"),
  2522. ("batch_no", "batch_no", "batchNo"),
  2523. ("storage_slot_id", "storage_slot_id", "storageSlotId"),
  2524. ("unit", "unit", "unit"),
  2525. ("product_images", "product_images", "productImages"),
  2526. ("remark", "remark", "remark"),
  2527. ]
  2528. for db_field, snake_key, camel_key in field_map:
  2529. val = it.get(snake_key) if it.get(snake_key) is not None else it.get(camel_key)
  2530. if val is not None:
  2531. setattr(product, db_field, (str(val) or "").strip() or None)
  2532. spec_text = _output_item_spec_text(it)
  2533. if _output_item_has_spec(it):
  2534. product.spec_text = spec_text
  2535. for num_field, snake_key, camel_key in [
  2536. ("expected_cell_count", "expected_cell_count", "expectedCellCount"),
  2537. ("viability", "viability", "viability"),
  2538. ]:
  2539. val = it.get(snake_key)
  2540. if val is None:
  2541. val = it.get(camel_key)
  2542. if val is not None:
  2543. setattr(product, num_field, val)
  2544. for date_field, snake_key, camel_key in [
  2545. ("freeze_date", "freeze_date", "freezeDate"),
  2546. ("expiry_date", "expiry_date", "expiryDate"),
  2547. ]:
  2548. val = it.get(snake_key) or it.get(camel_key)
  2549. if val is not None:
  2550. setattr(product, date_field, _parse_optional_date(val))
  2551. passage = it.get("passage")
  2552. if passage is not None:
  2553. product.passage = (str(passage) or "").strip() or None
  2554. quantity = it.get("quantity")
  2555. if quantity is not None:
  2556. product.quantity = quantity
  2557. product.update_by = user_id
  2558. product.update_time = now
  2559. def set_task_outputs(
  2560. db: Session, task_id: str, tenant_no: str,
  2561. user_id: Optional[str], items: List[Dict[str, Any]],
  2562. ) -> None:
  2563. now = datetime.now()
  2564. batch_slot_ids: List[str] = []
  2565. for it in items:
  2566. sid = (it.get("storage_slot_id") or it.get("storageSlotId") or "").strip()
  2567. if sid:
  2568. batch_slot_ids.append(sid)
  2569. occupancy_svc.assert_batch_slots_unique(batch_slot_ids)
  2570. # --- 1. 加载旧 outputs ---
  2571. old_rows = db.query(BizProductionTaskOutput).filter(
  2572. and_(
  2573. BizProductionTaskOutput.task_id == task_id,
  2574. BizProductionTaskOutput.del_flag == 0,
  2575. )
  2576. ).all()
  2577. old_cp_map: Dict[str, BizProductionTaskOutput] = {}
  2578. for o in old_rows:
  2579. if o.cell_product_id:
  2580. old_cp_map[o.cell_product_id] = o
  2581. # --- 2. 构建新的 cp_id 集合 ---
  2582. new_cp_ids: set = set()
  2583. for it in items:
  2584. cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
  2585. if cp_id:
  2586. new_cp_ids.add(cp_id)
  2587. # --- 3. 处理删除(旧有新无) ---
  2588. for cp_id, old_row in old_cp_map.items():
  2589. if cp_id not in new_cp_ids:
  2590. product = db.query(BizCellProduct).filter(
  2591. BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
  2592. ).first()
  2593. if product:
  2594. # 作废并软删:避免同编码再入库时留下 voided+frozen 双行,污染各类选择器
  2595. product.product_status = "voided"
  2596. product.del_flag = 1
  2597. product.update_by = user_id
  2598. product.update_time = now
  2599. db.add(product)
  2600. if product.storage_slot_id:
  2601. occupancy_svc.remove_by_item(db, "cell_product", cp_id, user_id=user_id)
  2602. old_row.del_flag = 1
  2603. old_row.update_by = user_id
  2604. old_row.update_time = now
  2605. db.add(old_row)
  2606. # --- 4. 处理新增和修改 ---
  2607. for idx, it in enumerate(items):
  2608. cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
  2609. if cp_id and cp_id in old_cp_map:
  2610. # === 修改 ===
  2611. product = db.query(BizCellProduct).filter(
  2612. BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
  2613. ).first()
  2614. if product:
  2615. old_slot_id = product.storage_slot_id
  2616. _update_product_from_output(db, product, it, tenant_no, user_id, now)
  2617. new_slot_id = product.storage_slot_id
  2618. db.add(product)
  2619. if old_slot_id != new_slot_id:
  2620. if old_slot_id:
  2621. occupancy_svc.remove_by_item(db, "cell_product", cp_id, user_id=user_id)
  2622. if new_slot_id:
  2623. _create_occupancy_for_product(db, product, tenant_no, user_id)
  2624. old_row = old_cp_map[cp_id]
  2625. old_row.output_name = product.product_name if product else ""
  2626. old_row.output_barcode = product.product_code if product else ""
  2627. old_row.storage_slot_id = product.storage_slot_id if product else None
  2628. old_row.quantity = it.get("quantity")
  2629. old_row.unit = (it.get("unit") or "").strip() or None
  2630. old_row.remark = (it.get("remark") or "").strip() or None
  2631. old_row.order_num = idx
  2632. old_row.update_by = user_id
  2633. old_row.update_time = now
  2634. db.add(old_row)
  2635. else:
  2636. # === 新增 ===
  2637. product = BizCellProduct(
  2638. id=generate_uuid(),
  2639. product_code=(it.get("product_code") or it.get("productCode") or "").strip(),
  2640. product_name=(it.get("product_name") or it.get("productName") or "").strip(),
  2641. product_type_id=(it.get("product_type_id") or it.get("productTypeId") or "").strip() or None,
  2642. spec_id=(it.get("spec_id") or it.get("specId") or "").strip() or None,
  2643. spec_text=_output_item_spec_text(it),
  2644. passage=(it.get("passage") or "").strip() or None,
  2645. batch_no=(it.get("batch_no") or it.get("batchNo") or "").strip() or None,
  2646. storage_slot_id=(it.get("storage_slot_id") or it.get("storageSlotId") or "").strip() or None,
  2647. expected_cell_count=it.get("expected_cell_count") if it.get("expected_cell_count") is not None else it.get("expectedCellCount"),
  2648. viability=it.get("viability"),
  2649. freeze_date=_parse_optional_date(it.get("freeze_date") or it.get("freezeDate")),
  2650. product_status=it.get("product_status") or it.get("productStatus") or "frozen",
  2651. source_type="production",
  2652. production_task_id=task_id,
  2653. quantity=it.get("quantity"),
  2654. unit=(it.get("unit") or "").strip() or None,
  2655. expiry_date=_parse_optional_date(it.get("expiry_date") or it.get("expiryDate")),
  2656. product_images=(it.get("product_images") or it.get("productImages") or "").strip() or None,
  2657. tenant_no=tenant_no,
  2658. order_num=idx,
  2659. remark=(it.get("remark") or "").strip() or None,
  2660. create_by=user_id,
  2661. create_dept_id="",
  2662. create_time=now,
  2663. update_by=user_id,
  2664. update_time=now,
  2665. del_flag=0,
  2666. )
  2667. db.add(product)
  2668. db.flush()
  2669. if product.storage_slot_id:
  2670. _create_occupancy_for_product(db, product, tenant_no, user_id)
  2671. output_row = BizProductionTaskOutput(
  2672. id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
  2673. cell_product_id=product.id,
  2674. storage_slot_id=product.storage_slot_id,
  2675. output_name=product.product_name or "",
  2676. output_barcode=product.product_code or "",
  2677. quantity=product.quantity,
  2678. unit=product.unit,
  2679. remark=product.remark,
  2680. attachments=(it.get("attachments") or "").strip() or None,
  2681. order_num=idx, create_by=user_id, create_dept_id="",
  2682. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2683. )
  2684. db.add(output_row)
  2685. db.flush()
  2686. db.commit()
  2687. def reorder_task_steps(db: Session, task_id: str, step_ids: List[str]) -> None:
  2688. now = datetime.now()
  2689. for idx, sid in enumerate(step_ids):
  2690. step = db.query(BizProductionTaskStep).filter(
  2691. and_(BizProductionTaskStep.id == sid, BizProductionTaskStep.task_id == task_id,
  2692. BizProductionTaskStep.del_flag == 0)
  2693. ).first()
  2694. if step:
  2695. step.step_order = idx + 1
  2696. step.update_time = now
  2697. db.add(step)
  2698. db.commit()
  2699. def copy_task_step(db: Session, step_id: str, *, tenant_no: str = "zkxw",
  2700. user_id: Optional[str] = None) -> Dict[str, Any]:
  2701. """复制生产任务步骤,插入到原步骤之后,名称追加(复制)"""
  2702. src = db.query(BizProductionTaskStep).filter(
  2703. and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
  2704. ).first()
  2705. if not src:
  2706. raise ValueError("步骤不存在")
  2707. now = datetime.now()
  2708. new_order = src.step_order + 1
  2709. # 后续步骤 step_order +1
  2710. later_steps = db.query(BizProductionTaskStep).filter(
  2711. and_(
  2712. BizProductionTaskStep.task_id == src.task_id,
  2713. BizProductionTaskStep.del_flag == 0,
  2714. BizProductionTaskStep.step_order > src.step_order,
  2715. )
  2716. ).order_by(BizProductionTaskStep.step_order.asc()).all()
  2717. for ls in later_steps:
  2718. ls.step_order += 1
  2719. ls.update_time = now
  2720. db.add(ls)
  2721. # 创建新步骤
  2722. new_name = (src.process_name or "").strip()
  2723. if not new_name.endswith("(复制)"):
  2724. new_name += "(复制)"
  2725. new_step = BizProductionTaskStep(
  2726. id=generate_uuid(), task_id=src.task_id,
  2727. step_order=new_order,
  2728. process_code=src.process_code, category_id=src.category_id,
  2729. process_name=new_name, description=src.description,
  2730. estimated_duration=src.estimated_duration,
  2731. actual_duration=src.actual_duration, sop_ids=src.sop_ids,
  2732. sop_content=src.sop_content,
  2733. step_status="dzx",
  2734. tenant_no=tenant_no, order_num=0,
  2735. remark=src.remark, create_by=user_id, create_dept_id="",
  2736. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2737. )
  2738. db.add(new_step)
  2739. db.flush()
  2740. # 复制子表:试剂(从生产任务步骤试剂表复制)
  2741. reagent_rows = db.query(BizProductionTaskStepReagent).filter(
  2742. and_(BizProductionTaskStepReagent.step_id == step_id, BizProductionTaskStepReagent.del_flag == 0)
  2743. ).order_by(
  2744. BizProductionTaskStepReagent.order_num.asc(),
  2745. BizProductionTaskStepReagent.create_time.asc(),
  2746. ).all()
  2747. for idx, r in enumerate(reagent_rows):
  2748. db.add(BizProductionTaskStepReagent(
  2749. id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
  2750. reagent_code=r.reagent_code, reagent_name=r.reagent_name,
  2751. spec=r.spec, standard_usage=r.standard_usage,
  2752. actual_usage=r.actual_usage,
  2753. unit=r.unit, is_key_material=r.is_key_material,
  2754. remark=r.remark,
  2755. order_num=_resolve_sub_table_order_num(r.order_num, idx, reagent_rows),
  2756. create_by=user_id, create_dept_id="",
  2757. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2758. ))
  2759. # 复制子表:耗材
  2760. consumable_rows = db.query(BizProductionTaskStepConsumable).filter(
  2761. and_(BizProductionTaskStepConsumable.step_id == step_id, BizProductionTaskStepConsumable.del_flag == 0)
  2762. ).order_by(
  2763. BizProductionTaskStepConsumable.order_num.asc(),
  2764. BizProductionTaskStepConsumable.create_time.asc(),
  2765. ).all()
  2766. for idx, c in enumerate(consumable_rows):
  2767. db.add(BizProductionTaskStepConsumable(
  2768. id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
  2769. consumable_code=c.consumable_code, consumable_name=c.consumable_name,
  2770. spec_model=c.spec_model, standard_usage=c.standard_usage,
  2771. actual_usage=c.actual_usage,
  2772. unit=c.unit, is_required=c.is_required,
  2773. remark=c.remark,
  2774. order_num=_resolve_sub_table_order_num(c.order_num, idx, consumable_rows),
  2775. create_by=user_id, create_dept_id="",
  2776. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2777. ))
  2778. # 复制子表:产出
  2779. output_rows = db.query(BizProductionTaskStepOutput).filter(
  2780. and_(BizProductionTaskStepOutput.step_id == step_id, BizProductionTaskStepOutput.del_flag == 0)
  2781. ).order_by(
  2782. BizProductionTaskStepOutput.order_num.asc(),
  2783. BizProductionTaskStepOutput.create_time.asc(),
  2784. ).all()
  2785. for idx, o in enumerate(output_rows):
  2786. db.add(BizProductionTaskStepOutput(
  2787. id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
  2788. output_type_id=o.output_type_id, output_name=o.output_name,
  2789. output_barcode=o.output_barcode,
  2790. remark=o.remark,
  2791. order_num=_resolve_sub_table_order_num(o.order_num, idx, output_rows),
  2792. create_by=user_id, create_dept_id="",
  2793. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2794. ))
  2795. # 复制子表:数据采集
  2796. collection_rows = db.query(BizProductionTaskStepDataCollection).filter(
  2797. and_(BizProductionTaskStepDataCollection.step_id == step_id, BizProductionTaskStepDataCollection.del_flag == 0)
  2798. ).order_by(
  2799. BizProductionTaskStepDataCollection.order_num.asc(),
  2800. BizProductionTaskStepDataCollection.create_time.asc(),
  2801. ).all()
  2802. for idx, d in enumerate(collection_rows):
  2803. db.add(BizProductionTaskStepDataCollection(
  2804. id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
  2805. collection_name=d.collection_name, unit=d.unit,
  2806. collection_value=d.collection_value,
  2807. remark=d.remark,
  2808. order_num=_resolve_sub_table_order_num(d.order_num, idx, collection_rows),
  2809. create_by=user_id, create_dept_id="",
  2810. create_time=now, update_by=user_id, update_time=now, del_flag=0,
  2811. ))
  2812. db.commit()
  2813. result = _step_to_dict(new_step)
  2814. result.update(_step_sub_tables_to_dict(db, new_step.id))
  2815. return result
  2816. def delete_production_task(db: Session, task_id: str, tenant_no: str, user_id: Optional[str]) -> None:
  2817. """软删除生产任务及其所有子表数据(步骤 + 投入 + 产出)"""
  2818. task = db.query(BizProductionTask).filter(
  2819. and_(
  2820. BizProductionTask.id == task_id,
  2821. BizProductionTask.tenant_no == tenant_no,
  2822. BizProductionTask.del_flag == 0,
  2823. )
  2824. ).first()
  2825. if not task:
  2826. raise ValueError("任务不存在")
  2827. now = datetime.now()
  2828. # 1. 软删除所有步骤及其子表
  2829. steps = db.query(BizProductionTaskStep).filter(
  2830. and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0)
  2831. ).all()
  2832. for step in steps:
  2833. step.del_flag = 1
  2834. step.update_by = user_id
  2835. step.update_time = now
  2836. db.add(step)
  2837. _soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step.id, now)
  2838. _soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step.id, now)
  2839. _soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step.id, now)
  2840. _soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step.id, now)
  2841. # 2. 软删除投入
  2842. _soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
  2843. # 3. 软删除产出
  2844. _soft_delete_sub(db, BizProductionTaskOutput, "task_id", task_id, now)
  2845. # 4. 软删除任务本体
  2846. task.del_flag = 1
  2847. task.update_by = user_id
  2848. task.update_time = now
  2849. db.add(task)
  2850. work_order_id = task.work_order_id or ""
  2851. from app.crud import production_work_order as production_work_order_svc
  2852. production_work_order_svc.sync_production_work_order_status_from_tasks(
  2853. db, work_order_id, tenant_no, user_id
  2854. )
  2855. db.commit()
  2856. def total_pages(total: int, page_size: int) -> int:
  2857. if total <= 0:
  2858. return 0
  2859. return int(math.ceil(total / float(page_size)))
  2860. _PRODUCTION_TAB_STEP_COUNT_KEYS = (
  2861. "inbound_cell_storage",
  2862. "inbound_research_donation",
  2863. "outbound_pilot_service",
  2864. "outbound_basic_experiment",
  2865. "temporary",
  2866. )
  2867. def _empty_production_tab_step_counts() -> Dict[str, Dict[str, int]]:
  2868. return {
  2869. k: {"overdueIncomplete": 0, "futureIncomplete": 0}
  2870. for k in _PRODUCTION_TAB_STEP_COUNT_KEYS
  2871. }
  2872. def _resolve_production_tab_key(
  2873. wo_type: Optional[str],
  2874. reception_source: Optional[str],
  2875. biz_type: Optional[str],
  2876. ) -> Optional[str]:
  2877. wt = (wo_type or "inbound").strip() or "inbound"
  2878. if wt == "temporary":
  2879. return "temporary"
  2880. if wt == "outbound":
  2881. bt = (biz_type or "pilot_service").strip() or "pilot_service"
  2882. if bt == "basic_experiment":
  2883. return "outbound_basic_experiment"
  2884. if bt == "clinical_application":
  2885. return None
  2886. return "outbound_pilot_service"
  2887. rs = (reception_source or "cell_storage").strip() or "cell_storage"
  2888. return "inbound_research_donation" if rs == "research_donation" else "inbound_cell_storage"
  2889. def _aggregate_production_tab_step_field(
  2890. counts: Dict[str, Dict[str, int]],
  2891. rows,
  2892. field: str,
  2893. ) -> None:
  2894. for wo_type, rs, bt, cnt in rows:
  2895. tab_key = _resolve_production_tab_key(wo_type, rs, bt)
  2896. if not tab_key or tab_key not in counts:
  2897. continue
  2898. counts[tab_key][field] += int(cnt or 0)
  2899. def _base_active_step_count_query(
  2900. db: Session,
  2901. *,
  2902. tenant_no: str,
  2903. user_info=None,
  2904. mine_user_id: Optional[str] = None,
  2905. ):
  2906. q = (
  2907. db.query(
  2908. BizProductionWorkOrder.work_order_type,
  2909. BizProductionWorkOrder.reception_source,
  2910. BizProductionWorkOrder.biz_type,
  2911. func.count(BizProductionTaskStep.id),
  2912. )
  2913. .select_from(BizProductionTaskStep)
  2914. .join(
  2915. BizProductionTask,
  2916. and_(
  2917. BizProductionTask.id == BizProductionTaskStep.task_id,
  2918. BizProductionTask.del_flag == 0,
  2919. BizProductionTask.tenant_no == tenant_no,
  2920. ),
  2921. )
  2922. .join(
  2923. BizProductionWorkOrder,
  2924. and_(
  2925. BizProductionWorkOrder.id == BizProductionTask.work_order_id,
  2926. BizProductionWorkOrder.del_flag == 0,
  2927. BizProductionWorkOrder.tenant_no == tenant_no,
  2928. ),
  2929. )
  2930. .filter(
  2931. BizProductionTaskStep.del_flag == 0,
  2932. BizProductionTaskStep.tenant_no == tenant_no,
  2933. BizProductionWorkOrder.work_order_status.in_(("dpz", "jxz")),
  2934. BizProductionTask.task_status.in_(("dpz", "jxz")),
  2935. BizProductionTaskStep.step_status != "ywc",
  2936. )
  2937. )
  2938. if user_info:
  2939. from app.core.data_scope import apply_data_scope
  2940. q = apply_data_scope(q, BizProductionWorkOrder, user_info, db, tenant_no)
  2941. uid = (mine_user_id or "").strip()
  2942. if uid:
  2943. step_task_ids = (
  2944. db.query(BizProductionTaskStep.task_id)
  2945. .join(
  2946. BizProductionTaskStepAssignee,
  2947. BizProductionTaskStepAssignee.step_id == BizProductionTaskStep.id,
  2948. )
  2949. .filter(
  2950. BizProductionTaskStepAssignee.user_id == uid,
  2951. BizProductionTaskStepAssignee.tenant_no == tenant_no,
  2952. BizProductionTaskStepAssignee.del_flag == 0,
  2953. BizProductionTaskStep.del_flag == 0,
  2954. )
  2955. .distinct()
  2956. .subquery()
  2957. )
  2958. q = q.filter(
  2959. or_(
  2960. BizProductionTask.id.in_(step_task_ids),
  2961. BizProductionTask.current_assignee_user_id == uid,
  2962. )
  2963. )
  2964. from app.crud.production_work_order import apply_exclude_inactive_protocol_filter
  2965. q = apply_exclude_inactive_protocol_filter(q, exclude=True)
  2966. return q
  2967. def count_step_tab_badges_by_tab(
  2968. db: Session,
  2969. *,
  2970. tenant_no: str,
  2971. user_info=None,
  2972. mine_user_id: Optional[str] = None,
  2973. ) -> Dict[str, Dict[str, int]]:
  2974. """按 Tab 统计工艺步骤角标:计划开始已到期未完成数 + 计划开始在今日之后未完成数。"""
  2975. counts = _empty_production_tab_step_counts()
  2976. today_end = datetime.now().replace(hour=23, minute=59, second=59, microsecond=0)
  2977. overdue_rows = (
  2978. _base_active_step_count_query(
  2979. db, tenant_no=tenant_no, user_info=user_info, mine_user_id=mine_user_id
  2980. )
  2981. .filter(
  2982. BizProductionTaskStep.planned_start_time.isnot(None),
  2983. BizProductionTaskStep.planned_start_time <= today_end,
  2984. )
  2985. .group_by(
  2986. BizProductionWorkOrder.work_order_type,
  2987. BizProductionWorkOrder.reception_source,
  2988. BizProductionWorkOrder.biz_type,
  2989. )
  2990. .all()
  2991. )
  2992. _aggregate_production_tab_step_field(counts, overdue_rows, "overdueIncomplete")
  2993. future_rows = (
  2994. _base_active_step_count_query(
  2995. db, tenant_no=tenant_no, user_info=user_info, mine_user_id=mine_user_id
  2996. )
  2997. .filter(
  2998. BizProductionTaskStep.planned_start_time.isnot(None),
  2999. BizProductionTaskStep.planned_start_time > today_end,
  3000. )
  3001. .group_by(
  3002. BizProductionWorkOrder.work_order_type,
  3003. BizProductionWorkOrder.reception_source,
  3004. BizProductionWorkOrder.biz_type,
  3005. )
  3006. .all()
  3007. )
  3008. _aggregate_production_tab_step_field(counts, future_rows, "futureIncomplete")
  3009. return counts
  3010. def count_production_work_order_tab_step_badges(
  3011. db: Session, *, tenant_no: str, user_info=None
  3012. ) -> Dict[str, Dict[str, int]]:
  3013. return count_step_tab_badges_by_tab(db, tenant_no=tenant_no, user_info=user_info)
  3014. def count_mine_production_work_order_tab_step_badges(
  3015. db: Session, *, tenant_no: str, assignee_user_id: str
  3016. ) -> Dict[str, Dict[str, int]]:
  3017. return count_step_tab_badges_by_tab(
  3018. db, tenant_no=tenant_no, mine_user_id=assignee_user_id
  3019. )