| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857 |
- """中试申请单 CRUD(申请单、患者信息、输注方案、输注计划)"""
- from __future__ import annotations
- import uuid
- from datetime import datetime
- from typing import Any, Dict, List, Optional
- from sqlalchemy import and_, func, or_
- from sqlalchemy.orm import Session
- from app.models.models import (
- BizChannel,
- BizChannelServiceOrder,
- BizCustomer,
- BizPilotApplication,
- BizPilotDelivery,
- BizPilotInfusionConfirm,
- BizPilotInfusionPlan,
- BizPilotInfusionSchedule,
- BizPilotInfusionScheduleProduct,
- BizPilotPatient,
- BizProductionWorkOrder,
- BizQcWorkOrder,
- )
- from app.crud import pilot_checkup
- from app.crud.collection_protocol import attachments_to_urls, urls_to_attachment_list
- # ────────────────────────────────────────────
- # Helpers: _row_to_dict
- # ────────────────────────────────────────────
- def _normalize_yyyymmdd(value: Any) -> str:
- if value is None:
- return ""
- s = str(value).strip().replace("-", "").replace("/", "")
- return s[:8] if len(s) >= 8 else s
- def _format_yyyy_mm_dd(value: Any) -> str:
- s = _normalize_yyyymmdd(value)
- if len(s) == 8:
- return f"{s[:4]}-{s[4:6]}-{s[6:8]}"
- return str(value or "").strip()
- def _parse_infusion_datetime(value: Any) -> Optional[datetime]:
- if value is None or value == "":
- return None
- if isinstance(value, datetime):
- return value
- s = str(value).strip()
- for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
- try:
- return datetime.strptime(s[:19] if fmt.endswith("%S") else s[:10], fmt)
- except ValueError:
- continue
- compact = _normalize_yyyymmdd(s)
- if len(compact) == 8:
- try:
- return datetime.strptime(compact, "%Y%m%d")
- except ValueError:
- return None
- return None
- def _release_application_no_for_soft_delete(application_no: str, row_id: str) -> str:
- """软删除时改写申请单号,避免 uk_application_no(application_no, del_flag) 与历史已删记录冲突。"""
- base = (application_no or "").strip()
- suffix = f"~{row_id.replace('-', '')[:20]}"
- max_base_len = max(0, 64 - len(suffix))
- return f"{base[:max_base_len]}{suffix}"
- def _generate_application_no(db: Session, tenant_no: str, biz_type: str = "pilot_service") -> str:
- today_str = datetime.now().strftime("%Y%m%d")
- if biz_type == "clinical_application":
- prefix = f"LCSQ{today_str}"
- elif biz_type == "basic_experiment":
- prefix = f"JCSY{today_str}"
- else:
- prefix = f"ZSSQ{today_str}"
- existing = (
- db.query(BizPilotApplication.application_no)
- .filter(
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.application_no.like(f"{prefix}%"),
- BizPilotApplication.del_flag == 0,
- )
- .order_by(BizPilotApplication.application_no.desc())
- .first()
- )
- seq = 1
- if existing and existing[0]:
- try:
- seq = int(existing[0][-4:]) + 1
- except ValueError:
- seq = 1
- return f"{prefix}{seq:04d}"
- def _resolve_application_attachment_payload(raw: dict) -> None:
- """解析保存载荷:附件列表 → URL 字段。"""
- if raw.get("consentAttachments") is not None:
- raw["consentFormUrls"] = attachments_to_urls(raw["consentAttachments"]) or None
- elif (raw.get("consentFormUrls") or "").strip():
- raw["consentFormUrls"] = (raw["consentFormUrls"] or "").strip()
- if raw.get("otherAttachments") is not None:
- raw["attachmentUrls"] = attachments_to_urls(raw["otherAttachments"]) or None
- elif (raw.get("attachmentUrls") or "").strip():
- raw["attachmentUrls"] = (raw["attachmentUrls"] or "").strip()
- def _row_to_dict_application(row, db=None, tenant_no=None) -> Dict[str, Any]:
- tn = tenant_no or row.tenant_no
- d: Dict[str, Any] = {
- "id": row.id,
- "applicationNo": row.application_no or "",
- "applicationName": row.application_name or "",
- "projectTitle": row.project_title or "",
- "customerName": row.customer_name or "",
- "customerContact": row.customer_contact or "",
- "customerPhone": row.customer_phone or "",
- "applicationStatus": row.application_status or "",
- "bizType": row.biz_type or "pilot_service",
- "channelServiceOrderId": row.channel_service_order_id or "",
- "channelId": row.channel_id or "",
- "channelName": row.channel_name or "",
- "tenantNo": row.tenant_no or "",
- "remark": row.remark or "",
- "consentFormUrls": row.consent_form_urls or "",
- "attachmentUrls": row.attachment_urls or "",
- "consentAttachmentFiles": urls_to_attachment_list(row.consent_form_urls, db, tn) if db else [],
- "attachmentFiles": urls_to_attachment_list(row.attachment_urls, db, tn) if db else [],
- "createBy": row.create_by or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- if db and tenant_no:
- # patientName
- patient = (
- db.query(BizPilotPatient.patient_name)
- .filter(
- BizPilotPatient.pilot_application_id == row.id,
- BizPilotPatient.tenant_no == tenant_no,
- BizPilotPatient.del_flag == 0,
- )
- .first()
- )
- d["patientName"] = patient.patient_name if patient else ""
- # infusionScheduleCount:该申请单下全部输注计划条数(跨输注方案汇总)
- schedule_count = (
- db.query(func.count(BizPilotInfusionSchedule.id))
- .join(
- BizPilotInfusionPlan,
- and_(
- BizPilotInfusionSchedule.infusion_plan_id == BizPilotInfusionPlan.id,
- BizPilotInfusionPlan.del_flag == 0,
- ),
- )
- .filter(
- BizPilotInfusionPlan.pilot_application_id == row.id,
- BizPilotInfusionPlan.tenant_no == tenant_no,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .scalar()
- ) or 0
- d["infusionScheduleCount"] = schedule_count
- # serviceOrderNo + orderType
- svc_order = (
- db.query(BizChannelServiceOrder)
- .filter(BizChannelServiceOrder.id == row.channel_service_order_id)
- .first()
- )
- if svc_order:
- name = (svc_order.order_name or "").strip()
- if name:
- d["serviceOrderNo"] = name
- else:
- oid = svc_order.id or ""
- d["serviceOrderNo"] = oid[-8:] if len(oid) >= 8 else oid
- d["orderType"] = svc_order.order_type or ""
- else:
- d["serviceOrderNo"] = ""
- d["orderType"] = ""
- return d
- def _row_to_dict_patient(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "pilotApplicationId": row.pilot_application_id or "",
- "customerId": row.customer_id or "",
- "patientName": row.patient_name or "",
- "sex": row.sex or "",
- "age": row.age if row.age is not None else None,
- "idCardNo": row.id_card_no or "",
- "phoneNo": row.phone_no or "",
- "emergencyContact": row.emergency_contact or "",
- "emergencyPhone": row.emergency_phone or "",
- "clinicalDiagnosis": row.clinical_diagnosis or "",
- "diagnosisAttachments": row.diagnosis_attachments or "",
- "needCheckup": row.need_checkup if row.need_checkup is not None else 1,
- "tenantNo": row.tenant_no or "",
- "remark": row.remark or "",
- "createBy": row.create_by or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def _row_to_dict_infusion_plan(row, db=None, tenant_no=None) -> Dict[str, Any]:
- d: Dict[str, Any] = {
- "id": row.id,
- "pilotApplicationId": row.pilot_application_id or "",
- "cellType": row.cell_type or "",
- "specText": row.spec_text or "",
- "totalCourseCount": row.total_course_count if row.total_course_count is not None else 0,
- "courseInterval": row.course_interval or "",
- "infusionRoute": row.infusion_route or "",
- "infusionStartDate": _format_yyyy_mm_dd(row.infusion_start_date) if row.infusion_start_date else "",
- "channelAgreementId": row.channel_agreement_id or "",
- "tenantNo": row.tenant_no or "",
- "remark": row.remark or "",
- "createBy": row.create_by or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- if db and tenant_no:
- # scheduleCount (all schedules)
- schedule_count = (
- db.query(func.count(BizPilotInfusionSchedule.id))
- .filter(
- BizPilotInfusionSchedule.infusion_plan_id == row.id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .scalar()
- ) or 0
- d["scheduleCount"] = schedule_count
- # completedCount (schedule_status='ywc')
- completed_count = (
- db.query(func.count(BizPilotInfusionSchedule.id))
- .filter(
- BizPilotInfusionSchedule.infusion_plan_id == row.id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- BizPilotInfusionSchedule.schedule_status == "ywc",
- )
- .scalar()
- ) or 0
- d["completedCount"] = completed_count
- return d
- def _row_to_dict_schedule_product(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "infusionScheduleId": row.infusion_schedule_id or "",
- "priceId": row.price_id or "",
- "projectCode": row.project_code or "",
- "projectName": row.project_name or "",
- "cellTypeName": row.cell_type_name or "",
- "specText": row.spec_text or "",
- "agreementPrice": float(row.agreement_price) if row.agreement_price is not None else None,
- "actualPrice": float(row.actual_price) if row.actual_price is not None else None,
- "quantity": row.quantity if row.quantity is not None else 1,
- "status": row.product_status or "",
- "remark": row.remark or "",
- }
- def _row_to_dict_infusion_schedule(row, cell_products: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
- infusion_time = ""
- if row.infusion_time:
- infusion_time = row.infusion_time.strftime("%Y-%m-%d")
- return {
- "id": row.id,
- "infusionPlanId": row.infusion_plan_id or "",
- "cellType": row.cell_type or "",
- "specText": row.spec_text or "",
- "infusionTime": infusion_time,
- "infusionLocation": row.infusion_location or "",
- "infusionMethod": row.infusion_method or "",
- "scheduleStatus": row.schedule_status or "",
- "infusionStatus": row.infusion_status or "dtz",
- "cancelReason": row.cancel_reason or "",
- "qcStatus": row.qc_status or "dzc",
- "cellProducts": cell_products if cell_products is not None else [],
- "attachments": row.attachments or "",
- "amount": float(row.amount) if row.amount else None,
- "tenantNo": row.tenant_no or "",
- "remark": row.remark or "",
- "createBy": row.create_by or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- # ────────────────────────────────────────────
- # Application CRUD
- # ────────────────────────────────────────────
- def list_applications(
- db: Session,
- tenant_no: str,
- page: int = 1,
- page_size: int = 10,
- channel_id: Optional[str] = None,
- application_status: Optional[str] = None,
- channel_service_order_id: Optional[str] = None,
- keyword: Optional[str] = None,
- biz_type: Optional[str] = None,
- ):
- q = db.query(BizPilotApplication).filter(
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.del_flag == 0,
- )
- if channel_id:
- q = q.filter(BizPilotApplication.channel_id == channel_id)
- if application_status:
- statuses = [s.strip() for s in application_status.split(",") if s.strip()]
- if statuses:
- q = q.filter(BizPilotApplication.application_status.in_(statuses))
- if channel_service_order_id:
- q = q.filter(BizPilotApplication.channel_service_order_id == channel_service_order_id)
- if biz_type:
- q = q.filter(BizPilotApplication.biz_type == biz_type)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- patient_app_ids = (
- db.query(BizPilotPatient.pilot_application_id)
- .filter(
- BizPilotPatient.tenant_no == tenant_no,
- BizPilotPatient.del_flag == 0,
- BizPilotPatient.patient_name.like(like),
- )
- .distinct()
- )
- order_ids = (
- db.query(BizChannelServiceOrder.id)
- .filter(
- BizChannelServiceOrder.tenant_no == tenant_no,
- BizChannelServiceOrder.del_flag == 0,
- BizChannelServiceOrder.order_name.like(like),
- )
- .distinct()
- )
- q = q.filter(
- or_(
- BizPilotApplication.application_no.like(like),
- BizPilotApplication.application_name.like(like),
- BizPilotApplication.channel_name.like(like),
- BizPilotApplication.id.in_(patient_app_ids),
- BizPilotApplication.channel_service_order_id.in_(order_ids),
- )
- )
- total = q.order_by(None).with_entities(func.count(BizPilotApplication.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = (
- q.order_by(BizPilotApplication.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- items = [_row_to_dict_application(r, db, tenant_no) for r in rows]
- return {
- "list": items,
- "pagination": {"page": page, "pageSize": page_size, "total": total},
- }
- def get_application(db: Session, app_id: str, tenant_no: str) -> Optional[Dict[str, Any]]:
- row = (
- db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id == app_id,
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.del_flag == 0,
- )
- .first()
- )
- if not row:
- return None
- result = _row_to_dict_application(row, db, tenant_no)
- plans = list_infusion_plans(db, app_id, tenant_no)
- if plans:
- plan = plans[0]
- result["infusionPlan"] = {
- "id": plan.get("id"),
- "totalCourseCount": plan.get("totalCourseCount"),
- "courseInterval": plan.get("courseInterval"),
- "infusionStartDate": plan.get("infusionStartDate"),
- "channelAgreementId": plan.get("channelAgreementId"),
- }
- plan_id = plan.get("id")
- if plan_id:
- result["infusionSchedules"] = list_schedules_by_plan(db, plan_id, tenant_no)
- if row.biz_type in ("clinical_application", "basic_experiment") and result["infusionSchedules"]:
- from app.crud import clinical_payment
- payment_map = clinical_payment.map_payments_by_schedule_id(
- db, application_id=app_id, tenant_no=tenant_no
- )
- for schedule in result["infusionSchedules"]:
- schedule_id = schedule.get("id")
- if schedule_id and schedule_id in payment_map:
- schedule["payment"] = payment_map[schedule_id]
- else:
- result["infusionPlan"] = None
- result["infusionSchedules"] = []
- return result
- def create_application(
- db: Session,
- data: dict,
- tenant_no: str,
- user_id: str,
- dept_id: str,
- ) -> Dict[str, Any]:
- channel_service_order_id = data.get("channelServiceOrderId") or ""
- biz_type = data.get("bizType") or "pilot_service"
- channel_id = (data.get("channelId") or "").strip()
- channel_name = (data.get("channelName") or "").strip()
- project_title = ""
- customer_name = ""
- customer_contact = ""
- customer_phone = ""
- consent_form_urls = data.get("consentFormUrls") or None
- if biz_type == "basic_experiment":
- if not (data.get("projectTitle") or "").strip():
- raise ValueError("请填写课题名称")
- if not (data.get("customerName") or "").strip():
- raise ValueError("请填写客户名称")
- if not (data.get("customerContact") or "").strip():
- raise ValueError("请填写客户联系人")
- if not (data.get("customerPhone") or "").strip():
- raise ValueError("请填写客户联系方式")
- project_title = (data.get("projectTitle") or "").strip()
- customer_name = (data.get("customerName") or "").strip()
- customer_contact = (data.get("customerContact") or "").strip()
- customer_phone = (data.get("customerPhone") or "").strip()
- channel_service_order_id = ""
- channel_id = ""
- channel_name = ""
- consent_form_urls = None
- elif not channel_service_order_id and biz_type != "clinical_application":
- raise ValueError("请选择渠道服务订单")
- # Validate service order exists and is active
- svc_order = None
- if channel_service_order_id:
- svc_order = (
- db.query(BizChannelServiceOrder)
- .filter(
- BizChannelServiceOrder.id == channel_service_order_id,
- BizChannelServiceOrder.del_flag == 0,
- )
- .first()
- )
- if not svc_order:
- raise ValueError("渠道服务订单不存在")
- if svc_order.order_status != "sxz":
- raise ValueError("渠道服务订单未生效,无法创建中试申请")
- if biz_type == "clinical_application":
- if svc_order:
- channel_id = svc_order.channel_id or channel_id
- channel_name = svc_order.channel_name or channel_name
- if not channel_id:
- raise ValueError("请选择渠道")
- if not channel_name:
- ch = (
- db.query(BizChannel)
- .filter(
- BizChannel.id == channel_id,
- BizChannel.tenant_no == tenant_no,
- BizChannel.del_flag == 0,
- )
- .first()
- )
- if not ch:
- raise ValueError("渠道不存在")
- channel_name = ch.channel_name or ""
- _resolve_application_attachment_payload(data)
- now = datetime.now()
- application_no = _generate_application_no(db, tenant_no, biz_type=biz_type)
- row = BizPilotApplication(
- id=str(uuid.uuid4()),
- application_no=application_no,
- application_name=data.get("applicationName") or "",
- project_title=project_title or None,
- customer_name=customer_name or None,
- customer_contact=customer_contact or None,
- customer_phone=customer_phone or None,
- application_status="dks",
- channel_service_order_id=channel_service_order_id or "",
- channel_id=(
- (svc_order.channel_id or "" if svc_order else "")
- or channel_id
- ),
- channel_name=(
- (svc_order.channel_name or "" if svc_order else "")
- or channel_name
- ),
- tenant_no=tenant_no,
- order_num=0,
- remark=data.get("remark") or "",
- consent_form_urls=consent_form_urls,
- attachment_urls=data.get("attachmentUrls") or None,
- create_by=user_id,
- create_dept_id=dept_id or "",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- biz_type=biz_type,
- )
- db.add(row)
- db.commit()
- db.refresh(row)
- return _row_to_dict_application(row, db, tenant_no)
- def update_application(db: Session, app_id: str, data: dict, tenant_no: str) -> Dict[str, Any]:
- row = (
- db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id == app_id,
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("申请单不存在")
- if row.application_status not in ("dks", "jxz"):
- raise ValueError("当前状态不允许编辑")
- _resolve_application_attachment_payload(data)
- if "remark" in data and data["remark"] is not None:
- row.remark = data["remark"]
- if "applicationName" in data and data["applicationName"] is not None:
- row.application_name = data["applicationName"]
- if "projectTitle" in data and data["projectTitle"] is not None:
- row.project_title = (data["projectTitle"] or "").strip() or None
- if "customerName" in data and data["customerName"] is not None:
- row.customer_name = (data["customerName"] or "").strip() or None
- if "customerContact" in data and data["customerContact"] is not None:
- row.customer_contact = (data["customerContact"] or "").strip() or None
- if "customerPhone" in data and data["customerPhone"] is not None:
- row.customer_phone = (data["customerPhone"] or "").strip() or None
- if "consentFormUrls" in data:
- row.consent_form_urls = data.get("consentFormUrls") or None
- if "attachmentUrls" in data:
- row.attachment_urls = data.get("attachmentUrls") or None
- db.commit()
- db.refresh(row)
- return _row_to_dict_application(row, db, tenant_no)
- def soft_delete_application(db: Session, app_id: str, tenant_no: str) -> None:
- row = (
- db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id == app_id,
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("申请单不存在")
- if row.application_status != "dks":
- raise ValueError("当前状态不允许删除")
- row.application_no = _release_application_no_for_soft_delete(row.application_no or "", row.id)
- row.del_flag = 1
- row.update_time = datetime.now()
- db.commit()
- def start_application(db: Session, app_id: str, tenant_no: str, user_id: str) -> Dict[str, Any]:
- row = (
- db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id == app_id,
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("申请单不存在")
- if row.application_status != "dks":
- raise ValueError("当前状态不允许开始")
- row.application_status = "jxz"
- row.update_by = user_id
- row.update_time = datetime.now()
- db.commit()
- db.refresh(row)
- return _row_to_dict_application(row, db, tenant_no)
- def complete_application(db: Session, app_id: str, tenant_no: str, user_id: str) -> Dict[str, Any]:
- row = (
- db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id == app_id,
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("申请单不存在")
- if row.application_status != "jxz":
- raise ValueError("当前状态不允许完成")
- row.application_status = "ywc"
- row.update_by = user_id
- row.update_time = datetime.now()
- db.commit()
- db.refresh(row)
- return _row_to_dict_application(row, db, tenant_no)
- def void_application(db: Session, app_id: str, tenant_no: str, user_id: str) -> Dict[str, Any]:
- row = (
- db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id == app_id,
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("申请单不存在")
- if row.application_status not in ("dks", "jxz"):
- raise ValueError("当前状态不允许作废")
- row.application_status = "yzf"
- row.update_by = user_id
- row.update_time = datetime.now()
- db.commit()
- db.refresh(row)
- return _row_to_dict_application(row, db, tenant_no)
- # ────────────────────────────────────────────
- # Patient CRUD
- # ────────────────────────────────────────────
- def get_patient_by_application(
- db: Session, app_id: str, tenant_no: str
- ) -> Optional[Dict[str, Any]]:
- row = (
- db.query(BizPilotPatient)
- .filter(
- BizPilotPatient.pilot_application_id == app_id,
- BizPilotPatient.tenant_no == tenant_no,
- BizPilotPatient.del_flag == 0,
- )
- .first()
- )
- if not row:
- return None
- return _row_to_dict_patient(row)
- def save_patient(
- db: Session,
- app_id: str,
- data: dict,
- tenant_no: str,
- user_id: str,
- dept_id: str,
- ) -> Dict[str, Any]:
- now = datetime.now()
- customer_id = data.get("customerId") or ""
- if customer_id:
- customer = (
- db.query(BizCustomer)
- .filter(BizCustomer.id == customer_id, BizCustomer.del_flag == 0)
- .first()
- )
- if customer:
- if data.get("patientName"):
- customer.customer_name = data["patientName"]
- if data.get("sex"):
- customer.sex = data["sex"]
- if data.get("age") is not None:
- customer.age = data["age"]
- if data.get("idCardNo"):
- customer.id_card_no = data["idCardNo"]
- if data.get("phoneNo"):
- customer.phone_no = data["phoneNo"]
- customer.update_by = user_id
- customer.update_time = now
- else:
- customer_id = str(uuid.uuid4())
- customer = BizCustomer(
- id=customer_id,
- customer_name=data.get("patientName") or "",
- sex=data.get("sex") or "2",
- id_card_no=data.get("idCardNo") or "",
- age=data.get("age"),
- phone_no=data.get("phoneNo") or "",
- tenant_no=tenant_no,
- order_num=0,
- remark="",
- create_by=user_id,
- create_dept_id=dept_id or "",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(customer)
- db.flush()
- existing = (
- db.query(BizPilotPatient)
- .filter(
- BizPilotPatient.pilot_application_id == app_id,
- BizPilotPatient.tenant_no == tenant_no,
- BizPilotPatient.del_flag == 0,
- )
- .first()
- )
- if existing:
- existing.customer_id = customer_id
- existing.patient_name = data.get("patientName") or ""
- existing.sex = data.get("sex") or ""
- existing.age = data.get("age")
- existing.id_card_no = data.get("idCardNo") or ""
- existing.phone_no = data.get("phoneNo") or ""
- existing.emergency_contact = data.get("emergencyContact") or ""
- existing.emergency_phone = data.get("emergencyPhone") or ""
- existing.clinical_diagnosis = data.get("clinicalDiagnosis") or ""
- existing.diagnosis_attachments = data.get("diagnosisAttachments") or ""
- existing.need_checkup = data.get("needCheckup") if data.get("needCheckup") is not None else 1
- existing.remark = data.get("remark") or ""
- existing.update_by = user_id
- existing.update_time = now
- else:
- existing = BizPilotPatient(
- id=str(uuid.uuid4()),
- pilot_application_id=app_id,
- customer_id=customer_id,
- patient_name=data.get("patientName") or "",
- sex=data.get("sex") or "",
- age=data.get("age"),
- id_card_no=data.get("idCardNo") or "",
- phone_no=data.get("phoneNo") or "",
- emergency_contact=data.get("emergencyContact") or "",
- emergency_phone=data.get("emergencyPhone") or "",
- clinical_diagnosis=data.get("clinicalDiagnosis") or "",
- diagnosis_attachments=data.get("diagnosisAttachments") or "",
- need_checkup=data.get("needCheckup") if data.get("needCheckup") is not None else 1,
- tenant_no=tenant_no,
- order_num=0,
- remark=data.get("remark") or "",
- create_by=user_id,
- create_dept_id=dept_id or "",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(existing)
- db.commit()
- db.refresh(existing)
- need_checkup = data.get("needCheckup") if data.get("needCheckup") is not None else 1
- if need_checkup == 1:
- pilot_checkup.ensure_checkup_for_patient(
- db, patient_id=existing.id,
- application_id=app_id, tenant_no=tenant_no, user_id=user_id,
- )
- else:
- pilot_checkup.remove_checkup_for_patient(
- db, patient_id=existing.id, tenant_no=tenant_no,
- )
- db.commit()
- return _row_to_dict_patient(existing)
- # ────────────────────────────────────────────
- # InfusionPlan CRUD
- # ────────────────────────────────────────────
- def list_infusion_plans(
- db: Session, app_id: str, tenant_no: str
- ) -> List[Dict[str, Any]]:
- rows = (
- db.query(BizPilotInfusionPlan)
- .filter(
- BizPilotInfusionPlan.pilot_application_id == app_id,
- BizPilotInfusionPlan.tenant_no == tenant_no,
- BizPilotInfusionPlan.del_flag == 0,
- )
- .order_by(BizPilotInfusionPlan.order_num.asc(), BizPilotInfusionPlan.create_time.asc())
- .all()
- )
- return [_row_to_dict_infusion_plan(r, db, tenant_no) for r in rows]
- def create_infusion_plan(
- db: Session,
- app_id: str,
- data: dict,
- tenant_no: str,
- user_id: str,
- dept_id: str,
- ) -> Dict[str, Any]:
- now = datetime.now()
- row = BizPilotInfusionPlan(
- id=str(uuid.uuid4()),
- pilot_application_id=app_id,
- cell_type=data.get("cellType") or "",
- spec_text=data.get("specText") or "",
- total_course_count=data.get("totalCourseCount") or 0,
- course_interval=data.get("courseInterval") or "",
- infusion_route=data.get("infusionRoute") or "",
- infusion_start_date=_normalize_yyyymmdd(data.get("infusionStartDate")),
- channel_agreement_id=data.get("channelAgreementId") or "",
- tenant_no=tenant_no,
- order_num=data.get("orderNum") or 0,
- remark=data.get("remark") or "",
- create_by=user_id,
- create_dept_id=dept_id or "",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(row)
- db.commit()
- db.refresh(row)
- return _row_to_dict_infusion_plan(row, db, tenant_no)
- _INFUSION_STATUS_CN_TO_CODE = {
- "待输注": "dtz",
- "已用户确认": "yrqr",
- "已取消": "yqx",
- "已完成": "ywc",
- }
- def _normalize_infusion_status_code(raw: Any) -> str:
- s = (str(raw).strip() if raw is not None else "") or ""
- if not s:
- return ""
- return _INFUSION_STATUS_CN_TO_CODE.get(s, s)
- def _schedule_infusion_editable(row: BizPilotInfusionSchedule) -> bool:
- return (row.infusion_status or "") in ("dtz", "yrqr")
- def _schedule_locked(row: BizPilotInfusionSchedule) -> bool:
- return (row.infusion_status or "") in ("ywc", "yqx")
- def _schedule_has_blocking_links(
- db: Session, schedule_id: str, tenant_no: str
- ) -> bool:
- delivery = (
- db.query(BizPilotDelivery.id)
- .filter(
- BizPilotDelivery.infusion_schedule_id == schedule_id,
- BizPilotDelivery.tenant_no == tenant_no,
- BizPilotDelivery.del_flag == 0,
- )
- .first()
- )
- if delivery:
- return True
- confirm = (
- db.query(BizPilotInfusionConfirm.id)
- .filter(
- BizPilotInfusionConfirm.infusion_schedule_id == schedule_id,
- BizPilotInfusionConfirm.tenant_no == tenant_no,
- BizPilotInfusionConfirm.del_flag == 0,
- )
- .first()
- )
- return confirm is not None
- def _schedule_deletable(
- db: Session, row: BizPilotInfusionSchedule, tenant_no: str
- ) -> tuple[bool, str]:
- label = f"第{row.order_num or '?'}疗程"
- st = row.infusion_status or ""
- if st in ("ywc", "yqx"):
- return False, f"{label}已完成或已取消,不可删除"
- if st != "dtz":
- return False, f"{label}仅待输注状态可删除"
- if (row.schedule_status or "") != "dsc":
- return False, f"{label}生产已开始,不可删除"
- wo = (
- db.query(BizProductionWorkOrder)
- .filter(
- BizProductionWorkOrder.infusion_schedule_id == row.id,
- BizProductionWorkOrder.tenant_no == tenant_no,
- BizProductionWorkOrder.del_flag == 0,
- BizProductionWorkOrder.work_order_type == "outbound",
- )
- .first()
- )
- if wo and (wo.work_order_status or "") not in ("dpz", "yzf"):
- return False, f"{label}关联出库工单进行中,不可删除"
- if _schedule_has_blocking_links(db, row.id, tenant_no):
- return False, f"{label}已有关联交付或确认记录,不可删除"
- return True, ""
- def _get_schedule_for_plan(
- db: Session,
- schedule_id: str,
- plan_id: str,
- tenant_no: str,
- ) -> BizPilotInfusionSchedule:
- row = (
- db.query(BizPilotInfusionSchedule)
- .filter(
- BizPilotInfusionSchedule.id == schedule_id,
- BizPilotInfusionSchedule.infusion_plan_id == plan_id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("输注计划不存在或不属于当前申请单")
- return row
- def _prepare_infusion_plan_payload(
- plan_data: Optional[dict],
- schedules_data: Optional[List[dict]],
- ) -> dict:
- plan_payload = dict(plan_data or {})
- cell_type = plan_payload.get("cellType") or ""
- spec_text = plan_payload.get("specText") or ""
- if not cell_type and schedules_data:
- for sched in schedules_data:
- for cp in sched.get("cellProducts") or []:
- if cp.get("cellTypeName"):
- cell_type = cp.get("cellTypeName")
- spec_text = cp.get("specText") or spec_text
- break
- if cell_type:
- break
- plan_payload.setdefault("cellType", cell_type or "-")
- plan_payload.setdefault("specText", spec_text)
- plan_payload.setdefault(
- "totalCourseCount",
- plan_payload.get("totalCourseCount") or len(schedules_data or []),
- )
- return plan_payload
- def update_infusion_plan(
- db: Session, plan_id: str, data: dict, tenant_no: str, *, commit: bool = True
- ) -> Dict[str, Any]:
- row = (
- db.query(BizPilotInfusionPlan)
- .filter(
- BizPilotInfusionPlan.id == plan_id,
- BizPilotInfusionPlan.tenant_no == tenant_no,
- BizPilotInfusionPlan.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("输注方案不存在")
- fields = {
- "cellType": "cell_type",
- "specText": "spec_text",
- "totalCourseCount": "total_course_count",
- "courseInterval": "course_interval",
- "infusionRoute": "infusion_route",
- "infusionStartDate": "infusion_start_date",
- "channelAgreementId": "channel_agreement_id",
- "remark": "remark",
- }
- for camel, snake in fields.items():
- if camel == "infusionStartDate" and data[camel] is not None:
- setattr(row, snake, _normalize_yyyymmdd(data[camel]))
- continue
- if camel in data and data[camel] is not None:
- setattr(row, snake, data[camel])
- row.update_time = datetime.now()
- if commit:
- db.commit()
- db.refresh(row)
- else:
- db.flush()
- return _row_to_dict_infusion_plan(row, db, tenant_no)
- def soft_delete_infusion_plan(db: Session, plan_id: str, tenant_no: str) -> None:
- now = datetime.now()
- row = (
- db.query(BizPilotInfusionPlan)
- .filter(
- BizPilotInfusionPlan.id == plan_id,
- BizPilotInfusionPlan.tenant_no == tenant_no,
- BizPilotInfusionPlan.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("输注方案不存在")
- row.del_flag = 1
- row.update_time = now
- schedule_ids = [
- r[0]
- for r in db.query(BizPilotInfusionSchedule.id)
- .filter(
- BizPilotInfusionSchedule.infusion_plan_id == plan_id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .all()
- ]
- if schedule_ids:
- db.query(BizPilotInfusionSchedule).filter(
- BizPilotInfusionSchedule.id.in_(schedule_ids),
- ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
- db.query(BizPilotInfusionScheduleProduct).filter(
- BizPilotInfusionScheduleProduct.infusion_schedule_id.in_(schedule_ids),
- BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
- BizPilotInfusionScheduleProduct.del_flag == 0,
- ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
- db.commit()
- # ────────────────────────────────────────────
- # InfusionSchedule CRUD
- # ────────────────────────────────────────────
- def list_schedules_by_plan(
- db: Session, plan_id: str, tenant_no: str
- ) -> List[Dict[str, Any]]:
- rows = (
- db.query(BizPilotInfusionSchedule)
- .filter(
- BizPilotInfusionSchedule.infusion_plan_id == plan_id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .order_by(BizPilotInfusionSchedule.order_num.asc())
- .all()
- )
- if not rows:
- return []
- schedule_ids = [r.id for r in rows]
- product_rows = (
- db.query(BizPilotInfusionScheduleProduct)
- .filter(
- BizPilotInfusionScheduleProduct.infusion_schedule_id.in_(schedule_ids),
- BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
- BizPilotInfusionScheduleProduct.del_flag == 0,
- )
- .order_by(BizPilotInfusionScheduleProduct.order_num.asc())
- .all()
- )
- products_by_schedule: Dict[str, List[Dict[str, Any]]] = {}
- for p in product_rows:
- products_by_schedule.setdefault(p.infusion_schedule_id, []).append(
- _row_to_dict_schedule_product(p)
- )
- return [
- _row_to_dict_infusion_schedule(r, products_by_schedule.get(r.id, []))
- for r in rows
- ]
- def auto_generate_schedules(
- db: Session,
- plan_id: str,
- tenant_no: str,
- user_id: str,
- dept_id: str,
- ) -> Dict[str, Any]:
- plan = (
- db.query(BizPilotInfusionPlan)
- .filter(
- BizPilotInfusionPlan.id == plan_id,
- BizPilotInfusionPlan.tenant_no == tenant_no,
- BizPilotInfusionPlan.del_flag == 0,
- )
- .first()
- )
- if not plan:
- raise ValueError("输注方案不存在")
- # 追溯申请单获取正确的 biz_type
- _app = (
- db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id == plan.pilot_application_id,
- BizPilotApplication.del_flag == 0,
- )
- .first()
- )
- schedule_biz_type = (_app.biz_type if _app else None) or "pilot_service"
- total = plan.total_course_count or 0
- if total <= 0:
- raise ValueError("总疗程数必须大于0")
- now = datetime.now()
- for i in range(1, total + 1):
- sched = BizPilotInfusionSchedule(
- id=str(uuid.uuid4()),
- infusion_plan_id=plan_id,
- cell_type=plan.cell_type or "",
- spec_text=plan.spec_text or "",
- schedule_status="dsc",
- infusion_status="dtz",
- qc_status="dzc",
- tenant_no=tenant_no,
- order_num=i,
- remark="",
- create_by=user_id,
- create_dept_id=dept_id or "",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(sched)
- from app.crud import production_work_order as pwo_svc
- pwo_svc.create_outbound_production_work_order(
- db,
- schedule_id=sched.id,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=dept_id,
- biz_type=schedule_biz_type,
- )
- from app.crud import qc_work_order as qc_svc
- qc_svc.create_outbound_qc_work_order(
- db,
- schedule_id=sched.id,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=dept_id,
- biz_type=schedule_biz_type,
- )
- db.commit()
- return {"generatedCount": total}
- def _create_schedule_products(
- db: Session,
- schedule_id: str,
- products: List[dict],
- tenant_no: str,
- user_id: str,
- dept_id: str,
- now: datetime,
- ) -> List[Dict[str, Any]]:
- result: List[Dict[str, Any]] = []
- for idx, item in enumerate(products or []):
- if not item:
- continue
- row = BizPilotInfusionScheduleProduct(
- id=str(uuid.uuid4()),
- infusion_schedule_id=schedule_id,
- price_id=item.get("priceId") or "",
- project_code=item.get("projectCode") or "",
- project_name=item.get("projectName") or "",
- cell_type_name=item.get("cellTypeName") or "",
- spec_text=item.get("specText") or "",
- agreement_price=item.get("agreementPrice"),
- actual_price=item.get("actualPrice"),
- quantity=item.get("quantity") if item.get("quantity") is not None else 1,
- product_status=item.get("status") or item.get("productStatus") or "dtz",
- tenant_no=tenant_no,
- order_num=item.get("sortNo") or (idx + 1),
- remark=item.get("remark") or "",
- create_by=user_id,
- create_dept_id=dept_id or "",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(row)
- result.append(_row_to_dict_schedule_product(row))
- return result
- def create_schedule(
- db: Session,
- plan_id: str,
- data: dict,
- tenant_no: str,
- user_id: str,
- dept_id: str,
- *,
- commit: bool = True,
- ) -> Dict[str, Any]:
- now = datetime.now()
- schedule_id = str(uuid.uuid4())
- cell_products = data.get("cellProducts") or []
- first_product = cell_products[0] if cell_products else {}
- # 追溯申请单获取正确的 biz_type
- _plan = (
- db.query(BizPilotInfusionPlan)
- .filter(BizPilotInfusionPlan.id == plan_id, BizPilotInfusionPlan.del_flag == 0)
- .first()
- )
- _app = (
- db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id == (_plan.pilot_application_id if _plan else None),
- BizPilotApplication.del_flag == 0,
- )
- .first()
- ) if _plan else None
- schedule_biz_type = (_app.biz_type if _app else None) or "pilot_service"
- row = BizPilotInfusionSchedule(
- id=schedule_id,
- infusion_plan_id=plan_id,
- cell_type=data.get("cellType") or first_product.get("cellTypeName") or "",
- spec_text=data.get("specText") or first_product.get("specText") or "",
- infusion_time=_parse_infusion_datetime(data.get("infusionTime")),
- infusion_location=data.get("infusionLocation") or "",
- infusion_method=data.get("infusionMethod") or "",
- schedule_status=data.get("scheduleStatus") or "dsc",
- infusion_status=data.get("infusionStatus") or "dtz",
- qc_status=data.get("qcStatus") or "dzc",
- attachments=data.get("attachments") or "",
- amount=data.get("amount") if data.get("amount") else None,
- tenant_no=tenant_no,
- order_num=data.get("sortNo") or data.get("orderNum") or 0,
- remark=data.get("remark") or "",
- create_by=user_id,
- create_dept_id=dept_id or "",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(row)
- products = _create_schedule_products(
- db, schedule_id, cell_products, tenant_no, user_id, dept_id, now
- )
- # 自动创建出库生产工单
- from app.crud import production_work_order as pwo_svc
- pwo_svc.create_outbound_production_work_order(
- db,
- schedule_id=schedule_id,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=dept_id,
- biz_type=schedule_biz_type,
- )
- # 自动创建出库质检工单
- from app.crud import qc_work_order as qc_svc
- qc_svc.create_outbound_qc_work_order(
- db,
- schedule_id=schedule_id,
- tenant_no=tenant_no,
- user_id=user_id,
- create_dept_id=dept_id,
- biz_type=schedule_biz_type,
- )
- # 临床应用自动创建缴费记录
- plan_row = _plan
- if plan_row:
- application = _app
- if (
- application
- and application.biz_type in ("clinical_application", "basic_experiment")
- and data.get("amount")
- ):
- from app.crud.clinical_payment import auto_create_payment
- auto_create_payment(
- db,
- application_id=application.id,
- schedule_id=schedule_id,
- amount=data["amount"],
- tenant_no=tenant_no,
- user_id=user_id,
- dept_id=dept_id,
- biz_type=application.biz_type,
- )
- if commit:
- db.commit()
- db.refresh(row)
- return _row_to_dict_infusion_schedule(row, products)
- def _update_schedule_editable(
- db: Session,
- row: BizPilotInfusionSchedule,
- data: dict,
- tenant_no: str,
- user_id: str,
- ) -> None:
- cell_products = data.get("cellProducts") or []
- first_product = cell_products[0] if cell_products else {}
- if data.get("infusionTime") is not None:
- row.infusion_time = _parse_infusion_datetime(data.get("infusionTime"))
- if data.get("infusionLocation") is not None:
- row.infusion_location = data.get("infusionLocation") or ""
- if data.get("infusionMethod") is not None:
- row.infusion_method = data.get("infusionMethod") or ""
- if data.get("sortNo") is not None or data.get("orderNum") is not None:
- row.order_num = data.get("sortNo") or data.get("orderNum") or row.order_num
- if cell_products:
- row.cell_type = data.get("cellType") or first_product.get("cellTypeName") or row.cell_type
- row.spec_text = data.get("specText") or first_product.get("specText") or row.spec_text
- if data.get("remark") is not None:
- row.remark = data.get("remark") or ""
- row.update_by = user_id
- row.update_time = datetime.now()
- def _upsert_schedule_products(
- db: Session,
- schedule_row: BizPilotInfusionSchedule,
- products: List[dict],
- tenant_no: str,
- user_id: str,
- dept_id: str,
- ) -> None:
- now = datetime.now()
- schedule_id = schedule_row.id
- existing_rows = (
- db.query(BizPilotInfusionScheduleProduct)
- .filter(
- BizPilotInfusionScheduleProduct.infusion_schedule_id == schedule_id,
- BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
- BizPilotInfusionScheduleProduct.del_flag == 0,
- )
- .all()
- )
- existing_by_id = {r.id: r for r in existing_rows}
- keep_ids: set[str] = set()
- default_status = schedule_row.infusion_status or "dtz"
- for idx, raw in enumerate(products or []):
- if not raw:
- continue
- item = dict(raw)
- pid = (item.get("id") or "").strip()
- if pid and pid in existing_by_id:
- prow = existing_by_id[pid]
- keep_ids.add(pid)
- prow.price_id = item.get("priceId") or ""
- prow.project_code = item.get("projectCode") or ""
- prow.project_name = item.get("projectName") or ""
- prow.cell_type_name = item.get("cellTypeName") or ""
- prow.spec_text = item.get("specText") or ""
- if item.get("agreementPrice") is not None:
- prow.agreement_price = item.get("agreementPrice")
- if item.get("actualPrice") is not None:
- prow.actual_price = item.get("actualPrice")
- if item.get("quantity") is not None:
- prow.quantity = item.get("quantity")
- prow.order_num = item.get("sortNo") or (idx + 1)
- prow.remark = item.get("remark") or ""
- prow.update_by = user_id
- prow.update_time = now
- continue
- status_raw = item.get("status") or item.get("productStatus")
- product_status = _normalize_infusion_status_code(status_raw) or default_status
- row = BizPilotInfusionScheduleProduct(
- id=str(uuid.uuid4()),
- infusion_schedule_id=schedule_id,
- price_id=item.get("priceId") or "",
- project_code=item.get("projectCode") or "",
- project_name=item.get("projectName") or "",
- cell_type_name=item.get("cellTypeName") or "",
- spec_text=item.get("specText") or "",
- agreement_price=item.get("agreementPrice"),
- actual_price=item.get("actualPrice"),
- quantity=item.get("quantity") if item.get("quantity") is not None else 1,
- product_status=product_status,
- tenant_no=tenant_no,
- order_num=item.get("sortNo") or (idx + 1),
- remark=item.get("remark") or "",
- create_by=user_id,
- create_dept_id=dept_id or "",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(row)
- for eid, erow in existing_by_id.items():
- if eid not in keep_ids:
- erow.del_flag = 1
- erow.update_by = user_id
- erow.update_time = now
- def _update_schedule_order_only(
- db: Session,
- row: BizPilotInfusionSchedule,
- data: dict,
- user_id: str,
- ) -> None:
- if data.get("sortNo") is not None or data.get("orderNum") is not None:
- row.order_num = data.get("sortNo") or data.get("orderNum") or row.order_num
- row.update_by = user_id
- row.update_time = datetime.now()
- def upsert_application_infusion(
- db: Session,
- app_id: str,
- plan_data: Optional[dict],
- schedules_data: Optional[List[dict]],
- tenant_no: str,
- user_id: str,
- dept_id: str,
- ) -> None:
- """编辑申请单:增量更新输注方案/计划,保留状态与 schedule id。"""
- if not plan_data and not schedules_data:
- return
- plan_payload = _prepare_infusion_plan_payload(plan_data, schedules_data)
- plans = list_infusion_plans(db, app_id, tenant_no)
- plan_id = (plans[0].get("id") if plans else None) or ""
- if plan_id:
- update_infusion_plan(
- db, plan_id, plan_payload, tenant_no, commit=False
- )
- else:
- plan_result = create_infusion_plan(
- db,
- app_id=app_id,
- data=plan_payload,
- tenant_no=tenant_no,
- user_id=user_id,
- dept_id=dept_id,
- )
- plan_id = plan_result.get("id") or ""
- if not plan_id:
- raise ValueError("创建输注方案失败")
- existing_schedules = list_schedules_by_plan(db, plan_id, tenant_no)
- existing_ids = {s["id"] for s in existing_schedules if s.get("id")}
- payload_ids: set[str] = set()
- for idx, sched in enumerate(schedules_data or []):
- sched_data = dict(sched)
- sched_data["sortNo"] = sched_data.get("sortNo") or (idx + 1)
- sid = (sched_data.get("id") or "").strip()
- label = f"第{idx + 1}疗程"
- if sid:
- if sid not in existing_ids:
- raise ValueError(f"{label}输注计划不存在或不属于当前申请单")
- row = _get_schedule_for_plan(db, sid, plan_id, tenant_no)
- payload_ids.add(sid)
- if _schedule_locked(row):
- _update_schedule_order_only(db, row, sched_data, user_id)
- continue
- if not _schedule_infusion_editable(row):
- raise ValueError(f"{label}当前状态不允许编辑")
- _update_schedule_editable(db, row, sched_data, tenant_no, user_id)
- _upsert_schedule_products(
- db, row, sched_data.get("cellProducts") or [], tenant_no, user_id, dept_id
- )
- continue
- create_schedule(
- db,
- plan_id=plan_id,
- data=sched_data,
- tenant_no=tenant_no,
- user_id=user_id,
- dept_id=dept_id,
- commit=False,
- )
- for sid in existing_ids - payload_ids:
- row = _get_schedule_for_plan(db, sid, plan_id, tenant_no)
- ok, msg = _schedule_deletable(db, row, tenant_no)
- if not ok:
- raise ValueError(msg)
- soft_delete_schedule(db, sid, tenant_no, commit=False)
- plan_row = (
- db.query(BizPilotInfusionPlan)
- .filter(
- BizPilotInfusionPlan.id == plan_id,
- BizPilotInfusionPlan.tenant_no == tenant_no,
- BizPilotInfusionPlan.del_flag == 0,
- )
- .first()
- )
- if plan_row:
- active_count = (
- db.query(func.count(BizPilotInfusionSchedule.id))
- .filter(
- BizPilotInfusionSchedule.infusion_plan_id == plan_id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .scalar()
- ) or 0
- plan_row.total_course_count = int(active_count)
- plan_row.update_by = user_id
- plan_row.update_time = datetime.now()
- db.commit()
- def _get_or_create_infusion_plan(
- db: Session,
- application_id: str,
- tenant_no: str,
- user_id: str,
- dept_id: str,
- ) -> BizPilotInfusionPlan:
- """获取或创建申请单下的输注/售卖方案(基础实验批量创建售卖计划用)。"""
- plans = list_infusion_plans(db, application_id, tenant_no)
- if plans:
- plan_row = (
- db.query(BizPilotInfusionPlan)
- .filter(
- BizPilotInfusionPlan.id == plans[0].get("id"),
- BizPilotInfusionPlan.tenant_no == tenant_no,
- BizPilotInfusionPlan.del_flag == 0,
- )
- .first()
- )
- if plan_row:
- return plan_row
- plan_result = create_infusion_plan(
- db,
- app_id=application_id,
- data={"cellType": "-", "totalCourseCount": 0},
- tenant_no=tenant_no,
- user_id=user_id,
- dept_id=dept_id,
- )
- plan_id = plan_result.get("id") or ""
- plan_row = (
- db.query(BizPilotInfusionPlan)
- .filter(
- BizPilotInfusionPlan.id == plan_id,
- BizPilotInfusionPlan.tenant_no == tenant_no,
- BizPilotInfusionPlan.del_flag == 0,
- )
- .first()
- )
- if not plan_row:
- raise ValueError("创建售卖方案失败")
- return plan_row
- def create_basic_experiment_sale_schedules(
- db: Session,
- application_id: str,
- product_lines: list,
- tenant_no: str,
- user_id: str,
- dept_id: str,
- ) -> List[Dict[str, Any]]:
- """基础实验:每个产品行创建一条售卖计划(一产品一计划)。"""
- app = (
- db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id == application_id,
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.del_flag == 0,
- )
- .first()
- )
- if not app:
- raise ValueError("申请单不存在")
- if (app.biz_type or "") != "basic_experiment":
- raise ValueError("仅基础实验申请单可批量创建售卖计划")
- if not product_lines:
- raise ValueError("请至少添加一行售卖产品")
- plan = _get_or_create_infusion_plan(
- db, application_id, tenant_no, user_id, dept_id
- )
- results: List[Dict[str, Any]] = []
- for idx, line in enumerate(product_lines):
- schedule_data = {
- "cellType": line.get("cellTypeName") or "",
- "specText": line.get("specText") or "",
- "infusionTime": line.get("pickupTime"),
- "amount": line.get("amount"),
- "sortNo": idx + 1,
- "cellProducts": [
- {
- "priceId": line.get("priceId"),
- "projectCode": line.get("projectCode"),
- "projectName": line.get("projectName"),
- "cellTypeName": line.get("cellTypeName"),
- "specText": line.get("specText"),
- "agreementPrice": line.get("agreementPrice"),
- "actualPrice": line.get("actualPrice"),
- "quantity": line.get("quantity") or 1,
- }
- ],
- }
- results.append(
- create_schedule(
- db,
- plan.id,
- schedule_data,
- tenant_no,
- user_id,
- dept_id,
- commit=False,
- )
- )
- active_count = (
- db.query(func.count(BizPilotInfusionSchedule.id))
- .filter(
- BizPilotInfusionSchedule.infusion_plan_id == plan.id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .scalar()
- ) or 0
- plan.total_course_count = int(active_count)
- plan.update_by = user_id
- plan.update_time = datetime.now()
- db.commit()
- return results
- def save_application_infusion(
- db: Session,
- app_id: str,
- plan_data: Optional[dict],
- schedules_data: Optional[List[dict]],
- tenant_no: str,
- user_id: str,
- dept_id: str,
- *,
- replace_existing: bool = False,
- ) -> None:
- if replace_existing:
- for plan in list_infusion_plans(db, app_id, tenant_no):
- plan_id = plan.get("id")
- if plan_id:
- soft_delete_infusion_plan(db, plan_id, tenant_no)
- if not plan_data and not schedules_data:
- return
- plan_payload = _prepare_infusion_plan_payload(plan_data, schedules_data)
- plan_result = create_infusion_plan(
- db,
- app_id=app_id,
- data=plan_payload,
- tenant_no=tenant_no,
- user_id=user_id,
- dept_id=dept_id,
- )
- plan_id = plan_result.get("id")
- if not plan_id:
- raise ValueError("创建输注方案失败")
- for idx, sched in enumerate(schedules_data or []):
- sched_data = dict(sched)
- sched_data["sortNo"] = sched_data.get("sortNo") or (idx + 1)
- create_schedule(
- db,
- plan_id=plan_id,
- data=sched_data,
- tenant_no=tenant_no,
- user_id=user_id,
- dept_id=dept_id,
- commit=False,
- )
- db.commit()
- def update_schedule(
- db: Session, schedule_id: str, data: dict, tenant_no: str
- ) -> Dict[str, Any]:
- row = (
- db.query(BizPilotInfusionSchedule)
- .filter(
- BizPilotInfusionSchedule.id == schedule_id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("输注计划不存在")
- if "infusionTime" in data and data["infusionTime"] is not None:
- row.infusion_time = data["infusionTime"]
- if "remark" in data and data["remark"] is not None:
- row.remark = data["remark"]
- if "attachments" in data and data["attachments"] is not None:
- row.attachments = data["attachments"]
- row.update_time = datetime.now()
- db.commit()
- db.refresh(row)
- return _row_to_dict_infusion_schedule(row)
- def soft_delete_schedule(
- db: Session, schedule_id: str, tenant_no: str, *, commit: bool = True
- ) -> None:
- row = (
- db.query(BizPilotInfusionSchedule)
- .filter(
- BizPilotInfusionSchedule.id == schedule_id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("输注计划不存在")
- now = datetime.now()
- outbound_wo = (
- db.query(BizProductionWorkOrder)
- .filter(
- BizProductionWorkOrder.infusion_schedule_id == schedule_id,
- BizProductionWorkOrder.tenant_no == tenant_no,
- BizProductionWorkOrder.del_flag == 0,
- BizProductionWorkOrder.work_order_type == "outbound",
- )
- .first()
- )
- row.schedule_status = "yqx"
- row.qc_status = "yqx"
- if outbound_wo:
- outbound_wo.work_order_status = "yzf"
- outbound_wo.update_time = now
- from app.crud.pilot_outbound_status_sync import sync_schedule_from_production_work_order
- sync_schedule_from_production_work_order(db, outbound_wo)
- for qc_wo in (
- db.query(BizQcWorkOrder)
- .filter(
- BizQcWorkOrder.infusion_schedule_id == schedule_id,
- BizQcWorkOrder.work_order_status.notin_(["ywc", "yzf"]),
- BizQcWorkOrder.del_flag == 0,
- )
- .all()
- ):
- qc_wo.work_order_status = "yzf"
- qc_wo.update_time = now
- from app.crud.pilot_outbound_status_sync import sync_schedule_from_qc_work_order
- sync_schedule_from_qc_work_order(db, qc_wo)
- db.query(BizPilotInfusionScheduleProduct).filter(
- BizPilotInfusionScheduleProduct.infusion_schedule_id == schedule_id,
- BizPilotInfusionScheduleProduct.tenant_no == tenant_no,
- BizPilotInfusionScheduleProduct.del_flag == 0,
- ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
- row.del_flag = 1
- row.update_time = now
- if commit:
- db.commit()
- else:
- db.flush()
- def mark_schedule_complete(
- db: Session, schedule_id: str, tenant_no: str, user_id: str
- ) -> Dict[str, Any]:
- row = (
- db.query(BizPilotInfusionSchedule)
- .filter(
- BizPilotInfusionSchedule.id == schedule_id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("输注计划不存在")
- row.schedule_status = "ysc"
- row.update_by = user_id
- row.update_time = datetime.now()
- from app.crud.pilot_outbound_status_sync import sync_pilot_outbound_from_schedule
- sync_pilot_outbound_from_schedule(db, row, user_id=user_id)
- db.commit()
- db.refresh(row)
- return _row_to_dict_infusion_schedule(row)
|