diff --git a/backend/app/api/pl/plan.py b/backend/app/api/pl/plan.py index 49da8a6e3..c3c137117 100644 --- a/backend/app/api/pl/plan.py +++ b/backend/app/api/pl/plan.py @@ -33,6 +33,7 @@ from app.schemas.pl.plan import ( PlanArchiveBody, PlanArchiveBrandQtyItem, PlanContractGapListResponse, + PlanContractGapRead, PlanCreate, PlanCreateFull, PlanDriverFeedbackUpdateBody, @@ -45,11 +46,12 @@ from app.schemas.pl.plan import ( PlanProductCertsUpdate, PlanProductRead, PlanRead, + PlanSideContractsUpdateBody, PlanSiteFeedbackUpdateBody, PlanUpdate, PlanWithProducts, ) -from app.services.pl_plan_contract_gap import list_plan_contract_gaps +from app.services.pl_plan_contract_gap import list_plan_contract_gaps, update_plan_side_contracts from app.services.pl_brand_archive import load_brand_qtys_by_plan_product_ids from app.services.pl_product_display import ( load_crm_product_alias_maps, @@ -551,6 +553,31 @@ def list_contract_gaps( return PlanContractGapListResponse(total=total, list=items) +@router.put("/{id}/contracts/{side}", response_model=PlanContractGapRead) +def put_plan_side_contracts( + id: str, + side: str, + payload: PlanSideContractsUpdateBody, + db: Session = Depends(get_db), + user: DbUserContext = Depends(get_db_current_user), +) -> PlanContractGapRead: + """单侧保存计划合同关联(合同补全)。""" + tenant_no = get_tenant_no(db, user.to_tenant_user_dict()) + plan = _get_plan(db, tenant_no, id, user) + result = update_plan_side_contracts( + db, + tenant_no=tenant_no, + plan=plan, + side=side, + contracts=payload.contracts, + user_id=user.id, + dept_id=user.dept_id, + ) + plan.update_by = user.id + db.commit() + return result + + # ============================================================================= # 创建计划 # ============================================================================= diff --git a/backend/app/services/pl_plan_contract_gap.py b/backend/app/services/pl_plan_contract_gap.py index eeba0c353..3a43b31a4 100644 --- a/backend/app/services/pl_plan_contract_gap.py +++ b/backend/app/services/pl_plan_contract_gap.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import date from typing import List, Optional, TYPE_CHECKING +from fastapi import HTTPException from sqlalchemy import and_, exists, func, or_, select from sqlalchemy.orm import Session @@ -13,7 +14,10 @@ from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES, is_intern from app.services.pl_plan_service import ( PLAN_CONTRACT_SIDE_DEMANDER, PLAN_CONTRACT_SIDE_SUPPLIER, + _normalize_contract_links, + assert_contract_selectable_for_plan, load_plan_contracts_grouped, + replace_plan_contracts, ) if TYPE_CHECKING: @@ -193,3 +197,65 @@ def list_plan_contract_gaps( ) ) return total, items + + +def update_plan_side_contracts( + db: Session, + *, + tenant_no: str, + plan: PlPlan, + side: str, + contracts: list[PlanContractLink], + user_id: Optional[str], + dept_id: Optional[str], +) -> PlanContractGapRead: + """单侧保存计划合同关联;允许空列表,不强制非内置侧必须有合同。""" + if plan.follow_status == "cancelled": + raise HTTPException(status_code=400, detail="已作废的计划不能修改") + if side not in (PLAN_CONTRACT_SIDE_SUPPLIER, PLAN_CONTRACT_SIDE_DEMANDER): + raise HTTPException(status_code=422, detail="side 必须为 supplier 或 demander") + + current_s, current_d = load_plan_contracts_grouped(db, tenant_no, [plan.id]).get( + plan.id, ([], []) + ) + links = _normalize_contract_links(contracts) + expected_customer_id = ( + plan.supplier_id if side == PLAN_CONTRACT_SIDE_SUPPLIER else plan.customer_id + ) + side_label = "供方合同" if side == PLAN_CONTRACT_SIDE_SUPPLIER else "需方合同" + resolved: list[PlanContractLink] = [] + for link in links: + contract = assert_contract_selectable_for_plan( + db, + link.contract_id, + tenant_no, + expected_customer_id=expected_customer_id, + side_label=side_label, + ) + resolved.append( + PlanContractLink( + contractId=contract.id, + contractNo=contract.contract_no or link.contract_no, + ) + ) + + supplier_links = resolved if side == PLAN_CONTRACT_SIDE_SUPPLIER else current_s + demander_links = resolved if side == PLAN_CONTRACT_SIDE_DEMANDER else current_d + replace_plan_contracts( + db, + tenant_no=tenant_no, + plan_id=plan.id, + supplier_contracts=supplier_links, + demander_contracts=demander_links, + user_id=user_id, + dept_id=dept_id, + ) + + supplier_contracts, demander_contracts = load_plan_contracts_grouped( + db, tenant_no, [plan.id] + ).get(plan.id, ([], [])) + return build_plan_contract_gap_read( + plan, + supplier_contracts=supplier_contracts, + demander_contracts=demander_contracts, + )