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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-09-05 18:18:06 +08:00
1 parent ca14bb7ca5
commit 6a7dfbcd38
2 files changed
+121 -25

No files matched your search

+78 -25
View File
@@ -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,7 +967,30 @@ def replace_plan_stops(
delivery.dibang_direction,
)
delivery_id = generate_uuid()
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,
@@ -975,6 +1017,8 @@ def replace_plan_stops(
)
)
_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)
db.add(
@@ -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
View File
@@ -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