|
|
@@ -0,0 +1,168 @@
|
|
|
+"""客户装/送货物流默认记忆"""
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import logging
|
|
|
+from decimal import Decimal
|
|
|
+from typing import Any, Mapping, Optional, Sequence
|
|
|
+
|
|
|
+from sqlalchemy.orm import Session
|
|
|
+
|
|
|
+from app.core.utils import generate_uuid
|
|
|
+from app.models.pl_models import PlCustomerStopPref
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+STOP_LOAD = "load"
|
|
|
+STOP_DELIVERY = "delivery"
|
|
|
+
|
|
|
+
|
|
|
+def _as_str(value: Any) -> str:
|
|
|
+ return str(value or "").strip()
|
|
|
+
|
|
|
+
|
|
|
+def _row_to_item(stop_type: str, row: Mapping[str, Any]) -> Optional[dict]:
|
|
|
+ customer_id = _as_str(row.get("customer_id") or row.get("customerId"))
|
|
|
+ if not customer_id:
|
|
|
+ return None
|
|
|
+ lng = row.get("lng")
|
|
|
+ lat = row.get("lat")
|
|
|
+ return {
|
|
|
+ "customer_id": customer_id,
|
|
|
+ "customer_name": _as_str(row.get("customer_name") or row.get("customerName")) or None,
|
|
|
+ "stop_type": stop_type,
|
|
|
+ "address": (row.get("address") if row.get("address") is not None else "") or "",
|
|
|
+ "lng": lng,
|
|
|
+ "lat": lat,
|
|
|
+ "contact": (row.get("contact") if row.get("contact") is not None else "") or "",
|
|
|
+ "notes": (row.get("notes") if row.get("notes") is not None else "") or "",
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def build_stop_pref_upsert_items(
|
|
|
+ *,
|
|
|
+ load: Optional[Mapping[str, Any]],
|
|
|
+ deliveries: Optional[Sequence[Mapping[str, Any]]],
|
|
|
+) -> list[dict]:
|
|
|
+ """抽出待 upsert 条目;同客户同 stop_type 后者覆盖(保持列表中每种键一条)。"""
|
|
|
+ merged: dict[tuple[str, str], dict] = {}
|
|
|
+ if load:
|
|
|
+ item = _row_to_item(STOP_LOAD, load)
|
|
|
+ if item:
|
|
|
+ merged[(item["customer_id"], item["stop_type"])] = item
|
|
|
+ for row in deliveries or []:
|
|
|
+ item = _row_to_item(STOP_DELIVERY, row)
|
|
|
+ if item:
|
|
|
+ merged[(item["customer_id"], item["stop_type"])] = item
|
|
|
+ return list(merged.values())
|
|
|
+
|
|
|
+
|
|
|
+def get_customer_stop_pref(
|
|
|
+ db: Session,
|
|
|
+ tenant_no: str,
|
|
|
+ customer_id: str,
|
|
|
+ stop_type: str,
|
|
|
+) -> Optional[PlCustomerStopPref]:
|
|
|
+ cid = _as_str(customer_id)
|
|
|
+ st = _as_str(stop_type)
|
|
|
+ if not cid or st not in (STOP_LOAD, STOP_DELIVERY):
|
|
|
+ return None
|
|
|
+ return (
|
|
|
+ db.query(PlCustomerStopPref)
|
|
|
+ .filter(
|
|
|
+ PlCustomerStopPref.tenant_no == tenant_no,
|
|
|
+ PlCustomerStopPref.customer_id == cid,
|
|
|
+ PlCustomerStopPref.stop_type == st,
|
|
|
+ PlCustomerStopPref.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def upsert_customer_stop_pref(
|
|
|
+ db: Session,
|
|
|
+ *,
|
|
|
+ tenant_no: str,
|
|
|
+ item: Mapping[str, Any],
|
|
|
+ user_id: Optional[str],
|
|
|
+ dept_id: Optional[str],
|
|
|
+) -> None:
|
|
|
+ customer_id = _as_str(item.get("customer_id"))
|
|
|
+ stop_type = _as_str(item.get("stop_type"))
|
|
|
+ if not customer_id or stop_type not in (STOP_LOAD, STOP_DELIVERY):
|
|
|
+ return
|
|
|
+ row = get_customer_stop_pref(db, tenant_no, customer_id, stop_type)
|
|
|
+ lng = item.get("lng")
|
|
|
+ lat = item.get("lat")
|
|
|
+ if row is None:
|
|
|
+ row = PlCustomerStopPref(
|
|
|
+ id=generate_uuid(),
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ customer_id=customer_id,
|
|
|
+ create_by=user_id,
|
|
|
+ create_dept_id=dept_id or "",
|
|
|
+ )
|
|
|
+ db.add(row)
|
|
|
+ row.customer_name = item.get("customer_name")
|
|
|
+ row.stop_type = stop_type
|
|
|
+ row.address = item.get("address") if item.get("address") is not None else ""
|
|
|
+ row.lng = Decimal(str(lng)) if lng is not None and str(lng) != "" else None
|
|
|
+ row.lat = Decimal(str(lat)) if lat is not None and str(lat) != "" else None
|
|
|
+ row.contact = item.get("contact") if item.get("contact") is not None else ""
|
|
|
+ row.notes = item.get("notes") if item.get("notes") is not None else ""
|
|
|
+ row.update_by = user_id
|
|
|
+ row.del_flag = 0
|
|
|
+
|
|
|
+
|
|
|
+def upsert_prefs_from_plan_stops(
|
|
|
+ db: Session,
|
|
|
+ *,
|
|
|
+ tenant_no: str,
|
|
|
+ load: Optional[Mapping[str, Any]],
|
|
|
+ deliveries: Optional[Sequence[Mapping[str, Any]]],
|
|
|
+ user_id: Optional[str],
|
|
|
+ dept_id: Optional[str],
|
|
|
+) -> None:
|
|
|
+ """供 plan 保存调用;异常由调用方用 savepoint 吞掉。"""
|
|
|
+ for item in build_stop_pref_upsert_items(load=load, deliveries=deliveries):
|
|
|
+ upsert_customer_stop_pref(
|
|
|
+ db,
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ item=item,
|
|
|
+ user_id=user_id,
|
|
|
+ dept_id=dept_id,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def safe_upsert_prefs_from_plan_stops(
|
|
|
+ db: Session,
|
|
|
+ *,
|
|
|
+ tenant_no: str,
|
|
|
+ load: Optional[Any],
|
|
|
+ deliveries: Optional[Sequence[Any]],
|
|
|
+ user_id: Optional[str],
|
|
|
+ dept_id: Optional[str],
|
|
|
+) -> None:
|
|
|
+ """savepoint 包裹;失败只记日志,不抛给计划主流程。"""
|
|
|
+ load_map = None
|
|
|
+ if load is not None:
|
|
|
+ load_map = load.model_dump(by_alias=False) if hasattr(load, "model_dump") else dict(load)
|
|
|
+ delivery_maps = []
|
|
|
+ for d in deliveries or []:
|
|
|
+ delivery_maps.append(
|
|
|
+ d.model_dump(by_alias=False) if hasattr(d, "model_dump") else dict(d)
|
|
|
+ )
|
|
|
+ try:
|
|
|
+ with db.begin_nested():
|
|
|
+ upsert_prefs_from_plan_stops(
|
|
|
+ db,
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ load=load_map,
|
|
|
+ deliveries=delivery_maps,
|
|
|
+ user_id=user_id,
|
|
|
+ dept_id=dept_id,
|
|
|
+ )
|
|
|
+ except Exception:
|
|
|
+ logger.exception(
|
|
|
+ "upsert customer stop pref failed tenant=%s (plan save continues)",
|
|
|
+ tenant_no,
|
|
|
+ )
|