944 lines
31 KiB
Python
944 lines
31 KiB
Python
"""运输计划 · 司机/现场按段回传"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Any, Literal, Optional
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.config import settings
|
|
from app.core.utils import generate_uuid
|
|
from app.models.pl_models import (
|
|
PlPlanDelivery,
|
|
PlPlanDeliveryProduct,
|
|
PlPlanLoad,
|
|
PlPlanLoadProduct,
|
|
PlTransport,
|
|
PlTransportDriverStopFeedback,
|
|
PlTransportSiteStopFeedback,
|
|
)
|
|
from app.services.pl_internal_customer import is_internal_customer_name
|
|
|
|
StopType = Literal["load", "delivery"]
|
|
|
|
|
|
@dataclass
|
|
class PublicStopProductRow:
|
|
stop_product_id: str
|
|
plan_product_id: str
|
|
product_id: str
|
|
product_name: str
|
|
model: Optional[str]
|
|
unit: str
|
|
package_spec: Optional[str]
|
|
delivery_method: Optional[str]
|
|
plan_qty: Optional[Decimal]
|
|
remark: Optional[str]
|
|
actual_qty: Optional[Decimal] = None
|
|
brand_qtys: Optional[list[dict[str, Any]]] = None
|
|
|
|
|
|
@dataclass
|
|
class PublicStopSegment:
|
|
stop_type: StopType
|
|
stop_id: str
|
|
seq: int
|
|
label: str
|
|
customer_name: str
|
|
address: Optional[str]
|
|
contact: Optional[str]
|
|
notes: Optional[str]
|
|
business_type: Optional[str]
|
|
plan_qty: Optional[Decimal]
|
|
attachment_urls: list[str]
|
|
remark: Optional[str]
|
|
feedback_time: Optional[datetime]
|
|
lng: Optional[Decimal] = None
|
|
lat: Optional[Decimal] = None
|
|
quantity: Optional[Decimal] = None
|
|
products: list[PublicStopProductRow] = field(default_factory=list)
|
|
|
|
|
|
def build_transport_site_url(token: str) -> str:
|
|
origin = (settings.PUBLIC_WEB_ORIGIN or "").rstrip("/")
|
|
return f"{origin}/#/public/pl/transport-site/{token}"
|
|
|
|
|
|
def ensure_transport_site_token(transport: PlTransport) -> str:
|
|
token = (transport.site_access_token or "").strip()
|
|
if not token:
|
|
token = generate_uuid().replace("-", "")[:32]
|
|
transport.site_access_token = token
|
|
return token
|
|
|
|
|
|
def rotate_transport_site_token(transport: PlTransport) -> str:
|
|
token = generate_uuid().replace("-", "")[:32]
|
|
transport.site_access_token = token
|
|
return token
|
|
|
|
|
|
def _parse_urls(raw: Optional[str]) -> list[str]:
|
|
if not raw:
|
|
return []
|
|
try:
|
|
parsed = json.loads(raw)
|
|
if isinstance(parsed, list):
|
|
return [str(item).strip() for item in parsed if str(item).strip()]
|
|
except (TypeError, json.JSONDecodeError):
|
|
pass
|
|
return [part.strip() for part in raw.split(",") if part.strip()]
|
|
|
|
|
|
def _dump_urls(urls: Optional[list[str]]) -> Optional[str]:
|
|
if urls is None:
|
|
return None
|
|
cleaned = [str(item).strip() for item in urls if str(item).strip()]
|
|
return json.dumps(cleaned, ensure_ascii=False)
|
|
|
|
|
|
def _direction_label(direction: Optional[str]) -> Optional[str]:
|
|
normalized = (direction or "").strip()
|
|
if normalized == "outbound":
|
|
return "出库"
|
|
if normalized == "inbound":
|
|
return "入库"
|
|
return None
|
|
|
|
|
|
def _sum_stop_qty(products: list[Any]) -> Optional[Decimal]:
|
|
total = Decimal("0")
|
|
has_qty = False
|
|
for row in products:
|
|
qty = getattr(row, "plan_qty", None)
|
|
if qty is not None:
|
|
total += Decimal(str(qty))
|
|
has_qty = True
|
|
return total if has_qty else None
|
|
|
|
|
|
def _merge_public_stop_products_by_sku(
|
|
products: list[PublicStopProductRow],
|
|
) -> list[PublicStopProductRow]:
|
|
merged: dict[tuple[str, str], PublicStopProductRow] = {}
|
|
order: list[tuple[str, str]] = []
|
|
for row in products:
|
|
key = (
|
|
str(row.product_id or "").strip(),
|
|
str(row.package_spec or "").strip(),
|
|
)
|
|
existing = merged.get(key)
|
|
if existing is None:
|
|
merged[key] = PublicStopProductRow(
|
|
stop_product_id=row.stop_product_id,
|
|
plan_product_id=row.plan_product_id,
|
|
product_id=row.product_id,
|
|
product_name=row.product_name,
|
|
model=row.model,
|
|
unit=row.unit,
|
|
package_spec=row.package_spec,
|
|
delivery_method=row.delivery_method,
|
|
plan_qty=row.plan_qty,
|
|
remark=row.remark,
|
|
actual_qty=row.actual_qty,
|
|
brand_qtys=list(row.brand_qtys) if row.brand_qtys else None,
|
|
)
|
|
order.append(key)
|
|
continue
|
|
if row.plan_qty is not None:
|
|
existing.plan_qty = (existing.plan_qty or Decimal("0")) + Decimal(str(row.plan_qty))
|
|
if row.actual_qty is not None:
|
|
existing.actual_qty = (existing.actual_qty or Decimal("0")) + Decimal(
|
|
str(row.actual_qty)
|
|
)
|
|
return [merged[k] for k in order]
|
|
|
|
|
|
def _stop_product_row_from_load(row: PlPlanLoadProduct) -> PublicStopProductRow:
|
|
return PublicStopProductRow(
|
|
stop_product_id=row.id,
|
|
plan_product_id=row.plan_product_id,
|
|
product_id=row.product_id,
|
|
product_name=row.product_name or "",
|
|
model=row.model,
|
|
unit=(row.unit or "吨").strip() or "吨",
|
|
package_spec=row.package_spec,
|
|
delivery_method=row.delivery_method,
|
|
plan_qty=row.plan_qty,
|
|
remark=(row.remark or "").strip() or None,
|
|
)
|
|
|
|
|
|
def _stop_product_row_from_delivery(row: PlPlanDeliveryProduct) -> PublicStopProductRow:
|
|
return PublicStopProductRow(
|
|
stop_product_id=row.id,
|
|
plan_product_id=row.plan_product_id,
|
|
product_id=row.product_id,
|
|
product_name=row.product_name or "",
|
|
model=row.model,
|
|
unit=(row.unit or "吨").strip() or "吨",
|
|
package_spec=row.package_spec,
|
|
delivery_method=row.delivery_method,
|
|
plan_qty=row.plan_qty,
|
|
remark=(row.remark or "").strip() or None,
|
|
)
|
|
|
|
|
|
def _parse_product_feedbacks(raw: Optional[str]) -> dict[str, dict[str, Any]]:
|
|
if not raw:
|
|
return {}
|
|
try:
|
|
parsed = json.loads(raw)
|
|
except (TypeError, json.JSONDecodeError):
|
|
return {}
|
|
if not isinstance(parsed, list):
|
|
return {}
|
|
result: dict[str, dict[str, Any]] = {}
|
|
for item in parsed:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
stop_product_id = str(
|
|
item.get("stopProductId") or item.get("stop_product_id") or ""
|
|
).strip()
|
|
if not stop_product_id:
|
|
continue
|
|
result[stop_product_id] = item
|
|
return result
|
|
|
|
|
|
def _dump_product_feedbacks(products: Optional[list[dict[str, Any]]]) -> Optional[str]:
|
|
if products is None:
|
|
return None
|
|
cleaned: list[dict[str, Any]] = []
|
|
seen: set[str] = set()
|
|
for item in products:
|
|
stop_product_id = str(
|
|
item.get("stopProductId") or item.get("stop_product_id") or ""
|
|
).strip()
|
|
if not stop_product_id or stop_product_id in seen:
|
|
from fastapi import HTTPException
|
|
raise HTTPException(422, '回传段产品 id 缺失或重复')
|
|
seen.add(stop_product_id)
|
|
actual_qty = item.get("actualQty", item.get("actual_qty"))
|
|
row: dict[str, Any] = {
|
|
"stopProductId": stop_product_id,
|
|
"actualQty": float(Decimal(str(actual_qty or 0))),
|
|
}
|
|
brand_qtys = item.get("brandQtys", item.get("brand_qtys"))
|
|
if brand_qtys is not None:
|
|
row["brandQtys"] = brand_qtys
|
|
cleaned.append(row)
|
|
return json.dumps(cleaned, ensure_ascii=False)
|
|
|
|
|
|
def _merge_submitted_feedback_fields(raw, products):
|
|
if products is None:
|
|
return None
|
|
old = _parse_product_feedbacks(raw)
|
|
merged = []
|
|
for item in products:
|
|
key = str(item.get('stopProductId') or item.get('stop_product_id') or '').strip()
|
|
fields = dict(old.get(key, {}))
|
|
fields.update({name: value for name, value in item.items() if value is not None})
|
|
merged.append(fields)
|
|
return merged
|
|
|
|
|
|
def _merge_product_feedbacks_into_segment(
|
|
segment: PublicStopSegment,
|
|
feedback_map: dict[str, dict[str, Any]],
|
|
) -> None:
|
|
if not feedback_map or not segment.products:
|
|
return
|
|
for product in segment.products:
|
|
item = feedback_map.get(product.stop_product_id)
|
|
if not item:
|
|
continue
|
|
actual = item.get("actualQty", item.get("actual_qty"))
|
|
if actual is not None:
|
|
product.actual_qty = Decimal(str(actual))
|
|
brand_qtys = item.get("brandQtys") or item.get("brand_qtys")
|
|
if brand_qtys:
|
|
product.brand_qtys = brand_qtys if isinstance(brand_qtys, list) else None
|
|
|
|
|
|
def _is_site_visible_stop(customer_name: Optional[str]) -> bool:
|
|
"""现场页/简述可见段:仅内部客户(山峡/云栋),与是否勾选生成地磅单无关。"""
|
|
return is_internal_customer_name(customer_name)
|
|
|
|
|
|
def list_plan_stop_segments(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan_id: str,
|
|
include_all_for_driver: bool = True,
|
|
site_internal_only: bool = False,
|
|
) -> list[PublicStopSegment]:
|
|
segments: list[PublicStopSegment] = []
|
|
|
|
load = (
|
|
db.query(PlPlanLoad)
|
|
.filter(
|
|
PlPlanLoad.plan_id == plan_id,
|
|
PlPlanLoad.tenant_no == tenant_no,
|
|
PlPlanLoad.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if load:
|
|
products = (
|
|
db.query(PlPlanLoadProduct)
|
|
.filter(
|
|
PlPlanLoadProduct.load_id == load.id,
|
|
PlPlanLoadProduct.tenant_no == tenant_no,
|
|
PlPlanLoadProduct.del_flag == 0,
|
|
)
|
|
.all()
|
|
)
|
|
if include_all_for_driver or (
|
|
site_internal_only and _is_site_visible_stop(load.customer_name)
|
|
):
|
|
stop_products = _merge_public_stop_products_by_sku(
|
|
[
|
|
_stop_product_row_from_load(row)
|
|
for row in sorted(
|
|
products,
|
|
key=lambda item: (item.order_num or 0, item.create_time or datetime.min),
|
|
)
|
|
]
|
|
)
|
|
segments.append(
|
|
PublicStopSegment(
|
|
stop_type="load",
|
|
stop_id=load.id,
|
|
seq=0,
|
|
label="装货",
|
|
customer_name=(load.customer_name or "").strip(),
|
|
address=load.address,
|
|
contact=load.contact,
|
|
notes=load.notes,
|
|
lng=load.lng,
|
|
lat=load.lat,
|
|
business_type=_direction_label(load.dibang_direction),
|
|
plan_qty=_sum_stop_qty(products),
|
|
attachment_urls=[],
|
|
remark=None,
|
|
feedback_time=None,
|
|
products=stop_products,
|
|
)
|
|
)
|
|
|
|
deliveries = (
|
|
db.query(PlPlanDelivery)
|
|
.filter(
|
|
PlPlanDelivery.plan_id == plan_id,
|
|
PlPlanDelivery.tenant_no == tenant_no,
|
|
PlPlanDelivery.del_flag == 0,
|
|
)
|
|
.order_by(PlPlanDelivery.seq.asc(), PlPlanDelivery.create_time.asc())
|
|
.all()
|
|
)
|
|
for delivery in deliveries:
|
|
products = (
|
|
db.query(PlPlanDeliveryProduct)
|
|
.filter(
|
|
PlPlanDeliveryProduct.delivery_id == delivery.id,
|
|
PlPlanDeliveryProduct.tenant_no == tenant_no,
|
|
PlPlanDeliveryProduct.del_flag == 0,
|
|
)
|
|
.all()
|
|
)
|
|
if include_all_for_driver or (
|
|
site_internal_only and _is_site_visible_stop(delivery.customer_name)
|
|
):
|
|
stop_products = _merge_public_stop_products_by_sku(
|
|
[
|
|
_stop_product_row_from_delivery(row)
|
|
for row in sorted(
|
|
products,
|
|
key=lambda item: (item.order_num or 0, item.create_time or datetime.min),
|
|
)
|
|
]
|
|
)
|
|
segments.append(
|
|
PublicStopSegment(
|
|
stop_type="delivery",
|
|
stop_id=delivery.id,
|
|
seq=int(delivery.seq or 0),
|
|
label=f"送货 #{delivery.seq or len(segments)}",
|
|
customer_name=(delivery.customer_name or "").strip(),
|
|
address=delivery.address,
|
|
contact=delivery.contact,
|
|
notes=delivery.notes,
|
|
lng=delivery.lng,
|
|
lat=delivery.lat,
|
|
business_type=_direction_label(delivery.dibang_direction),
|
|
plan_qty=_sum_stop_qty(products),
|
|
attachment_urls=[],
|
|
remark=None,
|
|
feedback_time=None,
|
|
products=stop_products,
|
|
)
|
|
)
|
|
|
|
return segments
|
|
|
|
|
|
def _merge_driver_feedback(
|
|
segments: list[PublicStopSegment],
|
|
feedback_rows: list[PlTransportDriverStopFeedback],
|
|
) -> None:
|
|
feedback_map = {(row.stop_type, row.stop_id): row for row in feedback_rows}
|
|
for segment in segments:
|
|
row = feedback_map.get((segment.stop_type, segment.stop_id))
|
|
if not row:
|
|
continue
|
|
segment.attachment_urls = _parse_urls(row.attachment_urls)
|
|
segment.remark = row.remark
|
|
segment.feedback_time = row.feedback_time
|
|
_merge_product_feedbacks_into_segment(
|
|
segment,
|
|
_parse_product_feedbacks(row.product_feedbacks),
|
|
)
|
|
|
|
|
|
def _merge_site_feedback(
|
|
segments: list[PublicStopSegment],
|
|
feedback_rows: list[PlTransportSiteStopFeedback],
|
|
) -> None:
|
|
feedback_map = {(row.stop_type, row.stop_id): row for row in feedback_rows}
|
|
for segment in segments:
|
|
row = feedback_map.get((segment.stop_type, segment.stop_id))
|
|
if not row:
|
|
continue
|
|
segment.quantity = row.quantity
|
|
segment.attachment_urls = _parse_urls(row.attachment_urls)
|
|
segment.remark = row.remark
|
|
segment.feedback_time = row.feedback_time
|
|
if row.business_type:
|
|
segment.business_type = row.business_type
|
|
_merge_product_feedbacks_into_segment(
|
|
segment,
|
|
_parse_product_feedbacks(row.product_feedbacks),
|
|
)
|
|
|
|
|
|
def load_driver_stop_segments(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan_id: str,
|
|
transport_id: str,
|
|
) -> list[PublicStopSegment]:
|
|
segments = list_plan_stop_segments(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
plan_id=plan_id,
|
|
include_all_for_driver=True,
|
|
site_internal_only=False,
|
|
)
|
|
rows = (
|
|
db.query(PlTransportDriverStopFeedback)
|
|
.filter(
|
|
PlTransportDriverStopFeedback.transport_id == transport_id,
|
|
PlTransportDriverStopFeedback.tenant_no == tenant_no,
|
|
PlTransportDriverStopFeedback.del_flag == 0,
|
|
)
|
|
.all()
|
|
)
|
|
_merge_driver_feedback(segments, rows)
|
|
return segments
|
|
|
|
|
|
def _load_site_stop_feedback_rows(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
transport_id: str,
|
|
) -> list[PlTransportSiteStopFeedback]:
|
|
return (
|
|
db.query(PlTransportSiteStopFeedback)
|
|
.filter(
|
|
PlTransportSiteStopFeedback.transport_id == transport_id,
|
|
PlTransportSiteStopFeedback.tenant_no == tenant_no,
|
|
PlTransportSiteStopFeedback.del_flag == 0,
|
|
)
|
|
.all()
|
|
)
|
|
|
|
|
|
def load_site_stop_segments(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan_id: str,
|
|
transport_id: str,
|
|
) -> list[PublicStopSegment]:
|
|
segments = list_plan_stop_segments(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
plan_id=plan_id,
|
|
include_all_for_driver=False,
|
|
site_internal_only=True,
|
|
)
|
|
_merge_site_feedback(segments, _load_site_stop_feedback_rows(db, tenant_no=tenant_no, transport_id=transport_id))
|
|
return segments
|
|
|
|
|
|
def load_io_order_site_stop_segments(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan_id: str,
|
|
transport_id: str,
|
|
) -> list[PublicStopSegment]:
|
|
"""入库/出库单现场页:展示全部装货/送货段及段内产品。"""
|
|
segments = list_plan_stop_segments(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
plan_id=plan_id,
|
|
include_all_for_driver=True,
|
|
site_internal_only=False,
|
|
)
|
|
_merge_site_feedback(segments, _load_site_stop_feedback_rows(db, tenant_no=tenant_no, transport_id=transport_id))
|
|
return segments
|
|
|
|
|
|
def _get_driver_feedback_row(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
transport_id: str,
|
|
stop_type: str,
|
|
stop_id: str,
|
|
) -> Optional[PlTransportDriverStopFeedback]:
|
|
return (
|
|
db.query(PlTransportDriverStopFeedback)
|
|
.filter(
|
|
PlTransportDriverStopFeedback.transport_id == transport_id,
|
|
PlTransportDriverStopFeedback.tenant_no == tenant_no,
|
|
PlTransportDriverStopFeedback.stop_type == stop_type,
|
|
PlTransportDriverStopFeedback.stop_id == stop_id,
|
|
PlTransportDriverStopFeedback.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
|
|
def save_driver_stop_feedback(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
transport_id: str,
|
|
plan_id: str,
|
|
stop_type: str,
|
|
stop_id: str,
|
|
attachment_urls: list[str],
|
|
products: Optional[list[dict[str, Any]]] = None,
|
|
remark: Optional[str] = None,
|
|
user_id: Optional[str] = None,
|
|
dept_id: str = "",
|
|
) -> PlTransportDriverStopFeedback:
|
|
row = _get_driver_feedback_row(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
transport_id=transport_id,
|
|
stop_type=stop_type,
|
|
stop_id=stop_id,
|
|
)
|
|
now = datetime.now()
|
|
product_feedbacks_dump = _dump_product_feedbacks(
|
|
_merge_submitted_feedback_fields(row.product_feedbacks if row else None, products))
|
|
if row:
|
|
row.attachment_urls = _dump_urls(attachment_urls)
|
|
if product_feedbacks_dump is not None:
|
|
row.product_feedbacks = product_feedbacks_dump
|
|
if remark is not None:
|
|
row.remark = remark
|
|
row.feedback_time = now
|
|
row.update_by = user_id
|
|
else:
|
|
row = PlTransportDriverStopFeedback(
|
|
id=generate_uuid(),
|
|
tenant_no=tenant_no,
|
|
transport_id=transport_id,
|
|
plan_id=plan_id,
|
|
stop_type=stop_type,
|
|
stop_id=stop_id,
|
|
attachment_urls=_dump_urls(attachment_urls),
|
|
product_feedbacks=product_feedbacks_dump,
|
|
remark=remark,
|
|
feedback_time=now,
|
|
create_by=user_id,
|
|
create_dept_id=dept_id,
|
|
)
|
|
db.add(row)
|
|
db.flush()
|
|
return row
|
|
|
|
|
|
def _get_site_feedback_row(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
transport_id: str,
|
|
stop_type: str,
|
|
stop_id: str,
|
|
) -> Optional[PlTransportSiteStopFeedback]:
|
|
return (
|
|
db.query(PlTransportSiteStopFeedback)
|
|
.filter(
|
|
PlTransportSiteStopFeedback.transport_id == transport_id,
|
|
PlTransportSiteStopFeedback.tenant_no == tenant_no,
|
|
PlTransportSiteStopFeedback.stop_type == stop_type,
|
|
PlTransportSiteStopFeedback.stop_id == stop_id,
|
|
PlTransportSiteStopFeedback.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
|
|
def _sum_product_actual_qty(products: Optional[list[dict[str, Any]]]) -> Optional[Decimal]:
|
|
if not products:
|
|
return None
|
|
total = Decimal("0")
|
|
has_qty = False
|
|
for item in products:
|
|
actual = item.get("actualQty", item.get("actual_qty"))
|
|
if actual is None:
|
|
continue
|
|
qty = Decimal(str(actual))
|
|
if qty > 0:
|
|
total += qty
|
|
has_qty = True
|
|
return total if has_qty else None
|
|
|
|
|
|
def _has_positive_feedback_qty(
|
|
quantity: Optional[Decimal],
|
|
products: Optional[list[dict[str, Any]]],
|
|
) -> bool:
|
|
product_total = _sum_product_actual_qty(products)
|
|
if product_total is not None:
|
|
return product_total > 0
|
|
return quantity is not None and quantity > 0
|
|
|
|
|
|
def save_site_stop_feedback(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
transport_id: str,
|
|
plan_id: str,
|
|
stop_type: str,
|
|
stop_id: str,
|
|
business_type: str,
|
|
quantity: Optional[Decimal] = None,
|
|
attachment_urls: Optional[list[str]] = None,
|
|
products: Optional[list[dict[str, Any]]] = None,
|
|
remark: Optional[str] = None,
|
|
user_id: Optional[str] = None,
|
|
dept_id: str = "",
|
|
require_qty_before_attachments: bool = True,
|
|
) -> PlTransportSiteStopFeedback:
|
|
product_feedbacks_dump = _dump_product_feedbacks(products)
|
|
effective_qty = quantity
|
|
if products is not None:
|
|
effective_qty = _sum_product_actual_qty(products)
|
|
|
|
if require_qty_before_attachments and attachment_urls and not _has_positive_feedback_qty(
|
|
effective_qty, products
|
|
):
|
|
raise ValueError("请先填写数量,再上传附件")
|
|
|
|
row = _get_site_feedback_row(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
transport_id=transport_id,
|
|
stop_type=stop_type,
|
|
stop_id=stop_id,
|
|
)
|
|
product_feedbacks_dump = _dump_product_feedbacks(
|
|
_merge_submitted_feedback_fields(row.product_feedbacks if row else None, products))
|
|
now = datetime.now()
|
|
if row:
|
|
if effective_qty is not None:
|
|
row.quantity = effective_qty
|
|
if attachment_urls is not None:
|
|
row.attachment_urls = _dump_urls(attachment_urls)
|
|
if product_feedbacks_dump is not None:
|
|
row.product_feedbacks = product_feedbacks_dump
|
|
if remark is not None:
|
|
row.remark = remark
|
|
row.feedback_time = now
|
|
row.update_by = user_id
|
|
else:
|
|
row = PlTransportSiteStopFeedback(
|
|
id=generate_uuid(),
|
|
tenant_no=tenant_no,
|
|
transport_id=transport_id,
|
|
plan_id=plan_id,
|
|
stop_type=stop_type,
|
|
stop_id=stop_id,
|
|
business_type=business_type,
|
|
quantity=effective_qty,
|
|
attachment_urls=_dump_urls(attachment_urls or []),
|
|
product_feedbacks=product_feedbacks_dump,
|
|
remark=remark,
|
|
feedback_time=now,
|
|
create_by=user_id,
|
|
create_dept_id=dept_id,
|
|
)
|
|
db.add(row)
|
|
db.flush()
|
|
return row
|
|
|
|
|
|
def list_driver_stop_feedback_reads(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
transport_id: str,
|
|
plan_id: str,
|
|
) -> list[dict[str, Any]]:
|
|
segments = load_driver_stop_segments(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
plan_id=plan_id,
|
|
transport_id=transport_id,
|
|
)
|
|
return [
|
|
{
|
|
"stopType": segment.stop_type,
|
|
"stopId": segment.stop_id,
|
|
"label": segment.label,
|
|
"customerName": segment.customer_name,
|
|
"attachmentUrls": segment.attachment_urls,
|
|
"products": [
|
|
{
|
|
"stopProductId": product.stop_product_id,
|
|
"planProductId": product.plan_product_id,
|
|
"productName": product.product_name,
|
|
"model": product.model,
|
|
"unit": product.unit,
|
|
"packageSpec": product.package_spec,
|
|
"quantity": float(product.plan_qty) if product.plan_qty is not None else None,
|
|
"actualQty": float(product.actual_qty)
|
|
if product.actual_qty is not None
|
|
else None,
|
|
"brandQtys": product.brand_qtys,
|
|
}
|
|
for product in segment.products
|
|
if product.actual_qty is not None
|
|
or (product.brand_qtys and len(product.brand_qtys) > 0)
|
|
],
|
|
"remark": segment.remark,
|
|
"feedbackTime": segment.feedback_time,
|
|
}
|
|
for segment in segments
|
|
if segment.attachment_urls
|
|
or segment.remark
|
|
or segment.feedback_time
|
|
or any(
|
|
product.actual_qty is not None
|
|
or (product.brand_qtys and len(product.brand_qtys) > 0)
|
|
for product in segment.products
|
|
)
|
|
]
|
|
|
|
|
|
def list_site_stop_feedback_reads(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
transport_id: str,
|
|
plan_id: str,
|
|
) -> list[dict[str, Any]]:
|
|
segments = load_site_stop_segments(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
plan_id=plan_id,
|
|
transport_id=transport_id,
|
|
)
|
|
return [
|
|
{
|
|
"stopType": segment.stop_type,
|
|
"stopId": segment.stop_id,
|
|
"label": segment.label,
|
|
"customerName": segment.customer_name,
|
|
"businessType": segment.business_type,
|
|
"quantity": float(segment.quantity) if segment.quantity is not None else None,
|
|
"attachmentUrls": segment.attachment_urls,
|
|
"products": [
|
|
{
|
|
"stopProductId": product.stop_product_id,
|
|
"planProductId": product.plan_product_id,
|
|
"productName": product.product_name,
|
|
"model": product.model,
|
|
"unit": product.unit,
|
|
"packageSpec": product.package_spec,
|
|
"quantity": float(product.plan_qty) if product.plan_qty is not None else None,
|
|
"actualQty": float(product.actual_qty)
|
|
if product.actual_qty is not None
|
|
else None,
|
|
"brandQtys": product.brand_qtys,
|
|
}
|
|
for product in segment.products
|
|
if product.actual_qty is not None
|
|
or (product.brand_qtys and len(product.brand_qtys) > 0)
|
|
],
|
|
"remark": segment.remark,
|
|
"feedbackTime": segment.feedback_time,
|
|
}
|
|
for segment in segments
|
|
if segment.quantity is not None
|
|
or segment.attachment_urls
|
|
or segment.remark
|
|
or any(
|
|
product.actual_qty is not None
|
|
or (product.brand_qtys and len(product.brand_qtys) > 0)
|
|
for product in segment.products
|
|
)
|
|
]
|
|
|
|
|
|
CRM_PRODUCT_UNIT = "crm_product_unit"
|
|
|
|
|
|
def _resolve_unit_label(unit_map: dict[str, str], unit_code: Optional[str]) -> str:
|
|
code = (unit_code or "").strip()
|
|
if not code:
|
|
return "吨"
|
|
return unit_map.get(code, code)
|
|
|
|
|
|
def build_public_stop_reads_for_segments(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan_id: str,
|
|
segments: list[PublicStopSegment],
|
|
) -> list[dict[str, Any]]:
|
|
"""为公共页组装装货/送货段及段内产品(含别名、品牌选项)。"""
|
|
from app.models.crm_models import CrmProductBrand
|
|
from app.models.pl_models import PlPlanProduct
|
|
from app.schemas.pl.transport import BrandOption, PublicTransportStopProductRead, PublicTransportStopRead
|
|
from app.services.pl_product_display import load_crm_product_alias_maps, resolve_product_display_names
|
|
from app.utils.excel_export import load_dict_label_maps
|
|
|
|
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()
|
|
)
|
|
plan_products_by_id = {row.id: row for row in plan_products}
|
|
crm_product_ids = {
|
|
(row.product_id or "").strip() for row in plan_products if (row.product_id or "").strip()
|
|
}
|
|
brand_options_by_product: dict[str, list[BrandOption]] = {}
|
|
if crm_product_ids:
|
|
brand_rows = (
|
|
db.query(CrmProductBrand)
|
|
.filter(
|
|
CrmProductBrand.product_id.in_(list(crm_product_ids)),
|
|
CrmProductBrand.tenant_no == tenant_no,
|
|
CrmProductBrand.status == "on",
|
|
CrmProductBrand.del_flag == 0,
|
|
)
|
|
.order_by(CrmProductBrand.order_num.asc(), CrmProductBrand.create_time.asc())
|
|
.all()
|
|
)
|
|
for row in brand_rows:
|
|
brand_options_by_product.setdefault(row.product_id, []).append(
|
|
BrandOption(brand_id=row.id, brand_name=row.brand_name or "")
|
|
)
|
|
|
|
standard_by_id, alias_options_by_id = load_crm_product_alias_maps(
|
|
db, tenant_no=tenant_no, product_ids=crm_product_ids
|
|
)
|
|
unit_map = load_dict_label_maps(db, [CRM_PRODUCT_UNIT], tenant_no=tenant_no).get(
|
|
CRM_PRODUCT_UNIT, {}
|
|
)
|
|
|
|
reads: list[PublicTransportStopRead] = []
|
|
for segment in segments:
|
|
show_product_qty = is_internal_customer_name(segment.customer_name)
|
|
product_reads: list[PublicTransportStopProductRead] = []
|
|
if show_product_qty:
|
|
for row in segment.products:
|
|
plan_prod = plan_products_by_id.get(row.plan_product_id)
|
|
product_id = (
|
|
row.product_id or (plan_prod.product_id if plan_prod else "") or ""
|
|
).strip()
|
|
display_name, alias_name = resolve_product_display_names(
|
|
saved_product_name=row.product_name,
|
|
product_id=product_id,
|
|
standard_by_id=standard_by_id,
|
|
alias_options_by_id=alias_options_by_id,
|
|
)
|
|
brand_qtys = None
|
|
if row.brand_qtys:
|
|
from app.schemas.pl.transport import BrandQtyItem
|
|
|
|
brand_qtys = [
|
|
BrandQtyItem(
|
|
brand_id=str(item.get("brandId") or item.get("brand_id") or ""),
|
|
brand_name=item.get("brandName") or item.get("brand_name"),
|
|
qty=item.get("qty") or 0,
|
|
)
|
|
for item in row.brand_qtys
|
|
if isinstance(item, dict)
|
|
] or None
|
|
product_reads.append(
|
|
PublicTransportStopProductRead(
|
|
stop_product_id=row.stop_product_id,
|
|
plan_product_id=row.plan_product_id,
|
|
product_name=display_name,
|
|
alias_name=alias_name,
|
|
model=row.model,
|
|
package_spec=row.package_spec,
|
|
unit=_resolve_unit_label(unit_map, row.unit),
|
|
quantity=row.plan_qty,
|
|
remark=row.remark,
|
|
brand_mode=plan_prod.brand_mode if plan_prod else None,
|
|
brand_id=plan_prod.brand_id if plan_prod else None,
|
|
brand_name=plan_prod.brand_name if plan_prod else None,
|
|
brand_options=brand_options_by_product.get(product_id) or None,
|
|
actual_qty=row.actual_qty,
|
|
brand_qtys=brand_qtys,
|
|
)
|
|
)
|
|
reads.append(
|
|
PublicTransportStopRead(
|
|
stop_type=segment.stop_type,
|
|
stop_id=segment.stop_id,
|
|
seq=segment.seq,
|
|
label=segment.label,
|
|
customer_name=segment.customer_name,
|
|
address=segment.address,
|
|
contact=segment.contact,
|
|
notes=segment.notes,
|
|
lng=segment.lng,
|
|
lat=segment.lat,
|
|
business_type=segment.business_type,
|
|
plan_qty=segment.plan_qty,
|
|
quantity=segment.quantity,
|
|
show_product_qty=show_product_qty,
|
|
products=product_reads,
|
|
attachment_urls=segment.attachment_urls,
|
|
remark=segment.remark,
|
|
feedback_time=segment.feedback_time,
|
|
)
|
|
)
|
|
return [row.model_dump(by_alias=True) for row in reads]
|
|
|