Files
shanxia2.0/backend/app/services/pl_transport_stop_feedback_service.py
T

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]