This commit is contained in:
zlwu committed 2026-08-27 19:38:01 +08:00
1 parent 4611dbc2b2
commit ebf006eeff
46 files changed
+36802 -20974

No files matched your search

+61 -108
View File
@@ -63,10 +63,15 @@ from app.schemas.pl.plan import (
PlanRead,
PlanSideContractsUpdateBody,
PlanSiteFeedbackUpdateBody,
PlanStopContractsUpdateBody,
PlanUpdate,
PlanWithProducts,
)
from app.services.pl_plan_contract_gap import list_plan_contract_gaps, update_plan_side_contracts
from app.services.pl_plan_contract_gap import (
list_plan_contract_gaps,
update_plan_side_contracts,
update_plan_stop_contracts,
)
from app.services.pl_brand_archive import load_brand_qtys_by_plan_product_ids
from app.services.pl_product_display import (
load_crm_product_alias_maps,
@@ -93,6 +98,7 @@ from app.services.pl_plan_archive_stop_service import (
sum_delivery_outbound_qty,
sync_plan_products_from_stop_archive,
)
from app.services.pl_plan_archive_export import build_plan_archive_export_rows
from app.services.pl_plan_list_attachments import load_plan_document_attachments_by_plan_ids
from app.services.pl_plan_print_service import get_delivery_slip_print_context
from app.services.pl_plan_stop_service import (
@@ -345,6 +351,11 @@ def list_plans(
alias="certUploadStatus",
description="incomplete=至少一张必需危化证书未上传",
),
hazard_cert_ready: Optional[int] = Query(
default=None,
alias="hazardCertReady",
description="0=未确认齐备 1=已确认齐备",
),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=500, alias="pageSize"),
db: Session = Depends(get_db),
@@ -411,6 +422,9 @@ def list_plans(
if (cert_upload_status or "").strip().lower() == "incomplete":
query = query.filter(plan_has_incomplete_hazard_cert_exists(tenant_no))
if hazard_cert_ready is not None:
query = query.filter(PlPlan.hazard_cert_ready == int(hazard_cert_ready))
query = apply_pl_scope(query, PlPlan, user, db, tenant_no)
# 统计总数
@@ -537,37 +551,33 @@ PLAN_EXPORT_COLUMNS = [
PLAN_ARCHIVE_EXPORT_COLUMNS = [
ExportColumn(title="序号", field="rowNo"),
ExportColumn(title="到货日期", field="deliveryDate"),
ExportColumn(title="供方名称", field="supplierName"),
ExportColumn(title="需方名称", field="customerName"),
ExportColumn(title="计划编号", field="planNo"),
ExportColumn(title="计划日期", field="planDate"),
ExportColumn(title="归档状态", field="archiveStatus"),
ExportColumn(title="跟进状态", field="followStatus"),
ExportColumn(title="单据类型", field="planType"),
ExportColumn(title="段次", field="stopKind"),
ExportColumn(title="客户", field="stopCustomer"),
ExportColumn(title="地址", field="stopAddress"),
ExportColumn(title="段次时间", field="stopTime"),
ExportColumn(title="产品名称", field="productName"),
ExportColumn(title="型号/规格", field="model"),
ExportColumn(title="包装", field="packageSpec"),
ExportColumn(title="单位", field="unit"),
ExportColumn(title="入库数", field="inboundQty"),
ExportColumn(title="入库价", field="inboundPrice"),
ExportColumn(title="入库金额", field="inboundAmount"),
ExportColumn(title="出库数", field="outboundQty"),
ExportColumn(title="出库价", field="outboundPrice"),
ExportColumn(title="出库金额", field="outboundAmount"),
ExportColumn(title="计划数量", field="planQty"),
ExportColumn(title="实际数量", field="actualQty"),
ExportColumn(title="入库数量", field="inboundQty"),
ExportColumn(title="出库数量", field="outboundQty"),
ExportColumn(title="单价(元)", field="unitPrice"),
ExportColumn(title="金额(元)", field="amount"),
ExportColumn(title="车牌号", field="licensePlate"),
ExportColumn(title="运费单位", field="freightType"),
ExportColumn(title="运费单价", field="freightPrice"),
ExportColumn(title="运费金额", field="freightAmount"),
ExportColumn(title="ERP", field="erpEntered"),
ExportColumn(title="备注", field="remark"),
]
PLAN_ARCHIVE_FREIGHT_TYPE_LABELS = {
"by_weight": "按重量",
"by_trip": "按趟",
}
def _format_plan_delivery_date_for_export(plan: PlPlan) -> str:
if plan.delivery_time is None:
return ""
return str(plan.delivery_time)[:10]
def _load_transports_by_id(db: Session, tenant_no: str, transport_ids: list[str]) -> dict[str, PlTransport]:
if not transport_ids:
@@ -584,89 +594,6 @@ def _load_transports_by_id(db: Session, tenant_no: str, transport_ids: list[str]
return {row.id: row for row in rows}
def _load_products_by_plan_id(db: Session, tenant_no: str, plan_ids: list[str]) -> dict[str, list[PlPlanProduct]]:
if not plan_ids:
return {}
products = (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.tenant_no == tenant_no,
PlPlanProduct.del_flag == 0,
PlPlanProduct.plan_id.in_(plan_ids),
)
.order_by(PlPlanProduct.order_num)
.all()
)
grouped: dict[str, list[PlPlanProduct]] = {}
for product in products:
grouped.setdefault(product.plan_id, []).append(product)
return grouped
def _build_plan_archive_export_rows(
plans: list[PlPlan],
products_by_plan: dict[str, list[PlPlanProduct]],
transports_by_id: dict[str, PlTransport],
) -> list[dict]:
rows: list[dict] = []
row_no = 0
for plan in plans:
products = products_by_plan.get(plan.id, [])
transport = transports_by_id.get(plan.transport_id) if plan.transport_id else None
freight_type_raw = transport.freight_type if transport else "by_weight"
freight_type_label = PLAN_ARCHIVE_FREIGHT_TYPE_LABELS.get(
freight_type_raw, freight_type_raw or ""
)
freight_price = transport.freight_price if transport else None
freight_amount = plan.freight_amount
if freight_amount is None and transport is not None:
freight_amount = transport.freight_amount
plan_common = {
"deliveryDate": _format_plan_delivery_date_for_export(plan),
"supplierName": plan.supplier_name or "",
"customerName": plan.customer_name or "",
"licensePlate": plan.license_plate or "",
"freightType": freight_type_label,
"freightPrice": freight_price,
"freightAmount": freight_amount,
"remark": "",
}
product_rows = products or [None]
for product in product_rows:
row_no += 1
row = {
**plan_common,
"rowNo": row_no,
"productName": "",
"model": "",
"packageSpec": "",
"unit": "",
"inboundQty": None,
"inboundPrice": None,
"inboundAmount": None,
"outboundQty": None,
"outboundPrice": None,
"outboundAmount": None,
}
if product is not None:
row.update(
{
"productName": product.product_name or "",
"model": product.model or "",
"packageSpec": product.package_spec or "",
"unit": product.unit or "",
"inboundQty": product.inbound_qty,
"inboundPrice": product.inbound_price,
"inboundAmount": product.inbound_amount,
"outboundQty": product.outbound_qty,
"outboundPrice": product.outbound_price,
"outboundAmount": product.outbound_amount,
}
)
rows.append(row)
return rows
class PlanExportBody(BaseModel):
q: Optional[str] = None
plan_no: Optional[str] = Field(default=None, alias="planNo")
@@ -681,6 +608,7 @@ class PlanExportBody(BaseModel):
plan_date_to: Optional[date] = Field(default=None, alias="planDateTo")
has_hazard_product: Optional[int] = Field(default=None, alias="hasHazardProduct")
cert_upload_status: Optional[str] = Field(default=None, alias="certUploadStatus")
hazard_cert_ready: Optional[int] = Field(default=None, alias="hazardCertReady")
ids: Optional[List[str]] = None
columns: Optional[List[str]] = None
export_format: Optional[str] = Field(default=None, alias="exportFormat", description="archive=运输计划归档专用导出")
@@ -743,6 +671,8 @@ def export_plans(
query = query.filter(plan_has_hazard_product_exists(tenant_no))
if (body.cert_upload_status or "").strip().lower() == "incomplete":
query = query.filter(plan_has_incomplete_hazard_cert_exists(tenant_no))
if body.hazard_cert_ready is not None:
query = query.filter(PlPlan.hazard_cert_ready == int(body.hazard_cert_ready))
if body.ids:
query = query.filter(PlPlan.id.in_(body.ids))
query = apply_pl_scope(query, PlPlan, user, db, tenant_no)
@@ -750,12 +680,10 @@ def export_plans(
date_str = datetime.now().strftime("%Y%m%d")
is_archive_export = (body.export_format or "").strip().lower() == "archive"
if is_archive_export:
plan_ids = [plan.id for plan in plans]
products_by_plan = _load_products_by_plan_id(db, tenant_no, plan_ids)
transport_ids = [plan.transport_id for plan in plans if plan.transport_id]
transports_by_id = _load_transports_by_id(db, tenant_no, transport_ids)
items = _build_plan_archive_export_rows(plans, products_by_plan, transports_by_id)
cols = PLAN_ARCHIVE_EXPORT_COLUMNS
items = build_plan_archive_export_rows(db, tenant_no, plans, transports_by_id)
cols = filter_export_columns(PLAN_ARCHIVE_EXPORT_COLUMNS, body.columns)
sheet_title = "计划归档"
filename_stem = f"计划归档_{date_str}"
else:
@@ -948,6 +876,31 @@ def export_contract_gaps(
raise HTTPException(status_code=400, detail=str(e)) from e
@router.put("/{id}/stop-contracts", response_model=PlanContractGapRead)
def put_plan_stop_contracts(
id: str,
payload: PlanStopContractsUpdateBody,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
) -> PlanContractGapRead:
"""保存某装货/送货段合同关联(合同补全)。"""
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
plan = _get_plan(db, tenant_no, id, user)
result = update_plan_stop_contracts(
db,
tenant_no=tenant_no,
plan=plan,
stop_type=payload.stop_type,
stop_id=payload.stop_id,
contracts=payload.contracts,
user_id=user.id,
dept_id=user.dept_id,
)
plan.update_by = user.id
db.commit()
return result
@router.put("/{id}/contracts/{side}", response_model=PlanContractGapRead)
def put_plan_side_contracts(
id: str,