Kaynağa Gözat

feat(pl): add PUT plan side contracts for contract completion

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 ay önce
ebeveyn
işleme
b984c8c857

+ 28 - 1
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
+
+
 # =============================================================================
 # 创建计划
 # =============================================================================

+ 66 - 0
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,
+    )