production_task.py 125 KB

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