feat(pl): add Excel export for plan contract gaps and document reminders

Complete remaining logistics list export pages under the PL module wave.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-21 18:19:07 +08:00
1 parent b1f734dbb0
commit fe15a130cb
6 files changed
+205

No files matched your search

+62
View File
@@ -408,6 +408,68 @@ def list_document_reminders(
return query.order_by(PlDocumentReminder.expire_date.asc()).all()
DOCUMENT_REMINDER_EXPORT_COLUMNS = [
ExportColumn(title="主体名称", field="ownerLabel"),
ExportColumn(title="主体类型", field="ownerType"),
ExportColumn(title="证件类型", field="documentType"),
ExportColumn(title="证件号", field="documentNo"),
ExportColumn(title="到期日期", field="expireDate"),
ExportColumn(title="剩余天数", field="daysRemaining"),
ExportColumn(title="状态", field="status"),
ExportColumn(title="处理备注", field="remark"),
]
class DocumentReminderExportBody(BaseModel):
status: Optional[str] = None
ids: Optional[List[str]] = None
columns: Optional[List[str]] = None
model_config = {"populate_by_name": True}
@router.post("/document-reminders/export")
def export_document_reminders(
body: DocumentReminderExportBody,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
):
try:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
scan_cert_expiry(db, tenant_no)
db.commit()
query = db.query(PlDocumentReminder).filter(
PlDocumentReminder.tenant_no == tenant_no,
PlDocumentReminder.del_flag == 0,
)
if body.status:
query = query.filter(PlDocumentReminder.status == body.status)
if body.ids:
query = query.filter(PlDocumentReminder.id.in_(body.ids))
query = apply_pl_scope(query, PlDocumentReminder, user, db, tenant_no)
items = query.order_by(PlDocumentReminder.expire_date.asc()).limit(EXPORT_MAX_ROWS + 1).all()
status_labels = {"pending": "待处理", "resolved": "已处理"}
owner_labels = {"vehicle": "车辆", "personnel": "人员", "driver": "司机"}
rows = []
for x in items:
data = DocumentReminderRead.model_validate(x).model_dump(by_alias=True)
data["remark"] = getattr(x, "remark", None) or ""
data["status"] = status_labels.get(str(data.get("status") or ""), data.get("status"))
data["ownerType"] = owner_labels.get(str(data.get("ownerType") or ""), data.get("ownerType"))
for k, v in list(data.items()):
if hasattr(v, "isoformat"):
data[k] = str(v)[:19]
rows.append(data)
cols = filter_export_columns(DOCUMENT_REMINDER_EXPORT_COLUMNS, body.columns)
date_str = datetime.now().strftime("%Y%m%d")
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.put("/document-reminders/{id}", response_model=DocumentReminderRead)
def update_document_reminder(
id: str,
+76
View File
@@ -679,6 +679,82 @@ def list_contract_gaps(
return PlanContractGapListResponse(total=total, list=items)
CONTRACT_GAP_EXPORT_COLUMNS = [
ExportColumn(title="计划编号", field="planNo"),
ExportColumn(title="计划日期", field="planDate"),
ExportColumn(title="单据类型", field="planType"),
ExportColumn(title="跟进状态", field="followStatus"),
ExportColumn(title="供方", field="supplierName"),
ExportColumn(title="需方", field="customerName"),
ExportColumn(title="缺口类型", field="gapType"),
]
class ContractGapExportBody(BaseModel):
plan_no: Optional[str] = Field(default=None, alias="planNo")
gap_type: Optional[str] = Field(default=None, alias="gapType")
customer_keyword: Optional[str] = Field(default=None, alias="customerKeyword")
plan_date_from: Optional[date] = Field(default=None, alias="planDateFrom")
plan_date_to: Optional[date] = Field(default=None, alias="planDateTo")
q: Optional[str] = None
ids: Optional[List[str]] = None
columns: Optional[List[str]] = None
model_config = {"populate_by_name": True}
@router.post("/contract-gaps/export")
def export_contract_gaps(
body: ContractGapExportBody,
db: Session = Depends(get_db),
user: DbUserContext = Depends(get_db_current_user),
):
try:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
_, items = list_plan_contract_gaps(
db,
tenant_no=tenant_no,
user=user,
page=1,
page_size=EXPORT_MAX_ROWS,
plan_no=body.plan_no,
gap_type=body.gap_type,
customer_keyword=body.customer_keyword,
plan_date_from=body.plan_date_from,
plan_date_to=body.plan_date_to,
q=body.q,
ids=body.ids,
max_page_size=EXPORT_MAX_ROWS,
)
plan_type_labels = {"direct": "直销单", "inbound": "入库单", "outbound": "出库单"}
gap_labels = {"supplier": "仅缺供方合同", "demander": "仅缺需方合同", "both": "双侧缺口"}
rows = []
for item in items:
data = item.model_dump(by_alias=True)
data["planType"] = plan_type_labels.get(str(data.get("planType") or ""), data.get("planType"))
sides = set(data.get("gapSides") or [])
if "supplier" in sides and "demander" in sides:
data["gapType"] = "双侧缺口"
elif "supplier" in sides:
data["gapType"] = "仅缺供方合同"
elif "demander" in sides:
data["gapType"] = "仅缺需方合同"
else:
data["gapType"] = ""
for k, v in list(data.items()):
if hasattr(v, "isoformat"):
data[k] = str(v)[:19]
rows.append(data)
cols = filter_export_columns(CONTRACT_GAP_EXPORT_COLUMNS, body.columns)
date_str = datetime.now().strftime("%Y%m%d")
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.put("/{id}/contracts/{side}", response_model=PlanContractGapRead)
def put_plan_side_contracts(
id: str,
@@ -151,6 +151,8 @@ def list_plan_contract_gaps(
plan_date_from: Optional[date] = None,
plan_date_to: Optional[date] = None,
q: Optional[str] = None,
ids: Optional[list] = None,
max_page_size: Optional[int] = None,
) -> tuple[int, list[PlanContractGapRead]]:
query = db.query(PlPlan).filter(
PlPlan.tenant_no == tenant_no,
@@ -202,8 +204,14 @@ def list_plan_contract_gaps(
)
)
if ids:
query = query.filter(PlPlan.id.in_(ids))
query = query.order_by(PlPlan.plan_date.desc(), PlPlan.create_time.desc())
total = query.count()
page = max(1, page)
cap = int(max_page_size) if max_page_size else 500
page_size = min(max(1, page_size), cap)
offset = (page - 1) * page_size
plans = query.offset(offset).limit(page_size).all()
+4
View File
@@ -755,6 +755,8 @@ export const planApi = {
request.post<string>(`/transport/plan/${id}/subpack-slip-no`),
listContractGaps: (params?: PlanContractGapQuery) =>
request.get<PlListResponse<PlanContractGapItem>>('/transport/plan/contract-gaps', { params }),
exportContractGaps: (data: Record<string, any>) =>
request.post('/transport/plan/contract-gaps/export', data, { responseType: 'blob' }),
updateSideContracts: (
id: string,
side: 'supplier' | 'demander',
@@ -1315,6 +1317,8 @@ export const inventoryApi = {
}) => request.post('/transport/inventory/warning-threshold', data),
documentReminders: (status?: string) =>
request.get<DocumentReminder[]>('/transport/inventory/document-reminders', { params: { status } }),
exportDocumentReminders: (data: Record<string, any>) =>
request.post('/transport/inventory/document-reminders/export', data, { responseType: 'blob' }),
documentReminderStats: () =>
request.get<DocumentReminderStats>('/transport/inventory/document-reminders/stats'),
updateDocumentReminder: (id: string, data: { status: string; remark?: string }) =>
@@ -43,6 +43,15 @@
<el-icon><CircleCheck /></el-icon>处理
</el-button>
<ListExportDropdown
:export-fn="inventoryApi.exportDocumentReminders"
:get-filter-params="getReminderExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => r.id)"
:visible-columns="reminderExportColumns"
filename="证件到期提醒.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
@@ -159,6 +168,7 @@
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import { CircleCheck, RefreshLeft, Search } from '@element-plus/icons-vue'
import { inventoryApi, type DocumentReminder } from '@/api/pl'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
@@ -189,6 +199,19 @@ const reminderSourceList = ref<DocumentReminder[]>([])
const quickSearchKeyword = ref('')
const selectedRows = ref<DocumentReminder[]>([])
function getReminderExportFilterParams() {
return {
status: searchForm.status || undefined
}
}
const reminderExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('ownerLabel')) cols.unshift('ownerLabel')
return cols
})
const searchForm = reactive({
status: ''
})
@@ -70,6 +70,14 @@
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<ListExportDropdown
:export-fn="planApi.exportContractGaps"
:get-filter-params="getContractGapExportFilterParams"
:has-selection="false"
:selected-ids="[]"
:visible-columns="contractGapExportColumns"
filename="计划合同完善.xlsx"
/>
<el-button class="btn-ghost" :loading="loading" @click="fetchData">
<el-icon><Refresh /></el-icon>刷新
</el-button>
@@ -347,6 +355,7 @@ import PlPlanContractMultiSelect from '@/components/business/pl-plan-contract-mu
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
import { useListDateShortcut } from '@/composables/useListDateShortcut'
import {
@@ -392,6 +401,29 @@ const pagination = reactive({
total: 0
})
function getContractGapExportFilterParams() {
const keyword = quickSearch.value.trim()
return {
q: keyword || undefined,
planNo: keyword ? undefined : searchForm.planNo || undefined,
gapType: searchForm.gapType || undefined,
customerKeyword: keyword ? undefined : searchForm.customerKeyword || undefined,
planDateFrom: dateRange.value?.[0] || undefined,
planDateTo: dateRange.value?.[1] || undefined
}
}
const contractGapExportColumns = [
'planNo',
'planDate',
'planType',
'followStatus',
'supplierName',
'customerName',
'gapType'
]
function cloneLinks(links?: PlanContractLink[]): PlanContractLink[] {
return (links || []).map(item => ({
contractId: item.contractId,