feat(pl): driver public feedback by product and brand qty

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-31 05:29:21 +08:00
1 parent 0667e8d77b
commit 2a215b2e1f
4 files changed
+436 -72

No files matched your search

+111 -2
View File
@@ -15,7 +15,16 @@ from app.core.config import settings
from app.core.database import get_db
from app.core.response import error_response, success_response
from app.crud import oss as oss_svc
from app.models.pl_models import PlPlan, PlPlanProduct, PlPlanTransport, PlTransport
from app.models.crm_models import CrmProductBrand
from app.models.pl_models import (
PlPlan,
PlPlanProduct,
PlPlanTransport,
PlTransport,
PlTransportProductBrandQty,
PlTransportProductFeedback,
)
from app.schemas.pl.io_order import BrandOption, BrandQtyItem
from app.schemas.pl.resource_snapshot import parse_resource_snapshot
from app.schemas.pl.transport import (
PublicTransportFeedback,
@@ -23,6 +32,7 @@ from app.schemas.pl.transport import (
PublicTransportPersonnelRead,
PublicTransportPlanProductRead,
)
from app.services.pl_feedback_brand_qty import persist_transport_product_feedback
from app.utils.excel_export import load_dict_label_maps
router = APIRouter(prefix="/public/pl/transport", tags=["运输计划-司机公共"])
@@ -128,19 +138,86 @@ def _build_public_page(db: Session, transport: PlTransport) -> PublicTransportPa
.all()
)
crm_product_ids = {(p.product_id or "").strip() for p in products if (p.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 == transport.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 "")
)
feedback_by_plan_product: dict[str, PlTransportProductFeedback] = {}
brand_qtys_by_plan_product: dict[str, list[BrandQtyItem]] = {}
feedback_rows = (
db.query(PlTransportProductFeedback)
.filter(
PlTransportProductFeedback.transport_id == transport.id,
PlTransportProductFeedback.tenant_no == transport.tenant_no,
PlTransportProductFeedback.del_flag == 0,
)
.order_by(PlTransportProductFeedback.order_num.asc())
.all()
)
for fb in feedback_rows:
feedback_by_plan_product[fb.plan_product_id] = fb
if feedback_rows:
feedback_ids = [fb.id for fb in feedback_rows]
brand_qty_rows = (
db.query(PlTransportProductBrandQty)
.filter(
PlTransportProductBrandQty.transport_id == transport.id,
PlTransportProductBrandQty.tenant_no == transport.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(
BrandQtyItem(
brand_id=row.brand_id or "",
brand_name=row.brand_name,
qty=row.qty or 0,
)
)
product_reads = []
unit_map = load_dict_label_maps(
db, [CRM_PRODUCT_UNIT], tenant_no=transport.tenant_no
).get(CRM_PRODUCT_UNIT, {})
for prod in products:
product_id = (prod.product_id or "").strip()
options = brand_options_by_product.get(product_id) or None
fb = feedback_by_plan_product.get(prod.id)
brand_qtys = brand_qtys_by_plan_product.get(prod.id) or None
product_reads.append(
PublicTransportPlanProductRead(
plan_product_id=prod.id,
product_name=prod.product_name or "",
model=prod.model,
package_spec=prod.package_spec,
unit=_resolve_unit_label(unit_map, prod.unit),
quantity=prod.quantity or prod.inbound_estimate_qty or prod.outbound_estimate_qty,
remark=(prod.remark or "").strip() or None,
brand_mode=prod.brand_mode,
brand_id=prod.brand_id,
brand_name=prod.brand_name,
brand_options=options,
actual_qty=fb.actual_qty if fb else None,
brand_qtys=brand_qtys,
)
)
@@ -203,7 +280,39 @@ def save_public_feedback(
if payload.feedback_remark is not None:
transport.feedback_remark = payload.feedback_remark
if payload.feedback_qty is not None:
if payload.products:
link = (
db.query(PlPlanTransport)
.filter(
PlPlanTransport.transport_id == transport.id,
PlPlanTransport.del_flag == 0,
)
.order_by(PlPlanTransport.create_time.desc())
.first()
)
if not link or not link.plan_id:
return error_response(400, "未找到关联计划,无法保存产品回传")
products_payload = [
{
"planProductId": item.plan_product_id,
"actualQty": item.actual_qty,
"brandQtys": [b.model_dump(by_alias=True) for b in (item.brand_qtys or [])],
}
for item in payload.products
]
try:
persist_transport_product_feedback(
db,
tenant_no=transport.tenant_no,
transport=transport,
plan_id=link.plan_id,
products=products_payload,
user_id=None,
enforce_brand=True,
)
except ValueError as exc:
return error_response(400, str(exc))
elif payload.feedback_qty is not None:
transport.feedback_qty = payload.feedback_qty
transport.feedback_time = datetime.now()
+16
View File
@@ -6,6 +6,7 @@ from typing import Optional
from pydantic import BaseModel, Field
from app.schemas.pl.datetime_parse import OptionalPlanDateTime
from app.schemas.pl.io_order import BrandOption, BrandQtyItem
from app.schemas.pl.resource_snapshot import PlanResourceSnapshotMixin
@@ -188,20 +189,35 @@ class TransportFeedback(BaseModel):
model_config = {"populate_by_name": True}
class PublicTransportProductFeedbackItem(BaseModel):
plan_product_id: str = Field(..., alias="planProductId")
actual_qty: Decimal = Field(..., alias="actualQty", ge=0)
brand_qtys: Optional[list[BrandQtyItem]] = Field(default=None, alias="brandQtys")
model_config = {"populate_by_name": True}
class PublicTransportFeedback(BaseModel):
feedback_images: Optional[list[str]] = Field(default=None, alias="feedbackImages")
feedback_remark: Optional[str] = Field(default=None, alias="feedbackRemark")
feedback_qty: Optional[Decimal] = Field(default=None, alias="feedbackQty", ge=0, description="回传数量")
products: Optional[list[PublicTransportProductFeedbackItem]] = None
model_config = {"populate_by_name": True}
class PublicTransportPlanProductRead(BaseModel):
plan_product_id: str = Field(alias="planProductId")
product_name: str = Field(alias="productName")
model: Optional[str] = None
package_spec: Optional[str] = Field(default=None, alias="packageSpec")
unit: Optional[str] = None
quantity: Optional[Decimal] = None
remark: Optional[str] = Field(default=None, alias="remark", description="运输计划产品备注")
brand_mode: Optional[str] = Field(default=None, alias="brandMode")
brand_id: Optional[str] = Field(default=None, alias="brandId")
brand_name: Optional[str] = Field(default=None, alias="brandName")
brand_options: Optional[list[BrandOption]] = Field(default=None, alias="brandOptions")
actual_qty: Optional[Decimal] = Field(default=None, alias="actualQty")
brand_qtys: Optional[list[BrandQtyItem]] = Field(default=None, alias="brandQtys")
model_config = {"populate_by_name": True}