Files
zkxw/backend/app/api/pilot_application.py
T
zlwu bd62416263 feat(project): add team attribution and business statistics
Add tenant-scoped teams and projects, order attribution across four businesses, conversion inheritance, scoped statistics and audited reassignment. Reuse existing Web and mobile UI patterns; team regions and hospitals remain informational.

Include incremental SQL and isolated regression coverage. Actual MySQL and browser/WebView integration validation remains pending; relates to issue #13.
2026-11-02 15:29:43 +08:00

928 lines
34 KiB
Python

"""中试服务申请单管理 API(申请单、患者信息、输注方案、输注计划、出库联动)"""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
from app.services.project_assignment import create_assignment, accessible_order
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin
from app.core.response import error_response, success_response
from app.crud import pilot_application
from app.models.models import SysUser, BizPilotApplication
router = APIRouter(prefix="/web/admin", tags=["中试服务申请单管理"])
def _get_tenant_no(db: Session, user: dict) -> str:
user_id = user.get("user_id")
if not user_id:
raise PermissionError("用户身份信息缺失,无法确定租户")
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
if not sys_user or not sys_user.tenant_no:
raise PermissionError("用户租户信息缺失")
return sys_user.tenant_no
# ---------------------------------------------------------------------------
# Pydantic Schemas
# ---------------------------------------------------------------------------
class IdBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str
class PlanIdBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
plan_id: str = Field(..., alias="infusionPlanId")
class ApplicationCreateBody(BaseModel):
project_id: Optional[str] = Field(None, alias="projectId")
model_config = ConfigDict(populate_by_name=True)
channel_service_order_id: Optional[str] = Field(None, alias="channelServiceOrderId")
remark: Optional[str] = None
biz_type: Optional[str] = Field(None, alias="bizType")
project_title: Optional[str] = Field(None, alias="projectTitle")
customer_name: Optional[str] = Field(None, alias="customerName")
customer_contact: Optional[str] = Field(None, alias="customerContact")
customer_phone: Optional[str] = Field(None, alias="customerPhone")
class ApplicationUpdateBody(BaseModel):
project_id: Optional[str] = Field(None, alias="projectId")
model_config = ConfigDict(populate_by_name=True)
id: str
remark: Optional[str] = None
project_title: Optional[str] = Field(None, alias="projectTitle")
customer_name: Optional[str] = Field(None, alias="customerName")
customer_contact: Optional[str] = Field(None, alias="customerContact")
customer_phone: Optional[str] = Field(None, alias="customerPhone")
class PatientSaveBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
application_id: str = Field(..., alias="pilotApplicationId")
customer_id: Optional[str] = Field(None, alias="customerId")
patient_name: str = Field(..., alias="patientName")
sex: Optional[str] = None
age: Optional[int] = None
id_card_no: Optional[str] = Field(None, alias="idCardNo")
phone_no: Optional[str] = Field(None, alias="phoneNo")
emergency_contact: Optional[str] = Field(None, alias="emergencyContact")
emergency_phone: Optional[str] = Field(None, alias="emergencyPhone")
clinical_diagnosis: Optional[str] = Field(None, alias="clinicalDiagnosis")
diagnosis_attachments: Optional[str] = Field(None, alias="diagnosisAttachments")
need_checkup: int = Field(1, alias="needCheckup")
class InfusionPlanCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
pilot_application_id: str = Field(..., alias="pilotApplicationId")
cell_type: str = Field(..., alias="cellType")
spec_text: Optional[str] = Field(None, alias="specText")
total_course_count: int = Field(..., alias="totalCourseCount")
course_interval: Optional[str] = Field(None, alias="courseInterval")
infusion_route: Optional[str] = Field(None, alias="infusionRoute")
infusion_start_date: Optional[str] = Field(None, alias="infusionStartDate")
class InfusionPlanUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str
cell_type: Optional[str] = Field(None, alias="cellType")
spec_text: Optional[str] = Field(None, alias="specText")
total_course_count: Optional[int] = Field(None, alias="totalCourseCount")
course_interval: Optional[str] = Field(None, alias="courseInterval")
infusion_route: Optional[str] = Field(None, alias="infusionRoute")
infusion_start_date: Optional[str] = Field(None, alias="infusionStartDate")
class InfusionScheduleUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: str
infusion_time: Optional[str] = Field(None, alias="infusionTime")
remark: Optional[str] = None
attachments: Optional[str] = None
class ScheduleCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
plan_id: str = Field(..., alias="infusionPlanId")
infusion_time: Optional[str] = Field(None, alias="infusionTime")
remark: Optional[str] = None
amount: Optional[float] = None
# --- 复合保存 Schema(参考采集协议单一次性保存模式)---
class PatientNestedBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
customer_id: Optional[str] = Field(None, alias="customerId")
patient_name: Optional[str] = Field(None, alias="patientName")
sex: Optional[str] = None
age: Optional[int] = None
id_card_no: Optional[str] = Field(None, alias="idCardNo")
phone_no: Optional[str] = Field(None, alias="phoneNo")
emergency_contact: Optional[str] = Field(None, alias="emergencyContact")
emergency_phone: Optional[str] = Field(None, alias="emergencyPhone")
clinical_diagnosis: Optional[str] = Field(None, alias="clinicalDiagnosis")
diagnosis_attachments: Optional[str] = Field(None, alias="diagnosisAttachments")
need_checkup: int = Field(1, alias="needCheckup")
class ScheduleProductNestedBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: Optional[str] = None
price_id: Optional[str] = Field(None, alias="priceId")
project_code: Optional[str] = Field(None, alias="projectCode")
project_name: Optional[str] = Field(None, alias="projectName")
cell_type_name: Optional[str] = Field(None, alias="cellTypeName")
spec_text: Optional[str] = Field(None, alias="specText")
package_id: Optional[str] = Field(None, alias="packageId")
package_text: Optional[str] = Field(None, alias="packageText")
agreement_price: Optional[float] = Field(None, alias="agreementPrice")
actual_price: Optional[float] = Field(None, alias="actualPrice")
quantity: Optional[int] = 1
status: Optional[str] = None
remark: Optional[str] = None
class InfusionScheduleNestedBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: Optional[str] = None
sort_no: Optional[int] = Field(None, alias="sortNo")
infusion_time: Optional[str] = Field(None, alias="infusionTime")
infusion_location: Optional[str] = Field(None, alias="infusionLocation")
infusion_method: Optional[str] = Field(None, alias="infusionMethod")
amount: Optional[float] = None
package_id: Optional[str] = Field(None, alias="packageId")
package_text: Optional[str] = Field(None, alias="packageText")
cell_products: Optional[list[ScheduleProductNestedBody]] = Field(None, alias="cellProducts")
class InfusionPlanNestedBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id: Optional[str] = None
cell_type: Optional[str] = Field(None, alias="cellType")
spec_text: Optional[str] = Field(None, alias="specText")
total_course_count: Optional[int] = Field(None, alias="totalCourseCount")
course_interval: Optional[str] = Field(None, alias="courseInterval")
infusion_route: Optional[str] = Field(None, alias="infusionRoute")
infusion_start_date: Optional[str] = Field(None, alias="infusionStartDate")
channel_agreement_id: Optional[str] = Field(None, alias="channelAgreementId")
class ApplicationSaveBody(BaseModel):
"""一揽子保存:申请单 + 患者信息 + 输注方案 + 输注计划(含细胞产品)"""
project_id: Optional[str] = Field(None, alias="projectId")
model_config = ConfigDict(populate_by_name=True)
id: Optional[str] = None
application_name: Optional[str] = Field(None, alias="applicationName")
channel_service_order_id: Optional[str] = Field(None, alias="channelServiceOrderId")
channel_id: Optional[str] = Field(None, alias="channelId")
biz_type: Optional[str] = Field(None, alias="bizType")
project_title: Optional[str] = Field(None, alias="projectTitle")
customer_name: Optional[str] = Field(None, alias="customerName")
customer_contact: Optional[str] = Field(None, alias="customerContact")
customer_phone: Optional[str] = Field(None, alias="customerPhone")
remark: Optional[str] = None
consent_form_urls: Optional[str] = Field(None, alias="consentFormUrls")
consent_attachments: Optional[list] = Field(None, alias="consentAttachments")
attachment_urls: Optional[str] = Field(None, alias="attachmentUrls")
other_attachments: Optional[list] = Field(None, alias="otherAttachments")
patient: Optional[PatientNestedBody] = None
infusion_plan: Optional[InfusionPlanNestedBody] = Field(None, alias="infusionPlan")
infusion_schedules: Optional[list[InfusionScheduleNestedBody]] = Field(
None, alias="infusionSchedules"
)
infusion_plans: Optional[list[InfusionPlanNestedBody]] = Field(None, alias="infusionPlans")
# ---------------------------------------------------------------------------
# 1. 申请单 CRUD + 状态变更
# ---------------------------------------------------------------------------
@router.get("/pilot-application/list")
async def api_list_applications(
projectId: Optional[str] = Query(None, alias="projectId"),
teamId: Optional[str] = Query(None, alias="teamId"),
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
keyword: Optional[str] = Query(None, alias="keyword"),
channelId: Optional[str] = Query(None, alias="channelId"),
applicationStatus: Optional[str] = Query(None, alias="applicationStatus"),
channelServiceOrderId: Optional[str] = Query(None, alias="channelServiceOrderId"),
bizType: Optional[str] = Query(None, alias="bizType"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
result = pilot_application.list_applications(
db,
tenant_no=tenant_no,
project_id=projectId,
team_id=teamId,
page=page,
page_size=pageSize,
channel_id=channelId,
application_status=applicationStatus,
channel_service_order_id=channelServiceOrderId,
keyword=keyword,
biz_type=bizType,
user_info=user,
)
return success_response(result)
@router.get("/pilot-application/detail")
async def api_get_application(
id: str = Query(...),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
accessible_order(db, BizPilotApplication, tenant_no, user, id)
result = pilot_application.get_application(db, app_id=id, tenant_no=tenant_no)
if result is None:
return error_response(404, "申请单不存在")
return success_response(result)
@router.post("/pilot-application/create")
async def api_create_application(
body: ApplicationCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {
"projectId": create_assignment(db, tenant_no, user, body.project_id, body.biz_type or "pilot_service"),
"channelServiceOrderId": body.channel_service_order_id or "",
"remark": body.remark,
"bizType": body.biz_type or "pilot_service",
"projectTitle": body.project_title,
"customerName": body.customer_name,
"customerContact": body.customer_contact,
"customerPhone": body.customer_phone,
}
result = pilot_application.create_application(
db,
data=data,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(result, message="创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/pilot-application/update")
async def api_update_application(
body: ApplicationUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
accessible_order(db, BizPilotApplication, tenant_no, user, body.id, lock=True)
data = {}
if "project_id" in body.model_fields_set:
data["projectId"] = body.project_id
if body.remark is not None:
data["remark"] = body.remark
if body.project_title is not None:
data["projectTitle"] = body.project_title
if body.customer_name is not None:
data["customerName"] = body.customer_name
if body.customer_contact is not None:
data["customerContact"] = body.customer_contact
if body.customer_phone is not None:
data["customerPhone"] = body.customer_phone
result = pilot_application.update_application(
db, app_id=body.id, data=data, tenant_no=tenant_no
)
return success_response(result, message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-application/save")
async def api_save_application(
body: ApplicationSaveBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
user_id = user.get("user_id")
dept_id = user.get("region_id", "")
is_edit = bool(body.id)
biz_type = body.biz_type or "pilot_service"
app_data: dict = {
"channelServiceOrderId": body.channel_service_order_id or "",
"channelId": body.channel_id or "",
"bizType": biz_type,
"remark": body.remark,
}
if is_edit:
if "project_id" in body.model_fields_set:
app_data["projectId"] = body.project_id
else:
app_data["projectId"] = create_assignment(db, tenant_no, user, body.project_id, biz_type)
if body.application_name is not None:
app_data["applicationName"] = body.application_name
if body.project_title is not None:
app_data["projectTitle"] = body.project_title
if body.customer_name is not None:
app_data["customerName"] = body.customer_name
if body.customer_contact is not None:
app_data["customerContact"] = body.customer_contact
if body.customer_phone is not None:
app_data["customerPhone"] = body.customer_phone
if body.consent_attachments is not None:
app_data["consentAttachments"] = body.consent_attachments
elif body.consent_form_urls is not None:
app_data["consentFormUrls"] = body.consent_form_urls
if body.other_attachments is not None:
app_data["otherAttachments"] = body.other_attachments
elif body.attachment_urls is not None:
app_data["attachmentUrls"] = body.attachment_urls
if (
not is_edit
and biz_type not in ("clinical_application", "basic_experiment")
and not app_data["channelServiceOrderId"]
):
return error_response(400, "请选择渠道服务订单")
if not is_edit and biz_type == "clinical_application" and not app_data["channelId"]:
return error_response(400, "请选择渠道")
if is_edit:
accessible_order(db, BizPilotApplication, tenant_no, user, body.id, lock=True)
if is_edit:
app_result = pilot_application.update_application(
db,
app_id=body.id,
data=app_data,
tenant_no=tenant_no,
)
app_id = body.id
else:
app_result = pilot_application.create_application(
db,
data=app_data,
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
app_id = app_result.get("id")
if not app_id:
return error_response(500, "创建申请单失败,未获取到ID")
if body.patient:
pilot_application.save_patient(
db,
app_id=app_id,
data=body.patient.model_dump(by_alias=True),
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
if body.infusion_plan is not None or body.infusion_schedules:
plan_dict = (
body.infusion_plan.model_dump(by_alias=True) if body.infusion_plan else {}
)
schedules = [
s.model_dump(by_alias=True) for s in (body.infusion_schedules or [])
]
if is_edit:
pilot_application.upsert_application_infusion(
db,
app_id=app_id,
plan_data=plan_dict,
schedules_data=schedules,
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
else:
pilot_application.save_application_infusion(
db,
app_id=app_id,
plan_data=plan_dict,
schedules_data=schedules,
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
replace_existing=False,
)
elif body.infusion_plans:
for plan_data in body.infusion_plans:
plan_result = pilot_application.create_infusion_plan(
db,
app_id=app_id,
data=plan_data.model_dump(by_alias=True),
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
plan_id = plan_result.get("id")
if plan_id and (plan_data.total_course_count or 0) > 0:
pilot_application.auto_generate_schedules(
db,
plan_id=plan_id,
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
)
app_result = pilot_application.get_application(db, app_id=app_id, tenant_no=tenant_no)
return success_response(app_result, message="保存成功")
except ValueError as e:
return error_response(400, str(e))
except HTTPException:
db.rollback()
raise
except Exception as e:
return error_response(500, str(e))
@router.delete("/pilot-application/delete")
async def api_delete_application(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
pilot_application.soft_delete_application(db, app_id=body.id, tenant_no=tenant_no)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-application/start")
async def api_start_application(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
result = pilot_application.start_application(
db, app_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
)
return success_response(result, message="操作成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-application/complete")
async def api_complete_application(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
result = pilot_application.complete_application(
db, app_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
)
return success_response(result, message="操作成功")
except ValueError as e:
return error_response(400, str(e))
class SaleScheduleProductLineBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
price_id: Optional[str] = Field(None, alias="priceId")
project_code: Optional[str] = Field(None, alias="projectCode")
project_name: Optional[str] = Field(None, alias="projectName")
cell_type_name: Optional[str] = Field(None, alias="cellTypeName")
spec_text: Optional[str] = Field(None, alias="specText")
package_id: Optional[str] = Field(None, alias="packageId")
package_text: Optional[str] = Field(None, alias="packageText")
agreement_price: Optional[float] = Field(None, alias="agreementPrice")
actual_price: Optional[float] = Field(None, alias="actualPrice")
quantity: Optional[int] = 1
pickup_time: Optional[str] = Field(None, alias="pickupTime")
amount: Optional[float] = None
class SaleSchedulesCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
product_lines: list[SaleScheduleProductLineBody] = Field(..., alias="productLines")
@router.post("/pilot-application/{application_id}/sale-schedules")
async def api_create_sale_schedules(
application_id: str,
body: SaleSchedulesCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
product_lines = [line.model_dump(by_alias=True) for line in body.product_lines]
results = pilot_application.create_basic_experiment_sale_schedules(
db,
application_id=application_id,
product_lines=product_lines,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(results, message="售卖计划创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-application/void")
async def api_void_application(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
result = pilot_application.void_application(
db, app_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
)
return success_response(result, message="操作成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 2. 患者信息
# ---------------------------------------------------------------------------
@router.get("/pilot-patient/detail")
async def api_get_patient(
application_id: str = Query(..., alias="pilotApplicationId"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
result = pilot_application.get_patient_by_application(
db, app_id=application_id, tenant_no=tenant_no
)
if result is None:
return success_response(None)
return success_response(result)
@router.post("/pilot-patient/save")
async def api_save_patient(
body: PatientSaveBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {
"customerId": body.customer_id,
"patientName": body.patient_name,
"sex": body.sex,
"age": body.age,
"idCardNo": body.id_card_no,
"phoneNo": body.phone_no,
"emergencyContact": body.emergency_contact,
"emergencyPhone": body.emergency_phone,
"clinicalDiagnosis": body.clinical_diagnosis,
"diagnosisAttachments": body.diagnosis_attachments,
"needCheckup": body.need_checkup,
}
result = pilot_application.save_patient(
db,
app_id=body.application_id,
data=data,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(result, message="保存成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 3. 输注方案
# ---------------------------------------------------------------------------
@router.get("/pilot-infusion-plan/list")
async def api_list_plans(
application_id: str = Query(..., alias="pilotApplicationId"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
result = pilot_application.list_infusion_plans(
db, app_id=application_id, tenant_no=tenant_no
)
return success_response(result)
@router.post("/pilot-infusion-plan/create")
async def api_create_plan(
body: InfusionPlanCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {
"cellType": body.cell_type,
"specText": body.spec_text,
"totalCourseCount": body.total_course_count,
"courseInterval": body.course_interval,
"infusionRoute": body.infusion_route,
"infusionStartDate": body.infusion_start_date,
}
result = pilot_application.create_infusion_plan(
db,
app_id=body.pilot_application_id,
data=data,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(result, message="创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/pilot-infusion-plan/update")
async def api_update_plan(
body: InfusionPlanUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {}
if body.cell_type is not None:
data["cellType"] = body.cell_type
if body.spec_text is not None:
data["specText"] = body.spec_text
if body.total_course_count is not None:
data["totalCourseCount"] = body.total_course_count
if body.course_interval is not None:
data["courseInterval"] = body.course_interval
if body.infusion_route is not None:
data["infusionRoute"] = body.infusion_route
if body.infusion_start_date is not None:
data["infusionStartDate"] = body.infusion_start_date
result = pilot_application.update_infusion_plan(
db, plan_id=body.id, data=data, tenant_no=tenant_no
)
return success_response(result, message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.delete("/pilot-infusion-plan/delete")
async def api_delete_plan(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
pilot_application.soft_delete_infusion_plan(
db, plan_id=body.id, tenant_no=tenant_no
)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 4. 输注计划
# ---------------------------------------------------------------------------
@router.get("/pilot-infusion-schedule/list")
async def api_list_schedules(
plan_id: str = Query(..., alias="infusionPlanId"),
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
result = pilot_application.list_schedules_by_plan(
db, plan_id=plan_id, tenant_no=tenant_no
)
return success_response(result)
@router.post("/pilot-infusion-schedule/auto-generate")
async def api_auto_generate(
body: PlanIdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
result = pilot_application.auto_generate_schedules(
db,
plan_id=body.plan_id,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(result, message="生成成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-infusion-schedule/create")
async def api_create_schedule(
body: ScheduleCreateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {
"infusionTime": body.infusion_time,
"remark": body.remark,
"amount": body.amount,
}
result = pilot_application.create_schedule(
db,
plan_id=body.plan_id,
data=data,
tenant_no=tenant_no,
user_id=user.get("user_id"),
dept_id=user.get("region_id", ""),
)
return success_response(result, message="创建成功")
except ValueError as e:
return error_response(400, str(e))
@router.put("/pilot-infusion-schedule/update")
async def api_update_schedule(
body: InfusionScheduleUpdateBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
data = {}
if body.infusion_time is not None:
data["infusionTime"] = body.infusion_time
if body.remark is not None:
data["remark"] = body.remark
if body.attachments is not None:
data["attachments"] = body.attachments
result = pilot_application.update_schedule(
db, schedule_id=body.id, data=data, tenant_no=tenant_no
)
return success_response(result, message="更新成功")
except ValueError as e:
return error_response(400, str(e))
@router.delete("/pilot-infusion-schedule/delete")
async def api_delete_schedule(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
pilot_application.soft_delete_schedule(
db, schedule_id=body.id, tenant_no=tenant_no
)
return success_response(None, message="删除成功")
except ValueError as e:
return error_response(400, str(e))
@router.post("/pilot-infusion-schedule/mark-complete")
async def api_mark_complete(
body: IdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
result = pilot_application.mark_schedule_complete(
db, schedule_id=body.id, tenant_no=tenant_no, user_id=user.get("user_id")
)
return success_response(result, message="操作成功")
except ValueError as e:
return error_response(400, str(e))
# ---------------------------------------------------------------------------
# 出库生产工单 / 出库质检工单 联动
# ---------------------------------------------------------------------------
class ScheduleIdBody(BaseModel):
scheduleId: str
@router.post("/pilot-infusion-schedule/create-production-work-order")
async def api_create_outbound_production(
body: ScheduleIdBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
from app.crud import production_work_order as pwo_svc
from app.crud.qc_work_order import resolve_outbound_biz_type_for_schedule
outbound_biz_type = resolve_outbound_biz_type_for_schedule(
db, body.scheduleId, tenant_no
)
wo = pwo_svc.create_outbound_production_work_order(
db,
schedule_id=body.scheduleId,
tenant_no=tenant_no,
user_id=user.get("user_id"),
create_dept_id=user.get("dept_id", ""),
biz_type=outbound_biz_type,
)
db.commit()
db.refresh(wo)
return success_response(pwo_svc._wo_to_dict(wo), message="创建出库生产工单成功")
except ValueError as e:
return error_response(400, str(e))
except Exception as e:
db.rollback()
return error_response(400, str(e))
class OutboundQcBody(BaseModel):
scheduleId: str
cellProductId: str = ""
@router.post("/pilot-infusion-schedule/create-qc-work-order")
async def api_create_outbound_qc(
body: OutboundQcBody,
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
from app.crud import qc_work_order as qc_svc
from app.crud.qc_work_order import resolve_outbound_biz_type_for_schedule
outbound_biz_type = resolve_outbound_biz_type_for_schedule(
db, body.scheduleId, tenant_no
)
wo = qc_svc.create_outbound_qc_work_order(
db,
schedule_id=body.scheduleId,
cell_product_id=body.cellProductId,
tenant_no=tenant_no,
user_id=user.get("user_id"),
create_dept_id=user.get("dept_id", ""),
biz_type=outbound_biz_type,
)
return success_response(qc_svc._wo_to_dict(wo), message="创建出库质检工单成功")
except ValueError as e:
return error_response(400, str(e))
except Exception as e:
db.rollback()
return error_response(400, str(e))