This commit is contained in:
2026-08-28 15:30:24 +08:00
parent 17bbde81b2
commit 9a4e0f7c03
16 changed files with 1016 additions and 183 deletions
@@ -250,7 +250,16 @@ export default {
},
batchDownload() {
const rows = this.selectedRows.length ? this.selectedRows : this.rows;
rows.forEach(this.downloadAttachment);
const downloadableRows = rows.filter(row => this.attachmentUrl(row));
if (!downloadableRows.length) {
this.$message.warning(
this.selectedRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
);
return;
}
downloadableRows.forEach((row, index) => {
window.setTimeout(() => this.downloadAttachment(row), index * 500);
});
},
previewAttachment(row) {
const url = this.attachmentUrl(row);
+1 -1
View File
@@ -1,5 +1,5 @@
export const billLedgerTableColumns = [
{ prop: 'billNo', label: '票据编号', minWidth: 170 },
{ prop: 'billNo', label: '票据编号', minWidth: 170, link: true },
{ prop: 'receiverName', label: '收票单位', minWidth: 170 },
{ prop: 'issuerName', label: '出票单位', minWidth: 170 },
{ prop: 'billTypeName', label: '汇票类型', minWidth: 110 },
+1 -1
View File
@@ -1,6 +1,6 @@
export const invoiceApplicationTableColumns = [
{ prop: 'applicationNo', label: '单据号', minWidth: 170, link: true },
{ prop: 'settlementNos', label: '结算单号', minWidth: 190, link: true },
{ prop: 'settlementNos', label: '结算单号', minWidth: 190 },
{ prop: 'projectName', label: '所属项目', minWidth: 140 },
{ prop: 'deptName', label: '所属组织', minWidth: 140 },
{ prop: 'issuerName', label: '开票方', minWidth: 150 },
+11 -4
View File
@@ -211,7 +211,7 @@
</el-form-item>
</section-card>
<section-card title="使用记录">
<section-card v-if="readonly" title="使用记录">
<el-table :data="usageRecords" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="applicationNo" label="申请单号" min-width="180" align="center" />
@@ -311,7 +311,7 @@ export default {
billType: [{ required: true, message: '请选择汇票类型', trigger: 'change' }],
faceAmount: [{ validator: this.validateFaceAmount, trigger: 'change' }],
issueDate: [{ required: true, message: '请选择出票日期', trigger: 'change' }],
maturityDate: [{ validator: this.validateMaturityDate, trigger: 'change' }],
maturityDate: [{ required: true, validator: this.validateMaturityDate, trigger: 'change' }],
availableDeptIds: [{ validator: this.validateDepartments, trigger: 'change' }],
feeBearerId: [{ required: true, message: '请选择费用承担方', trigger: 'change' }],
confirmedDiscountRate: [{ validator: this.validateRate, trigger: 'change' }],
@@ -359,6 +359,8 @@ export default {
this.form = {
...emptyForm(),
...data,
confirmedDiscountRate: this.normalizeDiscountRate(data.confirmedDiscountRate),
bankDiscountReferenceRate: this.normalizeDiscountRate(data.bankDiscountReferenceRate),
availableDeptIds: this.parse(data.availableDeptIdsJson),
attachments: this.parse(data.attachmentsJson),
};
@@ -461,6 +463,11 @@ export default {
}
callback();
},
normalizeDiscountRate(value) {
return value === null || value === undefined || value === '' || Number(value) === -1
? null
: value;
},
normalizeAttachments(files) {
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
const time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
@@ -492,9 +499,9 @@ export default {
availableDeptIdsJson: JSON.stringify(this.form.availableDeptIds || []),
availableDeptNames: this.form.availableDeptNames,
feeBearerId: this.form.feeBearerId,
confirmedDiscountRate: this.form.confirmedDiscountRate,
confirmedDiscountRate: this.normalizeDiscountRate(this.form.confirmedDiscountRate),
issuingBank: this.form.issuingBank,
bankDiscountReferenceRate: this.form.bankDiscountReferenceRate,
bankDiscountReferenceRate: this.normalizeDiscountRate(this.form.bankDiscountReferenceRate),
attachmentsJson: JSON.stringify(this.form.attachments || []),
remark: this.form.remark,
};
+119
View File
@@ -12,16 +12,89 @@
:loading="loading"
:page="page"
@add="openCreate"
@view="openView"
@edit="openEdit"
@delete="handleDelete"
@page-change="handlePageChange"
@size-change="handleSizeChange"
/>
<el-dialog
v-model="detailDialog.visible"
title="汇票台账详情"
width="86%"
append-to-body
destroy-on-close
>
<div v-loading="detailDialog.loading" class="bill-ledger-detail">
<section-card title="基本信息">
<el-descriptions :column="3" border>
<el-descriptions-item label="票据编号">{{
detailValue('billNo')
}}</el-descriptions-item>
<el-descriptions-item label="汇票类型">{{
detailValue('billTypeName', 'billType')
}}</el-descriptions-item>
<el-descriptions-item label="出票单位">{{
detailValue('issuerName')
}}</el-descriptions-item>
<el-descriptions-item label="收票单位">{{
detailValue('receiverName')
}}</el-descriptions-item>
<el-descriptions-item label="票面金额">{{
formatMoney(detailDialog.row.faceAmount)
}}</el-descriptions-item>
<el-descriptions-item label="可用余额">{{
formatMoney(detailDialog.row.availableBalance)
}}</el-descriptions-item>
<el-descriptions-item label="出票日期">{{
detailValue('issueDate')
}}</el-descriptions-item>
<el-descriptions-item label="到期日期">{{
detailValue('maturityDate')
}}</el-descriptions-item>
<el-descriptions-item label="出票行">{{
detailValue('issuingBank')
}}</el-descriptions-item>
<el-descriptions-item label="可用部门">{{
detailValue('availableDeptNames')
}}</el-descriptions-item>
<el-descriptions-item label="费用承担方">{{
detailValue('feeBearerName')
}}</el-descriptions-item>
<el-descriptions-item label="银行贴现参考率">
{{ formatRate(detailDialog.row.bankDiscountReferenceRate) }}
</el-descriptions-item>
<el-descriptions-item label="双方确认贴现率">
{{ formatRate(detailDialog.row.confirmedDiscountRate) }}
</el-descriptions-item>
<el-descriptions-item label="预计贴现费用">
{{ formatMoney(estimatedDiscountFee()) }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="3">
{{ detailValue('remark') }}
</el-descriptions-item>
</el-descriptions>
</section-card>
<section-card title="使用记录">
<el-table :data="detailDialog.usageRecords" border>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="applicationNo" label="申请单号" min-width="180" align="center" />
<el-table-column label="使用金额" min-width="150" align="right">
<template #default="{ row }">{{ formatMoney(row.usedAmount) }}</template>
</el-table-column>
<el-table-column prop="useDeptName" label="使用部门" min-width="180" align="center" />
<el-table-column prop="statusName" label="状态" min-width="120" align="center" />
</el-table>
<el-empty v-if="!detailDialog.usageRecords.length" description="暂无使用记录" />
</section-card>
</div>
</el-dialog>
</basic-container>
</template>
<script>
import * as api from '@/api/payment/billLedger';
import * as billPaymentApi from '@/api/payment/billPayment';
import BillLedgerSearch from './components/bill-ledger-search.vue';
import BillLedgerTable from './components/bill-ledger-table.vue';
@@ -45,6 +118,7 @@ export default {
rows: [],
loading: false,
page: { current: 1, size: 10, total: 0 },
detailDialog: { visible: false, loading: false, row: {}, usageRecords: [] },
};
},
mounted() {
@@ -89,6 +163,31 @@ export default {
openCreate() {
this.$router.push({ path: '/payment/bill-ledger/form', query: { mode: 'add' } });
},
async openView(row) {
this.detailDialog = { visible: true, loading: true, row: { ...row }, usageRecords: [] };
try {
const [detailResponse, paymentResponse] = await Promise.all([
api.getDetail(row.id),
billPaymentApi.getList(1, 1000, { billLedgerId: row.id }),
]);
const detail = this.unwrapData(detailResponse) || {};
const paymentData = this.unwrapData(paymentResponse) || {};
const paymentRows = paymentData.records || (Array.isArray(paymentData) ? paymentData : []);
const sourceRecords = paymentRows.length ? paymentRows : detail.usageRecords || [];
this.detailDialog.row = { ...row, ...detail };
this.detailDialog.usageRecords = sourceRecords.map(item => ({
...item,
applicationNo: item.applicationNo || item.paymentNo || '-',
usedAmount: Number(item.usedAmount || 0),
useDeptName: item.useDeptName || item.deptName || '-',
statusName: item.statusName || item.approvalStatusName || '已审核',
}));
} catch (error) {
this.$message.error('票据详情加载失败,请稍后重试');
} finally {
this.detailDialog.loading = false;
}
},
openEdit(row) {
this.$router.push({
path: '/payment/bill-ledger/form',
@@ -111,6 +210,26 @@ export default {
this.page.size = size;
this.loadData();
},
detailValue(...fields) {
const value = fields
.map(field => this.detailDialog.row?.[field])
.find(item => item !== undefined && item !== null && item !== '');
return value === undefined ? '-' : value;
},
formatMoney(value) {
return Number(value || 0).toFixed(2);
},
formatRate(value) {
if (Number(value) === -1) return '';
return value === null || value === undefined || value === '' ? '-' : `${Number(value)}%`;
},
estimatedDiscountFee() {
return (
(Number(this.detailDialog.row.faceAmount || 0) *
Number(this.detailDialog.row.confirmedDiscountRate || 0)) /
100
);
},
},
};
</script>
@@ -15,7 +15,10 @@
show-overflow-tooltip
>
<template #default="{ row }">
<span v-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<el-link v-if="column.link" type="primary" @click="$emit('view', row)">{{
displayValue(row[column.prop])
}}</el-link>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
</el-table-column>
@@ -65,7 +68,7 @@ export default {
loading: { type: Boolean, default: false },
page: { type: Object, required: true },
},
emits: ['add', 'edit', 'delete', 'page-change', 'size-change'],
emits: ['add', 'view', 'edit', 'delete', 'page-change', 'size-change'],
data() {
return { columns: billLedgerTableColumns };
},
+17 -6
View File
@@ -192,12 +192,6 @@
<div v-for="(sheet, sheetIndex) in form.sheets" :key="sheet.localId" class="invoice-sheet">
<div class="invoice-sheet__header">
<span>第{{ sheetIndex + 1 }}张</span>
<el-link
v-if="!readonly && form.sheets.length > 1"
type="danger"
@click="removeSheet(sheetIndex)"
>删除</el-link
>
</div>
<el-table :data="sheet.lines" border>
<el-table-column type="index" label="序号" width="64" align="center" />
@@ -314,6 +308,9 @@
</template>
</el-table-column>
</el-table>
<div v-if="!readonly && form.sheets.length > 1" class="invoice-sheet__actions">
<el-link type="danger" @click="removeSheet(sheetIndex)">删除</el-link>
</div>
</div>
</section-card>
@@ -1080,6 +1077,9 @@ export default {
</script>
<style scoped lang="scss">
.invoice-form-page__form {
padding-bottom: 72px;
}
.invoice-form-page__form :deep(.el-select),
.invoice-form-page__form :deep(.el-input-number) {
width: 100%;
@@ -1095,6 +1095,17 @@ export default {
background: #fafafa;
font-weight: 600;
}
.invoice-sheet__actions {
display: flex;
justify-content: flex-end;
padding: 8px 12px 0;
}
.invoice-form-page__links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.invoice-form-page__remark :deep(.el-form-item__content) {
margin-left: 0 !important;
}
+58 -2
View File
@@ -84,10 +84,21 @@
:key="column.prop"
v-bind="column"
align="center"
show-overflow-tooltip
:show-overflow-tooltip="column.prop !== 'settlementNos'"
>
<template #default="{ row }">
<el-link v-if="column.link && row[column.prop]" type="primary" @click="openView(row)">
<div v-if="column.prop === 'settlementNos'" class="invoice-page__settlement-nos">
<span
v-for="settlementNo in settlementNumberList(row.settlementNos)"
:key="settlementNo"
class="invoice-page__settlement-no"
@click.stop="openSettlementDetail(settlementNo)"
>
{{ settlementNo }}
</span>
<span v-if="!settlementNumberList(row.settlementNos).length">-</span>
</div>
<el-link v-else-if="column.link && row[column.prop]" type="primary" @click="openView(row)">
{{ row[column.prop] }}
</el-link>
<el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)" class="status-text">
@@ -221,6 +232,7 @@ import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as XLSX from 'xlsx';
import * as api from '@/api/payment/invoiceApplication';
import * as formalSettlementApi from '@/api/settlement/formalSettlement';
import { invoiceApplicationTableColumns } from '@/option/payment/invoiceApplication';
const emptyQuery = () => ({
@@ -303,6 +315,34 @@ export default {
query: { mode: 'view', id: row.id },
});
},
settlementNumberList(value) {
return String(value || '')
.split(/[、,]/)
.map(item => item.trim())
.filter(Boolean);
},
async openSettlementDetail(settlementNo) {
const normalizedNo = String(settlementNo || '').trim();
if (!normalizedNo) return;
try {
const data = this.unwrapData(
await formalSettlementApi.getList(1, 10, { formalSettlementNo: normalizedNo })
);
const row = (data.records || []).find(
item => String(item.formalSettlementNo || '').trim() === normalizedNo
);
if (!row?.id) {
this.$message.warning('未找到对应的正式结算单');
return;
}
this.$router.push({
path: '/settlement/formal-settlement/form',
query: { mode: 'view', id: row.id, name: '查看正式结算' },
});
} catch (error) {
this.$message.error('正式结算单详情加载失败');
}
},
async openFlow(row) {
this.flowDialog.row = { ...row };
this.flowDialog.records = [];
@@ -454,6 +494,22 @@ export default {
flex-wrap: wrap;
justify-content: center;
}
.invoice-page__settlement-nos {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px 8px;
white-space: normal;
}
.invoice-page__settlement-no {
color: #409eff;
cursor: pointer;
white-space: normal;
word-break: break-all;
}
.invoice-page__settlement-no:hover {
text-decoration: underline;
}
.invoice-page__pagination {
display: flex;
justify-content: flex-end;
+47 -10
View File
@@ -137,17 +137,15 @@
<div class="receipt-form-page__actions">
<el-button @click="goBack">取消</el-button>
<el-button v-if="!readonly" type="primary" plain @click="syncReferenceData">同步</el-button>
<el-button
v-if="!readonly && canSave"
type="primary"
plain
@click="saveDraft"
>保存</el-button>
<el-button v-if="!readonly && canSave" type="primary" plain @click="saveDraft"
>保存</el-button
>
<el-button
v-if="!readonly && canSave && hasPermission('invoice_receipt_submit')"
type="primary"
@click="submitForm"
>提交</el-button>
>提交</el-button
>
</div>
</el-form>
@@ -251,7 +249,7 @@ import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceReceipt';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
const invoicePoolMockRows = [
const invoicePoolMockTemplates = [
{
id: 90000001,
invoiceNo: '25312000000000000101',
@@ -311,6 +309,39 @@ const invoicePoolMockRows = [
},
];
const randomDigits = length =>
Array.from({ length }, () => Math.floor(Math.random() * 10)).join('');
const formatMockDate = date => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const createInvoicePoolMockRows = () =>
invoicePoolMockTemplates.map((template, index) => {
const invoiceDateValue = new Date();
invoiceDateValue.setDate(invoiceDateValue.getDate() - Math.floor(Math.random() * 15));
const invoiceDate = formatMockDate(invoiceDateValue);
const invoiceAmount = Math.floor(10000 + Math.random() * 190000);
const taxAmount = Number(
(
(invoiceAmount * Number(template.taxRate || 0)) /
(100 + Number(template.taxRate || 0))
).toFixed(2)
);
return {
...template,
id: Date.now() + index * 100 + Math.floor(Math.random() * 100),
invoiceNo: `25${randomDigits(18)}`,
invoiceDate,
invoiceAmount,
taxAmount,
kingdeeBillNo: `KD-FP-${invoiceDate.replaceAll('-', '')}${randomDigits(3)}`,
};
});
const emptyForm = () => ({
id: null,
kingdeeInvoicePoolId: null,
@@ -348,6 +379,7 @@ export default {
return {
Search,
form: emptyForm(),
invoicePoolMockRows: [],
customerEmailOptions: [],
departmentEmailOptions: [],
invoiceDialog: { visible: false, loading: false, keyword: '', rows: [], current: null },
@@ -415,6 +447,7 @@ export default {
.filter(Boolean);
},
async initialize() {
this.invoicePoolMockRows = createInvoicePoolMockRows();
const userInfo = this.$store.getters.userInfo || {};
const userEmail = userInfo.email || '';
this.departmentEmailOptions = userEmail ? [userEmail] : [];
@@ -466,6 +499,7 @@ export default {
async openInvoiceDialog() {
this.invoiceDialog.visible = true;
this.invoiceDialog.keyword = this.form.invoiceNo || '';
if (!this.recordId) this.invoicePoolMockRows = createInvoicePoolMockRows();
await this.loadInvoicePool();
},
async loadInvoicePool() {
@@ -485,8 +519,8 @@ export default {
const normalizedKeyword = String(keyword || '')
.trim()
.toLowerCase();
if (!normalizedKeyword) return invoicePoolMockRows.map(item => ({ ...item }));
return invoicePoolMockRows
if (!normalizedKeyword) return this.invoicePoolMockRows.map(item => ({ ...item }));
return this.invoicePoolMockRows
.filter(item =>
[item.invoiceNo, item.issuerName, item.receiverName].some(value =>
String(value || '')
@@ -705,6 +739,9 @@ export default {
</script>
<style scoped lang="scss">
.receipt-form-page__form {
padding-bottom: 72px;
}
.receipt-form-page__form :deep(.el-form-item) {
margin-bottom: 14px;
}
+409 -68
View File
@@ -41,30 +41,61 @@
></el-form-item
></el-col
>
<el-col :span="6"
><el-form-item label="所属项目"
><el-input v-model="form.projectName" disabled
><template v-if="form.paymentType === 'project_advance'" #append
><el-button :disabled="readonly" @click="openReference">选择</el-button></template
></el-input
></el-form-item
></el-col
>
<el-col :span="6"
><el-form-item label="合同名称"
><el-input v-model="form.contractName" disabled /></el-form-item
></el-col>
<el-col :span="6"
<el-col :span="6">
<el-form-item label="所属项目">
<el-select
v-if="form.paymentType === 'project_advance'"
v-model="form.projectId"
:disabled="readonly"
filterable
clearable
:loading="projectLoading"
placeholder="请选择项目"
@change="handleProjectChange"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="item.projectName || item.projectShortName || item.projectCode"
:value="item.id"
/>
</el-select>
<el-input v-else v-model="form.projectName" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="合同名称">
<el-select
v-if="form.paymentType === 'project_advance'"
v-model="form.contractId"
:disabled="readonly || !form.projectId"
filterable
clearable
:loading="contractLoading"
placeholder="请先选择项目"
@change="handleAdvanceContractChange"
>
<el-option
v-for="item in contractOptions"
:key="item.id"
:label="item.contractName || item.contractNo"
:value="item.id"
/>
</el-select>
<el-input v-else v-model="form.contractName" disabled />
</el-form-item>
</el-col>
<el-col v-if="form.paymentType !== 'project_advance'" :span="6"
><el-form-item label="结算金额" prop="settlementAmount"
><el-input-number
v-model="form.settlementAmount"
:disabled="readonly || form.paymentType !== 'project_advance'"
:disabled="readonly"
:min="0"
:precision="2"
:controls="false"
style="width: 100%" /></el-form-item
></el-col>
<el-col :span="6"
<el-col v-if="form.paymentType !== 'project_advance'" :span="6"
><el-form-item label="可付款金额"
><el-input v-model="form.payableAmount" disabled /></el-form-item
></el-col>
@@ -73,7 +104,7 @@
><el-input v-model="form.billType" disabled /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="付款比例" prop="paymentRatio"
><el-form-item label="付款比例%" prop="paymentRatio"
><div class="payment-form-page__percentage-input">
<el-input-number
v-model="form.paymentRatio"
@@ -108,7 +139,7 @@
v-bind="item" /></el-select></el-form-item
></el-col>
<el-col v-if="billPayment" :span="6"
><el-form-item label="汇票票据" prop="billLedgerId"
><el-form-item label="汇票单号" prop="billLedgerId"
><el-select
v-model="form.billLedgerId"
:disabled="readonly"
@@ -184,7 +215,7 @@
label="可付款金额"
min-width="130" /><el-table-column
prop="paymentRatio"
label="付款比例"
label="付款比例%"
min-width="110" /><el-table-column
prop="appliedAmount"
label="付款金额"
@@ -395,8 +426,9 @@
v-loading="formalPage.loading"
:data="formalRows"
border
@row-click="selectFormal"
@selection-change="formalSelection = $event"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="formalSettlementNo" label="结算单号" />
<el-table-column prop="projectName" label="所属项目" />
<el-table-column prop="contractName" label="合同名称" />
@@ -407,6 +439,9 @@
</template>
</el-table-column>
</el-table>
<div class="payment-form-page__reference-confirm">
<el-button type="primary" :disabled="!formalSelection.length" @click="confirmFormalSelection">确定</el-button>
</div>
<div class="payment-form-page__reference-pagination">
<el-pagination
v-model:current-page="formalPage.current"
@@ -420,7 +455,8 @@
</div>
</el-tab-pane>
<el-tab-pane label="预结算单" name="pre">
<el-table v-loading="prePage.loading" :data="preRows" border @row-click="selectPre">
<el-table v-loading="prePage.loading" :data="preRows" border @selection-change="preSelection = $event">
<el-table-column type="selection" width="50" />
<el-table-column prop="preSettlementNo" label="结算单号" />
<el-table-column prop="projectName" label="所属项目" />
<el-table-column prop="contractName" label="合同名称" />
@@ -442,6 +478,9 @@
@size-change="handlePreSizeChange"
/>
</div>
<div class="payment-form-page__reference-confirm">
<el-button type="primary" :disabled="!preSelection.length" @click="confirmPreSelection">确定</el-button>
</div>
</el-tab-pane>
</el-tabs></div
></el-dialog>
@@ -496,6 +535,7 @@ import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/paymentApplication';
import * as billLedgerApi from '@/api/payment/billLedger';
import { getList as getProjectList } from '@/api/business/project-apply';
import { getDetail as getContractDetail } from '@/api/business/contract-manage';
import * as formalApi from '@/api/settlement/formalSettlement';
import * as preApi from '@/api/settlement/preSettlement';
@@ -521,7 +561,7 @@ const ATTACHMENT_MATERIALS = {
contract: {
key: 'contract',
label: '合同签章文件',
keywords: ['合同签章文件', '合同签章', '签章'],
keywords: ['合同签章文件', '合同签章', '签章', '合同'],
},
specialApproval: {
key: 'specialApproval',
@@ -543,6 +583,7 @@ const emptyForm = () => ({
paymentNo: '',
paymentType: 'project_advance',
settlementId: null,
settlementIds: [],
preSettlementId: null,
preSettlementIds: [],
projectId: null,
@@ -594,7 +635,9 @@ export default {
contractName: '',
},
formalRows: [],
formalSelection: [],
preRows: [],
preSelection: [],
formalPage: {
current: 1,
size: 10,
@@ -607,6 +650,10 @@ export default {
total: 0,
loading: false,
},
projectOptions: [],
projectLoading: false,
contractOptions: [],
contractLoading: false,
contractRows: [],
billOptions: [],
billLoading: false,
@@ -674,13 +721,11 @@ export default {
return this.hasPermission(code);
},
billPayment() {
return ['bank_draft', 'commercial_draft'].includes(this.form.paymentMethod);
return this.form.paymentMethod !== 'bank_transfer';
},
paymentAmountBase() {
const amount =
this.form.paymentType === 'project_advance'
? this.form.payableAmount
: this.form.settlementAmount;
if (this.form.paymentType === 'project_advance') return 0;
const amount = this.form.settlementAmount;
return Number(amount || 0);
},
referenceLabel() {
@@ -697,12 +742,15 @@ export default {
return this.referenceRows.map(item => ({
...item,
paymentRatio: this.form.paymentRatio,
appliedAmount: Number(
(
(Number(item.settlementAmount || 0) * Number(this.form.paymentRatio || 0)) /
100
).toFixed(2)
),
appliedAmount:
item.fixedAppliedAmount === true
? Number(item.appliedAmount || 0)
: Number(
(
(Number(item.settlementAmount || 0) * Number(this.form.paymentRatio || 0)) /
100
).toFixed(2)
),
}));
}
if (!this.referenceLabel) return [];
@@ -809,6 +857,10 @@ export default {
this.$nextTick(() => this.$refs.formRef?.validateField('referenceId').catch(() => {}));
},
validateSettlementAmount(rule, value, callback) {
if (this.form.paymentType === 'project_advance') {
callback();
return;
}
const amount = Number(value);
if (!Number.isFinite(amount) || amount < 0) {
callback(new Error('结算金额不能小于0'));
@@ -823,7 +875,11 @@ export default {
callback(new Error('申请付款金额不能小于0'));
return;
}
if (payableAmount > 0 && amount > payableAmount) {
if (
this.form.paymentType !== 'project_advance' &&
payableAmount > 0 &&
amount > payableAmount
) {
callback(new Error('申请付款金额不能超过可付款金额'));
return;
}
@@ -835,7 +891,7 @@ export default {
return;
}
if (!value) {
callback(new Error('请选择可用汇票'));
callback(new Error('请选择汇票单号'));
return;
}
const selected = this.billOptions.find(item => String(item.id) === String(value));
@@ -911,6 +967,10 @@ export default {
const referenceRow = this.createReferenceRow(data);
this.referenceRows = referenceRow.settlementNo ? [referenceRow] : [];
this.paymentRecords = data.paymentRecords || [];
if (this.form.paymentType === 'project_advance') {
await this.loadProjectOptions();
await this.loadContractOptions(this.form.projectId);
}
await this.loadReceiptAccountOptions(false);
await this.loadAttachmentRuleData(await this.loadCurrentSettlementDetails());
if (this.billPayment) await this.loadBillOptions('', data.billLedgerId);
@@ -921,7 +981,12 @@ export default {
this.form.applyDate = this.$dayjs().format('YYYY-MM-DD');
this.form.applicantName =
this.$store.getters.userInfo?.realName || this.$store.getters.userInfo?.userName || '';
await this.loadTransferredPreSettlements();
if (this.form.paymentType === 'project_advance') await this.loadProjectOptions();
if (this.transferPayload?.sourceFormalSettlements?.length) {
await this.loadTransferredFormalSettlements();
} else {
await this.loadTransferredPreSettlements();
}
}
},
initialAddPaymentType() {
@@ -938,7 +1003,12 @@ export default {
.split(',')
.map(item => item.trim())
.filter(Boolean);
return [...new Set([...payloadIds, ...queryIds].map(String))];
return [...new Set([...payloadIds, ...queryIds].map(String))].filter(id =>
this.isValidReferenceId(id)
);
},
isValidReferenceId(id) {
return id !== null && id !== undefined && String(id).trim() !== '-1';
},
async loadTransferredPreSettlements() {
const fallbackRows = this.transferPayload?.sourcePreSettlements || [];
@@ -973,6 +1043,97 @@ export default {
throw error;
}
},
async loadTransferredFormalSettlements() {
const fallbackRows = this.transferPayload?.sourceFormalSettlements || [];
const rows = await Promise.all(
fallbackRows.map(async row => {
if (!this.isValidReferenceId(row.id)) return row;
try {
return { ...row, ...this.unwrapData(await formalApi.getDetail(row.id)) };
} catch (error) {
return row;
}
})
);
this.applyTransferredFormalSettlements(rows.filter(item => this.isValidReferenceId(item?.id)));
await Promise.all([this.loadReceiptAccountOptions(), this.loadAttachmentRuleData(rows)]);
},
applyTransferredFormalSettlements(rows = []) {
if (!rows.length) return;
const contractIds = new Set(rows.map(item => String(item.contractId || '')));
if (contractIds.size !== 1 || contractIds.has('')) {
this.$message.warning('所选正式结算单合同信息不一致,请返回后重新选择');
return;
}
const first = rows[0];
const referenceRows = rows.map(item => ({
...this.createReferenceRow(item),
settlementNo: item.formalSettlementNo || item.settlementNo || '',
appliedAmount: Number(item.appliedAmount || 0),
fixedAppliedAmount: true,
}));
const settlementAmount = referenceRows.reduce(
(total, item) => total + Number(item.settlementAmount || 0),
0
);
const payableAmount = referenceRows.reduce(
(total, item) => total + Number(item.payableAmount || 0),
0
);
const appliedAmount = referenceRows.reduce(
(total, item) => total + Number(item.appliedAmount || 0),
0
);
const paymentRatio = settlementAmount
? Number(((appliedAmount / settlementAmount) * 100).toFixed(2))
: 0;
const settlementIds = rows.map(item => item.settlementId || item.id).filter(Boolean);
this.amountSyncing = true;
this.referenceRows = referenceRows;
Object.assign(this.form, {
paymentType: 'settlement_payment',
settlementId: settlementIds[0] || null,
settlementIds,
preSettlementId: null,
preSettlementIds: [],
settlementNo: referenceRows
.map(item => item.settlementNo)
.filter(Boolean)
.join('、'),
preSettlementNo: '',
projectId: first.projectId,
projectName: first.projectName,
deptId: first.deptId,
deptName: first.deptName,
contractId: first.contractId,
contractNo: first.contractNo,
contractName: first.contractName,
payerName: first.payerName,
payeeName: first.payeeName,
settlementAmount: Number(settlementAmount.toFixed(2)),
payableAmount: Number(payableAmount.toFixed(2)),
billType: '正式结算单',
paymentRatio,
appliedAmount: Number(appliedAmount.toFixed(2)),
invoices: rows.flatMap(item =>
this.createInvoiceRows(item.invoices || [], item.formalSettlementNo || item.settlementNo)
),
});
this.paymentRecords = rows.flatMap(item =>
(item.paymentRecords || item.paymentApplications || []).map(record => ({
...record,
paidAmount: Number(record.paidAmount || 0),
paidDate: record.paidDate || record.paymentDate || record.createTime || '',
paymentNo: record.paymentNo || '',
voucherJson: record.voucherJson || '',
kingdeeBillNo: record.kingdeeBillNo || '',
}))
);
this.$nextTick(() => {
this.amountSyncing = false;
this.$refs.formRef?.clearValidate();
});
},
createReferenceRow(row) {
const settlementAmount = Number(row.settlementAmount || 0);
const appliedAmount = Number(row.advanceAppliedAmount || row.appliedPaymentAmount || 0);
@@ -1156,6 +1317,22 @@ export default {
material.keywords.some(keyword => fileName.includes(keyword.toLocaleLowerCase())))
);
},
resolveAttachmentTypeByFileName(file) {
let fileName = this.attachmentFileName(file) || this.attachmentFileUrl(file);
fileName = String(fileName || '').split(/[?#]/)[0].toLocaleLowerCase();
try {
fileName = decodeURIComponent(fileName);
} catch {}
const material = [...Object.values(ATTACHMENT_MATERIALS)]
.sort(
(a, b) =>
Math.max(...b.keywords.map(String.length)) - Math.max(...a.keywords.map(String.length))
)
.find(item =>
item.keywords.some(keyword => fileName.includes(String(keyword).toLocaleLowerCase()))
);
return material ? MATERIAL_TYPE_MAP[material.key] : 'other';
},
hasAttachmentMaterial(material) {
const hasUploadedAttachment = (this.form.attachments || []).some(file =>
this.attachmentMatchesMaterial(file, material)
@@ -1204,8 +1381,12 @@ export default {
async loadCurrentSettlementDetails() {
if (this.form.paymentType === 'project_advance') return [];
const requests = [];
if (this.form.preSettlementId) requests.push(preApi.getDetail(this.form.preSettlementId));
if (this.form.settlementId) requests.push(formalApi.getDetail(this.form.settlementId));
if (this.isValidReferenceId(this.form.preSettlementId)) {
requests.push(preApi.getDetail(this.form.preSettlementId));
}
if (this.isValidReferenceId(this.form.settlementId)) {
requests.push(formalApi.getDetail(this.form.settlementId));
}
if (!requests.length) return [];
const responses = await Promise.all(requests);
return responses.map(response => this.unwrapData(response)).filter(item => item?.id);
@@ -1279,15 +1460,32 @@ export default {
handleTypeChange() {
this.form.invoices = [];
this.paymentRecords = [];
if (this.form.paymentType === 'project_advance') {
this.form.settlementId = null;
this.form.preSettlementId = null;
this.form.preSettlementIds = [];
this.form.settlementNo = '';
this.form.preSettlementNo = '';
this.referenceRows = [];
}
Object.assign(this.form, {
settlementId: null,
settlementIds: [],
preSettlementId: null,
preSettlementIds: [],
settlementNo: '',
preSettlementNo: '',
projectId: null,
projectName: '',
deptId: null,
deptName: '',
contractId: null,
contractNo: '',
contractName: '',
payerName: '',
payeeName: '',
settlementAmount: 0,
payableAmount: 0,
appliedAmount: 0,
billType: '',
});
this.referenceRows = [];
this.contractOptions = [];
this.clearReceiptAccount();
this.loadAttachmentRuleData([]);
if (this.form.paymentType === 'project_advance') this.loadProjectOptions();
},
clearReceiptAccount() {
Object.assign(this.form, {
@@ -1300,6 +1498,108 @@ export default {
customerRecords(response) {
return this.unwrapData(response)?.records || [];
},
async loadProjectOptions() {
if (this.projectLoading) return;
this.projectLoading = true;
try {
const response = await getProjectList(1, 9999, {});
this.projectOptions = this.customerRecords(response);
if (
this.form.projectId &&
this.form.projectName &&
!this.projectOptions.some(item => String(item.id) === String(this.form.projectId))
) {
this.projectOptions.unshift({
id: this.form.projectId,
projectName: this.form.projectName,
});
}
} catch (error) {
this.projectOptions = [];
this.$message.warning('项目列表加载失败,请稍后重试');
} finally {
this.projectLoading = false;
}
},
async loadContractOptions(projectId) {
if (!projectId) {
this.contractOptions = [];
return;
}
this.contractLoading = true;
try {
const response = await formalApi.getContractOptions('', projectId);
this.contractOptions = (this.unwrapData(response) || []).filter(
item => String(item.projectId || '') === String(projectId)
);
if (
this.form.contractId &&
this.form.contractName &&
!this.contractOptions.some(item => String(item.id) === String(this.form.contractId))
) {
this.contractOptions.unshift({
id: this.form.contractId,
contractNo: this.form.contractNo,
contractName: this.form.contractName,
projectId,
});
}
} catch (error) {
this.contractOptions = [];
this.$message.warning('项目合同加载失败,请稍后重试');
} finally {
this.contractLoading = false;
}
},
async handleProjectChange(projectId) {
const project = this.projectOptions.find(item => String(item.id) === String(projectId));
this.form.projectName = project?.projectName || project?.projectShortName || '';
Object.assign(this.form, {
deptId: project?.undertakeDeptId || project?.businessDeptId || null,
deptName: project?.undertakeDeptName || project?.businessDeptName || '',
contractId: null,
contractNo: '',
contractName: '',
payerName: '',
payeeName: '',
settlementAmount: 0,
payableAmount: 0,
appliedAmount: 0,
billType: '',
});
this.contractOptions = [];
this.clearReceiptAccount();
this.form.invoices = [];
this.paymentRecords = [];
await this.loadContractOptions(projectId);
await this.loadAttachmentRuleData([]);
},
async handleAdvanceContractChange(contractId) {
const contract = this.contractOptions.find(
item => String(item.id) === String(contractId)
);
if (!contract) {
Object.assign(this.form, {
contractNo: '',
contractName: '',
payerName: '',
payeeName: '',
});
this.clearReceiptAccount();
return;
}
Object.assign(this.form, {
contractNo: contract.contractNo || '',
contractName: contract.contractName || '',
payerName: contract.payerName || contract.partyA || '',
payeeName: contract.payeeName || contract.partyB || '',
settlementAmount: 0,
payableAmount: 0,
billType: '',
});
await Promise.all([this.loadReceiptAccountOptions(), this.loadAttachmentRuleData([])]);
if (this.billPayment) this.loadBillOptions();
},
async loadReceiptAccountOptions(autoSelectDefault = true) {
const payeeName = String(this.form.payeeName || '').trim();
const preserveCurrentValue = autoSelectDefault === false;
@@ -1483,12 +1783,10 @@ export default {
this.loadPreReferences();
},
async openReference() {
if (this.form.paymentType === 'project_advance') return;
this.referenceVisible = true;
if (this.form.paymentType === 'project_advance') {
const response = await formalApi.getContractOptions('');
this.contractRows = this.unwrapData(response) || [];
return;
}
this.formalSelection = [];
this.preSelection = [];
this.formalPage.current = 1;
this.prePage.current = 1;
await this.loadSettlementReferences();
@@ -1498,7 +1796,6 @@ export default {
this.form.invoices = [];
this.paymentRecords = [];
this.clearReceiptAccount();
const settlementType = row.settlementType || 'payable';
Object.assign(this.form, {
projectId: row.projectId,
projectName: row.projectName,
@@ -1507,21 +1804,24 @@ export default {
contractId: row.id,
contractNo: row.contractNo,
contractName: row.contractName,
payerName:
row.payerName || (settlementType === 'receivable' ? row.partyB : row.partyA) || '',
payeeName:
row.payeeName || (settlementType === 'receivable' ? row.partyA : row.partyB) || '',
payableAmount: Number((Number(row.fundDemand || 0) * 10000).toFixed(2)),
billType: row.settlementMode ? '项目预付' : '',
payerName: row.payerName || row.partyA || '',
payeeName: row.payeeName || row.partyB || '',
settlementAmount: 0,
payableAmount: 0,
appliedAmount: 0,
billType: '',
});
this.referenceVisible = false;
await Promise.all([this.loadReceiptAccountOptions(), this.loadAttachmentRuleData([])]);
if (this.billPayment) this.loadBillOptions();
},
async selectFormal(row) {
const validReferenceId = this.isValidReferenceId(row.id);
const [amountResponse, detailResponse] = await Promise.all([
api.getReferenceAmount('settlement_payment', row.id, this.form.id),
formalApi.getDetail(row.id),
validReferenceId
? api.getReferenceAmount('settlement_payment', row.id, this.form.id)
: Promise.resolve(null),
validReferenceId ? formalApi.getDetail(row.id) : Promise.resolve(null),
]);
const amount = this.unwrapData(amountResponse) || {};
const settlementAmount = Number(amount.settlementAmount ?? row.settlementAmount ?? 0);
@@ -1531,6 +1831,7 @@ export default {
Object.assign(this.form, {
paymentType: 'settlement_payment',
settlementId: row.id,
settlementIds: [row.id],
preSettlementId: null,
preSettlementIds: [],
settlementNo: row.formalSettlementNo,
@@ -1565,10 +1866,27 @@ export default {
]);
if (this.billPayment) this.loadBillOptions();
},
async confirmFormalSelection() {
const rows = this.formalSelection || [];
if (!rows.length) return;
if (new Set(rows.map(item => String(item.contractId || ''))).size !== 1) {
this.$message.warning('所选正式结算单必须属于同一合同');
return;
}
const validRows = rows.filter(row => this.isValidReferenceId(row.id));
const details = await Promise.all(validRows.map(row => formalApi.getDetail(row.id)));
this.applyTransferredFormalSettlements(
details.map((response, index) => ({ ...validRows[index], ...this.unwrapData(response) }))
);
this.referenceVisible = false;
},
async selectPre(row) {
const validReferenceId = this.isValidReferenceId(row.id);
const [amountResponse, detailResponse] = await Promise.all([
api.getReferenceAmount('progress_advance', row.id, this.form.id),
preApi.getDetail(row.id),
validReferenceId
? api.getReferenceAmount('progress_advance', row.id, this.form.id)
: Promise.resolve(null),
validReferenceId ? preApi.getDetail(row.id) : Promise.resolve(null),
]);
const amount = this.unwrapData(amountResponse) || {};
const settlementAmount = Number(amount.settlementAmount ?? row.settlementAmount ?? 0);
@@ -1582,6 +1900,7 @@ export default {
preSettlementId: row.id,
preSettlementIds: [row.id],
settlementId: null,
settlementIds: [],
preSettlementNo: row.preSettlementNo,
settlementNo: '',
projectId: row.projectId,
@@ -1610,13 +1929,33 @@ export default {
]);
if (this.billPayment) this.loadBillOptions();
},
async confirmPreSelection() {
const rows = this.preSelection || [];
if (!rows.length) return;
if (new Set(rows.map(item => String(item.contractId || ''))).size !== 1) {
this.$message.warning('所选预结算单必须属于同一合同');
return;
}
const validRows = rows.filter(row => this.isValidReferenceId(row.id));
const details = await Promise.all(validRows.map(row => preApi.getDetail(row.id)));
this.applyTransferredPreSettlements(
details.map((response, index) => ({ ...validRows[index], ...this.unwrapData(response) }))
);
this.referenceVisible = false;
},
normalizeAttachments(files) {
this.form.attachments = (files || []).map(file => ({
...file,
attachmentType:
file.attachmentType ||
MATERIAL_TYPE_MAP[this.resolveAttachmentMaterial(file)?.key] ||
'other',
attachmentType: (() => {
const matchedType = this.resolveAttachmentTypeByFileName(file);
const existingType = [
...Object.values(MATERIAL_TYPE_MAP),
'other',
].includes(file.attachmentType)
? file.attachmentType
: 'other';
return matchedType !== 'other' ? matchedType : existingType;
})(),
description: file.description || '',
}));
},
@@ -1664,7 +2003,7 @@ export default {
this.downloadableAttachments.forEach((file, index) => {
window.setTimeout(
() => downloadFileByUrl(this.attachmentFileUrl(file), this.attachmentFileName(file)),
index * 300
index * 500
);
});
},
@@ -1673,6 +2012,7 @@ export default {
id,
paymentType,
settlementId,
settlementIds,
preSettlementId,
preSettlementIds,
projectId,
@@ -1704,6 +2044,7 @@ export default {
id,
paymentType,
settlementId,
settlementIds,
preSettlementId,
preSettlementIds,
projectId,
+36 -5
View File
@@ -75,7 +75,11 @@
{{ displayValue(row[column.prop]) }}
</el-link>
<span v-else-if="column.link">{{ displayValue(row[column.prop]) }}</span>
<el-tag v-else-if="column.status" :type="statusType(row.claimStatus)" class="status-text">
<el-tag
v-else-if="column.status"
:type="statusType(row.claimStatus)"
class="status-text"
>
{{ displayValue(row[column.prop]) }}
</el-tag>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
@@ -109,7 +113,7 @@ import { mapGetters } from 'vuex';
import * as api from '@/api/payment/receiptFlow';
import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
const receiptFlowMockRows = [
const receiptFlowMockTemplates = [
{
receiptNoticeNo: 'SKTZ20260827001',
payerName: '华东供应链管理有限公司',
@@ -136,6 +140,34 @@ const receiptFlowMockRows = [
},
];
const randomDigits = length =>
Array.from({ length }, () => Math.floor(Math.random() * 10)).join('');
const formatMockDateTime = date => {
const pad = value => String(value).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
date.getHours()
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const createReceiptFlowMockRows = () =>
receiptFlowMockTemplates.map((template, index) => {
const transactionDate = new Date();
transactionDate.setDate(transactionDate.getDate() - Math.floor(Math.random() * 7));
transactionDate.setHours(8 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 60), 0);
const transactionTime = formatMockDateTime(transactionDate);
const dateCode = transactionTime.slice(0, 10).replaceAll('-', '');
return {
...template,
receiptNoticeNo: `SKTZ${dateCode}${randomDigits(5)}`,
receiptAmount: Math.floor(10000 + Math.random() * 90000),
counterpartyAccount: `${template.counterpartyAccount.slice(0, -6)}${randomDigits(6)}`,
transactionTime,
detailSerialNo: `MOCK-RF-${dateCode}-${index + 1}${randomDigits(4)}`,
sourceUpdatedTime: transactionTime,
};
});
const emptyQuery = () => ({
receiptNoticeNo: '',
counterpartyName: '',
@@ -210,9 +242,8 @@ export default {
async handleSync() {
this.syncing = true;
try {
const syncedCount = Number(
this.unwrapData(await api.sync({ flows: receiptFlowMockRows })) || 0
);
const mockRows = createReceiptFlowMockRows();
const syncedCount = Number(this.unwrapData(await api.sync({ flows: mockRows })) || 0);
this.$message.success(`流水同步完成,共同步${syncedCount}`);
await this.loadTable();
} finally {
@@ -38,7 +38,11 @@
disabled
/><span v-else>{{
displayValue(
field.prop === 'settlementTypeName' ? settlementTypeName : form[field.prop]
field.prop === 'settlementTypeName'
? settlementTypeName
: field.prop === 'formalSettlementId'
? form.formalSettlementNo || form.formalSettlementId
: form[field.prop]
)
}}</span></el-form-item
></el-col
@@ -340,7 +344,8 @@ export default {
}
this.loading = true;
try {
const { data } = await api.getDetail(this.recordId);
const response = await api.getDetail(this.recordId);
const data = response?.data?.data || response?.data || response || {};
this.form = { ...createSettlementAdjustmentForm(), ...data };
this.details = (data.details || []).map(item => ({
...item,
@@ -124,6 +124,22 @@
class="status-text"
>{{ updateName(row.updateResult) }}</el-tag
>
<span v-else-if="column.prop === 'matchedExternalLineNo'">
{{ formatMatchedExternalLineNo(row.matchedExternalLineNo) }}
</span>
<span v-else-if="column.prop === 'transportType'">
{{ transportTypeLabel(row.transportType) }}
</span>
<span v-else-if="column.prop === 'transportQuantity'">
{{ formatTransportQuantity(row.transportQuantity) }}
</span>
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
<span
v-else-if="
column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime'
"
>{{ formatDate(row[column.prop]) }}</span
>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
@@ -160,7 +176,9 @@
>下载货物明细对账模板</el-button
>
<el-button type="primary" plain @click="chooseImport">导入</el-button>
<el-button type="primary" @click="handleMatch">开始匹配内部账单</el-button>
<el-button type="primary" :disabled="!canStartMatch" @click="handleMatch"
>开始匹配内部账单</el-button
>
<input
ref="fileInput"
type="file"
@@ -186,6 +204,19 @@
<el-tag v-if="column.prop === 'matchStatus'" :type="matchTagType(row.matchStatus)" class="status-text">{{
matchName(row.matchStatus)
}}</el-tag>
<span v-else-if="column.prop === 'transportType'">
{{ transportTypeLabel(row.transportType) }}
</span>
<span v-else-if="column.prop === 'transportQuantity'">
{{ formatTransportQuantity(row.transportQuantity) }}
</span>
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
<span
v-else-if="
column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime'
"
>{{ formatDate(row[column.prop]) }}</span
>
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
<span v-else>{{ displayValue(row[column.prop]) }}</span>
</template>
@@ -202,27 +233,6 @@
</el-table-column>
</el-table>
</section-card>
<section-card title="操作说明">
<el-collapse>
<el-collapse-item title="操作说明" name="help">
<div class="reconciliation-editor__help">
<p>1. 新增对账单时选择一张已审批通过的正式结算单系统自动带出内部账单明细</p>
<p>
2. 导入对方 Excel
账单后点击开始匹配内部账单系统按车号货物地址批次发货时间运输量和金额匹配
</p>
<p>3. 外部账单存在重复候选时会标记为疑似重复需要人工选择唯一明细匹配</p>
<p>4. 只有所有内外部明细一一匹配且差异为 0才允许按匹配结果更新账单或完成对账</p>
<p>
5.
已付金额大于外部匹配金额时禁止更新整车模式一车多货会跳过自动更新可通过调整逐货物修改
</p>
<p>6. 按匹配结果更新成功后生成变更记录完成对账后单据不可再编辑和删除</p>
</div>
</el-collapse-item>
</el-collapse>
</section-card>
</div>
<template #footer>
@@ -263,6 +273,11 @@
formatMoney(row.settlementAmount, row.currency)
}}</template></el-table-column
>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click="selectFormal(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<div class="reconciliation-editor__pagination">
<el-pagination
@@ -288,6 +303,7 @@
><el-input-number
v-model="row.transportQuantity"
:min="0"
:precision="2"
:controls="false" /></template
></el-table-column>
<el-table-column prop="unitPrice" label="运输单价" min-width="120"
@@ -325,7 +341,11 @@
<el-table-column prop="externalLineNo" label="外部行号" width="90" />
<el-table-column prop="vehicleNo" label="车牌号" width="110" />
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
<el-table-column prop="transportQuantity" label="运输量" width="110" />
<el-table-column prop="transportQuantity" label="运输量" width="110"
><template #default="{ row }">{{
formatTransportQuantity(row.transportQuantity)
}}</template></el-table-column
>
<el-table-column prop="settlementAmount" label="结算金额" width="130"
><template #default="{ row }">{{
formatMoney(row.settlementAmount)
@@ -346,7 +366,10 @@
</template>
<script>
import { mapGetters } from 'vuex';
import * as api from '@/api/settlement/transportReconciliation';
import * as formalSettlementApi from '@/api/settlement/formalSettlement';
import { getDictionary } from '@/api/system/dictbiz';
import { downloadXls } from '@/utils/util';
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
import {
@@ -373,6 +396,7 @@ export default {
form: this.emptyForm(),
formFields: transportReconciliationFormFields,
internalColumns,
transportTypeOptions: [],
internalDetails: [],
externalDetails: [],
internalQuery: { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' },
@@ -396,6 +420,7 @@ export default {
};
},
computed: {
...mapGetters(['userInfo']),
visible: {
get() {
return this.modelValue;
@@ -435,6 +460,9 @@ export default {
row => row.suspectedDuplicate || row.matchStatus === 'suspected_duplicate'
);
},
canStartMatch() {
return Boolean(this.form.formalSettlementId && this.externalDetails.length);
},
visibleExternalRows() {
return this.externalTab === 'duplicate' ? this.duplicateRows : this.externalDetails;
},
@@ -479,6 +507,7 @@ export default {
};
},
async initialize() {
this.loadTransportTypeOptions();
this.currentId = this.recordId;
this.form = this.emptyForm();
this.internalDetails = [];
@@ -487,7 +516,8 @@ export default {
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
if (!this.currentId) {
this.form.reconciliationMode = 'vehicle';
this.form.reconcilerName = '当前用户';
this.form.reconcilerName =
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '当前用户';
this.form.reconciliationDate = this.$dayjs().format('YYYY-MM-DD');
return;
}
@@ -499,10 +529,12 @@ export default {
}
},
async loadDetail() {
const { data } = await api.getDetail(this.currentId);
const data = this.unwrapData(await api.getDetail(this.currentId)) || {};
this.form = { ...this.emptyForm(), ...data };
this.internalDetails = data.internalDetails || [];
this.externalDetails = data.externalDetails || [];
this.internalDetails =
data.internalDetails || data.internalBillDetails || data.internals || [];
this.externalDetails =
data.externalDetails || data.externalBillDetails || data.externals || [];
},
openFormalDialog() {
if (!this.editable) return;
@@ -513,21 +545,42 @@ export default {
async loadFormalOptions() {
this.formalDialog.loading = true;
try {
const { data } = await api.getFormalOptions(
this.formalDialog.page.current,
this.formalDialog.page.size,
{ settlementType: this.settlementType, keyword: this.formalDialog.query.keyword }
const params = {
settlementType: this.settlementType,
keyword: this.formalDialog.query.keyword,
};
const data = this.unwrapData(
await api.getFormalOptions(
this.formalDialog.page.current,
this.formalDialog.page.size,
params
)
);
this.formalDialog.rows = data.records || [];
this.formalDialog.page.total = data.total || 0;
let rows = data.records || [];
let total = data.total || 0;
if (!rows.length) {
const fallback = await formalSettlementApi.getList(
this.formalDialog.page.current,
this.formalDialog.page.size,
{ ...params, approvalStatus: 'approved' }
);
const fallbackData = this.unwrapData(fallback);
rows = fallbackData.records || [];
total = fallbackData.total || 0;
}
this.formalDialog.rows = rows;
this.formalDialog.page.total = total;
} finally {
this.formalDialog.loading = false;
}
},
confirmFormal() {
async confirmFormal() {
if (this.formalDialog.selected.length !== 1)
return this.$message.warning('请选择一张正式结算单');
const selected = this.formalDialog.selected[0];
await this.selectFormal(this.formalDialog.selected[0]);
},
async selectFormal(selected) {
const formalSettlementChanged = this.form.formalSettlementId !== selected.id;
this.form = {
...this.form,
...selected,
@@ -535,33 +588,115 @@ export default {
formalSettlementNo: selected.formalSettlementNo,
reconciliationNo: this.form.reconciliationNo,
};
if (formalSettlementChanged) this.externalDetails = [];
await this.loadFormalInternalPreview(selected.id);
this.formalDialog.visible = false;
},
async handleSave() {
async loadFormalInternalPreview(formalSettlementId) {
const formal = this.unwrapData(await formalSettlementApi.getDetail(formalSettlementId));
const details = formal.details || [];
const buildInternalRow = (detail, overrides = {}, index = 0) => ({
...detail,
...overrides,
id: null,
lineNo: index + 1,
formalSettlementDetailId: detail.id,
settlementAmount: overrides.settlementAmount ?? detail.settlementAmountTax ?? 0,
matchResult: 'unmatched',
updateResult: 'not_updated',
});
if (this.form.reconciliationMode === 'cargo') {
const feeGroups = await Promise.all(
details.map(async detail => ({
detail,
fees: this.unwrapData(await formalSettlementApi.getDetailFees(detail.id)) || [],
}))
);
this.internalDetails = feeGroups.flatMap(({ detail, fees }) => {
if (!fees.length) return [buildInternalRow(detail, {}, 0)];
return fees.map((fee, index) =>
buildInternalRow(
detail,
{
...fee,
settlementAmount: fee.settlementAmountTax ?? 0,
},
index
)
);
});
} else {
this.internalDetails = details.map((detail, index) => buildInternalRow(detail, {}, index));
}
const internalQuantity = this.internalDetails.reduce(
(total, row) => total + Number(row.transportQuantity || 0),
0
);
const internalAmount = this.internalDetails.reduce(
(total, row) => total + Number(row.settlementAmount || 0),
0
);
this.form = {
...this.form,
internalBillCount: this.internalDetails.length,
internalQuantity,
internalAmount,
externalBillCount: 0,
externalQuantity: 0,
externalAmount: 0,
differenceCount: this.internalDetails.length,
differenceQuantity: internalQuantity,
differenceAmount: internalAmount,
matchedCount: 0,
unmatchedCount: this.internalDetails.length,
};
},
unwrapData(response) {
const body = response?.data || response || {};
return body?.data || body;
},
async loadTransportTypeOptions() {
try {
const data = this.unwrapData(await getDictionary({ code: 'transport_type' }));
const records = Array.isArray(data) ? data : data?.records || [];
this.transportTypeOptions = records.map(item => ({
label: item.dictValue || item.label || item.name,
value: item.dictKey || item.value || item.dictValue || item.name,
}));
} catch {
this.transportTypeOptions = [];
}
},
async handleSave(silent = false) {
const valid = await this.$refs.formRef.validate().catch(() => false);
if (!valid || !this.form.formalSettlementId) return this.$message.warning('请选择正式结算单');
if (!valid || !this.form.formalSettlementId) {
this.$message.warning('请选择正式结算单');
return null;
}
this.saving = true;
try {
const { data } = await api.save({
id: this.currentId,
formalSettlementId: this.form.formalSettlementId,
reconciliationMode: this.form.reconciliationMode,
reconciliationDate: this.form.reconciliationDate,
remark: this.form.remark,
});
const data = this.unwrapData(
await api.save({
id: this.currentId,
formalSettlementId: this.form.formalSettlementId,
reconciliationMode: this.form.reconciliationMode,
reconciliationDate: this.form.reconciliationDate,
remark: this.form.remark,
})
);
this.currentId = data;
await this.loadDetail();
this.$message.success('草稿保存成功');
this.$emit('success');
if (!silent) {
this.$message.success('草稿保存成功');
this.$emit('success');
}
return data;
} finally {
this.saving = false;
}
},
async handleMatch() {
if (!this.currentId) {
await this.handleSave();
if (!this.currentId) return;
}
if (!this.currentId) return this.$message.warning('请先保存草稿后再匹配');
this.actionLoading = true;
try {
await api.match(this.currentId);
@@ -572,10 +707,11 @@ export default {
}
},
async handleUpdate() {
if (!this.currentId) return this.$message.warning('请先保存对账单');
await this.$confirm('将以外部账单匹配金额更新内部结算明细,是否继续?', '更新账单', {
type: 'warning',
});
const savedId = await this.handleSave(true);
if (!savedId) return;
this.actionLoading = true;
try {
await api.updateByMatch(this.currentId);
@@ -586,22 +722,30 @@ export default {
}
},
async handleComplete() {
if (!this.currentId) return this.$message.warning('请先保存对账单');
if (
Number(this.form.differenceCount || 0) !== 0 ||
Number(this.form.differenceQuantity || 0) !== 0 ||
Number(this.form.differenceAmount || 0) !== 0
) {
return this.$message.warning('差异单数、差异货量和差异金额必须全部为0才可完成对账');
}
await this.$confirm('完成后对账单不可修改和删除,是否继续?', '完成对账', {
type: 'warning',
});
const savedId = await this.handleSave(true);
if (!savedId) return;
this.actionLoading = true;
try {
await api.complete(this.currentId);
await this.loadDetail();
this.$message.success('对账完成');
this.visible = false;
this.$emit('success');
} finally {
this.actionLoading = false;
}
},
chooseImport() {
if (!this.currentId) return this.$message.warning('请先保存对账单');
this.$refs.fileInput?.click();
},
async handleImport(event) {
@@ -609,6 +753,10 @@ export default {
event.target.value = '';
if (!file) return;
try {
if (!this.currentId) {
const savedId = await this.handleSave(true);
if (!savedId) return;
}
const response =
this.form.reconciliationMode === 'cargo'
? await api.importCargo(this.currentId, file)
@@ -731,6 +879,28 @@ export default {
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
formatMatchedExternalLineNo(value) {
return Number(value) < 0 ? '' : this.displayValue(value);
},
formatTransportQuantity(value) {
return value === null || value === undefined || value === ''
? '-'
: Number(value || 0).toFixed(2);
},
formatMileage(value) {
return Number(value) === -1 ? '' : this.displayValue(value);
},
formatDate(value) {
if (value === null || value === undefined || value === '') return '-';
const date = this.$dayjs(value);
return date.isValid() ? date.format('YYYY-MM-DD') : this.displayValue(value);
},
transportTypeLabel(value) {
if (value === null || value === undefined || value === '') return '-';
return (
this.transportTypeOptions.find(item => String(item.value) === String(value))?.label || value
);
},
formatMoney(value, currency = 'RMB') {
return `${Number(value || 0).toFixed(2)} ${currency || 'RMB'}`;
},
+55 -13
View File
@@ -160,7 +160,9 @@
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as XLSX from 'xlsx';
import { createSettlementTransfer } from '@/utils/settlement-transfer';
import * as api from '@/api/settlement/formalSettlement';
import * as paymentApi from '@/api/payment/paymentApplication';
import {
formalSettlementSearchFields,
invoiceStatusOptions,
@@ -362,27 +364,67 @@ export default {
this.$message.success(`同步成功,金蝶单据号:${data}`);
this.loadTable();
},
openPaymentDialog() {
async openPaymentDialog() {
if (!this.selection.length) return this.$message.warning('请至少选择一条正式结算单');
const rows = this.selection;
if (rows.some(row => row.approvalStatus !== 'approved' || row.settlementType !== 'payable'))
return this.$message.warning('仅审批通过的应付正式结算单允许发起付款申请');
const contractIds = new Set(rows.map(row => String(row.contractId || '')));
if (rows.length > 1 && (contractIds.size !== 1 || contractIds.has('')))
return this.$message.warning('批量付款申请必须选择同一个合同的正式结算单');
if (contractIds.size !== 1 || contractIds.has(''))
return this.$message.warning('付款申请必须选择同一个合同的正式结算单');
const paymentRows = rows.map(row => ({ ...row, appliedAmount: this.paymentRemaining(row) }));
if (paymentRows.some(row => row.appliedAmount <= 0))
return this.$message.warning('所选正式结算单均须存在剩余可申请金额');
this.paymentDialog = {
visible: true,
loading: false,
row: paymentRows[0],
rows: paymentRows,
form: {
appliedAmount: paymentRows.length === 1 ? paymentRows[0].appliedAmount : 0,
remark: '',
},
};
try {
const sourceFormalSettlements = await Promise.all(
paymentRows.map(async row => {
const [amountResponse, detailResponse] = await Promise.all([
paymentApi.getReferenceAmount('settlement_payment', row.id),
api.getDetail(row.id),
]);
const amount = this.unwrapData(amountResponse) || {};
const detail = this.unwrapData(detailResponse) || {};
const remainingAmount = Number(amount.payableAmount ?? this.paymentRemaining(row));
return {
...row,
id: row.id,
settlementId: row.id,
projectId: detail.projectId ?? row.projectId,
projectName: detail.projectName || row.projectName,
deptId: detail.deptId ?? row.deptId,
deptName: detail.deptName || row.deptName,
contractId: detail.contractId ?? row.contractId,
contractNo: detail.contractNo || row.contractNo,
contractName: detail.contractName || row.contractName,
payerName: detail.payerName || row.payerName,
payeeName: detail.payeeName || row.payeeName,
formalSettlementNo: detail.formalSettlementNo || row.formalSettlementNo,
settlementAmount: Number(
amount.settlementAmount ?? detail.settlementAmount ?? row.settlementAmount ?? 0
),
payableAmount: remainingAmount,
appliedAmount: remainingAmount,
invoices: detail.invoices || [],
paymentRecords: detail.paymentRecords || detail.paymentApplications || [],
};
})
);
if (sourceFormalSettlements.some(row => row.appliedAmount <= 0)) {
this.$message.warning('所选正式结算单均须存在剩余可申请金额');
return;
}
const transferToken = createSettlementTransfer({
sourceFormalSettlements,
settlementType: 'payable',
});
await this.$router.push({
path: '/payment/payment-application/form',
query: { mode: 'add', paymentType: 'settlement_payment', transferToken },
});
this.selection = [];
} catch (error) {
this.$message.error('结算信息加载失败,请稍后重试');
}
},
async submitPayment() {
const paymentRows = this.paymentDialog.rows;
@@ -138,10 +138,11 @@ export default {
async loadTable() {
this.loading = true;
try {
const { data } = await api.getList(this.page.current, this.page.size, {
const response = await api.getList(this.page.current, this.page.size, {
...this.query,
settlementType: this.settlementType,
});
const data = response?.data?.data || response?.data || response || {};
this.rows = data.records || [];
this.page.total = data.total || 0;
} finally {
@@ -202,10 +203,11 @@ export default {
this.loadTable();
},
async handleExport() {
const { data } = await api.getList(1, 100000, {
const response = await api.getList(1, 100000, {
...this.query,
settlementType: this.settlementType,
});
const data = response?.data?.data || response?.data || response || {};
const exportRows = (data.records || []).map(item => ({
对账单号: item.reconciliationNo,
付款方: item.payerName,