提交
This commit is contained in:
1 parent
833544def3
commit
16be6fa22d
204 files changed
+455089
-46491
No files matched your search
@@ -2074,3 +2074,55 @@ def delete_menu_favorite(
|
||||
db, user_id=user_id, tenant_no=tenant_no, menu_path=body.path
|
||||
)
|
||||
return success_response({"path": body.path})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Demo 模板页:Mock 数据由前端组装行后走统一 xlsx 导出(无 Demo 业务表)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DemoXlsxColumnSpec(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
title: str
|
||||
field: str
|
||||
|
||||
|
||||
class DemoXlsxExportBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
sheet_title: str = Field(default="导出", alias="sheetTitle")
|
||||
filename_stem: str = Field(default="export", alias="filenameStem")
|
||||
columns: List[DemoXlsxColumnSpec]
|
||||
rows: List[dict]
|
||||
|
||||
|
||||
@router.post("/demo/xlsx-export")
|
||||
def demo_xlsx_export(
|
||||
body: DemoXlsxExportBody,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Demo 列表导出:接收已筛选行与列定义,生成 xlsx。"""
|
||||
try:
|
||||
rows = body.rows or []
|
||||
if not rows:
|
||||
raise ExportValidationError("没有可导出的数据")
|
||||
if len(rows) > EXPORT_MAX_ROWS:
|
||||
raise ExportValidationError(f"导出超过 {EXPORT_MAX_ROWS} 条,请缩小筛选范围")
|
||||
if not body.columns:
|
||||
raise ExportValidationError("没有可导出的列")
|
||||
cols = [ExportColumn(title=c.title, field=c.field) for c in body.columns]
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
stem = (body.filename_stem or "export").strip() or "export"
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title=body.sheet_title or "导出",
|
||||
columns=cols,
|
||||
rows=rows,
|
||||
filename_stem=f"{stem}_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
+152
-15
@@ -28,6 +28,8 @@ from app.utils.crm_export_columns import (
|
||||
CUSTOMER_EXPORT_COLUMNS,
|
||||
CUSTOMER_FOLLOW_EXPORT_COLUMNS,
|
||||
OPPORTUNITY_EXPORT_COLUMNS,
|
||||
POOL_CUSTOMER_EXPORT_COLUMNS,
|
||||
POOL_RULE_EXPORT_COLUMNS,
|
||||
PRODUCT_EXPORT_COLUMNS,
|
||||
SUPPLIER_INFO_EXPORT_COLUMNS,
|
||||
)
|
||||
@@ -331,6 +333,23 @@ router = APIRouter(prefix="/web/crm", tags=["CRM管理"])
|
||||
# ── 产品分类 ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
def _filter_export_rows_by_ids(rows, ids):
|
||||
"""按导出 ids 裁剪行(dict 或对象)。"""
|
||||
if not ids:
|
||||
return rows
|
||||
id_set = {str(i) for i in ids}
|
||||
out = []
|
||||
for row in rows:
|
||||
if isinstance(row, dict):
|
||||
rid = row.get("id")
|
||||
else:
|
||||
rid = getattr(row, "id", None)
|
||||
if rid is not None and str(rid) in id_set:
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
@router.post("/product-categories/tree")
|
||||
def category_tree(
|
||||
body: ProductCategoryTreeBody = None,
|
||||
@@ -1093,18 +1112,23 @@ def customer_export(
|
||||
user_id,
|
||||
user_info=user_info,
|
||||
page=1,
|
||||
page_size=10000,
|
||||
page_size=EXPORT_MAX_ROWS,
|
||||
max_page_size=EXPORT_MAX_ROWS,
|
||||
**list_body.to_list_kwargs(),
|
||||
)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None))
|
||||
export_cols = filter_export_columns(CUSTOMER_EXPORT_COLUMNS, getattr(list_body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="客户列表",
|
||||
columns=CUSTOMER_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"客户列表_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
@@ -1399,6 +1423,52 @@ def pool_customer_list(
|
||||
return error_response(500, str(e))
|
||||
|
||||
|
||||
|
||||
|
||||
@router.post("/customer/pool/export")
|
||||
@oper_log(title="CRM客户管理", business_type=5, summary="导出公海客户列表")
|
||||
def pool_customer_export(
|
||||
body: PoolCustomerListBody = None,
|
||||
_user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出公海客户列表。"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
list_body = body or PoolCustomerListBody()
|
||||
rows, _ = list_pool_customers(
|
||||
db,
|
||||
tenant_no,
|
||||
page=1,
|
||||
page_size=EXPORT_MAX_ROWS,
|
||||
max_page_size=EXPORT_MAX_ROWS,
|
||||
keyword=list_body.keyword,
|
||||
level=list_body.level,
|
||||
industry=list_body.industry,
|
||||
park_area=list_body.parkArea,
|
||||
)
|
||||
rows = _filter_export_rows_by_ids(rows, list_body.ids)
|
||||
# map telephone -> phone for shared column if needed
|
||||
for r in rows:
|
||||
if isinstance(r, dict) and not r.get("telephone") and r.get("phone"):
|
||||
r["telephone"] = r.get("phone")
|
||||
export_cols = filter_export_columns(POOL_CUSTOMER_EXPORT_COLUMNS, list_body.columns)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="公海客户",
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"公海客户_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
return error_response(500, str(e))
|
||||
|
||||
@router.post("/customer/pool/assign")
|
||||
@oper_log(title="CRM客户管理", business_type=2, business_id_field="customerIds", summary="公海分配客户")
|
||||
def pool_customer_assign(
|
||||
@@ -1667,19 +1737,23 @@ def contact_export(
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
rows, _ = list_contacts(
|
||||
db, tenant_no, page=1, page_size=10000, user_info=user_info,
|
||||
db, tenant_no, page=1, page_size=EXPORT_MAX_ROWS, user_info=user_info,
|
||||
keyword=body.keyword if body else None,
|
||||
customer_id=body.customer_id if body else None,
|
||||
)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None))
|
||||
export_cols = filter_export_columns(CONTACT_EXPORT_COLUMNS, getattr(list_body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="联系人列表",
|
||||
columns=CONTACT_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"联系人列表_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
@@ -1932,19 +2006,23 @@ def follow_export(
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
rows, _ = list_follows(
|
||||
db, tenant_no, page=1, page_size=10000,
|
||||
db, tenant_no, page=1, page_size=EXPORT_MAX_ROWS,
|
||||
keyword=body.keyword if body else None,
|
||||
customer_id=body.customer_id if body else None,
|
||||
)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None))
|
||||
export_cols = filter_export_columns(CUSTOMER_FOLLOW_EXPORT_COLUMNS, getattr(list_body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="跟进记录",
|
||||
columns=CUSTOMER_FOLLOW_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"跟进记录_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
@@ -1985,6 +2063,56 @@ def pool_rule_list(
|
||||
return error_response(500, str(e))
|
||||
|
||||
|
||||
|
||||
|
||||
@router.post("/customer/pool/rule/export")
|
||||
@oper_log(title="CRM公海规则", business_type=5, summary="导出公海规则列表")
|
||||
def pool_rule_export(
|
||||
body: PoolRuleListBody = None,
|
||||
_user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出公海池规则列表。"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
list_body = body or PoolRuleListBody()
|
||||
rows, _ = list_pool_rules(
|
||||
db,
|
||||
tenant_no,
|
||||
user_info=user_info,
|
||||
page=1,
|
||||
page_size=EXPORT_MAX_ROWS,
|
||||
max_page_size=EXPORT_MAX_ROWS,
|
||||
pool_name=list_body.poolName,
|
||||
rule_type=list_body.ruleType,
|
||||
status=list_body.status,
|
||||
)
|
||||
rows = _filter_export_rows_by_ids(rows, list_body.ids)
|
||||
# humanize ruleType / status for export readability
|
||||
type_map = {"no_follow": "未跟进自动回收", "no_deal": "未成交自动回收"}
|
||||
status_map = {"enable": "启用", "disable": "禁用"}
|
||||
for r in rows:
|
||||
if isinstance(r, dict):
|
||||
r["ruleType"] = type_map.get(r.get("ruleType"), r.get("ruleType") or "")
|
||||
r["status"] = status_map.get(r.get("status"), r.get("status") or "")
|
||||
export_cols = filter_export_columns(POOL_RULE_EXPORT_COLUMNS, list_body.columns)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="公海规则",
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"公海规则_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
return error_response(500, str(e))
|
||||
|
||||
@router.post("/customer/pool/rule/detail")
|
||||
def pool_rule_detail(
|
||||
body: PoolRuleDetailBody,
|
||||
@@ -3104,26 +3232,27 @@ def opportunity_export(
|
||||
user_info=user_info,
|
||||
**_opp_list_kwargs(list_body, user_id),
|
||||
page=1,
|
||||
page_size=10000,
|
||||
page_size=EXPORT_MAX_ROWS,
|
||||
)
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None))
|
||||
export_cols = filter_export_columns(OPPORTUNITY_EXPORT_COLUMNS, getattr(list_body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="商机列表",
|
||||
columns=OPPORTUNITY_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"商机列表_{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
include_legacy_filename=False,
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
return error_response(500, str(e))
|
||||
|
||||
|
||||
return error_response(500, str(e))
|
||||
|
||||
|
||||
# ── 供方信息管理(P0,报价单前置) ─────────────────────────────
|
||||
|
||||
|
||||
@@ -3309,19 +3438,23 @@ def supplier_info_export(
|
||||
tenant_no,
|
||||
user_info=user_info,
|
||||
page=1,
|
||||
page_size=10000,
|
||||
page_size=EXPORT_MAX_ROWS,
|
||||
supplier_name=list_body.supplier_name,
|
||||
status=list_body.status,
|
||||
)
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None))
|
||||
export_cols = filter_export_columns(SUPPLIER_INFO_EXPORT_COLUMNS, getattr(list_body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="供方信息",
|
||||
columns=SUPPLIER_INFO_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"供方信息_{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
include_legacy_filename=False,
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
@@ -4112,7 +4245,7 @@ def clue_export(
|
||||
user_id,
|
||||
user_info=user_info,
|
||||
page=1,
|
||||
page_size=10000,
|
||||
page_size=EXPORT_MAX_ROWS,
|
||||
keyword=list_body.keyword or list_body.search_keyword,
|
||||
view_type=list_body.view_type,
|
||||
view_mode=list_body.view_mode,
|
||||
@@ -4137,14 +4270,18 @@ def clue_export(
|
||||
),
|
||||
)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None))
|
||||
export_cols = filter_export_columns(CLUE_EXPORT_COLUMNS, getattr(list_body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="线索列表",
|
||||
columns=CLUE_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"线索列表_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
|
||||
@@ -72,7 +72,12 @@ from app.schemas.crm.contract import (
|
||||
ContractWithdrawBody,
|
||||
)
|
||||
from app.utils.crm_export_columns import CONTRACT_EXPORT_COLUMNS
|
||||
from app.utils.excel_export import export_rows_to_xlsx_response
|
||||
from app.utils.excel_export import (
|
||||
ExportValidationError,
|
||||
export_rows_to_xlsx_response,
|
||||
filter_export_columns,
|
||||
)
|
||||
from app.api.crm import _filter_export_rows_by_ids
|
||||
|
||||
|
||||
def register_contract_routes(router):
|
||||
@@ -472,15 +477,19 @@ def register_contract_routes(router):
|
||||
exclude_none=True,
|
||||
)
|
||||
rows = export_contracts(db, tenant_no, filters=filters)
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None))
|
||||
export_cols = filter_export_columns(CONTRACT_EXPORT_COLUMNS, getattr(list_body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="合同",
|
||||
columns=CONTRACT_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"合同_{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
include_legacy_filename=False,
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
|
||||
@@ -62,7 +62,12 @@ from app.utils.crm_export_columns import (
|
||||
CONTRACT_CHANGE_EXPORT_COLUMNS,
|
||||
CONTRACT_TEMPLATE_EXPORT_COLUMNS,
|
||||
)
|
||||
from app.utils.excel_export import export_rows_to_xlsx_response
|
||||
from app.utils.excel_export import (
|
||||
ExportValidationError,
|
||||
export_rows_to_xlsx_response,
|
||||
filter_export_columns,
|
||||
)
|
||||
from app.api.crm import _filter_export_rows_by_ids
|
||||
|
||||
|
||||
def register_contract_sub_routes(router):
|
||||
@@ -275,11 +280,13 @@ def register_contract_sub_routes(router):
|
||||
exclude_none=True,
|
||||
)
|
||||
rows = export_contract_changes(db, tenant_no, filters=filters)
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None) or getattr(body, "ids", None))
|
||||
export_cols = filter_export_columns(CONTRACT_CHANGE_EXPORT_COLUMNS, getattr(list_body, "columns", None) or getattr(body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="合同变更记录",
|
||||
columns=CONTRACT_CHANGE_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"合同变更_{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
include_legacy_filename=False,
|
||||
@@ -476,11 +483,13 @@ def register_contract_sub_routes(router):
|
||||
exclude_none=True,
|
||||
)
|
||||
rows = export_contract_templates(db, tenant_no, filters=filters)
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None) or getattr(body, "ids", None))
|
||||
export_cols = filter_export_columns(CONTRACT_TEMPLATE_EXPORT_COLUMNS, getattr(list_body, "columns", None) or getattr(body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="合同模板",
|
||||
columns=CONTRACT_TEMPLATE_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"合同模板_{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
include_legacy_filename=False,
|
||||
@@ -627,11 +636,13 @@ def register_contract_sub_routes(router):
|
||||
exclude_none=True,
|
||||
)
|
||||
rows = export_contract_approvals(db, tenant_no, filters=filters)
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None) or getattr(body, "ids", None))
|
||||
export_cols = filter_export_columns(CONTRACT_APPROVAL_EXPORT_COLUMNS, getattr(list_body, "columns", None) or getattr(body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="合同审批记录",
|
||||
columns=CONTRACT_APPROVAL_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"合同审批_{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
include_legacy_filename=False,
|
||||
|
||||
@@ -79,7 +79,13 @@ from app.schemas.crm.quote import (
|
||||
QuoteVersionsBody,
|
||||
)
|
||||
from app.utils.crm_export_columns import QUOTE_EXPORT_COLUMNS
|
||||
from app.utils.excel_export import export_rows_to_xlsx_response
|
||||
from app.utils.excel_export import (
|
||||
EXPORT_MAX_ROWS,
|
||||
ExportValidationError,
|
||||
export_rows_to_xlsx_response,
|
||||
filter_export_columns,
|
||||
)
|
||||
from app.api.crm import _filter_export_rows_by_ids
|
||||
|
||||
|
||||
def register_quote_routes(router):
|
||||
@@ -440,18 +446,22 @@ def register_quote_routes(router):
|
||||
tenant_no,
|
||||
user_info=user_info,
|
||||
page=1,
|
||||
page_size=10000,
|
||||
page_size=EXPORT_MAX_ROWS,
|
||||
filters=list_body.model_dump(by_alias=True, exclude={"page", "pageSize"}),
|
||||
)
|
||||
rows = _filter_export_rows_by_ids(rows, getattr(list_body, "ids", None))
|
||||
export_cols = filter_export_columns(QUOTE_EXPORT_COLUMNS, getattr(list_body, "columns", None))
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="报价单",
|
||||
columns=QUOTE_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"报价单_{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
include_legacy_filename=False,
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -38,6 +38,13 @@ from app.services.db.product_helpers import (
|
||||
resolve_crm_product,
|
||||
)
|
||||
from app.services.db_inventory_site_token import build_site_inventory_url, ensure_site_access_token
|
||||
from app.utils.excel_export import (
|
||||
EXPORT_MAX_ROWS,
|
||||
ExportColumn,
|
||||
ExportValidationError,
|
||||
export_rows_to_xlsx_response,
|
||||
filter_export_columns,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/inventory", tags=["inventory"])
|
||||
|
||||
@@ -272,6 +279,94 @@ def list_inventory(
|
||||
return result_list
|
||||
|
||||
|
||||
INVENTORY_EXPORT_COLUMNS = [
|
||||
ExportColumn(title="ID", field="id"),
|
||||
ExportColumn(title="计划号", field="planNo"),
|
||||
ExportColumn(title="出入库单号", field="batchNo"),
|
||||
ExportColumn(title="日期", field="date"),
|
||||
ExportColumn(title="往来单位", field="partnerCompany"),
|
||||
ExportColumn(title="业务类型", field="businessType"),
|
||||
ExportColumn(title="状态", field="status"),
|
||||
ExportColumn(title="产品", field="product"),
|
||||
ExportColumn(title="规格", field="specification"),
|
||||
ExportColumn(title="车牌号", field="vehicleNo"),
|
||||
ExportColumn(title="数量(吨)", field="quantity"),
|
||||
ExportColumn(title="净重(吨)", field="netWeight"),
|
||||
ExportColumn(title="毛重(吨)", field="grossWeight"),
|
||||
ExportColumn(title="毛重时间", field="grossWeightTime"),
|
||||
ExportColumn(title="皮重(吨)", field="tareWeight"),
|
||||
ExportColumn(title="皮重时间", field="tareWeightTime"),
|
||||
ExportColumn(title="扣杂(吨)", field="deduction"),
|
||||
ExportColumn(title="备注", field="remarks"),
|
||||
ExportColumn(title="创建人", field="createByName"),
|
||||
]
|
||||
|
||||
|
||||
class InventoryExportBody(BaseModel):
|
||||
q: Optional[str] = None
|
||||
plan_no: Optional[str] = Field(default=None, alias="planNo")
|
||||
batch_no: Optional[str] = Field(default=None, alias="batchNo")
|
||||
partner_company: Optional[str] = Field(default=None, alias="partnerCompany")
|
||||
product: Optional[str] = None
|
||||
vehicle_no: Optional[str] = Field(default=None, alias="vehicleNo")
|
||||
date_from: Optional[str] = Field(default=None, alias="dateFrom")
|
||||
date_to: Optional[str] = Field(default=None, alias="dateTo")
|
||||
status: Optional[str] = None
|
||||
business_type: Optional[str] = Field(default=None, alias="businessType")
|
||||
ids: Optional[List[str]] = None
|
||||
columns: Optional[List[str]] = None
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
@router.post("/export")
|
||||
def export_inventory(
|
||||
body: InventoryExportBody,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
):
|
||||
"""导出出入库列表(当前筛选全量,或 ids 已选)。"""
|
||||
try:
|
||||
items = list_inventory(
|
||||
q=body.q,
|
||||
plan_no=body.plan_no,
|
||||
batch_no=body.batch_no,
|
||||
partner_company=body.partner_company,
|
||||
product=body.product,
|
||||
vehicle_no=body.vehicle_no,
|
||||
date_from=body.date_from,
|
||||
date_to=body.date_to,
|
||||
status_filter=body.status,
|
||||
business_type=body.business_type,
|
||||
db=db,
|
||||
user=user,
|
||||
)
|
||||
if body.ids:
|
||||
id_set = set(body.ids)
|
||||
items = [x for x in items if x.id in id_set]
|
||||
if len(items) > EXPORT_MAX_ROWS:
|
||||
raise ExportValidationError(f"导出超过 {EXPORT_MAX_ROWS} 条,请缩小筛选范围")
|
||||
rows = []
|
||||
for item in items:
|
||||
data = item.model_dump(by_alias=True)
|
||||
if not data.get("createByName"):
|
||||
data["createByName"] = data.get("createBy") or ""
|
||||
rows.append(data)
|
||||
cols = filter_export_columns(INVENTORY_EXPORT_COLUMNS, body.columns)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="出入库列表",
|
||||
columns=cols,
|
||||
rows=rows,
|
||||
filename_stem=f"出入库列表_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.post("/", response_model=InboundOutboundRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_inventory(
|
||||
payload: InboundOutboundCreate,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -35,6 +35,13 @@ from app.services.db.product_helpers import (
|
||||
apply_product_snapshot_to_plan,
|
||||
resolve_crm_product,
|
||||
)
|
||||
from app.utils.excel_export import (
|
||||
EXPORT_MAX_ROWS,
|
||||
ExportColumn,
|
||||
ExportValidationError,
|
||||
export_rows_to_xlsx_response,
|
||||
filter_export_columns,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/plans", tags=["plans"])
|
||||
|
||||
@@ -199,6 +206,96 @@ def list_plans(
|
||||
return result_list
|
||||
|
||||
|
||||
PLAN_EXPORT_COLUMNS = [
|
||||
ExportColumn(title="计划号", field="planNo"),
|
||||
ExportColumn(title="业务类型", field="businessType"),
|
||||
ExportColumn(title="往来单位", field="customerName"),
|
||||
ExportColumn(title="计划日期", field="planDate"),
|
||||
ExportColumn(title="起始日期", field="startDate"),
|
||||
ExportColumn(title="截止日期", field="endDate"),
|
||||
ExportColumn(title="产品", field="product"),
|
||||
ExportColumn(title="规格", field="specification"),
|
||||
ExportColumn(title="车牌号", field="vehicleNo"),
|
||||
ExportColumn(title="计划量(吨)", field="plannedQuantity"),
|
||||
ExportColumn(title="已执行量(吨)", field="executedQuantity"),
|
||||
ExportColumn(title="执行状态", field="executionStatus"),
|
||||
ExportColumn(title="备注", field="remarks"),
|
||||
ExportColumn(title="自动生成", field="autoGenerate"),
|
||||
ExportColumn(title="创建人", field="createByName"),
|
||||
]
|
||||
|
||||
|
||||
class PlanExportBody(BaseModel):
|
||||
q: Optional[str] = None
|
||||
plan_no: Optional[str] = Field(default=None, alias="planNo")
|
||||
customer_name: Optional[str] = Field(default=None, alias="customerName")
|
||||
product: Optional[str] = None
|
||||
business_type: Optional[str] = Field(default=None, alias="businessType")
|
||||
execution_status: Optional[str] = Field(default=None, alias="executionStatus")
|
||||
created_from: Optional[str] = Field(default=None, alias="createdFrom")
|
||||
created_to: Optional[str] = Field(default=None, alias="createdTo")
|
||||
start_date_from: Optional[str] = Field(default=None, alias="startDateFrom")
|
||||
start_date_to: Optional[str] = Field(default=None, alias="startDateTo")
|
||||
ids: Optional[List[str]] = None
|
||||
columns: Optional[List[str]] = None
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
@router.post("/export")
|
||||
def export_plans(
|
||||
body: PlanExportBody,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
):
|
||||
"""导出计划列表(当前筛选全量,或 ids 已选)。"""
|
||||
try:
|
||||
items = list_plans(
|
||||
q=body.q,
|
||||
plan_no=body.plan_no,
|
||||
customer_name=body.customer_name,
|
||||
product=body.product,
|
||||
business_type=body.business_type,
|
||||
execution_status=body.execution_status,
|
||||
created_from=body.created_from,
|
||||
created_to=body.created_to,
|
||||
start_date_from=body.start_date_from,
|
||||
start_date_to=body.start_date_to,
|
||||
db=db,
|
||||
user=user,
|
||||
)
|
||||
if body.ids:
|
||||
id_set = set(body.ids)
|
||||
items = [x for x in items if x.id in id_set]
|
||||
if len(items) > EXPORT_MAX_ROWS:
|
||||
raise ExportValidationError(f"导出超过 {EXPORT_MAX_ROWS} 条,请缩小筛选范围")
|
||||
rows = []
|
||||
for item in items:
|
||||
data = item.model_dump(by_alias=True)
|
||||
data["autoGenerate"] = "是" if data.get("autoGenerateEnabled") else "否"
|
||||
eq = data.get("executedQuantity") or 0
|
||||
try:
|
||||
data["executedQuantity"] = round(float(eq) / 1000, 2)
|
||||
except (TypeError, ValueError):
|
||||
data["executedQuantity"] = 0
|
||||
if not data.get("createByName"):
|
||||
data["createByName"] = data.get("createBy") or ""
|
||||
rows.append(data)
|
||||
cols = filter_export_columns(PLAN_EXPORT_COLUMNS, body.columns)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="计划列表",
|
||||
columns=cols,
|
||||
rows=rows,
|
||||
filename_stem=f"计划列表_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
|
||||
def _generate_plan_no() -> str:
|
||||
from app.services.db.plan_helpers import generate_plan_no
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -32,6 +32,13 @@ from app.services.db.product_helpers import (
|
||||
apply_product_snapshot_to_weighing,
|
||||
resolve_crm_product,
|
||||
)
|
||||
from app.utils.excel_export import (
|
||||
EXPORT_MAX_ROWS,
|
||||
ExportColumn,
|
||||
ExportValidationError,
|
||||
export_rows_to_xlsx_response,
|
||||
filter_export_columns,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/weighing", tags=["weighing"])
|
||||
|
||||
@@ -195,6 +202,106 @@ def list_weighing(
|
||||
]
|
||||
|
||||
|
||||
WEIGHING_EXPORT_COLUMNS = [
|
||||
ExportColumn(title="日期", field="inventoryDate"),
|
||||
ExportColumn(title="计划单号", field="planNo"),
|
||||
ExportColumn(title="出入库单号", field="batchNo"),
|
||||
ExportColumn(title="车牌号", field="vehicleNo"),
|
||||
ExportColumn(title="货物名称", field="cargoName"),
|
||||
ExportColumn(title="规格", field="specification"),
|
||||
ExportColumn(title="业务类型", field="businessType"),
|
||||
ExportColumn(title="单据状态", field="docStatus"),
|
||||
ExportColumn(title="供货单位", field="supplier"),
|
||||
ExportColumn(title="收货单位", field="consignee"),
|
||||
ExportColumn(title="本次量(吨)", field="currentQuantity"),
|
||||
ExportColumn(title="净重(吨)", field="netWeight"),
|
||||
ExportColumn(title="毛重(kg)", field="grossWeight"),
|
||||
ExportColumn(title="毛重时间", field="grossWeightTime"),
|
||||
ExportColumn(title="皮重(kg)", field="tareWeight"),
|
||||
ExportColumn(title="皮重时间", field="tareWeightTime"),
|
||||
ExportColumn(title="扣杂(kg)", field="deduction"),
|
||||
ExportColumn(title="创建人", field="createByName"),
|
||||
]
|
||||
|
||||
|
||||
class WeighingExportBody(BaseModel):
|
||||
q: Optional[str] = None
|
||||
date_from: Optional[str] = Field(default=None, alias="dateFrom")
|
||||
date_to: Optional[str] = Field(default=None, alias="dateTo")
|
||||
plan_no: Optional[str] = Field(default=None, alias="planNo")
|
||||
batch_no: Optional[str] = Field(default=None, alias="batchNo")
|
||||
vehicle_no: Optional[str] = Field(default=None, alias="vehicleNo")
|
||||
cargo_name: Optional[str] = Field(default=None, alias="cargoName")
|
||||
supplier: Optional[str] = None
|
||||
consignee: Optional[str] = None
|
||||
business_type: Optional[str] = Field(default=None, alias="businessType")
|
||||
ids: Optional[List[str]] = None
|
||||
columns: Optional[List[str]] = None
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
def _kg_to_ton(value) -> float:
|
||||
try:
|
||||
return round(float(value or 0) / 1000, 2)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
@router.post("/export")
|
||||
def export_weighing(
|
||||
body: WeighingExportBody,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
):
|
||||
"""导出地磅单列表(当前筛选全量,或 ids 已选)。"""
|
||||
try:
|
||||
items = list_weighing(
|
||||
q=body.q,
|
||||
date_from=body.date_from,
|
||||
date_to=body.date_to,
|
||||
plan_no=body.plan_no,
|
||||
batch_no=body.batch_no,
|
||||
vehicle_no=body.vehicle_no,
|
||||
cargo_name=body.cargo_name,
|
||||
supplier=body.supplier,
|
||||
consignee=body.consignee,
|
||||
business_type=body.business_type,
|
||||
db=db,
|
||||
user=user,
|
||||
)
|
||||
if body.ids:
|
||||
id_set = set(body.ids)
|
||||
items = [x for x in items if x.id in id_set]
|
||||
if len(items) > EXPORT_MAX_ROWS:
|
||||
raise ExportValidationError(f"导出超过 {EXPORT_MAX_ROWS} 条,请缩小筛选范围")
|
||||
rows = []
|
||||
for item in items:
|
||||
data = item.model_dump(by_alias=True)
|
||||
data["docStatus"] = "已作废" if data.get("isVoided") == 1 else "正常"
|
||||
data["currentQuantity"] = _kg_to_ton(data.get("currentQuantity"))
|
||||
data["netWeight"] = _kg_to_ton(data.get("netWeight"))
|
||||
if not data.get("inventoryDate"):
|
||||
gwt = data.get("grossWeightTime") or ""
|
||||
data["inventoryDate"] = str(gwt)[:10] if gwt else ""
|
||||
if not data.get("createByName"):
|
||||
data["createByName"] = data.get("createBy") or ""
|
||||
rows.append(data)
|
||||
cols = filter_export_columns(WEIGHING_EXPORT_COLUMNS, body.columns)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="地磅单列表",
|
||||
columns=cols,
|
||||
rows=rows,
|
||||
filename_stem=f"地磅单列表_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.post("/", response_model=WeighingSlipRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_weighing(
|
||||
payload: WeighingSlipCreate,
|
||||
|
||||
@@ -22,10 +22,12 @@ from app.models.pl_models import (
|
||||
PlOutboundOrder,
|
||||
PlOutboundOrderItem,
|
||||
PlPlan,
|
||||
PlPlanProduct,
|
||||
)
|
||||
from app.schemas.pl.io_order import (
|
||||
PublicIoOrderFeedback,
|
||||
PublicIoOrderHistoryPageRead,
|
||||
PublicIoOrderHistoryProductRead,
|
||||
PublicIoOrderHistoryRecordRead,
|
||||
)
|
||||
from app.services.pl_io_order_service import (
|
||||
@@ -34,10 +36,24 @@ from app.services.pl_io_order_service import (
|
||||
order_header_to_dict,
|
||||
parse_feedback_images,
|
||||
)
|
||||
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
|
||||
|
||||
inbound_public_router = APIRouter(prefix="/public/pl/inbound", tags=["运输计划-入库单公共"])
|
||||
outbound_public_router = APIRouter(prefix="/public/pl/outbound", tags=["运输计划-出库单公共"])
|
||||
|
||||
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 _models(kind: str):
|
||||
if kind == "inbound":
|
||||
@@ -99,7 +115,7 @@ def _query_kind_history(
|
||||
keyword: str,
|
||||
) -> list[tuple]:
|
||||
"""返回 [(create_time, PublicIoOrderHistoryRecordRead), ...]。"""
|
||||
header_m, _item_m = _models(kind)
|
||||
header_m, item_m = _models(kind)
|
||||
query = (
|
||||
db.query(header_m, PlPlan)
|
||||
.outerjoin(
|
||||
@@ -115,10 +131,19 @@ def _query_kind_history(
|
||||
kw = (keyword or "").strip()
|
||||
if kw:
|
||||
like = f"%{kw}%"
|
||||
product_match = (
|
||||
db.query(item_m.id)
|
||||
.filter(
|
||||
item_m.order_id == header_m.id,
|
||||
item_m.del_flag == 0,
|
||||
item_m.product_name.like(like),
|
||||
)
|
||||
.exists()
|
||||
)
|
||||
query = query.filter(
|
||||
or_(
|
||||
PlPlan.supplier_name.like(like),
|
||||
PlPlan.customer_name.like(like),
|
||||
header_m.vehicle_no.like(like),
|
||||
product_match,
|
||||
)
|
||||
)
|
||||
rows = query.order_by(header_m.create_time.desc()).all()
|
||||
@@ -140,6 +165,12 @@ def _query_kind_history(
|
||||
supplier_name=plan.supplier_name if plan else None,
|
||||
customer_name=plan.customer_name if plan else None,
|
||||
vehicle_no=header.vehicle_no,
|
||||
pickup_time=plan.pickup_time if plan else None,
|
||||
delivery_time=plan.delivery_time if plan else None,
|
||||
pickup_address=plan.pickup_address if plan else None,
|
||||
destination_address=(
|
||||
(plan.destination_address or plan.destination_name) if plan else None
|
||||
),
|
||||
site_access_token=tok or None,
|
||||
site_url=build_io_order_site_url(kind, tok) if tok else None,
|
||||
status=header.status,
|
||||
@@ -178,16 +209,108 @@ def _build_history_page(
|
||||
page_size = min(max(1, int(page_size or 20)), 500)
|
||||
start = (page - 1) * page_size
|
||||
page_rows = merged[start : start + page_size]
|
||||
records = [r for _, r in page_rows]
|
||||
_attach_history_products(db, tenant_no=header.tenant_no, records=records)
|
||||
return PublicIoOrderHistoryPageRead(
|
||||
date=day.isoformat(),
|
||||
keyword=kw or None,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total=total,
|
||||
records=[r for _, r in page_rows],
|
||||
records=records,
|
||||
)
|
||||
|
||||
|
||||
def _attach_history_products(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
records: list[PublicIoOrderHistoryRecordRead],
|
||||
) -> None:
|
||||
if not records:
|
||||
return
|
||||
|
||||
inbound_ids = [r.order_id for r in records if r.kind == "inbound"]
|
||||
outbound_ids = [r.order_id for r in records if r.kind == "outbound"]
|
||||
items_by_order: dict[str, list] = {}
|
||||
|
||||
if inbound_ids:
|
||||
for item in (
|
||||
db.query(PlInboundOrderItem)
|
||||
.filter(
|
||||
PlInboundOrderItem.order_id.in_(inbound_ids),
|
||||
PlInboundOrderItem.del_flag == 0,
|
||||
)
|
||||
.order_by(PlInboundOrderItem.order_num.asc(), PlInboundOrderItem.create_time.asc())
|
||||
.all()
|
||||
):
|
||||
items_by_order.setdefault(item.order_id, []).append(item)
|
||||
|
||||
if outbound_ids:
|
||||
for item in (
|
||||
db.query(PlOutboundOrderItem)
|
||||
.filter(
|
||||
PlOutboundOrderItem.order_id.in_(outbound_ids),
|
||||
PlOutboundOrderItem.del_flag == 0,
|
||||
)
|
||||
.order_by(PlOutboundOrderItem.order_num.asc(), PlOutboundOrderItem.create_time.asc())
|
||||
.all()
|
||||
):
|
||||
items_by_order.setdefault(item.order_id, []).append(item)
|
||||
|
||||
all_items = [it for items in items_by_order.values() for it in items]
|
||||
plan_product_ids = {
|
||||
(it.plan_product_id or "").strip() for it in all_items if (it.plan_product_id or "").strip()
|
||||
}
|
||||
plan_product_by_id: dict[str, PlPlanProduct] = {}
|
||||
if plan_product_ids:
|
||||
for prod in (
|
||||
db.query(PlPlanProduct)
|
||||
.filter(PlPlanProduct.id.in_(list(plan_product_ids)), PlPlanProduct.del_flag == 0)
|
||||
.all()
|
||||
):
|
||||
plan_product_by_id[prod.id] = prod
|
||||
|
||||
crm_product_ids = {
|
||||
(it.product_id or "").strip() for it in all_items if (it.product_id or "").strip()
|
||||
}
|
||||
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, {}
|
||||
)
|
||||
|
||||
for record in records:
|
||||
products: list[PublicIoOrderHistoryProductRead] = []
|
||||
for item in items_by_order.get(record.order_id) or []:
|
||||
product_id = (item.product_id or "").strip()
|
||||
plan_product = plan_product_by_id.get((item.plan_product_id or "").strip())
|
||||
display_name, _alias = resolve_product_display_names(
|
||||
saved_product_name=item.product_name,
|
||||
product_id=product_id,
|
||||
standard_by_id=standard_by_id,
|
||||
alias_options_by_id=alias_options_by_id,
|
||||
)
|
||||
model = (item.specification or "").strip() or (
|
||||
(plan_product.model or "").strip() if plan_product else ""
|
||||
) or None
|
||||
package_spec = (
|
||||
(plan_product.package_spec or "").strip() if plan_product else ""
|
||||
) or None
|
||||
qty = item.actual_qty if item.actual_qty is not None else item.plan_qty
|
||||
products.append(
|
||||
PublicIoOrderHistoryProductRead(
|
||||
product_name=display_name or item.product_name or "—",
|
||||
model=model,
|
||||
package_spec=package_spec,
|
||||
quantity=qty,
|
||||
unit=_resolve_unit_label(unit_map, item.unit),
|
||||
)
|
||||
)
|
||||
record.products = products
|
||||
|
||||
|
||||
def _build_page(db: Session, kind: str, header, items) -> dict:
|
||||
plan = db.query(PlPlan).filter(PlPlan.id == header.plan_id, PlPlan.del_flag == 0).first()
|
||||
voided, archived = _resolve_io_order_link_flags(plan, header)
|
||||
@@ -216,7 +339,7 @@ def _make_handlers(kind: str):
|
||||
def get_history(
|
||||
token: str,
|
||||
date: str = Query(..., description="筛选日 YYYY-MM-DD"),
|
||||
q: Optional[str] = Query(None, description="供方/需方名称模糊"),
|
||||
q: Optional[str] = Query(None, description="车牌号/产品名称模糊"),
|
||||
page: int = Query(1, ge=1, description="页码,从 1 开始"),
|
||||
page_size: int = Query(20, ge=1, le=500, alias="pageSize", description="每页条数"),
|
||||
db: Session = Depends(get_db),
|
||||
|
||||
@@ -30,6 +30,7 @@ from app.schemas.pl.resource_snapshot import parse_resource_snapshot
|
||||
from app.schemas.pl.transport import (
|
||||
PublicTransportFeedback,
|
||||
PublicTransportHistoryPageRead,
|
||||
PublicTransportHistoryProductRead,
|
||||
PublicTransportHistoryRecordRead,
|
||||
PublicTransportPageRead,
|
||||
PublicTransportPersonnelRead,
|
||||
@@ -389,11 +390,54 @@ def get_public_transport_history(
|
||||
)
|
||||
|
||||
seen_plan: set[str] = set()
|
||||
records: list[PublicTransportHistoryRecordRead] = []
|
||||
plan_rows: list[tuple[PlPlan, PlTransport]] = []
|
||||
for plan, tr in rows:
|
||||
if plan.id in seen_plan:
|
||||
continue
|
||||
seen_plan.add(plan.id)
|
||||
plan_rows.append((plan, tr))
|
||||
|
||||
products_by_plan: dict[str, list[PublicTransportHistoryProductRead]] = {}
|
||||
if plan_rows:
|
||||
plan_ids = [plan.id for plan, _ in plan_rows]
|
||||
product_rows = (
|
||||
db.query(PlPlanProduct)
|
||||
.filter(
|
||||
PlPlanProduct.plan_id.in_(plan_ids),
|
||||
PlPlanProduct.del_flag == 0,
|
||||
)
|
||||
.order_by(PlPlanProduct.order_num.asc(), PlPlanProduct.create_time.asc())
|
||||
.all()
|
||||
)
|
||||
crm_product_ids = {
|
||||
(p.product_id or "").strip() for p in product_rows if (p.product_id or "").strip()
|
||||
}
|
||||
standard_by_id, alias_options_by_id = load_crm_product_alias_maps(
|
||||
db, tenant_no=transport.tenant_no, product_ids=crm_product_ids
|
||||
)
|
||||
unit_map = load_dict_label_maps(
|
||||
db, [CRM_PRODUCT_UNIT], tenant_no=transport.tenant_no
|
||||
).get(CRM_PRODUCT_UNIT, {})
|
||||
for prod in product_rows:
|
||||
product_id = (prod.product_id or "").strip()
|
||||
display_name, _alias_name = resolve_product_display_names(
|
||||
saved_product_name=prod.product_name,
|
||||
product_id=product_id,
|
||||
standard_by_id=standard_by_id,
|
||||
alias_options_by_id=alias_options_by_id,
|
||||
)
|
||||
products_by_plan.setdefault(prod.plan_id, []).append(
|
||||
PublicTransportHistoryProductRead(
|
||||
product_name=display_name or prod.product_name or "—",
|
||||
model=prod.model,
|
||||
package_spec=prod.package_spec,
|
||||
quantity=prod.quantity or prod.inbound_estimate_qty or prod.outbound_estimate_qty,
|
||||
unit=_resolve_unit_label(unit_map, prod.unit),
|
||||
)
|
||||
)
|
||||
|
||||
records: list[PublicTransportHistoryRecordRead] = []
|
||||
for plan, tr in plan_rows:
|
||||
voided, archived = _resolve_driver_link_flags(plan, tr)
|
||||
tok = (tr.driver_access_token or "").strip()
|
||||
records.append(
|
||||
@@ -408,6 +452,7 @@ def get_public_transport_history(
|
||||
delivery_time=plan.delivery_time,
|
||||
pickup_address=plan.pickup_address,
|
||||
destination_address=plan.destination_address or plan.destination_name,
|
||||
products=products_by_plan.get(plan.id) or [],
|
||||
plan_voided=voided,
|
||||
plan_archived=archived,
|
||||
status_label=_history_status_label(voided, archived),
|
||||
|
||||
Reference in new issue
Block a user