289 lines
10 KiB
Python
289 lines
10 KiB
Python
"""计划归档按品牌数量校验与持久化"""
|
|
from decimal import Decimal
|
|
from typing import Any, Optional
|
|
|
|
from sqlalchemy import or_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.utils import generate_uuid
|
|
from app.models.pl_models import PlPlanProductBrandQty
|
|
|
|
|
|
def _is_plan_level_row(row: PlPlanProductBrandQty) -> bool:
|
|
return not (getattr(row, "stop_product_id", None) or "").strip()
|
|
|
|
|
|
def _row_to_brand_dict(row: PlPlanProductBrandQty) -> dict[str, Any]:
|
|
data: dict[str, Any] = {
|
|
"brandId": row.brand_id or "",
|
|
"brandName": row.brand_name,
|
|
"qty": Decimal(str(row.qty or 0)),
|
|
}
|
|
if row.unit_price is not None:
|
|
data["unitPrice"] = Decimal(str(row.unit_price))
|
|
return data
|
|
|
|
|
|
def _parse_unit_price(item: dict[str, Any]) -> Optional[Decimal]:
|
|
raw = item.get("unitPrice")
|
|
if raw is None:
|
|
raw = item.get("unit_price")
|
|
if raw is None:
|
|
raw = item.get("price")
|
|
if raw is None or raw == "":
|
|
return None
|
|
return Decimal(str(raw))
|
|
|
|
|
|
def load_brand_qtys_by_plan_product_ids(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan_product_ids: list[str],
|
|
) -> dict[str, list[dict[str, Any]]]:
|
|
"""按计划产品明细 ID 批量加载计划级归档品牌数量(brandId/brandName/qty/unitPrice)。"""
|
|
ids = [pid for pid in plan_product_ids if pid]
|
|
if not ids:
|
|
return {}
|
|
rows = (
|
|
db.query(PlPlanProductBrandQty)
|
|
.filter(
|
|
PlPlanProductBrandQty.plan_product_id.in_(ids),
|
|
PlPlanProductBrandQty.tenant_no == tenant_no,
|
|
PlPlanProductBrandQty.del_flag == 0,
|
|
or_(
|
|
PlPlanProductBrandQty.stop_product_id.is_(None),
|
|
PlPlanProductBrandQty.stop_product_id == "",
|
|
),
|
|
)
|
|
.order_by(PlPlanProductBrandQty.order_num.asc())
|
|
.all()
|
|
)
|
|
grouped: dict[str, list[dict[str, Any]]] = {}
|
|
for row in rows:
|
|
qty = Decimal(str(row.qty or 0))
|
|
if qty <= 0:
|
|
continue
|
|
grouped.setdefault(row.plan_product_id, []).append(_row_to_brand_dict(row))
|
|
return grouped
|
|
|
|
|
|
def load_brand_qtys_by_stop_product_ids(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
stop_product_ids: list[str],
|
|
) -> dict[str, list[dict[str, Any]]]:
|
|
"""按装货/送货段产品行 ID 批量加载段级归档品牌数量。"""
|
|
ids = [pid for pid in stop_product_ids if pid]
|
|
if not ids:
|
|
return {}
|
|
rows = (
|
|
db.query(PlPlanProductBrandQty)
|
|
.filter(
|
|
PlPlanProductBrandQty.stop_product_id.in_(ids),
|
|
PlPlanProductBrandQty.tenant_no == tenant_no,
|
|
PlPlanProductBrandQty.del_flag == 0,
|
|
)
|
|
.order_by(PlPlanProductBrandQty.order_num.asc())
|
|
.all()
|
|
)
|
|
grouped: dict[str, list[dict[str, Any]]] = {}
|
|
for row in rows:
|
|
qty = Decimal(str(row.qty or 0))
|
|
if qty <= 0:
|
|
continue
|
|
stop_id = (row.stop_product_id or "").strip()
|
|
if not stop_id:
|
|
continue
|
|
grouped.setdefault(stop_id, []).append(_row_to_brand_dict(row))
|
|
return grouped
|
|
|
|
|
|
def is_archived_for_export_brand(archive_status: Optional[str]) -> bool:
|
|
"""仅「已归档」走归档品牌明细;未归档 / 已暂存走产品指定品牌。"""
|
|
return (archive_status or "").strip() == "archived"
|
|
|
|
|
|
def format_plan_product_constraint_brand(plan_product: Any) -> str:
|
|
"""未归档导出:仅指定品牌写出品牌名。"""
|
|
if plan_product is None:
|
|
return ""
|
|
if (getattr(plan_product, "brand_mode", None) or "").strip() != "required":
|
|
return ""
|
|
return (getattr(plan_product, "brand_name", None) or "").strip()
|
|
|
|
|
|
def resolve_export_brand_qty_rows(
|
|
*,
|
|
stop_product_id: Optional[str],
|
|
plan_product_id: Optional[str],
|
|
by_stop_id: dict[str, list[dict[str, Any]]],
|
|
by_plan_product_id: dict[str, list[dict[str, Any]]],
|
|
) -> list[dict[str, Any]]:
|
|
"""导出用:段级品牌明细优先,否则回退计划级;皆无则空列表。"""
|
|
stop_id = (stop_product_id or "").strip()
|
|
if stop_id:
|
|
stop_rows = by_stop_id.get(stop_id) or []
|
|
if stop_rows:
|
|
return list(stop_rows)
|
|
pp_id = (plan_product_id or "").strip()
|
|
if pp_id:
|
|
return list(by_plan_product_id.get(pp_id) or [])
|
|
return []
|
|
|
|
|
|
def expand_export_rows_by_brand_qty(
|
|
base_row: dict[str, Any],
|
|
brand_rows: list[dict[str, Any]],
|
|
*,
|
|
apply_unit_price: bool = False,
|
|
) -> list[dict[str, Any]]:
|
|
"""按归档品牌拆导出行;无明细时品牌留空并返回单行。"""
|
|
if not brand_rows:
|
|
row = dict(base_row)
|
|
row["brandName"] = ""
|
|
return [row]
|
|
|
|
expanded: list[dict[str, Any]] = []
|
|
for item in brand_rows:
|
|
row = dict(base_row)
|
|
row["brandName"] = str(item.get("brandName") or item.get("brand_name") or "").strip()
|
|
qty = item.get("qty")
|
|
if qty is not None and qty != "":
|
|
qty_dec = Decimal(str(qty))
|
|
row["actualQty"] = qty_dec
|
|
else:
|
|
qty_dec = None
|
|
if apply_unit_price:
|
|
raw_price = item.get("unitPrice")
|
|
if raw_price is None:
|
|
raw_price = item.get("unit_price")
|
|
if raw_price is not None and raw_price != "":
|
|
price_dec = Decimal(str(raw_price))
|
|
row["unitPrice"] = price_dec
|
|
if qty_dec is not None:
|
|
row["amount"] = qty_dec * price_dec
|
|
elif qty_dec is not None and row.get("unitPrice") is not None:
|
|
try:
|
|
row["amount"] = qty_dec * Decimal(str(row["unitPrice"]))
|
|
except Exception:
|
|
pass
|
|
expanded.append(row)
|
|
return expanded
|
|
|
|
|
|
def expand_export_rows_for_plan_brand(
|
|
base_row: dict[str, Any],
|
|
*,
|
|
archive_status: Optional[str],
|
|
plan_product: Any = None,
|
|
stop_product_id: Optional[str] = None,
|
|
plan_product_id: Optional[str] = None,
|
|
by_stop_id: Optional[dict[str, list[dict[str, Any]]]] = None,
|
|
by_plan_product_id: Optional[dict[str, list[dict[str, Any]]]] = None,
|
|
apply_unit_price: bool = False,
|
|
) -> list[dict[str, Any]]:
|
|
"""已归档:按归档品牌明细拆行;未归档/已暂存:产品指定品牌单行。"""
|
|
if is_archived_for_export_brand(archive_status):
|
|
brand_rows = resolve_export_brand_qty_rows(
|
|
stop_product_id=stop_product_id,
|
|
plan_product_id=plan_product_id
|
|
or str(getattr(plan_product, "id", None) or ""),
|
|
by_stop_id=by_stop_id or {},
|
|
by_plan_product_id=by_plan_product_id or {},
|
|
)
|
|
return expand_export_rows_by_brand_qty(
|
|
base_row, brand_rows, apply_unit_price=apply_unit_price
|
|
)
|
|
row = dict(base_row)
|
|
row["brandName"] = format_plan_product_constraint_brand(plan_product)
|
|
return [row]
|
|
|
|
|
|
def validate_brand_qty_rows(
|
|
*,
|
|
brand_mode: Optional[str],
|
|
required_brand_id: Optional[str],
|
|
actual_qty: Decimal,
|
|
rows: list[dict],
|
|
) -> None:
|
|
actual_qty = Decimal(str(actual_qty))
|
|
if not brand_mode:
|
|
return
|
|
if brand_mode in ("required", "any"):
|
|
total = sum((Decimal(str(r.get("qty") or 0)) for r in rows), Decimal("0"))
|
|
if total != actual_qty:
|
|
raise ValueError("各品牌数量合计必须等于归档实际数量")
|
|
return
|
|
raise ValueError("无效的 brand_mode")
|
|
|
|
|
|
def prepare_brand_qty_rows(existing, rows, scope, *, include_price=False):
|
|
from fastapi import HTTPException
|
|
from app.services.pl_child_sync import prepare_keyed_rows
|
|
values = []
|
|
seen = set()
|
|
for item in rows:
|
|
brand_id = str(item.get('brandId') or item.get('brand_id') or '').strip()
|
|
if brand_id in seen:
|
|
raise HTTPException(422, '品牌数量不能重复提交同一品牌')
|
|
seen.add(brand_id)
|
|
qty = Decimal(str(item.get('qty') or 0))
|
|
if not qty.is_finite() or qty < 0:
|
|
raise HTTPException(422, '品牌数量须为非负有限数')
|
|
if qty == 0:
|
|
continue
|
|
value = dict(scope, brand_id=brand_id, qty=qty)
|
|
if item.get('id'):
|
|
value['id'] = item['id']
|
|
if 'brandName' in item or 'brand_name' in item:
|
|
value['brand_name'] = item.get('brandName', item.get('brand_name'))
|
|
if include_price and ('unitPrice' in item or 'unit_price' in item):
|
|
value['unit_price'] = _parse_unit_price(item)
|
|
values.append(value)
|
|
return prepare_keyed_rows(existing, values, ('brand_id',), '品牌数量')
|
|
|
|
|
|
def persist_plan_product_brand_qtys(
|
|
db: Session, *, tenant_no: str, plan_id: str, plan_product_id: str,
|
|
brand_mode: Optional[str], rows: Optional[list[dict[str, Any]]],
|
|
user_id: str, dept_id: str = '',
|
|
) -> None:
|
|
from app.services.pl_child_sync import apply_keyed_rows
|
|
if rows is None:
|
|
return
|
|
existing = db.query(PlPlanProductBrandQty).filter(
|
|
PlPlanProductBrandQty.plan_id == plan_id,
|
|
PlPlanProductBrandQty.plan_product_id == plan_product_id,
|
|
PlPlanProductBrandQty.tenant_no == tenant_no,
|
|
PlPlanProductBrandQty.del_flag == 0).all()
|
|
existing = [row for row in existing if _is_plan_level_row(row)]
|
|
prepared = prepare_brand_qty_rows(existing, rows,
|
|
dict(tenant_no=tenant_no, plan_id=plan_id, plan_product_id=plan_product_id,
|
|
stop_type=None, stop_product_id=None), include_price=True)
|
|
apply_keyed_rows(db, PlPlanProductBrandQty, prepared, user_id=user_id, dept_id=dept_id)
|
|
|
|
|
|
def persist_stop_product_brand_qtys(
|
|
db: Session, *, tenant_no: str, plan_id: str, plan_product_id: str,
|
|
stop_type: str, stop_product_id: str, brand_mode: Optional[str],
|
|
rows: Optional[list[dict[str, Any]]], user_id: str, dept_id: str = '',
|
|
) -> None:
|
|
from app.services.pl_child_sync import apply_keyed_rows
|
|
if rows is None:
|
|
return
|
|
stop_id = (stop_product_id or '').strip()
|
|
if not stop_id:
|
|
return
|
|
existing = db.query(PlPlanProductBrandQty).filter(
|
|
PlPlanProductBrandQty.plan_id == plan_id,
|
|
PlPlanProductBrandQty.stop_type == stop_type,
|
|
PlPlanProductBrandQty.stop_product_id == stop_id,
|
|
PlPlanProductBrandQty.tenant_no == tenant_no,
|
|
PlPlanProductBrandQty.del_flag == 0).all()
|
|
prepared = prepare_brand_qty_rows(existing, rows,
|
|
dict(tenant_no=tenant_no, plan_id=plan_id, plan_product_id=plan_product_id,
|
|
stop_type=stop_type, stop_product_id=stop_id), include_price=True)
|
|
apply_keyed_rows(db, PlPlanProductBrandQty, prepared, user_id=user_id, dept_id=dept_id)
|