Tms对接金蝶接口-前端 #3
@@ -1,5 +1,6 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
/dist
|
||||
|
||||
/tests/e2e/videos/
|
||||
|
||||
+5
-2
@@ -6,7 +6,8 @@
|
||||
"prod": "vite --mode production",
|
||||
"build": "vite build",
|
||||
"build:prod": "vite build --mode production",
|
||||
"serve": "vite preview --host"
|
||||
"serve": "vite preview --host",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
@@ -47,9 +48,11 @@
|
||||
"prettier": "^2.8.7",
|
||||
"sass": "^1.85.1",
|
||||
"terser": "^5.31.1",
|
||||
"typescript": "^7.0.2",
|
||||
"unplugin-auto-import": "^0.11.2",
|
||||
"vite": "^5.4.19",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0"
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vue-tsc": "^3.3.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,3 +58,14 @@ export const changeStatus = (id, status) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getBfiExchangeRate = (currencyCode, effectdate) => {
|
||||
return request({
|
||||
url: '/blade-transport/bfi/exchange-rate',
|
||||
method: 'get',
|
||||
params: {
|
||||
currencyCode,
|
||||
effectdate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const approve = data => request({ url: `${baseUrl}/approve`, method: 'pos
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
|
||||
|
||||
export const invoiceTypeOptions = [
|
||||
{ label: '电子专票', value: 'electronic_special' },
|
||||
|
||||
@@ -5,8 +5,17 @@ const baseUrl = '/blade-transport/invoice-receipt';
|
||||
export const getList = (current, size, params) =>
|
||||
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||
export const getInvoicePool = keyword =>
|
||||
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params: { keyword } });
|
||||
export const queryInvoicePool = params =>
|
||||
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
|
||||
export const downloadInvoicePoolAttachment = invoiceNo =>
|
||||
// 金蝶票池发票文件下载(后端代理 A801-2 流式回源),耗时较普通接口长,放宽到 120s
|
||||
request({
|
||||
url: `${baseUrl}/invoice-pool/attachment/download`,
|
||||
method: 'get',
|
||||
params: { invoiceNo },
|
||||
responseType: 'blob',
|
||||
timeout: 120000,
|
||||
});
|
||||
export const getSettlementCandidates = (keyword, receiptId) =>
|
||||
request({
|
||||
url: `${baseUrl}/settlement-candidates`,
|
||||
@@ -25,8 +34,8 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
||||
export const pushKingdee = ids =>
|
||||
request({ url: `${baseUrl}/push-kingdee`, method: 'post', timeout: 30000, data: ids });
|
||||
|
||||
export const approvalStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
@@ -36,8 +45,9 @@ export const approvalStatusOptions = [
|
||||
{ label: '已作废', value: 'voided' },
|
||||
];
|
||||
|
||||
export const kingdeeStatusOptions = [
|
||||
{ label: '未同步', value: 'unsynced' },
|
||||
{ label: '已同步', value: 'synced' },
|
||||
{ label: '同步失败', value: 'failed' },
|
||||
export const kingdeePushStatusOptions = [
|
||||
{ label: '未推送', value: 'unsynced' },
|
||||
{ label: '已保存收票单', value: 'pushed' },
|
||||
{ label: '已指定应付', value: 'assigned' },
|
||||
{ label: '推送失败', value: 'failed' },
|
||||
];
|
||||
|
||||
@@ -16,10 +16,15 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
// 同步金蝶涉及多次金蝶远程接口调用(A103-2 预检 + A103-1/A103-3 推送),单笔可达 20s+,覆盖全局 10s 超时
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
|
||||
// 批量同步逐单单据级事务串行推送,耗时随单量线性增长
|
||||
export const syncKingdeeBatch = ids =>
|
||||
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', data: ids });
|
||||
request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', timeout: 30000, data: ids });
|
||||
// 回写任务按 paymentNo 分批远程查询 A103-2/A103-4,同样可能超过全局超时
|
||||
export const syncKingdeeResult = () =>
|
||||
request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: 30000 });
|
||||
|
||||
export const paymentTypeOptions = [
|
||||
{ label: '项目预付', value: 'project_advance' },
|
||||
@@ -36,5 +41,8 @@ export const approvalStatusOptions = [
|
||||
export const kingdeeStatusOptions = [
|
||||
{ label: '未生成', value: 'unsynced' },
|
||||
{ label: '已生成', value: 'synced' },
|
||||
{ label: '付款中', value: 'paying' },
|
||||
{ label: '已付款', value: 'paid' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
{ label: '生成失败', value: 'failed' },
|
||||
];
|
||||
|
||||
@@ -8,14 +8,24 @@ export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get'
|
||||
export const updateAttachments = data =>
|
||||
request({ url: `${baseUrl}/attachments`, method: 'post', data });
|
||||
export const voidClaim = id => request({ url: `${baseUrl}/void`, method: 'post', params: { id } });
|
||||
export const queryKingdeeResult = () =>
|
||||
request({ url: `${baseUrl}/query-kingdee-result`, method: 'post' });
|
||||
|
||||
export const claimStatusOptions = [
|
||||
{ label: '已认领', value: 'claimed' },
|
||||
{ label: '已作废', value: 'voided' },
|
||||
];
|
||||
|
||||
export const pushStatusOptions = [
|
||||
{ label: '未推送', value: 'none' },
|
||||
{ label: '已推送', value: 'pushed' },
|
||||
{ label: '推送失败', value: 'failed' },
|
||||
];
|
||||
|
||||
export const kingdeeBillStatusOptions = [
|
||||
{ label: '未生成', value: 'none' },
|
||||
{ label: '已推送', value: 'pushed' },
|
||||
{ label: '审核通过', value: 'approved' },
|
||||
{ label: '已收款', value: 'received' },
|
||||
{ label: '处理失败', value: 'failed' },
|
||||
];
|
||||
|
||||
@@ -12,10 +12,25 @@ export const getSettlementCandidates = (keyword, flowId) =>
|
||||
params: { keyword, flowId },
|
||||
});
|
||||
export const claim = data => request({ url: `${baseUrl}/claim`, method: 'post', data });
|
||||
export const sync = data => request({ url: `${baseUrl}/sync`, method: 'post', data });
|
||||
export const sync = () => request({ url: `${baseUrl}/sync`, method: 'post' });
|
||||
export const pushKingdee = flowId =>
|
||||
request({ url: `${baseUrl}/push-kingdee`, method: 'post', params: { flowId } });
|
||||
|
||||
export const claimStatusOptions = [
|
||||
{ label: '未认领', value: 'unclaimed' },
|
||||
{ label: '部分认领', value: 'partial' },
|
||||
{ label: '认领完成', value: 'claimed' },
|
||||
];
|
||||
|
||||
export const pushStatusOptions = [
|
||||
{ label: '未推送', value: 'none' },
|
||||
{ label: '已推送', value: 'pushed' },
|
||||
{ label: '推送失败', value: 'failed' },
|
||||
];
|
||||
|
||||
export const kingdeeBillStatusOptions = [
|
||||
{ label: '已推送', value: 'pushed' },
|
||||
{ label: '审核通过', value: 'approved' },
|
||||
{ label: '已收款', value: 'received' },
|
||||
{ label: '处理失败', value: 'failed' },
|
||||
];
|
||||
|
||||
@@ -30,8 +30,9 @@ export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post'
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时
|
||||
export const syncKingdee = id =>
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } });
|
||||
request({ url: `${baseUrl}/sync-kingdee`, method: 'post', timeout: 30000, params: { id } });
|
||||
export const getDetailFees = detailId =>
|
||||
request({ url: `${baseUrl}/detail-fees`, method: 'get', params: { detailId } });
|
||||
export const getReceiptClaims = formalSettlementId =>
|
||||
@@ -48,3 +49,5 @@ export const applyPayments = data =>
|
||||
request({ url: `${baseUrl}/apply-payments`, method: 'post', data });
|
||||
export const claimInvoices = data =>
|
||||
request({ url: `${baseUrl}/claim-invoices`, method: 'post', data });
|
||||
export const queryInvoicePool = params =>
|
||||
request({ url: `${baseUrl}/invoice-pool`, method: 'get', params });
|
||||
|
||||
@@ -152,6 +152,28 @@ export const getScoreTemplate = quantificationId => {
|
||||
});
|
||||
};
|
||||
|
||||
export const queryKingdeeCustomer = creditCode => {
|
||||
return request({
|
||||
url: '/blade-transport/bfi/customer/query-by-credit-code',
|
||||
method: 'get',
|
||||
timeout: 30000,
|
||||
params: {
|
||||
creditCode,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const syncKingdee = id => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/sync-kingdee',
|
||||
method: 'post',
|
||||
timeout: 30000,
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const recognizeBusinessLicenseOcr = imageUrl => {
|
||||
return request({
|
||||
url: '/blade-transport/baidu-ocr/recognize-url',
|
||||
|
||||
+9
-1
@@ -271,10 +271,18 @@ axios.interceptors.response.use(
|
||||
},
|
||||
error => {
|
||||
NProgress.done();
|
||||
// 网络级失败(HTTP 状态 >500、网关错误、超时、断网等 validateStatus 放行之外的响应)
|
||||
// 原为静默 reject,接口报错页面无任何感知;统一补错误提示,取消请求与刷新令牌请求除外
|
||||
if (!axios.isCancel(error) && !isRefreshTokenRequest(error?.config)) {
|
||||
ElMessage({
|
||||
message: error?.response?.data?.msg || '请求失败,请稍后重试',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(error));
|
||||
// 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response
|
||||
// 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成
|
||||
// "[object Object]",用户只能看到无意义的通用提示。
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" width="92%" append-to-body destroy-on-close @open="handleOpen">
|
||||
<el-form :model="query" inline label-width="130px" @submit.prevent>
|
||||
<el-form-item label="发票号码">
|
||||
<el-input v-model="query.invoiceNo" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开票日期">
|
||||
<el-date-picker
|
||||
v-model="query.invoiceDateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="开票单位">
|
||||
<el-input v-model="query.salerName" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="受票单位">
|
||||
<el-input v-model="query.buyerName" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开票单位(销方)">
|
||||
<el-select
|
||||
v-model="query.salerTaxNo"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="keyword => searchCustomers(keyword, 'saler')"
|
||||
:loading="customerLoading.saler"
|
||||
placeholder="输入名称搜索或直接粘贴税号"
|
||||
style="width: 220px"
|
||||
@keyup.enter="handleQuery"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in customerOptions.saler"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="受票单位(购方)">
|
||||
<el-select
|
||||
v-model="query.buyerTaxNo"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="keyword => searchCustomers(keyword, 'buyer')"
|
||||
:loading="customerLoading.buyer"
|
||||
placeholder="输入名称搜索或直接粘贴税号"
|
||||
style="width: 220px"
|
||||
@keyup.enter="handleQuery"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in customerOptions.buyer"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发票类型">
|
||||
<el-select
|
||||
v-model="query.invoiceType"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="不限"
|
||||
style="width: 230px"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in invoiceTypeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="认领状态">
|
||||
<el-select v-model="query.claimed" clearable placeholder="不限" style="width: 120px">
|
||||
<el-option label="未认领" :value="false" />
|
||||
<el-option label="已认领" :value="true" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="登记状态">
|
||||
<el-select v-model="query.registered" clearable placeholder="不限" style="width: 120px">
|
||||
<el-option label="未登记" :value="false" />
|
||||
<el-option label="已登记" :value="true" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button :disabled="loading" @click="resetQuery">重置</el-button>
|
||||
<el-button type="primary" :disabled="loading" @click="handleQuery">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table
|
||||
ref="poolTable"
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:row-key="rowKey"
|
||||
border
|
||||
height="520"
|
||||
:highlight-current-row="!multiple"
|
||||
@selection-change="selection => (selected = selection)"
|
||||
@row-dblclick="handleRowDblclick"
|
||||
>
|
||||
<el-table-column v-if="multiple" type="selection" width="52" fixed="left" align="center" reserve-selection />
|
||||
<el-table-column v-else width="52" align="center" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<el-radio
|
||||
:model-value="current && current.invoiceNo"
|
||||
:value="row.invoiceNo"
|
||||
@change="current = row"
|
||||
>
|
||||
<span></span>
|
||||
</el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
|
||||
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
|
||||
<el-table-column prop="invoiceType" label="发票类型" min-width="140" />
|
||||
<el-table-column prop="issuerName" label="开票单位" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="receiverName" label="受票单位" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="金额(含税)" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税率" min-width="90" align="center">
|
||||
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税额" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余可用金额" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.availableTaxAmount) }}</template>
|
||||
</el-table-column>
|
||||
<!--<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="210" show-overflow-tooltip />-->
|
||||
<el-table-column label="认领状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.claimed" type="danger" size="small">已认领</el-tag>
|
||||
<el-tag v-else type="info" size="small">未认领</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="登记状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.registered" type="warning" size="small">已登记</el-tag>
|
||||
<el-tag v-else type="info" size="small">未登记</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
:disabled="attachmentBusy === row.invoiceNo"
|
||||
@click.stop="viewAttachment(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
:disabled="attachmentBusy === row.invoiceNo"
|
||||
@click.stop="downloadAttachment(row)"
|
||||
>下载</el-link
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!multiple" label="操作" width="90" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click.stop="confirmRows([row])">选择</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="kingdee-pool-dialog__pager">
|
||||
<span class="kingdee-pool-dialog__pager-tip">按名称检索范围为已同步票,未找到请改用发票号码或税号精确查找</span>
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:disabled="loading"
|
||||
@current-change="load"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm">{{ confirmText }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/axios';
|
||||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||
import {
|
||||
downloadKingdeeInvoiceFile,
|
||||
isKingdeePathAttachment,
|
||||
viewKingdeeInvoiceFile,
|
||||
} from '@/utils/kingdee-attachment';
|
||||
|
||||
export default {
|
||||
name: 'KingdeeInvoicePoolDialog',
|
||||
props: {
|
||||
modelValue: { type: Boolean, required: true },
|
||||
title: { type: String, default: '查询金蝶票据池' },
|
||||
confirmText: { type: String, default: '确定' },
|
||||
multiple: { type: Boolean, default: false },
|
||||
initialInvoiceNo: { type: String, default: '' },
|
||||
queryApi: { type: Function, required: true },
|
||||
},
|
||||
emits: ['update:modelValue', 'confirm'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
// 与后端 BfiConstant.INVOICE_TYPE_NAMES 对齐:value 为发票类型编码(镜像按 invoice_type_code 精确匹配)
|
||||
// 选票场景只开放数电票(普票/专票)
|
||||
invoiceTypeOptions: [
|
||||
{ value: '26', label: '数电发票(普通发票)' },
|
||||
{ value: '27', label: '数电发票(增值税专用发票)' },
|
||||
],
|
||||
rows: [],
|
||||
selected: [],
|
||||
current: null,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
// 正在查看/下载附件的发票号(行级 loading 守卫)
|
||||
attachmentBusy: null,
|
||||
// 销/购方客商下拉:远程按名称搜客商档案,选中带出税号查询;手动输入的裸税号直接作为值过滤
|
||||
customerOptions: { saler: [], buyer: [] },
|
||||
customerLoading: { saler: false, buyer: false },
|
||||
query: this.emptyQuery(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visible: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
emptyQuery() {
|
||||
return {
|
||||
invoiceNo: '',
|
||||
// 默认查近一个月,随请求传后端并在页面回显
|
||||
invoiceDateRange: [this.$dayjs().subtract(1, 'month').format('YYYY-MM-DD'), this.$dayjs().format('YYYY-MM-DD')],
|
||||
salerName: '',
|
||||
buyerName: '',
|
||||
salerTaxNo: '',
|
||||
buyerTaxNo: '',
|
||||
invoiceType: '',
|
||||
claimed: null,
|
||||
registered: null,
|
||||
};
|
||||
},
|
||||
// 镜像行主键为 id,回源入库前的响应仍可能无 id,用发票号码兜底
|
||||
rowKey(row) {
|
||||
return row.id || row.invoiceNo;
|
||||
},
|
||||
// 票池行附件字段为复数 attachmentsJson,行内首项即发票文件({source,path,name})
|
||||
rowAttachment(row = {}) {
|
||||
const value = row.attachmentsJson;
|
||||
if (!value) return null;
|
||||
let parsed = value;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return Array.isArray(parsed) ? parsed[0] || null : parsed || null;
|
||||
},
|
||||
attachmentName(row) {
|
||||
const attachment = this.rowAttachment(row);
|
||||
return attachment?.name || `${row.invoiceNo || '发票'}.pdf`;
|
||||
},
|
||||
async viewAttachment(row) {
|
||||
const attachment = this.rowAttachment(row);
|
||||
if (!isKingdeePathAttachment(attachment)) {
|
||||
this.$message.warning('当前发票暂无可查看附件');
|
||||
return;
|
||||
}
|
||||
this.attachmentBusy = row.invoiceNo;
|
||||
try {
|
||||
await viewKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
|
||||
} finally {
|
||||
this.attachmentBusy = null;
|
||||
}
|
||||
},
|
||||
async downloadAttachment(row) {
|
||||
const attachment = this.rowAttachment(row);
|
||||
if (!isKingdeePathAttachment(attachment)) {
|
||||
this.$message.warning('当前发票暂无可下载附件');
|
||||
return;
|
||||
}
|
||||
this.attachmentBusy = row.invoiceNo;
|
||||
try {
|
||||
await downloadKingdeeInvoiceFile(row.invoiceNo, this.attachmentName(row));
|
||||
} finally {
|
||||
this.attachmentBusy = null;
|
||||
}
|
||||
},
|
||||
handleOpen() {
|
||||
this.query = this.emptyQuery();
|
||||
this.query.invoiceNo = this.initialInvoiceNo || '';
|
||||
this.current = null;
|
||||
this.selected = [];
|
||||
this.$refs.poolTable?.clearSelection();
|
||||
this.page.current = 1;
|
||||
this.load();
|
||||
},
|
||||
handleQuery() {
|
||||
this.page.current = 1;
|
||||
this.load();
|
||||
},
|
||||
handleSizeChange() {
|
||||
this.page.current = 1;
|
||||
this.load();
|
||||
},
|
||||
resetQuery() {
|
||||
this.query = this.emptyQuery();
|
||||
this.handleQuery();
|
||||
},
|
||||
// 客商名称远程搜索:含 18 位社会信用代码特征(数字+大写字母)时按裸税号直查;
|
||||
// 否则搜客商档案,取客商发票信息(blade_customer_invoice_info)的抬头/税号作选项——票池税号是开票税号,非统一信用代码
|
||||
async searchCustomers(keyword, side) {
|
||||
const text = String(keyword || '').trim();
|
||||
if (!text) return;
|
||||
if (/^[0-9A-Z]{15,20}$/.test(text)) {
|
||||
this.customerOptions[side] = [{ label: `${text}(手输税号)`, value: text }];
|
||||
return;
|
||||
}
|
||||
this.customerLoading[side] = true;
|
||||
try {
|
||||
const data = this.unwrap(await getCustomerList(1, 20, { fullName: text }));
|
||||
const customers = (data.records || []).filter(item => item.id);
|
||||
if (!customers.length) {
|
||||
this.customerOptions[side] = [];
|
||||
return;
|
||||
}
|
||||
// 列表接口不带发票信息,逐户取 detail(detail 组装 invoices,每户一张发票信息)
|
||||
const options = await Promise.all(
|
||||
customers.map(async customer => {
|
||||
const detail = this.unwrap(await request({ url: '/blade-transport/customer-archive/detail', method: 'get', params: { id: customer.id } }));
|
||||
return (detail?.invoices || [])
|
||||
.filter(invoice => invoice.taxNo)
|
||||
.map(invoice => ({ label: `${invoice.invoiceTitle || customer.fullName}(${invoice.taxNo})`, value: invoice.taxNo }));
|
||||
})
|
||||
);
|
||||
this.customerOptions[side] = options.flat().filter(
|
||||
(option, index, list) => list.findIndex(item => item.value === option.value) === index
|
||||
);
|
||||
} finally {
|
||||
this.customerLoading[side] = false;
|
||||
}
|
||||
},
|
||||
unwrap(response) {
|
||||
const body = response?.data || response || {};
|
||||
return body?.data || body;
|
||||
},
|
||||
async load() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const range = this.query.invoiceDateRange || [];
|
||||
const data = this.unwrap(
|
||||
await this.queryApi({
|
||||
current: this.page.current,
|
||||
size: this.page.size,
|
||||
invoiceNo: this.query.invoiceNo || undefined,
|
||||
invoiceDateStart: range[0] || undefined,
|
||||
invoiceDateEnd: range[1] || undefined,
|
||||
salerName: this.query.salerName || undefined,
|
||||
buyerName: this.query.buyerName || undefined,
|
||||
salerTaxNo: this.query.salerTaxNo || undefined,
|
||||
buyerTaxNo: this.query.buyerTaxNo || undefined,
|
||||
invoiceType: this.query.invoiceType || undefined,
|
||||
claimed: this.query.claimed ?? undefined,
|
||||
registered: this.query.registered ?? undefined,
|
||||
})
|
||||
);
|
||||
this.rows = data.records || [];
|
||||
this.page.total = Number(data.total || 0);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleRowDblclick(row) {
|
||||
if (!this.multiple) {
|
||||
this.confirmRows([row]);
|
||||
}
|
||||
},
|
||||
handleConfirm() {
|
||||
const picked = this.multiple ? this.selected : this.current ? [this.current] : [];
|
||||
this.confirmRows(picked);
|
||||
},
|
||||
confirmRows(rows) {
|
||||
if (!rows.length) {
|
||||
this.$message.warning(this.multiple ? '请至少选择一条发票' : '请选择一张发票');
|
||||
return;
|
||||
}
|
||||
this.$emit('confirm', rows);
|
||||
this.visible = false;
|
||||
},
|
||||
formatMoney(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number.toFixed(2) : '-';
|
||||
},
|
||||
formatRate(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? `${number}%` : '-';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.kingdee-pool-dialog__pager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
.kingdee-pool-dialog__pager-tip {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -43,9 +43,23 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
|
||||
<el-table-column v-if="!readonly" label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||
<el-table-column
|
||||
v-if="!readonly || showViewActions"
|
||||
label="操作"
|
||||
:width="readonly ? 120 : 170"
|
||||
align="center"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row, $index }">
|
||||
<div class="vehicle-attachment-table__actions">
|
||||
<el-link type="primary" :disabled="kingdeeBusy" @click="previewAttachment(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link type="primary" :disabled="kingdeeBusy" @click="downloadAttachment(row)"
|
||||
>下载</el-link
|
||||
>
|
||||
<el-link v-if="!readonly" type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -71,6 +85,7 @@
|
||||
width="90%"
|
||||
top="4vh"
|
||||
class="vehicle-attachment-table__viewer-dialog"
|
||||
@closed="handlePreviewClosed"
|
||||
>
|
||||
<pdf-preview
|
||||
v-if="documentPreviewVisible && previewFile.url && previewFile.isPdf"
|
||||
@@ -111,6 +126,11 @@ import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import {
|
||||
downloadKingdeeInvoiceFile,
|
||||
isKingdeePathAttachment,
|
||||
requestKingdeeFile,
|
||||
} from '@/utils/kingdee-attachment';
|
||||
|
||||
const attachmentFileTypes = [
|
||||
'pdf',
|
||||
@@ -156,6 +176,16 @@ export default {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
// 金蝶侧附件(piaozone path 型)代理下载所依赖的发票号;行内自带 invoiceNo 时优先用行内值
|
||||
kingdeeInvoiceNo: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 查看态也展示「查看/下载」操作列(编辑态始终展示,含删除)
|
||||
showViewActions: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
@@ -167,6 +197,10 @@ export default {
|
||||
imagePreviewIndex: 0,
|
||||
documentPreviewVisible: false,
|
||||
previewFile: {},
|
||||
// 金蝶侧附件代理请求进行中(行级按钮共享 loading 守卫,防重复触发)
|
||||
kingdeeBusy: false,
|
||||
// 金蝶附件预览用的 objectURL,弹窗关闭时回收
|
||||
kingdeeObjectUrl: '',
|
||||
viewerPlugins,
|
||||
viewerToolbar: {
|
||||
download: true,
|
||||
@@ -201,6 +235,12 @@ export default {
|
||||
attachmentUrl(row = {}) {
|
||||
return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || row.src || '';
|
||||
},
|
||||
kingdeeInvoiceNoOf(row = {}) {
|
||||
return row.invoiceNo || this.kingdeeInvoiceNo || '';
|
||||
},
|
||||
isKingdeeRow(row = {}) {
|
||||
return isKingdeePathAttachment(row) && Boolean(this.kingdeeInvoiceNoOf(row));
|
||||
},
|
||||
attachmentExtension(row = {}) {
|
||||
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
|
||||
const index = source.lastIndexOf('.');
|
||||
@@ -248,6 +288,10 @@ export default {
|
||||
return `${(value / 1024 / 1024).toFixed(1)}MB`;
|
||||
},
|
||||
downloadAttachment(row) {
|
||||
// 金蝶侧附件(piaozone path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
|
||||
if (this.isKingdeeRow(row)) {
|
||||
return this.downloadKingdeeAttachment(row);
|
||||
}
|
||||
const url = this.attachmentUrl(row);
|
||||
if (!url) {
|
||||
this.$message.warning('附件地址为空,无法下载');
|
||||
@@ -255,20 +299,47 @@ export default {
|
||||
}
|
||||
downloadFileByUrl(url, this.attachmentName(row));
|
||||
},
|
||||
async downloadKingdeeAttachment(row) {
|
||||
if (this.kingdeeBusy) return;
|
||||
this.kingdeeBusy = true;
|
||||
try {
|
||||
await downloadKingdeeInvoiceFile(this.kingdeeInvoiceNoOf(row), this.attachmentName(row));
|
||||
} finally {
|
||||
this.kingdeeBusy = false;
|
||||
}
|
||||
},
|
||||
batchDownload() {
|
||||
const rows = this.selectedRows.length ? this.selectedRows : this.rows;
|
||||
const downloadableRows = rows.filter(row => this.attachmentUrl(row));
|
||||
if (!downloadableRows.length) {
|
||||
const kingdeeRows = rows.filter(row => this.isKingdeeRow(row));
|
||||
const urlRows = rows.filter(row => !this.isKingdeeRow(row) && this.attachmentUrl(row));
|
||||
if (!kingdeeRows.length && !urlRows.length) {
|
||||
this.$message.warning(
|
||||
this.selectedRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
|
||||
);
|
||||
return;
|
||||
}
|
||||
downloadableRows.forEach((row, index) => {
|
||||
urlRows.forEach((row, index) => {
|
||||
window.setTimeout(() => this.downloadAttachment(row), index * 500);
|
||||
});
|
||||
// 金蝶侧附件走代理接口,串行触发避免并发重复回源
|
||||
this.downloadKingdeeRows(kingdeeRows);
|
||||
},
|
||||
async downloadKingdeeRows(rows) {
|
||||
if (this.kingdeeBusy) return;
|
||||
this.kingdeeBusy = true;
|
||||
try {
|
||||
for (const row of rows) {
|
||||
await downloadKingdeeInvoiceFile(this.kingdeeInvoiceNoOf(row), this.attachmentName(row));
|
||||
}
|
||||
} finally {
|
||||
this.kingdeeBusy = false;
|
||||
}
|
||||
},
|
||||
previewAttachment(row) {
|
||||
// 金蝶侧附件经 A801-2 代理取流后进弹窗预览,OSS 直链维持原逻辑
|
||||
if (this.isKingdeeRow(row)) {
|
||||
return this.previewKingdeeAttachment(row);
|
||||
}
|
||||
const url = this.attachmentUrl(row);
|
||||
if (!url) {
|
||||
this.$message.warning('附件地址为空,无法预览');
|
||||
@@ -291,6 +362,42 @@ export default {
|
||||
};
|
||||
this.documentPreviewVisible = true;
|
||||
},
|
||||
async previewKingdeeAttachment(row) {
|
||||
// OFD 版式文件浏览器无法在线渲染,直接提示下载,避免拉流后预览失败
|
||||
if (this.attachmentExtension(row) === 'ofd') {
|
||||
this.$message.warning('OFD 版式文件不支持在线预览,请使用下载获取后查看');
|
||||
return;
|
||||
}
|
||||
if (this.kingdeeBusy) return;
|
||||
this.kingdeeBusy = true;
|
||||
try {
|
||||
const blob = await requestKingdeeFile(this.kingdeeInvoiceNoOf(row));
|
||||
this.releaseKingdeeObjectUrl();
|
||||
const isPdf = this.isPdf(row) || String(blob.type || '').includes('application/pdf');
|
||||
this.kingdeeObjectUrl = URL.createObjectURL(blob);
|
||||
this.previewFile = {
|
||||
name: this.attachmentName(row),
|
||||
url: this.kingdeeObjectUrl,
|
||||
mimeType: isPdf ? 'application/pdf' : blob.type || '',
|
||||
isPdf,
|
||||
};
|
||||
this.documentPreviewVisible = true;
|
||||
} catch (error) {
|
||||
if (!error?.silent) this.$message.error(error?.message || '附件预览失败');
|
||||
} finally {
|
||||
this.kingdeeBusy = false;
|
||||
}
|
||||
},
|
||||
handlePreviewClosed() {
|
||||
this.releaseKingdeeObjectUrl();
|
||||
this.previewFile = {};
|
||||
},
|
||||
releaseKingdeeObjectUrl() {
|
||||
if (this.kingdeeObjectUrl) {
|
||||
URL.revokeObjectURL(this.kingdeeObjectUrl);
|
||||
this.kingdeeObjectUrl = '';
|
||||
}
|
||||
},
|
||||
handlePreviewUnsupported() {
|
||||
this.$message.warning('当前文件暂不支持在线预览');
|
||||
},
|
||||
@@ -311,6 +418,27 @@ export default {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__file-name {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__file-name:hover,
|
||||
&__file-name:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
:deep(.el-link) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&__upload {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -13,6 +13,8 @@ export const invoiceApplicationTableColumns = [
|
||||
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
|
||||
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
|
||||
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
|
||||
{ prop: 'kingdeeLockStatus', label: '金蝶锁状态', minWidth: 150 },
|
||||
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
|
||||
];
|
||||
|
||||
export const invoiceApplicationDetailColumns = [
|
||||
|
||||
@@ -11,6 +11,8 @@ export const invoiceReceiptTableColumns = [
|
||||
{ prop: 'currentProcessor', label: '当前处理人', minWidth: 130 },
|
||||
{ prop: 'createUserName', label: '创建人', minWidth: 110 },
|
||||
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
|
||||
{ prop: 'kingdeeBillNo', label: '金蝶单据号', minWidth: 150 },
|
||||
{ prop: 'kingdeeStatusName', label: '金蝶单据状态', minWidth: 140 },
|
||||
{ prop: 'kingdeeReceivedInvoiceNo', label: '金蝶单据号', minWidth: 150 },
|
||||
{ prop: 'kingdeePushStatusName', label: '金蝶状态', minWidth: 130 },
|
||||
{ prop: 'kingdeePushMsg', label: '失败原因', minWidth: 160 },
|
||||
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
|
||||
];
|
||||
|
||||
@@ -161,5 +161,7 @@ export const paymentApplicationOption = ({
|
||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 120, formatter: textFormatter },
|
||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '金蝶单据状态', prop: 'kingdeeStatusName', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', slot: true, minWidth: 150 },
|
||||
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100, formatter: textFormatter },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -8,5 +8,11 @@ export const receiptFlowTableColumns = [
|
||||
{ prop: 'summary', label: '摘要', minWidth: 160 },
|
||||
{ prop: 'transactionTime', label: '交易时间', minWidth: 170 },
|
||||
{ prop: 'claimStatusName', label: '认领状态', minWidth: 110, status: true },
|
||||
{ prop: 'kingdeeClaimStatusName', label: '公告状态', minWidth: 110 },
|
||||
{ prop: 'claimedAmount', label: '已认领金额', minWidth: 130, money: true },
|
||||
{ prop: 'unclaimedAmount', label: '金蝶未认领金额', minWidth: 140, money: true },
|
||||
{ prop: 'pushStatusName', label: '推送状态', minWidth: 110, status: true },
|
||||
{ prop: 'pushFailReason', label: '推送失败原因', minWidth: 160 },
|
||||
{ prop: 'kingdeeBillNo', label: '金蝶收款单号', minWidth: 150 },
|
||||
{ prop: 'kingdeeBillStatusName', label: '金蝶状态', minWidth: 110 },
|
||||
];
|
||||
|
||||
@@ -21,6 +21,8 @@ export const formalSettlementTableColumns = [
|
||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 130 },
|
||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 150 },
|
||||
{ label: '金蝶单据状态', prop: 'kingdeeSyncStatus', minWidth: 135 },
|
||||
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', minWidth: 150 },
|
||||
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100 },
|
||||
{ label: '创建人', prop: 'createUserName', minWidth: 110 },
|
||||
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||
];
|
||||
|
||||
@@ -88,6 +88,8 @@ export const updateFeeFormFields = [
|
||||
];
|
||||
|
||||
export const transferSearchFields = [
|
||||
{ label: '单据号', prop: 'documentNo', type: 'input' },
|
||||
{ label: '合同编码', prop: 'contractNo', type: 'input' },
|
||||
{ label: '合同名称', prop: 'contractName', type: 'input' },
|
||||
{ label: '批次号', prop: 'batchNo', type: 'input' },
|
||||
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 金蝶侧发票附件查看/下载公共工具
|
||||
*
|
||||
* 票池发票附件为 {source:'piaozone', path, name}:path 是金蝶侧文件 id(非 http 直链),
|
||||
* 须由后端代理 A801-2 流式回源;OSS 直链附件(url/link/fileUrl)仍由前端直连下载/预览。
|
||||
* 入口共用:票池弹窗、正式结算认领发票行、付款申请发票附件行、收票登记附件表。
|
||||
*/
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
import { downloadInvoicePoolAttachment } from '@/api/payment/invoiceReceipt';
|
||||
|
||||
/**
|
||||
* 金蝶侧附件判定:source=piaozone,或有 path 且非 http(s) 直链
|
||||
* @param {Object|Null} attachment 附件对象(attachmentsJson/attachmentJson 行内首项)
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export const isKingdeePathAttachment = attachment => {
|
||||
if (!attachment) return false;
|
||||
if (attachment.source === 'piaozone') return true;
|
||||
return Boolean(attachment.path) && !/^https?:\/\//i.test(attachment.path);
|
||||
};
|
||||
|
||||
/**
|
||||
* 请求金蝶文件流:成功返回 Blob;业务失败为 HTTP 200 + R.fail JSON(响应通道未写流时后端即失败),
|
||||
* 按 Content-Type 识别并抛出带 msg 的错误;网络级失败拦截器已提示,标记 silent 避免重复提示。
|
||||
* 供查看(弹窗内预览)/下载两类入口复用
|
||||
* @param {String} invoiceNo 发票号码
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
export const requestKingdeeFile = async invoiceNo => {
|
||||
let res;
|
||||
try {
|
||||
res = await downloadInvoicePoolAttachment(invoiceNo);
|
||||
} catch (error) {
|
||||
throw Object.assign(new Error(error?.message || '附件下载失败'), { silent: true });
|
||||
}
|
||||
const blob = res?.data;
|
||||
if (!(blob instanceof Blob)) {
|
||||
throw new Error('响应不是文件流');
|
||||
}
|
||||
if (blob.type && blob.type.includes('application/json')) {
|
||||
let message = '附件下载失败';
|
||||
try {
|
||||
const parsed = JSON.parse(await blob.text());
|
||||
message = parsed.msg || parsed.message || message;
|
||||
} catch {
|
||||
// 错误体非 JSON 时按默认提示
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
return blob;
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载金蝶侧发票附件(经 A801-2 后端代理),失败统一 ElMessage 提示
|
||||
* @param {String} invoiceNo 发票号码
|
||||
* @param {String} fileName 下载文件名(缺省按 发票号.pdf)
|
||||
*/
|
||||
export const downloadKingdeeInvoiceFile = async (invoiceNo, fileName) => {
|
||||
if (!invoiceNo) {
|
||||
ElMessage.warning('当前发票缺少发票号码,无法下载金蝶侧附件');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const blob = await requestKingdeeFile(invoiceNo);
|
||||
downloadFile(blob, fileName || `${invoiceNo}.pdf`);
|
||||
} catch (error) {
|
||||
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件下载失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 新窗口预览金蝶侧发票附件:拉取 Blob 转 objectURL(PDF 可预览;OFD 等浏览器不可预览的格式由浏览器触发下载)
|
||||
* @param {String} invoiceNo 发票号码
|
||||
* @param {String} fileName 预览文件名(缺省按 发票号.pdf)
|
||||
*/
|
||||
export const viewKingdeeInvoiceFile = async (invoiceNo, fileName) => {
|
||||
if (!invoiceNo) {
|
||||
ElMessage.warning('当前发票缺少发票号码,无法预览金蝶侧附件');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const blob = await requestKingdeeFile(invoiceNo);
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank');
|
||||
// 延迟释放 objectURL:立即回收可能导致新窗口尚未加载完成即失效
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
||||
} catch (error) {
|
||||
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件预览失败');
|
||||
}
|
||||
};
|
||||
@@ -499,6 +499,7 @@ const emptyLine = () => ({
|
||||
localId: ++localId,
|
||||
goodsCategory: '',
|
||||
goodsName: '',
|
||||
taxClassificationCode: '',
|
||||
unit: '吨',
|
||||
quantity: 0,
|
||||
unitPriceNoTax: 0,
|
||||
@@ -742,9 +743,11 @@ export default {
|
||||
const item = this.findInvoiceItem(line);
|
||||
if (item) {
|
||||
line.taxRate = Number(item.defaultTaxRate || 0);
|
||||
line.taxClassificationCode = item.taxClassificationCode || '';
|
||||
} else if (clearInvalid) {
|
||||
line.goodsName = '';
|
||||
line.taxRate = 0;
|
||||
line.taxClassificationCode = '';
|
||||
}
|
||||
this.recalculateLine(line);
|
||||
},
|
||||
@@ -752,6 +755,7 @@ export default {
|
||||
const names = this.invoiceItemNames(line.goodsCategory);
|
||||
if (!names.some(item => item.shortName === line.goodsName)) line.goodsName = '';
|
||||
line.taxRate = 0;
|
||||
line.taxClassificationCode = this.findInvoiceItem(line)?.taxClassificationCode || '';
|
||||
if (names.length === 1) {
|
||||
line.goodsName = names[0].shortName;
|
||||
this.handleGoodsNameChange(line);
|
||||
@@ -761,9 +765,11 @@ export default {
|
||||
const item = this.findInvoiceItem(line);
|
||||
if (!item) {
|
||||
line.taxRate = 0;
|
||||
line.taxClassificationCode = '';
|
||||
return;
|
||||
}
|
||||
line.taxRate = Number(item.defaultTaxRate || 0);
|
||||
line.taxClassificationCode = item.taxClassificationCode || '';
|
||||
this.recalculateLine(line);
|
||||
},
|
||||
validateSettlements(rule, value, callback) {
|
||||
@@ -1159,6 +1165,7 @@ export default {
|
||||
lines: sheet.lines.map(line => ({
|
||||
goodsCategory: line.goodsCategory,
|
||||
goodsName: line.goodsName,
|
||||
taxClassificationCode: line.taxClassificationCode,
|
||||
unit: line.unit,
|
||||
quantity: line.quantity,
|
||||
unitPriceNoTax: line.unitPriceNoTax,
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
v-if="hasPermission('invoice_application_sync')"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="batchSyncing"
|
||||
@click="handleBatchSync"
|
||||
>批量同步</el-button
|
||||
>
|
||||
@@ -124,6 +125,12 @@
|
||||
{{ displayValue(row[column.prop]) }}
|
||||
</el-tag>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<template v-else-if="column.prop === 'kingdeeLockStatus'">
|
||||
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
|
||||
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -192,7 +199,9 @@
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('invoice_application_void') && row.approvalStatus === 'approved'
|
||||
hasPermission('invoice_application_void') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
row.kingdeeLockStatus !== 'locked'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleVoid(row)"
|
||||
@@ -261,6 +270,7 @@ export default {
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
batchSyncing: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
mkEnabled: false,
|
||||
};
|
||||
@@ -398,8 +408,13 @@ export default {
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' });
|
||||
await api.approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
const res = await api.approve({ id: row.id });
|
||||
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||
if (res.data.data === false) {
|
||||
this.$message.warning(res.data.msg);
|
||||
} else {
|
||||
this.$message.success(res.data.msg || '审批通过');
|
||||
}
|
||||
this.loadTable();
|
||||
},
|
||||
async handleReturn(row) {
|
||||
@@ -415,14 +430,31 @@ export default {
|
||||
this.loadTable();
|
||||
},
|
||||
async handleBatchSync() {
|
||||
if (this.batchSyncing) return;
|
||||
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
|
||||
if (!rows.length) {
|
||||
this.$message.warning('请选择至少一条审批通过的开票申请');
|
||||
return;
|
||||
}
|
||||
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
|
||||
this.$message.success(`已同步${rows.length}条开票申请`);
|
||||
this.loadTable();
|
||||
this.batchSyncing = true;
|
||||
try {
|
||||
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
|
||||
this.$message.success(`已同步${rows.length}条开票申请`);
|
||||
await this.loadTable();
|
||||
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
|
||||
const idSet = new Set(rows.map(row => String(row.id)));
|
||||
const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id))
|
||||
&& ['pending', 'partial'].includes(row.attachmentSyncStatus)).length;
|
||||
if (pendingCount) {
|
||||
this.$message({
|
||||
type: 'warning',
|
||||
message: `其中${pendingCount}单附件未全部上传,可重推补传`,
|
||||
duration: 8000,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
this.batchSyncing = false;
|
||||
}
|
||||
},
|
||||
async handleExport() {
|
||||
const data = this.unwrapData(await api.getList(1, 100000, this.query));
|
||||
@@ -452,6 +484,28 @@ export default {
|
||||
''
|
||||
);
|
||||
},
|
||||
lockTag(row) {
|
||||
if (row.kingdeeLockStatus === 'locked') {
|
||||
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
|
||||
}
|
||||
if (row.kingdeeLockStatus === 'failed') {
|
||||
if ((row.kingdeeTryCount || 0) >= 5) {
|
||||
return {
|
||||
show: true,
|
||||
type: 'danger',
|
||||
label: '锁定失败·已达上限',
|
||||
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
|
||||
};
|
||||
}
|
||||
return {
|
||||
show: true,
|
||||
type: 'warning',
|
||||
label: '锁定失败·补偿中',
|
||||
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
|
||||
};
|
||||
}
|
||||
return { show: false };
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<el-col :span="6">
|
||||
<el-form-item label="开票金额">
|
||||
<el-input
|
||||
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.invoiceAmount) : ''"
|
||||
:model-value="form.invoiceNo ? formatMoney(form.invoiceAmount) : ''"
|
||||
disabled
|
||||
placeholder="从金蝶票据池带出"
|
||||
/>
|
||||
@@ -56,7 +56,7 @@
|
||||
<el-col :span="6">
|
||||
<el-form-item label="税额">
|
||||
<el-input
|
||||
:model-value="form.kingdeeInvoicePoolId ? formatMoney(form.taxAmount) : ''"
|
||||
:model-value="form.invoiceNo ? formatMoney(form.taxAmount) : ''"
|
||||
disabled
|
||||
placeholder="从金蝶票据池带出"
|
||||
/>
|
||||
@@ -118,7 +118,13 @@
|
||||
</section-card>
|
||||
|
||||
<section-card title="附件信息">
|
||||
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
|
||||
<!-- 金蝶票池认领的发票附件为 piaozone path 型,需带发票号走 A801-2 代理查看/下载 -->
|
||||
<vehicle-attachment-table
|
||||
v-model="form.attachments"
|
||||
:readonly="readonly"
|
||||
show-view-actions
|
||||
:kingdee-invoice-no="form.invoiceNo"
|
||||
/>
|
||||
</section-card>
|
||||
|
||||
<section-card title="备注">
|
||||
@@ -149,62 +155,13 @@
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<el-dialog v-model="invoiceDialog.visible" title="查询金蝶票据池" width="86%" append-to-body>
|
||||
<div class="receipt-form-page__dialog-search">
|
||||
<el-input
|
||||
v-model="invoiceDialog.keyword"
|
||||
clearable
|
||||
placeholder="发票号码、开票单位或受票单位"
|
||||
@keyup.enter="loadInvoicePool"
|
||||
/>
|
||||
<el-button type="primary" @click="loadInvoicePool">查询</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="invoiceDialog.loading"
|
||||
:data="invoiceDialog.rows"
|
||||
border
|
||||
empty-text="暂无票据数据"
|
||||
highlight-current-row
|
||||
@current-change="invoiceDialog.current = $event"
|
||||
@row-dblclick="confirmInvoice"
|
||||
>
|
||||
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
|
||||
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
|
||||
<el-table-column prop="invoiceType" label="发票类型" min-width="130" />
|
||||
<el-table-column prop="issuerName" label="开票单位" min-width="170" />
|
||||
<el-table-column prop="receiverName" label="受票单位" min-width="170" />
|
||||
<el-table-column label="开票金额" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税率" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ formatRate(row.taxRate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税额" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click.stop="confirmInvoice(row)">选择</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="receipt-form-page__dialog-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="invoiceDialog.page.current"
|
||||
v-model:page-size="invoiceDialog.page.size"
|
||||
:total="invoiceDialog.page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="refreshInvoicePoolPage"
|
||||
@size-change="handleInvoicePoolSizeChange"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="invoiceDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmInvoice()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<kingdee-invoice-pool-dialog
|
||||
v-model="invoiceDialog.visible"
|
||||
title="查询金蝶票据池"
|
||||
:initial-invoice-no="form.invoiceNo"
|
||||
:query-api="apiQueryInvoicePool"
|
||||
@confirm="handleInvoicePicked"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="settlementDialog.visible"
|
||||
@@ -259,82 +216,13 @@
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/invoiceReceipt';
|
||||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
|
||||
const mockInvoiceTypes = ['增值税专用发票', '增值税普通发票'];
|
||||
const mockTaxRates = [3, 6, 9, 13];
|
||||
const mockBankNames = ['中国工商银行', '中国农业银行', '中国银行', '中国建设银行', '招商银行'];
|
||||
|
||||
const randomDigits = length =>
|
||||
Array.from({ length }, () => Math.floor(Math.random() * 10)).join('');
|
||||
|
||||
const formatMockDate = date => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const customerName = customer =>
|
||||
customer.fullName || customer.shortName || customer.customerCode || `客商${customer.id}`;
|
||||
|
||||
const customerEmail = (customer, index) => {
|
||||
const prefix = String(customer.customerCode || customer.id || `customer${index + 1}`)
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '')
|
||||
.toLowerCase();
|
||||
return `finance@${prefix || `customer${index + 1}`}.example.com`;
|
||||
};
|
||||
|
||||
const createInvoicePoolMockRows = (customers, counterpartyName, departmentEmail) => {
|
||||
const rows = [];
|
||||
const baseId = Date.now();
|
||||
customers.forEach((customer, customerIndex) => {
|
||||
const name = customerName(customer);
|
||||
['issuer', 'receiver'].forEach((role, roleIndex) => {
|
||||
const count = 3 + Math.floor(Math.random() * 5);
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const rowIndex = rows.length;
|
||||
const invoiceDateValue = new Date();
|
||||
invoiceDateValue.setDate(invoiceDateValue.getDate() - Math.floor(Math.random() * 90));
|
||||
const invoiceDate = formatMockDate(invoiceDateValue);
|
||||
const taxRate = mockTaxRates[(customerIndex + roleIndex + index) % mockTaxRates.length];
|
||||
const invoiceAmount = Math.floor(10000 + Math.random() * 190000);
|
||||
const taxAmount = Number(
|
||||
((invoiceAmount * Number(taxRate)) / (100 + Number(taxRate))).toFixed(2)
|
||||
);
|
||||
const bankName = mockBankNames[(customerIndex + index) % mockBankNames.length];
|
||||
rows.push({
|
||||
id: baseId + rowIndex,
|
||||
invoiceNo: `25${randomDigits(18)}`,
|
||||
invoiceDate,
|
||||
invoiceType: mockInvoiceTypes[(customerIndex + index) % mockInvoiceTypes.length],
|
||||
taxRate,
|
||||
invoiceAmount,
|
||||
taxAmount,
|
||||
receiverName: role === 'receiver' ? name : counterpartyName,
|
||||
issuerName: role === 'issuer' ? name : counterpartyName,
|
||||
bankName,
|
||||
bankAccount: randomDigits(19),
|
||||
issuingBank: `${bankName}营业部`,
|
||||
phone: customer.contactPhone || '',
|
||||
customerEmails: customerEmail(customer, customerIndex),
|
||||
departmentEmails: departmentEmail || '',
|
||||
attachmentsJson: '[]',
|
||||
kingdeeBillNo: `KD-FP-${invoiceDate.replaceAll('-', '')}${randomDigits(5)}`,
|
||||
kingdeeStatus: 'unsynced',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
};
|
||||
|
||||
const emptyForm = () => ({
|
||||
id: null,
|
||||
kingdeeInvoicePoolId: null,
|
||||
invoiceNo: '',
|
||||
invoiceDate: '',
|
||||
invoiceType: '',
|
||||
@@ -346,8 +234,6 @@ const emptyForm = () => ({
|
||||
bankName: '',
|
||||
bankAccount: '',
|
||||
issuingBank: '',
|
||||
kingdeeBillNo: '',
|
||||
kingdeeStatus: 'unsynced',
|
||||
phone: '',
|
||||
customerEmails: [],
|
||||
departmentEmails: [],
|
||||
@@ -363,24 +249,17 @@ const emptyForm = () => ({
|
||||
export default {
|
||||
name: 'InvoiceReceiptForm',
|
||||
components: {
|
||||
KingdeeInvoicePoolDialog,
|
||||
VehicleAttachmentTable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Search,
|
||||
form: emptyForm(),
|
||||
invoicePoolMockRows: [],
|
||||
invoicePoolMockLoaded: false,
|
||||
customerEmailOptions: [],
|
||||
departmentEmailOptions: [],
|
||||
invoiceDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
keyword: '',
|
||||
sourceRows: [],
|
||||
rows: [],
|
||||
current: null,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
},
|
||||
settlementDialog: {
|
||||
visible: false,
|
||||
@@ -426,6 +305,12 @@ export default {
|
||||
this.initialize();
|
||||
},
|
||||
methods: {
|
||||
apiQueryInvoicePool(params) {
|
||||
return api.queryInvoicePool(params);
|
||||
},
|
||||
handleInvoicePicked(rows) {
|
||||
this.confirmInvoice(rows[0]);
|
||||
},
|
||||
hasPermission(code) {
|
||||
return this.permission?.[code] !== false;
|
||||
},
|
||||
@@ -483,7 +368,7 @@ export default {
|
||||
};
|
||||
},
|
||||
validateInvoice(rule, value, callback) {
|
||||
if (!value || !this.form.kingdeeInvoicePoolId) {
|
||||
if (!value) {
|
||||
callback(new Error('请选择金蝶票据池发票'));
|
||||
return;
|
||||
}
|
||||
@@ -509,99 +394,17 @@ export default {
|
||||
}
|
||||
callback();
|
||||
},
|
||||
async openInvoiceDialog() {
|
||||
openInvoiceDialog() {
|
||||
this.invoiceDialog.visible = true;
|
||||
this.invoiceDialog.keyword = this.form.invoiceNo || '';
|
||||
await this.loadInvoicePool();
|
||||
},
|
||||
async loadInvoicePool() {
|
||||
this.invoiceDialog.loading = true;
|
||||
this.invoiceDialog.page.current = 1;
|
||||
this.invoiceDialog.current = null;
|
||||
try {
|
||||
if (!this.recordId) {
|
||||
if (!this.invoicePoolMockLoaded) await this.loadInvoicePoolMockRows();
|
||||
this.invoiceDialog.sourceRows = this.filterInvoicePoolMockRows(
|
||||
this.invoiceDialog.keyword
|
||||
);
|
||||
this.refreshInvoicePoolPage();
|
||||
return;
|
||||
}
|
||||
const data = this.unwrapData(await api.getInvoicePool(this.invoiceDialog.keyword));
|
||||
this.invoiceDialog.sourceRows = Array.isArray(data) ? data : data?.records || [];
|
||||
this.refreshInvoicePoolPage();
|
||||
} finally {
|
||||
this.invoiceDialog.loading = false;
|
||||
}
|
||||
},
|
||||
async loadInvoicePoolMockRows() {
|
||||
const pageSize = 500;
|
||||
const firstData = this.unwrapData(await getCustomerList(1, pageSize, {}));
|
||||
const firstRows = Array.isArray(firstData) ? firstData : firstData?.records || [];
|
||||
const total = Number(firstData?.total || firstRows.length);
|
||||
const pageCount = Math.ceil(total / pageSize);
|
||||
const remainingPages = await Promise.all(
|
||||
Array.from({ length: Math.max(pageCount - 1, 0) }, (_, index) =>
|
||||
getCustomerList(index + 2, pageSize, {})
|
||||
)
|
||||
);
|
||||
const customers = [
|
||||
...firstRows,
|
||||
...remainingPages.flatMap(response => {
|
||||
const data = this.unwrapData(response);
|
||||
return Array.isArray(data) ? data : data?.records || [];
|
||||
}),
|
||||
].filter(
|
||||
(customer, index, rows) =>
|
||||
rows.findIndex(item => String(item.id) === String(customer.id)) === index
|
||||
);
|
||||
const userInfo = this.$store.getters.userInfo || {};
|
||||
const counterpartyName = userInfo.deptName || userInfo.dept_name || '本企业';
|
||||
this.invoicePoolMockRows = createInvoicePoolMockRows(
|
||||
customers,
|
||||
counterpartyName,
|
||||
userInfo.email || ''
|
||||
);
|
||||
this.invoicePoolMockLoaded = true;
|
||||
},
|
||||
refreshInvoicePoolPage() {
|
||||
const page = this.invoiceDialog.page;
|
||||
const total = this.invoiceDialog.sourceRows.length;
|
||||
const maxPage = Math.max(Math.ceil(total / page.size), 1);
|
||||
if (page.current > maxPage) page.current = maxPage;
|
||||
const start = (page.current - 1) * page.size;
|
||||
page.total = total;
|
||||
this.invoiceDialog.current = null;
|
||||
this.invoiceDialog.rows = this.invoiceDialog.sourceRows.slice(start, start + page.size);
|
||||
},
|
||||
handleInvoicePoolSizeChange() {
|
||||
this.invoiceDialog.page.current = 1;
|
||||
this.refreshInvoicePoolPage();
|
||||
},
|
||||
filterInvoicePoolMockRows(keyword) {
|
||||
const normalizedKeyword = String(keyword || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!normalizedKeyword) return this.invoicePoolMockRows.map(item => ({ ...item }));
|
||||
return this.invoicePoolMockRows
|
||||
.filter(item =>
|
||||
[item.invoiceNo, item.issuerName, item.receiverName].some(value =>
|
||||
String(value || '')
|
||||
.toLowerCase()
|
||||
.includes(normalizedKeyword)
|
||||
)
|
||||
)
|
||||
.map(item => ({ ...item }));
|
||||
},
|
||||
confirmInvoice(row, options = {}) {
|
||||
const selected = row?.id ? row : this.invoiceDialog.current;
|
||||
const selected = row;
|
||||
if (!selected) {
|
||||
this.$message.warning('请选择一张金蝶票据池发票');
|
||||
return;
|
||||
}
|
||||
const preserveUserInput = options.preserveUserInput === true;
|
||||
Object.assign(this.form, {
|
||||
kingdeeInvoicePoolId: selected.id,
|
||||
invoiceNo: selected.invoiceNo,
|
||||
invoiceDate: selected.invoiceDate,
|
||||
invoiceType: selected.invoiceType,
|
||||
@@ -613,8 +416,6 @@ export default {
|
||||
bankName: selected.bankName,
|
||||
bankAccount: selected.bankAccount,
|
||||
issuingBank: selected.issuingBank,
|
||||
kingdeeBillNo: selected.kingdeeBillNo || '',
|
||||
kingdeeStatus: selected.kingdeeStatus || 'unsynced',
|
||||
phone: preserveUserInput ? this.form.phone : selected.phone || '',
|
||||
customerEmails: preserveUserInput
|
||||
? this.form.customerEmails
|
||||
@@ -725,7 +526,6 @@ export default {
|
||||
payload() {
|
||||
return {
|
||||
id: this.form.id,
|
||||
kingdeeInvoicePoolId: this.form.kingdeeInvoicePoolId,
|
||||
invoiceNo: this.form.invoiceNo,
|
||||
invoiceDate: this.form.invoiceDate,
|
||||
invoiceType: this.form.invoiceType,
|
||||
@@ -737,8 +537,6 @@ export default {
|
||||
bankName: this.form.bankName,
|
||||
bankAccount: this.form.bankAccount,
|
||||
issuingBank: this.form.issuingBank,
|
||||
kingdeeBillNo: this.form.kingdeeBillNo,
|
||||
kingdeeStatus: this.form.kingdeeStatus,
|
||||
phone: this.form.phone,
|
||||
customerEmails: this.form.customerEmails.join(';'),
|
||||
departmentEmails: this.form.departmentEmails.join(';'),
|
||||
@@ -782,13 +580,12 @@ export default {
|
||||
this.$message.warning('请先选择金蝶票据池发票');
|
||||
return;
|
||||
}
|
||||
const data = this.recordId
|
||||
? this.unwrapData(await api.getInvoicePool(this.form.invoiceNo))
|
||||
: this.filterInvoicePoolMockRows(this.form.invoiceNo);
|
||||
const rows = Array.isArray(data) ? data : data?.records || [];
|
||||
const invoice = rows.find(item => item.invoiceNo === this.form.invoiceNo);
|
||||
const data = this.unwrapData(
|
||||
await api.queryInvoicePool({ current: 1, size: 1, invoiceNo: this.form.invoiceNo })
|
||||
);
|
||||
const invoice = (data.records || []).find(item => item.invoiceNo === this.form.invoiceNo);
|
||||
if (!invoice) {
|
||||
this.$message.warning('金蝶票据池中未查询到该发票');
|
||||
this.$message.warning('金蝶票池未查询到该发票');
|
||||
return;
|
||||
}
|
||||
if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return;
|
||||
@@ -851,11 +648,6 @@ export default {
|
||||
.receipt-form-page__dialog-search .el-input {
|
||||
width: 360px;
|
||||
}
|
||||
.receipt-form-page__dialog-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.receipt-form-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="searchExpanded" label="金蝶单据状态">
|
||||
<el-select v-model="query.kingdeeStatus" clearable placeholder="请选择">
|
||||
<el-form-item v-if="searchExpanded" label="金蝶状态">
|
||||
<el-select v-model="query.kingdeePushStatus" clearable placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in kingdeeStatusOptions"
|
||||
v-for="item in kingdeePushStatusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
@@ -72,7 +72,8 @@
|
||||
v-if="hasPermission('invoice_receipt_sync')"
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleBatchSync"
|
||||
:loading="pushLoading"
|
||||
@click="handlePushKingdee"
|
||||
>批量同步</el-button
|
||||
>
|
||||
<el-button v-if="hasPermission('invoice_receipt_add')" type="primary" @click="openCreate"
|
||||
@@ -229,7 +230,7 @@ const emptyQuery = () => ({
|
||||
deptName: '',
|
||||
deptPath: [],
|
||||
approvalStatus: '',
|
||||
kingdeeStatus: '',
|
||||
kingdeePushStatus: '',
|
||||
});
|
||||
|
||||
export default {
|
||||
@@ -248,12 +249,13 @@ export default {
|
||||
searchVisible: true,
|
||||
tableSize: 'default',
|
||||
approvalStatusOptions: api.approvalStatusOptions,
|
||||
kingdeeStatusOptions: api.kingdeeStatusOptions,
|
||||
kingdeePushStatusOptions: api.kingdeePushStatusOptions,
|
||||
columns: invoiceReceiptTableColumns,
|
||||
visibleColumnKeys: invoiceReceiptTableColumns.map(column => column.prop),
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
pushLoading: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
mkEnabled: false,
|
||||
};
|
||||
@@ -363,8 +365,13 @@ export default {
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' });
|
||||
await api.approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
const res = await api.approve({ id: row.id });
|
||||
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||
if (res.data.data === false) {
|
||||
this.$message.warning(res.data.msg);
|
||||
} else {
|
||||
this.$message.success(res.data.msg || '审批通过');
|
||||
}
|
||||
this.loadTable();
|
||||
},
|
||||
async handleReturn(row) {
|
||||
@@ -379,15 +386,29 @@ export default {
|
||||
this.$message.success('作废成功');
|
||||
this.loadTable();
|
||||
},
|
||||
async handleBatchSync() {
|
||||
async handlePushKingdee() {
|
||||
if (this.pushLoading) return;
|
||||
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
|
||||
if (!rows.length) {
|
||||
this.$message.warning('请选择至少一条审批通过的收票登记');
|
||||
return;
|
||||
}
|
||||
await Promise.all(rows.map(row => api.syncKingdee(row.id)));
|
||||
this.$message.success(`已同步${rows.length}条收票登记`);
|
||||
this.loadTable();
|
||||
await this.$confirm(`确认将选中的 ${rows.length} 条收票登记同步至金蝶?`, '提示', {
|
||||
type: 'warning',
|
||||
});
|
||||
this.pushLoading = true;
|
||||
try {
|
||||
const summary = this.unwrapData(await api.pushKingdee(rows.map(row => row.id)));
|
||||
await this.loadTable();
|
||||
// 附件联带同步提示:推送成功但附件未全部上传时在结果弹窗追加说明,重推即自动补传
|
||||
const idSet = new Set(rows.map(row => String(row.id)));
|
||||
const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id))
|
||||
&& ['pending', 'partial'].includes(row.attachmentSyncStatus)).length;
|
||||
const attachmentTip = pendingCount ? `\n其中${pendingCount}单附件未全部上传,可重推补传` : '';
|
||||
this.$alert((summary || '推送完成') + attachmentTip, '推送结果', { confirmButtonText: '知道了' });
|
||||
} finally {
|
||||
this.pushLoading = false;
|
||||
}
|
||||
},
|
||||
async handleExport() {
|
||||
const data = this.unwrapData(await api.getList(1, 100000, this.query));
|
||||
@@ -404,8 +425,9 @@ export default {
|
||||
当前处理人: item.currentProcessor,
|
||||
创建人: item.createUserName,
|
||||
创建时间: item.createTime,
|
||||
金蝶单据号: item.kingdeeBillNo,
|
||||
金蝶单据状态: item.kingdeeStatusName,
|
||||
金蝶单据号: item.kingdeeReceivedInvoiceNo,
|
||||
金蝶状态: item.kingdeePushStatusName,
|
||||
失败原因: item.kingdeePushMsg,
|
||||
}));
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(exportRows), '收票登记');
|
||||
|
||||
@@ -283,8 +283,18 @@
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<div class="payment-form-page__invoice-actions">
|
||||
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
|
||||
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
:disabled="kingdeeAttachmentBusy"
|
||||
@click="viewInvoiceAttachment(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link
|
||||
type="primary"
|
||||
:disabled="kingdeeAttachmentBusy"
|
||||
@click="downloadInvoiceAttachment(row)"
|
||||
>下载</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="!readonly"
|
||||
type="danger"
|
||||
@@ -669,6 +679,11 @@ import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settle
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import {
|
||||
downloadKingdeeInvoiceFile,
|
||||
isKingdeePathAttachment,
|
||||
viewKingdeeInvoiceFile,
|
||||
} from '@/utils/kingdee-attachment';
|
||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||
|
||||
const attachmentViewerPlugins = [
|
||||
@@ -749,6 +764,8 @@ export default {
|
||||
form: emptyForm(),
|
||||
amountSyncing: false,
|
||||
transferPayload: null,
|
||||
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
|
||||
kingdeeAttachmentBusy: false,
|
||||
referenceRows: [],
|
||||
paymentRecords: [],
|
||||
referenceVisible: false,
|
||||
@@ -1598,8 +1615,19 @@ export default {
|
||||
this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf`
|
||||
);
|
||||
},
|
||||
viewInvoiceAttachment(row) {
|
||||
async viewInvoiceAttachment(row) {
|
||||
const attachment = this.invoiceAttachment(row);
|
||||
// 金蝶侧附件(path 型)经后端代理 A801-2 预览,OSS 直链维持本地预览组件
|
||||
if (isKingdeePathAttachment(attachment)) {
|
||||
if (this.kingdeeAttachmentBusy) return;
|
||||
this.kingdeeAttachmentBusy = true;
|
||||
try {
|
||||
await viewKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
|
||||
} finally {
|
||||
this.kingdeeAttachmentBusy = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this.attachmentFileUrl(attachment)) {
|
||||
this.$message.warning('当前发票暂无可查看附件');
|
||||
return;
|
||||
@@ -1609,8 +1637,20 @@ export default {
|
||||
[attachment]
|
||||
);
|
||||
},
|
||||
downloadInvoiceAttachment(row) {
|
||||
const url = this.invoiceAttachmentUrl(row);
|
||||
async downloadInvoiceAttachment(row) {
|
||||
const attachment = this.invoiceAttachment(row);
|
||||
// 金蝶侧附件(path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
|
||||
if (isKingdeePathAttachment(attachment)) {
|
||||
if (this.kingdeeAttachmentBusy) return;
|
||||
this.kingdeeAttachmentBusy = true;
|
||||
try {
|
||||
await downloadKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
|
||||
} finally {
|
||||
this.kingdeeAttachmentBusy = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const url = this.attachmentFileUrl(attachment);
|
||||
if (!url) {
|
||||
this.$message.warning('当前发票暂无可下载附件');
|
||||
return;
|
||||
|
||||
@@ -61,6 +61,13 @@
|
||||
}}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #kingdeeLockStatus="{ row }">
|
||||
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
|
||||
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
v-if="hasPermission('payment_application_view')"
|
||||
@@ -117,7 +124,11 @@
|
||||
>驳回</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('payment_application_void') && row.approvalStatus === 'approved'"
|
||||
v-if="
|
||||
hasPermission('payment_application_void') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
row.kingdeeLockStatus !== 'locked'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleVoid(row)"
|
||||
>作废</el-link
|
||||
@@ -149,6 +160,8 @@ export default {
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
syncLoading: false,
|
||||
resultSyncing: false,
|
||||
query: {},
|
||||
option: {},
|
||||
page: {
|
||||
@@ -288,8 +301,13 @@ export default {
|
||||
this.refreshChange();
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await api.approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
const res = await api.approve({ id: row.id });
|
||||
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||
if (res.data.data === false) {
|
||||
this.$message.warning(res.data.msg);
|
||||
} else {
|
||||
this.$message.success(res.data.msg || '审批通过');
|
||||
}
|
||||
this.refreshChange();
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
@@ -325,18 +343,60 @@ export default {
|
||||
async handleSyncRow(row) {
|
||||
const data = this.unwrapData(await api.syncKingdee(row.id));
|
||||
this.$message.success(`已生成金蝶付款单:${data}`);
|
||||
this.refreshChange();
|
||||
await this.refreshChange();
|
||||
this.warnAttachmentNotUploaded([row.id]);
|
||||
},
|
||||
async handleSync() {
|
||||
if (this.syncLoading) return;
|
||||
const rows = this.selection.filter(row => row.approvalStatus === 'approved');
|
||||
if (!rows.length) {
|
||||
this.$message.warning('请选择至少一条审批通过的付款申请');
|
||||
return;
|
||||
}
|
||||
await api.syncKingdeeBatch(rows.map(row => row.id));
|
||||
this.$message.success(`已同步${rows.length}条付款申请`);
|
||||
this.refreshChange();
|
||||
this.syncLoading = true;
|
||||
try {
|
||||
const data = this.unwrapData(await api.syncKingdeeBatch(rows.map(row => row.id))) || [];
|
||||
const failedList = data.filter(item => String(item).includes('同步失败'));
|
||||
if (failedList.length) {
|
||||
this.$message({
|
||||
type: 'warning',
|
||||
message: `同步完成:成功${data.length - failedList.length}条,失败${failedList.length}条。${failedList.join(';')}`,
|
||||
duration: 8000,
|
||||
});
|
||||
} else {
|
||||
this.$message.success(`已同步${data.length}条付款申请`);
|
||||
}
|
||||
await this.refreshChange();
|
||||
this.warnAttachmentNotUploaded(rows.map(row => row.id));
|
||||
} finally {
|
||||
this.syncLoading = false;
|
||||
}
|
||||
},
|
||||
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
|
||||
warnAttachmentNotUploaded(ids) {
|
||||
const idSet = new Set(ids.map(String));
|
||||
const pendingCount = (this.rows || []).filter(row => idSet.has(String(row.id))
|
||||
&& ['pending', 'partial'].includes(row.attachmentSyncStatus)).length;
|
||||
if (pendingCount) {
|
||||
this.$message({
|
||||
type: 'warning',
|
||||
message: `其中${pendingCount}单附件未全部上传,可重推补传`,
|
||||
duration: 8000,
|
||||
});
|
||||
}
|
||||
},
|
||||
async handleSyncResult() {
|
||||
if (this.resultSyncing) return;
|
||||
this.resultSyncing = true;
|
||||
try {
|
||||
const data = this.unwrapData(await api.syncKingdeeResult());
|
||||
this.$message.success(data || '金蝶付款结果回写完成');
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.resultSyncing = false;
|
||||
}
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
const columns = this.option.column.filter(column => !column.hide && column.prop);
|
||||
const headers = columns.map(item => item.label);
|
||||
@@ -361,6 +421,34 @@ export default {
|
||||
''
|
||||
);
|
||||
},
|
||||
lockTag(row) {
|
||||
if (row.kingdeeLockStatus === 'locked') {
|
||||
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
|
||||
}
|
||||
if (row.kingdeeLockStatus === 'failed') {
|
||||
if ((row.kingdeeTryCount || 0) >= 5) {
|
||||
return {
|
||||
show: true,
|
||||
type: 'danger',
|
||||
label: '锁定失败·已达上限',
|
||||
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
|
||||
};
|
||||
}
|
||||
return {
|
||||
show: true,
|
||||
type: 'warning',
|
||||
label: '锁定失败·补偿中',
|
||||
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
|
||||
};
|
||||
}
|
||||
return { show: false };
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
formatMoney(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -56,6 +56,16 @@
|
||||
<el-form-item v-show="searchExpanded" label="所属组织">
|
||||
<el-input v-model="query.claimerDeptName" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="推送状态">
|
||||
<el-select v-model="query.pushStatus" clearable placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in pushStatusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="金蝶单据状态">
|
||||
<el-select v-model="query.kingdeeBillStatus" clearable placeholder="请选择">
|
||||
<el-option
|
||||
@@ -83,6 +93,9 @@
|
||||
|
||||
<section class="claim-record-page__table-panel">
|
||||
<div class="claim-record-page__toolbar">
|
||||
<el-button :loading="refreshing" @click="handleRefreshKingdeeResult">
|
||||
查询金蝶认领结果
|
||||
</el-button>
|
||||
<table-header-tools
|
||||
:columns="columns"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
@@ -118,7 +131,11 @@
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="hasPermission('receipt_claim_record_void') && row.claimStatus === 'claimed'"
|
||||
v-if="
|
||||
hasPermission('receipt_claim_record_void') &&
|
||||
row.claimStatus === 'claimed' &&
|
||||
row.pushStatus !== 'pushed'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleVoid(row)"
|
||||
>作废</el-link
|
||||
@@ -164,6 +181,7 @@ const emptyQuery = () => ({
|
||||
claimDateRange: [],
|
||||
claimerDeptName: '',
|
||||
kingdeeBillStatus: '',
|
||||
pushStatus: '',
|
||||
});
|
||||
|
||||
export default {
|
||||
@@ -182,10 +200,12 @@ export default {
|
||||
tableSize: 'default',
|
||||
claimStatusOptions: api.claimStatusOptions,
|
||||
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
|
||||
pushStatusOptions: api.pushStatusOptions,
|
||||
columns: receiptClaimRecordTableColumns,
|
||||
visibleColumnKeys: receiptClaimRecordTableColumns.map(column => column.prop),
|
||||
rows: [],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
};
|
||||
},
|
||||
@@ -206,8 +226,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
unwrapData(response) {
|
||||
const body = response?.data || response || {};
|
||||
return body?.data || body;
|
||||
// 数字 0 是合法返回值(如回写/同步 0 条),须用 ?? 判空避免 0 被信封对象顶掉导致 Number() 得 NaN
|
||||
const body = response?.data ?? response ?? {};
|
||||
return body?.data ?? body;
|
||||
},
|
||||
queryParams() {
|
||||
const transactionRange = this.query.transactionDateRange || [];
|
||||
@@ -251,14 +272,24 @@ export default {
|
||||
},
|
||||
async handleVoid(row) {
|
||||
await this.$confirm(
|
||||
'作废后将回退本次认领金额,并生成审核通过状态的金蝶认领冲单,是否继续?',
|
||||
'作废后将回退本次认领金额,是否继续?',
|
||||
'作废认领记录',
|
||||
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
|
||||
);
|
||||
const kingdeeBillNo = this.unwrapData(await api.voidClaim(row.id));
|
||||
this.$message.success(`作废成功,金蝶冲单号:${kingdeeBillNo}`);
|
||||
await api.voidClaim(row.id);
|
||||
this.$message.success('作废成功');
|
||||
await this.loadTable();
|
||||
},
|
||||
async handleRefreshKingdeeResult() {
|
||||
this.refreshing = true;
|
||||
try {
|
||||
const updated = Number(this.unwrapData(await api.queryKingdeeResult()) || 0);
|
||||
this.$message.success(`金蝶认领结果回写完成,更新${updated}条`);
|
||||
await this.loadTable();
|
||||
} finally {
|
||||
this.refreshing = false;
|
||||
}
|
||||
},
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
},
|
||||
@@ -305,7 +336,7 @@ export default {
|
||||
}
|
||||
.claim-record-page__toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
<el-table-column label="已认领金额" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.claimedAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金蝶未认领金额" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.unclaimedAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="summary" label="摘要" min-width="180" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<el-row :gutter="32" class="receipt-claim-batch-form-page__claim-info">
|
||||
@@ -336,11 +339,11 @@ export default {
|
||||
for (const flow of this.flows) {
|
||||
const total = this.flowAllocatedTotal(flow.id);
|
||||
const remaining = Math.max(
|
||||
Number(flow.receiptAmount || 0) - Number(flow.claimedAmount || 0),
|
||||
Number(flow.unclaimedAmount || 0) - Number(flow.claimedAmount || 0),
|
||||
0
|
||||
);
|
||||
if (total > remaining + 0.001) {
|
||||
this.$message.warning(`流水${flow.receiptNoticeNo}的分摊金额不能超过剩余可认领金额`);
|
||||
this.$message.warning(`流水${flow.receiptNoticeNo}的累计认领金额不能超过公告未认领金额`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,14 @@
|
||||
><el-form-item label="已认领金额"
|
||||
><el-input :model-value="formatMoney(form.claimedAmount)" disabled /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item label="金蝶未认领金额"
|
||||
><el-input :model-value="formatMoney(form.unclaimedAmount)" disabled /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item label="剩余可认领金额"
|
||||
><el-input :model-value="formatMoney(claimableAmount)" disabled /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item label="摘要"><el-input v-model="form.summary" disabled /></el-form-item
|
||||
></el-col>
|
||||
@@ -197,6 +205,7 @@ const emptyForm = () => ({
|
||||
transactionTime: '',
|
||||
detailSerialNo: '',
|
||||
claimedAmount: 0,
|
||||
unclaimedAmount: 0,
|
||||
claimerName: '',
|
||||
claimerDeptName: '',
|
||||
claimDate: '',
|
||||
@@ -242,6 +251,12 @@ export default {
|
||||
0
|
||||
);
|
||||
},
|
||||
claimableAmount() {
|
||||
return Math.max(
|
||||
Number(this.form.unclaimedAmount || 0) - Number(this.form.claimedAmount || 0),
|
||||
0
|
||||
);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (!this.batchMode) this.initialize();
|
||||
@@ -331,8 +346,8 @@ export default {
|
||||
this.$message.warning('分摊收款金额必须大于0');
|
||||
return;
|
||||
}
|
||||
if (this.allocatedTotal > amount - Number(this.form.claimedAmount || 0) + 0.001) {
|
||||
this.$message.warning('本次分摊金额不能超过流水剩余可认领金额');
|
||||
if (this.allocatedTotal > this.claimableAmount + 0.001) {
|
||||
this.$message.warning('累计认领金额不能超过公告未认领金额(剩余可认领金额)');
|
||||
return;
|
||||
}
|
||||
for (const row of this.form.settlements)
|
||||
|
||||
@@ -15,10 +15,17 @@
|
||||
<el-form-item label="对方账号">
|
||||
<el-input v-model="query.counterpartyAccount" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="摘要">
|
||||
<el-input v-model="query.summary" clearable placeholder="请输入" />
|
||||
<el-form-item label="推送状态">
|
||||
<el-select v-model="query.pushStatus" clearable placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in pushStatusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="认领状态">
|
||||
<el-form-item label="认领状态">
|
||||
<el-select v-model="query.claimStatus" clearable placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in claimStatusOptions"
|
||||
@@ -28,7 +35,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="交易时间">
|
||||
<el-form-item label="交易时间">
|
||||
<el-date-picker
|
||||
v-model="query.transactionTimeRange"
|
||||
type="datetimerange"
|
||||
@@ -39,6 +46,45 @@
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="对方开户行">
|
||||
<el-input v-model="query.counterpartyBank" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="摘要">
|
||||
<el-input v-model="query.summary" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="明细流水号">
|
||||
<el-input v-model="query.detailSerialNo" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="金蝶状态">
|
||||
<el-select v-model="query.kingdeeBillStatus" clearable placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in kingdeeBillStatusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="收款金额">
|
||||
<div class="amount-range">
|
||||
<el-input-number
|
||||
v-model="query.receiptAmountStart"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
placeholder="最低金额"
|
||||
/>
|
||||
<span>~</span>
|
||||
<el-input-number
|
||||
v-model="query.receiptAmountEnd"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
placeholder="最高金额"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="searchExpanded" label="认领人">
|
||||
<el-input v-model="query.claimerName" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="receipt-flow-page__search-actions">
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
@@ -104,7 +150,7 @@
|
||||
</el-link>
|
||||
<el-tag
|
||||
v-else-if="column.status"
|
||||
:type="statusType(row.claimStatus)"
|
||||
:type="statusType(row, column)"
|
||||
class="status-text"
|
||||
>
|
||||
{{ displayValue(row[column.prop]) }}
|
||||
@@ -113,10 +159,23 @@
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<el-table-column label="操作" width="170" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="canClaim(row)" type="primary" @click="openClaim(row)">认领</el-link>
|
||||
<span v-else>-</span>
|
||||
<el-link
|
||||
v-if="canClaim(row)"
|
||||
type="primary"
|
||||
:disabled="pushingId === row.id"
|
||||
@click="openClaim(row)"
|
||||
>认领</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="canPush(row)"
|
||||
type="warning"
|
||||
:disabled="pushingId === row.id"
|
||||
@click="handlePush(row)"
|
||||
>推送金蝶</el-link
|
||||
>
|
||||
<span v-if="!canClaim(row) && !canPush(row)">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -167,7 +226,7 @@
|
||||
{{ displayValue(detailDialog.data.detailSerialNo) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="认领状态">
|
||||
<el-tag :type="statusType(detailDialog.data.claimStatus)">
|
||||
<el-tag :type="statusType(detailDialog.data, { prop: 'claimStatusName' })">
|
||||
{{ displayValue(detailDialog.data.claimStatusName) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
@@ -177,6 +236,23 @@
|
||||
<el-descriptions-item label="剩余可认领金额">
|
||||
{{ formatMoney(detailDialog.data.remainingAmount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="金蝶未认领金额">
|
||||
{{ formatMoney(detailDialog.data.unclaimedAmount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="推送状态">
|
||||
<el-tag :type="statusType(detailDialog.data, { prop: 'pushStatusName' })">
|
||||
{{ displayValue(detailDialog.data.pushStatusName) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="金蝶收款单号">
|
||||
{{ displayValue(detailDialog.data.kingdeeBillNo) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="金蝶状态">
|
||||
{{ displayValue(detailDialog.data.kingdeeBillStatusName) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="推送失败原因">
|
||||
{{ displayValue(detailDialog.data.pushFailReason) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="摘要">
|
||||
{{ displayValue(detailDialog.data.summary) }}
|
||||
</el-descriptions-item>
|
||||
@@ -191,7 +267,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="operatorName" label="认领人" min-width="120" align="center" />
|
||||
<el-table-column label="状态变化" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ claimStatusChange(row) }}</template>
|
||||
<template #default="{ row }">
|
||||
{{ row.actionType === 'push' ? row.actionName : claimStatusChange(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="认领说明" min-width="220" />
|
||||
</el-table>
|
||||
@@ -211,71 +289,19 @@ import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const receiptFlowMockTemplates = [
|
||||
{
|
||||
receiptNoticeNo: 'SKTZ20260827001',
|
||||
payerName: '华东供应链管理有限公司',
|
||||
receiptAmount: 48000,
|
||||
counterpartyName: '上海安捷运输有限公司',
|
||||
counterpartyAccount: '310000000000001001',
|
||||
counterpartyBank: '中国工商银行上海分行',
|
||||
summary: '运输服务款',
|
||||
transactionTime: '2026-08-27 09:30:00',
|
||||
detailSerialNo: 'MOCK-RF-20260827-001',
|
||||
sourceUpdatedTime: '2026-08-27 09:30:00',
|
||||
},
|
||||
{
|
||||
receiptNoticeNo: 'SKTZ20260827002',
|
||||
payerName: '华东供应链管理有限公司',
|
||||
receiptAmount: 26500,
|
||||
counterpartyName: '江苏联运物流有限公司',
|
||||
counterpartyAccount: '320100000000002002',
|
||||
counterpartyBank: '中国建设银行南京分行',
|
||||
summary: '物流服务款',
|
||||
transactionTime: '2026-08-27 13:45:00',
|
||||
detailSerialNo: 'MOCK-RF-20260827-002',
|
||||
sourceUpdatedTime: '2026-08-27 13:45:00',
|
||||
},
|
||||
];
|
||||
|
||||
const randomDigits = length =>
|
||||
Array.from({ length }, () => Math.floor(Math.random() * 10)).join('');
|
||||
|
||||
const formatMockDateTime = date => {
|
||||
const pad = value => String(value).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
|
||||
date.getHours()
|
||||
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
};
|
||||
|
||||
const createReceiptFlowMockRows = () => {
|
||||
const count = 20 + Math.floor(Math.random() * 31);
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const template = receiptFlowMockTemplates[index % receiptFlowMockTemplates.length];
|
||||
const transactionDate = new Date();
|
||||
transactionDate.setDate(transactionDate.getDate() - Math.floor(Math.random() * 7));
|
||||
transactionDate.setHours(8 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 60), 0);
|
||||
const transactionTime = formatMockDateTime(transactionDate);
|
||||
const dateCode = transactionTime.slice(0, 10).replaceAll('-', '');
|
||||
return {
|
||||
...template,
|
||||
receiptNoticeNo: `SKTZ${dateCode}${randomDigits(5)}`,
|
||||
receiptAmount: Math.floor(10000 + Math.random() * 90000),
|
||||
counterpartyAccount: `${template.counterpartyAccount.slice(0, -6)}${randomDigits(6)}`,
|
||||
transactionTime,
|
||||
detailSerialNo: `MOCK-RF-${dateCode}-${index + 1}${randomDigits(4)}`,
|
||||
sourceUpdatedTime: transactionTime,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const emptyQuery = () => ({
|
||||
receiptNoticeNo: '',
|
||||
counterpartyName: '',
|
||||
counterpartyBank: '',
|
||||
counterpartyAccount: '',
|
||||
summary: '',
|
||||
detailSerialNo: '',
|
||||
claimStatus: '',
|
||||
pushStatus: '',
|
||||
kingdeeBillStatus: '',
|
||||
receiptAmountStart: undefined,
|
||||
receiptAmountEnd: undefined,
|
||||
claimerName: '',
|
||||
transactionTimeRange: [],
|
||||
});
|
||||
|
||||
@@ -288,6 +314,8 @@ export default {
|
||||
ArrowUp,
|
||||
query: emptyQuery(),
|
||||
claimStatusOptions: api.claimStatusOptions,
|
||||
pushStatusOptions: api.pushStatusOptions,
|
||||
kingdeeBillStatusOptions: api.kingdeeBillStatusOptions,
|
||||
columns: receiptFlowTableColumns,
|
||||
visibleColumnKeys: receiptFlowTableColumns.map(column => column.prop),
|
||||
searchVisible: true,
|
||||
@@ -296,6 +324,7 @@ export default {
|
||||
selectedRows: [],
|
||||
loading: false,
|
||||
syncing: false,
|
||||
pushingId: null,
|
||||
searchExpanded: false,
|
||||
// 与 avue-crud 一致:搜索项数量超过 searchIndex(默认一行 4 项)时才显示“展开/收起”
|
||||
searchIndex: 4,
|
||||
@@ -321,8 +350,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
unwrapData(response) {
|
||||
const body = response?.data || response || {};
|
||||
return body?.data || body;
|
||||
// 数字 0 是合法返回值(如回写/同步 0 条),须用 ?? 判空避免 0 被信封对象顶掉导致 Number() 得 NaN
|
||||
const body = response?.data ?? response ?? {};
|
||||
return body?.data ?? body;
|
||||
},
|
||||
async loadTable() {
|
||||
this.loading = true;
|
||||
@@ -358,9 +388,8 @@ export default {
|
||||
async handleSync() {
|
||||
this.syncing = true;
|
||||
try {
|
||||
const mockRows = createReceiptFlowMockRows();
|
||||
const syncedCount = Number(this.unwrapData(await api.sync({ flows: mockRows })) || 0);
|
||||
this.$message.success(`流水同步完成,共同步${syncedCount}条`);
|
||||
const syncedCount = Number(this.unwrapData(await api.sync()) || 0);
|
||||
this.$message.success(`流水同步完成,共同步${syncedCount}条认领公告`);
|
||||
await this.loadTable();
|
||||
} finally {
|
||||
this.syncing = false;
|
||||
@@ -419,8 +448,41 @@ export default {
|
||||
canClaim(row) {
|
||||
return this.hasPermission('receipt_flow_claim') && Number(row.remainingAmount || 0) > 0;
|
||||
},
|
||||
statusType(status) {
|
||||
return { claimed: 'success', partial: 'warning', unclaimed: 'info' }[status] || '';
|
||||
canPush(row) {
|
||||
return (
|
||||
this.hasPermission('receipt_flow_push') &&
|
||||
!!row.billId &&
|
||||
row.claimStatus === 'claimed' &&
|
||||
row.pushStatus !== 'pushed'
|
||||
);
|
||||
},
|
||||
async handlePush(row) {
|
||||
try {
|
||||
await this.$confirm(
|
||||
`确认将流水【${row.receiptNoticeNo || row.detailSerialNo || ''}】推送金蝶生成收款单?`,
|
||||
'推送金蝶',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
this.pushingId = row.id;
|
||||
try {
|
||||
const data = this.unwrapData(await api.pushKingdee(row.id));
|
||||
const billNo = data?.kingdeeBillNo;
|
||||
this.$message.success(billNo ? `推送成功,金蝶收款单号:${billNo}` : '推送成功');
|
||||
await this.loadTable();
|
||||
} finally {
|
||||
this.pushingId = null;
|
||||
}
|
||||
},
|
||||
statusType(row, column) {
|
||||
if (column?.prop === 'pushStatusName') {
|
||||
return { pushed: 'success', failed: 'danger' }[row.pushStatus] || 'info';
|
||||
}
|
||||
return (
|
||||
{ claimed: 'success', partial: 'warning', unclaimed: 'info' }[row.claimStatus] || ''
|
||||
);
|
||||
},
|
||||
claimStatusChange(row) {
|
||||
const label = status =>
|
||||
@@ -453,7 +515,14 @@ export default {
|
||||
}
|
||||
.receipt-flow-page__search :deep(.el-input),
|
||||
.receipt-flow-page__search :deep(.el-select),
|
||||
.receipt-flow-page__search :deep(.el-date-editor) {
|
||||
.receipt-flow-page__search :deep(.el-date-editor),
|
||||
.receipt-flow-page__search :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
.receipt-flow-page__search .amount-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
.receipt-flow-page__search-actions {
|
||||
@@ -487,6 +556,9 @@ export default {
|
||||
.receipt-flow-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
.receipt-flow-page :deep(.el-table .cell .el-link + .el-link) {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.receipt-flow-page :deep(.el-table__row:nth-child(even) > td.el-table__cell) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
@change="fetchBfiExchangeRate"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'project' && editable"
|
||||
@@ -448,78 +449,61 @@
|
||||
<span v-else>{{ formatMoney(row.matchedAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center">
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<div class="formal-editor__links">
|
||||
<el-link type="primary" @click="viewInvoiceAttachment(row)">查看</el-link>
|
||||
<el-link type="primary" @click="downloadInvoiceAttachment(row)">下载</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
:disabled="kingdeeAttachmentBusy"
|
||||
@click="viewInvoiceAttachment(row)"
|
||||
>查看</el-link
|
||||
>
|
||||
<el-link
|
||||
type="primary"
|
||||
:disabled="kingdeeAttachmentBusy"
|
||||
@click="downloadInvoiceAttachment(row)"
|
||||
>下载</el-link
|
||||
>
|
||||
<el-link v-if="editable" type="danger" @click="removeInvoice($index)">删除</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div v-if="issuedInvoices.length" class="formal-editor__issued-invoices">
|
||||
<div class="formal-editor__issued-title">
|
||||
开票发票(本结算单开票申请经金蝶开票回写,文件由金蝶发票云直链提供)
|
||||
</div>
|
||||
<el-table :data="issuedInvoices" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column prop="invoiceNo" label="发票号" min-width="150" align="center" />
|
||||
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center">
|
||||
<template #default="{ row }">{{ displayValue(row.invoiceDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="invoiceAmount" label="价税合计" min-width="130" align="center">
|
||||
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="formal-editor__links">
|
||||
<el-link type="primary" @click="viewIssuedInvoice(row)">查看</el-link>
|
||||
<el-link type="primary" @click="downloadIssuedInvoice(row)">下载</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section-card>
|
||||
|
||||
<el-dialog
|
||||
<kingdee-invoice-pool-dialog
|
||||
v-model="invoiceClaimDialog.visible"
|
||||
title="查询可认领发票"
|
||||
width="92%"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-table
|
||||
ref="invoiceClaimTable"
|
||||
v-loading="invoiceClaimDialog.loading"
|
||||
:data="invoiceClaimPagedRows"
|
||||
row-key="invoiceNo"
|
||||
border
|
||||
height="520"
|
||||
@selection-change="invoiceClaimDialog.selected = $event"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="52"
|
||||
fixed="left"
|
||||
align="center"
|
||||
reserve-selection
|
||||
/>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column prop="invoiceNo" label="发票号" min-width="180" align="center" />
|
||||
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center" />
|
||||
<el-table-column prop="invoiceType" label="发票类型" min-width="150" align="center" />
|
||||
<el-table-column prop="issuerName" label="开票单位" min-width="180" align="center" />
|
||||
<el-table-column prop="receiverName" label="受票单位" min-width="180" align="center" />
|
||||
<el-table-column
|
||||
prop="invoiceAmount"
|
||||
label="发票金额(含税)"
|
||||
min-width="160"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">{{ formatMoney(row.invoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="matchedAmount" label="认领金额" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.matchedAmount) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="formal-editor__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="invoiceClaimDialog.page.current"
|
||||
v-model:page-size="invoiceClaimDialog.page.size"
|
||||
:total="invoiceClaimDialog.page.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="invoiceClaimDialog.visible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="invoiceClaimDialog.confirming"
|
||||
@click="confirmInvoiceClaims"
|
||||
>确认认领</el-button
|
||||
>
|
||||
</template>
|
||||
</el-dialog>
|
||||
confirm-text="确认认领"
|
||||
multiple
|
||||
:initial-invoice-no="String(invoiceClaimNo || '').trim()"
|
||||
:query-api="invoicePoolQueryApi"
|
||||
@confirm="handleInvoiceClaimConfirm"
|
||||
/>
|
||||
|
||||
<section-card
|
||||
v-if="readonly"
|
||||
@@ -995,6 +979,7 @@ import {
|
||||
getNextNo,
|
||||
getReceiptClaims,
|
||||
claimInvoices as claimInvoicesApi,
|
||||
queryInvoicePool,
|
||||
save,
|
||||
} from '@/api/settlement/formalSettlement';
|
||||
import {
|
||||
@@ -1002,6 +987,7 @@ import {
|
||||
getDetailFees as getPreSettlementDetailFees,
|
||||
} from '@/api/settlement/preSettlement';
|
||||
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
|
||||
import { getBfiExchangeRate } from '@/api/base/currency';
|
||||
import {
|
||||
createFormalSettlementForm,
|
||||
formalSettlementFormFields,
|
||||
@@ -1019,12 +1005,18 @@ import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import {
|
||||
downloadKingdeeInvoiceFile,
|
||||
isKingdeePathAttachment,
|
||||
viewKingdeeInvoiceFile,
|
||||
} from '@/utils/kingdee-attachment';
|
||||
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlementEditor',
|
||||
components: { InfoFilled },
|
||||
components: { InfoFilled, KingdeeInvoicePoolDialog },
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
recordId: [String, Number],
|
||||
@@ -1056,15 +1048,16 @@ export default {
|
||||
attachments: [],
|
||||
selectedAttachmentRows: [],
|
||||
invoices: [],
|
||||
// 开票申请经金蝶回写的发票结果(只读展示,含板式文件/数电PDF下载地址)
|
||||
issuedInvoices: [],
|
||||
invoiceClaimNo: '',
|
||||
invoiceClaimLoading: false,
|
||||
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
|
||||
kingdeeAttachmentBusy: false,
|
||||
invoiceClaimDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
confirming: false,
|
||||
rows: [],
|
||||
selected: [],
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
confirming: false,
|
||||
},
|
||||
attachmentUploadFiles: [],
|
||||
attachmentTypeOptions: [],
|
||||
@@ -1221,11 +1214,6 @@ export default {
|
||||
});
|
||||
return Array.from(names);
|
||||
},
|
||||
invoiceClaimPagedRows() {
|
||||
const { current, size } = this.invoiceClaimDialog.page;
|
||||
const start = (current - 1) * size;
|
||||
return this.invoiceClaimDialog.rows.slice(start, start + size);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
@@ -1290,6 +1278,7 @@ export default {
|
||||
this.attachments = this.parseAttachments(data.attachmentsJson);
|
||||
this.selectedAttachmentRows = [];
|
||||
this.invoices = data.invoices || [];
|
||||
this.issuedInvoices = data.issuedInvoices || [];
|
||||
this.sortAttachments();
|
||||
this.contracts = this.filterContracts(this.allContracts, this.form.projectId);
|
||||
},
|
||||
@@ -1319,9 +1308,9 @@ export default {
|
||||
this.attachments = [];
|
||||
this.selectedAttachmentRows = [];
|
||||
this.invoices = [];
|
||||
this.issuedInvoices = [];
|
||||
this.invoiceClaimNo = '';
|
||||
this.invoiceClaimDialog.visible = false;
|
||||
this.invoiceClaimDialog.rows = [];
|
||||
this.invoiceClaimDialog.selected = [];
|
||||
this.attachmentUploadFiles = [];
|
||||
this.billingRuleDialog = {
|
||||
@@ -1345,6 +1334,7 @@ export default {
|
||||
if (this.initialData) await this.applyInitialData();
|
||||
Object.assign(this.form, newRecordAudit);
|
||||
await this.refreshFormalSettlementNo();
|
||||
await this.fetchBfiExchangeRate();
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
@@ -1579,6 +1569,7 @@ export default {
|
||||
(settlementType === 'receivable' ? contract.partyA : contract.partyB),
|
||||
settlementType,
|
||||
settlementTypeName: settlementType === 'receivable' ? '应收' : '应付',
|
||||
currency: contract.settlementCurrency || 'RMB',
|
||||
});
|
||||
this.sources = [];
|
||||
this.details = [];
|
||||
@@ -1586,6 +1577,23 @@ export default {
|
||||
this.form.sourcePreSettlementIds = [];
|
||||
this.form.sourceDetailIds = [];
|
||||
if (refreshSettlementNo) this.refreshFormalSettlementNo();
|
||||
if (this.form.currency !== 'RMB') this.fetchBfiExchangeRate();
|
||||
},
|
||||
async fetchBfiExchangeRate() {
|
||||
const currency = this.form.currency;
|
||||
if (!currency || currency === 'RMB') return;
|
||||
if (!this.form.exchangeRateDate) return;
|
||||
try {
|
||||
const { data } = await getBfiExchangeRate(currency, this.form.exchangeRateDate);
|
||||
const rateData = data?.data;
|
||||
if (rateData?.excval != null) {
|
||||
this.form.exchangeRate = Number(rateData.excval);
|
||||
} else {
|
||||
this.$message.warning(`未查询到 ${currency} 的BFI汇率数据`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||||
}
|
||||
},
|
||||
openCandidateDialog() {
|
||||
this.candidate.visible = true;
|
||||
@@ -2310,79 +2318,19 @@ export default {
|
||||
this.adjust.saving = false;
|
||||
}
|
||||
},
|
||||
claimInvoices() {
|
||||
this.invoiceClaimLoading = true;
|
||||
try {
|
||||
const keyword = String(this.invoiceClaimNo || '').trim();
|
||||
const defaultNo = `MOCK${this.$dayjs().format('YYYYMMDD')}`;
|
||||
const baseNo = (keyword || defaultNo).slice(0, 29);
|
||||
const numbers = [baseNo, `${baseNo}-02`, `${baseNo}-03`];
|
||||
const invoiceAmounts = [1000, 2000, 3000];
|
||||
const availableAmounts = [1000, 1000, 3000];
|
||||
const preferredMatchedAmounts = [1000, 1000, 2000];
|
||||
let remainingAmount = Math.max(
|
||||
Number(this.form.settlementAmount || this.summaryTotal || 0) -
|
||||
this.invoices.reduce((total, item) => total + Number(item.matchedAmount || 0), 0),
|
||||
0
|
||||
);
|
||||
const existingNumbers = new Set(this.invoices.map(item => String(item.invoiceNo)));
|
||||
const mockRows = numbers
|
||||
.map((invoiceNo, index) => {
|
||||
const matchedAmount = Math.min(
|
||||
preferredMatchedAmounts[index],
|
||||
availableAmounts[index],
|
||||
remainingAmount
|
||||
);
|
||||
remainingAmount = Number((remainingAmount - matchedAmount).toFixed(2));
|
||||
return {
|
||||
invoiceNo,
|
||||
invoiceDate: this.$dayjs().subtract(index, 'day').format('YYYY-MM-DD'),
|
||||
invoiceType: index === 1 ? '增值税普通发票' : '增值税专用发票',
|
||||
taxRate: [3, 6, 9][index],
|
||||
invoiceAmount: invoiceAmounts[index],
|
||||
availableInvoiceAmount: availableAmounts[index],
|
||||
matchedAmount,
|
||||
attachmentJson: '',
|
||||
mockData: true,
|
||||
};
|
||||
})
|
||||
.filter(item => !existingNumbers.has(item.invoiceNo));
|
||||
if (!mockRows.length) {
|
||||
this.$message.warning('当前发票号码的Mock数据已认领');
|
||||
return;
|
||||
}
|
||||
this.invoices = [...this.invoices, ...mockRows];
|
||||
this.$message.success(`已生成${mockRows.length}条Mock发票数据`);
|
||||
} finally {
|
||||
this.invoiceClaimLoading = false;
|
||||
}
|
||||
},
|
||||
openInvoiceClaimDialog() {
|
||||
const stamp = Date.now();
|
||||
this.invoiceClaimDialog.loading = true;
|
||||
this.invoiceClaimDialog.rows = Array.from({ length: 80 }, (_, index) => {
|
||||
const amount = Math.floor(1000 + Math.random() * 49000);
|
||||
return {
|
||||
invoiceNo: `MOCK${stamp}${String(index + 1).padStart(3, '0')}`.slice(0, 32),
|
||||
invoiceDate: this.$dayjs()
|
||||
.subtract(Math.floor(Math.random() * 90), 'day')
|
||||
.format('YYYY-MM-DD'),
|
||||
invoiceType: index % 2 ? '增值税普通发票' : '增值税专用发票',
|
||||
issuerName: this.form.payerName || 'Mock开票单位',
|
||||
receiverName: this.form.payeeName || 'Mock受票单位',
|
||||
invoiceAmount: amount,
|
||||
availableInvoiceAmount: amount,
|
||||
matchedAmount: amount,
|
||||
taxRate: [3, 6, 9, 13][index % 4],
|
||||
attachmentJson: '',
|
||||
mockData: true,
|
||||
};
|
||||
});
|
||||
this.invoiceClaimDialog.selected = [];
|
||||
this.invoiceClaimDialog.page.current = 1;
|
||||
this.invoiceClaimDialog.page.total = 80;
|
||||
this.invoiceClaimDialog.visible = true;
|
||||
this.invoiceClaimDialog.loading = false;
|
||||
},
|
||||
invoicePoolQueryApi(params) {
|
||||
return queryInvoicePool(params);
|
||||
},
|
||||
async handleInvoiceClaimConfirm(rows) {
|
||||
// 统一票池行附件字段为复数 attachmentsJson,认领请求契约是单数 attachmentJson,中转处归一
|
||||
this.invoiceClaimDialog.selected = rows.map(row => ({
|
||||
...row,
|
||||
attachmentJson: row.attachmentJson || row.attachmentsJson,
|
||||
}));
|
||||
await this.confirmInvoiceClaims();
|
||||
},
|
||||
async confirmInvoiceClaims() {
|
||||
if (!this.invoiceClaimDialog.selected.length) {
|
||||
@@ -2408,7 +2356,11 @@ export default {
|
||||
if (Number(this.form.settlementAmount || 0) > 0) {
|
||||
remaining = Math.max(0, remaining - matchedAmount);
|
||||
}
|
||||
return { ...row, matchedAmount };
|
||||
return {
|
||||
...row,
|
||||
invoiceType: row.invoiceTypeName || row.invoiceType || '',
|
||||
matchedAmount,
|
||||
};
|
||||
})
|
||||
.filter(row => row.matchedAmount > 0);
|
||||
if (!invoices.length) {
|
||||
@@ -2494,16 +2446,64 @@ export default {
|
||||
const attachment = this.invoiceAttachment(row);
|
||||
return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`;
|
||||
},
|
||||
viewInvoiceAttachment(row) {
|
||||
async viewInvoiceAttachment(row) {
|
||||
const attachment = this.invoiceAttachment(row);
|
||||
// 金蝶侧附件(path 型)经后端代理 A801-2 预览,OSS 直链维持 window.open
|
||||
if (isKingdeePathAttachment(attachment)) {
|
||||
if (this.kingdeeAttachmentBusy) return;
|
||||
this.kingdeeAttachmentBusy = true;
|
||||
try {
|
||||
await viewKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
|
||||
} finally {
|
||||
this.kingdeeAttachmentBusy = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const url = this.invoiceAttachmentUrl(row);
|
||||
if (!url) return this.$message.warning('当前Mock发票暂无可查看附件');
|
||||
if (!url) return this.$message.warning('当前发票暂无可查看附件');
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
downloadInvoiceAttachment(row) {
|
||||
async downloadInvoiceAttachment(row) {
|
||||
const attachment = this.invoiceAttachment(row);
|
||||
// 金蝶侧附件(path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
|
||||
if (isKingdeePathAttachment(attachment)) {
|
||||
if (this.kingdeeAttachmentBusy) return;
|
||||
this.kingdeeAttachmentBusy = true;
|
||||
try {
|
||||
await downloadKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row));
|
||||
} finally {
|
||||
this.kingdeeAttachmentBusy = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const url = this.invoiceAttachmentUrl(row);
|
||||
if (!url) return this.$message.warning('当前Mock发票暂无可下载附件');
|
||||
if (!url) return this.$message.warning('当前发票暂无可下载附件');
|
||||
downloadFileByUrl(url, this.invoiceAttachmentName(row));
|
||||
},
|
||||
// 开票发票预览地址:数电PDF优先(OFD浏览器无法预览),退预览图、板式文件
|
||||
issuedInvoicePreviewUrl(row) {
|
||||
return row.invoicePdfFileUrl || row.invoiceImageUrl || row.invoiceFileUrl || '';
|
||||
},
|
||||
// 开票发票下载地址:数电PDF优先,退板式文件、数电XML
|
||||
issuedInvoiceDownloadUrl(row) {
|
||||
return row.invoicePdfFileUrl || row.invoiceFileUrl || row.invoiceXmlFileUrl || '';
|
||||
},
|
||||
// 下载文件名按直链扩展名推断(数电票 .pdf / 板式文件 .ofd|.pdf),缺省 .pdf
|
||||
issuedInvoiceFileName(row, url) {
|
||||
const match = String(url || '').split('?')[0].match(/\.([a-zA-Z0-9]+)$/);
|
||||
const extension = match ? `.${match[1].toLowerCase()}` : '.pdf';
|
||||
return `${row.invoiceNo || '发票'}${extension}`;
|
||||
},
|
||||
viewIssuedInvoice(row) {
|
||||
const url = this.issuedInvoicePreviewUrl(row);
|
||||
if (!url) return this.$message.warning('当前发票暂无可查看文件,可稍后重试开票结果同步');
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
downloadIssuedInvoice(row) {
|
||||
const url = this.issuedInvoiceDownloadUrl(row);
|
||||
if (!url) return this.$message.warning('当前发票暂无可下载文件,可稍后重试开票结果同步');
|
||||
downloadFileByUrl(url, this.issuedInvoiceFileName(row, url));
|
||||
},
|
||||
buildSavePayload() {
|
||||
return {
|
||||
id: this.form.id,
|
||||
@@ -2993,6 +2993,14 @@ export default {
|
||||
.formal-editor__invoice-claim :deep(.el-input) {
|
||||
width: 360px;
|
||||
}
|
||||
.formal-editor__issued-invoices {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.formal-editor__issued-title {
|
||||
margin-bottom: 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.formal-editor__negative-amount {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="selection.length !== 1"
|
||||
:loading="syncLoading"
|
||||
@click="$emit('sync')"
|
||||
>同步金蝶</el-button
|
||||
>
|
||||
@@ -91,6 +92,12 @@
|
||||
<span v-else-if="column.prop === 'kingdeeSyncStatus'">{{
|
||||
kingdeeSyncName(row.kingdeeSyncStatus)
|
||||
}}</span>
|
||||
<template v-else-if="column.prop === 'kingdeeLockStatus'">
|
||||
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
|
||||
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -137,6 +144,7 @@ export default {
|
||||
rows: { type: Array, default: () => [] },
|
||||
columns: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
syncLoading: { type: Boolean, default: false },
|
||||
page: { type: Object, required: true },
|
||||
hasPermission: { type: Function, required: true },
|
||||
mkEnabled: { type: Boolean, default: false },
|
||||
@@ -267,6 +275,28 @@ export default {
|
||||
kingdeeSyncName(value) {
|
||||
return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-';
|
||||
},
|
||||
lockTag(row) {
|
||||
if (row.kingdeeLockStatus === 'locked') {
|
||||
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
|
||||
}
|
||||
if (row.kingdeeLockStatus === 'failed') {
|
||||
if ((row.kingdeeTryCount || 0) >= 5) {
|
||||
return {
|
||||
show: true,
|
||||
type: 'danger',
|
||||
label: '锁定失败·已达上限',
|
||||
tip: row.kingdeeLockFailReason || '自动补偿次数已用完,请重推「同步金蝶」立即补锁',
|
||||
};
|
||||
}
|
||||
return {
|
||||
show: true,
|
||||
type: 'warning',
|
||||
label: '锁定失败·补偿中',
|
||||
tip: row.kingdeeLockFailReason || '系统将自动补偿重锁',
|
||||
};
|
||||
}
|
||||
return { show: false };
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
:disabled="!editable || form.currency === 'RMB'"
|
||||
placeholder="请选择"
|
||||
@change="recalculateLocalAmount"
|
||||
@change="handleExchangeRateDateChange"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="field.type === 'number'"
|
||||
@@ -952,6 +952,7 @@ import {
|
||||
submit,
|
||||
} from '@/api/settlement/preSettlement';
|
||||
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
|
||||
import { getBfiExchangeRate } from '@/api/base/currency';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import {
|
||||
emptyPreSettlementForm,
|
||||
@@ -1289,7 +1290,11 @@ export default {
|
||||
await this.loadAttachmentTypeOptions();
|
||||
}
|
||||
if (this.recordId) await this.loadDetail();
|
||||
else if (this.initialData) await this.applyInitialData();
|
||||
else if (this.initialData) {
|
||||
await this.applyInitialData();
|
||||
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
|
||||
await this.fetchBfiExchangeRate();
|
||||
}
|
||||
},
|
||||
async applyInitialData() {
|
||||
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
|
||||
@@ -1586,7 +1591,12 @@ export default {
|
||||
this.form.settlementType = contract.settlementType || 'payable';
|
||||
this.form.payerName = contract.partyA;
|
||||
this.form.payeeName = contract.partyB;
|
||||
this.form.currency = contract.settlementCurrency || 'RMB';
|
||||
this.summaryFees = [];
|
||||
if (this.form.currency !== 'RMB') {
|
||||
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
|
||||
this.fetchBfiExchangeRate();
|
||||
}
|
||||
},
|
||||
async saveDraft(shouldSubmit) {
|
||||
await this.$refs.formRef?.validate();
|
||||
@@ -1861,6 +1871,26 @@ export default {
|
||||
.toFixed(2)
|
||||
);
|
||||
},
|
||||
handleExchangeRateDateChange() {
|
||||
this.fetchBfiExchangeRate();
|
||||
this.recalculateLocalAmount();
|
||||
},
|
||||
async fetchBfiExchangeRate() {
|
||||
if (!this.form.currency || this.form.currency === 'RMB') return;
|
||||
if (!this.form.exchangeRateDate) return;
|
||||
try {
|
||||
const { data } = await getBfiExchangeRate(this.form.currency, this.form.exchangeRateDate);
|
||||
const rateData = data?.data;
|
||||
if (rateData?.excval != null) {
|
||||
this.form.exchangeRate = Number(rateData.excval);
|
||||
this.recalculateLocalAmount();
|
||||
} else {
|
||||
this.$message.warning(`未查询到 ${this.form.currency} 的BFI汇率数据`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||||
}
|
||||
},
|
||||
recalculateLocalAmount() {
|
||||
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
|
||||
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
:has-permission="hasPermissionFn"
|
||||
:sync-loading="syncLoading"
|
||||
:mk-enabled="mkEnabled"
|
||||
v-model:search-visible="searchVisible"
|
||||
@create="openCreate"
|
||||
@@ -84,6 +85,7 @@
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
:has-permission="hasPermissionFn"
|
||||
:sync-loading="syncLoading"
|
||||
:mk-enabled="mkEnabled"
|
||||
v-model:search-visible="searchVisible"
|
||||
@create="openCreate"
|
||||
@@ -221,6 +223,7 @@ export default {
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
syncLoading: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
editor: { visible: false, id: null, readonly: false },
|
||||
paymentDialog: {
|
||||
@@ -393,8 +396,13 @@ export default {
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
await api.approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
const res = await api.approve({ id: row.id });
|
||||
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||
if (res.data.data === false) {
|
||||
this.$message.warning(res.data.msg);
|
||||
} else {
|
||||
this.$message.success(res.data.msg || '审批通过');
|
||||
}
|
||||
this.loadTable();
|
||||
},
|
||||
async handleReturn(row) {
|
||||
@@ -421,12 +429,28 @@ export default {
|
||||
return this.selection[0];
|
||||
},
|
||||
async handleSync() {
|
||||
if (this.syncLoading) return;
|
||||
const row = this.selectedOne('同步金蝶');
|
||||
if (!row) return;
|
||||
const response = await api.syncKingdee(row.id);
|
||||
const data = this.unwrapData(response);
|
||||
this.$message.success(`同步成功,金蝶单据号:${data}`);
|
||||
this.loadTable();
|
||||
|
||||
this.syncLoading = true;
|
||||
try {
|
||||
const response = await api.syncKingdee(row.id);
|
||||
const data = this.unwrapData(response);
|
||||
this.$message.success(`同步成功,金蝶单据号:${data}`);
|
||||
await this.loadTable();
|
||||
// 附件联带同步提示:同步成功但附件未全部上传时追加 warning,重推即自动补传
|
||||
const freshRow = (this.rows || []).find(item => String(item.id) === String(row.id));
|
||||
if (freshRow && ['pending', 'partial'].includes(freshRow.attachmentSyncStatus)) {
|
||||
this.$message({
|
||||
type: 'warning',
|
||||
message: '该单附件未全部上传,可重推补传',
|
||||
duration: 8000,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
this.syncLoading = false;
|
||||
}
|
||||
},
|
||||
async openPaymentDialog() {
|
||||
if (!this.selection.length) return this.$message.warning('请至少选择一条正式结算单');
|
||||
|
||||
@@ -109,6 +109,12 @@
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #kingdeeStatus="{ row }">
|
||||
<el-tag v-if="row.kingdeeStatus === 'synced'" type="success">已推送</el-tag>
|
||||
<el-tag v-else-if="row.kingdeeStatus === 'exist'" type="primary">金蝶已有</el-tag>
|
||||
<el-tag v-else-if="row.kingdeeStatus === 'failed'" type="danger">推送失败</el-tag>
|
||||
<el-tag v-else type="info">未推送</el-tag>
|
||||
</template>
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
@@ -196,6 +202,18 @@
|
||||
>
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
:disabled="kingdeeSyncingId === row.id"
|
||||
v-if="
|
||||
hasPermission('customer_archive_sync') &&
|
||||
row.approvalStatus === 'approved' &&
|
||||
row.kingdeeStatus === 'failed'
|
||||
"
|
||||
@click="handleSyncKingdee(row)"
|
||||
>
|
||||
{{ kingdeeSyncingId === row.id ? '推送中…' : '同步金蝶' }}
|
||||
</el-link>
|
||||
<el-link
|
||||
type="danger"
|
||||
v-if="hasPermission('customer_archive_delete') && row.approvalStatus === 'draft'"
|
||||
@@ -297,6 +315,7 @@
|
||||
placeholder="请输入"
|
||||
maxlength="18"
|
||||
show-word-limit
|
||||
:disabled="kingdeeLocked && Boolean(archiveForm.id)"
|
||||
@input="
|
||||
archiveForm.unifiedCreditCode = String(
|
||||
archiveForm.unifiedCreditCode || ''
|
||||
@@ -312,6 +331,7 @@
|
||||
placeholder="请输入"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
:disabled="kingdeeLocked"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -335,7 +355,12 @@
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="企业法人" prop="legalPerson">
|
||||
<el-input v-model="archiveForm.legalPerson" maxlength="10" show-word-limit />
|
||||
<el-input
|
||||
v-model="archiveForm.legalPerson"
|
||||
maxlength="10"
|
||||
show-word-limit
|
||||
:disabled="kingdeeLocked"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
@@ -476,6 +501,7 @@
|
||||
<el-input
|
||||
v-model="archiveForm.registeredCapital"
|
||||
maxlength="18"
|
||||
:disabled="kingdeeLocked"
|
||||
@input="value => handleDecimalInput('registeredCapital', value)"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -1528,6 +1554,8 @@ import {
|
||||
remove,
|
||||
getScoreTemplate,
|
||||
recognizeBusinessLicenseOcr,
|
||||
queryKingdeeCustomer,
|
||||
syncKingdee,
|
||||
} from '@/api/vehicle/customer-archive';
|
||||
import {
|
||||
getList as getCreditScoreQuantificationList,
|
||||
@@ -1617,23 +1645,13 @@ export default {
|
||||
const code = String(value || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
const charset = '0123456789ABCDEFGHJKLMNPQRTUWXY';
|
||||
const weights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
|
||||
if (!code) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (code.length !== 18 || ![...code].every(char => charset.includes(char))) {
|
||||
callback(new Error('统一社会信用代码必须为18位有效字符'));
|
||||
return;
|
||||
}
|
||||
const sum = [...code.slice(0, 17)].reduce(
|
||||
(total, char, index) => total + charset.indexOf(char) * weights[index],
|
||||
0
|
||||
);
|
||||
const checkCode = charset[(31 - (sum % 31)) % 31];
|
||||
if (code[17] !== checkCode) {
|
||||
callback(new Error('统一社会信用代码校验码错误'));
|
||||
const error = this.unifiedCreditCodeError(code);
|
||||
if (error) {
|
||||
callback(new Error(error));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
@@ -1664,6 +1682,9 @@ export default {
|
||||
originalAccessType: '',
|
||||
activeTab: 'scores',
|
||||
archiveForm: this.emptyArchive(),
|
||||
kingdeeBlocked: false,
|
||||
kingdeeQueryTimer: null,
|
||||
kingdeeSyncingId: null,
|
||||
currentScore: { details: [] },
|
||||
scoreRecordIndex: -1,
|
||||
scoreAttachmentFiles: [],
|
||||
@@ -2006,6 +2027,12 @@ export default {
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
minWidth: 170,
|
||||
},
|
||||
{
|
||||
label: '金蝶同步状态',
|
||||
prop: 'kingdeeStatus',
|
||||
slot: true,
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
@@ -2090,6 +2117,9 @@ export default {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
kingdeeLocked() {
|
||||
return this.archiveForm.kingdeeStatus === 'exist';
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: false,
|
||||
@@ -2295,6 +2325,9 @@ export default {
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
},
|
||||
'archiveForm.unifiedCreditCode'(val) {
|
||||
this.handleKingdeeCreditCodeChange(val);
|
||||
},
|
||||
'archiveForm.registeredDetailAddress'() {
|
||||
this.$nextTick(() => this.checkOverflow('registeredDetailInput', 'registeredDetailOverflow'));
|
||||
},
|
||||
@@ -2348,6 +2381,119 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
unifiedCreditCodeError(value) {
|
||||
const code = String(value || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
const charset = '0123456789ABCDEFGHJKLMNPQRTUWXY';
|
||||
const weights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
|
||||
if (code.length !== 18 || ![...code].every(char => charset.includes(char))) {
|
||||
return '统一社会信用代码必须为18位有效字符';
|
||||
}
|
||||
const sum = [...code.slice(0, 17)].reduce(
|
||||
(total, char, index) => total + charset.indexOf(char) * weights[index],
|
||||
0
|
||||
);
|
||||
const checkCode = charset[(31 - (sum % 31)) % 31];
|
||||
if (code[17] !== checkCode) {
|
||||
return '统一社会信用代码校验码错误';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
resetKingdeeQueryState() {
|
||||
this.kingdeeBlocked = false;
|
||||
clearTimeout(this.kingdeeQueryTimer);
|
||||
this.kingdeeQueryTimer = null;
|
||||
},
|
||||
// 新增态下重新输入统一社会信用代码后,此前金蝶回填的数据即失效,重置回填状态并允许对新代码重新查询
|
||||
resetKingdeeBackfill() {
|
||||
const form = this.archiveForm;
|
||||
form.kingdeeStatus = 'none';
|
||||
form.fullName = '';
|
||||
form.legalPerson = '';
|
||||
form.registeredCapital = '';
|
||||
form.registeredDetailAddress = '';
|
||||
form.businessScope = [];
|
||||
form.businessTermType = '固定期限';
|
||||
form.businessEndDate = '';
|
||||
},
|
||||
// 新增态下统一社会信用代码录入完成后自动查询金蝶(A902-1),防抖 500ms;
|
||||
// 代码一经变化即重新查询(含改回历史代码),不做同码去重
|
||||
handleKingdeeCreditCodeChange(val) {
|
||||
this.kingdeeBlocked = false;
|
||||
if (this.readonly || this.archiveForm.id) return;
|
||||
if (this.archiveForm.kingdeeStatus === 'exist') {
|
||||
this.resetKingdeeBackfill();
|
||||
}
|
||||
const code = String(val || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
if (code.length !== 18 || this.unifiedCreditCodeError(code)) return;
|
||||
clearTimeout(this.kingdeeQueryTimer);
|
||||
this.kingdeeQueryTimer = setTimeout(() => this.fetchKingdeeCustomer(code), 500);
|
||||
},
|
||||
async fetchKingdeeCustomer(code) {
|
||||
let customer = null;
|
||||
try {
|
||||
const res = await queryKingdeeCustomer(code);
|
||||
customer = res?.data?.data || null;
|
||||
} catch (error) {
|
||||
this.$message.warning('金蝶查询失败,可继续填写,审批通过后系统将自动复查');
|
||||
return;
|
||||
}
|
||||
// 防止响应返回时用户已修改代码
|
||||
if (String(this.archiveForm.unifiedCreditCode || '').trim().toUpperCase() !== code) return;
|
||||
// 金蝶不存在(rows 为空,data 为 null 或空对象):静默返回,正常填写,不做提示
|
||||
if (!customer || !customer.status) return;
|
||||
if (customer.status === 'C') {
|
||||
this.applyKingdeeCustomer(customer);
|
||||
this.$alert(
|
||||
'金蝶已创建该客户,将获取金蝶的工商信息,相关字段已自动回填并锁定。如信用代码有误,可直接修改,系统将重新查询。',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.kingdeeBlocked = true;
|
||||
this.$alert('金蝶审核中,请稍后重新添加。', '提示', { confirmButtonText: '确定' });
|
||||
}
|
||||
},
|
||||
applyKingdeeCustomer(customer) {
|
||||
const form = this.archiveForm;
|
||||
form.kingdeeStatus = 'exist';
|
||||
// 工商信息以金蝶为准,直接覆盖(后续字段锁定)
|
||||
if (customer.name) form.fullName = customer.name;
|
||||
if (customer.artificialperson) form.legalPerson = customer.artificialperson;
|
||||
if (customer.bizpartnerAddress) form.registeredDetailAddress = customer.bizpartnerAddress;
|
||||
if (customer.regcapital) {
|
||||
const capital = String(customer.regcapital).replace(/,/g, '').match(/\d+(?:\.\d+)?/);
|
||||
if (capital) form.registeredCapital = capital[0];
|
||||
}
|
||||
if (customer.businessterm) this.applyBusinessTermRecognition(customer.businessterm);
|
||||
if (customer.businessscope) {
|
||||
const matchedScopes = this.businessScopeOptions
|
||||
.filter(
|
||||
item =>
|
||||
customer.businessscope.includes(item.label) ||
|
||||
customer.businessscope.includes(item.value)
|
||||
)
|
||||
.map(item => item.value);
|
||||
if (matchedScopes.length) form.businessScope = matchedScopes;
|
||||
}
|
||||
if (!form.shortName && customer.simplename) form.shortName = customer.simplename;
|
||||
this.$nextTick(() => {
|
||||
[
|
||||
'unifiedCreditCode',
|
||||
'fullName',
|
||||
'legalPerson',
|
||||
'registeredCapital',
|
||||
'businessEndDate',
|
||||
'businessScope',
|
||||
'registeredDetailAddress',
|
||||
].forEach(prop => this.$refs.archiveForm?.validateField(prop));
|
||||
});
|
||||
},
|
||||
// 检测输入框文本是否溢出,仅溢出时才允许 tooltip 显示完整地址
|
||||
checkOverflow(refName, flag) {
|
||||
const inst = this.$refs[refName];
|
||||
@@ -2377,6 +2523,9 @@ export default {
|
||||
invoices: [],
|
||||
scores: [],
|
||||
changeRecords: [],
|
||||
kingdeeStatus: 'none',
|
||||
kingdeeFailReason: '',
|
||||
kingdeeSyncTime: null,
|
||||
};
|
||||
},
|
||||
getDetailList(type) {
|
||||
@@ -4134,6 +4283,7 @@ export default {
|
||||
}
|
||||
this.archiveForm = this.emptyArchive();
|
||||
this.originalAccessType = '';
|
||||
this.resetKingdeeQueryState();
|
||||
this.qualificationFiles = [];
|
||||
this.qualificationUploadFiles = [];
|
||||
this.ocrQualificationUploadFiles = [];
|
||||
@@ -4143,6 +4293,7 @@ export default {
|
||||
},
|
||||
resetArchive() {
|
||||
this.readonly = false;
|
||||
this.resetKingdeeQueryState();
|
||||
this.changeRecordDetailVisible = false;
|
||||
this.changeRecordDetail = null;
|
||||
this.changeRecordDetailRows = [];
|
||||
@@ -4266,6 +4417,10 @@ export default {
|
||||
return archive;
|
||||
},
|
||||
saveArchive() {
|
||||
if (this.kingdeeBlocked) {
|
||||
this.$message.warning('金蝶审核中,请稍后重新添加');
|
||||
return;
|
||||
}
|
||||
this.$refs.archiveForm.validate(valid => {
|
||||
if (!valid) {
|
||||
this.$message.warning('请先完整填写必填项');
|
||||
@@ -4279,6 +4434,10 @@ export default {
|
||||
});
|
||||
},
|
||||
saveAndSubmitArchive() {
|
||||
if (this.kingdeeBlocked) {
|
||||
this.$message.warning('金蝶审核中,请稍后重新添加');
|
||||
return;
|
||||
}
|
||||
this.$refs.archiveForm.validate(valid => {
|
||||
if (!valid) {
|
||||
this.$message.warning('请先完整填写必填项');
|
||||
@@ -5047,6 +5206,33 @@ export default {
|
||||
this.$message({ type: 'success', message: '审核通过!' });
|
||||
});
|
||||
},
|
||||
async handleSyncKingdee(row) {
|
||||
if (this.kingdeeSyncingId) return;
|
||||
try {
|
||||
await this.$confirm('是否确认推送金蝶?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
} catch (exception) {
|
||||
return;
|
||||
}
|
||||
this.kingdeeSyncingId = row.id;
|
||||
try {
|
||||
const res = await syncKingdee(row.id);
|
||||
const archive = res?.data?.data || {};
|
||||
if (archive.kingdeeStatus === 'synced') {
|
||||
this.$message.success('推送成功,金蝶客户编码与客商编号一致');
|
||||
} else if (archive.kingdeeStatus === 'exist') {
|
||||
this.$message.info('金蝶已有该客商档案,无需推送');
|
||||
} else {
|
||||
this.$message.warning(`推送失败:${archive.kingdeeFailReason || '未知原因'}`);
|
||||
}
|
||||
this.onLoad(this.page);
|
||||
} finally {
|
||||
this.kingdeeSyncingId = null;
|
||||
}
|
||||
},
|
||||
handleReject(row) {
|
||||
this.$confirm('是否确认审核不通过?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
|
||||
+24
-22
@@ -18,6 +18,8 @@ export default ({ mode, command }) => {
|
||||
target: 'esnext',
|
||||
minify: isProd ? 'terser' : 'esbuild', // 根据环境选择压缩工具
|
||||
rollupOptions: {
|
||||
// @napi-rs/canvas 是 Node 原生模块(@open-file-viewer 传递依赖),浏览器 bundle 打不了,跳过
|
||||
external: [/^@napi-rs\//],
|
||||
output: {
|
||||
assetFileNames,
|
||||
},
|
||||
@@ -47,28 +49,28 @@ export default ({ mode, command }) => {
|
||||
__VUE_I18N_LEGACY_API__: true,
|
||||
__INTLIFY_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
// server: {
|
||||
// port: 2888,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// // 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
|
||||
// // 注意:本机 8080 一般是 Nacos,不是业务网关
|
||||
// target: 'http://localhost',
|
||||
// // target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
|
||||
// changeOrigin: true,
|
||||
// rewrite: path => path.replace(/^\/api/, ''),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
server: {
|
||||
port: 2889,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://172.16.203.221:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 2888,
|
||||
proxy: {
|
||||
'/api': {
|
||||
// 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
|
||||
// 注意:本机 8080 一般是 Nacos,不是业务网关
|
||||
target: 'http://localhost',
|
||||
// target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
//server: {
|
||||
// port: 2889,
|
||||
// proxy: {
|
||||
// '/api': {
|
||||
// target: 'http://172.16.203.221:8000',
|
||||
// changeOrigin: true,
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': resolve(__dirname, './'),
|
||||
|
||||
Reference in New Issue
Block a user