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:
1 parent
b1f734dbb0
commit
fe15a130cb
6 files changed
+205
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user