| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286 |
- """生产任务 CRUD(biz_production_task + 步骤 + 子表 + 投入/产出)"""
- from __future__ import annotations
- import math
- from datetime import datetime
- from typing import Any, Dict, List, Optional, Tuple
- from sqlalchemy import and_, func, or_
- from sqlalchemy.orm import Session
- from app.core.utils import format_date, generate_uuid, parse_datetime
- from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot
- from app.models.models import (
- BizCellProduct,
- BizCollectionProtocol,
- BizProcess,
- BizProcessConsumable,
- BizProcessDataCollection,
- BizProcessOutput,
- BizProcessReagent,
- BizProcessRoute,
- BizProcessRouteStep,
- BizProcessRouteStepConsumable,
- BizProcessRouteStepDataCollection,
- BizProcessRouteStepOutput,
- BizProcessRouteStepReagent,
- BizProductionTask,
- BizProductionTaskHandover,
- BizProductionTaskInput,
- BizProductionTaskOutput,
- BizProductionTaskStep,
- BizProductionTaskStepAssignee,
- BizProductionTaskStepConsumable,
- BizProductionTaskStepConfiguredReagent,
- BizProductionTaskStepDataCollection,
- BizProductionTaskStepOutput,
- BizProductionTaskStepReagent,
- BizProductionWorkOrder,
- BizProtocolSample,
- BizSampleReception,
- BizStorageSlotOccupancy,
- SysUser,
- )
- from app.crud import storage_occupancy as occupancy_svc
- from app.crud.cell_product import (
- _row_to_dict as _cp_row_to_dict,
- _enrich_product_dicts as _cp_enrich,
- product_code_exists as _cp_product_code_exists,
- )
- from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
- def _require_requisition_for_catalog_material(
- db: Session,
- tenant_no: str,
- material_code: Optional[str],
- material_type: str,
- actual_usage: Optional[str],
- requisition_usage_id: Optional[str],
- ) -> None:
- code = (material_code or "").strip()
- actual = (actual_usage or "").strip()
- req_id = (requisition_usage_id or "").strip()
- if not code or not actual or req_id:
- return
- from app.models.production_material import BizProductionMaterial
- catalog_row = db.query(BizProductionMaterial).filter(
- and_(
- BizProductionMaterial.material_code == code,
- BizProductionMaterial.material_type == material_type,
- BizProductionMaterial.tenant_no == tenant_no,
- BizProductionMaterial.del_flag == 0,
- )
- ).first()
- if catalog_row:
- raise ValueError(f"物料「{code}」需选择领用记录")
- _PROTOCOL_STATUS_LABELS = {
- "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zky": "转科研"
- }
- _DONATION_STATUS_LABELS = {
- "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "sdfq": "属地废弃", "zckc": "转细胞存储"
- }
- _RECEPTION_SOURCE_LABELS = {
- "cell_storage": "细胞存储",
- "research_donation": "科研捐赠",
- }
- _OUTBOUND_BIZ_TYPE_LABELS = {
- "pilot_service": "中试服务",
- "clinical_application": "临床应用",
- }
- _APPLICATION_STATUS_LABELS = {
- "dks": "待开始",
- "jxz": "进行中",
- "ywc": "已完成",
- "yzf": "已作废",
- }
- _PRODUCTION_CONCLUSION_LABELS = {
- "cg": "成功",
- "sb": "失败",
- }
- def _production_conclusion_label(v: Optional[str]) -> str:
- code = (v or "").strip()
- if not code:
- return ""
- return _PRODUCTION_CONCLUSION_LABELS.get(code, code)
- def _fmt_dt(t: Optional[datetime]) -> str:
- return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
- def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
- val = (value or "").strip()
- if len(val) == 10:
- return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
- return val
- def _enrich_outbound_task_items(
- db: Session,
- tenant_no: str,
- items: List[Dict[str, Any]],
- wo_ctx_map: Dict[str, Dict[str, Any]],
- ) -> None:
- """出库生产任务:来源类型=业务类型,协议号=申请单号,协议状态=申请单状态。"""
- from app.crud.pilot_infusion_schedule_mgmt import (
- _get_plan_schedule_course_meta,
- _resolve_course_display,
- )
- from app.models.models import (
- BizPilotApplication,
- BizPilotInfusionPlan,
- BizPilotInfusionSchedule,
- BizPilotPatient,
- )
- schedule_ids = [
- ctx.get("infusionScheduleId")
- for ctx in wo_ctx_map.values()
- if ctx.get("workOrderType") == "outbound" and ctx.get("infusionScheduleId")
- ]
- if not schedule_ids:
- return
- schedules = {
- s.id: s
- for s in db.query(BizPilotInfusionSchedule)
- .filter(
- BizPilotInfusionSchedule.id.in_(schedule_ids),
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .all()
- }
- plan_ids = list({s.infusion_plan_id for s in schedules.values() if s.infusion_plan_id})
- plan_map = {}
- if plan_ids:
- plan_map = {
- p.id: p
- for p in db.query(BizPilotInfusionPlan)
- .filter(
- BizPilotInfusionPlan.id.in_(plan_ids),
- BizPilotInfusionPlan.tenant_no == tenant_no,
- BizPilotInfusionPlan.del_flag == 0,
- )
- .all()
- }
- app_ids = list({p.pilot_application_id for p in plan_map.values() if p.pilot_application_id})
- app_map = {}
- patient_map: Dict[str, BizPilotPatient] = {}
- if app_ids:
- app_map = {
- a.id: a
- for a in db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id.in_(app_ids),
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.del_flag == 0,
- )
- .all()
- }
- patients = (
- db.query(BizPilotPatient)
- .filter(
- BizPilotPatient.pilot_application_id.in_(app_ids),
- BizPilotPatient.tenant_no == tenant_no,
- BizPilotPatient.del_flag == 0,
- )
- .all()
- )
- patient_map = {p.pilot_application_id: p for p in patients}
- plan_course_meta = _get_plan_schedule_course_meta(db, plan_ids, tenant_no)
- schedule_app_map: Dict[str, BizPilotApplication] = {}
- for sid, sched in schedules.items():
- plan = plan_map.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
- app = app_map.get(plan.pilot_application_id) if plan and plan.pilot_application_id else None
- if app:
- schedule_app_map[sid] = app
- for item in items:
- ctx = wo_ctx_map.get(item.get("workOrderId") or "")
- if not ctx or ctx.get("workOrderType") != "outbound":
- continue
- biz_type = ctx.get("bizType") or ""
- biz_label = _OUTBOUND_BIZ_TYPE_LABELS.get(biz_type, biz_type)
- item["bizType"] = biz_type
- item["bizTypeLabel"] = biz_label
- item["receptionSource"] = biz_type
- item["receptionSourceLabel"] = biz_label
- sched_id = ctx.get("infusionScheduleId") or ""
- sched = schedules.get(sched_id)
- app = schedule_app_map.get(sched_id)
- if app:
- app_status = app.application_status or ""
- item["protocolNo"] = app.application_no or ""
- item["protocolStatus"] = app_status
- item["protocolStatusLabel"] = _APPLICATION_STATUS_LABELS.get(app_status, app_status)
- item["applicationNo"] = app.application_no or ""
- item["applicationName"] = app.application_name or ""
- item["channelName"] = app.channel_name or ""
- item["projectTitle"] = app.project_title or ""
- item["customerName"] = app.customer_name or ""
- else:
- item["protocolNo"] = ""
- item["protocolStatus"] = ""
- item["protocolStatusLabel"] = ""
- item["applicationNo"] = ""
- item["applicationName"] = ""
- item["channelName"] = ""
- item["projectTitle"] = ""
- item["customerName"] = ""
- if sched:
- plan = plan_map.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
- patient = (
- patient_map.get(plan.pilot_application_id)
- if plan and plan.pilot_application_id
- else None
- )
- plan_meta = (
- plan_course_meta.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
- )
- course = _resolve_course_display(sched, plan, plan_meta)
- item["patientName"] = patient.patient_name if patient else ""
- item["sortNo"] = course.get("sortNo")
- item["totalCourseCount"] = course.get("totalCourseCount")
- item["infusionTime"] = _fmt_dt(sched.infusion_time) if sched.infusion_time else ""
- def _generate_task_no(db: Session, tenant_no: str) -> str:
- day = datetime.now().strftime("%Y%m%d")
- like = f"RW{day}%"
- cnt = (
- db.query(func.count(BizProductionTask.id))
- .filter(
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.task_no.like(like),
- )
- .scalar()
- )
- return f"RW{day}{(cnt or 0) + 1:04d}"
- def _task_to_dict(t: BizProductionTask, *, work_order_no: str = "") -> Dict[str, Any]:
- return {
- "id": t.id,
- "taskNo": t.task_no,
- "workOrderId": t.work_order_id,
- "orderSource": getattr(t, "order_source", "inbound") or "inbound",
- "workOrderNo": work_order_no,
- "taskType": t.task_type or "cell_storage",
- "taskName": t.task_name or "",
- "description": t.description or "",
- "routeId": t.route_id or "",
- "taskStatus": t.task_status,
- "currentAssigneeUserId": t.current_assignee_user_id or "",
- "currentAssigneeUserName": "",
- "startedTime": _fmt_dt(t.started_time),
- "completedTime": _fmt_dt(t.completed_time),
- "conclusion": (getattr(t, "conclusion", None) or "").strip(),
- "conclusionLabel": _production_conclusion_label(getattr(t, "conclusion", None)),
- "remark": t.remark or "",
- "attachments": t.attachments or "",
- "equipmentIds": parse_equipment_ids(getattr(t, "equipment_ids", None)),
- "equipmentName": getattr(t, "equipment_name", None) or "",
- "tenantNo": t.tenant_no,
- "orderNum": t.order_num if t.order_num is not None else 0,
- "createTime": _fmt_dt(t.create_time),
- "updateTime": _fmt_dt(t.update_time),
- }
- def _step_to_dict(s: BizProductionTaskStep) -> Dict[str, Any]:
- return {
- "id": s.id,
- "taskId": s.task_id,
- "stepOrder": s.step_order if s.step_order is not None else 0,
- "processCode": s.process_code or "",
- "categoryId": s.category_id or "",
- "processName": s.process_name or "",
- "description": s.description or "",
- "estimatedDuration": s.estimated_duration or "",
- "actualDuration": s.actual_duration or "",
- "sopIds": s.sop_ids or "",
- "sopContent": s.sop_content or "",
- "stepStatus": s.step_status or "dzx",
- "plannedStartTime": format_date(getattr(s, "planned_start_time", None)) or "",
- "startedTime": _fmt_dt(s.started_time),
- "completedTime": _fmt_dt(s.completed_time),
- "completedBy": getattr(s, "completed_by", None) or "",
- "completedByName": "",
- "assigneeUserIds": [],
- "assigneeNames": [],
- "assignees": [],
- "remark": s.remark or "",
- }
- def _step_reagent_to_dict(r: BizProductionTaskStepReagent) -> Dict[str, Any]:
- return {
- "id": r.id,
- "stepId": r.step_id,
- "reagentCode": r.reagent_code or "",
- "reagentName": r.reagent_name or "",
- "spec": r.spec or "",
- "standardUsage": r.standard_usage or "",
- "actualUsage": r.actual_usage or "",
- "unit": r.unit or "",
- "requisitionUsageId": (r.requisition_usage_id or "").strip(),
- "isKeyMaterial": r.is_key_material if r.is_key_material is not None else "0",
- "orderNum": r.order_num if r.order_num is not None else 0,
- "remark": r.remark or "",
- }
- def _step_configured_reagent_to_dict(r: BizProductionTaskStepConfiguredReagent) -> Dict[str, Any]:
- return {
- "id": r.id,
- "stepId": r.step_id,
- "configuredReagentCode": r.configured_reagent_code or "",
- "configuredReagentName": r.configured_reagent_name or "",
- "spec": r.spec or "",
- "standardUsage": r.standard_usage or "",
- "actualUsage": r.actual_usage or "",
- "unit": r.unit or "",
- "requisitionUsageId": (r.requisition_usage_id or "").strip(),
- "orderNum": r.order_num if r.order_num is not None else 0,
- "remark": r.remark or "",
- }
- def _step_consumable_to_dict(c: BizProductionTaskStepConsumable) -> Dict[str, Any]:
- return {
- "id": c.id,
- "stepId": c.step_id,
- "consumableCode": c.consumable_code or "",
- "consumableName": c.consumable_name or "",
- "specModel": c.spec_model or "",
- "standardUsage": c.standard_usage or "",
- "actualUsage": c.actual_usage or "",
- "unit": c.unit or "",
- "requisitionUsageId": (c.requisition_usage_id or "").strip(),
- "isRequired": c.is_required if c.is_required is not None else "1",
- "orderNum": c.order_num if c.order_num is not None else 0,
- "remark": c.remark or "",
- }
- def _step_output_to_dict(o: BizProductionTaskStepOutput) -> Dict[str, Any]:
- return {
- "id": o.id,
- "stepId": o.step_id,
- "outputTypeId": o.output_type_id or "",
- "outputName": o.output_name or "",
- "outputBarcode": o.output_barcode or "",
- "orderNum": o.order_num if o.order_num is not None else 0,
- "remark": o.remark or "",
- }
- def _step_data_collection_to_dict(d: BizProductionTaskStepDataCollection) -> Dict[str, Any]:
- return {
- "id": d.id,
- "stepId": d.step_id,
- "collectionName": d.collection_name or "",
- "unit": d.unit or "",
- "collectionValue": d.collection_value or "",
- "orderNum": d.order_num if d.order_num is not None else 0,
- "remark": d.remark or "",
- }
- def _task_input_to_dict(i: BizProductionTaskInput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
- result = {
- "id": i.id,
- "taskId": i.task_id,
- "inputType": i.input_type or "",
- "inputId": i.input_id or "",
- "inputCode": i.input_code or "",
- "inputName": i.input_name or "",
- "remark": i.remark or "",
- }
- if product_dict:
- for key in [
- "productCode", "productName", "productTypeId", "productTypeName",
- "specId", "specName", "specText", "passage", "batchNo", "storageSlotName",
- "expectedCellCount", "viability", "freezeDate", "expiryDate",
- "productStatus", "productImages", "storageSlotId",
- ]:
- if key in product_dict:
- result[key] = product_dict[key]
- return result
- def _task_output_to_dict(o: BizProductionTaskOutput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
- result = {
- "id": o.id,
- "taskId": o.task_id,
- "cellProductId": o.cell_product_id or "",
- "storageSlotId": o.storage_slot_id or "",
- "outputName": o.output_name or "",
- "outputBarcode": o.output_barcode or "",
- "quantity": float(o.quantity) if o.quantity is not None else None,
- "unit": o.unit or "",
- "remark": o.remark or "",
- "attachments": o.attachments or "",
- }
- if product_dict:
- for key in [
- "productCode", "productName", "productTypeId", "productTypeName",
- "specId", "specName", "specText", "passage", "batchNo", "storageSlotName",
- "expectedCellCount", "viability", "freezeDate", "expiryDate",
- "productStatus", "productImages",
- ]:
- if key in product_dict:
- result[key] = product_dict[key]
- return result
- def _soft_delete_sub(db: Session, model_class, parent_field: str, parent_id: str, now: datetime) -> None:
- rows = db.query(model_class).filter(
- and_(
- getattr(model_class, parent_field) == parent_id,
- model_class.del_flag == 0,
- )
- ).all()
- for r in rows:
- r.del_flag = 1
- r.update_time = now
- db.add(r)
- def _resolve_sub_table_order_num(order_num: Optional[int], idx: int, rows: list) -> int:
- """复制/克隆子表排序号:保留源 order_num;若源记录均为 0 则按遍历顺序写入 idx。"""
- if rows and len(rows) > 1 and all((getattr(r, "order_num", None) or 0) == 0 for r in rows):
- return idx
- if order_num is not None:
- return order_num
- return idx
- def _empty_step_sub_tables() -> Dict[str, Any]:
- return {
- "reagents": [],
- "consumables": [],
- "configuredReagents": [],
- "outputs": [],
- "dataCollections": [],
- }
- def _batch_load_step_sub_tables(db: Session, step_ids: List[str]) -> Dict[str, Dict[str, Any]]:
- """批量加载步骤子表,避免 get_production_task_detail 逐步骤 N+1 查询。"""
- ids = [sid for sid in step_ids if (sid or "").strip()]
- if not ids:
- return {}
- result: Dict[str, Dict[str, Any]] = {sid: _empty_step_sub_tables() for sid in ids}
- reagents = (
- db.query(BizProductionTaskStepReagent)
- .filter(
- and_(
- BizProductionTaskStepReagent.step_id.in_(ids),
- BizProductionTaskStepReagent.del_flag == 0,
- )
- )
- .order_by(
- BizProductionTaskStepReagent.step_id.asc(),
- BizProductionTaskStepReagent.order_num.asc(),
- BizProductionTaskStepReagent.create_time.asc(),
- )
- .all()
- )
- for row in reagents:
- result[row.step_id]["reagents"].append(_step_reagent_to_dict(row))
- consumables = (
- db.query(BizProductionTaskStepConsumable)
- .filter(
- and_(
- BizProductionTaskStepConsumable.step_id.in_(ids),
- BizProductionTaskStepConsumable.del_flag == 0,
- )
- )
- .order_by(
- BizProductionTaskStepConsumable.step_id.asc(),
- BizProductionTaskStepConsumable.order_num.asc(),
- BizProductionTaskStepConsumable.create_time.asc(),
- )
- .all()
- )
- for row in consumables:
- result[row.step_id]["consumables"].append(_step_consumable_to_dict(row))
- outputs = (
- db.query(BizProductionTaskStepOutput)
- .filter(
- and_(
- BizProductionTaskStepOutput.step_id.in_(ids),
- BizProductionTaskStepOutput.del_flag == 0,
- )
- )
- .order_by(
- BizProductionTaskStepOutput.step_id.asc(),
- BizProductionTaskStepOutput.order_num.asc(),
- BizProductionTaskStepOutput.create_time.asc(),
- )
- .all()
- )
- for row in outputs:
- result[row.step_id]["outputs"].append(_step_output_to_dict(row))
- data_collections = (
- db.query(BizProductionTaskStepDataCollection)
- .filter(
- and_(
- BizProductionTaskStepDataCollection.step_id.in_(ids),
- BizProductionTaskStepDataCollection.del_flag == 0,
- )
- )
- .order_by(
- BizProductionTaskStepDataCollection.step_id.asc(),
- BizProductionTaskStepDataCollection.order_num.asc(),
- BizProductionTaskStepDataCollection.create_time.asc(),
- )
- .all()
- )
- for row in data_collections:
- result[row.step_id]["dataCollections"].append(_step_data_collection_to_dict(row))
- configured_reagents = (
- db.query(BizProductionTaskStepConfiguredReagent)
- .filter(
- and_(
- BizProductionTaskStepConfiguredReagent.step_id.in_(ids),
- BizProductionTaskStepConfiguredReagent.del_flag == 0,
- )
- )
- .order_by(
- BizProductionTaskStepConfiguredReagent.step_id.asc(),
- BizProductionTaskStepConfiguredReagent.order_num.asc(),
- BizProductionTaskStepConfiguredReagent.create_time.asc(),
- )
- .all()
- )
- for row in configured_reagents:
- result[row.step_id]["configuredReagents"].append(_step_configured_reagent_to_dict(row))
- return result
- def _step_sub_tables_to_dict(db: Session, step_id: str) -> Dict[str, Any]:
- """加载步骤四张子表并序列化(与 get_production_task_detail 一致)。"""
- return _batch_load_step_sub_tables(db, [step_id]).get(step_id) or _empty_step_sub_tables()
- def _clone_step_reagents(
- db: Session, src_step_id: str, new_step_id: str,
- tenant_no: str, user_id: Optional[str], now: datetime,
- ) -> None:
- rows = db.query(BizProcessRouteStepReagent).filter(
- and_(BizProcessRouteStepReagent.step_id == src_step_id, BizProcessRouteStepReagent.del_flag == 0)
- ).order_by(
- BizProcessRouteStepReagent.order_num.asc(),
- BizProcessRouteStepReagent.create_time.asc(),
- ).all()
- for idx, r in enumerate(rows):
- db.add(BizProductionTaskStepReagent(
- id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
- reagent_code=r.reagent_code, reagent_name=r.reagent_name,
- spec=r.spec, standard_usage=r.standard_usage,
- unit=r.unit, is_key_material=r.is_key_material,
- remark=r.remark,
- order_num=_resolve_sub_table_order_num(r.order_num, idx, rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- def _clone_step_consumables(
- db: Session, src_step_id: str, new_step_id: str,
- tenant_no: str, user_id: Optional[str], now: datetime,
- ) -> None:
- rows = db.query(BizProcessRouteStepConsumable).filter(
- and_(BizProcessRouteStepConsumable.step_id == src_step_id, BizProcessRouteStepConsumable.del_flag == 0)
- ).order_by(
- BizProcessRouteStepConsumable.order_num.asc(),
- BizProcessRouteStepConsumable.create_time.asc(),
- ).all()
- for idx, c in enumerate(rows):
- db.add(BizProductionTaskStepConsumable(
- id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
- consumable_code=c.consumable_code, consumable_name=c.consumable_name,
- spec_model=c.spec_model, standard_usage=c.standard_usage,
- unit=c.unit, is_required=c.is_required,
- remark=c.remark,
- order_num=_resolve_sub_table_order_num(c.order_num, idx, rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- def _clone_step_outputs(
- db: Session, src_step_id: str, new_step_id: str,
- tenant_no: str, user_id: Optional[str], now: datetime,
- ) -> None:
- rows = db.query(BizProcessRouteStepOutput).filter(
- and_(BizProcessRouteStepOutput.step_id == src_step_id, BizProcessRouteStepOutput.del_flag == 0)
- ).order_by(
- BizProcessRouteStepOutput.order_num.asc(),
- BizProcessRouteStepOutput.create_time.asc(),
- ).all()
- for idx, o in enumerate(rows):
- db.add(BizProductionTaskStepOutput(
- id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
- output_type_id=o.output_type_id, output_name=o.output_name,
- output_barcode=o.output_barcode,
- remark=o.remark,
- order_num=_resolve_sub_table_order_num(o.order_num, idx, rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- def _clone_step_data_collections(
- db: Session, src_step_id: str, new_step_id: str,
- tenant_no: str, user_id: Optional[str], now: datetime,
- ) -> None:
- rows = db.query(BizProcessRouteStepDataCollection).filter(
- and_(BizProcessRouteStepDataCollection.step_id == src_step_id, BizProcessRouteStepDataCollection.del_flag == 0)
- ).order_by(
- BizProcessRouteStepDataCollection.order_num.asc(),
- BizProcessRouteStepDataCollection.create_time.asc(),
- ).all()
- for idx, d in enumerate(rows):
- db.add(BizProductionTaskStepDataCollection(
- id=generate_uuid(), step_id=new_step_id, tenant_no=tenant_no,
- collection_name=d.collection_name, unit=d.unit,
- remark=d.remark,
- order_num=_resolve_sub_table_order_num(d.order_num, idx, rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- def _clone_route_to_task(
- db: Session, task_id: str, route_id: str,
- tenant_no: str, user_id: Optional[str], create_dept_id: str,
- ) -> None:
- route_steps = (
- db.query(BizProcessRouteStep)
- .filter(and_(BizProcessRouteStep.route_id == route_id, BizProcessRouteStep.del_flag == 0))
- .order_by(BizProcessRouteStep.step_order.asc(), BizProcessRouteStep.create_time.asc())
- .all()
- )
- now = datetime.now()
- for rs in route_steps:
- new_step = BizProductionTaskStep(
- id=generate_uuid(), task_id=task_id,
- step_order=rs.step_order,
- process_code=rs.process_code, category_id=rs.category_id,
- process_name=rs.process_name, description=rs.description,
- estimated_duration=rs.estimated_duration,
- sop_ids=rs.sop_ids,
- sop_content=rs.sop_content,
- step_status="dzx",
- tenant_no=tenant_no, order_num=0,
- remark=None, create_by=user_id, create_dept_id=create_dept_id or "",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- )
- db.add(new_step)
- db.flush()
- _clone_step_reagents(db, rs.id, new_step.id, tenant_no, user_id, now)
- _clone_step_consumables(db, rs.id, new_step.id, tenant_no, user_id, now)
- _clone_step_outputs(db, rs.id, new_step.id, tenant_no, user_id, now)
- _clone_step_data_collections(db, rs.id, new_step.id, tenant_no, user_id, now)
- def _validate_assignee_user_id(db: Session, user_id: str, tenant_no: str) -> str:
- uid = (user_id or "").strip()
- if not uid:
- raise ValueError("请选择负责人")
- row = (
- db.query(SysUser.id)
- .filter(
- SysUser.id == uid,
- SysUser.tenant_no == tenant_no,
- SysUser.del_flag == 0,
- SysUser.status == "1",
- )
- .first()
- )
- if not row:
- raise ValueError("负责人用户无效或已停用")
- return uid
- def _enrich_assignee_names(db: Session, items: List[Dict[str, Any]]) -> None:
- user_ids = list({(x.get("currentAssigneeUserId") or "").strip() for x in items if (x.get("currentAssigneeUserId") or "").strip()})
- if not user_ids:
- return
- rows = (
- db.query(SysUser.id, SysUser.nick_name)
- .filter(SysUser.id.in_(user_ids), SysUser.del_flag == 0)
- .all()
- )
- name_map = {r.id: (r.nick_name or "").strip() for r in rows}
- for item in items:
- uid = (item.get("currentAssigneeUserId") or "").strip()
- if uid:
- item["currentAssigneeUserName"] = name_map.get(uid, "")
- def _enrich_handover_times(
- db: Session,
- items: List[Dict[str, Any]],
- *,
- from_user_id: str,
- tenant_no: str,
- ) -> None:
- task_ids = [x.get("id") for x in items if x.get("id")]
- if not task_ids:
- return
- rows = (
- db.query(
- BizProductionTaskHandover.task_id,
- func.max(BizProductionTaskHandover.handover_time),
- )
- .filter(
- BizProductionTaskHandover.task_id.in_(task_ids),
- BizProductionTaskHandover.from_user_id == from_user_id,
- BizProductionTaskHandover.tenant_no == tenant_no,
- BizProductionTaskHandover.del_flag == 0,
- )
- .group_by(BizProductionTaskHandover.task_id)
- .all()
- )
- time_map = {r[0]: _fmt_dt(r[1]) for r in rows}
- for item in items:
- item["handoverTime"] = time_map.get(item.get("id"), "")
- def get_production_task_row(
- db: Session, task_id: str, tenant_no: str
- ) -> Optional[BizProductionTask]:
- return (
- db.query(BizProductionTask)
- .filter(
- BizProductionTask.id == task_id,
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.del_flag == 0,
- )
- .first()
- )
- def assert_task_assignee(
- db: Session, task_id: str, user_id: str, tenant_no: str
- ) -> BizProductionTask:
- """Deprecated:优先使用 assert_task_step_assignee_or_legacy。"""
- return assert_task_step_assignee_or_legacy(db, task_id, user_id, tenant_no)
- def assert_task_step_assignee_or_legacy(
- db: Session, task_id: str, user_id: str, tenant_no: str
- ) -> BizProductionTask:
- from app.crud import production_task_step_assignee as step_assignee_svc
- task = get_production_task_row(db, task_id, tenant_no)
- if not task:
- raise ValueError("任务不存在")
- if step_assignee_svc.user_is_any_step_assignee_of_task(db, task_id, user_id, tenant_no):
- return task
- if (task.current_assignee_user_id or "") == (user_id or ""):
- return task
- raise PermissionError("非任务步骤负责人,无权操作")
- def assert_step_assignee_for_write(
- db: Session,
- step_id: str,
- user_id: str,
- tenant_no: str,
- *,
- manage: bool = False,
- ) -> BizProductionTaskStep:
- if manage:
- step = db.query(BizProductionTaskStep).filter(
- BizProductionTaskStep.id == step_id,
- BizProductionTaskStep.tenant_no == tenant_no,
- BizProductionTaskStep.del_flag == 0,
- ).first()
- if not step:
- raise ValueError("步骤不存在")
- return step
- from app.crud import production_task_step_assignee as step_assignee_svc
- step = db.query(BizProductionTaskStep).filter(
- BizProductionTaskStep.id == step_id,
- BizProductionTaskStep.tenant_no == tenant_no,
- BizProductionTaskStep.del_flag == 0,
- ).first()
- if not step:
- raise ValueError("步骤不存在")
- if not step_assignee_svc.user_is_step_assignee(db, step_id, user_id, tenant_no):
- raise PermissionError("非当前步骤负责人,无权操作")
- return step
- def _record_task_handover(
- db: Session,
- *,
- task_id: str,
- from_user_id: str,
- to_user_id: str,
- tenant_no: str,
- operator_id: Optional[str],
- create_dept_id: str,
- remark: Optional[str] = None,
- ) -> None:
- now = datetime.now()
- db.add(
- BizProductionTaskHandover(
- id=generate_uuid(),
- task_id=task_id,
- from_user_id=from_user_id,
- to_user_id=to_user_id,
- handover_time=now,
- remark=(remark or "").strip() or None,
- tenant_no=tenant_no,
- order_num=0,
- create_by=operator_id,
- create_dept_id=create_dept_id or "",
- create_time=now,
- update_by=operator_id,
- update_time=now,
- del_flag=0,
- )
- )
- def forward_production_task(
- db: Session,
- task_id: str,
- *,
- from_user_id: str,
- to_user_id: str,
- tenant_no: str,
- operator_id: Optional[str] = None,
- create_dept_id: str = "",
- remark: Optional[str] = None,
- require_assignee: bool = True,
- ) -> BizProductionTask:
- to_uid = _validate_assignee_user_id(db, to_user_id, tenant_no)
- from_uid = (from_user_id or "").strip()
- if not from_uid:
- raise ValueError("转出用户无效")
- if from_uid == to_uid:
- raise ValueError("不能转发给自己")
- if require_assignee:
- task = assert_task_assignee(db, task_id, from_uid, tenant_no)
- else:
- task = get_production_task_row(db, task_id, tenant_no)
- if not task:
- raise ValueError("任务不存在")
- handover_from = (task.current_assignee_user_id or from_uid).strip() or from_uid
- _record_task_handover(
- db,
- task_id=task_id,
- from_user_id=handover_from,
- to_user_id=to_uid,
- tenant_no=tenant_no,
- operator_id=operator_id,
- create_dept_id=create_dept_id,
- remark=remark,
- )
- task.current_assignee_user_id = to_uid
- task.update_by = operator_id
- task.update_time = datetime.now()
- db.add(task)
- db.flush()
- return task
- def reassign_production_task(
- db: Session,
- task_id: str,
- *,
- assignee_user_id: str,
- tenant_no: str,
- operator_id: Optional[str] = None,
- create_dept_id: str = "",
- remark: Optional[str] = None,
- ) -> BizProductionTask:
- task = get_production_task_row(db, task_id, tenant_no)
- if not task:
- raise ValueError("任务不存在")
- to_uid = _validate_assignee_user_id(db, assignee_user_id, tenant_no)
- from_uid = (task.current_assignee_user_id or operator_id or "").strip()
- if from_uid and from_uid != to_uid:
- _record_task_handover(
- db,
- task_id=task_id,
- from_user_id=from_uid,
- to_user_id=to_uid,
- tenant_no=tenant_no,
- operator_id=operator_id,
- create_dept_id=create_dept_id,
- remark=(remark or "").strip() or "管理员改派",
- )
- elif not from_uid:
- _record_task_handover(
- db,
- task_id=task_id,
- from_user_id=operator_id or to_uid,
- to_user_id=to_uid,
- tenant_no=tenant_no,
- operator_id=operator_id,
- create_dept_id=create_dept_id,
- remark=(remark or "").strip() or "管理员改派",
- )
- task.current_assignee_user_id = to_uid
- task.update_by = operator_id
- task.update_time = datetime.now()
- db.add(task)
- db.flush()
- return task
- def create_production_task(
- db: Session, *, tenant_no: str, user_id: Optional[str] = None,
- create_dept_id: str = "", data: Dict[str, Any],
- order_source: str = "inbound",
- ) -> BizProductionTask:
- work_order_id = (data.get("work_order_id") or "").strip()
- wo = None
- if work_order_id:
- wo = (
- db.query(BizProductionWorkOrder)
- .filter(
- BizProductionWorkOrder.id == work_order_id,
- BizProductionWorkOrder.tenant_no == tenant_no,
- BizProductionWorkOrder.del_flag == 0,
- )
- .first()
- )
- if wo:
- wo_type = getattr(wo, "work_order_type", None) or "inbound"
- if wo_type == "temporary":
- order_source = "temporary"
- elif wo_type == "outbound":
- order_source = "outbound"
- task_type = (data.get("task_type") or "").strip() or "cell_storage"
- if wo and (getattr(wo, "work_order_type", None) or "inbound") == "outbound":
- if not (data.get("task_type") or "").strip():
- task_type = "outbound"
- assignee_raw = (data.get("assignee_user_id") or data.get("assigneeUserId") or "").strip()
- assignee_user_id = None
- if assignee_raw:
- assignee_user_id = _validate_assignee_user_id(db, assignee_raw, tenant_no)
- task_no = _generate_task_no(db, tenant_no)
- now = datetime.now()
- task = BizProductionTask(
- id=generate_uuid(),
- task_no=task_no,
- work_order_id=data.get("work_order_id", ""),
- order_source=order_source,
- task_type=task_type,
- task_name=(data.get("task_name") or "").strip() or None,
- description=(data.get("description") or "").strip() or None,
- route_id=data.get("route_id") or None,
- task_status="dpz",
- current_assignee_user_id=assignee_user_id,
- remark=(data.get("remark") or "").strip() or None,
- attachments=(data.get("attachments") or "").strip() or None,
- tenant_no=tenant_no,
- order_num=0,
- create_by=user_id,
- create_dept_id=create_dept_id or "",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(task)
- db.flush()
- route_id = data.get("route_id")
- if route_id:
- _clone_route_to_task(db, task.id, route_id, tenant_no, user_id, create_dept_id)
- route = (
- db.query(BizProcessRoute)
- .filter(
- BizProcessRoute.id == route_id,
- BizProcessRoute.tenant_no == tenant_no,
- BizProcessRoute.del_flag == 0,
- )
- .first()
- )
- if route and (getattr(route, "equipment_ids", None) or getattr(route, "equipment_name", None)):
- task.equipment_ids = route.equipment_ids
- task.equipment_name = route.equipment_name
- db.add(task)
- from app.crud import production_work_order as production_work_order_svc
- production_work_order_svc.sync_production_work_order_status_from_tasks(
- db, task.work_order_id or "", tenant_no, user_id
- )
- if wo and (getattr(wo, "work_order_type", None) or "inbound") == "temporary":
- if not getattr(wo, "configured_time", None):
- wo.configured_time = now
- wo.configured_by = user_id
- wo.update_by = user_id
- wo.update_time = now
- db.add(wo)
- db.flush()
- return task
- def dispatch_task_steps(
- db: Session,
- task_id: str,
- *,
- tenant_no: str,
- steps_payload: List[Dict[str, Any]],
- operator_id: Optional[str] = None,
- create_dept_id: str = "",
- ) -> Dict[str, Any]:
- from app.crud import production_task_step_assignee as step_assignee_svc
- task = get_production_task_row(db, task_id, tenant_no)
- if not task:
- raise ValueError("任务不存在")
- if not steps_payload:
- raise ValueError("请至少指定一个步骤的负责人")
- step_rows = {
- s.id: s
- for s in db.query(BizProductionTaskStep).filter(
- BizProductionTaskStep.task_id == task_id,
- BizProductionTaskStep.tenant_no == tenant_no,
- BizProductionTaskStep.del_flag == 0,
- ).all()
- }
- for item in steps_payload:
- sid = (item.get("step_id") or item.get("stepId") or "").strip()
- if sid not in step_rows:
- raise ValueError(f"步骤不存在或不属于该任务: {sid}")
- user_ids = item.get("assignee_user_ids") or item.get("assigneeUserIds") or []
- step_assignee_svc.sync_assignees_for_step(
- db,
- sid,
- user_ids,
- tenant_no=tenant_no,
- operator_id=operator_id,
- create_dept_id=create_dept_id,
- )
- step = step_rows[sid]
- if "planned_start_time" in item or "plannedStartTime" in item:
- pst = item.get("planned_start_time")
- if pst is None and "plannedStartTime" in item:
- pst = item.get("plannedStartTime")
- if pst is None or pst == "":
- step.planned_start_time = None
- else:
- if isinstance(pst, datetime):
- step.planned_start_time = pst
- else:
- parsed = parse_datetime(str(pst).strip())
- if parsed is None:
- raise ValueError("计划开始时间格式无效")
- step.planned_start_time = parsed
- step.update_by = operator_id
- step.update_time = datetime.now()
- db.add(step)
- db.flush()
- task_dict = _task_to_dict(task)
- _enrich_tasks_with_step_summaries(db, [task_dict], tenant_no=tenant_no)
- return {"taskId": task_id, "steps": task_dict.get("steps") or []}
- def _enrich_tasks_with_step_summaries(
- db: Session, items: List[Dict[str, Any]], *, tenant_no: str
- ) -> None:
- """批量附加工艺步骤摘要(无试剂/耗材/产出等子表),供列表卡片展示。"""
- if not items:
- return
- task_ids = [it["id"] for it in items if (it.get("id") or "").strip()]
- if not task_ids:
- return
- rows = (
- db.query(BizProductionTaskStep)
- .filter(
- BizProductionTaskStep.task_id.in_(task_ids),
- BizProductionTaskStep.tenant_no == tenant_no,
- BizProductionTaskStep.del_flag == 0,
- )
- .order_by(
- BizProductionTaskStep.task_id.asc(),
- BizProductionTaskStep.step_order.asc(),
- BizProductionTaskStep.create_time.asc(),
- )
- .all()
- )
- step_dicts = [_step_to_dict(s) for s in rows]
- if step_dicts:
- from app.crud.production_task_step_assignee import enrich_step_assignees
- enrich_step_assignees(db, step_dicts, tenant_no=tenant_no)
- by_task: Dict[str, List[Dict[str, Any]]] = {tid: [] for tid in task_ids}
- for sd in step_dicts:
- tid = (sd.get("taskId") or "").strip()
- if tid in by_task:
- by_task[tid].append(sd)
- for item in items:
- tid = (item.get("id") or "").strip()
- item["steps"] = by_task.get(tid, [])
- def list_production_tasks(
- db: Session, *, tenant_no: str,
- page: int = 1, page_size: int = 10,
- keyword: Optional[str] = None,
- work_order_id: Optional[str] = None,
- work_order_ids: Optional[List[str]] = None,
- task_no: Optional[str] = None,
- task_status: Optional[str] = None,
- started_time_start: Optional[str] = None,
- started_time_end: Optional[str] = None,
- planned_start_time_start: Optional[str] = None,
- planned_start_time_end: Optional[str] = None,
- planned_start_time_mine_user_id: Optional[str] = None,
- current_assignee_user_id: Optional[str] = None,
- step_assignee_user_id: Optional[str] = None,
- mine_history_user_id: Optional[str] = None,
- work_order_type: Optional[str] = None,
- reception_source: Optional[str] = None,
- biz_type: Optional[str] = None,
- user_info=None,
- include_step_summary: bool = False,
- exclude_void_protocol: bool = True,
- ) -> Tuple[List[Dict[str, Any]], int]:
- def _filtered():
- q = db.query(BizProductionTask).filter(
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.del_flag == 0,
- )
- joined_wo = False
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- wo_type = (work_order_type or "").strip()
- if wo_type == "temporary":
- keyword_conditions = [BizProductionWorkOrder.remark.like(like)]
- else:
- keyword_conditions = [
- BizProductionTask.task_no.like(like),
- BizProductionWorkOrder.work_order_no.like(like),
- BizProductionWorkOrder.sample_code.like(like),
- ]
- if tenant_no:
- from app.crud.production_work_order import _outbound_work_order_ids_by_keyword
- outbound_ids = _outbound_work_order_ids_by_keyword(db, tenant_no, kw)
- if outbound_ids:
- keyword_conditions.append(BizProductionWorkOrder.id.in_(outbound_ids))
- q = q.outerjoin(
- BizProductionWorkOrder,
- and_(
- BizProductionWorkOrder.id == BizProductionTask.work_order_id,
- BizProductionWorkOrder.del_flag == 0,
- ),
- ).filter(or_(*keyword_conditions))
- joined_wo = True
- elif task_no and task_no.strip():
- q = q.filter(BizProductionTask.task_no.like(f"%{task_no.strip()}%"))
- wo_ids = [x.strip() for x in (work_order_ids or []) if x and str(x).strip()]
- if wo_ids:
- q = q.filter(BizProductionTask.work_order_id.in_(wo_ids))
- elif work_order_id and work_order_id.strip():
- q = q.filter(BizProductionTask.work_order_id == work_order_id.strip())
- if task_status and task_status.strip():
- q = q.filter(BizProductionTask.task_status == task_status.strip())
- if started_time_start and started_time_start.strip():
- q = q.filter(
- BizProductionTask.started_time
- >= _normalize_date_bound(started_time_start.strip())
- )
- if started_time_end and started_time_end.strip():
- q = q.filter(
- BizProductionTask.started_time
- <= _normalize_date_bound(started_time_end.strip(), end_of_day=True)
- )
- pst_start = (planned_start_time_start or "").strip()
- pst_end = (planned_start_time_end or "").strip()
- if pst_start or pst_end:
- if not joined_wo:
- q = q.join(
- BizProductionWorkOrder,
- and_(
- BizProductionWorkOrder.id == BizProductionTask.work_order_id,
- BizProductionWorkOrder.del_flag == 0,
- ),
- )
- joined_wo = True
- from app.crud.production_work_order import apply_planned_start_time_step_filter
- q = apply_planned_start_time_step_filter(
- q,
- tenant_no=tenant_no,
- work_order_id_column=BizProductionTask.work_order_id,
- planned_start_time_start=planned_start_time_start,
- planned_start_time_end=planned_start_time_end,
- mine_user_id=planned_start_time_mine_user_id,
- )
- ca_uid = (current_assignee_user_id or "").strip()
- if ca_uid:
- q = q.filter(BizProductionTask.current_assignee_user_id == ca_uid)
- sa_uid = (step_assignee_user_id or "").strip()
- if sa_uid:
- step_task_ids = (
- db.query(BizProductionTaskStep.task_id)
- .join(
- BizProductionTaskStepAssignee,
- BizProductionTaskStepAssignee.step_id == BizProductionTaskStep.id,
- )
- .filter(
- BizProductionTaskStepAssignee.user_id == sa_uid,
- BizProductionTaskStepAssignee.tenant_no == tenant_no,
- BizProductionTaskStepAssignee.del_flag == 0,
- BizProductionTaskStep.del_flag == 0,
- )
- .distinct()
- .subquery()
- )
- q = q.filter(
- or_(
- BizProductionTask.id.in_(step_task_ids),
- BizProductionTask.current_assignee_user_id == sa_uid,
- )
- )
- mh_uid = (mine_history_user_id or "").strip()
- if mh_uid:
- handover_task_ids = (
- db.query(BizProductionTaskHandover.task_id)
- .filter(
- BizProductionTaskHandover.from_user_id == mh_uid,
- BizProductionTaskHandover.tenant_no == tenant_no,
- BizProductionTaskHandover.del_flag == 0,
- )
- .distinct()
- .subquery()
- )
- q = q.filter(
- BizProductionTask.current_assignee_user_id != mh_uid,
- BizProductionTask.id.in_(handover_task_ids),
- )
- wo_type = (work_order_type or "").strip()
- if wo_type:
- if not joined_wo:
- q = q.join(
- BizProductionWorkOrder,
- and_(
- BizProductionWorkOrder.id == BizProductionTask.work_order_id,
- BizProductionWorkOrder.del_flag == 0,
- ),
- )
- joined_wo = True
- q = q.filter(BizProductionWorkOrder.work_order_type == wo_type)
- rs = (reception_source or "").strip()
- if rs:
- if not joined_wo:
- q = q.join(
- BizProductionWorkOrder,
- and_(
- BizProductionWorkOrder.id == BizProductionTask.work_order_id,
- BizProductionWorkOrder.del_flag == 0,
- ),
- )
- joined_wo = True
- q = q.filter(BizProductionWorkOrder.reception_source == rs)
- bt = (biz_type or "").strip()
- if bt:
- if not joined_wo:
- q = q.join(
- BizProductionWorkOrder,
- and_(
- BizProductionWorkOrder.id == BizProductionTask.work_order_id,
- BizProductionWorkOrder.del_flag == 0,
- ),
- )
- joined_wo = True
- q = q.filter(BizProductionWorkOrder.biz_type == bt)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizProductionTask, user_info, db, tenant_no)
- if exclude_void_protocol:
- if not joined_wo:
- q = q.join(
- BizProductionWorkOrder,
- and_(
- BizProductionWorkOrder.id == BizProductionTask.work_order_id,
- BizProductionWorkOrder.del_flag == 0,
- ),
- )
- from app.crud.production_work_order import apply_exclude_inactive_protocol_filter
- q = apply_exclude_inactive_protocol_filter(q, exclude=True)
- return q
- total = _filtered().order_by(None).with_entities(func.count(BizProductionTask.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = (
- _filtered()
- .order_by(BizProductionTask.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- items = [_task_to_dict(x) for x in rows]
- # 批量补充路线名称
- route_ids = [x.route_id for x in rows if x.route_id]
- if route_ids:
- route_map = {
- r.id: r.route_name
- for r in db.query(BizProcessRoute.id, BizProcessRoute.route_name)
- .filter(BizProcessRoute.id.in_(route_ids))
- .all()
- }
- for item, row in zip(items, rows):
- item["routeName"] = route_map.get(row.route_id, "")
- # 批量补充工单号 + 协议号 + 样本编码
- wo_ids = [x.work_order_id for x in rows if x.work_order_id]
- wo_map: Dict[str, Any] = {}
- wo_ctx_map: Dict[str, Dict[str, Any]] = {}
- if wo_ids:
- wo_rows = (
- db.query(
- BizProductionWorkOrder.id,
- BizProductionWorkOrder.work_order_no,
- BizProductionWorkOrder.work_order_type,
- BizProductionWorkOrder.biz_type,
- BizProductionWorkOrder.infusion_schedule_id,
- BizProductionWorkOrder.protocol_no,
- BizProductionWorkOrder.sample_code,
- BizProductionWorkOrder.reception_id,
- BizProductionWorkOrder.protocol_sample_id,
- BizProductionWorkOrder.collection_protocol_id,
- BizProductionWorkOrder.reception_source,
- )
- .filter(BizProductionWorkOrder.id.in_(wo_ids))
- .all()
- )
- for w in wo_rows:
- wo_map[w.id] = w
- wo_type = getattr(w, "work_order_type", None) or "inbound"
- wo_ctx_map[w.id] = {
- "workOrderType": wo_type,
- "bizType": getattr(w, "biz_type", None) or "",
- "infusionScheduleId": getattr(w, "infusion_schedule_id", None) or "",
- }
- cell_wo_ids = [
- w.collection_protocol_id
- for w in wo_rows
- if (w.reception_source or "cell_storage") != "research_donation"
- and (getattr(w, "work_order_type", None) or "inbound") != "outbound"
- and w.collection_protocol_id
- ]
- donation_wo_ids = [
- w.collection_protocol_id
- for w in wo_rows
- if (w.reception_source or "") == "research_donation"
- and (getattr(w, "work_order_type", None) or "inbound") != "outbound"
- and w.collection_protocol_id
- ]
- proto_status_map: Dict[str, str] = {}
- proto_void_type_map: Dict[str, str] = {}
- if cell_wo_ids:
- for p in (
- db.query(
- BizCollectionProtocol.id,
- BizCollectionProtocol.protocol_status,
- BizCollectionProtocol.void_type,
- )
- .filter(BizCollectionProtocol.id.in_(cell_wo_ids), BizCollectionProtocol.del_flag == 0)
- .all()
- ):
- proto_status_map[p.id] = p.protocol_status or ""
- proto_void_type_map[p.id] = p.void_type or ""
- donation_status_map: Dict[str, str] = {}
- donation_void_type_map: Dict[str, str] = {}
- if donation_wo_ids:
- for d in (
- db.query(
- BizResearchDonation.id,
- BizResearchDonation.donation_status,
- BizResearchDonation.void_type,
- )
- .filter(BizResearchDonation.id.in_(donation_wo_ids), BizResearchDonation.del_flag == 0)
- .all()
- ):
- donation_status_map[d.id] = d.donation_status or ""
- donation_void_type_map[d.id] = d.void_type or ""
- from app.core.protocol_status_display import format_protocol_status_label
- for item, row in zip(items, rows):
- wo = wo_map.get(row.work_order_id)
- if wo:
- item["workOrderNo"] = wo.work_order_no or ""
- wo_type = getattr(wo, "work_order_type", None) or "inbound"
- if wo_type != "outbound":
- item["protocolNo"] = wo.protocol_no or ""
- item["sampleCode"] = wo.sample_code or ""
- _source = wo.reception_source or "cell_storage"
- if _source == "research_donation":
- _st = donation_status_map.get(wo.collection_protocol_id, "")
- _vt = donation_void_type_map.get(wo.collection_protocol_id, "")
- item["protocolStatus"] = _st
- item["voidType"] = _vt
- item["protocolStatusLabel"] = format_protocol_status_label(
- _st, _vt, kind="donation", db=db, tenant_no=tenant_no
- )
- else:
- _st = proto_status_map.get(wo.collection_protocol_id, "")
- _vt = proto_void_type_map.get(wo.collection_protocol_id, "")
- item["protocolStatus"] = _st
- item["voidType"] = _vt
- item["protocolStatusLabel"] = format_protocol_status_label(
- _st, _vt, kind="protocol", db=db, tenant_no=tenant_no
- )
- else:
- item["sampleCode"] = wo.sample_code or ""
- _enrich_outbound_task_items(db, tenant_no, items, wo_ctx_map)
- # 批量补充接收人、接收时间、样本类型(与质检任务/工单列表一致:优先接收记录样本 id)
- from app.crud.qc_work_order import (
- _effective_donation_sample_id,
- _effective_protocol_sample_id,
- _effective_reception_source,
- _load_sample_type_name_map,
- _sample_type_name_for_item,
- )
- from app.crud.sample_reception import _donation_sample_id_col
- reception_ids = [wo.reception_id for wo in wo_map.values() if getattr(wo, "reception_id", None)]
- reception_map: Dict[str, BizSampleReception] = {}
- if reception_ids:
- reception_map = {
- r.id: r
- for r in db.query(BizSampleReception)
- .filter(BizSampleReception.id.in_(reception_ids), BizSampleReception.del_flag == 0)
- .all()
- }
- wo_item_ctx: Dict[str, Dict[str, Any]] = {}
- cell_sample_ids: List[str] = []
- donation_sample_ids: List[str] = []
- for wo in wo_map.values():
- rec = reception_map.get(wo.reception_id) if wo.reception_id else None
- ctx = {
- "receptionSource": getattr(wo, "reception_source", "cell_storage") or "cell_storage",
- "protocolSampleId": wo.protocol_sample_id or "",
- "collectionProtocolId": wo.collection_protocol_id or "",
- }
- wo_item_ctx[wo.id] = ctx
- source = _effective_reception_source(ctx, rec)
- if source == "research_donation":
- dsid = _effective_donation_sample_id(ctx, rec)
- if dsid:
- donation_sample_ids.append(dsid)
- else:
- psid = _effective_protocol_sample_id(ctx, rec)
- if psid:
- cell_sample_ids.append(psid)
- cell_sample_ids = list(dict.fromkeys(cell_sample_ids))
- donation_sample_ids = list(dict.fromkeys(donation_sample_ids))
- protocol_samples: Dict[str, BizProtocolSample] = {}
- if cell_sample_ids:
- protocol_samples = {
- s.id: s
- for s in db.query(BizProtocolSample)
- .filter(BizProtocolSample.id.in_(cell_sample_ids), BizProtocolSample.del_flag == 0)
- .all()
- }
- donation_samples: Dict[str, BizResearchDonationSample] = {}
- if donation_sample_ids:
- for s in (
- db.query(BizResearchDonationSample)
- .filter(
- _donation_sample_id_col(BizResearchDonationSample.id).in_(donation_sample_ids),
- BizResearchDonationSample.del_flag == 0,
- )
- .all()
- ):
- donation_samples[s.id] = s
- donation_samples[(s.id or "").strip()] = s
- type_ids = list(
- {
- s.sample_type_id
- for s in list(protocol_samples.values()) + list(donation_samples.values())
- if s.sample_type_id
- }
- )
- type_map = _load_sample_type_name_map(db, type_ids)
- # 将补充字段写入 items
- for item, row in zip(items, rows):
- wo = wo_map.get(row.work_order_id)
- if not wo:
- continue
- wo_type = getattr(wo, "work_order_type", None) or "inbound"
- if wo_type == "outbound":
- continue
- item["protocolNo"] = wo.protocol_no or ""
- item["sampleCode"] = wo.sample_code or ""
- rec = reception_map.get(wo.reception_id) if wo.reception_id else None
- item["receiverName"] = rec.handler_name or "" if rec else ""
- item["receptionTime"] = _fmt_dt(rec.reception_time) if rec else ""
- ctx = wo_item_ctx.get(wo.id, {})
- source = _effective_reception_source(ctx, rec)
- item["sampleTypeName"] = _sample_type_name_for_item(
- source=source,
- donation_sample_id=_effective_donation_sample_id(ctx, rec),
- protocol_sample_id=_effective_protocol_sample_id(ctx, rec),
- type_map=type_map,
- protocol_samples=protocol_samples,
- donation_samples=donation_samples,
- )
- _rs = source
- item["receptionSource"] = _rs
- item["receptionSourceLabel"] = _RECEPTION_SOURCE_LABELS.get(_rs, _rs)
- _enrich_assignee_names(db, items)
- if include_step_summary and items:
- _enrich_tasks_with_step_summaries(db, items, tenant_no=tenant_no)
- return items, int(total)
- def list_mine_production_tasks(
- db: Session,
- *,
- assignee_user_id: str,
- tenant_no: str,
- scope: str = "current",
- page: int = 1,
- page_size: int = 10,
- keyword: Optional[str] = None,
- task_status: Optional[str] = None,
- work_order_type: Optional[str] = None,
- reception_source: Optional[str] = None,
- biz_type: Optional[str] = None,
- include_step_summary: bool = False,
- exclude_void_protocol: bool = True,
- planned_start_time_start: Optional[str] = None,
- planned_start_time_end: Optional[str] = None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- uid = (assignee_user_id or "").strip()
- if not uid:
- return [], 0
- pst_start = (planned_start_time_start or "").strip()
- pst_end = (planned_start_time_end or "").strip()
- mine_planned_user_id = uid if (pst_start or pst_end) else None
- sc = (scope or "current").strip().lower()
- if sc == "history":
- items, total = list_production_tasks(
- db,
- tenant_no=tenant_no,
- page=page,
- page_size=page_size,
- keyword=keyword,
- task_status=task_status,
- mine_history_user_id=uid,
- work_order_type=work_order_type,
- reception_source=reception_source,
- biz_type=biz_type,
- include_step_summary=include_step_summary,
- exclude_void_protocol=exclude_void_protocol,
- planned_start_time_start=planned_start_time_start,
- planned_start_time_end=planned_start_time_end,
- planned_start_time_mine_user_id=mine_planned_user_id,
- )
- _enrich_handover_times(db, items, from_user_id=uid, tenant_no=tenant_no)
- return items, total
- return list_production_tasks(
- db,
- tenant_no=tenant_no,
- page=page,
- page_size=page_size,
- keyword=keyword,
- task_status=task_status,
- step_assignee_user_id=uid,
- work_order_type=work_order_type,
- reception_source=reception_source,
- biz_type=biz_type,
- include_step_summary=include_step_summary,
- exclude_void_protocol=exclude_void_protocol,
- planned_start_time_start=planned_start_time_start,
- planned_start_time_end=planned_start_time_end,
- planned_start_time_mine_user_id=mine_planned_user_id,
- )
- def get_production_task_detail(
- db: Session,
- task_id: str,
- tenant_no: str,
- *,
- include_step_sub_tables: bool = True,
- ) -> Optional[Dict[str, Any]]:
- task = db.query(BizProductionTask).filter(
- and_(
- BizProductionTask.id == task_id,
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.del_flag == 0,
- )
- ).first()
- if not task:
- return None
- wo = db.query(BizProductionWorkOrder).filter(BizProductionWorkOrder.id == task.work_order_id).first()
- result = _task_to_dict(task, work_order_no=wo.work_order_no if wo else "")
- if wo:
- wo_type = getattr(wo, "work_order_type", None) or "inbound"
- result["workOrderType"] = wo_type
- if wo_type == "outbound":
- result["bizType"] = getattr(wo, "biz_type", None) or ""
- elif wo_type == "inbound":
- result["receptionSource"] = (
- getattr(wo, "reception_source", "cell_storage") or "cell_storage"
- )
- result["sampleCode"] = wo.sample_code or ""
- if wo.reception_id:
- rec = (
- db.query(BizSampleReception)
- .filter(
- BizSampleReception.id == wo.reception_id,
- BizSampleReception.tenant_no == tenant_no,
- BizSampleReception.del_flag == 0,
- )
- .first()
- )
- if rec and (rec.lab_storage_code or "").strip():
- result["labStorageCode"] = (rec.lab_storage_code or "").strip()
- if wo_type == "outbound":
- wo_ctx = {
- wo.id: {
- "workOrderType": wo_type,
- "bizType": getattr(wo, "biz_type", None) or "",
- "infusionScheduleId": getattr(wo, "infusion_schedule_id", None) or "",
- }
- }
- _enrich_outbound_task_items(db, tenant_no, [result], wo_ctx)
- else:
- result["protocolNo"] = wo.protocol_no or ""
- _src = getattr(wo, "reception_source", "cell_storage") or "cell_storage"
- _cpid = wo.collection_protocol_id or ""
- if _src == "research_donation" and _cpid:
- d = (
- db.query(BizResearchDonation.donation_status, BizResearchDonation.void_type)
- .filter(BizResearchDonation.id == _cpid, BizResearchDonation.del_flag == 0)
- .first()
- )
- _st = d.donation_status or "" if d else ""
- _vt = d.void_type or "" if d else ""
- result["protocolStatus"] = _st
- result["voidType"] = _vt
- from app.core.protocol_status_display import format_protocol_status_label
- result["protocolStatusLabel"] = format_protocol_status_label(
- _st, _vt, kind="donation", db=db, tenant_no=tenant_no
- )
- elif _cpid:
- p = (
- db.query(BizCollectionProtocol.protocol_status, BizCollectionProtocol.void_type)
- .filter(BizCollectionProtocol.id == _cpid, BizCollectionProtocol.del_flag == 0)
- .first()
- )
- _st = p.protocol_status or "" if p else ""
- _vt = p.void_type or "" if p else ""
- result["protocolStatus"] = _st
- result["voidType"] = _vt
- from app.core.protocol_status_display import format_protocol_status_label
- result["protocolStatusLabel"] = format_protocol_status_label(
- _st, _vt, kind="protocol", db=db, tenant_no=tenant_no
- )
- else:
- result["protocolStatus"] = ""
- result["protocolStatusLabel"] = ""
- steps = (
- db.query(BizProductionTaskStep)
- .filter(and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0))
- .order_by(BizProductionTaskStep.step_order.asc(), BizProductionTaskStep.create_time.asc())
- .all()
- )
- step_ids = [s.id for s in steps]
- sub_tables_by_step: Dict[str, Dict[str, Any]] = {}
- if include_step_sub_tables and step_ids:
- sub_tables_by_step = _batch_load_step_sub_tables(db, step_ids)
- step_list = []
- for s in steps:
- sd = _step_to_dict(s)
- sd.update(sub_tables_by_step.get(s.id) or _empty_step_sub_tables())
- step_list.append(sd)
- from app.crud.production_task_step_assignee import enrich_step_assignees, enrich_step_completed_by_names
- enrich_step_assignees(db, step_list, tenant_no=tenant_no)
- enrich_step_completed_by_names(db, step_list)
- inputs = db.query(BizProductionTaskInput).filter(
- and_(BizProductionTaskInput.task_id == task_id, BizProductionTaskInput.del_flag == 0)
- ).order_by(BizProductionTaskInput.order_num.asc(), BizProductionTaskInput.create_time.asc()).all()
- outputs = db.query(BizProductionTaskOutput).filter(
- and_(BizProductionTaskOutput.task_id == task_id, BizProductionTaskOutput.del_flag == 0)
- ).order_by(BizProductionTaskOutput.order_num.asc(), BizProductionTaskOutput.create_time.asc()).all()
- result["steps"] = step_list
- # --- enrich inputs with cell product info ---
- input_dicts = [_task_input_to_dict(i) for i in inputs]
- cp_input_ids = [i.input_id for i in inputs if i.input_type == "cell_product" and i.input_id]
- input_product_map: Dict[str, Dict[str, Any]] = {}
- if cp_input_ids:
- cp_rows = db.query(BizCellProduct).filter(
- BizCellProduct.id.in_(set(cp_input_ids)), BizCellProduct.del_flag == 0
- ).all()
- cp_dicts = [_cp_row_to_dict(c) for c in cp_rows]
- cp_dicts = _cp_enrich(db, tenant_no, cp_dicts)
- input_product_map = {d["id"]: d for d in cp_dicts}
- for idx, inp in enumerate(inputs):
- if inp.input_type == "cell_product" and inp.input_id and inp.input_id in input_product_map:
- input_dicts[idx] = _task_input_to_dict(inp, product_dict=input_product_map[inp.input_id])
- sample_codes: List[str] = []
- task_sample_code = (result.get("sampleCode") or "").strip()
- if task_sample_code:
- sample_codes.append(task_sample_code)
- for item in input_dicts:
- if item.get("inputType") == "sample":
- sc = (item.get("inputCode") or "").strip()
- if sc:
- sample_codes.append(sc)
- if sample_codes:
- from app.crud import sample_reception as reception_svc
- code_map = reception_svc.get_lab_storage_code_map_by_sample_codes(
- db, tenant_no, sample_codes
- )
- if task_sample_code and code_map.get(task_sample_code) and not result.get("labStorageCode"):
- result["labStorageCode"] = code_map[task_sample_code]
- for item in input_dicts:
- if item.get("inputType") == "sample":
- sc = (item.get("inputCode") or "").strip()
- item["labStorageCode"] = code_map.get(sc, "")
- result["inputs"] = input_dicts
- # --- enrich outputs with cell product info ---
- output_dicts = [_task_output_to_dict(o) for o in outputs]
- cp_output_ids = [o.cell_product_id for o in outputs if o.cell_product_id]
- output_product_map: Dict[str, Dict[str, Any]] = {}
- if cp_output_ids:
- cp_rows = db.query(BizCellProduct).filter(
- BizCellProduct.id.in_(set(cp_output_ids)), BizCellProduct.del_flag == 0
- ).all()
- cp_dicts = [_cp_row_to_dict(c) for c in cp_rows]
- cp_dicts = _cp_enrich(db, tenant_no, cp_dicts)
- output_product_map = {d["id"]: d for d in cp_dicts}
- for idx, out in enumerate(outputs):
- if out.cell_product_id and out.cell_product_id in output_product_map:
- output_dicts[idx] = _task_output_to_dict(out, product_dict=output_product_map[out.cell_product_id])
- result["outputs"] = output_dicts
- return result
- def _build_process_library_for_editor(
- processes: List[Dict[str, Any]],
- categories: List[Dict[str, Any]],
- ) -> List[Dict[str, Any]]:
- """按分类分组工艺库,结构与前端 route-config 左栏一致。"""
- cat_map: Dict[str, Dict[str, Any]] = {}
- u_key = "__uncategorized__"
- cat_map[u_key] = {"categoryId": u_key, "categoryName": "未分类", "processes": []}
- for cat in categories or []:
- cat_id = (cat.get("id") or "").strip()
- if not cat_id:
- continue
- cat_map[cat_id] = {
- "categoryId": cat_id,
- "categoryName": cat.get("categoryName") or cat.get("name") or "未命名分类",
- "processes": [],
- }
- for proc in processes or []:
- cat_id = (proc.get("categoryId") or "").strip() or u_key
- if cat_id not in cat_map:
- cat_map[cat_id] = {"categoryId": cat_id, "categoryName": "未知分类", "processes": []}
- cat_map[cat_id]["processes"].append(proc)
- return list(cat_map.values())
- def get_production_task_editor_init(
- db: Session,
- task_id: str,
- tenant_no: str,
- *,
- user_info=None,
- include_reference: bool = True,
- ) -> Optional[Dict[str, Any]]:
- """编辑生产任务弹窗初始化:任务详情 + 工艺库 + 步骤配置参考数据(单次聚合)。"""
- task = get_production_task_detail(
- db, task_id, tenant_no, include_step_sub_tables=True
- )
- if not task:
- return None
- result: Dict[str, Any] = {"task": task}
- if not include_reference:
- return result
- from app.crud import consumable as consumable_svc
- from app.crud import equipment as equipment_svc
- from app.crud import item_unit as item_unit_svc
- from app.crud import output_type as output_type_svc
- from app.crud import process as process_svc
- from app.crud import process_category as process_category_svc
- from app.crud import reagent as reagent_svc
- from app.crud import sop_document as sop_document_svc
- processes = process_svc.list_all_processes(db, tenant_no, user_info=user_info)
- categories, _ = process_category_svc.list_process_categories(
- db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
- )
- sops, _ = sop_document_svc.list_sop_documents(
- db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
- )
- output_types, _ = output_type_svc.list_output_types(
- db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
- )
- equipments, _ = equipment_svc.list_equipments(
- db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
- )
- reagents, _ = reagent_svc.list_reagents(
- db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
- )
- consumables, _ = consumable_svc.list_consumables(
- db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
- )
- item_units, _ = item_unit_svc.list_item_units(
- db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user_info
- )
- result["processLibrary"] = _build_process_library_for_editor(processes, categories)
- result["options"] = {
- "processCategories": categories,
- "sops": sops,
- "outputTypes": output_types,
- "equipments": equipments,
- "reagents": reagents,
- "consumables": consumables,
- "itemUnits": item_units,
- }
- return result
- def update_production_task(
- db: Session, task_id: str, tenant_no: str,
- user_id: Optional[str], data: Dict[str, Any],
- ) -> BizProductionTask:
- task = db.query(BizProductionTask).filter(
- and_(
- BizProductionTask.id == task_id,
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.del_flag == 0,
- )
- ).first()
- if not task:
- raise ValueError("任务不存在")
- for field in ["task_name", "description", "remark", "attachments"]:
- val = data.get(field)
- if val is not None:
- setattr(task, field, (val or "").strip() or None)
- if "equipment_ids" in data:
- eids, enames = resolve_equipment_list_snapshot(db, data.get("equipment_ids"), tenant_no)
- task.equipment_ids = eids
- task.equipment_name = enames
- task.update_by = user_id
- task.update_time = datetime.now()
- db.add(task)
- db.commit()
- db.refresh(task)
- return task
- def _consume_task_input_cell_products(
- db: Session,
- task_id: str,
- user_id: Optional[str],
- now: datetime,
- ) -> None:
- """任务首次完成时,将原材料中的细胞产品标记为已使用并释放库位占用。"""
- input_rows = db.query(BizProductionTaskInput).filter(
- and_(
- BizProductionTaskInput.task_id == task_id,
- BizProductionTaskInput.del_flag == 0,
- BizProductionTaskInput.input_type == "cell_product",
- BizProductionTaskInput.input_id.isnot(None),
- BizProductionTaskInput.input_id != "",
- )
- ).all()
- seen_ids: set = set()
- consumable_from = frozenset({"frozen", "thawed", "in_production"})
- for row in input_rows:
- cp_id = (row.input_id or "").strip()
- if not cp_id or cp_id in seen_ids:
- continue
- seen_ids.add(cp_id)
- product = db.query(BizCellProduct).filter(
- BizCellProduct.id == cp_id,
- BizCellProduct.del_flag == 0,
- ).first()
- if not product:
- continue
- current = (product.product_status or "").strip()
- if current in ("used", "voided", "sold"):
- continue
- if current not in consumable_from:
- continue
- product.product_status = "used"
- product.update_by = user_id
- product.update_time = now
- db.add(product)
- if product.storage_slot_id:
- try:
- occupancy_svc.remove_by_item(
- db, "cell_product", product.id, user_id=user_id,
- )
- except Exception:
- pass
- def _force_complete_task_steps(
- db: Session,
- task_id: str,
- tenant_no: str,
- user_id: Optional[str],
- now: datetime,
- ) -> int:
- """将任务下未完成步骤置为 ywc;不 commit。返回更新条数。"""
- steps = (
- db.query(BizProductionTaskStep)
- .filter(
- and_(
- BizProductionTaskStep.task_id == task_id,
- BizProductionTaskStep.tenant_no == tenant_no,
- BizProductionTaskStep.del_flag == 0,
- BizProductionTaskStep.step_status != "ywc",
- )
- )
- .all()
- )
- for step in steps:
- step.step_status = "ywc"
- if user_id:
- step.completed_by = user_id
- step.completed_time = now
- step.update_by = user_id
- step.update_time = now
- db.add(step)
- return len(steps)
- def update_task_status(
- db: Session,
- task_id: str,
- tenant_no: str,
- user_id: Optional[str],
- status: str,
- *,
- conclusion: Optional[str] = None,
- remark: Optional[str] = None,
- ) -> BizProductionTask:
- task = db.query(BizProductionTask).filter(
- and_(
- BizProductionTask.id == task_id,
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.del_flag == 0,
- )
- ).first()
- if not task:
- raise ValueError("任务不存在")
- if task.task_status == "ywc" and status != "ywc":
- raise ValueError("任务已完成,不可变更状态")
- now = datetime.now()
- previous_status = task.task_status
- task.task_status = status
- if status == "jxz" and not task.started_time:
- task.started_time = now
- if status == "ywc":
- concl = (conclusion or "").strip()
- if concl not in ("cg", "sb"):
- raise ValueError("须选择生产结论(成功或失败)")
- if previous_status != "ywc":
- _force_complete_task_steps(db, task_id, tenant_no, user_id, now)
- task.conclusion = concl
- if remark is not None:
- task.remark = (remark or "").strip() or None
- if previous_status != "ywc" and not task.completed_time:
- task.completed_time = now
- if previous_status != "ywc":
- _consume_task_input_cell_products(db, task.id, user_id, now)
- task.update_by = user_id
- task.update_time = now
- db.add(task)
- from app.crud import production_work_order as production_work_order_svc
- production_work_order_svc.sync_production_work_order_status_from_tasks(
- db, task.work_order_id or "", tenant_no, user_id
- )
- db.commit()
- db.refresh(task)
- return task
- def add_task_step(
- db: Session, task_id: str, tenant_no: str,
- user_id: Optional[str], data: Dict[str, Any],
- ) -> Dict[str, Any]:
- task = db.query(BizProductionTask).filter(
- and_(
- BizProductionTask.id == task_id,
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.del_flag == 0,
- )
- ).first()
- if not task:
- raise ValueError("任务不存在")
- now = datetime.now()
- step_order = data.get("step_order")
- if step_order is None:
- max_order = db.query(func.max(BizProductionTaskStep.step_order)).filter(
- and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0)
- ).scalar()
- step_order = (max_order or 0) + 1
- # 如果传入了 process_id,从工艺主表深拷贝基本信息和子表数据
- process_id = data.get("process_id")
- if process_id:
- proc = db.query(BizProcess).filter(
- and_(BizProcess.id == process_id, BizProcess.del_flag == 0)
- ).first()
- if not proc:
- raise ValueError("工艺不存在")
- step = BizProductionTaskStep(
- id=generate_uuid(), task_id=task_id,
- step_order=step_order,
- process_code=proc.process_code,
- category_id=proc.category_id,
- process_name=proc.process_name,
- description=proc.description,
- estimated_duration=proc.estimated_duration,
- sop_ids=proc.sop_ids,
- sop_content=proc.sop_content,
- step_status="dzx",
- tenant_no=tenant_no, order_num=0,
- remark=None,
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- )
- db.add(step)
- db.flush()
- # 深拷贝工艺子表 → 任务步骤子表
- reagent_rows = db.query(BizProcessReagent).filter(
- and_(BizProcessReagent.process_id == process_id, BizProcessReagent.del_flag == 0)
- ).order_by(BizProcessReagent.order_num.asc(), BizProcessReagent.create_time.asc()).all()
- for idx, r in enumerate(reagent_rows):
- db.add(BizProductionTaskStepReagent(
- id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
- reagent_code=r.reagent_code, reagent_name=r.reagent_name,
- spec=r.spec, standard_usage=r.standard_usage,
- unit=r.unit, is_key_material=r.is_key_material,
- remark=r.remark,
- order_num=_resolve_sub_table_order_num(r.order_num, idx, reagent_rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- consumable_rows = db.query(BizProcessConsumable).filter(
- and_(BizProcessConsumable.process_id == process_id, BizProcessConsumable.del_flag == 0)
- ).order_by(BizProcessConsumable.order_num.asc(), BizProcessConsumable.create_time.asc()).all()
- for idx, c in enumerate(consumable_rows):
- db.add(BizProductionTaskStepConsumable(
- id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
- consumable_code=c.consumable_code, consumable_name=c.consumable_name,
- spec_model=c.spec_model, standard_usage=c.standard_usage,
- unit=c.unit, is_required=c.is_required,
- remark=c.remark,
- order_num=_resolve_sub_table_order_num(c.order_num, idx, consumable_rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- output_rows = db.query(BizProcessOutput).filter(
- and_(BizProcessOutput.process_id == process_id, BizProcessOutput.del_flag == 0)
- ).order_by(BizProcessOutput.order_num.asc(), BizProcessOutput.create_time.asc()).all()
- for idx, o in enumerate(output_rows):
- db.add(BizProductionTaskStepOutput(
- id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
- output_type_id=o.output_type_id, output_name=o.output_name,
- output_barcode=o.output_barcode,
- remark=o.remark,
- order_num=_resolve_sub_table_order_num(o.order_num, idx, output_rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- collection_rows = db.query(BizProcessDataCollection).filter(
- and_(BizProcessDataCollection.process_id == process_id, BizProcessDataCollection.del_flag == 0)
- ).order_by(BizProcessDataCollection.order_num.asc(), BizProcessDataCollection.create_time.asc()).all()
- for idx, d in enumerate(collection_rows):
- db.add(BizProductionTaskStepDataCollection(
- id=generate_uuid(), step_id=step.id, tenant_no=tenant_no,
- collection_name=d.collection_name, unit=d.unit,
- remark=d.remark,
- order_num=_resolve_sub_table_order_num(d.order_num, idx, collection_rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- db.commit()
- else:
- # 无 process_id 时,使用前端传来的字段直接创建(兼容旧逻辑)
- step = BizProductionTaskStep(
- id=generate_uuid(), task_id=task_id,
- step_order=step_order,
- process_code=(data.get("process_code") or "").strip() or None,
- category_id=(data.get("category_id") or "").strip() or None,
- process_name=(data.get("process_name") or "").strip() or None,
- description=(data.get("description") or "").strip() or None,
- estimated_duration=(data.get("estimated_duration") or "").strip() or None,
- actual_duration=(data.get("actual_duration") or "").strip() or None,
- sop_ids=(data.get("sop_ids") or "").strip() or None,
- sop_content=(data.get("sop_content") if str(data.get("sop_content") or "").strip() else None),
- step_status=(data.get("step_status") or "dzx"),
- tenant_no=tenant_no, order_num=0,
- remark=(data.get("remark") or "").strip() or None,
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- )
- db.add(step)
- db.commit()
- # 返回带子表的完整步骤信息(与 get_production_task_detail 格式一致)
- result = _step_to_dict(step)
- reagents = db.query(BizProductionTaskStepReagent).filter(
- and_(BizProductionTaskStepReagent.step_id == step.id, BizProductionTaskStepReagent.del_flag == 0)
- ).order_by(BizProductionTaskStepReagent.order_num.asc(), BizProductionTaskStepReagent.create_time.asc()).all()
- consumables = db.query(BizProductionTaskStepConsumable).filter(
- and_(BizProductionTaskStepConsumable.step_id == step.id, BizProductionTaskStepConsumable.del_flag == 0)
- ).order_by(BizProductionTaskStepConsumable.order_num.asc(), BizProductionTaskStepConsumable.create_time.asc()).all()
- outputs = db.query(BizProductionTaskStepOutput).filter(
- and_(BizProductionTaskStepOutput.step_id == step.id, BizProductionTaskStepOutput.del_flag == 0)
- ).order_by(BizProductionTaskStepOutput.order_num.asc(), BizProductionTaskStepOutput.create_time.asc()).all()
- data_collections = db.query(BizProductionTaskStepDataCollection).filter(
- and_(BizProductionTaskStepDataCollection.step_id == step.id, BizProductionTaskStepDataCollection.del_flag == 0)
- ).order_by(BizProductionTaskStepDataCollection.order_num.asc(), BizProductionTaskStepDataCollection.create_time.asc()).all()
- configured_reagents = db.query(BizProductionTaskStepConfiguredReagent).filter(
- and_(BizProductionTaskStepConfiguredReagent.step_id == step.id, BizProductionTaskStepConfiguredReagent.del_flag == 0)
- ).order_by(BizProductionTaskStepConfiguredReagent.order_num.asc(), BizProductionTaskStepConfiguredReagent.create_time.asc()).all()
- result["reagents"] = [_step_reagent_to_dict(r) for r in reagents]
- result["consumables"] = [_step_consumable_to_dict(c) for c in consumables]
- result["configuredReagents"] = [_step_configured_reagent_to_dict(r) for r in configured_reagents]
- result["outputs"] = [_step_output_to_dict(o) for o in outputs]
- result["dataCollections"] = [_step_data_collection_to_dict(d) for d in data_collections]
- return result
- def update_task_step(
- db: Session, step_id: str, tenant_no: str,
- user_id: Optional[str], data: Dict[str, Any],
- ) -> BizProductionTaskStep:
- step = db.query(BizProductionTaskStep).filter(
- and_(
- BizProductionTaskStep.id == step_id,
- BizProductionTaskStep.tenant_no == tenant_no,
- BizProductionTaskStep.del_flag == 0,
- )
- ).first()
- if not step:
- raise ValueError("步骤不存在")
- for field in [
- "step_order", "process_code", "category_id", "process_name",
- "description", "estimated_duration", "actual_duration",
- "sop_ids", "sop_content", "step_status", "remark",
- ]:
- val = data.get(field)
- if val is not None:
- if field == "step_order":
- setattr(step, field, int(val))
- elif field == "sop_content":
- setattr(step, field, val if str(val).strip() else None)
- else:
- setattr(step, field, (val or "").strip() or None)
- if data.get("started_time"):
- parsed = parse_datetime(str(data["started_time"]).strip())
- if parsed is None:
- raise ValueError("开始时间格式无效")
- step.started_time = parsed
- if data.get("completed_time"):
- parsed = parse_datetime(str(data["completed_time"]).strip())
- if parsed is None:
- raise ValueError("完成时间格式无效")
- step.completed_time = parsed
- new_status = (data.get("step_status") or "").strip()
- if new_status == "ywc" and user_id:
- step.completed_by = user_id
- step.update_by = user_id
- step.update_time = datetime.now()
- db.add(step)
- db.commit()
- db.refresh(step)
- return step
- def delete_task_step(db: Session, step_id: str, tenant_no: str, user_id: Optional[str]) -> None:
- step = db.query(BizProductionTaskStep).filter(
- and_(
- BizProductionTaskStep.id == step_id,
- BizProductionTaskStep.tenant_no == tenant_no,
- BizProductionTaskStep.del_flag == 0,
- )
- ).first()
- if not step:
- raise ValueError("步骤不存在")
- now = datetime.now()
- step.del_flag = 1
- step.update_by = user_id
- step.update_time = now
- db.add(step)
- _soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step_id, now)
- _soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step_id, now)
- _soft_delete_sub(db, BizProductionTaskStepConfiguredReagent, "step_id", step_id, now)
- _soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step_id, now)
- _soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step_id, now)
- db.commit()
- def replace_step_reagents(
- db: Session, step_id: str, tenant_no: str,
- user_id: Optional[str], items: List[Dict[str, Any]],
- ) -> None:
- now = datetime.now()
- # Look up task info from step_id for stock log
- step_row = db.query(BizProductionTaskStep).filter(
- and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
- ).first()
- task_id = ""
- task_no = ""
- step_name = ""
- if step_row:
- step_name = step_row.process_name or ""
- task_row = db.query(BizProductionTask).filter(
- and_(BizProductionTask.id == step_row.task_id, BizProductionTask.del_flag == 0)
- ).first()
- if task_row:
- task_id = task_row.id
- task_no = task_row.task_no or ""
- try:
- _soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step_id, now)
- usage_items: List[Dict[str, Any]] = []
- for idx, it in enumerate(items):
- req_id = (
- (it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
- )
- _require_requisition_for_catalog_material(
- db,
- tenant_no,
- it.get("reagent_code"),
- "reagent",
- it.get("actual_usage"),
- req_id,
- )
- db.add(BizProductionTaskStepReagent(
- id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
- reagent_code=(it.get("reagent_code") or "").strip() or None,
- reagent_name=(it.get("reagent_name") or "").strip() or None,
- spec=(it.get("spec") or "").strip() or None,
- standard_usage=(it.get("standard_usage") or "").strip() or None,
- actual_usage=(it.get("actual_usage") or "").strip() or None,
- unit=(it.get("unit") or "").strip() or None,
- requisition_usage_id=req_id or None,
- is_key_material=it.get("is_key_material", "0"),
- remark=(it.get("remark") or "").strip() or None,
- order_num=idx, create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- if req_id:
- usage_items.append({
- "requisitionUsageId": req_id,
- "qty": it.get("actual_usage"),
- "usageUnit": it.get("unit"),
- "remark": it.get("remark"),
- })
- from app.crud import production_material_usage as pm_usage_crud
- pm_usage_crud.register_step_usage_from_requisition(
- db, step_id, step_name, task_id, task_no, usage_items, tenant_no, user_id, "",
- )
- db.commit()
- except Exception:
- db.rollback()
- raise
- def replace_step_consumables(
- db: Session, step_id: str, tenant_no: str,
- user_id: Optional[str], items: List[Dict[str, Any]],
- ) -> None:
- now = datetime.now()
- # Look up task info from step_id for stock log
- step_row = db.query(BizProductionTaskStep).filter(
- and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
- ).first()
- task_id = ""
- task_no = ""
- step_name = ""
- if step_row:
- step_name = step_row.process_name or ""
- task_row = db.query(BizProductionTask).filter(
- and_(BizProductionTask.id == step_row.task_id, BizProductionTask.del_flag == 0)
- ).first()
- if task_row:
- task_id = task_row.id
- task_no = task_row.task_no or ""
- try:
- _soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step_id, now)
- usage_items: List[Dict[str, Any]] = []
- for idx, it in enumerate(items):
- req_id = (
- (it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
- )
- _require_requisition_for_catalog_material(
- db,
- tenant_no,
- it.get("consumable_code"),
- "consumable",
- it.get("actual_usage"),
- req_id,
- )
- db.add(BizProductionTaskStepConsumable(
- id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
- consumable_code=(it.get("consumable_code") or "").strip() or None,
- consumable_name=(it.get("consumable_name") or "").strip() or None,
- spec_model=(it.get("spec_model") or "").strip() or None,
- standard_usage=(it.get("standard_usage") or "").strip() or None,
- actual_usage=(it.get("actual_usage") or "").strip() or None,
- unit=(it.get("unit") or "").strip() or None,
- requisition_usage_id=req_id or None,
- is_required=it.get("is_required", "1"),
- remark=(it.get("remark") or "").strip() or None,
- order_num=idx, create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- if req_id:
- usage_items.append({
- "requisitionUsageId": req_id,
- "qty": it.get("actual_usage"),
- "usageUnit": it.get("unit"),
- "remark": it.get("remark"),
- })
- from app.crud import production_material_usage as pm_usage_crud
- pm_usage_crud.register_step_usage_from_requisition(
- db, step_id, step_name, task_id, task_no, usage_items, tenant_no, user_id, "",
- )
- db.commit()
- except Exception:
- db.rollback()
- raise
- def replace_step_configured_reagents(
- db: Session, step_id: str, tenant_no: str,
- user_id: Optional[str], items: List[Dict[str, Any]],
- ) -> None:
- now = datetime.now()
- step_row = db.query(BizProductionTaskStep).filter(
- and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
- ).first()
- task_id = ""
- task_no = ""
- step_name = ""
- if step_row:
- step_name = step_row.process_name or ""
- task_row = db.query(BizProductionTask).filter(
- and_(BizProductionTask.id == step_row.task_id, BizProductionTask.del_flag == 0)
- ).first()
- if task_row:
- task_id = task_row.id
- task_no = task_row.task_no or ""
- try:
- _soft_delete_sub(db, BizProductionTaskStepConfiguredReagent, "step_id", step_id, now)
- usage_items: List[Dict[str, Any]] = []
- for idx, it in enumerate(items):
- req_id = (
- (it.get("requisition_usage_id") or it.get("requisitionUsageId") or "").strip()
- )
- _require_requisition_for_catalog_material(
- db,
- tenant_no,
- it.get("configured_reagent_code"),
- "configured_reagent",
- it.get("actual_usage"),
- req_id,
- )
- db.add(BizProductionTaskStepConfiguredReagent(
- id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
- configured_reagent_code=(it.get("configured_reagent_code") or "").strip() or None,
- configured_reagent_name=(it.get("configured_reagent_name") or "").strip() or None,
- spec=(it.get("spec") or "").strip() or None,
- standard_usage=(it.get("standard_usage") or "").strip() or None,
- actual_usage=(it.get("actual_usage") or "").strip() or None,
- unit=(it.get("unit") or "").strip() or None,
- requisition_usage_id=req_id or None,
- remark=(it.get("remark") or "").strip() or None,
- order_num=idx, create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- if req_id:
- usage_items.append({
- "requisitionUsageId": req_id,
- "qty": it.get("actual_usage"),
- "usageUnit": it.get("unit"),
- "remark": it.get("remark"),
- })
- from app.crud import production_material_usage as pm_usage_crud
- pm_usage_crud.register_step_usage_from_requisition(
- db, step_id, step_name, task_id, task_no, usage_items, tenant_no, user_id, "",
- )
- db.commit()
- except Exception:
- db.rollback()
- raise
- def replace_step_outputs(
- db: Session, step_id: str, tenant_no: str,
- user_id: Optional[str], items: List[Dict[str, Any]],
- ) -> None:
- now = datetime.now()
- _soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step_id, now)
- for idx, it in enumerate(items):
- db.add(BizProductionTaskStepOutput(
- id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
- output_type_id=(it.get("output_type_id") or "").strip() or None,
- output_name=(it.get("output_name") or "").strip() or None,
- output_barcode=(it.get("output_barcode") or "").strip() or None,
- remark=(it.get("remark") or "").strip() or None,
- order_num=idx, create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- db.commit()
- def replace_step_data_collections(
- db: Session, step_id: str, tenant_no: str,
- user_id: Optional[str], items: List[Dict[str, Any]],
- ) -> None:
- now = datetime.now()
- _soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step_id, now)
- for idx, it in enumerate(items):
- db.add(BizProductionTaskStepDataCollection(
- id=generate_uuid(), step_id=step_id, tenant_no=tenant_no,
- collection_name=(it.get("collection_name") or "").strip() or None,
- unit=(it.get("unit") or "").strip() or None,
- collection_value=(it.get("collection_value") or "").strip() or None,
- remark=(it.get("remark") or "").strip() or None,
- order_num=idx, create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- db.commit()
- def spawn_temporary_work_order_from_task(
- db: Session,
- source_task_id: str,
- *,
- tenant_no: str,
- user_id: Optional[str] = None,
- create_dept_id: str = "",
- remark: str = "",
- ) -> Dict[str, Any]:
- """从生产任务创建临时生产工单,并返回待复制到新任务的原材料载荷(不含产出)。"""
- task = (
- db.query(BizProductionTask)
- .filter(
- and_(
- BizProductionTask.id == source_task_id,
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.del_flag == 0,
- )
- )
- .first()
- )
- if not task:
- raise ValueError("任务不存在")
- inputs = (
- db.query(BizProductionTaskInput)
- .filter(
- and_(
- BizProductionTaskInput.task_id == source_task_id,
- BizProductionTaskInput.del_flag == 0,
- )
- )
- .order_by(BizProductionTaskInput.order_num.asc(), BizProductionTaskInput.create_time.asc())
- .all()
- )
- if not inputs:
- raise ValueError("请先在原材料中填写并保存后再派发临时工单")
- copy_items: List[Dict[str, Any]] = []
- for inp in inputs:
- inp_type = (inp.input_type or "").strip()
- inp_id = (inp.input_id or "").strip()
- inp_code = (inp.input_code or "").strip()
- inp_name = (inp.input_name or "").strip()
- if not inp_type or (not inp_id and not inp_code and not inp_name):
- raise ValueError("原材料数据不完整,请保存后再派发")
- copy_items.append({
- "inputType": inp_type,
- "inputId": inp_id or None,
- "inputCode": inp_code or None,
- "inputName": inp_name or None,
- "remark": (inp.remark or "").strip() or None,
- })
- user_remark = (remark or "").strip()
- full_remark = user_remark[:512] if user_remark else ""
- from app.crud import production_work_order as production_work_order_svc
- new_wo = production_work_order_svc.create_temp_production_work_order(
- db,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=create_dept_id,
- remark=full_remark,
- )
- return {
- "workOrder": production_work_order_svc._temp_wo_to_dict(new_wo),
- "sourceTaskId": task.id,
- "sourceTaskNo": task.task_no or "",
- "inputItems": copy_items,
- }
- def set_task_inputs(
- db: Session, task_id: str, tenant_no: str,
- user_id: Optional[str], items: List[Dict[str, Any]],
- ) -> None:
- now = datetime.now()
- # --- 1. 加载旧 inputs 并按 cell_product 恢复状态 ---
- old_rows = db.query(BizProductionTaskInput).filter(
- and_(
- BizProductionTaskInput.task_id == task_id,
- BizProductionTaskInput.del_flag == 0,
- )
- ).all()
- for old_row in old_rows:
- # 如果是细胞产品,恢复状态
- if old_row.input_type == "cell_product" and old_row.input_id:
- product = db.query(BizCellProduct).filter(
- BizCellProduct.id == old_row.input_id, BizCellProduct.del_flag == 0
- ).first()
- if product and product.product_status == "in_production":
- product.product_status = "frozen"
- product.update_by = user_id
- product.update_time = now
- db.add(product)
- # --- 2. 全量软删除旧行 ---
- _soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
- # --- 3. 按传入顺序批量新建 ---
- for idx, it in enumerate(items):
- inp_type = (it.get("input_type") or it.get("inputType") or "").strip()
- inp_id = (it.get("input_id") or it.get("inputId") or "").strip()
- # 如果是细胞产品,设置状态为 in_production
- if inp_type == "cell_product" and inp_id:
- product = db.query(BizCellProduct).filter(
- BizCellProduct.id == inp_id, BizCellProduct.del_flag == 0
- ).first()
- if product and product.product_status in ("frozen", "thawed"):
- product.product_status = "in_production"
- product.update_by = user_id
- product.update_time = now
- db.add(product)
- db.add(BizProductionTaskInput(
- id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
- input_type=inp_type or None,
- input_id=inp_id or None,
- input_code=(it.get("input_code") or it.get("inputCode") or "").strip() or None,
- input_name=(it.get("input_name") or it.get("inputName") or "").strip() or None,
- remark=(it.get("remark") or "").strip() or None,
- order_num=idx, create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- db.flush()
- db.commit()
- def _parse_optional_date(val):
- """解析可选日期字符串(兼容 YYYY-MM-DD 与 ISO 8601)"""
- if not val:
- return None
- if isinstance(val, datetime):
- return val
- return parse_datetime(str(val).strip())
- def _create_occupancy_for_product(db: Session, product: BizCellProduct, tenant_no: str, user_id: Optional[str]):
- """为细胞产品创建库位占用记录"""
- slot_id = (product.storage_slot_id or "").strip()
- if not slot_id:
- return
- occupancy_svc.assert_slot_vacant(
- db, slot_id, tenant_no=tenant_no, exclude_product_id=product.id,
- )
- occupancy_svc._add_stored_occupancy_no_commit(
- db,
- slot_id=slot_id,
- item_type="cell_product",
- item_id=product.id,
- item_code=product.product_code or "",
- item_name=product.product_name or "",
- item_source="production",
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=product.create_dept_id or "",
- )
- def _output_item_has_spec(it: Dict[str, Any]) -> bool:
- return any(k in it for k in ("spec_text", "specText", "spec_name", "specName"))
- def _output_item_spec_text(it: Dict[str, Any]) -> Optional[str]:
- for key in ("spec_text", "specText", "spec_name", "specName"):
- if key not in it:
- continue
- val = it[key]
- if val is None:
- return None
- s = str(val).strip()
- return s or None
- val = it.get("spec_text") or it.get("specText") or it.get("spec_name") or it.get("specName")
- if val is None:
- return None
- s = str(val).strip()
- return s or None
- def _update_product_from_output(
- db: Session,
- product: BizCellProduct,
- it: Dict[str, Any],
- tenant_no: str,
- user_id,
- now,
- ):
- """从 output item 更新细胞产品字段"""
- new_code = it.get("product_code") if it.get("product_code") is not None else it.get("productCode")
- if new_code is not None:
- code = (str(new_code) or "").strip()
- if code and code != (product.product_code or "").strip():
- if _cp_product_code_exists(db, tenant_no, code, exclude_id=product.id):
- raise ValueError(f"产品编码 {code} 已存在")
- product.product_code = code
- field_map = [
- ("product_name", "product_name", "productName"),
- ("product_type_id", "product_type_id", "productTypeId"),
- ("spec_id", "spec_id", "specId"),
- ("spec_text", "spec_text", "specText"),
- ("batch_no", "batch_no", "batchNo"),
- ("storage_slot_id", "storage_slot_id", "storageSlotId"),
- ("unit", "unit", "unit"),
- ("product_images", "product_images", "productImages"),
- ("remark", "remark", "remark"),
- ]
- for db_field, snake_key, camel_key in field_map:
- val = it.get(snake_key) if it.get(snake_key) is not None else it.get(camel_key)
- if val is not None:
- setattr(product, db_field, (str(val) or "").strip() or None)
- spec_text = _output_item_spec_text(it)
- if _output_item_has_spec(it):
- product.spec_text = spec_text
- for num_field, snake_key, camel_key in [
- ("expected_cell_count", "expected_cell_count", "expectedCellCount"),
- ("viability", "viability", "viability"),
- ]:
- val = it.get(snake_key)
- if val is None:
- val = it.get(camel_key)
- if val is not None:
- setattr(product, num_field, val)
- for date_field, snake_key, camel_key in [
- ("freeze_date", "freeze_date", "freezeDate"),
- ("expiry_date", "expiry_date", "expiryDate"),
- ]:
- val = it.get(snake_key) or it.get(camel_key)
- if val is not None:
- setattr(product, date_field, _parse_optional_date(val))
- passage = it.get("passage")
- if passage is not None:
- product.passage = (str(passage) or "").strip() or None
- quantity = it.get("quantity")
- if quantity is not None:
- product.quantity = quantity
- product.update_by = user_id
- product.update_time = now
- def set_task_outputs(
- db: Session, task_id: str, tenant_no: str,
- user_id: Optional[str], items: List[Dict[str, Any]],
- ) -> None:
- now = datetime.now()
- batch_slot_ids: List[str] = []
- for it in items:
- sid = (it.get("storage_slot_id") or it.get("storageSlotId") or "").strip()
- if sid:
- batch_slot_ids.append(sid)
- occupancy_svc.assert_batch_slots_unique(batch_slot_ids)
- # --- 1. 加载旧 outputs ---
- old_rows = db.query(BizProductionTaskOutput).filter(
- and_(
- BizProductionTaskOutput.task_id == task_id,
- BizProductionTaskOutput.del_flag == 0,
- )
- ).all()
- old_cp_map: Dict[str, BizProductionTaskOutput] = {}
- for o in old_rows:
- if o.cell_product_id:
- old_cp_map[o.cell_product_id] = o
- # --- 2. 构建新的 cp_id 集合 ---
- new_cp_ids: set = set()
- for it in items:
- cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
- if cp_id:
- new_cp_ids.add(cp_id)
- # --- 3. 处理删除(旧有新无) ---
- for cp_id, old_row in old_cp_map.items():
- if cp_id not in new_cp_ids:
- product = db.query(BizCellProduct).filter(
- BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
- ).first()
- if product:
- # 作废并软删:避免同编码再入库时留下 voided+frozen 双行,污染各类选择器
- product.product_status = "voided"
- product.del_flag = 1
- product.update_by = user_id
- product.update_time = now
- db.add(product)
- if product.storage_slot_id:
- occupancy_svc.remove_by_item(db, "cell_product", cp_id, user_id=user_id)
- old_row.del_flag = 1
- old_row.update_by = user_id
- old_row.update_time = now
- db.add(old_row)
- # --- 4. 处理新增和修改 ---
- for idx, it in enumerate(items):
- cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
- if cp_id and cp_id in old_cp_map:
- # === 修改 ===
- product = db.query(BizCellProduct).filter(
- BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
- ).first()
- if product:
- old_slot_id = product.storage_slot_id
- _update_product_from_output(db, product, it, tenant_no, user_id, now)
- new_slot_id = product.storage_slot_id
- db.add(product)
- if old_slot_id != new_slot_id:
- if old_slot_id:
- occupancy_svc.remove_by_item(db, "cell_product", cp_id, user_id=user_id)
- if new_slot_id:
- _create_occupancy_for_product(db, product, tenant_no, user_id)
- old_row = old_cp_map[cp_id]
- old_row.output_name = product.product_name if product else ""
- old_row.output_barcode = product.product_code if product else ""
- old_row.storage_slot_id = product.storage_slot_id if product else None
- old_row.quantity = it.get("quantity")
- old_row.unit = (it.get("unit") or "").strip() or None
- old_row.remark = (it.get("remark") or "").strip() or None
- old_row.order_num = idx
- old_row.update_by = user_id
- old_row.update_time = now
- db.add(old_row)
- else:
- # === 新增 ===
- product = BizCellProduct(
- id=generate_uuid(),
- product_code=(it.get("product_code") or it.get("productCode") or "").strip(),
- product_name=(it.get("product_name") or it.get("productName") or "").strip(),
- product_type_id=(it.get("product_type_id") or it.get("productTypeId") or "").strip() or None,
- spec_id=(it.get("spec_id") or it.get("specId") or "").strip() or None,
- spec_text=_output_item_spec_text(it),
- passage=(it.get("passage") or "").strip() or None,
- batch_no=(it.get("batch_no") or it.get("batchNo") or "").strip() or None,
- storage_slot_id=(it.get("storage_slot_id") or it.get("storageSlotId") or "").strip() or None,
- expected_cell_count=it.get("expected_cell_count") if it.get("expected_cell_count") is not None else it.get("expectedCellCount"),
- viability=it.get("viability"),
- freeze_date=_parse_optional_date(it.get("freeze_date") or it.get("freezeDate")),
- product_status=it.get("product_status") or it.get("productStatus") or "frozen",
- source_type="production",
- production_task_id=task_id,
- quantity=it.get("quantity"),
- unit=(it.get("unit") or "").strip() or None,
- expiry_date=_parse_optional_date(it.get("expiry_date") or it.get("expiryDate")),
- product_images=(it.get("product_images") or it.get("productImages") or "").strip() or None,
- tenant_no=tenant_no,
- order_num=idx,
- remark=(it.get("remark") or "").strip() or None,
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(product)
- db.flush()
- if product.storage_slot_id:
- _create_occupancy_for_product(db, product, tenant_no, user_id)
- output_row = BizProductionTaskOutput(
- id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
- cell_product_id=product.id,
- storage_slot_id=product.storage_slot_id,
- output_name=product.product_name or "",
- output_barcode=product.product_code or "",
- quantity=product.quantity,
- unit=product.unit,
- remark=product.remark,
- attachments=(it.get("attachments") or "").strip() or None,
- order_num=idx, create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- )
- db.add(output_row)
- db.flush()
- db.commit()
- def reorder_task_steps(db: Session, task_id: str, step_ids: List[str]) -> None:
- now = datetime.now()
- for idx, sid in enumerate(step_ids):
- step = db.query(BizProductionTaskStep).filter(
- and_(BizProductionTaskStep.id == sid, BizProductionTaskStep.task_id == task_id,
- BizProductionTaskStep.del_flag == 0)
- ).first()
- if step:
- step.step_order = idx + 1
- step.update_time = now
- db.add(step)
- db.commit()
- def copy_task_step(db: Session, step_id: str, *, tenant_no: str = "zkxw",
- user_id: Optional[str] = None) -> Dict[str, Any]:
- """复制生产任务步骤,插入到原步骤之后,名称追加(复制)"""
- src = db.query(BizProductionTaskStep).filter(
- and_(BizProductionTaskStep.id == step_id, BizProductionTaskStep.del_flag == 0)
- ).first()
- if not src:
- raise ValueError("步骤不存在")
- now = datetime.now()
- new_order = src.step_order + 1
- # 后续步骤 step_order +1
- later_steps = db.query(BizProductionTaskStep).filter(
- and_(
- BizProductionTaskStep.task_id == src.task_id,
- BizProductionTaskStep.del_flag == 0,
- BizProductionTaskStep.step_order > src.step_order,
- )
- ).order_by(BizProductionTaskStep.step_order.asc()).all()
- for ls in later_steps:
- ls.step_order += 1
- ls.update_time = now
- db.add(ls)
- # 创建新步骤
- new_name = (src.process_name or "").strip()
- if not new_name.endswith("(复制)"):
- new_name += "(复制)"
- new_step = BizProductionTaskStep(
- id=generate_uuid(), task_id=src.task_id,
- step_order=new_order,
- process_code=src.process_code, category_id=src.category_id,
- process_name=new_name, description=src.description,
- estimated_duration=src.estimated_duration,
- actual_duration=src.actual_duration, sop_ids=src.sop_ids,
- sop_content=src.sop_content,
- step_status="dzx",
- tenant_no=tenant_no, order_num=0,
- remark=src.remark, create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- )
- db.add(new_step)
- db.flush()
- # 复制子表:试剂(从生产任务步骤试剂表复制)
- reagent_rows = db.query(BizProductionTaskStepReagent).filter(
- and_(BizProductionTaskStepReagent.step_id == step_id, BizProductionTaskStepReagent.del_flag == 0)
- ).order_by(
- BizProductionTaskStepReagent.order_num.asc(),
- BizProductionTaskStepReagent.create_time.asc(),
- ).all()
- for idx, r in enumerate(reagent_rows):
- db.add(BizProductionTaskStepReagent(
- id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
- reagent_code=r.reagent_code, reagent_name=r.reagent_name,
- spec=r.spec, standard_usage=r.standard_usage,
- actual_usage=r.actual_usage,
- unit=r.unit, is_key_material=r.is_key_material,
- remark=r.remark,
- order_num=_resolve_sub_table_order_num(r.order_num, idx, reagent_rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- # 复制子表:耗材
- consumable_rows = db.query(BizProductionTaskStepConsumable).filter(
- and_(BizProductionTaskStepConsumable.step_id == step_id, BizProductionTaskStepConsumable.del_flag == 0)
- ).order_by(
- BizProductionTaskStepConsumable.order_num.asc(),
- BizProductionTaskStepConsumable.create_time.asc(),
- ).all()
- for idx, c in enumerate(consumable_rows):
- db.add(BizProductionTaskStepConsumable(
- id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
- consumable_code=c.consumable_code, consumable_name=c.consumable_name,
- spec_model=c.spec_model, standard_usage=c.standard_usage,
- actual_usage=c.actual_usage,
- unit=c.unit, is_required=c.is_required,
- remark=c.remark,
- order_num=_resolve_sub_table_order_num(c.order_num, idx, consumable_rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- # 复制子表:产出
- output_rows = db.query(BizProductionTaskStepOutput).filter(
- and_(BizProductionTaskStepOutput.step_id == step_id, BizProductionTaskStepOutput.del_flag == 0)
- ).order_by(
- BizProductionTaskStepOutput.order_num.asc(),
- BizProductionTaskStepOutput.create_time.asc(),
- ).all()
- for idx, o in enumerate(output_rows):
- db.add(BizProductionTaskStepOutput(
- id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
- output_type_id=o.output_type_id, output_name=o.output_name,
- output_barcode=o.output_barcode,
- remark=o.remark,
- order_num=_resolve_sub_table_order_num(o.order_num, idx, output_rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- # 复制子表:数据采集
- collection_rows = db.query(BizProductionTaskStepDataCollection).filter(
- and_(BizProductionTaskStepDataCollection.step_id == step_id, BizProductionTaskStepDataCollection.del_flag == 0)
- ).order_by(
- BizProductionTaskStepDataCollection.order_num.asc(),
- BizProductionTaskStepDataCollection.create_time.asc(),
- ).all()
- for idx, d in enumerate(collection_rows):
- db.add(BizProductionTaskStepDataCollection(
- id=generate_uuid(), step_id=new_step.id, tenant_no=tenant_no,
- collection_name=d.collection_name, unit=d.unit,
- collection_value=d.collection_value,
- remark=d.remark,
- order_num=_resolve_sub_table_order_num(d.order_num, idx, collection_rows),
- create_by=user_id, create_dept_id="",
- create_time=now, update_by=user_id, update_time=now, del_flag=0,
- ))
- db.commit()
- result = _step_to_dict(new_step)
- result.update(_step_sub_tables_to_dict(db, new_step.id))
- return result
- def delete_production_task(db: Session, task_id: str, tenant_no: str, user_id: Optional[str]) -> None:
- """软删除生产任务及其所有子表数据(步骤 + 投入 + 产出)"""
- task = db.query(BizProductionTask).filter(
- and_(
- BizProductionTask.id == task_id,
- BizProductionTask.tenant_no == tenant_no,
- BizProductionTask.del_flag == 0,
- )
- ).first()
- if not task:
- raise ValueError("任务不存在")
- now = datetime.now()
- # 1. 软删除所有步骤及其子表
- steps = db.query(BizProductionTaskStep).filter(
- and_(BizProductionTaskStep.task_id == task_id, BizProductionTaskStep.del_flag == 0)
- ).all()
- for step in steps:
- step.del_flag = 1
- step.update_by = user_id
- step.update_time = now
- db.add(step)
- _soft_delete_sub(db, BizProductionTaskStepReagent, "step_id", step.id, now)
- _soft_delete_sub(db, BizProductionTaskStepConsumable, "step_id", step.id, now)
- _soft_delete_sub(db, BizProductionTaskStepOutput, "step_id", step.id, now)
- _soft_delete_sub(db, BizProductionTaskStepDataCollection, "step_id", step.id, now)
- # 2. 软删除投入
- _soft_delete_sub(db, BizProductionTaskInput, "task_id", task_id, now)
- # 3. 软删除产出
- _soft_delete_sub(db, BizProductionTaskOutput, "task_id", task_id, now)
- # 4. 软删除任务本体
- task.del_flag = 1
- task.update_by = user_id
- task.update_time = now
- db.add(task)
- work_order_id = task.work_order_id or ""
- from app.crud import production_work_order as production_work_order_svc
- production_work_order_svc.sync_production_work_order_status_from_tasks(
- db, work_order_id, tenant_no, user_id
- )
- db.commit()
- def total_pages(total: int, page_size: int) -> int:
- if total <= 0:
- return 0
- return int(math.ceil(total / float(page_size)))
- _PRODUCTION_TAB_STEP_COUNT_KEYS = (
- "inbound_cell_storage",
- "inbound_research_donation",
- "outbound_pilot_service",
- "outbound_basic_experiment",
- "temporary",
- )
- def _empty_production_tab_step_counts() -> Dict[str, Dict[str, int]]:
- return {
- k: {"overdueIncomplete": 0, "futureIncomplete": 0}
- for k in _PRODUCTION_TAB_STEP_COUNT_KEYS
- }
- def _resolve_production_tab_key(
- wo_type: Optional[str],
- reception_source: Optional[str],
- biz_type: Optional[str],
- ) -> Optional[str]:
- wt = (wo_type or "inbound").strip() or "inbound"
- if wt == "temporary":
- return "temporary"
- if wt == "outbound":
- bt = (biz_type or "pilot_service").strip() or "pilot_service"
- if bt == "basic_experiment":
- return "outbound_basic_experiment"
- if bt == "clinical_application":
- return None
- return "outbound_pilot_service"
- rs = (reception_source or "cell_storage").strip() or "cell_storage"
- return "inbound_research_donation" if rs == "research_donation" else "inbound_cell_storage"
- def _aggregate_production_tab_step_field(
- counts: Dict[str, Dict[str, int]],
- rows,
- field: str,
- ) -> None:
- for wo_type, rs, bt, cnt in rows:
- tab_key = _resolve_production_tab_key(wo_type, rs, bt)
- if not tab_key or tab_key not in counts:
- continue
- counts[tab_key][field] += int(cnt or 0)
- def _base_active_step_count_query(
- db: Session,
- *,
- tenant_no: str,
- user_info=None,
- mine_user_id: Optional[str] = None,
- ):
- q = (
- db.query(
- BizProductionWorkOrder.work_order_type,
- BizProductionWorkOrder.reception_source,
- BizProductionWorkOrder.biz_type,
- func.count(BizProductionTaskStep.id),
- )
- .select_from(BizProductionTaskStep)
- .join(
- BizProductionTask,
- and_(
- BizProductionTask.id == BizProductionTaskStep.task_id,
- BizProductionTask.del_flag == 0,
- BizProductionTask.tenant_no == tenant_no,
- ),
- )
- .join(
- BizProductionWorkOrder,
- and_(
- BizProductionWorkOrder.id == BizProductionTask.work_order_id,
- BizProductionWorkOrder.del_flag == 0,
- BizProductionWorkOrder.tenant_no == tenant_no,
- ),
- )
- .filter(
- BizProductionTaskStep.del_flag == 0,
- BizProductionTaskStep.tenant_no == tenant_no,
- BizProductionWorkOrder.work_order_status.in_(("dpz", "jxz")),
- BizProductionTask.task_status.in_(("dpz", "jxz")),
- BizProductionTaskStep.step_status != "ywc",
- )
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizProductionWorkOrder, user_info, db, tenant_no)
- uid = (mine_user_id or "").strip()
- if uid:
- step_task_ids = (
- db.query(BizProductionTaskStep.task_id)
- .join(
- BizProductionTaskStepAssignee,
- BizProductionTaskStepAssignee.step_id == BizProductionTaskStep.id,
- )
- .filter(
- BizProductionTaskStepAssignee.user_id == uid,
- BizProductionTaskStepAssignee.tenant_no == tenant_no,
- BizProductionTaskStepAssignee.del_flag == 0,
- BizProductionTaskStep.del_flag == 0,
- )
- .distinct()
- .subquery()
- )
- q = q.filter(
- or_(
- BizProductionTask.id.in_(step_task_ids),
- BizProductionTask.current_assignee_user_id == uid,
- )
- )
- from app.crud.production_work_order import apply_exclude_inactive_protocol_filter
- q = apply_exclude_inactive_protocol_filter(q, exclude=True)
- return q
- def count_step_tab_badges_by_tab(
- db: Session,
- *,
- tenant_no: str,
- user_info=None,
- mine_user_id: Optional[str] = None,
- ) -> Dict[str, Dict[str, int]]:
- """按 Tab 统计工艺步骤角标:计划开始已到期未完成数 + 计划开始在今日之后未完成数。"""
- counts = _empty_production_tab_step_counts()
- today_end = datetime.now().replace(hour=23, minute=59, second=59, microsecond=0)
- overdue_rows = (
- _base_active_step_count_query(
- db, tenant_no=tenant_no, user_info=user_info, mine_user_id=mine_user_id
- )
- .filter(
- BizProductionTaskStep.planned_start_time.isnot(None),
- BizProductionTaskStep.planned_start_time <= today_end,
- )
- .group_by(
- BizProductionWorkOrder.work_order_type,
- BizProductionWorkOrder.reception_source,
- BizProductionWorkOrder.biz_type,
- )
- .all()
- )
- _aggregate_production_tab_step_field(counts, overdue_rows, "overdueIncomplete")
- future_rows = (
- _base_active_step_count_query(
- db, tenant_no=tenant_no, user_info=user_info, mine_user_id=mine_user_id
- )
- .filter(
- BizProductionTaskStep.planned_start_time.isnot(None),
- BizProductionTaskStep.planned_start_time > today_end,
- )
- .group_by(
- BizProductionWorkOrder.work_order_type,
- BizProductionWorkOrder.reception_source,
- BizProductionWorkOrder.biz_type,
- )
- .all()
- )
- _aggregate_production_tab_step_field(counts, future_rows, "futureIncomplete")
- return counts
- def count_production_work_order_tab_step_badges(
- db: Session, *, tenant_no: str, user_info=None
- ) -> Dict[str, Dict[str, int]]:
- return count_step_tab_badges_by_tab(db, tenant_no=tenant_no, user_info=user_info)
- def count_mine_production_work_order_tab_step_badges(
- db: Session, *, tenant_no: str, assignee_user_id: str
- ) -> Dict[str, Dict[str, int]]:
- return count_step_tab_badges_by_tab(
- db, tenant_no=tenant_no, mine_user_id=assignee_user_id
- )
|