Compare commits

...
17 changed files with 823 additions and 208 deletions
+9
View File
@@ -34,6 +34,15 @@ export const syncKingdeeBatch = ids =>
// 回写任务按 paymentNo 分批远程查询 A103-2/A103-4,同样可能超过全局超时 // 回写任务按 paymentNo 分批远程查询 A103-2/A103-4,同样可能超过全局超时
export const syncKingdeeResult = () => export const syncKingdeeResult = () =>
request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: APPROVE_TIMEOUT }); request({ url: `${baseUrl}/sync-kingdee-result`, method: 'post', timeout: APPROVE_TIMEOUT });
// 付款记录付款凭证下载(后端代理 A801-2 流式回源),耗时较普通接口长,放宽到 120s
export const downloadRecordVoucher = recordId =>
request({
url: `${baseUrl}/record/download-voucher`,
method: 'get',
params: { recordId },
responseType: 'blob',
timeout: 120000,
});
export const paymentTypeOptions = [ export const paymentTypeOptions = [
{ label: '项目预付', value: 'project_advance' }, { label: '项目预付', value: 'project_advance' },
@@ -0,0 +1,145 @@
<template>
<div class="invoice-write-off-table">
<el-table v-loading="loading" :data="rows" border>
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column prop="arrivalAddress" label="到货地址" min-width="150" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.manualFlag === 1 ? '—' : displayValue(row.arrivalAddress) }}</template>
</el-table-column>
<el-table-column prop="actualDepartureTime" label="实际发货时间" width="150" align="center">
<template #default="{ row }">{{ row.manualFlag === 1 ? '—' : formatDateTime(row.actualDepartureTime) }}</template>
</el-table-column>
<el-table-column prop="completionTime" label="实际完成时间" width="150" align="center">
<template #default="{ row }">{{ row.manualFlag === 1 ? '—' : formatDateTime(row.completionTime) }}</template>
</el-table-column>
<el-table-column prop="transportType" label="运输类型" width="100" align="center">
<template #default="{ row }">{{ row.manualFlag === 1 ? '—' : displayValue(row.transportType) }}</template>
</el-table-column>
<el-table-column prop="cargoName" label="货物名称" min-width="110" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.manualFlag === 1 ? '—' : displayValue(row.cargoName) }}</template>
</el-table-column>
<el-table-column prop="cargoType" label="货物类型" min-width="110" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.manualFlag === 1 ? '—' : displayValue(row.cargoType) }}</template>
</el-table-column>
<el-table-column prop="transportQuantity" label="运输总量" width="110" align="center">
<template #default="{ row }">{{ row.manualFlag === 1 ? '—' : displayValue(row.transportQuantity) }}</template>
</el-table-column>
<el-table-column prop="mileage" label="里程 (KM)" width="100" align="center">
<template #default="{ row }">{{ row.manualFlag === 1 ? '—' : displayValue(row.mileage) }}</template>
</el-table-column>
<el-table-column prop="batchNo" label="批次号" width="100" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.manualFlag === 1 ? '—' : displayValue(row.batchNo) }}</template>
</el-table-column>
<el-table-column prop="invoiceNo" label="发票号" min-width="160" align="center" show-overflow-tooltip />
<!-- 按费用项目动态分组列头,每组三列:结算金额/未开票费用/本次开票金额 -->
<el-table-column v-for="group in feeItemGroups" :key="group.name" :label="group.name" align="center">
<el-table-column :label="'结算金额'" width="110" align="center">
<template #default="{ row }">{{ formatMoney(row.settlementAmounts[group.name]) }}</template>
</el-table-column>
<el-table-column :label="'未开票费用'" width="110" align="center">
<template #default="{ row }">{{ formatMoney(row.beforeRemainAmounts[group.name]) }}</template>
</el-table-column>
<el-table-column :label="'本次开票金额'" width="120" align="center">
<template #default="{ row }">{{ formatMoney(row.writeOffAmounts[group.name]) }}</template>
</el-table-column>
</el-table-column>
</el-table>
<div v-if="!loading && !rows.length" class="invoice-write-off-table__empty">暂无冲销明细</div>
</div>
</template>
<script>
import request from '@/axios';
/**
* 发票冲销明细公共表格(开票申请/收票登记/付款申请通用)
* 数据源:/blade-transport/invoice-write-off/list?bizType=&bizId=
* 行按运单完成时间升序(先完成的运单先冲销),手动添加费用无运输信息显示"—"
*/
export default {
name: 'InvoiceWriteOffTable',
props: {
// invoice_apply-开票申请 receipt-收票登记 payment_receipt-付款申请生成的收票单
bizType: { type: String, required: true },
bizId: { type: [String, Number], default: null },
},
data() {
return {
loading: false,
rows: [],
};
},
computed: {
feeItemGroups() {
const names = [];
this.rows.forEach(row => {
if (row.feeItemName && !names.includes(row.feeItemName)) names.push(row.feeItemName);
});
return names.map(name => ({ name }));
},
},
watch: {
bizId: {
immediate: true,
handler(value) {
if (value) this.load();
},
},
},
methods: {
load() {
this.loading = true;
request({
url: '/blade-transport/invoice-write-off/list',
method: 'get',
params: { bizType: this.bizType, bizId: this.bizId },
})
.then(res => {
const list = res.data.data || [];
// 同一运单同一费用项目可能被多张发票分段冲抵(跨发票拆分),按运单+费用项目归并展示
const merged = new Map();
list.forEach(row => {
const key = `${row.waybillId || row.summaryFeeId || ''}|${row.feeItemName}`;
if (!merged.has(key)) {
merged.set(key, {
...row,
settlementAmounts: { [row.feeItemName]: row.settlementAmount },
beforeRemainAmounts: { [row.feeItemName]: row.beforeRemainAmount },
writeOffAmounts: { [row.feeItemName]: row.writeOffAmount },
});
return;
}
const target = merged.get(key);
target.writeOffAmounts[row.feeItemName] = Number(target.writeOffAmounts[row.feeItemName] || 0) + Number(row.writeOffAmount || 0);
// 未开票费用取首段冲抵前余额,发票号追加展示
if (!String(target.invoiceNo).includes(row.invoiceNo)) {
target.invoiceNo = `${target.invoiceNo},${row.invoiceNo}`;
}
});
this.rows = [...merged.values()];
})
.finally(() => {
this.loading = false;
});
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatDateTime(value) {
if (!value) return '-';
return this.$dayjs(value).format('YYYY-MM-DD HH:mm:ss');
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
},
};
</script>
<style scoped>
.invoice-write-off-table__empty {
padding: 24px 0;
text-align: center;
color: #909399;
font-size: 13px;
}
</style>
@@ -1,8 +1,8 @@
<template> <template>
<el-dialog v-model="visible" :title="title" width="92%" append-to-body destroy-on-close @open="handleOpen"> <el-dialog v-model="visible" :title="title" width="92%" append-to-body align-center destroy-on-close @open="handleOpen">
<el-form :model="query" inline label-width="130px" @submit.prevent> <el-form class="kingdee-pool-dialog__form" :model="query" inline label-position="right" label-width="120px" @submit.prevent>
<el-form-item label="发票号码"> <el-form-item label="发票号码">
<el-input v-model="query.invoiceNo" clearable @keyup.enter="handleQuery" /> <el-input v-model="query.invoiceNo" clearable placeholder="请输入" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="开票日期"> <el-form-item label="开票日期">
<el-date-picker <el-date-picker
@@ -10,15 +10,16 @@
type="daterange" type="daterange"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
format="YYYY-MM-DD" format="YYYY-MM-DD"
start-placeholder="开始日期" range-separator="-"
end-placeholder="结束日期" start-placeholder="请选择"
end-placeholder="请选择"
/> />
</el-form-item> </el-form-item>
<el-form-item label="开票单位"> <el-form-item label="开票单位">
<el-input v-model="query.salerName" clearable @keyup.enter="handleQuery" /> <el-input v-model="query.salerName" clearable placeholder="请输入" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="受票单位"> <el-form-item label="受票单位">
<el-input v-model="query.buyerName" clearable @keyup.enter="handleQuery" /> <el-input v-model="query.buyerName" clearable placeholder="请输入" @keyup.enter="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="开票单位(销方)"> <el-form-item label="开票单位(销方)">
<el-select <el-select
@@ -93,6 +94,9 @@
<el-form-item> <el-form-item>
<el-button :disabled="loading" @click="resetQuery">重置</el-button> <el-button :disabled="loading" @click="resetQuery">重置</el-button>
<el-button type="primary" :disabled="loading" @click="handleQuery">查询</el-button> <el-button type="primary" :disabled="loading" @click="handleQuery">查询</el-button>
<el-tooltip content="按名称检索范围为已同步票,未找到请改用发票号码或税号精确查找" placement="top">
<el-icon class="kingdee-pool-dialog__query-tip"><QuestionFilled /></el-icon>
</el-tooltip>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table <el-table
@@ -101,7 +105,7 @@
:data="rows" :data="rows"
:row-key="rowKey" :row-key="rowKey"
border border
height="520" max-height="50vh"
:highlight-current-row="!multiple" :highlight-current-row="!multiple"
@selection-change="selection => (selected = selection)" @selection-change="selection => (selected = selection)"
@row-dblclick="handleRowDblclick" @row-dblclick="handleRowDblclick"
@@ -118,6 +122,7 @@
</el-radio> </el-radio>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column type="index" label="序号" width="70" align="center" :index="indexMethod" />
<el-table-column prop="invoiceNo" label="发票号码" min-width="180" /> <el-table-column prop="invoiceNo" label="发票号码" min-width="180" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" /> <el-table-column prop="invoiceDate" label="开票日期" min-width="120" />
<el-table-column prop="invoiceType" label="发票类型" min-width="140" /> <el-table-column prop="invoiceType" label="发票类型" min-width="140" />
@@ -172,8 +177,8 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="kingdee-pool-dialog__pager"> <div class="kingdee-pool-dialog__pagination">
<span class="kingdee-pool-dialog__pager-tip">按名称检索范围为已同步票,未找到请改用发票号码或税号精确查找</span> <span class="kingdee-pool-dialog__pagination-tip">按名称检索范围为已同步票,未找到请改用发票号码或税号精确查找</span>
<el-pagination <el-pagination
v-model:current-page="page.current" v-model:current-page="page.current"
v-model:page-size="page.size" v-model:page-size="page.size"
@@ -323,6 +328,9 @@ export default {
this.page.current = 1; this.page.current = 1;
this.load(); this.load();
}, },
indexMethod(index) {
return (this.page.current - 1) * this.page.size + index + 1;
},
resetQuery() { resetQuery() {
this.query = this.emptyQuery(); this.query = this.emptyQuery();
this.handleQuery(); this.handleQuery();
@@ -420,15 +428,28 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.kingdee-pool-dialog__pager { .kingdee-pool-dialog__form {
margin-bottom: 16px;
padding: 12px 12px 4px;
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.kingdee-pool-dialog__pagination {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-top: 12px; padding: 16px 0;
gap: 16px; gap: 16px;
} }
.kingdee-pool-dialog__pager-tip { .kingdee-pool-dialog__pagination-tip {
color: #909399; color: #909399;
font-size: 12px; font-size: 12px;
white-space: nowrap;
}
.kingdee-pool-dialog__query-tip {
margin-left: 8px;
color: #909399;
cursor: pointer;
} }
</style> </style>
+1
View File
@@ -6,6 +6,7 @@ export const invoiceApplicationTableColumns = [
{ prop: 'issuerName', label: '开票方', minWidth: 150 }, { prop: 'issuerName', label: '开票方', minWidth: 150 },
{ prop: 'receiverName', label: '受票方', minWidth: 150 }, { prop: 'receiverName', label: '受票方', minWidth: 150 },
{ prop: 'invoiceAmount', label: '开票金额', minWidth: 130, money: true }, { prop: 'invoiceAmount', label: '开票金额', minWidth: 130, money: true },
{ prop: 'estimatedCost', label: '暂估成本', minWidth: 130, money: true },
{ prop: 'createUserName', label: '创建人', minWidth: 110 }, { prop: 'createUserName', label: '创建人', minWidth: 110 },
{ prop: 'createTime', label: '创建时间', minWidth: 170 }, { prop: 'createTime', label: '创建时间', minWidth: 170 },
{ prop: 'approvalStatusName', label: '状态', minWidth: 110, status: true }, { prop: 'approvalStatusName', label: '状态', minWidth: 110, status: true },
+1 -1
View File
@@ -12,7 +12,7 @@ export const invoiceReceiptTableColumns = [
{ prop: 'createUserName', label: '创建人', minWidth: 110 }, { prop: 'createUserName', label: '创建人', minWidth: 110 },
{ prop: 'createTime', label: '创建时间', minWidth: 170 }, { prop: 'createTime', label: '创建时间', minWidth: 170 },
{ prop: 'kingdeeReceivedInvoiceNo', label: '金蝶单据号', minWidth: 150 }, { prop: 'kingdeeReceivedInvoiceNo', label: '金蝶单据号', minWidth: 150 },
{ prop: 'kingdeePushStatusName', label: '金蝶状态', minWidth: 130 }, { prop: 'kingdeePushStatusName', label: '金蝶状态', minWidth: 130, kingdeeStatus: true },
{ prop: 'kingdeePushMsg', label: '失败原因', minWidth: 160 }, { prop: 'kingdeePushMsg', label: '失败原因', minWidth: 160 },
{ prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 }, { prop: 'attachmentSyncStatusName', label: '附件同步', minWidth: 100 },
]; ];
+13
View File
@@ -77,6 +77,19 @@ export default [
}, },
], ],
}, },
{
// 发票冲销明细独立页(不挂菜单,供开票申请/收票登记/付款申请页面跳转引用)
path: '/payment/invoice-write-off',
component: Layout,
children: [
{
path: '',
name: '发票冲销明细',
meta: { keepAlive: false, activeMenu: '/payment/invoice-application' },
component: loadView(() => import('@/views/payment/invoice-write-off.vue')),
},
],
},
{ {
path: '/payment/payment-application/form', path: '/payment/payment-application/form',
component: Layout, component: Layout,
+47 -20
View File
@@ -21,16 +21,17 @@ export const isKingdeePathAttachment = attachment => {
}; };
/** /**
* 请求金蝶文件流:成功返回 Blob;业务失败为 HTTP 200 + R.fail JSON(响应通道未写流时后端即失败), * 请求金蝶文件流(公共核):成功返回 Blob;业务失败为 HTTP 200 + R.fail JSON
* 按 Content-Type 识别并抛出带 msg 的错误;网络级失败拦截器已提示,标记 silent 避免重复提示。 * (响应通道未写流时后端即失败),按 Content-Type 识别并抛出带 msg 的错误;
* 网络级失败拦截器已提示,标记 silent 避免重复提示。
* 供查看(弹窗内预览)/下载两类入口复用 * 供查看(弹窗内预览)/下载两类入口复用
* @param {String} invoiceNo 发票号码 * @param {Function} fetcher 文件流请求函数(返回 axios blob 响应)
* @returns {Promise<Blob>} * @returns {Promise<Blob>}
*/ */
export const requestKingdeeFile = async invoiceNo => { const fetchKingdeeBlob = async fetcher => {
let res; let res;
try { try {
res = await downloadInvoicePoolAttachment(invoiceNo); res = await fetcher();
} catch (error) { } catch (error) {
throw Object.assign(new Error(error?.message || '附件下载失败'), { silent: true }); throw Object.assign(new Error(error?.message || '附件下载失败'), { silent: true });
} }
@@ -51,6 +52,45 @@ export const requestKingdeeFile = async invoiceNo => {
return blob; return blob;
}; };
/**
* 请求金蝶文件流:票池发票按发票号回源(后端代理 A801-2)
* @param {String} invoiceNo 发票号码
* @returns {Promise<Blob>}
*/
export const requestKingdeeFile = invoiceNo => fetchKingdeeBlob(() => downloadInvoicePoolAttachment(invoiceNo));
/**
* 下载金蝶侧附件(经 A801-2 后端代理,公共入口),失败统一 ElMessage 提示
* @param {Function} fetcher 文件流请求函数
* @param {String} fileName 下载文件名
*/
export const downloadKingdeeFile = async (fetcher, fileName) => {
try {
const blob = await fetchKingdeeBlob(fetcher);
downloadFile(blob, fileName);
} catch (error) {
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件下载失败');
}
};
/**
* 新窗口预览金蝶侧附件(经 A801-2 后端代理,公共入口):拉取 Blob 转 objectURL
* (PDF 可预览;OFD 等浏览器不可预览的格式由浏览器触发下载)
* @param {Function} fetcher 文件流请求函数
* @param {String} fileName 预览文件名
*/
export const viewKingdeeFile = async (fetcher, fileName) => {
try {
const blob = await fetchKingdeeBlob(fetcher);
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
// 延迟释放 objectURL:立即回收可能导致新窗口尚未加载完成即失效
setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch (error) {
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件预览失败');
}
};
/** /**
* 下载金蝶侧发票附件(经 A801-2 后端代理),失败统一 ElMessage 提示 * 下载金蝶侧发票附件(经 A801-2 后端代理),失败统一 ElMessage 提示
* @param {String} invoiceNo 发票号码 * @param {String} invoiceNo 发票号码
@@ -61,12 +101,7 @@ export const downloadKingdeeInvoiceFile = async (invoiceNo, fileName) => {
ElMessage.warning('当前发票缺少发票号码,无法下载金蝶侧附件'); ElMessage.warning('当前发票缺少发票号码,无法下载金蝶侧附件');
return; return;
} }
try { await downloadKingdeeFile(() => downloadInvoicePoolAttachment(invoiceNo), fileName || `${invoiceNo}.pdf`);
const blob = await requestKingdeeFile(invoiceNo);
downloadFile(blob, fileName || `${invoiceNo}.pdf`);
} catch (error) {
if (!error?.silent) ElMessage.error(error?.message || '金蝶附件下载失败');
}
}; };
/** /**
@@ -79,13 +114,5 @@ export const viewKingdeeInvoiceFile = async (invoiceNo, fileName) => {
ElMessage.warning('当前发票缺少发票号码,无法预览金蝶侧附件'); ElMessage.warning('当前发票缺少发票号码,无法预览金蝶侧附件');
return; return;
} }
try { await viewKingdeeFile(() => downloadInvoicePoolAttachment(invoiceNo), fileName || `${invoiceNo}.pdf`);
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 || '金蝶附件预览失败');
}
}; };
+11
View File
@@ -14,6 +14,17 @@ export const KINGDEE_STATUS_TAG_TYPES = {
/** 金蝶单据状态 → el-tag 颜色类型,未映射状态返回 info */ /** 金蝶单据状态 → el-tag 颜色类型,未映射状态返回 info */
export const kingdeeStatusType = status => KINGDEE_STATUS_TAG_TYPES[status] || 'info'; export const kingdeeStatusType = status => KINGDEE_STATUS_TAG_TYPES[status] || 'info';
/** 收票登记推送状态 → el-tag 颜色类型(已指定应付=绿、已保存收票单=黄、推送失败=红、未推送=灰) */
export const KINGDEE_PUSH_STATUS_TAG_TYPES = {
unsynced: 'info',
pushed: 'warning',
assigned: 'success',
failed: 'danger',
};
/** 收票登记推送状态 → el-tag 颜色类型,未映射状态返回 info */
export const kingdeePushStatusType = status => KINGDEE_PUSH_STATUS_TAG_TYPES[status] || 'info';
/** 金蝶冲销状态 → el-tag 颜色类型(已冲销=绿、未冲销=黄) */ /** 金蝶冲销状态 → el-tag 颜色类型(已冲销=绿、未冲销=黄) */
export const KINGDEE_WRITEOFF_TAG_TYPES = { export const KINGDEE_WRITEOFF_TAG_TYPES = {
writtenoff: 'success', writtenoff: 'success',
+304 -91
View File
@@ -8,9 +8,6 @@
:data="data" :data="data"
:table-loading="loading" :table-loading="loading"
:permission="permissionList" :permission="permissionList"
:before-open="beforeOpen"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel" @row-del="rowDel"
@search-change="searchChange" @search-change="searchChange"
@search-reset="searchReset" @search-reset="searchReset"
@@ -20,11 +17,21 @@
@refresh-change="refreshChange" @refresh-change="refreshChange"
@on-load="onLoad" @on-load="onLoad"
> >
<template #menu-left>
<el-button
v-if="permission.invoice_item_add"
type="primary"
icon="el-icon-plus"
@click="openDialog()"
>
新增
</el-button>
</template>
<template #menu="scope"> <template #menu="scope">
<el-link <el-link
v-if="permission.invoice_item_edit" v-if="permission.invoice_item_edit"
type="primary" type="primary"
@click="$refs.crud.rowEdit(scope.row, scope.index)" @click="openDialog(scope.row)"
> >
修改 修改
</el-link> </el-link>
@@ -37,24 +44,125 @@
</el-link> </el-link>
</template> </template>
<template #defaultTaxRate="scope">{{ scope.row.defaultTaxRate }}%</template> <template #defaultTaxRate="scope">{{ scope.row.defaultTaxRate }}%</template>
<template #defaultTaxRate-form>
<el-input
v-model="form.defaultTaxRate"
inputmode="decimal"
maxlength="6"
clearable
placeholder="请输入默认税率"
@input="handleTaxRateInput"
>
<template #append>%</template>
</el-input>
</template>
</avue-crud> </avue-crud>
<el-dialog
v-model="dialog.visible"
:title="dialog.title"
width="900px"
append-to-body
:close-on-click-modal="false"
>
<el-form ref="dialogForm" :model="dialog.form" label-width="130px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="货物或服务简称" required prop="shortName">
<el-input v-model="dialog.form.shortName" maxlength="100" clearable placeholder="请输入货物或服务简称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="税收分类编码" required prop="taxClassificationCode">
<el-input v-model="dialog.form.taxClassificationCode" maxlength="30" clearable placeholder="请输入税收分类编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="商品和服务分类名称" required prop="categoryName">
<el-input v-model="dialog.form.categoryName" maxlength="200" clearable placeholder="请输入商品和服务分类名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="默认税率" required prop="defaultTaxRate">
<el-input
v-model="dialog.form.defaultTaxRate"
inputmode="decimal"
maxlength="6"
clearable
placeholder="请输入默认税率"
@input="handleTaxRateInput"
>
<template #append>%</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="关联费用项目">
<div class="fee-item-box">
<span class="fee-item-tip">可拖动排序,发票冲抵费用项目时,按设置的顺序冲抵费用</span>
<el-button type="primary" size="small" @click="addFeeItem">添加</el-button>
<table class="fee-item-table">
<thead>
<tr>
<th width="36"></th>
<th width="60">序号</th>
<th>费用类型</th>
<th>费用项目</th>
<th width="60">操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in dialog.form.feeItems"
:key="index"
draggable="true"
@dragstart="onFeeDragStart(index, $event)"
@dragover.prevent
@drop="onFeeDrop(index, $event)"
@dragend="onFeeDragEnd"
>
<td class="fee-item-drag" :class="{ 'is-dragging': feeDragIndex === index }">≡</td>
<td>{{ index + 1 }}</td>
<td>
<el-select v-model="item.feeCategory" placeholder="请选择" clearable filterable @change="() => onFeeCategoryChange(item)">
<el-option
v-for="category in feeCategoryOptions"
:key="category.value"
:label="category.label"
:value="category.value"
/>
</el-select>
</td>
<td>
<el-select
v-model="item.feeItemId"
placeholder="请先选择费用类型"
:disabled="!item.feeCategory"
clearable
filterable
@change="onFeeItemChange(item)"
>
<el-option
v-for="fee in feeItemsOfCategory(item.feeCategory)"
:key="fee.id"
:label="fee.name"
:value="fee.id"
/>
</el-select>
</td>
<td>
<el-link type="danger" @click="removeFeeItem(index)">删除</el-link>
</td>
</tr>
<tr v-if="!dialog.form.feeItems.length">
<td colspan="5" class="fee-item-empty">暂无数据</td>
</tr>
</tbody>
</table>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialog.visible = false">取消</el-button>
<el-button type="primary" :loading="dialog.saving" @click="saveDialog">保存</el-button>
</template>
</el-dialog>
</basic-container> </basic-container>
</template> </template>
<script> <script>
import { getList, getDetail, submit, remove } from '@/api/base/invoice-item'; import { getList, getDetail, submit, remove } from '@/api/base/invoice-item';
import { getList as getFeeItemList } from '@/api/base/fee-item';
import { getDictionaryAll } from '@/api/system/dictbiz';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
@@ -65,6 +173,16 @@ export default {
loading: true, loading: true,
query: {}, query: {},
selectionList: [], selectionList: [],
feeItemOptions: [],
// 费用类型字典选项(fee_category 字典:dictKey-编码A01,dictValue-名称运输)
feeCategoryDictOptions: [],
feeDragIndex: null,
dialog: {
visible: false,
title: '新增',
saving: false,
form: this.emptyForm(),
},
page: { page: {
pageSize: 10, pageSize: 10,
pageSizes: [10, 30, 50, 100, 200], pageSizes: [10, 30, 50, 100, 200],
@@ -81,13 +199,11 @@ export default {
indexLabel: '序号', indexLabel: '序号',
indexWidth: 70, indexWidth: 70,
selection: false, selection: false,
addBtn: true, addBtn: false,
editBtn: false, editBtn: false,
delBtn: false, delBtn: false,
viewBtn: false, viewBtn: false,
menuWidth: 140, menuWidth: 140,
dialogWidth: 900,
dialogClickModal: false,
labelPosition: 'right', labelPosition: 'right',
labelWidth: 'auto', labelWidth: 'auto',
searchShow: true, searchShow: true,
@@ -100,49 +216,22 @@ export default {
label: '货物或服务简称', label: '货物或服务简称',
prop: 'shortName', prop: 'shortName',
minWidth: 160, minWidth: 160,
span: 12,
maxlength: 100,
rules: [{ required: true, message: '请输入货物或服务简称', trigger: 'blur' }],
}, },
{ {
label: '税收分类编码', label: '税收分类编码',
prop: 'taxClassificationCode', prop: 'taxClassificationCode',
minWidth: 190, minWidth: 190,
span: 12,
maxlength: 30,
rules: [{ required: true, message: '请输入税收分类编码', trigger: 'blur' }],
}, },
{ {
label: '商品和服务分类名称', label: '商品和服务分类名称',
prop: 'categoryName', prop: 'categoryName',
minWidth: 220, minWidth: 220,
span: 12,
maxlength: 200,
rules: [{ required: true, message: '请输入商品和服务分类名称', trigger: 'blur' }],
}, },
{ {
label: '默认税率', label: '默认税率',
prop: 'defaultTaxRate', prop: 'defaultTaxRate',
type: 'input',
formslot: true,
slot: true,
minWidth: 120, minWidth: 120,
span: 12, slot: true,
rules: [
{ required: true, message: '请输入默认税率', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const text = String(value ?? '');
const rate = Number(text);
if (!/^\d+(\.\d+)?$/.test(text) || rate < 0 || rate > 100) {
callback(new Error('默认税率必须是0到100之间的数字'));
return;
}
callback();
},
trigger: 'blur',
},
],
}, },
{ {
label: '税收项目', label: '税收项目',
@@ -193,27 +282,131 @@ export default {
delBtn: false, delBtn: false,
}; };
}, },
// 费用类型下拉选项:优先字典(显示"名称/编码"),无字典数据时兜底费用项目库提取
feeCategoryOptions() {
if (this.feeCategoryDictOptions.length) return this.feeCategoryDictOptions;
return [...new Set(this.feeItemOptions.map(fee => fee.feeCategory).filter(Boolean))]
.map(category => ({ label: category, value: category }));
},
}, },
methods: { methods: {
rowSave(row, done, loading) { emptyForm() {
submit(this.normalizeTaxRateRow(row)).then( return {
() => { id: null,
this.onLoad(this.page); shortName: '',
this.$message.success('操作成功'); taxClassificationCode: '',
done(); categoryName: '',
}, defaultTaxRate: 0,
() => loading() feeItems: [],
); };
}, },
rowUpdate(row, index, done, loading) { openDialog(row) {
submit(this.normalizeTaxRateRow(row)).then( this.dialog.form = this.emptyForm();
() => { this.dialog.title = row ? '编辑' : '新增';
this.onLoad(this.page); this.dialog.visible = true;
this.loadFeeItemOptions();
this.initFeeCategoryOptions();
if (row) {
// 编辑回填:后端 detail 返回关联费用项目(按排序号升序)
getDetail(row.id).then(res => {
const detail = res.data.data || {};
this.dialog.form = {
...this.emptyForm(),
...detail,
defaultTaxRate: detail.defaultTaxRate ?? 0,
feeItems: (detail.feeItems || []).map(item => ({
feeItemId: item.feeItemId,
feeItemName: item.feeItemName,
feeCategory: item.feeCategory,
})),
};
});
}
},
loadFeeItemOptions() {
if (this.feeItemOptions.length) return;
getFeeItemList(1, 1000, { status: 1 }).then(res => {
this.feeItemOptions = (res.data.data?.records) || [];
});
},
// 费用类型字典:code=fee_category(dictKey-编码如A01,dictValue-名称如运输),label 显示"名称/编码"与费用项目页一致
initFeeCategoryOptions() {
if (this.feeCategoryDictOptions.length) return;
getDictionaryAll().then(res => {
this.feeCategoryDictOptions = (res.data.data || [])
.filter(item => item.code === 'fee_category' && Number(item.parentId) > 0)
.sort((first, second) => Number(first.sort || 0) - Number(second.sort || 0))
.map(item => ({
label: `${item.dictValue}/${item.dictKey}`,
value: String(item.dictKey),
}));
});
},
// 级联:该费用类型下的费用项目选项
feeItemsOfCategory(feeCategory) {
if (!feeCategory) return [];
return this.feeItemOptions.filter(fee => fee.feeCategory === feeCategory);
},
// 先选费用类型:清空原费用项目(原项目可能不属于新类型)
onFeeCategoryChange(item) {
item.feeItemId = null;
item.feeItemName = '';
},
// 后选费用项目:带出名称(费用类型已由上级确定)
onFeeItemChange(item) {
const fee = this.feeItemsOfCategory(item.feeCategory).find(option => option.id === item.feeItemId);
item.feeItemName = fee ? fee.name : '';
},
addFeeItem() {
this.dialog.form.feeItems.push({ feeItemId: null, feeItemName: '', feeCategory: '' });
},
removeFeeItem(index) {
this.dialog.form.feeItems.splice(index, 1);
},
onFeeDragStart(index, event) {
this.feeDragIndex = index;
event.dataTransfer.effectAllowed = 'move';
},
onFeeDrop(index) {
const from = this.feeDragIndex;
if (from === null || from === index) return;
const items = this.dialog.form.feeItems;
items.splice(index, 0, items.splice(from, 1)[0]);
this.feeDragIndex = null;
},
onFeeDragEnd() {
this.feeDragIndex = null;
},
saveDialog() {
const formItem = this.dialog.form;
if (!formItem.shortName) return this.$message.warning('请输入货物或服务简称');
if (!formItem.taxClassificationCode) return this.$message.warning('请输入税收分类编码');
if (!formItem.categoryName) return this.$message.warning('请输入商品和服务分类名称');
const rate = Number(formItem.defaultTaxRate);
if (!/^\d+(\.\d+)?$/.test(String(formItem.defaultTaxRate ?? '')) || rate < 0 || rate > 100) {
return this.$message.warning('默认税率必须是0到100之间的数字');
}
if (formItem.feeItems.some(item => !item.feeItemId)) {
return this.$message.warning('请选择关联费用项目');
}
this.dialog.saving = true;
submit({
...formItem,
defaultTaxRate: rate,
feeItems: formItem.feeItems.map(item => ({
feeItemId: item.feeItemId,
feeItemName: item.feeItemName,
feeCategory: item.feeCategory,
})),
})
.then(() => {
this.$message.success('操作成功'); this.$message.success('操作成功');
done(); this.dialog.visible = false;
}, this.onLoad(this.page);
() => loading() })
); .finally(() => {
this.dialog.saving = false;
});
}, },
rowDel(row) { rowDel(row) {
this.$confirm('确定将选择数据删除?', '提示', { type: 'warning' }) this.$confirm('确定将选择数据删除?', '提示', { type: 'warning' })
@@ -223,27 +416,11 @@ export default {
this.$message.success('操作成功'); this.$message.success('操作成功');
}); });
}, },
beforeOpen(done, type) {
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data || {};
done();
});
return;
}
this.form = { defaultTaxRate: 0 };
done();
},
handleTaxRateInput(value) { handleTaxRateInput(value) {
let normalized = String(value || '').replace(/[^\d.]/g, ''); let normalized = String(value || '').replace(/[^\d.]/g, '');
normalized = normalized.replace(/(\..*)\./g, '$1'); normalized = normalized.replace(/(\..*)\./g, '$1');
if (normalized.startsWith('.')) normalized = `0${normalized}`; if (normalized.startsWith('.')) normalized = `0${normalized}`;
this.form.defaultTaxRate = normalized; this.dialog.form.defaultTaxRate = normalized;
},
normalizeTaxRateRow(row) {
const values = { ...row };
values.defaultTaxRate = Number(values.defaultTaxRate);
return values;
}, },
searchChange(params, done) { searchChange(params, done) {
this.query = { this.query = {
@@ -292,17 +469,53 @@ export default {
margin-bottom: 8px; margin-bottom: 8px;
} }
.invoice-item-page :deep(.avue-crud__dialog .el-form-item) {
margin-bottom: 28px;
}
.invoice-item-page :deep(.avue-crud__dialog .el-form-item__error) {
position: relative;
line-height: 20px;
white-space: normal;
}
.invoice-item-page :deep(.avue-crud__menu .el-link) { .invoice-item-page :deep(.avue-crud__menu .el-link) {
margin-right: 8px; margin-right: 8px;
} }
.fee-item-box {
width: 100%;
}
.fee-item-tip {
display: inline-block;
margin-right: 12px;
color: #f56c6c;
font-size: 12px;
}
.fee-item-table {
width: 100%;
margin-top: 8px;
border-collapse: collapse;
}
.fee-item-table th,
.fee-item-table td {
padding: 8px;
border: 1px solid #ebeef5;
text-align: center;
font-size: 13px;
}
.fee-item-table th {
background: #f8f8f9;
color: #515a6e;
font-weight: 500;
}
.fee-item-drag {
cursor: move;
color: #909399;
font-size: 16px;
user-select: none;
}
.fee-item-drag.is-dragging {
color: #409eff;
}
.fee-item-empty {
color: #909399;
}
</style> </style>
+64 -3
View File
@@ -86,6 +86,27 @@
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6">
<el-form-item label="项目利润率">
<el-input
:model-value="profitRatePercent"
:disabled="readonly"
placeholder="选结算单后自动带出,可修改"
@input="handleProfitRateInput"
>
<template #append>%</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="暂估成本(不含税)">
<el-input
:model-value="estimatedCost > 0 ? formatMoney(estimatedCost) : ''"
disabled
placeholder="开票不含税金额合计×项目利润率"
/>
</el-form-item>
</el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="部门邮箱" prop="departmentEmails"> <el-form-item label="部门邮箱" prop="departmentEmails">
<el-select <el-select
@@ -326,12 +347,18 @@
<el-table-column label="结算总金额" min-width="140" align="right"> <el-table-column label="结算总金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template> <template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="历史累计开票金额" min-width="150" align="right">
<template #default="{ row }">{{ formatMoney(row.historyInvoicedAmount) }}</template>
</el-table-column>
<el-table-column label="本次开票金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.allocatedInvoiceAmount) }}</template>
</el-table-column>
<el-table-column label="剩余未开票金额" min-width="150" align="right">
<template #default="{ row }">{{ formatMoney(row.remainingUninvoicedAmount) }}</template>
</el-table-column>
<el-table-column label="可开票金额" min-width="140" align="right"> <el-table-column label="可开票金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.availableInvoiceAmount) }}</template> <template #default="{ row }">{{ formatMoney(row.availableInvoiceAmount) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="分摊发票金额" min-width="140" align="right">
<template #default="{ row }">{{ formatMoney(row.allocatedInvoiceAmount) }}</template>
</el-table-column>
</el-table> </el-table>
</section-card> </section-card>
@@ -385,6 +412,10 @@
</el-table> </el-table>
</section-card> </section-card>
<section-card v-if="recordId" title="发票冲销明细">
<invoice-write-off-table biz-type="invoice_apply" :biz-id="recordId" />
</section-card>
<section-card title="附件信息"> <section-card title="附件信息">
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" /> <vehicle-attachment-table v-model="form.attachments" :readonly="readonly" />
</section-card> </section-card>
@@ -491,6 +522,7 @@ import { getList as getReceivablePayableDetailList } from '@/api/settlement/rece
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication'; import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import InvoiceWriteOffTable from '@/components/invoice-write-off-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process'; import { getMkPublicDetail } from '@/api/mk-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
@@ -538,12 +570,15 @@ const emptyForm = () => ({
sheets: [emptySheet()], sheets: [emptySheet()],
attachments: [], attachments: [],
remark: '', remark: '',
// 项目利润率(小数,如0.04表示4%;保存时后端快照并重算暂估成本)
profitRate: null,
}); });
export default { export default {
name: 'InvoiceApplicationForm', name: 'InvoiceApplicationForm',
components: { components: {
VehicleAttachmentTable, VehicleAttachmentTable,
InvoiceWriteOffTable,
}, },
data() { data() {
return { return {
@@ -621,6 +656,24 @@ export default {
0 0
); );
}, },
// 项目利润率百分数展示/输入(后端存小数:0.04 ↔ 4)
profitRatePercent() {
const rate = Number(this.form.profitRate);
if (!Number.isFinite(rate) || rate <= 0) return '';
return String(Math.round(rate * 10000) / 100);
},
// 暂估成本 = 开票不含税金额合计 × 项目利润率(实时计算,后端保存时重算覆盖)
estimatedCost() {
const rate = Number(this.form.profitRate);
if (!Number.isFinite(rate) || rate <= 0) return 0;
const amountNoTaxTotal = this.form.sheets.reduce(
(sheetTotal, sheet) =>
sheetTotal +
sheet.lines.reduce((lineTotal, line) => lineTotal + Number(line.amountNoTax || 0), 0),
0
);
return amountNoTaxTotal * rate;
},
detailColumns() { detailColumns() {
return [...this.detailBaseColumns, ...this.feeItemColumns]; return [...this.detailBaseColumns, ...this.feeItemColumns];
}, },
@@ -732,6 +785,13 @@ export default {
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value
); );
}, },
// 项目利润率输入(百分数),转小数存储:4 → 0.04
handleProfitRateInput(value) {
let normalized = String(value || '').replace(/[^\d.]/g, '');
normalized = normalized.replace(/(\..*)\./g, '$1');
const percent = Number(normalized);
this.form.profitRate = Number.isFinite(percent) && percent > 0 ? percent / 100 : null;
},
invoiceItemNames(categoryName) { invoiceItemNames(categoryName) {
return this.invoiceItems.filter(item => item.categoryName === categoryName); return this.invoiceItems.filter(item => item.categoryName === categoryName);
}, },
@@ -1173,6 +1233,7 @@ export default {
contactName: this.form.contactName, contactName: this.form.contactName,
contactPhone: this.form.contactPhone, contactPhone: this.form.contactPhone,
email: this.form.email, email: this.form.email,
profitRate: Number(this.form.profitRate) > 0 ? Number(this.form.profitRate) : null,
attachmentsJson: JSON.stringify(this.form.attachments || []), attachmentsJson: JSON.stringify(this.form.attachments || []),
remark: this.form.remark, remark: this.form.remark,
settlements: this.form.settlements.map(item => ({ settlements: this.form.settlements.map(item => ({
+25 -1
View File
@@ -128,7 +128,12 @@
</section-card> </section-card>
<section-card title="发票明细"> <section-card title="发票明细">
<el-table :data="invoiceItems" border empty-text="选择金蝶票据池发票后带出发票明细行"> <template #extra>
<el-button size="small" type="primary" :loading="invoiceDetailLoading" :disabled="!form.invoiceNo" @click="reloadInvoiceDetail">
查询明细
</el-button>
</template>
<el-table v-loading="invoiceDetailLoading" :data="invoiceItems" border empty-text="选择金蝶票据池发票后带出发票明细行">
<el-table-column type="index" label="序号" width="64" align="center" /> <el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="goodsName" label="货物名称" min-width="160" show-overflow-tooltip /> <el-table-column prop="goodsName" label="货物名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="specModel" label="规格型号" min-width="120" show-overflow-tooltip /> <el-table-column prop="specModel" label="规格型号" min-width="120" show-overflow-tooltip />
@@ -179,6 +184,10 @@
</el-table> </el-table>
</section-card> </section-card>
<section-card v-if="recordId" title="发票冲销明细">
<invoice-write-off-table biz-type="receipt" :biz-id="recordId" />
</section-card>
<section-card title="附件信息"> <section-card title="附件信息">
<!-- 金蝶票池认领的发票附件为 piaozone path 型,需带发票号走 A801-2 代理查看/下载 --> <!-- 金蝶票池认领的发票附件为 piaozone path 型,需带发票号走 A801-2 代理查看/下载 -->
<vehicle-attachment-table <vehicle-attachment-table
@@ -276,6 +285,7 @@ import * as api from '@/api/payment/invoiceReceipt';
import { getList as getContractList } from '@/api/business/contract-manage'; import { getList as getContractList } from '@/api/business/contract-manage';
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue'; import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue'; import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import InvoiceWriteOffTable from '@/components/invoice-write-off-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process'; import { getMkPublicDetail } from '@/api/mk-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
@@ -315,6 +325,7 @@ export default {
components: { components: {
KingdeeInvoicePoolDialog, KingdeeInvoicePoolDialog,
VehicleAttachmentTable, VehicleAttachmentTable,
InvoiceWriteOffTable,
}, },
data() { data() {
return { return {
@@ -323,6 +334,7 @@ export default {
customerEmailOptions: [], customerEmailOptions: [],
departmentEmailOptions: [], departmentEmailOptions: [],
invoiceItems: [], invoiceItems: [],
invoiceDetailLoading: false,
contractDialog: { contractDialog: {
visible: false, visible: false,
loading: false, loading: false,
@@ -504,6 +516,18 @@ export default {
this.invoiceItems = []; this.invoiceItems = [];
} }
}, },
// 手动按发票号查询金蝶发票明细(历史单据无缓存明细时点击刷新)
async reloadInvoiceDetail() {
this.invoiceDetailLoading = true;
try {
this.invoiceItems = this.unwrapData(await api.invoiceDetailQuery(this.form.invoiceNo)) || [];
if (!this.invoiceItems.length) this.$message.info('未查询到该发票的明细行');
} catch {
this.$message.error('查询金蝶发票明细失败');
} finally {
this.invoiceDetailLoading = false;
}
},
// 所属合同=承运商合同:选择后带出承办部门(结算组织=承办部门对应平台公司金蝶编码,后端派生) // 所属合同=承运商合同:选择后带出承办部门(结算组织=承办部门对应平台公司金蝶编码,后端派生)
openContractDialog() { openContractDialog() {
this.contractDialog.visible = true; this.contractDialog.visible = true;
+5
View File
@@ -119,6 +119,9 @@
<el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)" class="status-text"> <el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)" class="status-text">
{{ displayValue(row[column.prop]) }} {{ displayValue(row[column.prop]) }}
</el-tag> </el-tag>
<el-tag v-else-if="column.kingdeeStatus" :type="kingdeePushStatusType(row.kingdeePushStatus)">
{{ displayValue(row[column.prop]) }}
</el-tag>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span> <span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span> <span v-else>{{ displayValue(row[column.prop]) }}</span>
</template> </template>
@@ -222,6 +225,7 @@ import TableHeaderTools from '@/components/table-header-tools/main.vue';
import organizationSearch from '@/mixins/organization-search'; import organizationSearch from '@/mixins/organization-search';
import { getMKApprovalUrl } from '@/api/system/business-process'; import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import { kingdeePushStatusType } from '@/utils/kingdee';
// 表单提交成功后带标记返回,keep-alive 缓存的列表页据此重新拉取 // 表单提交成功后带标记返回,keep-alive 缓存的列表页据此重新拉取
const invoiceReceiptListRefreshKey = 'invoice-receipt-list-refresh'; const invoiceReceiptListRefreshKey = 'invoice-receipt-list-refresh';
@@ -480,6 +484,7 @@ export default {
'' ''
); );
}, },
kingdeePushStatusType,
displayValue(value) { displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value; return value === null || value === undefined || value === '' ? '-' : value;
}, },
+28
View File
@@ -0,0 +1,28 @@
<template>
<basic-container>
<section-card title="发票冲销明细">
<invoice-write-off-table :biz-type="bizType" :biz-id="bizId" />
</section-card>
</basic-container>
</template>
<script>
import InvoiceWriteOffTable from '@/components/invoice-write-off-table/main.vue';
/**
* 发票冲销明细独立页(不挂菜单,供其他页面 $router 跳转引用)
* 用法:$router.push({ path: '/payment/invoice-write-off', query: { bizType: 'invoice_apply', bizId: id } })
*/
export default {
name: 'InvoiceWriteOff',
components: { InvoiceWriteOffTable },
computed: {
bizType() {
return String(this.$route.query.bizType || 'invoice_apply');
},
bizId() {
return this.$route.query.bizId || null;
},
},
};
</script>
+95 -46
View File
@@ -316,6 +316,13 @@
<el-table-column label="匹配金额(含税)" min-width="150"> <el-table-column label="匹配金额(含税)" min-width="150">
<template #default="{ row }">{{ formatMoney(row.matchedAmount) }}</template> <template #default="{ row }">{{ formatMoney(row.matchedAmount) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="冲销状态" width="110" align="center">
<template #default="{ row }">
<el-tag v-if="row.writeOffStatus === 'finished'" type="success">已完成</el-tag>
<el-tag v-else-if="row.invoiceNo" type="warning">未完成</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center"> <el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<div class="payment-form-page__invoice-actions"> <div class="payment-form-page__invoice-actions">
@@ -343,6 +350,9 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</section-card> </section-card>
<section-card v-if="recordId" title="发票冲销明细">
<invoice-write-off-table biz-type="payment_receipt" :biz-id="recordId" />
</section-card>
<section-card title="付款记录"> <section-card title="付款记录">
<el-table :data="paymentRecords" border <el-table :data="paymentRecords" border
><el-table-column type="index" label="序号" width="70" /><el-table-column ><el-table-column type="index" label="序号" width="70" /><el-table-column
@@ -354,11 +364,23 @@
><el-table-column prop="paidDate" label="付款日期" min-width="150" /><el-table-column ><el-table-column prop="paidDate" label="付款日期" min-width="150" /><el-table-column
prop="paymentNo" prop="paymentNo"
label="付款单号" label="付款单号"
min-width="160" /><el-table-column min-width="160" /><el-table-column label="付款凭证" min-width="160"
prop="kingdeeBillNo" ><template #default="{ row }"
label="付款凭证" ><div v-if="row.voucherFileName" class="payment-form-page__invoice-actions"
min-width="160" ><el-link
/></el-table> type="primary"
:disabled="kingdeeAttachmentBusy"
@click="viewRecordVoucher(row)"
>查看</el-link
><el-link
type="primary"
:disabled="kingdeeAttachmentBusy"
@click="downloadRecordVoucher(row)"
>下载</el-link
></div
><span v-else>-</span></template
></el-table-column
></el-table>
</section-card> </section-card>
<section-card> <section-card>
<template #title> <template #title>
@@ -733,14 +755,15 @@ import {
} from '@/api/vehicle/customer-archive'; } from '@/api/vehicle/customer-archive';
import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer'; import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer';
import { formatContractOptionLabel } from '@/utils/contract'; import { formatContractOptionLabel } from '@/utils/contract';
import { downloadFileByUrl } from '@/utils/util'; import { downloadFile, downloadFileByUrl } from '@/utils/util';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval'; import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import { import {
downloadKingdeeInvoiceFile, downloadKingdeeFile,
isKingdeePathAttachment, requestKingdeeFile,
viewKingdeeInvoiceFile, viewKingdeeFile,
} from '@/utils/kingdee-attachment'; } from '@/utils/kingdee-attachment';
import PdfPreview from '@/components/pdf-preview/main.vue'; import PdfPreview from '@/components/pdf-preview/main.vue';
import InvoiceWriteOffTable from '@/components/invoice-write-off-table/main.vue';
const attachmentViewerPlugins = [ const attachmentViewerPlugins = [
imagePlugin(), imagePlugin(),
@@ -817,6 +840,7 @@ export default {
ElImageViewer, ElImageViewer,
OpenFileViewer, OpenFileViewer,
PdfPreview, PdfPreview,
InvoiceWriteOffTable,
}, },
data() { data() {
return { return {
@@ -1899,55 +1923,80 @@ export default {
return {}; return {};
} }
}, },
invoiceAttachmentUrl(row) {
return this.attachmentFileUrl(this.invoiceAttachment(row));
},
invoiceAttachmentName(row) { invoiceAttachmentName(row) {
return ( return (
this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf` this.attachmentFileName(this.invoiceAttachment(row)) || `${row.invoiceNo || '发票'}.pdf`
); );
}, },
/** 金蝶附件查看/下载互斥闸门(A801-2 拉流较慢,进行中忽略重复点击,结束自动释放) */
async withKingdeeAttachmentBusy(action) {
if (this.kingdeeAttachmentBusy) return;
this.kingdeeAttachmentBusy = true;
try {
await action();
} finally {
this.kingdeeAttachmentBusy = false;
}
},
async viewInvoiceAttachment(row) { async viewInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row); await this.withKingdeeAttachmentBusy(async () => {
// 金蝶侧附件(path 型)经后端代理 A801-2 预览,OSS 直链维持本地预览组件 // 发票版式文件按票号走金蝶票池(A801-2 镜像优先/实时回源),登记扫描件仅在票池无此票时兜底
if (isKingdeePathAttachment(attachment)) {
if (this.kingdeeAttachmentBusy) return;
this.kingdeeAttachmentBusy = true;
try { try {
await viewKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row)); const blob = await requestKingdeeFile(row.invoiceNo);
} finally { const url = URL.createObjectURL(blob);
this.kingdeeAttachmentBusy = false; window.open(url, '_blank');
// 延迟释放 objectURL:立即回收可能导致新窗口尚未加载完成即失效
setTimeout(() => URL.revokeObjectURL(url), 60000);
return;
} catch (error) {
// 网络级失败(silent,拦截器已提示)不再回退;票池无此票回退登记附件
if (error?.silent) return;
} }
return; const attachment = this.invoiceAttachment(row);
} if (!this.attachmentFileUrl(attachment)) {
if (!this.attachmentFileUrl(attachment)) { this.$message.warning('当前发票没有金蝶版式文件,也没有登记附件');
this.$message.warning('当前发票暂无可查看附件'); return;
return; }
} this.previewAttachment(
this.previewAttachment( { ...attachment, name: this.invoiceAttachmentName(row) },
{ ...attachment, name: this.invoiceAttachmentName(row) }, [attachment]
[attachment] );
); });
}, },
async downloadInvoiceAttachment(row) { async downloadInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row); await this.withKingdeeAttachmentBusy(async () => {
// 金蝶侧附件(path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载
if (isKingdeePathAttachment(attachment)) {
if (this.kingdeeAttachmentBusy) return;
this.kingdeeAttachmentBusy = true;
try { try {
await downloadKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row)); const blob = await requestKingdeeFile(row.invoiceNo);
} finally { downloadFile(blob, this.invoiceAttachmentName(row));
this.kingdeeAttachmentBusy = false; return;
} catch (error) {
if (error?.silent) return;
} }
return; const attachment = this.invoiceAttachment(row);
} const url = this.attachmentFileUrl(attachment);
const url = this.attachmentFileUrl(attachment); if (!url) {
if (!url) { this.$message.warning('当前发票没有金蝶版式文件,也没有登记附件');
this.$message.warning('当前发票暂无可下载附件'); return;
return; }
} downloadFileByUrl(url, this.invoiceAttachmentName(row));
downloadFileByUrl(url, this.invoiceAttachmentName(row)); });
},
/** 付款记录付款凭证(金蝶电子回单,A801-2 代理):查看=新窗口预览,下载=保存文件 */
viewRecordVoucher(row) {
return this.withKingdeeAttachmentBusy(() =>
viewKingdeeFile(
() => api.downloadRecordVoucher(row.id),
`${row.paymentNo || '付款记录'}-付款凭证.pdf`
)
);
},
downloadRecordVoucher(row) {
return this.withKingdeeAttachmentBusy(() =>
downloadKingdeeFile(
() => api.downloadRecordVoucher(row.id),
`${row.paymentNo || '付款记录'}-付款凭证.pdf`
)
);
}, },
attachmentFileName(file) { attachmentFileName(file) {
return String(file?.originalName || file?.name || file?.fileName || '').trim(); return String(file?.originalName || file?.name || file?.fileName || '').trim();
@@ -1133,12 +1133,8 @@ import {
settlementRecordTypeName, settlementRecordTypeName,
settlementStatusName, settlementStatusName,
} from '@/utils/settlementStatus'; } from '@/utils/settlementStatus';
import { downloadFileByUrl } from '@/utils/util'; import { downloadFile, downloadFileByUrl } from '@/utils/util';
import { import { requestKingdeeFile } from '@/utils/kingdee-attachment';
downloadKingdeeInvoiceFile,
isKingdeePathAttachment,
viewKingdeeInvoiceFile,
} from '@/utils/kingdee-attachment';
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue'; import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue'; import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import ProjectAdvanceWriteOff from './project-advance-write-off.vue'; import ProjectAdvanceWriteOff from './project-advance-write-off.vue';
@@ -2918,38 +2914,45 @@ export default {
return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`; return attachment.originalName || attachment.name || `${row.invoiceNo || '发票'}.pdf`;
}, },
async viewInvoiceAttachment(row) { async viewInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row); if (this.kingdeeAttachmentBusy) return;
// 金蝶侧附件(path 型)经后端代理 A801-2 预览,OSS 直链维持 window.open this.kingdeeAttachmentBusy = true;
if (isKingdeePathAttachment(attachment)) { try {
if (this.kingdeeAttachmentBusy) return; // 发票版式文件按票号走金蝶票池(A801-2 镜像优先/实时回源),登记扫描件仅在票池无此票时兜底
this.kingdeeAttachmentBusy = true;
try { try {
await viewKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row)); const blob = await requestKingdeeFile(row.invoiceNo);
} finally { const url = URL.createObjectURL(blob);
this.kingdeeAttachmentBusy = false; window.open(url, '_blank');
// 延迟释放 objectURL:立即回收可能导致新窗口尚未加载完成即失效
setTimeout(() => URL.revokeObjectURL(url), 60000);
return;
} catch (error) {
// 网络级失败(silent,拦截器已提示)不再回退;票池无此票回退登记附件
if (error?.silent) return;
} }
return; const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前发票没有金蝶版式文件,也没有登记附件');
window.open(url, '_blank');
} finally {
this.kingdeeAttachmentBusy = false;
} }
const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前发票暂无可查看附件');
window.open(url, '_blank');
}, },
async downloadInvoiceAttachment(row) { async downloadInvoiceAttachment(row) {
const attachment = this.invoiceAttachment(row); if (this.kingdeeAttachmentBusy) return;
// 金蝶侧附件(path 型)经后端代理 A801-2 下载,OSS 直链维持前端直连下载 this.kingdeeAttachmentBusy = true;
if (isKingdeePathAttachment(attachment)) { try {
if (this.kingdeeAttachmentBusy) return;
this.kingdeeAttachmentBusy = true;
try { try {
await downloadKingdeeInvoiceFile(row.invoiceNo, this.invoiceAttachmentName(row)); const blob = await requestKingdeeFile(row.invoiceNo);
} finally { downloadFile(blob, this.invoiceAttachmentName(row));
this.kingdeeAttachmentBusy = false; return;
} catch (error) {
if (error?.silent) return;
} }
return; const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前发票没有金蝶版式文件,也没有登记附件');
downloadFileByUrl(url, this.invoiceAttachmentName(row));
} finally {
this.kingdeeAttachmentBusy = false;
} }
const url = this.invoiceAttachmentUrl(row);
if (!url) return this.$message.warning('当前发票暂无可下载附件');
downloadFileByUrl(url, this.invoiceAttachmentName(row));
}, },
// 开票发票预览地址:数电PDF优先(OFD浏览器无法预览),退预览图、板式文件 // 开票发票预览地址:数电PDF优先(OFD浏览器无法预览),退预览图、板式文件
issuedInvoicePreviewUrl(row) { issuedInvoicePreviewUrl(row) {
@@ -74,6 +74,7 @@
<script> <script>
import { claimCandidates } from '@/api/payment/invoiceReceipt'; import { claimCandidates } from '@/api/payment/invoiceReceipt';
import { kingdeePushStatusType } from '@/utils/kingdee';
export default { export default {
name: 'InvoiceReceiptCandidatesDialog', name: 'InvoiceReceiptCandidatesDialog',
@@ -152,7 +153,7 @@ export default {
); );
}, },
pushStatusType(value) { pushStatusType(value) {
return { pushed: 'warning', assigned: 'success', failed: 'danger', unsynced: 'info' }[value] || 'info'; return kingdeePushStatusType(value);
}, },
formatMoney(value) { formatMoney(value) {
const number = Number(value); const number = Number(value);
+6 -2
View File
@@ -145,10 +145,10 @@
<el-descriptions-item label="创建时间"> <el-descriptions-item label="创建时间">
{{ invoiceRegisterDialog.row.createTime || '-' }} {{ invoiceRegisterDialog.row.createTime || '-' }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="金蝶同步状态" :span="2"> <el-descriptions-item label="金蝶同步状态">
{{ kingdeeSyncName(invoiceRegisterDialog.row.kingdeeSyncStatus) }} {{ kingdeeSyncName(invoiceRegisterDialog.row.kingdeeSyncStatus) }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="备注" :span="2"> <el-descriptions-item label="备注">
{{ invoiceRegisterDialog.row.remark || '-' }} {{ invoiceRegisterDialog.row.remark || '-' }}
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
@@ -847,6 +847,10 @@ export default {
settlementTypeName(value) { settlementTypeName(value) {
return { payable: '付款', receivable: '收款' }[value] || '-'; return { payable: '付款', receivable: '收款' }[value] || '-';
}, },
// 模板无法直接访问 script 顶部 import,包装成实例方法(与 formal-settlement-table-panel 同模式)
kingdeeSyncName(value) {
return kingdeeSyncName(value);
},
unwrapData(response) { unwrapData(response) {
const body = response?.data || response || {}; const body = response?.data || response || {};
return body?.data || body; return body?.data || body;