fix(pl): keep multi-brand site feedback when plan product row is stale

Persist submitted brand qtys even if plan brand_mode lookup fails; resolve soft-deleted plan products for brand metadata instead of substituting another product; stop seeding all actual qty onto the first brand option.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-31 07:06:41 +08:00
1 parent d1daedc17d
commit 5a9abfdd5d
6 files changed
+220 -54

No files matched your search

+10 -1
View File
@@ -68,7 +68,9 @@ def persist_io_item_brand_qtys(
row.del_flag = 1
row.update_by = user_id
if not brand_mode:
# 客户端显式提交了品牌明细时必须落库;勿因计划行 brand_mode 缺失/软删查找失败而只删不写
positive = [item for item in rows if Decimal(str(item.get("qty") or 0)) > 0]
if not brand_mode and not positive:
return
for idx, item in enumerate(rows):
@@ -287,6 +289,13 @@ def load_transport_driver_product_feedbacks(
{
"planProductId": fb.plan_product_id,
"productName": (pp.product_name if pp else None) or "",
"productNo": (pp.product_no if pp else None) or None,
"model": (pp.model if pp else None) or None,
"unit": (pp.unit if pp else None) or None,
"packageSpec": (pp.package_spec if pp else None) or None,
"brandMode": (pp.brand_mode if pp else None) or None,
"brandId": (pp.brand_id if pp else None) or None,
"brandName": (pp.brand_name if pp else None) or None,
"actualQty": fb.actual_qty,
"brandQtys": brand_qtys or None,
"brandSummary": format_brand_qty_summary(brand_qtys),
+106 -13
View File
@@ -34,6 +34,7 @@ from app.services.pl_feedback_brand_qty import (
persist_io_item_brand_qtys,
require_brand_qtys_if_needed,
)
from app.services.pl_product_display import apply_product_alias_display, load_crm_product_alias_maps
OrderHeader = Union[PlInboundOrder, PlOutboundOrder]
@@ -352,6 +353,19 @@ def update_io_order_items_qty_and_brand(
.all()
)
products_by_id = {r.id: r for r in rows}
# 明细仍指向已软删计划产品行时,用原行品牌约定(避免 brand_mode 空导致只删不写)
missing_ids = [pid for pid in plan_product_ids if pid and pid not in products_by_id]
if missing_ids:
stale_rows = (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.id.in_(missing_ids),
PlPlanProduct.tenant_no == tenant_no,
)
.all()
)
for row in stale_rows:
products_by_id.setdefault(row.id, row)
brand_model = _io_item_brand_qty_model(kind)
@@ -364,6 +378,9 @@ def update_io_order_items_qty_and_brand(
brand_touched = item_brand_qtys is not None and item.id in item_brand_qtys
if brand_touched:
rows = item_brand_qtys[item.id]
# 客户端带了品牌明细时,即使计划行 brand_mode 丢失也按 any 规则校验/落库
if not brand_mode and rows:
brand_mode = "any"
elif enforce_brand:
rows = []
else:
@@ -492,27 +509,34 @@ def merge_plan_product_into_item(
) -> None:
if not product:
item.setdefault("productNo", None)
item.setdefault("categoryName", None)
item.setdefault("packageSpec", None)
item.setdefault("deliveryMethod", None)
item.setdefault("remark", None)
item["unitLabel"] = resolve_unit_label(unit_map, item.get("unit"))
return
# 优先 CRM 产品编码;计划冗余 product_no 可能被误存为 product_id
product_id = (product.product_id or item.get("productId") or "").strip()
product_id = (product.product_id or item.get("productId") or item.get("product_id") or "").strip()
plan_no = (product.product_no or "").strip()
resolved_no = (crm_product_no or "").strip()
if not resolved_no and plan_no and plan_no != product_id:
resolved_no = plan_no
# CRM 缺失时仍回退计划 product_no,避免前端编码列空白
if not resolved_no and plan_no:
resolved_no = plan_no
item["productNo"] = resolved_no or None
item["packageSpec"] = product.package_spec
item["categoryName"] = product.category_name
item["packageSpec"] = product.package_spec or item.get("packageSpec")
item["deliveryMethod"] = product.delivery_method
item["remark"] = (product.remark or "").strip() or None
item["remark"] = (product.remark or "").strip() or item.get("remark")
if not item.get("specification") and product.model:
item["specification"] = product.model
item["unitLabel"] = resolve_unit_label(unit_map, item.get("unit") or product.unit)
# 无品牌拆分量时,回退计划产品上的品牌快照
if product.brand_name and not item.get("brandSummary"):
item["brandName"] = product.brand_name
if product.brand_mode:
item.setdefault("brandMode", product.brand_mode)
def _format_io_product_summary_line(item: dict) -> str:
@@ -562,8 +586,11 @@ def enrich_io_items_with_brand_feedback(
item_id = (it.get("id") or "").strip()
if item_id:
order_item_ids.append(item_id)
pp = products_by_id.get(it.get("planProductId") or "")
product_id = ((pp.product_id if pp else None) or it.get("productId") or "").strip()
plan_product_id = (it.get("planProductId") or it.get("plan_product_id") or "").strip()
pp = products_by_id.get(plan_product_id)
product_id = (
(pp.product_id if pp else None) or it.get("productId") or it.get("product_id") or ""
).strip()
if product_id:
crm_product_ids.add(product_id)
@@ -608,8 +635,11 @@ def enrich_io_items_with_brand_feedback(
)
for it in items:
pp = products_by_id.get(it.get("planProductId") or "")
product_id = ((pp.product_id if pp else None) or it.get("productId") or "").strip()
plan_product_id = (it.get("planProductId") or it.get("plan_product_id") or "").strip()
pp = products_by_id.get(plan_product_id)
product_id = (
(pp.product_id if pp else None) or it.get("productId") or it.get("product_id") or ""
).strip()
if pp:
it["brandMode"] = pp.brand_mode
@@ -667,9 +697,9 @@ def enrich_io_order_dict(
return data
product_ids = [
it.get("planProductId")
(it.get("planProductId") or it.get("plan_product_id") or "").strip()
for it in raw_items
if it.get("planProductId")
if (it.get("planProductId") or it.get("plan_product_id") or "").strip()
]
products_by_id: dict[str, PlPlanProduct] = {}
if product_ids:
@@ -683,6 +713,39 @@ def enrich_io_order_dict(
.all()
)
products_by_id = {r.id: r for r in rows}
# 明细仍挂已软删计划产品时,保留原行用于品牌约定与 CRM 产品定位(勿误用其它在用产品)
missing_ids = [pid for pid in product_ids if pid and pid not in products_by_id]
if missing_ids:
stale_rows = (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.id.in_(missing_ids),
PlPlanProduct.tenant_no == tenant_no,
)
.all()
)
for row in stale_rows:
products_by_id.setdefault(row.id, row)
# 计划产品行可能被编辑重建导致 plan_product_id 失效:按计划再拉全量做 product_id / 序号兜底
plan_products: list[PlPlanProduct] = []
products_by_crm_id: dict[str, PlPlanProduct] = {}
if plan_id:
plan_products = (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.plan_id == plan_id,
PlPlanProduct.tenant_no == tenant_no,
PlPlanProduct.del_flag == 0,
)
.order_by(PlPlanProduct.order_num.asc(), PlPlanProduct.create_time.asc())
.all()
)
for row in plan_products:
products_by_id.setdefault(row.id, row)
crm_key = (row.product_id or "").strip()
if crm_key and crm_key not in products_by_crm_id:
products_by_crm_id[crm_key] = row
crm_ids = {
(pp.product_id or "").strip()
@@ -690,7 +753,7 @@ def enrich_io_order_dict(
if (pp.product_id or "").strip()
}
for it in raw_items:
pid = (it.get("productId") or "").strip()
pid = (it.get("productId") or it.get("product_id") or "").strip()
if pid:
crm_ids.add(pid)
crm_no_by_id: dict[str, str] = {}
@@ -709,15 +772,45 @@ def enrich_io_order_dict(
if no:
crm_no_by_id[row.id] = no
for it in raw_items:
pp = products_by_id.get(it.get("planProductId") or "")
product_id = ((pp.product_id if pp else None) or it.get("productId") or "").strip()
standard_by_id, alias_options_by_id = load_crm_product_alias_maps(
db, tenant_no=tenant_no, product_ids=crm_ids
)
for idx, it in enumerate(raw_items):
plan_product_id = (it.get("planProductId") or it.get("plan_product_id") or "").strip()
if plan_product_id and not it.get("planProductId"):
it["planProductId"] = plan_product_id
pp = products_by_id.get(plan_product_id) if plan_product_id else None
# 仅在明细未挂 planProductId 时用 CRM/序号兜底;已挂 id 时勿换成其它在用产品(会错带指定品牌)
if not pp and not plan_product_id:
crm_key = (it.get("productId") or it.get("product_id") or "").strip()
if crm_key:
pp = products_by_crm_id.get(crm_key)
if not pp and len(plan_products) == len(raw_items):
pp = plan_products[idx]
if pp and not it.get("planProductId"):
it["planProductId"] = pp.id
product_id = (
(pp.product_id if pp else None) or it.get("productId") or it.get("product_id") or ""
).strip()
# 优先用计划产品已存名(可能是别名)做拆分;明细行 productName 同步
saved_name = (pp.product_name if pp else None) or it.get("productName") or it.get(
"product_name"
)
if saved_name:
it["productName"] = saved_name
merge_plan_product_into_item(
it,
pp,
unit_map=unit_map,
crm_product_no=crm_no_by_id.get(product_id),
)
apply_product_alias_display(
it,
product_id=product_id,
standard_by_id=standard_by_id,
alias_options_by_id=alias_options_by_id,
)
if public_unit_as_label:
it["unit"] = it.get("unitLabel") or resolve_unit_label(unit_map, it.get("unit"))