Compare commits

...
Author SHA1 Message Date
刘泉佳 8d39b00fba fix: 结算单同步金蝶前端超时30s放宽至60s,避免后端推送成功前端假报失败 2026-11-10 18:15:24 +08:00
刘泉佳 ff44137a37 fix: 开票回写发票行匹配结算单金额改用后端冲抵汇总数据 2026-11-10 17:48:46 +08:00
刘泉佳 ba1e60d809 fix: 开票回写发票行回显票面金额作为可匹配与匹配结算单金额 2026-11-10 17:39:04 +08:00
刘泉佳 64aba30e3f feat: 结算单详情开票回写发票合并至发票信息表格展示 2026-11-10 17:35:13 +08:00
刘泉佳 96822cc24b fix: 冲销明细运输类型字典翻译并加宽发票号列 2026-11-10 14:44:04 +08:00
刘泉佳 58b208201a fix: 冲销明细费用项目子列文案按业务区分并对齐设计图占位符 2026-11-10 14:29:11 +08:00
刘泉佳 da616f83c5 feat: 收票登记查询明细底部弹窗展示金蝶发票全量信息与全部明细行 2026-11-10 14:20:18 +08:00
刘泉佳 0cd2e3b28f feat: 开票付款申请发票冲销改底部弹窗展示并选结算单带出项目利润率 2026-11-10 12:19:34 +08:00
刘泉佳 29a0cdf9fd feat: 付款申请恢复冲销状态列与冲销明细区块 2026-11-10 11:24:23 +08:00
刘泉佳 291dbf5fed refactor: 付款申请移除冲销状态列与冲销明细区块 2026-11-10 10:07:17 +08:00
刘泉佳 4a4b9af694 feat: 付款申请发票信息显示冲销状态并接入冲销明细 2026-11-09 18:18:15 +08:00
刘泉佳 4b31f7ee78 feat: 收票登记新增发票明细查询按钮并接入冲销明细 2026-11-09 18:18:15 +08:00
刘泉佳 0764bf3a42 feat: 开票申请接入暂估成本与冲销明细展示 2026-11-09 18:18:15 +08:00
刘泉佳 921ca3cd7a feat: 新增发票冲销明细公共组件与独立页面路由 2026-11-09 18:18:14 +08:00
刘泉佳 799220642b feat: 开票项目支持关联费用项目拖动排序与级联选择 2026-11-09 18:18:14 +08:00
刘泉佳 44cec23147 fix: 发票信息查看下载优先走金蝶票池版式文件 2026-11-08 14:01:21 +08:00
刘泉佳 b218fc2345 feat: 付款记录付款凭证查看下载 2026-11-06 17:39:34 +08:00
刘泉佳 eb74466c30 refactor: 抽取金蝶附件查看下载公共方法 2026-11-06 17:39:30 +08:00
刘泉佳 79e903bcd8 fix: 修复发票登记弹窗金蝶同步状态不显示 2026-11-05 18:05:58 +08:00
刘泉佳 119d71b4f5 fix: 收票管理列表金蝶状态增加颜色标签与其他页面一致 2026-11-05 16:49:01 +08:00
刘泉佳 d57d3f86da fix: 金蝶票据池弹框样式与批量转结算保持一致 2026-11-05 16:34:04 +08:00
19 changed files with 1141 additions and 248 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' },
+3 -2
View File
@@ -32,13 +32,14 @@ export const approve = data =>
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT }); request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
export const returnBill = data => request({ url: `${baseUrl}/return`, 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 voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时 // 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存 + 附件补传),实测可达 35s+,
// 低于后端实际耗时会导致前端先超时报错而后端推送成功(如 FJS2026111000001 实测 34.6s),覆盖全局 10s 超时
// config.meta.suppressErrorMessage=true 时由页面弹框汇总结果,避免拦截器重复 toast // config.meta.suppressErrorMessage=true 时由页面弹框汇总结果,避免拦截器重复 toast
export const syncKingdee = (id, config = {}) => export const syncKingdee = (id, config = {}) =>
request({ request({
url: `${baseUrl}/sync-kingdee`, url: `${baseUrl}/sync-kingdee`,
method: 'post', method: 'post',
timeout: 30000, timeout: 60000,
params: { id }, params: { id },
...config, ...config,
}); });
+123
View File
@@ -0,0 +1,123 @@
<template>
<teleport to="body">
<transition name="el-fade-in-linear">
<section v-if="modelValue" class="bottom-detail-panel" aria-label="详情面板">
<header class="bottom-detail-panel__header">
<h3>{{ title }}</h3>
<el-tooltip content="关闭" placement="top">
<el-button :icon="Close" text aria-label="关闭详情面板" @click="close" />
</el-tooltip>
</header>
<div class="bottom-detail-panel__body">
<slot />
</div>
<footer class="bottom-detail-panel__footer">
<el-button @click="close">关闭</el-button>
</footer>
</section>
</transition>
</teleport>
</template>
<script>
import { Close } from '@element-plus/icons-vue';
/**
* 底部弹出详情面板(表单页通用)
* 固定于视口底部并避让侧边栏,交互对齐应收/应付明细页点「单据号」弹出的单据详情面板
*/
export default {
name: 'BottomDetailPanel',
props: {
modelValue: { type: Boolean, default: false },
title: { type: String, default: '详情' },
},
emits: ['update:modelValue'],
data() {
return { Close };
},
watch: {
// 表单页多被 keep-alive 缓存,切换菜单时组件不销毁,需监听路由收起 teleport 到 body 的面板
$route() {
this.close();
},
},
methods: {
close() {
this.$emit('update:modelValue', false);
},
},
};
</script>
<style scoped lang="scss">
.bottom-detail-panel {
position: fixed;
z-index: 100;
bottom: 0;
left: $sidebar_width;
width: calc(100% - #{$sidebar_width});
max-height: min(60vh, 560px);
border-top: 1px solid #eff1f7;
background: #fff;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
}
:global(.avue--collapse) .bottom-detail-panel {
left: $sidebar_collapse;
width: calc(100% - #{$sidebar_collapse});
}
:global(.avue-layout--horizontal) .bottom-detail-panel {
left: 0;
width: 100%;
}
.bottom-detail-panel__header {
display: flex;
min-height: 48px;
align-items: center;
justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid #eff1f7;
h3 {
margin: 0;
padding-left: 12px;
border-left: 4px solid #409eff;
font-size: 16px;
font-weight: 600;
}
}
.bottom-detail-panel__body {
max-height: calc(min(60vh, 560px) - 106px);
padding: 16px 20px;
overflow: auto;
:deep(.el-table) {
--el-table-border-color: #eff1f7;
}
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
background: #fafafa;
}
}
.bottom-detail-panel__footer {
display: flex;
min-height: 57px;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 0 20px;
border-top: 1px solid #eff1f7;
}
@media screen and (max-width: 992px) {
.bottom-detail-panel {
left: 0;
width: 100%;
}
}
</style>
@@ -0,0 +1,160 @@
<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 ? '-' : transportTypeLabel(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="220" 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="remainLabel" width="110" align="center">
<template #default="{ row }">{{ formatMoney(row.beforeRemainAmounts[group.name]) }}</template>
</el-table-column>
<el-table-column :label="currentLabel" width="120" align="center">
<template #default="{ row }">{{ formatMoney(row.writeOffAmounts[group.name]) }}</template>
</el-table-column>
</el-table-column>
</el-table>
</div>
</template>
<script>
import request from '@/axios';
import { getDictionary } from '@/api/system/dictbiz';
/**
* 发票冲销明细公共表格(开票申请/收票登记/付款申请通用)
* 数据源:/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: [],
transportTypeOptions: [],
};
},
computed: {
// 费用项目分组子列文案随业务切换:开票为"未开票金额/本次开票金额",收票/付款为"未收票金额/本次收票金额"
remainLabel() {
return this.bizType === 'invoice_apply' ? '未开票金额' : '未收票金额';
},
currentLabel() {
return this.bizType === 'invoice_apply' ? '本次开票金额' : '本次收票金额';
},
feeItemGroups() {
const names = [];
this.rows.forEach(row => {
if (row.feeItemName && !names.includes(row.feeItemName)) names.push(row.feeItemName);
});
return names.map(name => ({ name }));
},
},
created() {
this.loadTransportTypeOptions();
},
watch: {
bizId: {
immediate: true,
handler(value) {
if (value) this.load();
},
},
},
methods: {
// 运输类型字典翻译(与开票申请明细表同口径 transport_type)
async loadTransportTypeOptions() {
const res = await getDictionary({ code: 'transport_type' });
const records = Array.isArray(res?.data?.data) ? res.data.data : res?.data?.data?.records || [];
this.transportTypeOptions = records.map(item => ({
label: item.dictValue || item.label || item.name,
value: item.dictKey || item.value || item.dictValue || item.name,
}));
},
transportTypeLabel(value) {
if (!value) return '-';
return this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value;
},
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>
@@ -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>
+90 -4
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,16 +347,27 @@
<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>
<section-card title="开票明细"> <section-card title="开票明细">
<template #extra>
<el-button v-if="recordId" type="primary" @click="writeOffPanelVisible = true"
>发票冲销</el-button
>
</template>
<el-table <el-table
ref="detailTable" ref="detailTable"
:data="detailRows" :data="detailRows"
@@ -385,6 +417,10 @@
</el-table> </el-table>
</section-card> </section-card>
<bottom-detail-panel v-model="writeOffPanelVisible" title="发票冲销明细">
<invoice-write-off-table biz-type="invoice_apply" :biz-id="recordId" />
</bottom-detail-panel>
<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>
@@ -488,9 +524,12 @@ import * as api from '@/api/payment/invoiceApplication';
import { getAll as getInvoiceItems } from '@/api/base/invoice-item'; import { getAll as getInvoiceItems } from '@/api/base/invoice-item';
import { getList as getWaybillList } from '@/api/business/waybill-manage'; import { getList as getWaybillList } from '@/api/business/waybill-manage';
import { getList as getReceivablePayableDetailList } from '@/api/settlement/receivable-payable-detail'; import { getList as getReceivablePayableDetailList } from '@/api/settlement/receivable-payable-detail';
import { getDetail as getProjectApplyDetail } from '@/api/business/project-apply';
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 BottomDetailPanel from '@/components/bottom-detail-panel/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,18 +577,23 @@ 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,
BottomDetailPanel,
}, },
data() { data() {
return { return {
form: emptyForm(), form: emptyForm(),
detailRows: [], detailRows: [],
selectedDetailIds: [], selectedDetailIds: [],
writeOffPanelVisible: false,
receiverInvoiceOptions: [], receiverInvoiceOptions: [],
contactOptions: [], contactOptions: [],
departmentEmailOptions: [], departmentEmailOptions: [],
@@ -621,6 +665,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 +794,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);
}, },
@@ -918,11 +987,27 @@ export default {
allocatedInvoiceAmount: 0, allocatedInvoiceAmount: 0,
})); }));
this.form.projectName = first.projectName; this.form.projectName = first.projectName;
this.form.projectId = first.projectId;
this.form.deptName = first.deptName; this.form.deptName = first.deptName;
this.form.issuerName = first.issuerName; this.form.issuerName = first.issuerName;
this.form.undertakingDeptName = first.deptName; this.form.undertakingDeptName = first.deptName;
this.settlementDialog.visible = false; this.settlementDialog.visible = false;
await Promise.all([this.loadSettlementDetails(), this.loadReceiverInformation()]); await Promise.all([
this.loadSettlementDetails(),
this.loadReceiverInformation(),
this.loadProjectProfitRate(),
]);
},
// 项目利润率:选结算单后带出项目立项利润率(百分数转小数存 form),已手填时不覆盖
async loadProjectProfitRate() {
if (!this.form.projectId || Number(this.form.profitRate) > 0) return;
try {
const data = this.unwrapData(await getProjectApplyDetail(this.form.projectId));
const rate = Number(data?.profitRate);
if (Number.isFinite(rate) && rate > 0) this.form.profitRate = rate / 100;
} catch {
// 带出失败不阻断选单,保存时后端仍会兜底取项目立项利润率
}
}, },
async loadSettlementDetails(resetSelection = true) { async loadSettlementDetails(resetSelection = true) {
this.detailRows = this.detailRows =
@@ -1173,6 +1258,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 => ({
+134 -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="openInvoiceDetailPanel">
查询明细
</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 />
@@ -152,6 +157,84 @@
</el-table> </el-table>
</section-card> </section-card>
<bottom-detail-panel v-model="invoiceDetailPanelVisible" title="发票信息">
<el-descriptions v-loading="invoiceDetailLoading" :column="3" border size="small">
<el-descriptions-item label="发票号码">{{
displayValue(invoicePoolDetail?.invoiceNo)
}}</el-descriptions-item>
<el-descriptions-item label="开票日期">{{
displayValue(invoicePoolDetail?.invoiceDate)
}}</el-descriptions-item>
<el-descriptions-item label="发票类型">{{
displayValue(invoicePoolDetail?.invoiceType)
}}</el-descriptions-item>
<el-descriptions-item label="税率">
{{ invoicePoolDetail?.taxRate != null ? `${invoicePoolDetail.taxRate}%` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="发票金额(价税合计)">{{
formatMoney(invoicePoolDetail?.invoiceAmount)
}}</el-descriptions-item>
<el-descriptions-item label="税额">{{
formatMoney(invoicePoolDetail?.taxAmount)
}}</el-descriptions-item>
<el-descriptions-item label="受票单位(购方)">{{
displayValue(invoicePoolDetail?.receiverName)
}}</el-descriptions-item>
<el-descriptions-item label="购方税号">{{
displayValue(invoicePoolDetail?.buyerTaxNo)
}}</el-descriptions-item>
<el-descriptions-item label="开票单位(销方)">{{
displayValue(invoicePoolDetail?.issuerName)
}}</el-descriptions-item>
<el-descriptions-item label="销方税号">{{
displayValue(invoicePoolDetail?.salerTaxNo)
}}</el-descriptions-item>
<el-descriptions-item label="销方开户行">{{
displayValue(invoicePoolDetail?.bankName)
}}</el-descriptions-item>
<el-descriptions-item label="销方银行账号">{{
displayValue(invoicePoolDetail?.bankAccount)
}}</el-descriptions-item>
<el-descriptions-item label="销方电话">{{
displayValue(invoicePoolDetail?.phone)
}}</el-descriptions-item>
<el-descriptions-item label="查验状态">{{
checkStatusText(invoicePoolDetail?.checkStatus)
}}</el-descriptions-item>
<el-descriptions-item label="金蝶单据号">{{
displayValue(invoicePoolDetail?.kingdeeBillNo)
}}</el-descriptions-item>
</el-descriptions>
<div class="receipt-form-page__invoice-items-title">发票明细行</div>
<el-table v-loading="invoiceDetailLoading" :data="invoiceItems" border empty-text="暂无发票明细行">
<el-table-column prop="seq" label="分录行号" width="90" align="center" />
<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="unit" label="单位" min-width="80" align="center" />
<el-table-column label="数量" min-width="100" align="right">
<template #default="{ row }">{{ formatQuantity(row.num) }}</template>
</el-table-column>
<el-table-column label="单价" min-width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.unitPrice) }}</template>
</el-table-column>
<el-table-column label="金额" min-width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.detailAmount) }}</template>
</el-table-column>
<el-table-column label="税率" min-width="90" align="center">
<template #default="{ row }">{{ formatRateText(row.taxRate) }}</template>
</el-table-column>
<el-table-column label="税额" min-width="110" align="right">
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
</el-table-column>
<el-table-column prop="goodsCode" label="税收分类编码" min-width="140" show-overflow-tooltip />
<el-table-column prop="discountType" label="折扣类型" min-width="100" align="center" />
<el-table-column prop="preferentialPolicy" label="优惠政策" min-width="120" show-overflow-tooltip />
<el-table-column prop="vatException" label="增值税特殊事项" min-width="130" show-overflow-tooltip />
<el-table-column prop="zeroTaxRateFlag" label="零税率标识" min-width="100" align="center" />
<el-table-column prop="versionNo" label="版本号" min-width="90" align="center" />
</el-table>
</bottom-detail-panel>
<section-card title="结算信息"> <section-card title="结算信息">
<el-table :data="form.settlements" border empty-text="暂无认领数据(认领关系由付款结算单侧维护)"> <el-table :data="form.settlements" border empty-text="暂无认领数据(认领关系由付款结算单侧维护)">
<el-table-column type="index" label="序号" width="64" align="center" /> <el-table-column type="index" label="序号" width="64" align="center" />
@@ -179,6 +262,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 +363,8 @@ 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 BottomDetailPanel from '@/components/bottom-detail-panel/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 +404,8 @@ export default {
components: { components: {
KingdeeInvoicePoolDialog, KingdeeInvoicePoolDialog,
VehicleAttachmentTable, VehicleAttachmentTable,
InvoiceWriteOffTable,
BottomDetailPanel,
}, },
data() { data() {
return { return {
@@ -323,6 +414,9 @@ export default {
customerEmailOptions: [], customerEmailOptions: [],
departmentEmailOptions: [], departmentEmailOptions: [],
invoiceItems: [], invoiceItems: [],
invoiceDetailLoading: false,
invoiceDetailPanelVisible: false,
invoicePoolDetail: null,
contractDialog: { contractDialog: {
visible: false, visible: false,
loading: false, loading: false,
@@ -504,6 +598,37 @@ export default {
this.invoiceItems = []; this.invoiceItems = [];
} }
}, },
// 查询明细:底部弹窗展示金蝶发票全量信息(头部票池镜像 + 全部明细行),历史单据无缓存时回源金蝶
async openInvoiceDetailPanel() {
if (!this.form.invoiceNo) return;
this.invoiceDetailPanelVisible = true;
this.invoiceDetailLoading = true;
try {
const [poolResult, itemsResult] = await Promise.allSettled([
api.queryInvoicePool({ current: 1, size: 1, invoiceNo: this.form.invoiceNo }),
api.invoiceDetailQuery(this.form.invoiceNo),
]);
if (poolResult.status === 'fulfilled') {
const records = this.unwrapData(poolResult.value)?.records || [];
this.invoicePoolDetail =
records.find(item => item.invoiceNo === this.form.invoiceNo) || records[0] || null;
} else {
this.invoicePoolDetail = null;
}
if (itemsResult.status === 'fulfilled') {
this.invoiceItems = this.unwrapData(itemsResult.value) || [];
if (!this.invoiceItems.length) this.$message.info('未查询到该发票的明细行');
} else {
this.$message.error('查询金蝶发票明细失败');
}
} finally {
this.invoiceDetailLoading = false;
}
},
checkStatusText(value) {
const map = { 0: '未查验', 1: '查验通过', 2: '查验不通过' };
return map[value] ?? this.displayValue(value);
},
// 所属合同=承运商合同:选择后带出承办部门(结算组织=承办部门对应平台公司金蝶编码,后端派生) // 所属合同=承运商合同:选择后带出承办部门(结算组织=承办部门对应平台公司金蝶编码,后端派生)
openContractDialog() { openContractDialog() {
this.contractDialog.visible = true; this.contractDialog.visible = true;
@@ -625,6 +750,9 @@ export default {
goBack() { goBack() {
this.$router.push('/payment/invoice-receipt'); this.$router.push('/payment/invoice-receipt');
}, },
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMoney(value) { formatMoney(value) {
return Number(value || 0).toFixed(2); return Number(value || 0).toFixed(2);
}, },
@@ -660,6 +788,11 @@ export default {
display: block; display: block;
width: 100%; width: 100%;
} }
.receipt-form-page__invoice-items-title {
margin: 16px 0 8px;
font-size: 14px;
font-weight: 600;
}
.receipt-form-page__remark :deep(.el-form-item__content) { .receipt-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important; margin-left: 0 !important;
} }
+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>
+103 -46
View File
@@ -290,6 +290,11 @@
</el-table> </el-table>
</section-card> </section-card>
<section-card v-if="showInvoiceSection" title="发票信息"> <section-card v-if="showInvoiceSection" title="发票信息">
<template #extra>
<el-button v-if="recordId" type="primary" @click="writeOffPanelVisible = true"
>发票冲销</el-button
>
</template>
<el-table :data="form.invoices" border> <el-table :data="form.invoices" border>
<el-table-column type="index" label="序号" width="70" /> <el-table-column type="index" label="序号" width="70" />
<el-table-column prop="settlementNo" label="结算单号" min-width="120"> <el-table-column prop="settlementNo" label="结算单号" min-width="120">
@@ -316,6 +321,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 +355,9 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</section-card> </section-card>
<bottom-detail-panel v-model="writeOffPanelVisible" title="发票冲销明细">
<invoice-write-off-table biz-type="payment_receipt" :biz-id="recordId" />
</bottom-detail-panel>
<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 +369,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 +760,16 @@ 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';
import BottomDetailPanel from '@/components/bottom-detail-panel/main.vue';
const attachmentViewerPlugins = [ const attachmentViewerPlugins = [
imagePlugin(), imagePlugin(),
@@ -817,12 +846,15 @@ export default {
ElImageViewer, ElImageViewer,
OpenFileViewer, OpenFileViewer,
PdfPreview, PdfPreview,
InvoiceWriteOffTable,
BottomDetailPanel,
}, },
data() { data() {
return { return {
form: emptyForm(), form: emptyForm(),
amountSyncing: false, amountSyncing: false,
transferPayload: null, transferPayload: null,
writeOffPanelVisible: false,
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫) // 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
kingdeeAttachmentBusy: false, kingdeeAttachmentBusy: false,
referenceRows: [], referenceRows: [],
@@ -1899,55 +1931,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();
@@ -450,7 +450,7 @@
> >
<el-button v-else type="primary" @click="openInvoiceClaimDialog">认领查询</el-button> <el-button v-else type="primary" @click="openInvoiceClaimDialog">认领查询</el-button>
</div> </div>
<el-table :data="invoices" border> <el-table :data="invoiceRows" border>
<el-table-column type="index" label="序号" width="64" align="center" /> <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="invoiceNo" label="发票号" min-width="150" align="center" />
<el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center"> <el-table-column prop="invoiceDate" label="开票日期" min-width="130" align="center">
@@ -526,13 +526,13 @@
<el-link <el-link
type="primary" type="primary"
:disabled="kingdeeAttachmentBusy" :disabled="kingdeeAttachmentBusy"
@click="viewInvoiceAttachment(row)" @click="viewInvoiceFile(row)"
>查看</el-link >查看</el-link
> >
<el-link <el-link
type="primary" type="primary"
:disabled="kingdeeAttachmentBusy" :disabled="kingdeeAttachmentBusy"
@click="downloadInvoiceAttachment(row)" @click="downloadInvoiceFile(row)"
>下载</el-link >下载</el-link
> >
<!-- 查看页重推入口:付款结算单已同步金蝶且认领票未指定应付(failed/pushed) --> <!-- 查看页重推入口:付款结算单已同步金蝶且认领票未指定应付(failed/pushed) -->
@@ -543,35 +543,13 @@
@click="repushInvoice(row)" @click="repushInvoice(row)"
>重新推送</el-link >重新推送</el-link
> >
<el-link v-if="editable" type="danger" @click="removeInvoice($index)">删除</el-link> <el-link v-if="editable && !row.issued" type="danger" @click="removeInvoice($index)"
>删除</el-link
>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </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> </section-card>
<kingdee-invoice-pool-dialog <kingdee-invoice-pool-dialog
@@ -1123,6 +1101,7 @@ import {
sourceColumns, sourceColumns,
summaryColumns, summaryColumns,
} from '@/option/settlement/formalSettlementTable'; } from '@/option/settlement/formalSettlementTable';
import { invoiceTypeOptions } from '@/api/payment/invoiceApplication';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { h } from 'vue'; import { h } from 'vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
@@ -1133,12 +1112,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';
@@ -1328,6 +1303,24 @@ export default {
editable() { editable() {
return !this.readonly; return !this.readonly;
}, },
// 发票信息合并展示:结算单发票行在前 + 金蝶回写开票发票行在后(issued 标记,只读不保存);
// 回写行恒在尾部,保证下方 $index 删除操作与 this.invoices 索引一致;
// 回写行票种按开票申请字典翻译;可匹配金额=票面价税合计,匹配结算单金额=后端票级冲抵明细汇总
invoiceRows() {
return [
...this.invoices,
...this.issuedInvoices.map(row => ({
...row,
issued: true,
invoiceType:
invoiceTypeOptions.find(item => item.value === row.invoiceType)?.label ||
row.invoiceType ||
'',
availableInvoiceAmount: Number(row.invoiceAmount || 0),
matchedAmount: Number(row.matchedAmount || 0),
})),
];
},
// 查看页面「同步」按钮:仅已审批且未同步金蝶(含推送失败)的单据显示,口径与后端推送闸门一致 // 查看页面「同步」按钮:仅已审批且未同步金蝶(含推送失败)的单据显示,口径与后端推送闸门一致
canSync() { canSync() {
return ( return (
@@ -2706,9 +2699,10 @@ export default {
openInvoiceReceiptDialog() { openInvoiceReceiptDialog() {
this.invoiceReceiptDialog.visible = true; this.invoiceReceiptDialog.visible = true;
}, },
// 查看页重推入口展示条件:只读 + 付款结算单 + 审批通过 + 金蝶应付单已同步 + 该票未指定应付(failed/pushed) // 查看页重推入口展示条件:只读 + 付款结算单 + 审批通过 + 金蝶应付单已同步 + 该票未指定应付(failed/pushed);金蝶回写行不参与
canRepushInvoice(row) { canRepushInvoice(row) {
return ( return (
!row.issued &&
!this.editable && !this.editable &&
this.form.settlementType !== 'receivable' && this.form.settlementType !== 'receivable' &&
this.form.approvalStatus === 'approved' && this.form.approvalStatus === 'approved' &&
@@ -2918,38 +2912,52 @@ 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)); viewInvoiceFile(row) {
return row.issued ? this.viewIssuedInvoice(row) : this.viewInvoiceAttachment(row);
},
downloadInvoiceFile(row) {
return row.issued ? this.downloadIssuedInvoice(row) : this.downloadInvoiceAttachment(row);
}, },
// 开票发票预览地址:数电PDF优先(OFD浏览器无法预览),退预览图、板式文件 // 开票发票预览地址:数电PDF优先(OFD浏览器无法预览),退预览图、板式文件
issuedInvoicePreviewUrl(row) { issuedInvoicePreviewUrl(row) {
@@ -3588,14 +3596,6 @@ export default {
justify-content: flex-end; justify-content: flex-end;
margin-bottom: 12px; margin-bottom: 12px;
} }
.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 { .formal-editor__negative-amount {
color: var(--el-color-danger); color: var(--el-color-danger);
} }
@@ -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;