This commit is contained in:
zlwu committed 2026-08-22 09:08:05 +08:00
1 parent 833544def3
commit 16be6fa22d
204 files changed
+455089 -46491

No files matched your search

+52
View File
@@ -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
View File
@@ -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:
+11 -2
View File
@@ -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:
+15 -4
View File
@@ -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,
+13 -3
View File
@@ -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:
+97 -2
View File
@@ -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,
+99 -2
View File
@@ -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
+109 -2
View File
@@ -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,
+128 -5
View File
@@ -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),
+46 -1
View File
@@ -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),