| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721 |
- """中试交付管理 CRUD(交付信息、交付物料、交付方式 + 费用扣减)"""
- from __future__ import annotations
- import uuid
- from datetime import datetime
- from typing import Any, Dict, List, Optional
- from sqlalchemy import and_, func
- from sqlalchemy.orm import Session
- from app.models.models import (
- BizChannelAccount,
- BizChannelAgreementProductPrice,
- BizChannelProductQuota,
- BizChannelServiceOrder,
- BizChannelServiceOrderProduct,
- BizExpressCompany,
- BizItemUnit,
- BizPilotApplication,
- BizPilotDelivery,
- BizPilotDeliveryMaterial,
- BizPilotDeliveryMethod,
- BizPilotInfusionPlan,
- BizPilotInfusionSchedule,
- )
- from app.crud.pilot_service import (
- _get_or_create_channel_account,
- _reduce_channel_product_quota,
- )
- # ────────────────────────────────────────────
- # Helpers: _row_to_dict
- # ────────────────────────────────────────────
- def _generate_delivery_no(db: Session, tenant_no: str) -> str:
- today_str = datetime.now().strftime("%Y%m%d")
- prefix = f"ZSJF{today_str}"
- existing = (
- db.query(BizPilotDelivery.delivery_no)
- .filter(
- BizPilotDelivery.tenant_no == tenant_no,
- BizPilotDelivery.delivery_no.like(f"{prefix}%"),
- BizPilotDelivery.del_flag == 0,
- )
- .order_by(BizPilotDelivery.delivery_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 _row_to_dict_delivery(row, db=None, tenant_no=None) -> Dict[str, Any]:
- d: Dict[str, Any] = {
- "id": row.id,
- "infusionScheduleId": row.infusion_schedule_id or "",
- "deliveryNo": row.delivery_no 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:
- # materialCount
- material_count = (
- db.query(func.count(BizPilotDeliveryMaterial.id))
- .filter(
- BizPilotDeliveryMaterial.delivery_id == row.id,
- BizPilotDeliveryMaterial.tenant_no == tenant_no,
- BizPilotDeliveryMaterial.del_flag == 0,
- )
- .scalar()
- ) or 0
- d["materialCount"] = material_count
- # hasMethod
- method = (
- db.query(BizPilotDeliveryMethod.id)
- .filter(
- BizPilotDeliveryMethod.delivery_id == row.id,
- BizPilotDeliveryMethod.tenant_no == tenant_no,
- BizPilotDeliveryMethod.del_flag == 0,
- )
- .first()
- )
- d["hasMethod"] = method is not None
- return d
- def _row_to_dict_delivery_material(row, db=None) -> Dict[str, Any]:
- d: Dict[str, Any] = {
- "id": row.id,
- "deliveryId": row.delivery_id or "",
- "materialName": row.material_name or "",
- "qty": float(row.qty) if row.qty is not None else None,
- "unitId": row.unit_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 "",
- }
- # unitName lookup
- if db and row.unit_id:
- unit = (
- db.query(BizItemUnit.unit_name)
- .filter(
- BizItemUnit.id == row.unit_id,
- BizItemUnit.del_flag == 0,
- )
- .first()
- )
- d["unitName"] = unit.unit_name if unit else ""
- else:
- d["unitName"] = ""
- return d
- def _row_to_dict_delivery_method(row, db=None) -> Dict[str, Any]:
- d: Dict[str, Any] = {
- "id": row.id,
- "deliveryId": row.delivery_id or "",
- "deliveryMode": row.delivery_mode or "",
- "deliveryAttachments": row.delivery_attachments or "",
- "shipTime": row.ship_time.strftime("%Y-%m-%d %H:%M:%S") if row.ship_time else "",
- "handoverTemp": row.handover_temp or "",
- "pickupPerson": row.pickup_person or "",
- "pickupPhone": row.pickup_phone or "",
- "senderName": row.sender_name or "",
- "expressCompanyId": row.express_company_id or "",
- "waybillNo": row.waybill_no or "",
- "expressFee": float(row.express_fee) if row.express_fee is not None else None,
- "etaTime": row.eta_time.strftime("%Y-%m-%d %H:%M:%S") if row.eta_time else "",
- "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 "",
- }
- # expressCompanyName lookup
- if db and row.express_company_id:
- company = (
- db.query(BizExpressCompany.express_name)
- .filter(
- BizExpressCompany.id == row.express_company_id,
- BizExpressCompany.del_flag == 0,
- )
- .first()
- )
- d["expressCompanyName"] = company.express_name if company else ""
- else:
- d["expressCompanyName"] = ""
- return d
- # ────────────────────────────────────────────
- # Fee Deduction Logic
- # ────────────────────────────────────────────
- def _deduct_fee_on_delivery(db: Session, schedule_id: str, tenant_no: str, user_id: str):
- """
- 创建交付时扣减费用。
- 流程:
- 1. schedule → plan → application
- 2. application.channel_service_order_id → service order
- 3. service_order.order_type (yfk/gmcp)
- 4. schedule.cell_type, schedule.spec_text
- 5. yfk(预付款型): 查协议价 → 扣渠道账户余额 → 更新订单余额
- 6. gmcp(购买型): 扣份数 + 更新订单产品持仓
- """
- # 1. Find schedule
- schedule = (
- db.query(BizPilotInfusionSchedule)
- .filter(
- BizPilotInfusionSchedule.id == schedule_id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .first()
- )
- if not schedule:
- raise ValueError("输注计划不存在")
- # 2. Find plan → application
- plan = (
- db.query(BizPilotInfusionPlan)
- .filter(
- BizPilotInfusionPlan.id == schedule.infusion_plan_id,
- BizPilotInfusionPlan.del_flag == 0,
- )
- .first()
- )
- if not plan:
- raise ValueError("输注方案不存在")
- application = (
- db.query(BizPilotApplication)
- .filter(
- BizPilotApplication.id == plan.pilot_application_id,
- BizPilotApplication.tenant_no == tenant_no,
- BizPilotApplication.del_flag == 0,
- )
- .first()
- )
- if not application:
- raise ValueError("中试申请单不存在")
- # 临床应用走 clinical_payment 模块缴费,不走服务订单扣费
- if (application.biz_type or "") == "clinical_application":
- return
- # 3. Get service order
- if not application.channel_service_order_id:
- raise ValueError("申请单未关联渠道服务订单,无法扣费")
- order = (
- db.query(BizChannelServiceOrder)
- .filter(
- BizChannelServiceOrder.id == application.channel_service_order_id,
- BizChannelServiceOrder.del_flag == 0,
- )
- .first()
- )
- if not order:
- raise ValueError("渠道服务订单不存在")
- order_type = order.order_type or ""
- # 4. yfk: deduct from channel account balance
- if order_type == "yfk":
- # Find agreement price
- agreement_id = order.channel_agreement_id
- if not agreement_id:
- raise ValueError("服务订单未关联渠道协议,无法确定扣费金额")
- agreement_product = (
- db.query(BizChannelAgreementProductPrice)
- .filter(
- BizChannelAgreementProductPrice.channel_agreement_id == agreement_id,
- BizChannelAgreementProductPrice.cell_type_code == (schedule.cell_type or ""),
- BizChannelAgreementProductPrice.spec_text == (schedule.spec_text or ""),
- BizChannelAgreementProductPrice.del_flag == 0,
- )
- .first()
- )
- deduction_amount = float(agreement_product.agreement_price) if agreement_product else 0
- if deduction_amount <= 0:
- # No price found, skip deduction
- return
- # Get channel account with row lock
- account = _get_or_create_channel_account(
- db, order.channel_id, tenant_no, user_id
- )
- balance_before = float(account.balance_amount) if account.balance_amount else 0
- if balance_before < deduction_amount:
- raise ValueError(
- f"渠道账户余额不足,当前余额 {balance_before} 元,需扣减 {deduction_amount} 元"
- )
- # Deduct balance
- account.balance_amount = balance_before - deduction_amount
- balance_after = float(account.balance_amount)
- account.update_by = user_id
- account.update_time = datetime.now()
- db.add(account)
- db.flush()
- # Update order current_balance
- order.current_balance = float(order.current_balance or 0) - deduction_amount
- order.balance_before = balance_before
- order.balance_after = balance_after
- order.update_by = user_id
- order.update_time = datetime.now()
- db.add(order)
- db.flush()
- # 5. gmcp: deduct quota and update order product
- elif order_type == "gmcp":
- # Find matching service order product for project_code
- order_product = (
- db.query(BizChannelServiceOrderProduct)
- .filter(
- BizChannelServiceOrderProduct.service_order_id == order.id,
- BizChannelServiceOrderProduct.cell_type == (schedule.cell_type or ""),
- BizChannelServiceOrderProduct.spec_text == (schedule.spec_text or ""),
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- .first()
- )
- project_code = order_product.project_code if order_product else ""
- # Reduce channel product quota
- _reduce_channel_product_quota(
- db,
- channel_id=order.channel_id,
- project_code=project_code,
- cell_type_code=schedule.cell_type or "",
- spec_text=schedule.spec_text or "",
- reduce_qty=1,
- user_id=user_id,
- )
- # Update BizChannelServiceOrderProduct.current_qty
- if order_product:
- current_qty = float(order_product.current_qty or 0)
- if current_qty < 1:
- raise ValueError("订单产品持仓数量不足")
- order_product.current_qty = current_qty - 1
- order_product.update_by = user_id
- order_product.update_time = datetime.now()
- db.add(order_product)
- db.flush()
- # ────────────────────────────────────────────
- # Delivery CRUD
- # ────────────────────────────────────────────
- def get_delivery_by_schedule(
- db: Session, schedule_id: str, tenant_no: str
- ) -> Optional[Dict[str, Any]]:
- row = (
- db.query(BizPilotDelivery)
- .filter(
- BizPilotDelivery.infusion_schedule_id == schedule_id,
- BizPilotDelivery.tenant_no == tenant_no,
- BizPilotDelivery.del_flag == 0,
- )
- .first()
- )
- if not row:
- return None
- return _row_to_dict_delivery(row, db, tenant_no)
- def list_deliveries_by_application(
- db: Session, application_id: str, tenant_no: str
- ) -> List[Dict[str, Any]]:
- """List all deliveries for an application (through plan → schedule → delivery)."""
- # Find all plan IDs for this application
- plan_ids = [
- r[0]
- for r in db.query(BizPilotInfusionPlan.id)
- .filter(
- BizPilotInfusionPlan.pilot_application_id == application_id,
- BizPilotInfusionPlan.tenant_no == tenant_no,
- BizPilotInfusionPlan.del_flag == 0,
- )
- .all()
- ]
- if not plan_ids:
- return []
- # Find all schedule IDs for these plans
- schedule_ids = [
- r[0]
- for r in db.query(BizPilotInfusionSchedule.id)
- .filter(
- BizPilotInfusionSchedule.infusion_plan_id.in_(plan_ids),
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .all()
- ]
- if not schedule_ids:
- return []
- # Find all deliveries for these schedules
- rows = (
- db.query(BizPilotDelivery)
- .filter(
- BizPilotDelivery.infusion_schedule_id.in_(schedule_ids),
- BizPilotDelivery.tenant_no == tenant_no,
- BizPilotDelivery.del_flag == 0,
- )
- .order_by(BizPilotDelivery.create_time.desc())
- .all()
- )
- return [_row_to_dict_delivery(r, db, tenant_no) for r in rows]
- def create_delivery(
- db: Session,
- schedule_id: str,
- data: dict,
- tenant_no: str,
- user_id: str,
- dept_id: str,
- ) -> Dict[str, Any]:
- # Validate schedule exists and belongs to tenant
- schedule = (
- db.query(BizPilotInfusionSchedule)
- .filter(
- BizPilotInfusionSchedule.id == schedule_id,
- BizPilotInfusionSchedule.tenant_no == tenant_no,
- BizPilotInfusionSchedule.del_flag == 0,
- )
- .first()
- )
- if not schedule:
- raise ValueError("输注计划不存在")
- # Check if delivery already exists for this schedule
- existing = (
- db.query(BizPilotDelivery)
- .filter(
- BizPilotDelivery.infusion_schedule_id == schedule_id,
- BizPilotDelivery.tenant_no == tenant_no,
- BizPilotDelivery.del_flag == 0,
- )
- .first()
- )
- if existing:
- raise ValueError("该输注计划已创建交付信息")
- now = datetime.now()
- delivery_no = _generate_delivery_no(db, tenant_no)
- row = BizPilotDelivery(
- id=str(uuid.uuid4()),
- infusion_schedule_id=schedule_id,
- delivery_no=delivery_no,
- 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(row)
- db.flush()
- # Trigger fee deduction
- try:
- _deduct_fee_on_delivery(db, schedule_id, tenant_no, user_id)
- except ValueError:
- db.rollback()
- raise
- # Update schedule status to yjf (已交付)
- schedule.schedule_status = "yjf"
- schedule.update_by = user_id
- schedule.update_time = now
- from app.crud.pilot_outbound_status_sync import sync_pilot_outbound_from_schedule
- sync_pilot_outbound_from_schedule(db, schedule, user_id=user_id)
- db.commit()
- db.refresh(row)
- return _row_to_dict_delivery(row, db, tenant_no)
- def soft_delete_delivery(db: Session, delivery_id: str, tenant_no: str) -> None:
- now = datetime.now()
- row = (
- db.query(BizPilotDelivery)
- .filter(
- BizPilotDelivery.id == delivery_id,
- BizPilotDelivery.tenant_no == tenant_no,
- BizPilotDelivery.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("交付信息不存在")
- row.del_flag = 1
- row.update_time = now
- # Cascade soft-delete to materials
- db.query(BizPilotDeliveryMaterial).filter(
- BizPilotDeliveryMaterial.delivery_id == delivery_id,
- BizPilotDeliveryMaterial.tenant_no == tenant_no,
- BizPilotDeliveryMaterial.del_flag == 0,
- ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
- # Cascade soft-delete to method
- db.query(BizPilotDeliveryMethod).filter(
- BizPilotDeliveryMethod.delivery_id == delivery_id,
- BizPilotDeliveryMethod.tenant_no == tenant_no,
- BizPilotDeliveryMethod.del_flag == 0,
- ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
- db.commit()
- # ────────────────────────────────────────────
- # Delivery Material CRUD
- # ────────────────────────────────────────────
- def list_delivery_materials(
- db: Session, delivery_id: str, tenant_no: str
- ) -> List[Dict[str, Any]]:
- rows = (
- db.query(BizPilotDeliveryMaterial)
- .filter(
- BizPilotDeliveryMaterial.delivery_id == delivery_id,
- BizPilotDeliveryMaterial.tenant_no == tenant_no,
- BizPilotDeliveryMaterial.del_flag == 0,
- )
- .order_by(
- BizPilotDeliveryMaterial.order_num.asc(),
- BizPilotDeliveryMaterial.create_time.asc(),
- )
- .all()
- )
- return [_row_to_dict_delivery_material(r, db) for r in rows]
- def save_delivery_materials(
- db: Session,
- delivery_id: str,
- items_json: list,
- tenant_no: str,
- user_id: str,
- ) -> List[Dict[str, Any]]:
- """Bulk save: soft-delete existing + insert new."""
- now = datetime.now()
- # Validate delivery exists
- delivery = (
- db.query(BizPilotDelivery)
- .filter(
- BizPilotDelivery.id == delivery_id,
- BizPilotDelivery.tenant_no == tenant_no,
- BizPilotDelivery.del_flag == 0,
- )
- .first()
- )
- if not delivery:
- raise ValueError("交付信息不存在")
- # Soft-delete existing materials
- db.query(BizPilotDeliveryMaterial).filter(
- BizPilotDeliveryMaterial.delivery_id == delivery_id,
- BizPilotDeliveryMaterial.tenant_no == tenant_no,
- BizPilotDeliveryMaterial.del_flag == 0,
- ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
- db.flush()
- # Insert new
- created = []
- for idx, item in enumerate(items_json):
- material = BizPilotDeliveryMaterial(
- id=str(uuid.uuid4()),
- delivery_id=delivery_id,
- material_name=item.get("material_name") or "",
- qty=item.get("qty"),
- unit_id=item.get("unit_id") or "",
- tenant_no=tenant_no,
- order_num=idx + 1,
- remark=item.get("remark") or "",
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(material)
- created.append(material)
- db.commit()
- for m in created:
- db.refresh(m)
- return [_row_to_dict_delivery_material(m, db) for m in created]
- def delete_delivery_material(
- db: Session, material_id: str, tenant_no: str
- ) -> None:
- row = (
- db.query(BizPilotDeliveryMaterial)
- .filter(
- BizPilotDeliveryMaterial.id == material_id,
- BizPilotDeliveryMaterial.tenant_no == tenant_no,
- BizPilotDeliveryMaterial.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise ValueError("交付物料不存在")
- row.del_flag = 1
- row.update_time = datetime.now()
- db.commit()
- # ────────────────────────────────────────────
- # Delivery Method CRUD
- # ────────────────────────────────────────────
- def get_delivery_method(
- db: Session, delivery_id: str, tenant_no: str
- ) -> Optional[Dict[str, Any]]:
- row = (
- db.query(BizPilotDeliveryMethod)
- .filter(
- BizPilotDeliveryMethod.delivery_id == delivery_id,
- BizPilotDeliveryMethod.tenant_no == tenant_no,
- BizPilotDeliveryMethod.del_flag == 0,
- )
- .first()
- )
- if not row:
- return None
- return _row_to_dict_delivery_method(row, db)
- def save_delivery_method(
- db: Session,
- delivery_id: str,
- data: dict,
- tenant_no: str,
- user_id: str,
- ) -> Dict[str, Any]:
- """Create or update delivery method (upsert pattern)."""
- now = datetime.now()
- # Validate delivery exists
- delivery = (
- db.query(BizPilotDelivery)
- .filter(
- BizPilotDelivery.id == delivery_id,
- BizPilotDelivery.tenant_no == tenant_no,
- BizPilotDelivery.del_flag == 0,
- )
- .first()
- )
- if not delivery:
- raise ValueError("交付信息不存在")
- existing = (
- db.query(BizPilotDeliveryMethod)
- .filter(
- BizPilotDeliveryMethod.delivery_id == delivery_id,
- BizPilotDeliveryMethod.tenant_no == tenant_no,
- BizPilotDeliveryMethod.del_flag == 0,
- )
- .first()
- )
- # Parse datetime fields
- ship_time = data.get("ship_time")
- eta_time = data.get("eta_time")
- if existing:
- # Update
- existing.delivery_mode = data.get("delivery_mode") or existing.delivery_mode
- existing.delivery_attachments = data.get("delivery_attachments") if data.get("delivery_attachments") is not None else existing.delivery_attachments
- existing.ship_time = ship_time if ship_time is not None else existing.ship_time
- existing.handover_temp = data.get("handover_temp") if data.get("handover_temp") is not None else existing.handover_temp
- existing.pickup_person = data.get("pickup_person") if data.get("pickup_person") is not None else existing.pickup_person
- existing.pickup_phone = data.get("pickup_phone") if data.get("pickup_phone") is not None else existing.pickup_phone
- existing.sender_name = data.get("sender_name") if data.get("sender_name") is not None else existing.sender_name
- existing.express_company_id = data.get("express_company_id") if data.get("express_company_id") is not None else existing.express_company_id
- existing.waybill_no = data.get("waybill_no") if data.get("waybill_no") is not None else existing.waybill_no
- existing.express_fee = data.get("express_fee") if data.get("express_fee") is not None else existing.express_fee
- existing.eta_time = eta_time if eta_time is not None else existing.eta_time
- existing.remark = data.get("remark") if data.get("remark") is not None else existing.remark
- existing.update_by = user_id
- existing.update_time = now
- db.commit()
- db.refresh(existing)
- return _row_to_dict_delivery_method(existing, db)
- # Create
- row = BizPilotDeliveryMethod(
- id=str(uuid.uuid4()),
- delivery_id=delivery_id,
- delivery_mode=data.get("delivery_mode") or "",
- delivery_attachments=data.get("delivery_attachments") or "",
- ship_time=ship_time,
- handover_temp=data.get("handover_temp") or "",
- pickup_person=data.get("pickup_person") or "",
- pickup_phone=data.get("pickup_phone") or "",
- sender_name=data.get("sender_name") or "",
- express_company_id=data.get("express_company_id") or "",
- waybill_no=data.get("waybill_no") or "",
- express_fee=data.get("express_fee"),
- eta_time=eta_time,
- tenant_no=tenant_no,
- order_num=0,
- remark=data.get("remark") or "",
- create_by=user_id,
- create_dept_id="",
- 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_delivery_method(row, db)
|