feat: 收票登记先票后款表单改造(手选结算组织/发票明细区块/去关联结算单)
This commit is contained in:
@@ -38,6 +38,12 @@ export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'p
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
export const pushKingdee = ids =>
|
||||
request({ url: `${baseUrl}/push-kingdee`, method: 'post', timeout: 30000, data: ids });
|
||||
// 先票后款:发票明细查询(收票登记明细区块按票号带出发票明细行)
|
||||
export const invoiceDetailQuery = invoiceNo =>
|
||||
request({ url: `${baseUrl}/invoice-detail-query`, method: 'get', params: { invoiceNo } });
|
||||
// 先票后款:结算单认领收票单候选(审批通过且未被有效应付结算单认领)
|
||||
export const claimCandidates = keyword =>
|
||||
request({ url: `${baseUrl}/claim-candidates`, method: 'get', params: { keyword } });
|
||||
|
||||
export const approvalStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
|
||||
@@ -109,34 +109,50 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="关联结算单" prop="settlementIds">
|
||||
<el-tooltip
|
||||
:content="settlementLabel"
|
||||
:disabled="!settlementLabel"
|
||||
placement="top"
|
||||
:show-after="200"
|
||||
>
|
||||
<div class="receipt-form-page__overflow-trigger">
|
||||
<el-input
|
||||
:model-value="settlementLabel"
|
||||
readonly
|
||||
placeholder="请选择应付正式结算单"
|
||||
>
|
||||
<template v-if="!readonly" #append>
|
||||
<el-tooltip content="选择付款结算单" placement="top">
|
||||
<el-button :icon="Search" @click="openSettlementDialog" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-form-item label="结算组织" prop="deptId">
|
||||
<el-cascader
|
||||
v-if="!readonly"
|
||||
:model-value="deptCascaderValue"
|
||||
:options="deptTreeOptions"
|
||||
:props="deptCascaderProps"
|
||||
placeholder="请选择结算组织(合同承办部门)"
|
||||
filterable
|
||||
clearable
|
||||
@change="handleDeptChange"
|
||||
/>
|
||||
<el-input v-else :model-value="form.deptName" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section-card>
|
||||
|
||||
<section-card title="发票明细">
|
||||
<el-table :data="invoiceItems" border empty-text="选择金蝶票据池发票后带出发票明细行">
|
||||
<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="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>
|
||||
</section-card>
|
||||
|
||||
<section-card title="结算信息">
|
||||
<el-table :data="form.settlements" border>
|
||||
<el-table :data="form.settlements" border empty-text="暂无认领数据(认领关系由付款结算单侧维护)">
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column
|
||||
label="结算单号"
|
||||
@@ -156,17 +172,8 @@
|
||||
<el-table-column label="已收票金额" min-width="150" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.receivedInvoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分摊发票金额" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.allocatedInvoiceAmount"
|
||||
:disabled="readonly"
|
||||
:min="0"
|
||||
:max="remainingAmount(row)"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
/>
|
||||
</template>
|
||||
<el-table-column label="分摊发票金额" min-width="150" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.allocatedInvoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section-card>
|
||||
@@ -216,71 +223,6 @@
|
||||
:query-api="apiQueryInvoicePool"
|
||||
@confirm="handleInvoicePicked"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="settlementDialog.visible"
|
||||
title="选择应付正式结算单"
|
||||
width="86%"
|
||||
append-to-body
|
||||
>
|
||||
<div class="receipt-form-page__dialog-search">
|
||||
<el-input
|
||||
v-model="settlementDialog.keyword"
|
||||
clearable
|
||||
placeholder="结算单号、项目或合同"
|
||||
@keyup.enter="loadSettlementCandidates"
|
||||
/>
|
||||
<el-button type="primary" @click="loadSettlementCandidates">查询</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
ref="settlementTable"
|
||||
v-loading="settlementDialog.loading"
|
||||
:data="settlementDialog.rows"
|
||||
row-key="id"
|
||||
border
|
||||
@selection-change="settlementDialog.selection = $event"
|
||||
>
|
||||
<el-table-column type="selection" width="52" align="center" reserve-selection />
|
||||
<el-table-column
|
||||
prop="formalSettlementNo"
|
||||
label="结算单号"
|
||||
min-width="170"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="projectName"
|
||||
label="所属项目"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="deptName" label="所属组织" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
prop="contractNo"
|
||||
label="合同编号"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="payerName" label="付款方" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="payeeName" label="收款方" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="结算总金额" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.settlementAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已收票金额" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.receivedInvoiceAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click.stop="selectSettlement(row)">选择</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="settlementDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmSettlements">
|
||||
确定{{ settlementDialog.selection.length ? `(${settlementDialog.selection.length})` : '' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -288,6 +230,7 @@
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/invoiceReceipt';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
@@ -310,6 +253,7 @@ const emptyForm = () => ({
|
||||
customerEmails: [],
|
||||
departmentEmails: [],
|
||||
projectName: '',
|
||||
deptId: null,
|
||||
deptName: '',
|
||||
payerName: '',
|
||||
payeeName: '',
|
||||
@@ -330,26 +274,24 @@ export default {
|
||||
form: emptyForm(),
|
||||
customerEmailOptions: [],
|
||||
departmentEmailOptions: [],
|
||||
invoiceItems: [],
|
||||
deptTreeOptions: [],
|
||||
deptMap: new Map(),
|
||||
// 结算组织=合同承办部门:部门树级联单选(取末级 id),后端沿部门树上溯平台公司取金蝶组织
|
||||
deptCascaderProps: { checkStrictly: true, emitPath: true },
|
||||
invoiceDialog: {
|
||||
visible: false,
|
||||
},
|
||||
settlementDialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
keyword: '',
|
||||
rows: [],
|
||||
selection: [],
|
||||
},
|
||||
rules: {
|
||||
invoiceNo: [{ required: true, validator: this.validateInvoice, trigger: 'change' }],
|
||||
settlementIds: [{ required: true, validator: this.validateSettlements, trigger: 'change' }],
|
||||
deptId: [{ required: true, validator: this.validateDept, trigger: 'change' }],
|
||||
customerEmails: [{ validator: this.validateEmails, trigger: 'change' }],
|
||||
departmentEmails: [{ validator: this.validateEmails, trigger: 'change' }],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
recordId() {
|
||||
return this.$route.query.id || '';
|
||||
},
|
||||
@@ -363,14 +305,11 @@ export default {
|
||||
const code = this.recordId ? 'invoice_receipt_edit' : 'invoice_receipt_add';
|
||||
return this.hasPermission(code);
|
||||
},
|
||||
settlementLabel() {
|
||||
return this.form.settlements
|
||||
.map(item => item.formalSettlementNo)
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
},
|
||||
settlementIds() {
|
||||
return this.form.settlements.map(item => item.formalSettlementId || item.settlementId);
|
||||
deptCascaderValue() {
|
||||
if (this.form.deptId === null || this.form.deptId === undefined || this.form.deptId === '') {
|
||||
return [];
|
||||
}
|
||||
return [String(this.form.deptId)];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
@@ -406,6 +345,7 @@ export default {
|
||||
.filter(Boolean);
|
||||
},
|
||||
async initialize() {
|
||||
this.loadDepartments();
|
||||
if (this.isPublicViewPage) {
|
||||
await this.loadPublicDetail();
|
||||
return;
|
||||
@@ -419,14 +359,13 @@ export default {
|
||||
}
|
||||
const data = this.unwrapData(await api.getDetail(this.recordId));
|
||||
this.applyDetail(data);
|
||||
if (this.settlementIds.length) await this.loadReferenceInformation();
|
||||
},
|
||||
async loadPublicDetail() {
|
||||
if (!this.recordId) return;
|
||||
const data = this.unwrapData(await getMkPublicDetail('invoice-receipt', this.recordId));
|
||||
this.applyDetail(data);
|
||||
},
|
||||
applyDetail(data) {
|
||||
async applyDetail(data) {
|
||||
this.form = {
|
||||
...emptyForm(),
|
||||
...data,
|
||||
@@ -438,6 +377,7 @@ export default {
|
||||
})),
|
||||
attachments: this.parse(data.attachmentsJson),
|
||||
};
|
||||
await this.loadInvoiceDetail();
|
||||
},
|
||||
validateInvoice(rule, value, callback) {
|
||||
if (!value) {
|
||||
@@ -446,9 +386,9 @@ export default {
|
||||
}
|
||||
callback();
|
||||
},
|
||||
validateSettlements(rule, value, callback) {
|
||||
if (!this.form.settlements.length) {
|
||||
callback(new Error('请选择应付正式结算单'));
|
||||
validateDept(rule, value, callback) {
|
||||
if (this.form.deptId === null || this.form.deptId === undefined || this.form.deptId === '') {
|
||||
callback(new Error('请选择结算组织'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
@@ -469,7 +409,7 @@ export default {
|
||||
openInvoiceDialog() {
|
||||
this.invoiceDialog.visible = true;
|
||||
},
|
||||
confirmInvoice(row, options = {}) {
|
||||
async confirmInvoice(row, options = {}) {
|
||||
const selected = row;
|
||||
if (!selected) {
|
||||
this.$message.warning('请选择一张金蝶票据池发票');
|
||||
@@ -504,92 +444,63 @@ export default {
|
||||
...new Set([...this.departmentEmailOptions, ...this.form.departmentEmails]),
|
||||
];
|
||||
this.invoiceDialog.visible = false;
|
||||
await this.loadInvoiceDetail();
|
||||
this.$refs.formRef?.validateField('invoiceNo').catch(() => {});
|
||||
return true;
|
||||
},
|
||||
async openSettlementDialog() {
|
||||
this.settlementDialog.visible = true;
|
||||
this.settlementDialog.selection = [];
|
||||
await this.loadSettlementCandidates();
|
||||
await this.$nextTick();
|
||||
const selectedIds = new Set(this.settlementIds.map(id => String(id)));
|
||||
this.settlementDialog.rows.forEach(row => {
|
||||
if (selectedIds.has(String(row.id))) {
|
||||
this.$refs.settlementTable?.toggleRowSelection(row, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
async loadSettlementCandidates() {
|
||||
this.settlementDialog.loading = true;
|
||||
// 发票明细区块:按票号带出金蝶发票明细行(A801-1 镜像,推送 A303-2 分录同源)
|
||||
async loadInvoiceDetail() {
|
||||
if (!this.form.invoiceNo) {
|
||||
this.invoiceItems = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.settlementDialog.rows =
|
||||
this.unwrapData(
|
||||
await api.getSettlementCandidates(this.settlementDialog.keyword, this.form.id)
|
||||
) || [];
|
||||
} finally {
|
||||
this.settlementDialog.loading = false;
|
||||
this.invoiceItems = this.unwrapData(await api.invoiceDetailQuery(this.form.invoiceNo)) || [];
|
||||
} catch {
|
||||
this.invoiceItems = [];
|
||||
}
|
||||
},
|
||||
async confirmSettlements() {
|
||||
const rows = this.settlementDialog.selection;
|
||||
if (!rows.length) {
|
||||
this.$message.warning('请至少选择一张应付正式结算单');
|
||||
return;
|
||||
}
|
||||
const first = rows[0];
|
||||
const incompatible = rows.some(
|
||||
item =>
|
||||
String(item.projectId) !== String(first.projectId) ||
|
||||
String(item.deptId) !== String(first.deptId) ||
|
||||
item.payerName !== first.payerName ||
|
||||
item.payeeName !== first.payeeName
|
||||
);
|
||||
if (incompatible) {
|
||||
this.$message.warning('关联结算单必须属于同一项目、组织及收付款方');
|
||||
return;
|
||||
}
|
||||
const previousAmount = new Map(
|
||||
this.form.settlements.map(item => [
|
||||
String(item.formalSettlementId || item.settlementId),
|
||||
item.allocatedInvoiceAmount,
|
||||
])
|
||||
);
|
||||
this.form.settlements = rows.map(item => ({
|
||||
...item,
|
||||
formalSettlementId: item.id,
|
||||
settlementId: item.id,
|
||||
allocatedInvoiceAmount: previousAmount.has(String(item.id))
|
||||
? Number(previousAmount.get(String(item.id)) || 0)
|
||||
: rows.length === 1
|
||||
? Number(item.settlementAmount || 0)
|
||||
: 0,
|
||||
loadDepartments() {
|
||||
// 结算组织取租户部门树(过滤外部组织),选中取末级部门 id
|
||||
getDeptTree(this.userInfo?.tenantId)
|
||||
.then(response => {
|
||||
const rows = this.excludeExternalOrganization(this.unwrapData(response) || []);
|
||||
this.deptTreeOptions = this.toDeptCascaderOptions(rows);
|
||||
this.deptMap = new Map();
|
||||
this.flattenDepartments(rows).forEach(item =>
|
||||
this.deptMap.set(String(item.id), item.title || item.deptName || item.name)
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
excludeExternalOrganization(list = []) {
|
||||
return list.reduce((result, item) => {
|
||||
const deptName = item.title || item.deptName || item.name;
|
||||
if (String(deptName || '').trim() === '外部组织') return result;
|
||||
result.push({
|
||||
...item,
|
||||
children: item.children?.length
|
||||
? this.excludeExternalOrganization(item.children)
|
||||
: item.children,
|
||||
});
|
||||
return result;
|
||||
}, []);
|
||||
},
|
||||
toDeptCascaderOptions(list = []) {
|
||||
return list.map(item => ({
|
||||
label: item.title || item.deptName || item.name,
|
||||
value: item.id == null || item.id === '' ? '' : String(item.id),
|
||||
children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined,
|
||||
}));
|
||||
this.form.projectName = first.projectName;
|
||||
this.form.deptName = first.deptName;
|
||||
this.form.payerName = first.payerName;
|
||||
this.form.payeeName = first.payeeName;
|
||||
this.settlementDialog.visible = false;
|
||||
await this.loadReferenceInformation();
|
||||
this.$refs.formRef?.validateField('settlementIds').catch(() => {});
|
||||
},
|
||||
selectSettlement(row) {
|
||||
this.$refs.settlementTable?.toggleRowSelection(row);
|
||||
flattenDepartments(rows) {
|
||||
return (rows || []).flatMap(item => [item, ...this.flattenDepartments(item.children || [])]);
|
||||
},
|
||||
async loadReferenceInformation() {
|
||||
const data = this.unwrapData(await api.getReferenceInformation(this.settlementIds.join(',')));
|
||||
this.customerEmailOptions = data.customerEmails || [];
|
||||
this.departmentEmailOptions = [
|
||||
...new Set([...this.departmentEmailOptions, ...(data.departmentEmails || [])]),
|
||||
];
|
||||
if (!this.form.customerEmails.length && this.customerEmailOptions.length) {
|
||||
this.form.customerEmails = [this.customerEmailOptions[0]];
|
||||
}
|
||||
},
|
||||
remainingAmount(row) {
|
||||
return Math.max(
|
||||
Number(row.settlementAmount || 0) - Number(row.receivedInvoiceAmount || 0),
|
||||
0
|
||||
);
|
||||
handleDeptChange(value) {
|
||||
const deptId = Array.isArray(value) ? value.at(-1) : value;
|
||||
this.form.deptId = deptId || null;
|
||||
this.form.deptName = deptId ? this.deptMap.get(String(deptId)) || '' : '';
|
||||
this.$refs.formRef?.validateField('deptId').catch(() => {});
|
||||
},
|
||||
openSettlementDetail(row) {
|
||||
const id = row.formalSettlementId || row.settlementId || row.id;
|
||||
@@ -602,15 +513,6 @@ export default {
|
||||
validateBusiness() {
|
||||
const invoiceAmount = Number(this.form.invoiceAmount || 0);
|
||||
if (invoiceAmount <= 0) throw new Error('开票金额必须大于0');
|
||||
for (const row of this.form.settlements) {
|
||||
const allocated = Number(row.allocatedInvoiceAmount);
|
||||
if (!Number.isFinite(allocated) || allocated < 0) {
|
||||
throw new Error('分摊发票金额必须大于或等于0');
|
||||
}
|
||||
if (allocated - this.remainingAmount(row) > 0.001) {
|
||||
throw new Error(`结算单${row.formalSettlementNo}的累计收票金额不能超过结算总金额`);
|
||||
}
|
||||
}
|
||||
},
|
||||
payload() {
|
||||
return {
|
||||
@@ -631,10 +533,9 @@ export default {
|
||||
departmentEmails: this.form.departmentEmails.join(';'),
|
||||
attachmentsJson: JSON.stringify(this.form.attachments || []),
|
||||
remark: this.form.remark,
|
||||
settlements: this.form.settlements.map(item => ({
|
||||
settlementId: item.formalSettlementId || item.settlementId,
|
||||
allocatedInvoiceAmount: item.allocatedInvoiceAmount,
|
||||
})),
|
||||
// 结算组织=页面手选部门(先票后款不挂结算单,认领关系由结算单侧维护,不再传 settlements)
|
||||
deptId: this.form.deptId,
|
||||
deptName: this.form.deptName,
|
||||
};
|
||||
},
|
||||
async saveDraft() {
|
||||
@@ -678,8 +579,7 @@ export default {
|
||||
this.$message.warning('金蝶票池未查询到该发票');
|
||||
return;
|
||||
}
|
||||
if (!this.confirmInvoice(invoice, { preserveUserInput: true })) return;
|
||||
if (this.settlementIds.length) await this.loadReferenceInformation();
|
||||
if (!(await this.confirmInvoice(invoice, { preserveUserInput: true }))) return;
|
||||
this.$message.success('同步成功');
|
||||
},
|
||||
goBack() {
|
||||
@@ -688,6 +588,14 @@ export default {
|
||||
formatMoney(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
formatQuantity(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? String(number) : '-';
|
||||
},
|
||||
formatRateText(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
return `${value}%`;
|
||||
},
|
||||
formatRate(value) {
|
||||
return value === null || value === undefined || value === '' ? '' : `${Number(value)}%`;
|
||||
},
|
||||
@@ -738,15 +646,6 @@ export default {
|
||||
:global(.avue-layout--horizontal .receipt-form-page__actions) {
|
||||
left: 0;
|
||||
}
|
||||
.receipt-form-page__dialog-search {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.receipt-form-page__dialog-search .el-input {
|
||||
width: 360px;
|
||||
}
|
||||
.receipt-form-page :deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user