feat(pl): upsert plan deliveries by stable id on save.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
ca14bb7ca5
commit
6a7dfbcd38
2 files changed
+121
-25
No files matched your search
@@ -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)
|
||||
@@ -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
|
||||
Reference in new issue
Block a user