Преглед изворни кода

feat(pl): upsert plan deliveries by stable id on save.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu пре 1 месец
родитељ
комит
6a7dfbcd38
2 измењених фајлова са 145 додато и 49 уклоњено
  1. 102 49
      backend/app/services/pl_plan_stop_service.py
  2. 43 0
      backend/tests/test_pl_plan_stop_upsert.py

+ 102 - 49
backend/app/services/pl_plan_stop_service.py

@@ -687,12 +687,13 @@ def sync_plan_legacy_from_stops(
     plan.destination_notes = first.notes
 
 
-def _soft_delete_plan_stops(
+def _soft_delete_load_products_and_stop_contracts(
     db: Session,
     tenant_no: str,
     plan_id: str,
     user_id: Optional[str],
 ) -> None:
+    """软删装货产品与装/送货段合同;送货段本体由 upsert 保留主键,不在此全量软删。"""
     now = datetime.now()
     load_ids = [
         row.id
@@ -710,28 +711,6 @@ def _soft_delete_plan_stops(
             PlPlanLoadProduct.del_flag == 0,
         ).update({"del_flag": 1, "update_by": user_id, "update_time": now}, synchronize_session=False)
 
-    delivery_ids = [
-        row.id
-        for row in db.query(PlPlanDelivery)
-        .filter(
-            PlPlanDelivery.plan_id == plan_id,
-            PlPlanDelivery.tenant_no == tenant_no,
-            PlPlanDelivery.del_flag == 0,
-        )
-        .all()
-    ]
-    if delivery_ids:
-        db.query(PlPlanDeliveryProduct).filter(
-            PlPlanDeliveryProduct.delivery_id.in_(delivery_ids),
-            PlPlanDeliveryProduct.del_flag == 0,
-        ).update({"del_flag": 1, "update_by": user_id, "update_time": now}, synchronize_session=False)
-
-    db.query(PlPlanDelivery).filter(
-        PlPlanDelivery.plan_id == plan_id,
-        PlPlanDelivery.tenant_no == tenant_no,
-        PlPlanDelivery.del_flag == 0,
-    ).update({"del_flag": 1, "update_by": user_id, "update_time": now}, synchronize_session=False)
-
     db.query(PlPlanContract).filter(
         PlPlanContract.plan_id == plan_id,
         PlPlanContract.tenant_no == tenant_no,
@@ -742,6 +721,35 @@ def _soft_delete_plan_stops(
     ).update({"del_flag": 1, "update_by": user_id, "update_time": now}, synchronize_session=False)
 
 
+def _soft_delete_delivery_products(
+    db: Session,
+    delivery_ids: list[str],
+    user_id: Optional[str],
+    now: datetime,
+) -> None:
+    if not delivery_ids:
+        return
+    db.query(PlPlanDeliveryProduct).filter(
+        PlPlanDeliveryProduct.delivery_id.in_(delivery_ids),
+        PlPlanDeliveryProduct.del_flag == 0,
+    ).update({"del_flag": 1, "update_by": user_id, "update_time": now}, synchronize_session=False)
+
+
+def resolve_delivery_id_for_upsert(
+    *,
+    plan_id: str,
+    payload_id: Optional[str],
+    existing_by_id: dict,
+) -> tuple[str, Optional[object]]:
+    pid = (payload_id or "").strip()
+    if not pid:
+        return generate_uuid(), None
+    row = existing_by_id.get(pid)
+    if row is None or getattr(row, "plan_id", None) != plan_id:
+        raise HTTPException(status_code=422, detail=f"送货段 id 无效或不属于本计划:{pid}")
+    return pid, row
+
+
 def _write_stop_contracts(
     db: Session,
     tenant_no: str,
@@ -884,7 +892,7 @@ def replace_plan_stops(
     dept_id: Optional[str],
     plan_products: Optional[list] = None,
 ) -> None:
-    _soft_delete_plan_stops(db, tenant_no, plan_id, user_id)
+    _soft_delete_load_products_and_stop_contracts(db, tenant_no, plan_id, user_id)
     now = datetime.now()
 
     load_id = _resolve_or_create_plan_load(
@@ -940,6 +948,17 @@ def replace_plan_stops(
         contract_order,
     )
 
+    existing_deliveries = (
+        db.query(PlPlanDelivery)
+        .filter(
+            PlPlanDelivery.plan_id == plan_id,
+            PlPlanDelivery.tenant_no == tenant_no,
+        )
+        .all()
+    )
+    existing_by_id = {row.id: row for row in existing_deliveries}
+    kept_ids: set[str] = set()
+
     for delivery in deliveries:
         generate_dibang, dibang_direction = normalize_stop_dibang_fields(
             delivery.customer_name,
@@ -948,32 +967,57 @@ def replace_plan_stops(
             delivery.dibang_direction,
         )
 
-        delivery_id = generate_uuid()
-        db.add(
-            PlPlanDelivery(
-                id=delivery_id,
-                plan_id=plan_id,
-                seq=delivery.seq or 1,
-                customer_id=delivery.customer_id,
-                customer_name=delivery.customer_name,
-                address=delivery.address,
-                lng=delivery.lng,
-                lat=delivery.lat,
-                delivery_time=delivery.delivery_time,
-                contact=delivery.contact,
-                notes=delivery.notes,
-                generate_dibang=generate_dibang,
-                dibang_direction=dibang_direction,
-                tenant_no=tenant_no,
-                remark=delivery.remark,
-                create_by=user_id,
-                create_dept_id=dept_id or "",
-                create_time=now,
-                update_by=user_id,
-                update_time=now,
-                del_flag=0,
-            )
+        delivery_id, existing = resolve_delivery_id_for_upsert(
+            plan_id=plan_id,
+            payload_id=delivery.id,
+            existing_by_id=existing_by_id,
         )
+        kept_ids.add(delivery_id)
+
+        if existing is not None:
+            existing.seq = delivery.seq or 1
+            existing.customer_id = delivery.customer_id
+            existing.customer_name = delivery.customer_name
+            existing.address = delivery.address
+            existing.lng = delivery.lng
+            existing.lat = delivery.lat
+            existing.delivery_time = delivery.delivery_time
+            existing.contact = delivery.contact
+            existing.notes = delivery.notes
+            existing.generate_dibang = generate_dibang
+            existing.dibang_direction = dibang_direction
+            existing.remark = delivery.remark
+            existing.del_flag = 0
+            existing.update_by = user_id
+            existing.update_time = now
+        else:
+            db.add(
+                PlPlanDelivery(
+                    id=delivery_id,
+                    plan_id=plan_id,
+                    seq=delivery.seq or 1,
+                    customer_id=delivery.customer_id,
+                    customer_name=delivery.customer_name,
+                    address=delivery.address,
+                    lng=delivery.lng,
+                    lat=delivery.lat,
+                    delivery_time=delivery.delivery_time,
+                    contact=delivery.contact,
+                    notes=delivery.notes,
+                    generate_dibang=generate_dibang,
+                    dibang_direction=dibang_direction,
+                    tenant_no=tenant_no,
+                    remark=delivery.remark,
+                    create_by=user_id,
+                    create_dept_id=dept_id or "",
+                    create_time=now,
+                    update_by=user_id,
+                    update_time=now,
+                    del_flag=0,
+                )
+            )
+
+        _soft_delete_delivery_products(db, [delivery_id], user_id, now)
 
         for idx, prod in enumerate(delivery.products or []):
             plan_product_id = _resolve_plan_product_id(prod, plan_product_rows)
@@ -1014,3 +1058,12 @@ def replace_plan_stops(
             dept_id,
             contract_order,
         )
+
+    removed_ids = [row.id for row in existing_deliveries if row.id not in kept_ids and row.del_flag == 0]
+    if removed_ids:
+        _soft_delete_delivery_products(db, removed_ids, user_id, now)
+        db.query(PlPlanDelivery).filter(
+            PlPlanDelivery.id.in_(removed_ids),
+            PlPlanDelivery.tenant_no == tenant_no,
+            PlPlanDelivery.del_flag == 0,
+        ).update({"del_flag": 1, "update_by": user_id, "update_time": now}, synchronize_session=False)

+ 43 - 0
backend/tests/test_pl_plan_stop_upsert.py

@@ -0,0 +1,43 @@
+"""replace_plan_stops 应按 payload.id upsert 送货段,而非每次换新 UUID。"""
+from unittest.mock import MagicMock
+
+import pytest
+from fastapi import HTTPException
+
+from app.services.pl_plan_stop_service import resolve_delivery_id_for_upsert
+
+
+def test_upsert_delivery_rejects_foreign_plan_id():
+    with pytest.raises(HTTPException) as ei:
+        resolve_delivery_id_for_upsert(
+            plan_id="plan-1",
+            payload_id="del-other-plan",
+            existing_by_id={},
+        )
+    assert ei.value.status_code == 422
+
+
+def test_upsert_delivery_keeps_payload_id():
+    existing = MagicMock()
+    existing.id = "del-keep"
+    existing.plan_id = "plan-1"
+    new_id, row = resolve_delivery_id_for_upsert(
+        plan_id="plan-1",
+        payload_id="del-keep",
+        existing_by_id={"del-keep": existing},
+    )
+    assert new_id == "del-keep"
+    assert row is existing
+
+
+def test_upsert_delivery_generates_id_when_missing(monkeypatch):
+    from app.services import pl_plan_stop_service as svc
+
+    monkeypatch.setattr(svc, "generate_uuid", lambda: "new-del")
+    new_id, row = resolve_delivery_id_for_upsert(
+        plan_id="plan-1",
+        payload_id=None,
+        existing_by_id={},
+    )
+    assert new_id == "new-del"
+    assert row is None