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>
305 lines
9.6 KiB
Python
305 lines
9.6 KiB
Python
"""现场入出库明细与运输回传的品牌数量校验与持久化"""
|
|
from decimal import Decimal
|
|
from typing import Any, Optional, Type
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.utils import generate_uuid
|
|
from app.services.pl_brand_archive import validate_brand_qty_rows
|
|
|
|
|
|
def format_brand_qty_summary(rows: list[dict]) -> Optional[str]:
|
|
parts = []
|
|
for r in rows:
|
|
qty = Decimal(str(r.get("qty") or 0))
|
|
if qty <= 0:
|
|
continue
|
|
name = (r.get("brandName") or r.get("brand_name") or "").strip() or "未命名品牌"
|
|
qty_text = format(qty.normalize(), "f")
|
|
parts.append(f"{name} {qty_text}")
|
|
return " / ".join(parts) if parts else None
|
|
|
|
|
|
def require_brand_qtys_if_needed(
|
|
*,
|
|
brand_mode: Optional[str],
|
|
required_brand_id: Optional[str],
|
|
actual_qty: Decimal,
|
|
rows: list[dict],
|
|
enforce: bool,
|
|
) -> None:
|
|
if not brand_mode:
|
|
return
|
|
if not enforce:
|
|
return
|
|
positive = [r for r in rows if Decimal(str(r.get("qty") or 0)) > 0]
|
|
if not positive:
|
|
raise ValueError("有品牌产品须填写至少一个品牌数量")
|
|
validate_brand_qty_rows(
|
|
brand_mode=brand_mode,
|
|
required_brand_id=required_brand_id,
|
|
actual_qty=Decimal(str(actual_qty)),
|
|
rows=rows,
|
|
)
|
|
|
|
|
|
def persist_io_item_brand_qtys(
|
|
db: Session,
|
|
*,
|
|
model: Type[Any],
|
|
tenant_no: str,
|
|
order_id: str,
|
|
order_item_id: str,
|
|
plan_product_id: str,
|
|
brand_mode: Optional[str],
|
|
rows: list[dict],
|
|
user_id: Optional[str] = None,
|
|
) -> None:
|
|
existing = (
|
|
db.query(model)
|
|
.filter(
|
|
model.order_item_id == order_item_id,
|
|
model.tenant_no == tenant_no,
|
|
model.del_flag == 0,
|
|
)
|
|
.all()
|
|
)
|
|
for row in existing:
|
|
row.del_flag = 1
|
|
row.update_by = user_id
|
|
|
|
# 客户端显式提交了品牌明细时必须落库;勿因计划行 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):
|
|
qty = Decimal(str(item.get("qty") or 0))
|
|
if qty <= 0:
|
|
continue
|
|
db.add(
|
|
model(
|
|
id=generate_uuid(),
|
|
tenant_no=tenant_no,
|
|
order_id=order_id,
|
|
order_item_id=order_item_id,
|
|
plan_product_id=plan_product_id,
|
|
brand_id=item.get("brandId") or item.get("brand_id") or "",
|
|
brand_name=item.get("brandName") or item.get("brand_name"),
|
|
qty=qty,
|
|
order_num=idx,
|
|
create_by=user_id,
|
|
create_dept_id="",
|
|
update_by=user_id,
|
|
)
|
|
)
|
|
|
|
|
|
def _dict_field(item: dict, *keys: str):
|
|
for key in keys:
|
|
if key in item and item[key] is not None:
|
|
return item[key]
|
|
return None
|
|
|
|
|
|
def persist_transport_product_feedback(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
transport,
|
|
plan_id: str,
|
|
products: list[dict],
|
|
user_id: Optional[str] = None,
|
|
enforce_brand: bool = True,
|
|
) -> None:
|
|
from app.models.pl_models import (
|
|
PlPlanProduct,
|
|
PlTransportProductBrandQty,
|
|
PlTransportProductFeedback,
|
|
)
|
|
|
|
transport_id = transport.id
|
|
|
|
for row in (
|
|
db.query(PlTransportProductFeedback)
|
|
.filter(
|
|
PlTransportProductFeedback.transport_id == transport_id,
|
|
PlTransportProductFeedback.tenant_no == tenant_no,
|
|
PlTransportProductFeedback.del_flag == 0,
|
|
)
|
|
.all()
|
|
):
|
|
row.del_flag = 1
|
|
row.update_by = user_id
|
|
|
|
for row in (
|
|
db.query(PlTransportProductBrandQty)
|
|
.filter(
|
|
PlTransportProductBrandQty.transport_id == transport_id,
|
|
PlTransportProductBrandQty.tenant_no == tenant_no,
|
|
PlTransportProductBrandQty.del_flag == 0,
|
|
)
|
|
.all()
|
|
):
|
|
row.del_flag = 1
|
|
row.update_by = user_id
|
|
|
|
total_qty = Decimal("0")
|
|
|
|
for idx, item in enumerate(products):
|
|
plan_product_id = _dict_field(item, "planProductId", "plan_product_id")
|
|
actual_qty = Decimal(str(_dict_field(item, "actualQty", "actual_qty") or 0))
|
|
brand_qtys = _dict_field(item, "brandQtys", "brand_qtys") or []
|
|
total_qty += actual_qty
|
|
|
|
plan_product = (
|
|
db.query(PlPlanProduct)
|
|
.filter(
|
|
PlPlanProduct.id == plan_product_id,
|
|
PlPlanProduct.tenant_no == tenant_no,
|
|
PlPlanProduct.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
brand_mode = _dict_field(item, "brandMode", "brand_mode")
|
|
required_brand_id = _dict_field(item, "brandId", "brand_id")
|
|
if plan_product:
|
|
brand_mode = brand_mode or plan_product.brand_mode
|
|
required_brand_id = required_brand_id if required_brand_id is not None else plan_product.brand_id
|
|
|
|
require_brand_qtys_if_needed(
|
|
brand_mode=brand_mode,
|
|
required_brand_id=required_brand_id,
|
|
actual_qty=actual_qty,
|
|
rows=brand_qtys,
|
|
enforce=enforce_brand,
|
|
)
|
|
|
|
feedback_id = generate_uuid()
|
|
db.add(
|
|
PlTransportProductFeedback(
|
|
id=feedback_id,
|
|
tenant_no=tenant_no,
|
|
transport_id=transport_id,
|
|
plan_id=plan_id,
|
|
plan_product_id=plan_product_id,
|
|
actual_qty=actual_qty,
|
|
order_num=idx,
|
|
create_by=user_id,
|
|
create_dept_id="",
|
|
update_by=user_id,
|
|
)
|
|
)
|
|
|
|
if not brand_mode:
|
|
continue
|
|
|
|
for bidx, bitem in enumerate(brand_qtys):
|
|
qty = Decimal(str(bitem.get("qty") or 0))
|
|
if qty <= 0:
|
|
continue
|
|
db.add(
|
|
PlTransportProductBrandQty(
|
|
id=generate_uuid(),
|
|
tenant_no=tenant_no,
|
|
transport_id=transport_id,
|
|
plan_product_id=plan_product_id,
|
|
feedback_id=feedback_id,
|
|
brand_id=bitem.get("brandId") or bitem.get("brand_id") or "",
|
|
brand_name=bitem.get("brandName") or bitem.get("brand_name"),
|
|
qty=qty,
|
|
order_num=bidx,
|
|
create_by=user_id,
|
|
create_dept_id="",
|
|
update_by=user_id,
|
|
)
|
|
)
|
|
|
|
transport.feedback_qty = total_qty
|
|
|
|
|
|
def load_transport_driver_product_feedbacks(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
transport_id: str,
|
|
) -> list[dict]:
|
|
"""管理端读运输单时附司机按产品回传及品牌摘要。"""
|
|
from app.models.pl_models import (
|
|
PlPlanProduct,
|
|
PlTransportProductBrandQty,
|
|
PlTransportProductFeedback,
|
|
)
|
|
|
|
feedback_rows = (
|
|
db.query(PlTransportProductFeedback)
|
|
.filter(
|
|
PlTransportProductFeedback.transport_id == transport_id,
|
|
PlTransportProductFeedback.tenant_no == tenant_no,
|
|
PlTransportProductFeedback.del_flag == 0,
|
|
)
|
|
.order_by(PlTransportProductFeedback.order_num.asc())
|
|
.all()
|
|
)
|
|
if not feedback_rows:
|
|
return []
|
|
|
|
plan_product_ids = [fb.plan_product_id for fb in feedback_rows if fb.plan_product_id]
|
|
products_by_id: dict[str, PlPlanProduct] = {}
|
|
if plan_product_ids:
|
|
product_rows = (
|
|
db.query(PlPlanProduct)
|
|
.filter(
|
|
PlPlanProduct.id.in_(plan_product_ids),
|
|
PlPlanProduct.tenant_no == tenant_no,
|
|
PlPlanProduct.del_flag == 0,
|
|
)
|
|
.all()
|
|
)
|
|
products_by_id = {row.id: row for row in product_rows}
|
|
|
|
brand_qtys_by_plan_product: dict[str, list[dict]] = {}
|
|
feedback_ids = [fb.id for fb in feedback_rows]
|
|
brand_qty_rows = (
|
|
db.query(PlTransportProductBrandQty)
|
|
.filter(
|
|
PlTransportProductBrandQty.transport_id == transport_id,
|
|
PlTransportProductBrandQty.tenant_no == tenant_no,
|
|
PlTransportProductBrandQty.del_flag == 0,
|
|
PlTransportProductBrandQty.feedback_id.in_(feedback_ids),
|
|
)
|
|
.order_by(PlTransportProductBrandQty.order_num.asc())
|
|
.all()
|
|
)
|
|
for row in brand_qty_rows:
|
|
brand_qtys_by_plan_product.setdefault(row.plan_product_id, []).append(
|
|
{
|
|
"brandId": row.brand_id or "",
|
|
"brandName": row.brand_name,
|
|
"qty": row.qty,
|
|
}
|
|
)
|
|
|
|
result: list[dict] = []
|
|
for fb in feedback_rows:
|
|
brand_qtys = brand_qtys_by_plan_product.get(fb.plan_product_id, [])
|
|
pp = products_by_id.get(fb.plan_product_id or "")
|
|
result.append(
|
|
{
|
|
"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),
|
|
}
|
|
)
|
|
return result
|