feat(pl): upsert plan product rows by id on update.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-09-05 18:24:43 +08:00
1 parent 6a7dfbcd38
commit 857067ec80
3 files changed
+126 -23

No files matched your search

+37 -22
View File
@@ -128,6 +128,7 @@ from app.services.pl_plan_service import (
apply_plan_site_feedback,
apply_vehicle_prefill_to_plan,
assert_contract_selectable_for_plan,
apply_plan_product_fields,
build_plan_product,
calc_plan_total_amount,
clear_draft_transport_for_plan,
@@ -1640,16 +1641,15 @@ def update_plan(
plan_type = payload.plan_type or plan.plan_type
# 更新产品明细
# 更新产品明细(按 id upsert,保持 pl_plan_product.id 稳定)
if payload.products is not None:
existing_products = (
existing_all = (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.plan_id == id,
PlPlanProduct.del_flag == 0,
)
.filter(PlPlanProduct.plan_id == id)
.all()
)
existing_all_by_id = {row.id: row for row in existing_all}
existing_products = [row for row in existing_all if row.del_flag == 0]
old_hazard_product_ids = collect_hazard_product_ids(db, tenant_no, existing_products)
cert_by_product_id = {
row.product_id: (
@@ -1660,11 +1660,7 @@ def update_plan(
for row in existing_products
}
db.query(PlPlanProduct).filter(
PlPlanProduct.plan_id == id,
PlPlanProduct.del_flag == 0,
).update({"del_flag": 1})
kept_ids: set[str] = set()
plan_products: list[PlPlanProduct] = []
for idx, prod in enumerate(payload.products):
is_toxic, is_explosive = resolve_hazard_flags_from_crm(
@@ -1689,17 +1685,36 @@ def update_plan(
"legal_use_cert_url": legal_use_cert,
}
)
pp = build_plan_product(
tenant_no=tenant_no,
plan_id=id,
plan_type=plan_type,
prod=prod_payload,
order_num=idx,
user_id=user.id,
dept_id=user.dept_id,
)
db.add(pp)
plan_products.append(pp)
row_id = (prod.id or "").strip() or None
existing = existing_all_by_id.get(row_id) if row_id else None
if existing is not None:
apply_plan_product_fields(
existing,
plan_type=plan_type,
prod=prod_payload,
order_num=idx,
user_id=user.id,
)
kept_ids.add(existing.id)
plan_products.append(existing)
else:
pp = build_plan_product(
tenant_no=tenant_no,
plan_id=id,
plan_type=plan_type,
prod=prod_payload,
order_num=idx,
user_id=user.id,
dept_id=user.dept_id,
)
db.add(pp)
kept_ids.add(pp.id)
plan_products.append(pp)
for row in existing_products:
if row.id not in kept_ids:
row.del_flag = 1
row.update_by = user.id
new_hazard_product_ids = collect_hazard_product_ids(db, tenant_no, payload.products)
if old_hazard_product_ids != new_hazard_product_ids:
+47 -1
View File
@@ -111,12 +111,13 @@ def build_plan_product(
order_num: int,
user_id: str,
dept_id: str,
product_row_id: Optional[str] = None,
) -> PlPlanProduct:
qty = _product_quantity(prod)
estimates = _estimate_qty_fields(plan_type, qty if qty else None)
tax_price = prod.tax_price
return PlPlanProduct(
id=generate_uuid(),
id=(product_row_id or "").strip() or generate_uuid(),
tenant_no=tenant_no,
plan_id=plan_id,
product_id=prod.product_id,
@@ -152,6 +153,51 @@ def build_plan_product(
)
def apply_plan_product_fields(
row: PlPlanProduct,
*,
plan_type: str,
prod: PlanProductCreate,
order_num: int,
user_id: str,
) -> None:
"""就地更新已有计划产品行字段(保持 id),用于 update upsert。"""
qty = _product_quantity(prod)
estimates = _estimate_qty_fields(plan_type, qty if qty else None)
tax_price = prod.tax_price
row.product_id = prod.product_id
row.product_no = prod.product_no
row.product_name = prod.product_name
row.brand_mode = prod.brand_mode
row.brand_id = prod.brand_id
row.brand_name = prod.brand_name
row.model = prod.model
row.unit = prod.unit or "吨"
row.category_name = prod.category_name
row.package_spec = prod.package_spec
row.delivery_method = prod.delivery_method
row.quantity = qty if qty else None
row.actual_qty = Decimal(str(prod.actual_qty)) if prod.actual_qty is not None else None
row.tax_price = tax_price
row.tax_amount = prod.tax_amount
row.tax_subtotal = prod.tax_subtotal
row.is_explosive = prod.is_explosive
row.is_toxic = prod.is_toxic
row.purchase_cert_url = prod.purchase_cert_url
row.transport_cert_url = prod.transport_cert_url
row.legal_use_cert_url = prod.legal_use_cert_url
row.inbound_price = tax_price
row.outbound_price = tax_price
row.inbound_amount = prod.tax_subtotal
row.outbound_amount = prod.tax_subtotal
row.remark = prod.remark
row.order_num = order_num
row.update_by = user_id
row.del_flag = 0
for key, value in estimates.items():
setattr(row, key, value)
def _apply_vehicle_snapshot_to_plan_transport(
db: Session,
*,
@@ -0,0 +1,42 @@
"""build_plan_product 应支持传入 product_row_id,编辑时保持 pl_plan_product.id 稳定。"""
from app.schemas.pl.plan import PlanProductCreate
from app.services import pl_plan_service as svc
from app.services.pl_plan_service import build_plan_product
def _minimal_prod(**kwargs) -> PlanProductCreate:
data = {
"product_id": "crm-p1",
"product_no": "P001",
"product_name": "甲醇",
}
data.update(kwargs)
return PlanProductCreate(**data)
def test_build_plan_product_keeps_row_id():
result = build_plan_product(
tenant_no="t1",
plan_id="plan-1",
plan_type="outbound",
prod=_minimal_prod(),
order_num=0,
user_id="u1",
dept_id="d1",
product_row_id="pp-1",
)
assert result.id == "pp-1"
def test_build_plan_product_generates_when_missing(monkeypatch):
monkeypatch.setattr(svc, "generate_uuid", lambda: "gen-1")
result = build_plan_product(
tenant_no="t1",
plan_id="plan-1",
plan_type="outbound",
prod=_minimal_prod(),
order_num=0,
user_id="u1",
dept_id="d1",
)
assert result.id == "gen-1"