调整结算模块
This commit is contained in:
@@ -16,8 +16,6 @@ export const getFormalOptions = (current, size, params) =>
|
|||||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||||
export const match = id => request({ url: `${baseUrl}/match`, method: 'post', params: { id } });
|
export const match = id => request({ url: `${baseUrl}/match`, method: 'post', params: { id } });
|
||||||
export const manualMatch = data =>
|
|
||||||
request({ url: `${baseUrl}/manual-match`, method: 'post', data });
|
|
||||||
export const unmatch = internalId =>
|
export const unmatch = internalId =>
|
||||||
request({ url: `${baseUrl}/unmatch`, method: 'post', params: { internalId } });
|
request({ url: `${baseUrl}/unmatch`, method: 'post', params: { internalId } });
|
||||||
export const adjust = data => request({ url: `${baseUrl}/adjust`, method: 'post', data });
|
export const adjust = data => request({ url: `${baseUrl}/adjust`, method: 'post', data });
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ export const transportReconciliationFormFields = [
|
|||||||
{ prop: 'reconciliationNo', label: '对账单号', readonly: true },
|
{ prop: 'reconciliationNo', label: '对账单号', readonly: true },
|
||||||
{ prop: 'reconciliationMode', label: '对账模式', type: 'mode' },
|
{ prop: 'reconciliationMode', label: '对账模式', type: 'mode' },
|
||||||
{ prop: 'formalSettlementNo', label: '正式结算单', type: 'formal' },
|
{ prop: 'formalSettlementNo', label: '正式结算单', type: 'formal' },
|
||||||
{ prop: 'payerName', label: '付款方', readonly: true },
|
{ prop: 'customerName', label: '客户/承运商', readonly: true },
|
||||||
{ prop: 'payeeName', label: '收款方', readonly: true },
|
|
||||||
{ prop: 'projectName', label: '项目', readonly: true },
|
{ prop: 'projectName', label: '项目', readonly: true },
|
||||||
{ prop: 'deptName', label: '所属组织', readonly: true },
|
|
||||||
{ prop: 'contractName', label: '合同名称', readonly: true },
|
{ prop: 'contractName', label: '合同名称', readonly: true },
|
||||||
{ prop: 'paidAmount', label: '已付合计', money: true, readonly: true },
|
{ prop: 'paidAmount', label: '已付合计', money: true, readonly: true },
|
||||||
{ prop: 'reconcilerName', label: '对账人', readonly: true },
|
{ prop: 'reconcilerName', label: '对账人', readonly: true },
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
export const reconciliationMatchStatusOptions = [
|
export const reconciliationMatchStatusOptions = [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: 'all' },
|
||||||
{ label: '未匹配', value: 'unmatched' },
|
{ label: '未匹配', value: 'unmatched' },
|
||||||
{ label: '部分匹配', value: 'partial' },
|
{ label: '部分匹配', value: 'partial' },
|
||||||
{ label: '已匹配', value: 'matched' },
|
{ label: '已匹配', value: 'matched' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const reconciliationStatusOptions = [
|
export const reconciliationStatusOptions = [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: 'all' },
|
||||||
{ label: '未完成', value: 'unfinished' },
|
{ label: '未完成', value: 'unfinished' },
|
||||||
{ label: '已完成', value: 'completed' },
|
{ label: '已完成', value: 'completed' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -27,13 +27,10 @@ export const internalColumns = [
|
|||||||
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
|
{ prop: 'transportType', label: '运输类型', minWidth: 120 },
|
||||||
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
||||||
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
||||||
{ prop: 'specification', label: '规格', minWidth: 100 },
|
|
||||||
{ prop: 'model', label: '型号', minWidth: 100 },
|
|
||||||
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
||||||
{ prop: 'mileage', label: '里程(KM)', minWidth: 100, number: true },
|
{ prop: 'mileage', label: '里程(KM)', minWidth: 120, number: true },
|
||||||
{ prop: 'batchNo', label: '批次号', minWidth: 100 },
|
{ prop: 'batchNo', label: '批次号', minWidth: 100 },
|
||||||
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
||||||
{ prop: 'freightAmount', label: '运输费', minWidth: 110, money: true },
|
|
||||||
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
|
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
|
||||||
{ prop: 'matchResult', label: '匹配结果', minWidth: 100 },
|
{ prop: 'matchResult', label: '匹配结果', minWidth: 100 },
|
||||||
{ prop: 'updateResult', label: '账单更新结果', minWidth: 130 },
|
{ prop: 'updateResult', label: '账单更新结果', minWidth: 130 },
|
||||||
@@ -50,7 +47,7 @@ export const externalVehicleColumns = [
|
|||||||
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
{ prop: 'cargoName', label: '货物名称', minWidth: 120 },
|
||||||
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
{ prop: 'cargoType', label: '货物类型', minWidth: 120 },
|
||||||
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
{ prop: 'transportQuantity', label: '运输总量', minWidth: 100, number: true },
|
||||||
{ prop: 'mileage', label: '里程(KM)', minWidth: 100, number: true },
|
{ prop: 'mileage', label: '里程(KM)', minWidth: 120, number: true },
|
||||||
{ prop: 'batchNo', label: '批次号', minWidth: 100 },
|
{ prop: 'batchNo', label: '批次号', minWidth: 100 },
|
||||||
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
||||||
{ prop: 'freightAmount', label: '运费', minWidth: 110, money: true },
|
{ prop: 'freightAmount', label: '运费', minWidth: 110, money: true },
|
||||||
@@ -71,7 +68,7 @@ export const externalCargoColumns = [
|
|||||||
{ prop: 'model', label: '型号', minWidth: 100 },
|
{ prop: 'model', label: '型号', minWidth: 100 },
|
||||||
{ prop: 'transportQuantity', label: '运输量', minWidth: 100, number: true },
|
{ prop: 'transportQuantity', label: '运输量', minWidth: 100, number: true },
|
||||||
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
{ prop: 'unitPrice', label: '运输单价', minWidth: 110, money: true },
|
||||||
{ prop: 'mileage', label: '里程(KM)', minWidth: 100, number: true },
|
{ prop: 'mileage', label: '里程(KM)', minWidth: 120, number: true },
|
||||||
{ prop: 'freightAmount', label: '运输费', minWidth: 110, money: true },
|
{ prop: 'freightAmount', label: '运输费', minWidth: 110, money: true },
|
||||||
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
|
{ prop: 'settlementAmount', label: '结算金额', minWidth: 120, money: true },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -116,6 +116,21 @@ export default [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/settlement/transport-reconciliation/form',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '新增编辑运输对账单',
|
||||||
|
meta: {
|
||||||
|
keepAlive: false,
|
||||||
|
activeMenu: '/settlement/transport-reconciliation',
|
||||||
|
},
|
||||||
|
component: () => import('@/views/settlement/transport-reconciliation-form.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/business/contract-manage/form',
|
path: '/business/contract-manage/form',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@@ -164,6 +179,18 @@ export default [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/business/waybill-import/form',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: '新建导入运单',
|
||||||
|
meta: { keepAlive: false, activeMenu: '/business/waybill-import' },
|
||||||
|
component: () => import('@/views/business/waybill-import-form.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/business/loading-manage/form',
|
path: '/business/loading-manage/form',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|||||||
+15
-11
@@ -54,8 +54,8 @@
|
|||||||
<template #taxRate-form>
|
<template #taxRate-form>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.taxRate"
|
v-model="form.taxRate"
|
||||||
inputmode="decimal"
|
inputmode="numeric"
|
||||||
maxlength="6"
|
maxlength="3"
|
||||||
clearable
|
clearable
|
||||||
placeholder="请输入税率"
|
placeholder="请输入税率"
|
||||||
@input="handleTaxRateInput"
|
@input="handleTaxRateInput"
|
||||||
@@ -183,8 +183,8 @@ export default {
|
|||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
const text = String(value ?? '').trim();
|
const text = String(value ?? '').trim();
|
||||||
const rate = Number(text);
|
const rate = Number(text);
|
||||||
if (!/^\d+(\.\d{1,2})?$/.test(text) || rate < 0 || rate > 100) {
|
if (!/^\d+$/.test(text) || rate < 0 || rate > 100) {
|
||||||
callback(new Error('税率必须是0到100之间且最多保留2位小数的数字'));
|
callback(new Error('税率必须是0到100之间的整数'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
@@ -364,7 +364,10 @@ export default {
|
|||||||
return option ? `${option.dictValue}/${option.dictKey}` : feeCategory;
|
return option ? `${option.dictValue}/${option.dictKey}` : feeCategory;
|
||||||
},
|
},
|
||||||
formatTaxRate(value) {
|
formatTaxRate(value) {
|
||||||
return value === undefined || value === null || value === '' ? '' : `${value}%`;
|
const text = String(value ?? '').trim();
|
||||||
|
if (text === '') return '';
|
||||||
|
const rate = Number(text);
|
||||||
|
return Number.isFinite(rate) ? `${Math.trunc(Math.abs(rate))}%` : `${text}%`;
|
||||||
},
|
},
|
||||||
getFeeItemCodeSuffix(code, feeCategory) {
|
getFeeItemCodeSuffix(code, feeCategory) {
|
||||||
const value = String(code || '').trim();
|
const value = String(code || '').trim();
|
||||||
@@ -397,6 +400,11 @@ export default {
|
|||||||
const values = { ...row };
|
const values = { ...row };
|
||||||
values.feeCategory = String(values.feeCategory || '').trim();
|
values.feeCategory = String(values.feeCategory || '').trim();
|
||||||
values.englishName = this.getFeeItemCodeSuffix(values.englishName, values.feeCategory);
|
values.englishName = this.getFeeItemCodeSuffix(values.englishName, values.feeCategory);
|
||||||
|
const taxRateText = String(values.taxRate ?? '').trim();
|
||||||
|
const taxRate = Number(taxRateText);
|
||||||
|
if (taxRateText !== '' && Number.isFinite(taxRate)) {
|
||||||
|
values.taxRate = String(Math.trunc(Math.abs(taxRate)));
|
||||||
|
}
|
||||||
return values;
|
return values;
|
||||||
},
|
},
|
||||||
validateUnique(row) {
|
validateUnique(row) {
|
||||||
@@ -497,12 +505,8 @@ export default {
|
|||||||
done();
|
done();
|
||||||
},
|
},
|
||||||
handleTaxRateInput(value) {
|
handleTaxRateInput(value) {
|
||||||
let normalized = String(value || '').replace(/[^\d.]/g, '');
|
let normalized = String(value ?? '').replace(/\D/g, '');
|
||||||
normalized = normalized.replace(/(\..*)\./g, '$1');
|
if (normalized.length > 1) normalized = normalized.replace(/^0+/, '') || '0';
|
||||||
if (normalized.startsWith('.')) normalized = `0${normalized}`;
|
|
||||||
const [integerPart, decimalPart] = normalized.split('.');
|
|
||||||
normalized =
|
|
||||||
decimalPart === undefined ? integerPart : `${integerPart}.${decimalPart.slice(0, 2)}`;
|
|
||||||
this.form.taxRate = normalized;
|
this.form.taxRate = normalized;
|
||||||
},
|
},
|
||||||
searchReset() {
|
searchReset() {
|
||||||
|
|||||||
+24
-25
@@ -42,13 +42,6 @@
|
|||||||
</section-card>
|
</section-card>
|
||||||
|
|
||||||
<el-button-group>
|
<el-button-group>
|
||||||
<el-button
|
|
||||||
v-if="permission.region_add"
|
|
||||||
type="primary"
|
|
||||||
icon="el-icon-plus"
|
|
||||||
@click="addCountry"
|
|
||||||
>新增国家
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
<el-button
|
||||||
v-if="permission.region_add"
|
v-if="permission.region_add"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -181,19 +174,17 @@ export default {
|
|||||||
treeLoad: (node, resolve) => {
|
treeLoad: (node, resolve) => {
|
||||||
const parentCode = node.level === 0 ? ROOT_PARENT_CODE : node.data.id;
|
const parentCode = node.level === 0 ? ROOT_PARENT_CODE : node.data.id;
|
||||||
getLazyTree(parentCode, this.searchForm).then(res => {
|
getLazyTree(parentCode, this.searchForm).then(res => {
|
||||||
resolve(
|
const treeData = this.normalizeTreeData(res.data.data);
|
||||||
res.data.data.map(item => {
|
if (node.level === 0) {
|
||||||
return {
|
this.setDefaultExpandedKeys(treeData);
|
||||||
...item,
|
}
|
||||||
leaf: !item.hasChildren,
|
resolve(treeData);
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addBtn: false,
|
addBtn: false,
|
||||||
menu: false,
|
menu: false,
|
||||||
defaultExpandAll: false,
|
defaultExpandAll: false,
|
||||||
|
defaultExpandedKeys: [],
|
||||||
size: 'default',
|
size: 'default',
|
||||||
props: {
|
props: {
|
||||||
labelText: '标题',
|
labelText: '标题',
|
||||||
@@ -260,6 +251,7 @@ export default {
|
|||||||
prop: 'regionLevel',
|
prop: 'regionLevel',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
dicUrl: '/blade-system/dict/dictionary?code=region',
|
dicUrl: '/blade-system/dict/dictionary?code=region',
|
||||||
|
dicFormatter: list => list.filter(item => Number(item.dictKey) !== 0),
|
||||||
props: {
|
props: {
|
||||||
label: 'dictValue',
|
label: 'dictValue',
|
||||||
value: 'dictKey',
|
value: 'dictKey',
|
||||||
@@ -436,24 +428,31 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initTree() {
|
normalizeTreeData(data) {
|
||||||
this.treeData = [];
|
return data.map(item => {
|
||||||
getLazyTree(this.topCode, this.searchForm).then(res => {
|
|
||||||
this.treeData = res.data.data.map(item => {
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
leaf: !item.hasChildren,
|
leaf: !item.hasChildren,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
setDefaultExpandedKeys(treeData) {
|
||||||
|
const valueKey = this.treeOption.props.value;
|
||||||
|
this.treeOption.defaultExpandedKeys = treeData
|
||||||
|
.filter(item => item.hasChildren)
|
||||||
|
.map(item => item[valueKey]);
|
||||||
|
},
|
||||||
|
initTree() {
|
||||||
|
this.treeData = [];
|
||||||
|
getLazyTree(this.topCode, this.searchForm).then(res => {
|
||||||
|
const treeData = this.normalizeTreeData(res.data.data);
|
||||||
|
this.treeData = treeData;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.setDefaultExpandedKeys(treeData);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
hasSearchCriteria() {
|
|
||||||
return Object.values(this.searchForm).some(
|
|
||||||
value => value !== '' && value !== undefined && value !== null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
this.treeOption.defaultExpandAll = this.hasSearchCriteria();
|
|
||||||
this.initTree();
|
this.initTree();
|
||||||
this.regionForm = {};
|
this.regionForm = {};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -116,8 +116,8 @@
|
|||||||
><el-input
|
><el-input
|
||||||
v-else
|
v-else
|
||||||
v-model="row.taxRate"
|
v-model="row.taxRate"
|
||||||
inputmode="decimal"
|
inputmode="numeric"
|
||||||
maxlength="6"
|
maxlength="3"
|
||||||
clearable
|
clearable
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
@input="value => taxRateInput(row, value)"
|
@input="value => taxRateInput(row, value)"
|
||||||
@@ -678,23 +678,26 @@ export default {
|
|||||||
const feeItem = this.feeItemOptions(row).find(
|
const feeItem = this.feeItemOptions(row).find(
|
||||||
item => String(item.name || item.englishName || '') === String(value || '')
|
item => String(item.name || item.englishName || '') === String(value || '')
|
||||||
);
|
);
|
||||||
row.taxRate = this.hasTaxRate(feeItem?.taxRate) ? String(feeItem.taxRate) : '';
|
row.taxRate = this.hasTaxRate(feeItem?.taxRate) ? this.normalizeTaxRate(feeItem.taxRate) : '';
|
||||||
},
|
},
|
||||||
fillRuleTaxRate(row) {
|
fillRuleTaxRate(row) {
|
||||||
if (this.hasTaxRate(row.taxRate) || !row.feeItem) return;
|
if (this.hasTaxRate(row.taxRate) || !row.feeItem) return;
|
||||||
const feeItem = this.feeItemOptions(row).find(
|
const feeItem = this.feeItemOptions(row).find(
|
||||||
item => String(item.name || item.englishName || '') === String(row.feeItem)
|
item => String(item.name || item.englishName || '') === String(row.feeItem)
|
||||||
);
|
);
|
||||||
if (this.hasTaxRate(feeItem?.taxRate)) row.taxRate = String(feeItem.taxRate);
|
if (this.hasTaxRate(feeItem?.taxRate)) row.taxRate = this.normalizeTaxRate(feeItem.taxRate);
|
||||||
},
|
},
|
||||||
hasTaxRate(value) {
|
hasTaxRate(value) {
|
||||||
return value !== undefined && value !== null && String(value).trim() !== '';
|
return value !== undefined && value !== null && String(value).trim() !== '';
|
||||||
},
|
},
|
||||||
|
normalizeTaxRate(value) {
|
||||||
|
const rate = Number(String(value ?? '').trim());
|
||||||
|
return Number.isFinite(rate) ? String(Math.trunc(Math.abs(rate))) : '';
|
||||||
|
},
|
||||||
taxRateInput(row, value) {
|
taxRateInput(row, value) {
|
||||||
const text = String(value ?? '').replace(/[^\d.]/g, '');
|
let text = String(value ?? '').replace(/\D/g, '');
|
||||||
const parts = text.split('.');
|
if (text.length > 1) text = text.replace(/^0+/, '') || '0';
|
||||||
row.taxRate =
|
row.taxRate = text;
|
||||||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
|
||||||
},
|
},
|
||||||
billingTypes(row) {
|
billingTypes(row) {
|
||||||
return this.typeMap[row.billingElement] || [];
|
return this.typeMap[row.billingElement] || [];
|
||||||
@@ -845,8 +848,8 @@ export default {
|
|||||||
feeItemSet.add(feeItem);
|
feeItemSet.add(feeItem);
|
||||||
if (this.hasTaxRate(row.taxRate)) {
|
if (this.hasTaxRate(row.taxRate)) {
|
||||||
const taxRate = Number(row.taxRate);
|
const taxRate = Number(row.taxRate);
|
||||||
if (!/^\d+(\.\d{1,2})?$/.test(String(row.taxRate)) || taxRate < 0 || taxRate > 100) {
|
if (!/^\d+$/.test(String(row.taxRate).trim()) || taxRate < 0 || taxRate > 100) {
|
||||||
this.$message.warning(`第${i + 1}行税率必须在0到100之间且最多保留2位小数`);
|
this.$message.warning(`第${i + 1}行税率必须是0到100之间的整数`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<component
|
<component
|
||||||
|
v-if="!createPage"
|
||||||
:is="standalone ? 'div' : 'el-dialog'"
|
:is="standalone ? 'div' : 'el-dialog'"
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="导入运单"
|
title="导入运单"
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="waybill-import-toolbar__actions">
|
<div class="waybill-import-toolbar__actions">
|
||||||
<el-button type="primary" @click="openCreate">新建导入</el-button
|
<el-button type="primary" @click="handleCreate">新建导入</el-button
|
||||||
><el-button type="danger" plain :disabled="!selected.length" @click="removeBatches"
|
><el-button type="danger" plain :disabled="!selected.length" @click="removeBatches"
|
||||||
>批量删除</el-button
|
>批量删除</el-button
|
||||||
>
|
>
|
||||||
@@ -84,7 +85,11 @@
|
|||||||
>
|
>
|
||||||
<template #default="{ row }"
|
<template #default="{ row }"
|
||||||
><el-link type="primary" @click="openDetail(row)">查看</el-link
|
><el-link type="primary" @click="openDetail(row)">查看</el-link
|
||||||
><el-link type="primary" @click="editBatch(row)">编辑</el-link
|
><el-link
|
||||||
|
v-if="row.importStatus === 'draft'"
|
||||||
|
type="primary"
|
||||||
|
@click="editBatch(row)"
|
||||||
|
>编辑</el-link
|
||||||
><el-link type="danger" @click="removeBatch(row)">删除</el-link></template
|
><el-link type="danger" @click="removeBatch(row)">删除</el-link></template
|
||||||
>
|
>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -173,13 +178,14 @@
|
|||||||
/>
|
/>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
<component
|
||||||
|
:is="createPage ? 'div' : 'el-dialog'"
|
||||||
v-model="createVisible"
|
v-model="createVisible"
|
||||||
title="新建导入"
|
title="新建导入"
|
||||||
width="85%"
|
width="85%"
|
||||||
append-to-body
|
append-to-body
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
class="waybill-import-create"
|
:class="['waybill-import-create', { 'waybill-import-create--page': createPage }]"
|
||||||
>
|
>
|
||||||
<section class="waybill-import-create__form-panel">
|
<section class="waybill-import-create__form-panel">
|
||||||
<el-form
|
<el-form
|
||||||
@@ -236,9 +242,12 @@
|
|||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="承运类型" prop="carrierType">
|
<el-form-item label="承运类型" prop="carrierType">
|
||||||
<el-select v-model="form.carrierType" @change="carrierTypeChange">
|
<el-select v-model="form.carrierType" @change="carrierTypeChange">
|
||||||
<el-option label="承运商" value="承运商" />
|
<el-option
|
||||||
<el-option label="自运" value="自运" />
|
v-for="item in carrierTypeOptions"
|
||||||
<el-option label="网货平台" value="网货平台" />
|
:key="item"
|
||||||
|
:label="item"
|
||||||
|
:value="item"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -267,8 +276,12 @@
|
|||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="导入状态" prop="status">
|
<el-form-item label="导入状态" prop="status">
|
||||||
<el-select v-model="form.status">
|
<el-select v-model="form.status">
|
||||||
<el-option label="完成" value="completed" />
|
<el-option
|
||||||
<el-option label="进行中" value="processing" />
|
v-for="item in importStatusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -307,8 +320,8 @@
|
|||||||
><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item
|
><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item
|
||||||
>
|
>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="waybill-import-create__form-actions">
|
<div v-if="!createPage" class="waybill-import-create__form-actions">
|
||||||
<el-button @click="createVisible = false">关闭</el-button
|
<el-button @click="closeCreate">关闭</el-button
|
||||||
><el-button @click="saveDraft">保存草稿</el-button
|
><el-button @click="saveDraft">保存草稿</el-button
|
||||||
><el-button type="primary" @click="confirmImport">确认导入</el-button>
|
><el-button type="primary" @click="confirmImport">确认导入</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -373,7 +386,8 @@
|
|||||||
clearable
|
clearable
|
||||||
filterable
|
filterable
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
@change="value => handleEditorChange(column, row, value)" /><el-select
|
@change="value => handleEditorChange(column, row, value)"
|
||||||
|
/><el-select
|
||||||
v-else-if="column.editor === 'cargoName'"
|
v-else-if="column.editor === 'cargoName'"
|
||||||
v-model="row.cargoName"
|
v-model="row.cargoName"
|
||||||
clearable
|
clearable
|
||||||
@@ -392,20 +406,24 @@
|
|||||||
type="date"
|
type="date"
|
||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
placeholder="请选择" /><el-input
|
placeholder="请选择"
|
||||||
|
/><el-input
|
||||||
v-else-if="column.editor === 'textarea'"
|
v-else-if="column.editor === 'textarea'"
|
||||||
v-model="row[column.prop]"
|
v-model="row[column.prop]"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:autosize="{ minRows: 1, maxRows: 3 }"
|
:autosize="{ minRows: 1, maxRows: 3 }"
|
||||||
:placeholder="`请输入${column.label}`" /><el-input
|
:placeholder="`请输入${column.label}`"
|
||||||
|
/><el-input
|
||||||
v-else-if="column.editor === 'number'"
|
v-else-if="column.editor === 'number'"
|
||||||
:model-value="row[column.prop]"
|
:model-value="row[column.prop]"
|
||||||
inputmode="decimal"
|
inputmode="decimal"
|
||||||
:placeholder="`请输入${column.label}`"
|
:placeholder="`请输入${column.label}`"
|
||||||
@input="value => updateEditorValue(column, row, value)" /><el-input
|
@input="value => updateEditorValue(column, row, value)"
|
||||||
v-else
|
/><el-input
|
||||||
|
v-else-if="column.editor"
|
||||||
v-model="row[column.prop]"
|
v-model="row[column.prop]"
|
||||||
:placeholder="`请输入${column.label}`" /></template
|
:placeholder="`请输入${column.label}`"
|
||||||
|
/><span v-else>{{ formatCell(row, column) }}</span></template
|
||||||
><span v-else>{{ formatCell(row, column) }}</span></template
|
><span v-else>{{ formatCell(row, column) }}</span></template
|
||||||
></el-table-column
|
></el-table-column
|
||||||
><el-table-column label="操作" width="180" fixed="right"
|
><el-table-column label="操作" width="180" fixed="right"
|
||||||
@@ -421,12 +439,19 @@
|
|||||||
></el-table
|
></el-table
|
||||||
>
|
>
|
||||||
</section>
|
</section>
|
||||||
</el-dialog>
|
<div v-if="createPage" class="waybill-import-create__footer">
|
||||||
|
<el-button @click="closeCreate">取消</el-button>
|
||||||
|
<el-button @click="saveDraft">保存草稿</el-button>
|
||||||
|
<el-button type="primary" @click="confirmImport">确认导入</el-button>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||||
import { getList as getContractList } from '@/api/business/contract-manage';
|
import { getList as getContractList } from '@/api/business/contract-manage';
|
||||||
@@ -436,19 +461,22 @@ import { getDictionary } from '@/api/system/dictbiz';
|
|||||||
import * as api from '@/api/business/waybill-manage';
|
import * as api from '@/api/business/waybill-manage';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
|
|
||||||
const props = defineProps({ modelValue: Boolean, standalone: Boolean });
|
const props = defineProps({ modelValue: Boolean, standalone: Boolean, createPage: Boolean });
|
||||||
const emit = defineEmits(['update:modelValue', 'closed']);
|
const emit = defineEmits(['update:modelValue', 'closed']);
|
||||||
|
const router = useRouter();
|
||||||
const visible = computed({
|
const visible = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: value => emit('update:modelValue', value),
|
set: value => emit('update:modelValue', value),
|
||||||
});
|
});
|
||||||
const handleClosed = () => emit('closed');
|
const handleClosed = () => emit('closed');
|
||||||
const createVisible = ref(false),
|
const createVisible = ref(props.createPage),
|
||||||
detailVisible = ref(false),
|
detailVisible = ref(false),
|
||||||
formRef = ref();
|
formRef = ref();
|
||||||
const query = reactive({ batchNo: '', carrierId: '', createUser: '', createTimeRange: [] });
|
const query = reactive({ batchNo: '', carrierId: '', createUser: '', createTimeRange: [] });
|
||||||
const detailQuery = reactive({ vehicleNo: '', cargoName: '' });
|
const detailQuery = reactive({ vehicleNo: '', cargoName: '' });
|
||||||
const createDefaultForm = () => ({
|
const createDefaultForm = () => ({
|
||||||
|
id: '',
|
||||||
|
batchNo: '',
|
||||||
projectId: '',
|
projectId: '',
|
||||||
projectName: '',
|
projectName: '',
|
||||||
customerId: '',
|
customerId: '',
|
||||||
@@ -466,6 +494,14 @@ const createDefaultForm = () => ({
|
|||||||
file: null,
|
file: null,
|
||||||
});
|
});
|
||||||
const form = reactive(createDefaultForm());
|
const form = reactive(createDefaultForm());
|
||||||
|
// 批量导入状态仅保留草稿与导入完成两种,与后端 importStatus 取值一致。
|
||||||
|
const importStatusOptions = [
|
||||||
|
{ label: '草稿', value: 'draft' },
|
||||||
|
{ label: '导入完成', value: 'completed' },
|
||||||
|
];
|
||||||
|
// 编辑草稿时明细已入库,此时不再强制重新上传附件。
|
||||||
|
const validateImportFile = (rule, value, callback) =>
|
||||||
|
form.file || rows.value.length ? callback() : callback(new Error('请上传明细表'));
|
||||||
const rules = {
|
const rules = {
|
||||||
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
||||||
customerName: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
customerName: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||||
@@ -475,8 +511,10 @@ const rules = {
|
|||||||
carrierContractId: [{ required: true, message: '请选择承运商合同', trigger: 'change' }],
|
carrierContractId: [{ required: true, message: '请选择承运商合同', trigger: 'change' }],
|
||||||
status: [{ required: true, message: '请选择导入状态', trigger: 'change' }],
|
status: [{ required: true, message: '请选择导入状态', trigger: 'change' }],
|
||||||
importType: [{ required: true, message: '请选择导入类型', trigger: 'change' }],
|
importType: [{ required: true, message: '请选择导入类型', trigger: 'change' }],
|
||||||
file: [{ required: true, message: '请上传明细表', trigger: 'change' }],
|
file: [{ required: true, validator: validateImportFile, trigger: 'change' }],
|
||||||
};
|
};
|
||||||
|
// 保存草稿只落库批次与已解析的明细,仅校验后端建批次必填项,不校验附件与明细完整性。
|
||||||
|
const draftRequiredFields = ['projectId', 'contractId', 'carrierType', 'importType'];
|
||||||
const batches = ref([]),
|
const batches = ref([]),
|
||||||
details = ref([]),
|
details = ref([]),
|
||||||
rows = ref([]),
|
rows = ref([]),
|
||||||
@@ -501,7 +539,14 @@ const duplicateRows = computed(() => rows.value.filter(row => row._duplicate));
|
|||||||
const previewRows = computed(() =>
|
const previewRows = computed(() =>
|
||||||
previewTab.value === 'duplicate' ? duplicateRows.value : rows.value
|
previewTab.value === 'duplicate' ? duplicateRows.value : rows.value
|
||||||
);
|
);
|
||||||
|
const carrierContractsLoaded = ref(false),
|
||||||
|
hasCarrierContracts = ref(null);
|
||||||
|
const carrierTypes = ['承运商', '自运', '网货平台'];
|
||||||
const isSelfOperated = computed(() => form.carrierType === '自运');
|
const isSelfOperated = computed(() => form.carrierType === '自运');
|
||||||
|
// 项目下没有承运商合同时只允许自运,与运单管理的承运类型收窄规则一致。
|
||||||
|
const carrierTypeOptions = computed(() =>
|
||||||
|
carrierContractsLoaded.value && hasCarrierContracts.value === false ? ['自运'] : carrierTypes
|
||||||
|
);
|
||||||
const carrierSelectionValue = computed(() =>
|
const carrierSelectionValue = computed(() =>
|
||||||
isSelfOperated.value ? form.carrierName : form.carrierContractId
|
isSelfOperated.value ? form.carrierName : form.carrierContractId
|
||||||
);
|
);
|
||||||
@@ -518,46 +563,26 @@ const resetCreateForm = () => {
|
|||||||
contracts.value = [];
|
contracts.value = [];
|
||||||
carrierContracts.value = [];
|
carrierContracts.value = [];
|
||||||
carrierOptions.value = [];
|
carrierOptions.value = [];
|
||||||
|
carrierContractsLoaded.value = false;
|
||||||
|
hasCarrierContracts.value = null;
|
||||||
};
|
};
|
||||||
|
// 运单批次号由系统自动生成,整批共用一个,明细表内只读展示。
|
||||||
const detailColumns = [
|
const detailColumns = [
|
||||||
{ prop: 'batchNo', label: '运单批次号', editor: 'input', minWidth: 150 },
|
{ prop: 'batchNo', label: '运单批次号', minWidth: 150 },
|
||||||
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
|
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
|
||||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
|
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
|
||||||
{ prop: 'driverName', label: '司机/船长', editor: 'driver', minWidth: 170 },
|
{ prop: 'driverName', label: '司机/船长', editor: 'driver', minWidth: 170 },
|
||||||
{ prop: 'transportType', label: '运输方式', editor: 'transportType', minWidth: 150 },
|
{ prop: 'transportType', label: '运输类型', editor: 'transportType', minWidth: 150 },
|
||||||
{ prop: 'cargoType', label: '货物类型', editor: 'cargoType', minWidth: 220 },
|
|
||||||
{ prop: 'cargoName', label: '货物名称', editor: 'cargoName', minWidth: 220 },
|
{ prop: 'cargoName', label: '货物名称', editor: 'cargoName', minWidth: 220 },
|
||||||
|
{ prop: 'cargoType', label: '货物类型', editor: 'cargoType', minWidth: 220 },
|
||||||
{ prop: 'quantity', label: '重量', editor: 'number', minWidth: 130 },
|
{ prop: 'quantity', label: '重量', editor: 'number', minWidth: 130 },
|
||||||
{ prop: 'departureAddress', label: '发货地址', editor: 'textarea', minWidth: 260 },
|
|
||||||
{ prop: 'departureContact', label: '发货联系人', editor: 'input', minWidth: 140 },
|
|
||||||
{ prop: 'departurePhone', label: '发货联系人电话', editor: 'input', minWidth: 160 },
|
|
||||||
{ prop: 'arrivalAddress', label: '到货地址', editor: 'textarea', minWidth: 260 },
|
|
||||||
{ prop: 'arrivalContact', label: '收货联系人', editor: 'input', minWidth: 140 },
|
|
||||||
{ prop: 'arrivalPhone', label: '收货联系人电话', editor: 'input', minWidth: 160 },
|
|
||||||
{ prop: 'startDate', label: '开始时间', editor: 'date', minWidth: 150 },
|
|
||||||
{ prop: 'endDate', label: '结束时间', editor: 'date', minWidth: 150 },
|
|
||||||
{ prop: 'unitPrice', label: '单价', editor: 'number', minWidth: 130 },
|
|
||||||
{ prop: 'freight', label: '运费', editor: 'number', minWidth: 130 },
|
|
||||||
{ prop: 'otherFeeTotal', label: '其他费用合计', editor: 'number', minWidth: 150 },
|
|
||||||
{ prop: 'freightTotal', label: '运费合计', editor: 'number', minWidth: 130 },
|
|
||||||
{ prop: 'remark', label: '备注', editor: 'textarea', minWidth: 180 },
|
|
||||||
];
|
];
|
||||||
const requiredDetailFields = [
|
const requiredDetailFields = [
|
||||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
|
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
|
||||||
{ prop: 'driverName', label: '司机/船长' },
|
{ prop: 'driverName', label: '司机/船长' },
|
||||||
{ prop: 'transportType', label: '运输方式' },
|
{ prop: 'transportType', label: '运输类型' },
|
||||||
{ prop: 'cargoName', label: '货物名称' },
|
{ prop: 'cargoName', label: '货物名称' },
|
||||||
{ prop: 'cargoType', label: '货物类型' },
|
{ prop: 'cargoType', label: '货物类型' },
|
||||||
{ prop: 'departureAddress', label: '发货地址' },
|
|
||||||
{ prop: 'departureContact', label: '发货联系人' },
|
|
||||||
{ prop: 'departurePhone', label: '发货联系人电话' },
|
|
||||||
{ prop: 'arrivalAddress', label: '到货地址' },
|
|
||||||
{ prop: 'arrivalContact', label: '到货联系人' },
|
|
||||||
{ prop: 'arrivalPhone', label: '收货联系人电话' },
|
|
||||||
{ prop: 'startDate', label: '开始时间' },
|
|
||||||
{ prop: 'endDate', label: '结束时间' },
|
|
||||||
{ prop: 'unitPrice', label: '单价' },
|
|
||||||
{ prop: 'freight', label: '运费' },
|
|
||||||
];
|
];
|
||||||
const projectQueryParams = { approvalStatuses: 'approved,change_approved' };
|
const projectQueryParams = { approvalStatuses: 'approved,change_approved' };
|
||||||
const extractRecords = res => {
|
const extractRecords = res => {
|
||||||
@@ -568,24 +593,62 @@ const extractRecords = res => {
|
|||||||
if (Array.isArray(data?.data?.records)) return data.data.records;
|
if (Array.isArray(data?.data?.records)) return data.data.records;
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
// 批次号规则:PC + 导入日期 + 当日批次流水号(4 位),如 PC202606010001。
|
||||||
|
const batchNoPrefix = 'PC';
|
||||||
|
const batchNoSequenceLength = 4;
|
||||||
|
const generateBatchNo = async () => {
|
||||||
|
const prefix = `${batchNoPrefix}${dayjs().format('YYYYMMDD')}`;
|
||||||
|
let maxSequence = 0;
|
||||||
|
try {
|
||||||
|
const res = await api.getImportBatches({ current: 1, size: 9999 });
|
||||||
|
extractRecords(res).forEach(item => {
|
||||||
|
const no = String(item.batchNo || '').trim();
|
||||||
|
if (!no.startsWith(prefix)) return;
|
||||||
|
maxSequence = Math.max(maxSequence, Number(no.slice(prefix.length)) || 0);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
maxSequence = 0;
|
||||||
|
}
|
||||||
|
return `${prefix}${String(maxSequence + 1).padStart(batchNoSequenceLength, '0')}`;
|
||||||
|
};
|
||||||
|
// 承运商合同的承运商固定取合同乙方,与运单管理(waybill-manage-page)保持一致。
|
||||||
|
const getCarrierContractPartyName = (contract = {}) =>
|
||||||
|
String(
|
||||||
|
contract.partyB ||
|
||||||
|
contract.partyBName ||
|
||||||
|
contract.contractPartyB ||
|
||||||
|
contract.customerContractPartyB ||
|
||||||
|
contract.secondParty ||
|
||||||
|
contract.secondPartyName ||
|
||||||
|
''
|
||||||
|
).trim();
|
||||||
const buildCarrierContractOptions = contractRows => {
|
const buildCarrierContractOptions = contractRows => {
|
||||||
const validContracts = contractRows.filter(
|
const validContracts = contractRows.filter(
|
||||||
item => String(item.contractCategory || '').trim() === '承运商合同'
|
item => String(item.contractCategory || '').trim() === '承运商合同'
|
||||||
);
|
);
|
||||||
const nameCounts = validContracts.reduce((result, item) => {
|
const nameCounts = validContracts.reduce((result, item) => {
|
||||||
const name = String(item.partyA || item.partyAName || '').trim();
|
const name = getCarrierContractPartyName(item);
|
||||||
if (name) result[name] = (result[name] || 0) + 1;
|
if (name) result[name] = (result[name] || 0) + 1;
|
||||||
return result;
|
return result;
|
||||||
}, {});
|
}, {});
|
||||||
return validContracts
|
return validContracts
|
||||||
.map(contract => {
|
.map(contract => {
|
||||||
const carrierName = String(contract.partyA || contract.partyAName || '').trim();
|
const carrierName = getCarrierContractPartyName(contract);
|
||||||
if (!contract.id || !carrierName) return null;
|
if (!contract.id || !carrierName) return null;
|
||||||
const identifier = contract.contractName || contract.contractNo || contract.id;
|
const identifier = contract.contractName || contract.contractNo || contract.id;
|
||||||
return {
|
return {
|
||||||
id: contract.partyAId || contract.partyAUserId || contract.customerId || '',
|
id:
|
||||||
|
contract.partyBId ||
|
||||||
|
contract.partyBUserId ||
|
||||||
|
contract.contractPartyBId ||
|
||||||
|
contract.customerContractPartyBId ||
|
||||||
|
contract.secondPartyId ||
|
||||||
|
contract.secondPartyUserId ||
|
||||||
|
contract.carrierId ||
|
||||||
|
'',
|
||||||
carrierContractId: contract.id,
|
carrierContractId: contract.id,
|
||||||
carrierName,
|
carrierName,
|
||||||
|
fullName: carrierName,
|
||||||
label: nameCounts[carrierName] > 1 ? `${carrierName}(${identifier})` : carrierName,
|
label: nameCounts[carrierName] > 1 ? `${carrierName}(${identifier})` : carrierName,
|
||||||
value: contract.id,
|
value: contract.id,
|
||||||
};
|
};
|
||||||
@@ -604,7 +667,7 @@ const syncCarrierOptions = () => {
|
|||||||
const customerContract = contracts.value.find(
|
const customerContract = contracts.value.find(
|
||||||
item => String(item.id) === String(form.contractId)
|
item => String(item.id) === String(form.contractId)
|
||||||
);
|
);
|
||||||
const carrierName = String(customerContract?.partyB || '').trim();
|
const carrierName = getCarrierContractPartyName(customerContract || {});
|
||||||
if (!carrierName) {
|
if (!carrierName) {
|
||||||
carrierOptions.value = [];
|
carrierOptions.value = [];
|
||||||
return;
|
return;
|
||||||
@@ -757,6 +820,7 @@ const loadBatches = async () => {
|
|||||||
};
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.standalone) loadBatches();
|
if (props.standalone) loadBatches();
|
||||||
|
if (props.createPage) openCreate();
|
||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
@@ -782,22 +846,50 @@ const loadDetails = async () => {
|
|||||||
const openCreate = async () => {
|
const openCreate = async () => {
|
||||||
resetCreateForm();
|
resetCreateForm();
|
||||||
createVisible.value = true;
|
createVisible.value = true;
|
||||||
const [projectRes, , optionRes] = await Promise.all([
|
const [projectRes, , optionRes, batchNo] = await Promise.all([
|
||||||
getProjectList(1, 9999, projectQueryParams),
|
getProjectList(1, 9999, projectQueryParams),
|
||||||
loadEditorOptions(),
|
loadEditorOptions(),
|
||||||
api.getImportOptions?.(),
|
api.getImportOptions?.(),
|
||||||
|
generateBatchNo(),
|
||||||
]);
|
]);
|
||||||
|
form.batchNo = batchNo;
|
||||||
projects.value = projectRes.data?.data?.records || [];
|
projects.value = projectRes.data?.data?.records || [];
|
||||||
const data = optionRes?.data?.data || {};
|
const data = optionRes?.data?.data || {};
|
||||||
queryCarriers.value = data.carriers || [];
|
queryCarriers.value = data.carriers || [];
|
||||||
creators.value = data.creators || [];
|
creators.value = data.creators || [];
|
||||||
plans.value = data.plans || [];
|
plans.value = data.plans || [];
|
||||||
};
|
};
|
||||||
|
const handleCreate = () => {
|
||||||
|
if (props.standalone) {
|
||||||
|
router.push({ path: '/business/waybill-import/form' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openCreate();
|
||||||
|
};
|
||||||
|
const closeCreate = () => {
|
||||||
|
if (props.createPage) {
|
||||||
|
router.push({ path: '/business/waybill-import' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createVisible.value = false;
|
||||||
|
};
|
||||||
const openDetail = row => {
|
const openDetail = row => {
|
||||||
detailQuery.batchId = row.id;
|
detailQuery.batchId = row.id;
|
||||||
detailVisible.value = true;
|
detailVisible.value = true;
|
||||||
loadDetails();
|
loadDetails();
|
||||||
};
|
};
|
||||||
|
// 草稿明细已落库为草稿运单,编辑时回填到明细表,避免重新上传附件。
|
||||||
|
const loadBatchRows = async batchId => {
|
||||||
|
if (!batchId) return;
|
||||||
|
const res = await api.getImportDetails({ batchId, current: 1, size: 9999 });
|
||||||
|
rows.value = extractRecords(res).map(({ id, ...item }, index) => ({
|
||||||
|
...item,
|
||||||
|
_key: `${id || 'row'}-${index}`,
|
||||||
|
batchNo: item.batchNo || form.batchNo,
|
||||||
|
}));
|
||||||
|
rowSelection.value = [];
|
||||||
|
previewTab.value = 'all';
|
||||||
|
};
|
||||||
const editBatch = async row => {
|
const editBatch = async row => {
|
||||||
await openCreate();
|
await openCreate();
|
||||||
const snapshot = { ...row };
|
const snapshot = { ...row };
|
||||||
@@ -805,6 +897,9 @@ const editBatch = async row => {
|
|||||||
Object.assign(form, snapshot, {
|
Object.assign(form, snapshot, {
|
||||||
projectId: snapshot.projectId || '',
|
projectId: snapshot.projectId || '',
|
||||||
projectName: snapshot.projectName || form.projectName,
|
projectName: snapshot.projectName || form.projectName,
|
||||||
|
// 列表返回的是 importStatus,表单沿用 status 字段提交给后端。
|
||||||
|
status: snapshot.importStatus || 'completed',
|
||||||
|
file: null,
|
||||||
});
|
});
|
||||||
const contract = contracts.value.find(item => String(item.id) === String(snapshot.contractId));
|
const contract = contracts.value.find(item => String(item.id) === String(snapshot.contractId));
|
||||||
form.contractId = contract?.id || '';
|
form.contractId = contract?.id || '';
|
||||||
@@ -814,6 +909,7 @@ const editBatch = async row => {
|
|||||||
const carrierValue =
|
const carrierValue =
|
||||||
form.carrierType === '自运' ? snapshot.carrierName : snapshot.carrierContractId;
|
form.carrierType === '自运' ? snapshot.carrierName : snapshot.carrierContractId;
|
||||||
if (carrierValue) carrierChange(carrierValue);
|
if (carrierValue) carrierChange(carrierValue);
|
||||||
|
await loadBatchRows(snapshot.id);
|
||||||
};
|
};
|
||||||
const removeBatches = () =>
|
const removeBatches = () =>
|
||||||
api.removeImportBatches(selected.value.map(item => item.id).join(',')).then(loadBatches);
|
api.removeImportBatches(selected.value.map(item => item.id).join(',')).then(loadBatches);
|
||||||
@@ -829,6 +925,8 @@ const projectChange = async id => {
|
|||||||
contracts.value = [];
|
contracts.value = [];
|
||||||
carrierContracts.value = [];
|
carrierContracts.value = [];
|
||||||
carrierOptions.value = [];
|
carrierOptions.value = [];
|
||||||
|
carrierContractsLoaded.value = false;
|
||||||
|
hasCarrierContracts.value = null;
|
||||||
clearCarrier();
|
clearCarrier();
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
const [customerContractRes, carrierContractRes] = await Promise.all([
|
const [customerContractRes, carrierContractRes] = await Promise.all([
|
||||||
@@ -848,12 +946,14 @@ const projectChange = async id => {
|
|||||||
contract => String(contract.contractCategory || '').trim() === '客户合同'
|
contract => String(contract.contractCategory || '').trim() === '客户合同'
|
||||||
);
|
);
|
||||||
carrierContracts.value = buildCarrierContractOptions(extractRecords(carrierContractRes));
|
carrierContracts.value = buildCarrierContractOptions(extractRecords(carrierContractRes));
|
||||||
|
carrierContractsLoaded.value = true;
|
||||||
|
hasCarrierContracts.value = carrierContracts.value.length > 0;
|
||||||
|
if (!carrierContracts.value.length) form.carrierType = '自运';
|
||||||
if (contracts.value.length) {
|
if (contracts.value.length) {
|
||||||
form.contractId = contracts.value[0].id;
|
form.contractId = contracts.value[0].id;
|
||||||
contractChange(form.contractId);
|
contractChange(form.contractId);
|
||||||
} else {
|
|
||||||
syncCarrierOptions();
|
|
||||||
}
|
}
|
||||||
|
syncCarrierOptions();
|
||||||
};
|
};
|
||||||
const contractChange = id => {
|
const contractChange = id => {
|
||||||
const item = contracts.value.find(row => String(row.id) === String(id));
|
const item = contracts.value.find(row => String(row.id) === String(id));
|
||||||
@@ -918,7 +1018,7 @@ const fileChange = async (file, list) => {
|
|||||||
};
|
};
|
||||||
const count = new Map();
|
const count = new Map();
|
||||||
rows.value = source.map((item, index) => {
|
rows.value = source.map((item, index) => {
|
||||||
const row = { _key: `${Date.now()}-${index}`, batchNo: '', ...item };
|
const row = { _key: `${Date.now()}-${index}`, ...item, batchNo: form.batchNo };
|
||||||
Object.entries(keyMap).forEach(([key, labels]) => {
|
Object.entries(keyMap).forEach(([key, labels]) => {
|
||||||
row[key] =
|
row[key] =
|
||||||
labels.map(label => item[label]).find(value => String(value ?? '').trim()) ??
|
labels.map(label => item[label]).find(value => String(value ?? '').trim()) ??
|
||||||
@@ -1029,8 +1129,7 @@ const removeSelectedRows = () => {
|
|||||||
rows.value = rows.value.filter(row => !keys.has(row._key));
|
rows.value = rows.value.filter(row => !keys.has(row._key));
|
||||||
rowSelection.value = [];
|
rowSelection.value = [];
|
||||||
};
|
};
|
||||||
const firstNotEmpty = (...values) =>
|
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
|
||||||
values.find(value => String(value ?? '').trim()) ?? '';
|
|
||||||
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
|
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
|
||||||
if (!String(value ?? '').trim()) return null;
|
if (!String(value ?? '').trim()) return null;
|
||||||
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
|
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
|
||||||
@@ -1052,9 +1151,25 @@ const normalizeImportRow = row => ({
|
|||||||
row['*运输方式']
|
row['*运输方式']
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
const buildImportPayload = () => ({ ...form, rows: rows.value.map(normalizeImportRow) });
|
const buildImportPayload = (status = form.status) => {
|
||||||
const saveDraft = () => api.saveImportDraft(buildImportPayload());
|
const { file, ...batch } = form;
|
||||||
|
return { ...batch, status, rows: rows.value.map(normalizeImportRow) };
|
||||||
|
};
|
||||||
|
// 独立页面下返回批次列表路由,弹窗形态下关闭新建面板并刷新列表。
|
||||||
|
const backToList = () => {
|
||||||
|
closeCreate();
|
||||||
|
if (!props.createPage) loadBatches();
|
||||||
|
};
|
||||||
|
// 保存草稿:批次与已解析明细一并入库,成功后返回批次列表。
|
||||||
|
const saveDraft = async () => {
|
||||||
|
await formRef.value?.validateField(draftRequiredFields);
|
||||||
|
await api.saveImportDraft(buildImportPayload('draft'));
|
||||||
|
ElMessage.success('保存草稿成功');
|
||||||
|
backToList();
|
||||||
|
};
|
||||||
const confirmImport = async () => {
|
const confirmImport = async () => {
|
||||||
|
// 导入状态选择草稿时不生成正式运单,等同于保存草稿。
|
||||||
|
if (form.status === 'draft') return saveDraft();
|
||||||
await formRef.value?.validate();
|
await formRef.value?.validate();
|
||||||
if (!validateImportRows()) return;
|
if (!validateImportRows()) return;
|
||||||
const count = rows.value.filter(row => row._duplicate).length;
|
const count = rows.value.filter(row => row._duplicate).length;
|
||||||
@@ -1063,8 +1178,7 @@ const confirmImport = async () => {
|
|||||||
await ElMessageBox.confirm('确认后将同时生成运单、应收应付明细、计结算单,是否继续?', '提示');
|
await ElMessageBox.confirm('确认后将同时生成运单、应收应付明细、计结算单,是否继续?', '提示');
|
||||||
await api.confirmImport(buildImportPayload());
|
await api.confirmImport(buildImportPayload());
|
||||||
ElMessage.success('导入成功');
|
ElMessage.success('导入成功');
|
||||||
createVisible.value = false;
|
backToList();
|
||||||
loadBatches();
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1133,5 +1247,41 @@ const confirmImport = async () => {
|
|||||||
:deep(.el-table .el-date-editor) {
|
:deep(.el-table .el-date-editor) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 230px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-height: 64px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #eff1f7;
|
||||||
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
// 统一 12px 间距,与全站底部操作栏(去 gap,由相邻按钮 margin 提供)一致
|
||||||
|
.el-button + .el-button {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--page {
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
padding: 12px 24px 96px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.avue--collapse .waybill-import-create__footer) {
|
||||||
|
left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.avue-layout--horizontal .waybill-import-create__footer) {
|
||||||
|
left: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -907,6 +907,27 @@ const defaultForm = () => ({
|
|||||||
feeGenerationMode: 'system',
|
feeGenerationMode: 'system',
|
||||||
remark: '',
|
remark: '',
|
||||||
});
|
});
|
||||||
|
// 复制新增:仅保留可编辑字段与明细,剔除主键、编号、审批与流程信息
|
||||||
|
const createCopiedDetail = (detail = {}) => {
|
||||||
|
const form = defaultForm();
|
||||||
|
Object.keys(form).forEach(prop => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(detail, prop)) form[prop] = detail[prop];
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...form,
|
||||||
|
contractNo: '',
|
||||||
|
feeGenerationMode: detail.feeGenerationMode || '',
|
||||||
|
billingEnabled: detail.billingEnabled,
|
||||||
|
contractFileJson: detail.contractFileJson || '',
|
||||||
|
attachmentsJson: detail.attachmentsJson || '',
|
||||||
|
billingPlanJson: detail.billingPlanJson || '',
|
||||||
|
paymentRatioJson: detail.paymentRatioJson || '',
|
||||||
|
settlementRuleJson: detail.settlementRuleJson || '',
|
||||||
|
preSettlementConfigJson: detail.preSettlementConfigJson || '',
|
||||||
|
formalSettlementConfigJson: detail.formalSettlementConfigJson || '',
|
||||||
|
changeRecordJson: '',
|
||||||
|
};
|
||||||
|
};
|
||||||
const parseArray = value => {
|
const parseArray = value => {
|
||||||
if (Array.isArray(value)) return value;
|
if (Array.isArray(value)) return value;
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
@@ -1458,11 +1479,7 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
canCopy(row) {
|
canCopy(row) {
|
||||||
return (
|
return this.hasPermission(`${this.config.permission}_copy`) && !row.readonly;
|
||||||
this.hasPermission(`${this.config.permission}_copy`) &&
|
|
||||||
!row.readonly &&
|
|
||||||
typeof this.api.copy === 'function'
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
editLabel(row) {
|
editLabel(row) {
|
||||||
return this.config.editLabelStatusMap?.[row.approvalStatus] || '编辑';
|
return this.config.editLabelStatusMap?.[row.approvalStatus] || '编辑';
|
||||||
@@ -1559,6 +1576,11 @@ export default {
|
|||||||
initFormPage() {
|
initFormPage() {
|
||||||
this.resetFormState();
|
this.resetFormState();
|
||||||
if (this.formMode === 'add') {
|
if (this.formMode === 'add') {
|
||||||
|
const copyId = this.$route.query.copyId;
|
||||||
|
if (copyId) {
|
||||||
|
this.api.getDetail(copyId).then(res => this.applyCopyDetail(res.data?.data || {}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.loadContractPartyOptions();
|
this.loadContractPartyOptions();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1624,6 +1646,12 @@ export default {
|
|||||||
this.syncCurrentOrganizationOption();
|
this.syncCurrentOrganizationOption();
|
||||||
this.loadContractPartyOptions();
|
this.loadContractPartyOptions();
|
||||||
},
|
},
|
||||||
|
applyCopyDetail(detail) {
|
||||||
|
this.applyDetail(createCopiedDetail(detail));
|
||||||
|
this.form.handlerUserName =
|
||||||
|
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
|
||||||
|
this.$nextTick(() => this.$refs.contractForm?.clearValidate());
|
||||||
|
},
|
||||||
closeForm() {
|
closeForm() {
|
||||||
this.$router.$avueRouter?.closeTag?.();
|
this.$router.$avueRouter?.closeTag?.();
|
||||||
this.$router.push('/business/contract-manage');
|
this.$router.push('/business/contract-manage');
|
||||||
@@ -1796,10 +1824,15 @@ export default {
|
|||||||
if (this.contractPartyLoading) return;
|
if (this.contractPartyLoading) return;
|
||||||
const requestId = ++this.contractPartyRequestId;
|
const requestId = ++this.contractPartyRequestId;
|
||||||
this.contractPartyLoading = true;
|
this.contractPartyLoading = true;
|
||||||
getCustomerArchiveList(1, 9999, { status: 1, approvalStatus: 'approved' })
|
const queryParams = { status: 1, approvalStatus: 'approved' };
|
||||||
.then(res => {
|
Promise.all([
|
||||||
|
getCustomerArchiveList(1, 9999, queryParams),
|
||||||
|
getCustomerArchiveList(1, 9999, { ...queryParams, accessType: 'temporary' }),
|
||||||
|
])
|
||||||
|
.then(responses => {
|
||||||
if (requestId !== this.contractPartyRequestId) return;
|
if (requestId !== this.contractPartyRequestId) return;
|
||||||
const names = extractRecords(res)
|
const names = responses
|
||||||
|
.flatMap(extractRecords)
|
||||||
.map(item => item.fullName || item.shortName || item.customerName || item.customerCode)
|
.map(item => item.fullName || item.shortName || item.customerName || item.customerCode)
|
||||||
.map(name => String(name || '').trim())
|
.map(name => String(name || '').trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@@ -2336,11 +2369,9 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleCopy(row) {
|
handleCopy(row) {
|
||||||
this.$confirm('确定复制该合同?', '提示', { type: 'warning' })
|
this.$router.push({
|
||||||
.then(() => this.api.copy(row.id))
|
path: '/business/contract-manage/form',
|
||||||
.then(() => {
|
query: { mode: 'add', copyId: row.id, name: '新增合同管理' },
|
||||||
this.$message.success('复制成功');
|
|
||||||
this.onLoad(this.page, this.query);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
openFlow(row, loaded = false) {
|
openFlow(row, loaded = false) {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="waybill-import-form-page-container">
|
||||||
|
<div class="waybill-import-form-page-title">新建导入</div>
|
||||||
|
<waybill-import-dialog create-page />
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import WaybillImportDialog from './components/waybill-import-dialog.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.waybill-import-form-page-container {
|
||||||
|
:deep(.basic-container__card > .el-card__body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.waybill-import-form-page-title {
|
||||||
|
position: relative;
|
||||||
|
padding: 16px 24px 12px 36px;
|
||||||
|
border-bottom: 1px solid #eff1f7;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 24px;
|
||||||
|
width: 4px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #409eff;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -447,9 +447,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</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">
|
<div class="payment-form-page__reference-pagination">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="formalPage.current"
|
v-model:current-page="formalPage.current"
|
||||||
@@ -486,12 +483,21 @@
|
|||||||
@size-change="handlePreSizeChange"
|
@size-change="handlePreSizeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="payment-form-page__reference-confirm">
|
|
||||||
<el-button type="primary" :disabled="!preSelection.length" @click="confirmPreSelection">确定</el-button>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs></div
|
</el-tabs></div
|
||||||
></el-dialog>
|
>
|
||||||
|
<template v-if="form.paymentType !== 'project_advance'" #footer>
|
||||||
|
<div class="payment-form-page__reference-footer">
|
||||||
|
<el-button @click="referenceVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!referenceSelection.length"
|
||||||
|
@click="confirmReferenceSelection"
|
||||||
|
>确定</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
<el-dialog v-model="billDialogVisible" title="选择汇票" width="80%" append-to-body>
|
<el-dialog v-model="billDialogVisible" title="选择汇票" width="80%" append-to-body>
|
||||||
<div class="payment-form-page__reference-search">
|
<div class="payment-form-page__reference-search">
|
||||||
<el-form :model="billQuery" label-position="right" label-width="160px" @submit.prevent>
|
<el-form :model="billQuery" label-position="right" label-width="160px" @submit.prevent>
|
||||||
@@ -798,6 +804,9 @@ export default {
|
|||||||
const amount = this.form.settlementAmount;
|
const amount = this.form.settlementAmount;
|
||||||
return Number(amount || 0);
|
return Number(amount || 0);
|
||||||
},
|
},
|
||||||
|
referenceSelection() {
|
||||||
|
return (this.referenceTab === 'formal' ? this.formalSelection : this.preSelection) || [];
|
||||||
|
},
|
||||||
referenceLabel() {
|
referenceLabel() {
|
||||||
if (this.referenceRows.length) {
|
if (this.referenceRows.length) {
|
||||||
return this.referenceRows
|
return this.referenceRows
|
||||||
@@ -1484,16 +1493,23 @@ export default {
|
|||||||
material.keywords.some(keyword => fileName.includes(keyword.toLocaleLowerCase())))
|
material.keywords.some(keyword => fileName.includes(keyword.toLocaleLowerCase())))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
resolveAttachmentTypeByFileName(file) {
|
matchAttachmentMaterialByFileName(file, materials = Object.values(ATTACHMENT_MATERIALS)) {
|
||||||
const fileName = this.normalizedAttachmentFileName(file);
|
const fileName = this.normalizedAttachmentFileName(file);
|
||||||
const material = [...Object.values(ATTACHMENT_MATERIALS)]
|
if (!fileName) return null;
|
||||||
.sort(
|
const matched = (materials || [])
|
||||||
(a, b) =>
|
.flatMap(material =>
|
||||||
Math.max(...b.keywords.map(String.length)) - Math.max(...a.keywords.map(String.length))
|
material.keywords.map(keyword => ({
|
||||||
|
material,
|
||||||
|
keyword: String(keyword).toLocaleLowerCase(),
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
.find(item =>
|
.filter(item => item.keyword)
|
||||||
item.keywords.some(keyword => fileName.includes(String(keyword).toLocaleLowerCase()))
|
.sort((a, b) => b.keyword.length - a.keyword.length)
|
||||||
);
|
.find(item => fileName.includes(item.keyword));
|
||||||
|
return matched?.material || null;
|
||||||
|
},
|
||||||
|
resolveAttachmentTypeByFileName(file) {
|
||||||
|
const material = this.matchAttachmentMaterialByFileName(file);
|
||||||
return material ? MATERIAL_TYPE_MAP[material.key] : 'other';
|
return material ? MATERIAL_TYPE_MAP[material.key] : 'other';
|
||||||
},
|
},
|
||||||
hasAttachmentMaterial(material) {
|
hasAttachmentMaterial(material) {
|
||||||
@@ -1509,12 +1525,11 @@ export default {
|
|||||||
return hasUploadedAttachment;
|
return hasUploadedAttachment;
|
||||||
},
|
},
|
||||||
resolveAttachmentMaterial(file, materials = Object.values(ATTACHMENT_MATERIALS)) {
|
resolveAttachmentMaterial(file, materials = Object.values(ATTACHMENT_MATERIALS)) {
|
||||||
return [...materials]
|
if (!this.attachmentFileUrl(file)) return null;
|
||||||
.sort(
|
const typeMatched = (materials || []).find(
|
||||||
(a, b) =>
|
material => file.attachmentType === MATERIAL_TYPE_MAP[material.key]
|
||||||
Math.max(...b.keywords.map(String.length)) - Math.max(...a.keywords.map(String.length))
|
);
|
||||||
)
|
return typeMatched || this.matchAttachmentMaterialByFileName(file, materials);
|
||||||
.find(material => this.attachmentMatchesMaterial(file, material));
|
|
||||||
},
|
},
|
||||||
mergeAutomaticAttachments(files, materials, sourceName, defaultMaterial = null) {
|
mergeAutomaticAttachments(files, materials, sourceName, defaultMaterial = null) {
|
||||||
const existingUrls = new Set(
|
const existingUrls = new Set(
|
||||||
@@ -2099,6 +2114,10 @@ export default {
|
|||||||
]);
|
]);
|
||||||
if (this.billPayment) this.loadBillOptions();
|
if (this.billPayment) this.loadBillOptions();
|
||||||
},
|
},
|
||||||
|
confirmReferenceSelection() {
|
||||||
|
if (this.referenceTab === 'formal') return this.confirmFormalSelection();
|
||||||
|
return this.confirmPreSelection();
|
||||||
|
},
|
||||||
async confirmFormalSelection() {
|
async confirmFormalSelection() {
|
||||||
const rows = this.formalSelection || [];
|
const rows = this.formalSelection || [];
|
||||||
if (!rows.length) return;
|
if (!rows.length) return;
|
||||||
@@ -2370,7 +2389,8 @@ export default {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.payment-form-page__reference-search-actions,
|
.payment-form-page__reference-search-actions,
|
||||||
.payment-form-page__reference-pagination {
|
.payment-form-page__reference-pagination,
|
||||||
|
.payment-form-page__reference-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="item in feeOptions"
|
v-for="item in feeOptions"
|
||||||
:key="item.feeType"
|
:key="item.feeType"
|
||||||
:label="item.feeTypeName || item.feeType"
|
:label="feeCategoryName(item.feeType)"
|
||||||
:value="item.feeType"
|
:value="item.feeType"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -773,17 +773,30 @@
|
|||||||
:min="0"
|
:min="0"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
@change="recalculateAdjustRow(row, 'transportQuantity')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="里程(KM)" min-width="140" align="center">
|
<el-table-column label="里程(KM)" min-width="140" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-input-number v-model="row.mileage" :min="0" :precision="2" :controls="false" />
|
<el-input-number
|
||||||
|
v-model="row.mileage"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:controls="false"
|
||||||
|
@change="recalculateAdjustRow(row, 'mileage')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="运输单价" min-width="140" align="center">
|
<el-table-column label="运输单价" min-width="140" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-input-number v-model="row.unitPrice" :min="0" :precision="2" :controls="false" />
|
<el-input-number
|
||||||
|
v-model="row.unitPrice"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:controls="false"
|
||||||
|
@change="recalculateAdjustRow(row, 'unitPrice')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@@ -915,6 +928,7 @@ import {
|
|||||||
getDetail as getPreSettlementDetail,
|
getDetail as getPreSettlementDetail,
|
||||||
getDetailFees as getPreSettlementDetailFees,
|
getDetailFees as getPreSettlementDetailFees,
|
||||||
} from '@/api/settlement/preSettlement';
|
} from '@/api/settlement/preSettlement';
|
||||||
|
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
|
||||||
import {
|
import {
|
||||||
createFormalSettlementForm,
|
createFormalSettlementForm,
|
||||||
formalSettlementFormFields,
|
formalSettlementFormFields,
|
||||||
@@ -995,6 +1009,7 @@ export default {
|
|||||||
allContracts: [],
|
allContracts: [],
|
||||||
projects: [],
|
projects: [],
|
||||||
feeOptions: [],
|
feeOptions: [],
|
||||||
|
feeCategoryOptions: [],
|
||||||
transportTypeOptions: [],
|
transportTypeOptions: [],
|
||||||
fields: formalSettlementFormFields,
|
fields: formalSettlementFormFields,
|
||||||
sourceTableColumns: sourceColumns,
|
sourceTableColumns: sourceColumns,
|
||||||
@@ -1029,10 +1044,14 @@ export default {
|
|||||||
loading: false,
|
loading: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
detailId: null,
|
detailId: null,
|
||||||
|
sourceDetailId: null,
|
||||||
|
feeKey: '',
|
||||||
reason: '',
|
reason: '',
|
||||||
rows: [],
|
rows: [],
|
||||||
targetRow: null,
|
targetRow: null,
|
||||||
},
|
},
|
||||||
|
detailFeeSnapshots: {},
|
||||||
|
pendingAdjustments: {},
|
||||||
billingRuleDialog: {
|
billingRuleDialog: {
|
||||||
visible: false,
|
visible: false,
|
||||||
rule: null,
|
rule: null,
|
||||||
@@ -1148,6 +1167,9 @@ export default {
|
|||||||
this.sources = data.sources || [];
|
this.sources = data.sources || [];
|
||||||
this.details = data.details || [];
|
this.details = data.details || [];
|
||||||
this.summaryFees = data.summaryFees || [];
|
this.summaryFees = data.summaryFees || [];
|
||||||
|
// 服务端数据已刷新,费用快照与暂存调整全部作废。
|
||||||
|
this.detailFeeSnapshots = {};
|
||||||
|
this.pendingAdjustments = {};
|
||||||
this.paymentApplications = data.paymentApplications || [];
|
this.paymentApplications = data.paymentApplications || [];
|
||||||
if (this.readonly && this.form.settlementType === 'receivable') {
|
if (this.readonly && this.form.settlementType === 'receivable') {
|
||||||
this.receiptClaims = this.unwrapData(await getReceiptClaims(id)) || [];
|
this.receiptClaims = this.unwrapData(await getReceiptClaims(id)) || [];
|
||||||
@@ -1174,6 +1196,8 @@ export default {
|
|||||||
this.sources = [];
|
this.sources = [];
|
||||||
this.details = [];
|
this.details = [];
|
||||||
this.summaryFees = [];
|
this.summaryFees = [];
|
||||||
|
this.detailFeeSnapshots = {};
|
||||||
|
this.pendingAdjustments = {};
|
||||||
this.paymentApplications = [];
|
this.paymentApplications = [];
|
||||||
this.receiptClaims = [];
|
this.receiptClaims = [];
|
||||||
this.adjustments = [];
|
this.adjustments = [];
|
||||||
@@ -1195,6 +1219,7 @@ export default {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.loadAllContracts(),
|
this.loadAllContracts(),
|
||||||
this.loadFeeOptions(),
|
this.loadFeeOptions(),
|
||||||
|
this.loadFeeCategoryOptions(),
|
||||||
this.loadAttachmentTypeOptions(),
|
this.loadAttachmentTypeOptions(),
|
||||||
this.loadTransportTypeOptions(),
|
this.loadTransportTypeOptions(),
|
||||||
]);
|
]);
|
||||||
@@ -1284,7 +1309,7 @@ export default {
|
|||||||
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount,
|
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount,
|
||||||
}));
|
}));
|
||||||
this.form.sourceDetailIds = this.details.map(item => item.sourceDetailId);
|
this.form.sourceDetailIds = this.details.map(item => item.sourceDetailId);
|
||||||
this.buildSummaryFeesFromDetails();
|
await this.refreshSummaryFees();
|
||||||
},
|
},
|
||||||
async applyInitialSources(rows) {
|
async applyInitialSources(rows) {
|
||||||
const first = rows[0];
|
const first = rows[0];
|
||||||
@@ -1347,7 +1372,7 @@ export default {
|
|||||||
summaryMap.set(key, current);
|
summaryMap.set(key, current);
|
||||||
});
|
});
|
||||||
this.summaryFees = [...summaryMap.values()];
|
this.summaryFees = [...summaryMap.values()];
|
||||||
if (!this.summaryFees.length) this.buildSummaryFeesFromDetails();
|
if (!this.summaryFees.length) await this.refreshSummaryFees();
|
||||||
},
|
},
|
||||||
async loadAllContracts() {
|
async loadAllContracts() {
|
||||||
const response = await getContractOptions('');
|
const response = await getContractOptions('');
|
||||||
@@ -1375,6 +1400,10 @@ export default {
|
|||||||
const response = await getFeeOptions();
|
const response = await getFeeOptions();
|
||||||
this.feeOptions = this.unwrapData(response) || [];
|
this.feeOptions = this.unwrapData(response) || [];
|
||||||
},
|
},
|
||||||
|
async loadFeeCategoryOptions() {
|
||||||
|
const { data } = await getDictionary({ code: 'fee_category' });
|
||||||
|
this.feeCategoryOptions = data?.data || [];
|
||||||
|
},
|
||||||
handleProjectChange(id) {
|
handleProjectChange(id) {
|
||||||
const project = this.projects.find(item => String(item.id) === String(id));
|
const project = this.projects.find(item => String(item.id) === String(id));
|
||||||
this.form.projectName = project?.name || '';
|
this.form.projectName = project?.name || '';
|
||||||
@@ -1505,7 +1534,7 @@ export default {
|
|||||||
this.detailCandidate.page.current = 1;
|
this.detailCandidate.page.current = 1;
|
||||||
this.loadDetailCandidates();
|
this.loadDetailCandidates();
|
||||||
},
|
},
|
||||||
confirmDetailCandidates() {
|
async confirmDetailCandidates() {
|
||||||
if (!this.detailCandidate.selected.length) {
|
if (!this.detailCandidate.selected.length) {
|
||||||
return this.$message.warning('请选择结算明细');
|
return this.$message.warning('请选择结算明细');
|
||||||
}
|
}
|
||||||
@@ -1526,7 +1555,12 @@ export default {
|
|||||||
...existing.values(),
|
...existing.values(),
|
||||||
];
|
];
|
||||||
this.form.sourceDetailIds = [...existing.values()].map(item => item.sourceDetailId);
|
this.form.sourceDetailIds = [...existing.values()].map(item => item.sourceDetailId);
|
||||||
this.buildSummaryFeesFromDetails();
|
this.detailCandidate.loading = true;
|
||||||
|
try {
|
||||||
|
await this.refreshSummaryFees();
|
||||||
|
} finally {
|
||||||
|
this.detailCandidate.loading = false;
|
||||||
|
}
|
||||||
this.detailCandidate.visible = false;
|
this.detailCandidate.visible = false;
|
||||||
},
|
},
|
||||||
async removeDetail(row) {
|
async removeDetail(row) {
|
||||||
@@ -1537,7 +1571,7 @@ export default {
|
|||||||
this.form.sourceDetailIds = this.details
|
this.form.sourceDetailIds = this.details
|
||||||
.filter(item => !item.sourcePreSettlementId)
|
.filter(item => !item.sourcePreSettlementId)
|
||||||
.map(item => item.sourceDetailId);
|
.map(item => item.sourceDetailId);
|
||||||
this.buildSummaryFeesFromDetails();
|
await this.refreshSummaryFees();
|
||||||
},
|
},
|
||||||
addSummaryFee() {
|
addSummaryFee() {
|
||||||
this.summaryFees.push({
|
this.summaryFees.push({
|
||||||
@@ -1559,7 +1593,13 @@ export default {
|
|||||||
manualFeeItems(feeType) {
|
manualFeeItems(feeType) {
|
||||||
return this.feeOptions.find(item => item.feeType === feeType)?.feeItems || [];
|
return this.feeOptions.find(item => item.feeType === feeType)?.feeItems || [];
|
||||||
},
|
},
|
||||||
|
// fee_category 业务字典:dictKey 为字典键值(落库值),dictValue 为字典名称(展示值)。
|
||||||
feeCategoryName(value) {
|
feeCategoryName(value) {
|
||||||
|
if (value === undefined || value === null || value === '') return '';
|
||||||
|
const option = this.feeCategoryOptions.find(
|
||||||
|
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
|
||||||
|
);
|
||||||
|
if (option) return option.dictValue ?? value;
|
||||||
return (
|
return (
|
||||||
this.feeOptions.find(item => String(item.feeType) === String(value))?.feeTypeName || value
|
this.feeOptions.find(item => String(item.feeType) === String(value))?.feeTypeName || value
|
||||||
);
|
);
|
||||||
@@ -1589,16 +1629,25 @@ export default {
|
|||||||
return totalText;
|
return totalText;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async refreshSummaryFees() {
|
||||||
|
await this.ensureDetailFeeSnapshots();
|
||||||
|
this.buildSummaryFeesFromDetails();
|
||||||
|
},
|
||||||
|
// 结算合计按费用项归集:原金额取明细进入结算单时的费用金额,结算金额取调整后的金额,调整金额为两者之差。
|
||||||
buildSummaryFeesFromDetails() {
|
buildSummaryFeesFromDetails() {
|
||||||
const generatedMap = new Map();
|
const generatedMap = new Map();
|
||||||
const existingGenerated = new Map(
|
const existingGenerated = new Map(
|
||||||
this.summaryFees
|
this.summaryFees
|
||||||
.filter(row => row.manualFlag !== 1)
|
.filter(row => Number(row.manualFlag) !== 1)
|
||||||
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||||||
);
|
);
|
||||||
const append = (feeItem, value, fallbackFeeType = '') => {
|
const append = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
|
||||||
const amount = Number(value || 0);
|
const originalAmount = Number(originalValue || 0);
|
||||||
if (!feeItem || !Number.isFinite(amount) || Math.abs(amount) < 0.005) return;
|
const settlementAmount = Number(settlementValue || 0);
|
||||||
|
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Math.abs(originalAmount) < 0.005 && Math.abs(settlementAmount) < 0.005) return;
|
||||||
const option = this.feeOptions.find(item =>
|
const option = this.feeOptions.find(item =>
|
||||||
(item.feeItems || []).some(name => String(name) === String(feeItem))
|
(item.feeItems || []).some(name => String(name) === String(feeItem))
|
||||||
);
|
);
|
||||||
@@ -1613,35 +1662,212 @@ export default {
|
|||||||
feeType,
|
feeType,
|
||||||
feeItem,
|
feeItem,
|
||||||
originalAmount: 0,
|
originalAmount: 0,
|
||||||
adjustAmount: Number(old?.adjustAmount || 0),
|
adjustAmount: 0,
|
||||||
settlementAmount: 0,
|
settlementAmount: 0,
|
||||||
remark: old?.remark || '',
|
remark: old?.remark || '',
|
||||||
manualFlag: 0,
|
manualFlag: 0,
|
||||||
};
|
};
|
||||||
current.originalAmount = Number((current.originalAmount + amount).toFixed(2));
|
current.originalAmount = Number((current.originalAmount + originalAmount).toFixed(2));
|
||||||
current.settlementAmount = Number(
|
current.settlementAmount = Number((current.settlementAmount + settlementAmount).toFixed(2));
|
||||||
(current.originalAmount + Number(current.adjustAmount || 0)).toFixed(2)
|
current.adjustAmount = Number(
|
||||||
|
(current.settlementAmount - current.originalAmount).toFixed(2)
|
||||||
);
|
);
|
||||||
generatedMap.set(key, current);
|
generatedMap.set(key, current);
|
||||||
};
|
};
|
||||||
this.details.forEach(row => {
|
this.details.forEach(detail => {
|
||||||
const feeItems = this.parseFeeItems(row.feeItemsJson || row.feeItems);
|
this.resolveDetailFeePairs(detail).forEach(([baseRow, currentRow]) =>
|
||||||
const entries = Object.entries(feeItems);
|
this.appendFeeRowSummary(baseRow, currentRow, detail, append)
|
||||||
entries.forEach(([feeItem, amount]) => append(feeItem, amount, row.feeType));
|
|
||||||
let knownAmount = entries.reduce((total, [, amount]) => total + Number(amount || 0), 0);
|
|
||||||
if (!entries.some(([feeItem]) => this.isFreightFeeItem(feeItem))) {
|
|
||||||
append('运输费', row.freightAmount, '物流配送');
|
|
||||||
knownAmount += Number(row.freightAmount || 0);
|
|
||||||
}
|
|
||||||
const totalAmount = Number(
|
|
||||||
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount ?? 0
|
|
||||||
);
|
);
|
||||||
const residualAmount = Number((totalAmount - knownAmount).toFixed(2));
|
|
||||||
if (Math.abs(residualAmount) >= 0.005) append('其他费用', residualAmount, '其他费用');
|
|
||||||
});
|
});
|
||||||
const manualRows = this.summaryFees.filter(row => row.manualFlag === 1);
|
const manualRows = this.summaryFees.filter(row => Number(row.manualFlag) === 1);
|
||||||
this.summaryFees = [...generatedMap.values(), ...manualRows];
|
this.summaryFees = [...generatedMap.values(), ...manualRows];
|
||||||
},
|
},
|
||||||
|
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少费用快照时退化为明细自身的汇总行。
|
||||||
|
resolveDetailFeePairs(detail) {
|
||||||
|
const feeKey = this.detailFeeKey(detail);
|
||||||
|
const baseRows = this.detailFeeSnapshots[feeKey] || [];
|
||||||
|
if (!baseRows.length) return [this.buildDetailFeePair(detail)];
|
||||||
|
const pendingRows = this.pendingAdjustments[feeKey]?.rows || [];
|
||||||
|
return baseRows.map((baseRow, index) => {
|
||||||
|
const matched =
|
||||||
|
pendingRows.find(row => baseRow.id && String(row.id) === String(baseRow.id)) ||
|
||||||
|
pendingRows[index];
|
||||||
|
return [baseRow, matched || baseRow];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildDetailFeePair(detail) {
|
||||||
|
const baseRow = {
|
||||||
|
feeItems: this.parseFeeItems(detail.feeItemsJson || detail.feeItems),
|
||||||
|
feeType: detail.feeType,
|
||||||
|
freightAmount: Number(detail.freightAmount || 0),
|
||||||
|
settlementAmountTax: Number(
|
||||||
|
detail.originalAmount ?? this.detailSettlementAmount(detail)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return [baseRow, { ...baseRow, settlementAmountTax: this.detailSettlementAmount(detail) }];
|
||||||
|
},
|
||||||
|
detailSettlementAmount(detail) {
|
||||||
|
return Number(
|
||||||
|
detail?.settlementAmountTax ??
|
||||||
|
detail?.totalAmount ??
|
||||||
|
detail?.afterAmount ??
|
||||||
|
detail?.settlementAmount ??
|
||||||
|
0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
appendFeeRowSummary(baseRow, currentRow, detail, append) {
|
||||||
|
const fallbackFeeType = currentRow.feeType || detail.feeType || '';
|
||||||
|
const feeItemNames = Array.from(
|
||||||
|
new Set([...Object.keys(baseRow.feeItems || {}), ...Object.keys(currentRow.feeItems || {})])
|
||||||
|
);
|
||||||
|
feeItemNames.forEach(name =>
|
||||||
|
append(name, baseRow.feeItems?.[name], currentRow.feeItems?.[name], fallbackFeeType)
|
||||||
|
);
|
||||||
|
const hasFreightItem = feeItemNames.some(name => this.isFreightFeeItem(name));
|
||||||
|
if (!hasFreightItem) {
|
||||||
|
append('运输费', baseRow.freightAmount, currentRow.freightAmount, '物流配送');
|
||||||
|
}
|
||||||
|
const knownAmount = row =>
|
||||||
|
this.sumFeeItems(row.feeItems) + (hasFreightItem ? 0 : Number(row.freightAmount || 0));
|
||||||
|
const baseResidual = Number(
|
||||||
|
(Number(baseRow.settlementAmountTax || 0) - knownAmount(baseRow)).toFixed(2)
|
||||||
|
);
|
||||||
|
const currentResidual = Number(
|
||||||
|
(Number(currentRow.settlementAmountTax || 0) - knownAmount(currentRow)).toFixed(2)
|
||||||
|
);
|
||||||
|
if (Math.abs(baseResidual) < 0.005 && Math.abs(currentResidual) < 0.005) return;
|
||||||
|
// 结算金额被手工改写且无法拆分到具体费用项时,计入金额占比最大的费用项,否则归入其他费用。
|
||||||
|
append(
|
||||||
|
this.residualFeeItem(baseRow, currentRow),
|
||||||
|
baseResidual,
|
||||||
|
currentResidual,
|
||||||
|
fallbackFeeType
|
||||||
|
);
|
||||||
|
},
|
||||||
|
residualFeeItem(baseRow, currentRow) {
|
||||||
|
const feeItems = { ...(baseRow.feeItems || {}), ...(currentRow.feeItems || {}) };
|
||||||
|
const dominant = Object.entries(feeItems)
|
||||||
|
.filter(([, amount]) => Math.abs(Number(amount || 0)) >= 0.005)
|
||||||
|
.sort((a, b) => Math.abs(Number(b[1] || 0)) - Math.abs(Number(a[1] || 0)))[0];
|
||||||
|
if (dominant) return dominant[0];
|
||||||
|
return (
|
||||||
|
currentRow.billingRule?.feeItem ||
|
||||||
|
currentRow.feeItem ||
|
||||||
|
baseRow.billingRule?.feeItem ||
|
||||||
|
baseRow.feeItem ||
|
||||||
|
'其他费用'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sumFeeItems(feeItems) {
|
||||||
|
return Number(
|
||||||
|
Object.values(feeItems || {})
|
||||||
|
.reduce((total, value) => total + Number(value || 0), 0)
|
||||||
|
.toFixed(2)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
detailFeeKey(detail) {
|
||||||
|
if (detail.formalSettlementId && detail.id) return `formal:${detail.id}`;
|
||||||
|
if (detail.sourcePreSettlementId && detail.id) return `pre:${detail.id}`;
|
||||||
|
const sourceDetailId = detail.sourceDetailId || detail.id;
|
||||||
|
return sourceDetailId ? `src:${sourceDetailId}` : '';
|
||||||
|
},
|
||||||
|
async loadDetailFeeRows(detail) {
|
||||||
|
if (detail.formalSettlementId && detail.id) {
|
||||||
|
const rows = this.unwrapData(await getDetailFees(detail.id)) || [];
|
||||||
|
return rows.map(item => this.normalizeAdjustRow(item));
|
||||||
|
}
|
||||||
|
if (detail.sourcePreSettlementId && detail.id) {
|
||||||
|
const rows = this.unwrapData(await getPreSettlementDetailFees(detail.id)) || [];
|
||||||
|
return rows.map(item => this.normalizeAdjustRow(item));
|
||||||
|
}
|
||||||
|
const sourceDetailId = detail.sourceDetailId || detail.id;
|
||||||
|
if (!sourceDetailId) return [];
|
||||||
|
const data = this.unwrapData(await getFeeDetail(sourceDetailId)) || {};
|
||||||
|
return (data.records || []).map(item =>
|
||||||
|
this.normalizeAdjustRow(item, data.feeItemNames || [])
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// 明细列表只有汇总金额,费用项级别的原始金额需要按明细单独拉取并缓存为调整基准。
|
||||||
|
async ensureDetailFeeSnapshots() {
|
||||||
|
const targets = this.details.filter(detail => {
|
||||||
|
const feeKey = this.detailFeeKey(detail);
|
||||||
|
return feeKey && !this.detailFeeSnapshots[feeKey];
|
||||||
|
});
|
||||||
|
if (!targets.length) return;
|
||||||
|
await Promise.all(
|
||||||
|
targets.map(async detail => {
|
||||||
|
const feeKey = this.detailFeeKey(detail);
|
||||||
|
try {
|
||||||
|
const feeRows = await this.loadDetailFeeRows(detail);
|
||||||
|
if (!feeRows.length) return;
|
||||||
|
this.detailFeeSnapshots[feeKey] = feeRows;
|
||||||
|
this.applySnapshotToDetail(detail, feeRows);
|
||||||
|
} catch (error) {
|
||||||
|
// 取不到费用明细时退回按明细汇总金额归集,后续操作会再次尝试拉取。
|
||||||
|
delete this.detailFeeSnapshots[feeKey];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
applySnapshotToDetail(detail, feeRows) {
|
||||||
|
if (!feeRows.length || detail.pendingDetailAdjustment) return;
|
||||||
|
const feeItems = {};
|
||||||
|
feeRows.forEach(row =>
|
||||||
|
Object.entries(row.feeItems || {}).forEach(([name, amount]) => {
|
||||||
|
feeItems[name] = Number((Number(feeItems[name] || 0) + Number(amount || 0)).toFixed(2));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const settlementAmount = feeRows.reduce(
|
||||||
|
(total, row) => total + Number(row.settlementAmountTax || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const freightAmount = feeRows.reduce(
|
||||||
|
(total, row) => total + Number(row.freightAmount || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
Object.assign(detail, {
|
||||||
|
feeItemsJson: JSON.stringify(feeItems),
|
||||||
|
freightAmount: Number(freightAmount.toFixed(2)),
|
||||||
|
originalAmount: Number(settlementAmount.toFixed(2)),
|
||||||
|
adjustAmount: 0,
|
||||||
|
settlementAmountTax: Number(settlementAmount.toFixed(2)),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
normalizeAdjustRow(item, feeItemNames = []) {
|
||||||
|
const rawFeeItems =
|
||||||
|
item.feeItems && typeof item.feeItems === 'object'
|
||||||
|
? item.feeItems
|
||||||
|
: this.parseFeeItems(item.feeItemsJson);
|
||||||
|
const names = feeItemNames.length ? feeItemNames : Object.keys(rawFeeItems);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
transportQuantity: Number(item.transportQuantity || 0),
|
||||||
|
mileage:
|
||||||
|
item.mileage === null || item.mileage === undefined || Number(item.mileage) === -1
|
||||||
|
? null
|
||||||
|
: Number(item.mileage),
|
||||||
|
freightAmount: Number(item.freightAmount || 0),
|
||||||
|
feeItems: Object.fromEntries(names.map(name => [name, Number(rawFeeItems[name] || 0)])),
|
||||||
|
settlementAmountTax: Number(
|
||||||
|
item.settlementAmountTax ?? item.totalAmount ?? item.afterAmount ?? item.settlementAmount ?? 0
|
||||||
|
),
|
||||||
|
billingRule: this.normalizeBillingRule(item),
|
||||||
|
calculating: false,
|
||||||
|
calculateError: '',
|
||||||
|
adjustCalculateTimer: null,
|
||||||
|
adjustCalculateVersion: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cloneAdjustRows(rows) {
|
||||||
|
return (rows || []).map(row => ({
|
||||||
|
...row,
|
||||||
|
feeItems: { ...row.feeItems },
|
||||||
|
calculating: false,
|
||||||
|
calculateError: '',
|
||||||
|
adjustCalculateTimer: null,
|
||||||
|
adjustCalculateVersion: 0,
|
||||||
|
}));
|
||||||
|
},
|
||||||
isFreightFeeItem(name) {
|
isFreightFeeItem(name) {
|
||||||
return name && (name.includes('运费') || name.includes('运输费'));
|
return name && (name.includes('运费') || name.includes('运输费'));
|
||||||
},
|
},
|
||||||
@@ -1768,69 +1994,93 @@ export default {
|
|||||||
row.settlementAmountTax = Number(
|
row.settlementAmountTax = Number(
|
||||||
(hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2)
|
(hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2)
|
||||||
);
|
);
|
||||||
|
if (changedField && this.adjust.sourceDetailId && (row.sourceFeeId || row.id)) {
|
||||||
|
this.scheduleSourceAdjustCalculation(row);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scheduleSourceAdjustCalculation(row) {
|
||||||
|
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
|
||||||
|
row.adjustCalculateVersion = Number(row.adjustCalculateVersion || 0) + 1;
|
||||||
|
row.calculating = true;
|
||||||
|
const version = row.adjustCalculateVersion;
|
||||||
|
row.adjustCalculateTimer = setTimeout(async () => {
|
||||||
|
row.adjustCalculateTimer = null;
|
||||||
|
try {
|
||||||
|
const response = await calculateAdjustedFee({
|
||||||
|
detailId: this.adjust.sourceDetailId,
|
||||||
|
feeId: row.sourceFeeId || row.id,
|
||||||
|
transportQuantity: Number(row.transportQuantity || 0),
|
||||||
|
mileage: Number(row.mileage || 0),
|
||||||
|
freightAmount: Number(row.freightAmount || 0),
|
||||||
|
feeItems: Object.fromEntries(
|
||||||
|
Object.entries(row.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (version !== row.adjustCalculateVersion || !this.adjust.visible) return;
|
||||||
|
const data = response.data?.data || response.data || {};
|
||||||
|
row.freightAmount = Number(data.freightAmount || 0);
|
||||||
|
row.feeItems = Object.fromEntries(
|
||||||
|
Object.entries(data.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
|
||||||
|
);
|
||||||
|
row.adjustAmount = Number(data.adjustAmount || 0);
|
||||||
|
row.settlementAmountTax = Number(data.afterAmount || 0);
|
||||||
|
} catch (error) {
|
||||||
|
row.calculateError = error.message || '费用试算失败';
|
||||||
|
} finally {
|
||||||
|
if (version === row.adjustCalculateVersion) row.calculating = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
},
|
},
|
||||||
async openAdjustDialog(row) {
|
async openAdjustDialog(row) {
|
||||||
if (!row.formalSettlementId) {
|
const feeKey = this.detailFeeKey(row);
|
||||||
|
const pending = feeKey ? this.pendingAdjustments[feeKey] : null;
|
||||||
this.adjust = {
|
this.adjust = {
|
||||||
visible: true,
|
visible: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
saving: false,
|
saving: false,
|
||||||
detailId: null,
|
detailId: row.formalSettlementId ? row.id : null,
|
||||||
reason: '',
|
sourceDetailId: row.sourceDetailId || null,
|
||||||
|
feeKey,
|
||||||
|
reason: pending?.reason || '',
|
||||||
rows: [],
|
rows: [],
|
||||||
targetRow: row,
|
targetRow: row.formalSettlementId ? null : row,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
let feeRows = [];
|
// 已暂存的调整结果优先回显,避免重新打开时又回到调整前的数据。
|
||||||
if (row.sourcePreSettlementId && row.id) {
|
if (pending?.rows?.length) {
|
||||||
const response = await getPreSettlementDetailFees(row.id);
|
this.adjust.rows = this.cloneAdjustRows(pending.rows);
|
||||||
feeRows = this.unwrapData(response) || [];
|
|
||||||
}
|
|
||||||
this.adjust.rows = (feeRows.length ? feeRows : [row]).map(item => ({
|
|
||||||
...item,
|
|
||||||
feeItems: this.parseFeeItems(item.feeItemsJson || item.feeItems),
|
|
||||||
billingRule: this.normalizeBillingRule(item),
|
|
||||||
settlementAmountTax:
|
|
||||||
item.settlementAmountTax ??
|
|
||||||
item.totalAmount ??
|
|
||||||
item.afterAmount ??
|
|
||||||
item.settlementAmount ??
|
|
||||||
0,
|
|
||||||
}));
|
|
||||||
} finally {
|
|
||||||
this.adjust.loading = false;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const detailRow = row;
|
if (feeKey && !this.detailFeeSnapshots[feeKey]) {
|
||||||
this.adjust = {
|
const feeRows = await this.loadDetailFeeRows(row);
|
||||||
visible: true,
|
if (feeRows.length) this.detailFeeSnapshots[feeKey] = feeRows;
|
||||||
loading: true,
|
}
|
||||||
saving: false,
|
const snapshot = feeKey ? this.detailFeeSnapshots[feeKey] : null;
|
||||||
detailId: detailRow.id,
|
this.adjust.rows = snapshot?.length
|
||||||
reason: '',
|
? this.cloneAdjustRows(snapshot)
|
||||||
rows: [],
|
: [this.normalizeAdjustRow(row)];
|
||||||
targetRow: null,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const response = await getDetailFees(detailRow.id);
|
|
||||||
const data = this.unwrapData(response);
|
|
||||||
this.adjust.rows = (data || []).map(item => ({
|
|
||||||
...item,
|
|
||||||
feeItems: this.parseFeeItems(item.feeItemsJson),
|
|
||||||
billingRule: this.normalizeBillingRule(item),
|
|
||||||
}));
|
|
||||||
} finally {
|
} finally {
|
||||||
this.adjust.loading = false;
|
this.adjust.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async saveAdjustment() {
|
async saveAdjustment() {
|
||||||
if (!this.adjust.reason.trim()) return this.$message.warning('请输入调整原因');
|
if (!this.adjust.reason.trim()) return this.$message.warning('请输入调整原因');
|
||||||
|
if (this.adjust.rows.some(row => row.calculating)) {
|
||||||
|
return this.$message.warning('费用正在重新计算,请稍候');
|
||||||
|
}
|
||||||
if (!this.adjust.detailId) {
|
if (!this.adjust.detailId) {
|
||||||
const target = this.adjust.targetRow;
|
const target = this.adjust.targetRow;
|
||||||
const edited = this.adjust.rows[0];
|
const edited = this.adjust.rows[0];
|
||||||
if (!target || !edited) return;
|
if (!target || !edited) return;
|
||||||
const originalAmount = Number(
|
// 原金额以明细进入结算单时的费用快照为基准,调整金额 = 结算金额 - 原金额。
|
||||||
|
const baseRows = this.detailFeeSnapshots[this.adjust.feeKey] || [];
|
||||||
|
const originalAmount = baseRows.length
|
||||||
|
? Number(
|
||||||
|
baseRows
|
||||||
|
.reduce((total, row) => total + Number(row.settlementAmountTax || 0), 0)
|
||||||
|
.toFixed(2)
|
||||||
|
)
|
||||||
|
: Number(
|
||||||
target.originalAmount ??
|
target.originalAmount ??
|
||||||
target.totalAmount ??
|
target.totalAmount ??
|
||||||
target.afterAmount ??
|
target.afterAmount ??
|
||||||
@@ -1870,9 +2120,15 @@ export default {
|
|||||||
remark: edited.remark,
|
remark: edited.remark,
|
||||||
pendingDetailAdjustment: true,
|
pendingDetailAdjustment: true,
|
||||||
});
|
});
|
||||||
this.buildSummaryFeesFromDetails();
|
if (this.adjust.feeKey) {
|
||||||
|
this.pendingAdjustments[this.adjust.feeKey] = {
|
||||||
|
reason: this.adjust.reason,
|
||||||
|
rows: this.cloneAdjustRows(this.adjust.rows),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await this.refreshSummaryFees();
|
||||||
this.adjust.visible = false;
|
this.adjust.visible = false;
|
||||||
this.$message.success('调整已暂存,请保存正式结算单提交');
|
this.$message.success('调整已暂存,保存正式结算单后生效');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.adjust.saving = true;
|
this.adjust.saving = true;
|
||||||
|
|||||||
@@ -102,9 +102,9 @@
|
|||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in feeCategoryOptions"
|
v-for="item in feeCategoryOptions"
|
||||||
:key="item.id || item.dictValue"
|
:key="item.id || item.dictKey"
|
||||||
:label="item.dictKey || item.dictValue"
|
:label="item.dictValue || item.dictKey"
|
||||||
:value="item.dictValue"
|
:value="item.dictKey"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-select
|
<el-select
|
||||||
@@ -701,6 +701,7 @@
|
|||||||
:min="0"
|
:min="0"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
@change="recalculateAdjustRow(row, 'transportQuantity')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -712,6 +713,7 @@
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
:disabled="adjustDialog.readonly"
|
:disabled="adjustDialog.readonly"
|
||||||
|
@change="recalculateAdjustRow(row, 'mileage')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -723,6 +725,7 @@
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
:disabled="adjustDialog.readonly"
|
:disabled="adjustDialog.readonly"
|
||||||
|
@change="recalculateAdjustRow(row, 'unitPrice')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -921,6 +924,7 @@ import {
|
|||||||
save,
|
save,
|
||||||
submit,
|
submit,
|
||||||
} from '@/api/settlement/preSettlement';
|
} from '@/api/settlement/preSettlement';
|
||||||
|
import { calculateAdjustedFee, getFeeDetail } from '@/api/settlement/receivable-payable-detail';
|
||||||
import { getDictionary } from '@/api/system/dictbiz';
|
import { getDictionary } from '@/api/system/dictbiz';
|
||||||
import {
|
import {
|
||||||
emptyPreSettlementForm,
|
emptyPreSettlementForm,
|
||||||
@@ -968,10 +972,6 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
deferSave: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'success'],
|
emits: ['update:modelValue', 'success'],
|
||||||
data() {
|
data() {
|
||||||
@@ -1071,10 +1071,13 @@ export default {
|
|||||||
readonly: false,
|
readonly: false,
|
||||||
activeTab: 'adjust',
|
activeTab: 'adjust',
|
||||||
detailId: '',
|
detailId: '',
|
||||||
|
sourceDetailId: '',
|
||||||
detailLineNo: '',
|
detailLineNo: '',
|
||||||
reason: '',
|
reason: '',
|
||||||
},
|
},
|
||||||
adjustRows: [],
|
adjustRows: [],
|
||||||
|
pendingAdjustments: {},
|
||||||
|
sourceFeeSnapshots: {},
|
||||||
adjustChangeRecordVisible: false,
|
adjustChangeRecordVisible: false,
|
||||||
adjustChangeRecord: null,
|
adjustChangeRecord: null,
|
||||||
adjustChangeRecordDetailRows: [],
|
adjustChangeRecordDetailRows: [],
|
||||||
@@ -1149,24 +1152,11 @@ export default {
|
|||||||
return this.form.settlementType === 'receivable' ? '应收' : '应付';
|
return this.form.settlementType === 'receivable' ? '应收' : '应付';
|
||||||
},
|
},
|
||||||
summaryTotal() {
|
summaryTotal() {
|
||||||
|
// 与结算合计表格的合计行保持一致:包含手工添加的费用与明细调整后的金额。
|
||||||
if (this.summaryFees.length) {
|
if (this.summaryFees.length) {
|
||||||
return this.summaryFees.reduce(
|
return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0);
|
||||||
(total, row) => total + Number(row.settlementAmount || 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return this.details.reduce(
|
return this.details.reduce((total, row) => total + this.detailSettlementAmount(row), 0);
|
||||||
(total, row) =>
|
|
||||||
total +
|
|
||||||
Number(
|
|
||||||
row.settlementAmountTax ??
|
|
||||||
row.totalAmount ??
|
|
||||||
row.afterAmount ??
|
|
||||||
row.settlementAmount ??
|
|
||||||
0
|
|
||||||
),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
visibleDetailColumns() {
|
visibleDetailColumns() {
|
||||||
if (!this.readonly && !this.pageMode) return this.detailColumns;
|
if (!this.readonly && !this.pageMode) return this.detailColumns;
|
||||||
@@ -1226,9 +1216,9 @@ export default {
|
|||||||
await this.loadFeeCategoryOptions();
|
await this.loadFeeCategoryOptions();
|
||||||
await this.loadTransportTypeOptions();
|
await this.loadTransportTypeOptions();
|
||||||
if (this.recordId) await this.loadDetail();
|
if (this.recordId) await this.loadDetail();
|
||||||
else if (this.initialData) this.applyInitialData();
|
else if (this.initialData) await this.applyInitialData();
|
||||||
},
|
},
|
||||||
applyInitialData() {
|
async applyInitialData() {
|
||||||
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
|
const rows = Array.isArray(this.initialData?.rows) ? this.initialData.rows : [];
|
||||||
if (!rows.length) return;
|
if (!rows.length) return;
|
||||||
const first = rows[0];
|
const first = rows[0];
|
||||||
@@ -1264,25 +1254,55 @@ export default {
|
|||||||
localCurrency: first.localCurrency || 'RMB',
|
localCurrency: first.localCurrency || 'RMB',
|
||||||
});
|
});
|
||||||
this.details = rows.map(row => this.normalizeSourceDetail(row));
|
this.details = rows.map(row => this.normalizeSourceDetail(row));
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
await this.ensureSourceFeeSnapshots();
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
this.buildSummaryFeesFromDetails();
|
this.buildSummaryFeesFromDetails();
|
||||||
},
|
},
|
||||||
normalizeSourceDetail(row) {
|
normalizeSourceDetail(row) {
|
||||||
const originalAmount =
|
const settlementAmount = this.detailSettlementAmount(row);
|
||||||
row.originalAmount ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount ?? 0;
|
const originalValue =
|
||||||
|
row.originalAmount ?? row.originalSettlementAmount ?? row.totalAmount ?? settlementAmount;
|
||||||
|
const originalAmount = Number(originalValue || 0);
|
||||||
|
const adjustValue = row.adjustAmount ?? row.adjustmentAmount;
|
||||||
|
const adjustAmount =
|
||||||
|
adjustValue === undefined || adjustValue === null
|
||||||
|
? settlementAmount - originalAmount
|
||||||
|
: Number(adjustValue || 0);
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
sourceDetailId: row.sourceDetailId || row.id,
|
sourceDetailId: row.sourceDetailId || row.id,
|
||||||
originalAmount,
|
originalAmount: Number(originalAmount.toFixed(2)),
|
||||||
adjustAmount: row.adjustAmount ?? 0,
|
adjustAmount: Number(adjustAmount.toFixed(2)),
|
||||||
settlementAmountTax:
|
settlementAmountTax: Number(settlementAmount.toFixed(2)),
|
||||||
row.settlementAmountTax ??
|
|
||||||
row.totalAmount ??
|
|
||||||
row.afterAmount ??
|
|
||||||
row.settlementAmount ??
|
|
||||||
originalAmount,
|
|
||||||
feeItemsJson: row.feeItemsJson || JSON.stringify(row.feeItems || {}),
|
feeItemsJson: row.feeItemsJson || JSON.stringify(row.feeItems || {}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
detailOriginalAmount(row) {
|
||||||
|
return Number(row?.originalAmount ?? 0);
|
||||||
|
},
|
||||||
|
detailAdjustAmount(row) {
|
||||||
|
const value = row?.adjustAmount ?? row?.adjustmentAmount;
|
||||||
|
return Number(
|
||||||
|
Number(
|
||||||
|
value === undefined || value === null
|
||||||
|
? this.detailSettlementAmount(row) - this.detailOriginalAmount(row)
|
||||||
|
: value
|
||||||
|
).toFixed(2)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
detailSettlementAmount(row) {
|
||||||
|
return Number(
|
||||||
|
row?.settlementAmountTax ??
|
||||||
|
row?.totalAmount ??
|
||||||
|
row?.afterAmount ??
|
||||||
|
row?.settlementAmount ??
|
||||||
|
0
|
||||||
|
);
|
||||||
|
},
|
||||||
resetEditor() {
|
resetEditor() {
|
||||||
this.form = emptyPreSettlementForm();
|
this.form = emptyPreSettlementForm();
|
||||||
this.summaryFees = [];
|
this.summaryFees = [];
|
||||||
@@ -1300,8 +1320,11 @@ export default {
|
|||||||
this.attachments = [];
|
this.attachments = [];
|
||||||
this.selectedAttachmentRows = [];
|
this.selectedAttachmentRows = [];
|
||||||
this.adjustRows = [];
|
this.adjustRows = [];
|
||||||
|
this.pendingAdjustments = {};
|
||||||
|
this.sourceFeeSnapshots = {};
|
||||||
this.adjustDialog.activeTab = 'adjust';
|
this.adjustDialog.activeTab = 'adjust';
|
||||||
this.adjustDialog.detailId = '';
|
this.adjustDialog.detailId = '';
|
||||||
|
this.adjustDialog.sourceDetailId = '';
|
||||||
this.adjustDialog.detailLineNo = '';
|
this.adjustDialog.detailLineNo = '';
|
||||||
this.adjustChangeRecordVisible = false;
|
this.adjustChangeRecordVisible = false;
|
||||||
this.adjustChangeRecord = null;
|
this.adjustChangeRecord = null;
|
||||||
@@ -1323,7 +1346,7 @@ export default {
|
|||||||
const detail = data?.data || {};
|
const detail = data?.data || {};
|
||||||
this.form = { ...emptyPreSettlementForm(), ...detail };
|
this.form = { ...emptyPreSettlementForm(), ...detail };
|
||||||
this.summaryFees = (detail.summaryFees || []).map(row => ({ ...row }));
|
this.summaryFees = (detail.summaryFees || []).map(row => ({ ...row }));
|
||||||
this.details = (detail.details || []).map(row => ({ ...row }));
|
this.details = (detail.details || []).map(row => this.normalizeSourceDetail(row));
|
||||||
this.advances = (detail.advances || []).map(row => ({
|
this.advances = (detail.advances || []).map(row => ({
|
||||||
...row,
|
...row,
|
||||||
createUserName: row.createUserName || detail.createUserName,
|
createUserName: row.createUserName || detail.createUserName,
|
||||||
@@ -1499,6 +1522,8 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const { data } = await save(this.buildSavePayload());
|
const { data } = await save(this.buildSavePayload());
|
||||||
this.form.id = data?.data || this.form.id;
|
this.form.id = data?.data || this.form.id;
|
||||||
|
await this.loadDetail();
|
||||||
|
await this.persistPendingAdjustments();
|
||||||
if (shouldSubmit) {
|
if (shouldSubmit) {
|
||||||
await submit({ id: this.form.id });
|
await submit({ id: this.form.id });
|
||||||
this.$message.success('审批流程已发起');
|
this.$message.success('审批流程已发起');
|
||||||
@@ -1507,7 +1532,6 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$message.success('保存成功');
|
this.$message.success('保存成功');
|
||||||
await this.loadDetail();
|
|
||||||
this.$emit('success', this.form.id);
|
this.$emit('success', this.form.id);
|
||||||
} finally {
|
} finally {
|
||||||
this[stateKey] = false;
|
this[stateKey] = false;
|
||||||
@@ -1555,19 +1579,22 @@ export default {
|
|||||||
)?.feeItems || []
|
)?.feeItems || []
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
feeCategoryValue(value) {
|
findFeeCategory(value) {
|
||||||
if (value === undefined || value === null || value === '') return '';
|
return this.feeCategoryOptions.find(
|
||||||
const option = this.feeCategoryOptions.find(
|
|
||||||
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
|
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
|
||||||
);
|
);
|
||||||
return option?.dictValue || value;
|
},
|
||||||
|
// fee_category 业务字典:dictKey 为字典键值(落库值),dictValue 为字典名称(展示值)。
|
||||||
|
feeCategoryValue(value) {
|
||||||
|
if (value === undefined || value === null || value === '') return '';
|
||||||
|
return this.findFeeCategory(value)?.dictKey ?? value;
|
||||||
},
|
},
|
||||||
feeCategoryName(value) {
|
feeCategoryName(value) {
|
||||||
if (value === undefined || value === null || value === '') return '';
|
if (value === undefined || value === null || value === '') return '';
|
||||||
const option = this.feeCategoryOptions.find(
|
const option = this.findFeeCategory(value);
|
||||||
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
|
if (option) return option.dictValue ?? value;
|
||||||
);
|
const feeOption = this.feeOptions.find(item => String(item.feeType) === String(value));
|
||||||
return option?.dictKey || value;
|
return feeOption?.feeTypeName || value;
|
||||||
},
|
},
|
||||||
transportTypeName(value) {
|
transportTypeName(value) {
|
||||||
if (value === undefined || value === null || value === '') return '';
|
if (value === undefined || value === null || value === '') return '';
|
||||||
@@ -1607,71 +1634,139 @@ export default {
|
|||||||
return totalText;
|
return totalText;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// 结算合计按费用项归集:原金额取来源费用明细的初始金额,结算金额取调整后的金额,调整金额为两者之差。
|
||||||
buildSummaryFeesFromDetails() {
|
buildSummaryFeesFromDetails() {
|
||||||
const summaryMap = new Map();
|
const summaryMap = new Map();
|
||||||
const defaultFreightItem =
|
const generatedFees = new Map(
|
||||||
this.feeOptions
|
this.summaryFees
|
||||||
.flatMap(item => item.feeItems || [])
|
.filter(row => Number(row.manualFlag) !== 1)
|
||||||
.find(name => this.isFreightFeeItem(name)) || '运输费';
|
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||||||
const appendSummary = (feeItem, value, fallbackFeeType = '') => {
|
);
|
||||||
const amount = Number(value || 0);
|
const appendSummary = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
|
||||||
if (!feeItem || !Number.isFinite(amount) || Math.abs(amount) < 0.005) return;
|
const originalAmount = Number(originalValue || 0);
|
||||||
|
const settlementAmount = Number(settlementValue || 0);
|
||||||
|
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Math.abs(originalAmount) < 0.005 && Math.abs(settlementAmount) < 0.005) return;
|
||||||
const feeOption = this.feeOptions.find(item =>
|
const feeOption = this.feeOptions.find(item =>
|
||||||
(item.feeItems || []).some(name => String(name) === String(feeItem))
|
(item.feeItems || []).some(name => String(name) === String(feeItem))
|
||||||
);
|
);
|
||||||
const feeType = this.feeCategoryValue(feeOption?.feeType || fallbackFeeType);
|
const feeType = this.feeCategoryValue(feeOption?.feeType || fallbackFeeType);
|
||||||
const key = `${feeType}\u0000${feeItem}`;
|
const key = `${feeType}\u0000${feeItem}`;
|
||||||
|
const generated = generatedFees.get(key);
|
||||||
const current = summaryMap.get(key) || {
|
const current = summaryMap.get(key) || {
|
||||||
id: '',
|
id: generated?.id || '',
|
||||||
feeType,
|
feeType,
|
||||||
feeItem,
|
feeItem,
|
||||||
originalAmount: 0,
|
originalAmount: 0,
|
||||||
adjustAmount: 0,
|
adjustAmount: 0,
|
||||||
settlementAmount: 0,
|
settlementAmount: 0,
|
||||||
remark: '',
|
remark: generated?.remark || '',
|
||||||
manualFlag: 0,
|
manualFlag: 0,
|
||||||
};
|
};
|
||||||
current.originalAmount = Number((current.originalAmount + amount).toFixed(2));
|
current.originalAmount = Number((current.originalAmount + originalAmount).toFixed(2));
|
||||||
current.settlementAmount = current.originalAmount;
|
current.settlementAmount = Number((current.settlementAmount + settlementAmount).toFixed(2));
|
||||||
|
current.adjustAmount = Number(
|
||||||
|
(current.settlementAmount - current.originalAmount).toFixed(2)
|
||||||
|
);
|
||||||
summaryMap.set(key, current);
|
summaryMap.set(key, current);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.details.forEach(row => {
|
this.details.forEach(detail => {
|
||||||
const feeItems = this.parseFeeItems(row.feeItemsJson || row.feeItems);
|
this.resolveDetailFeePairs(detail).forEach(([baseRow, currentRow]) =>
|
||||||
const feeItemEntries = Object.entries(feeItems);
|
this.appendFeeRowSummary(baseRow, currentRow, detail, appendSummary)
|
||||||
feeItemEntries.forEach(([feeItem, amount]) => appendSummary(feeItem, amount, row.feeType));
|
|
||||||
|
|
||||||
let knownAmount = feeItemEntries.reduce(
|
|
||||||
(total, [, amount]) => total + Number(amount || 0),
|
|
||||||
0
|
|
||||||
);
|
);
|
||||||
if (!feeItemEntries.some(([feeItem]) => this.isFreightFeeItem(feeItem))) {
|
|
||||||
const freightAmount = Number(row.freightAmount || 0);
|
|
||||||
appendSummary(defaultFreightItem, freightAmount, row.feeType);
|
|
||||||
knownAmount += freightAmount;
|
|
||||||
}
|
|
||||||
if (!feeItemEntries.some(([feeItem]) => !this.isFreightFeeItem(feeItem))) {
|
|
||||||
const otherFeeAmount = Number(row.otherFeeAmount || 0);
|
|
||||||
appendSummary('其他费用', otherFeeAmount, row.feeType);
|
|
||||||
knownAmount += otherFeeAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalAmount = Number(
|
|
||||||
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount ?? 0
|
|
||||||
);
|
|
||||||
const residualAmount = Number((totalAmount - knownAmount).toFixed(2));
|
|
||||||
if (Math.abs(residualAmount) >= 0.005) {
|
|
||||||
appendSummary('其他费用', residualAmount, row.feeType);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const manualFees = this.summaryFees.filter(row => row.manualFlag === 1);
|
const manualFees = this.summaryFees.filter(row => Number(row.manualFlag) === 1);
|
||||||
this.summaryFees = [...summaryMap.values(), ...manualFees];
|
this.summaryFees = [...summaryMap.values(), ...manualFees];
|
||||||
this.form.settlementAmount = this.summaryFees
|
|
||||||
.reduce((total, row) => total + Number(row.settlementAmount || 0), 0)
|
|
||||||
.toFixed(2);
|
|
||||||
this.recalculateLocalAmount();
|
this.recalculateLocalAmount();
|
||||||
},
|
},
|
||||||
|
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少来源费用快照时退化为明细自身的汇总行。
|
||||||
|
resolveDetailFeePairs(detail) {
|
||||||
|
const sourceDetailId = String(detail.sourceDetailId || detail.id || '');
|
||||||
|
const baseRows = this.sourceFeeSnapshots[sourceDetailId] || [];
|
||||||
|
if (!baseRows.length) return [this.buildDetailFeePair(detail)];
|
||||||
|
const pendingRows = this.pendingAdjustments[sourceDetailId]?.rows || [];
|
||||||
|
return baseRows.map((baseRow, index) => {
|
||||||
|
const matched =
|
||||||
|
pendingRows.find(row => baseRow.id && String(row.id) === String(baseRow.id)) ||
|
||||||
|
pendingRows[index];
|
||||||
|
return [baseRow, matched || baseRow];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildDetailFeePair(detail) {
|
||||||
|
const baseRow = {
|
||||||
|
feeItems: this.parseFeeItems(detail.feeItemsJson || detail.feeItems),
|
||||||
|
feeType: detail.feeType,
|
||||||
|
freightAmount: Number(detail.freightAmount || 0),
|
||||||
|
settlementAmountTax: this.detailOriginalAmount(detail),
|
||||||
|
};
|
||||||
|
return [baseRow, { ...baseRow, settlementAmountTax: this.detailSettlementAmount(detail) }];
|
||||||
|
},
|
||||||
|
appendFeeRowSummary(baseRow, currentRow, detail, appendSummary) {
|
||||||
|
const fallbackFeeType = currentRow.feeType || detail.feeType || '';
|
||||||
|
const feeItemNames = Array.from(
|
||||||
|
new Set([...Object.keys(baseRow.feeItems || {}), ...Object.keys(currentRow.feeItems || {})])
|
||||||
|
);
|
||||||
|
feeItemNames.forEach(name =>
|
||||||
|
appendSummary(name, baseRow.feeItems?.[name], currentRow.feeItems?.[name], fallbackFeeType)
|
||||||
|
);
|
||||||
|
const hasFreightItem = feeItemNames.some(name => this.isFreightFeeItem(name));
|
||||||
|
if (!hasFreightItem) {
|
||||||
|
appendSummary(
|
||||||
|
this.defaultFreightFeeItem(),
|
||||||
|
baseRow.freightAmount,
|
||||||
|
currentRow.freightAmount,
|
||||||
|
fallbackFeeType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const knownAmount = row =>
|
||||||
|
this.sumFeeItems(row.feeItems) + (hasFreightItem ? 0 : Number(row.freightAmount || 0));
|
||||||
|
const baseResidual = Number(
|
||||||
|
(Number(baseRow.settlementAmountTax || 0) - knownAmount(baseRow)).toFixed(2)
|
||||||
|
);
|
||||||
|
const currentResidual = Number(
|
||||||
|
(Number(currentRow.settlementAmountTax || 0) - knownAmount(currentRow)).toFixed(2)
|
||||||
|
);
|
||||||
|
if (Math.abs(baseResidual) < 0.005 && Math.abs(currentResidual) < 0.005) return;
|
||||||
|
// 结算金额被手工改写且无法拆分到具体费用项时,计入金额占比最大的费用项,否则归入其他费用。
|
||||||
|
appendSummary(
|
||||||
|
this.residualFeeItem(baseRow, currentRow),
|
||||||
|
baseResidual,
|
||||||
|
currentResidual,
|
||||||
|
fallbackFeeType
|
||||||
|
);
|
||||||
|
},
|
||||||
|
defaultFreightFeeItem() {
|
||||||
|
return (
|
||||||
|
this.feeOptions
|
||||||
|
.flatMap(item => item.feeItems || [])
|
||||||
|
.find(name => this.isFreightFeeItem(name)) || '运输费'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
residualFeeItem(baseRow, currentRow) {
|
||||||
|
const feeItems = { ...(baseRow.feeItems || {}), ...(currentRow.feeItems || {}) };
|
||||||
|
const dominant = Object.entries(feeItems)
|
||||||
|
.filter(([, amount]) => Math.abs(Number(amount || 0)) >= 0.005)
|
||||||
|
.sort((a, b) => Math.abs(Number(b[1] || 0)) - Math.abs(Number(a[1] || 0)))[0];
|
||||||
|
if (dominant) return dominant[0];
|
||||||
|
return (
|
||||||
|
currentRow.billingRule?.feeItem ||
|
||||||
|
currentRow.feeItem ||
|
||||||
|
baseRow.billingRule?.feeItem ||
|
||||||
|
baseRow.feeItem ||
|
||||||
|
'其他费用'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sumFeeItems(feeItems) {
|
||||||
|
return Number(
|
||||||
|
Object.values(feeItems || {})
|
||||||
|
.reduce((total, value) => total + Number(value || 0), 0)
|
||||||
|
.toFixed(2)
|
||||||
|
);
|
||||||
|
},
|
||||||
recalculateLocalAmount() {
|
recalculateLocalAmount() {
|
||||||
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
|
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
|
||||||
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
|
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
|
||||||
@@ -1731,9 +1826,11 @@ export default {
|
|||||||
this.details.push(this.normalizeSourceDetail(row));
|
this.details.push(this.normalizeSourceDetail(row));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.buildSummaryFeesFromDetails();
|
|
||||||
this.candidateDialog.confirming = true;
|
this.candidateDialog.confirming = true;
|
||||||
if (this.deferSave || (this.pageMode && !this.form.id)) {
|
await this.ensureSourceFeeSnapshots();
|
||||||
|
this.buildSummaryFeesFromDetails();
|
||||||
|
// 页面模式选择明细先更新本地数据,保存/提交时再统一落库。
|
||||||
|
if (this.pageMode) {
|
||||||
this.candidateDialog.visible = false;
|
this.candidateDialog.visible = false;
|
||||||
this.candidateDialog.confirming = false;
|
this.candidateDialog.confirming = false;
|
||||||
this.$message.success('结算明细添加成功,请点击保存提交');
|
this.$message.success('结算明细添加成功,请点击保存提交');
|
||||||
@@ -1774,48 +1871,129 @@ export default {
|
|||||||
};
|
};
|
||||||
this.appliedDetailQuery = { ...this.detailQuery };
|
this.appliedDetailQuery = { ...this.detailQuery };
|
||||||
},
|
},
|
||||||
async persistDetailForAdjustment(row) {
|
normalizeAdjustRow(item, feeItemNames = []) {
|
||||||
const sourceDetailId = row.sourceDetailId || row.id;
|
const rawFeeItems =
|
||||||
const isPersistedDetail =
|
item.feeItems && typeof item.feeItems === 'object'
|
||||||
row.id && sourceDetailId && String(row.id) !== String(sourceDetailId);
|
? item.feeItems
|
||||||
if (isPersistedDetail) return row;
|
: this.parseFeeItems(item.feeItemsJson);
|
||||||
|
const names = feeItemNames.length ? feeItemNames : Object.keys(rawFeeItems);
|
||||||
await this.$refs.formRef?.validate();
|
const adjusted = {
|
||||||
this.loading = true;
|
...item,
|
||||||
try {
|
transportQuantity: Number(item.transportQuantity || 0),
|
||||||
const { data } = await save(this.buildSavePayload());
|
mileage:
|
||||||
this.form.id = data?.data || this.form.id;
|
item.mileage === null || item.mileage === undefined || Number(item.mileage) === -1
|
||||||
await this.loadDetail();
|
? null
|
||||||
const savedRow = this.details.find(
|
: Number(item.mileage),
|
||||||
item => String(item.sourceDetailId || '') === String(sourceDetailId || '')
|
freightAmount: Number(item.freightAmount || 0),
|
||||||
|
originalAmount: Number(item.originalAmount || 0),
|
||||||
|
feeItems: Object.fromEntries(names.map(name => [name, Number(rawFeeItems[name] || 0)])),
|
||||||
|
settlementAmountTax: Number(item.settlementAmountTax ?? item.afterAmount ?? 0),
|
||||||
|
settlementAmountNoTax: item.settlementAmountNoTax,
|
||||||
|
billingRule: this.normalizeBillingRule(item),
|
||||||
|
calculating: false,
|
||||||
|
calculateError: '',
|
||||||
|
adjustCalculateTimer: null,
|
||||||
|
adjustCalculateVersion: 0,
|
||||||
|
};
|
||||||
|
if (item.settlementAmountTax === undefined && item.afterAmount === undefined) {
|
||||||
|
this.recalculateAdjustRow(adjusted);
|
||||||
|
}
|
||||||
|
return adjusted;
|
||||||
|
},
|
||||||
|
cloneAdjustRows(rows) {
|
||||||
|
return (rows || []).map(row => ({
|
||||||
|
...row,
|
||||||
|
feeItems: { ...row.feeItems },
|
||||||
|
calculating: false,
|
||||||
|
calculateError: '',
|
||||||
|
adjustCalculateTimer: null,
|
||||||
|
adjustCalculateVersion: 0,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
async loadSourceFeeRows(sourceDetailId) {
|
||||||
|
const response = await getFeeDetail(sourceDetailId);
|
||||||
|
const data = response.data?.data || response.data || {};
|
||||||
|
return (data.records || []).map(item =>
|
||||||
|
this.normalizeAdjustRow(item, data.feeItemNames || [])
|
||||||
);
|
);
|
||||||
if (!savedRow) {
|
},
|
||||||
this.$message.warning('结算明细保存失败,请重试');
|
// 明细列表只有汇总金额,费用项级别的原始金额需要按来源明细单独拉取并缓存为调整基准。
|
||||||
return null;
|
async ensureSourceFeeSnapshots() {
|
||||||
}
|
const targets = this.details.filter(row => {
|
||||||
this.$emit('success', this.form.id);
|
const sourceDetailId = String(row.sourceDetailId || row.id || '');
|
||||||
return savedRow;
|
return sourceDetailId && !this.sourceFeeSnapshots[sourceDetailId];
|
||||||
} finally {
|
});
|
||||||
this.loading = false;
|
if (!targets.length) return;
|
||||||
|
await Promise.all(
|
||||||
|
targets.map(async row => {
|
||||||
|
const sourceDetailId = String(row.sourceDetailId || row.id || '');
|
||||||
|
try {
|
||||||
|
const feeRows = await this.loadSourceFeeRows(sourceDetailId);
|
||||||
|
if (!feeRows.length) return;
|
||||||
|
this.sourceFeeSnapshots[sourceDetailId] = feeRows;
|
||||||
|
this.applySnapshotToDetail(row, feeRows);
|
||||||
|
} catch (error) {
|
||||||
|
// 取不到费用明细时退回按明细汇总金额归集,后续操作会再次尝试拉取。
|
||||||
|
delete this.sourceFeeSnapshots[sourceDetailId];
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
applySnapshotToDetail(detail, feeRows) {
|
||||||
|
if (!feeRows.length) return;
|
||||||
|
const feeItems = {};
|
||||||
|
feeRows.forEach(row =>
|
||||||
|
Object.entries(row.feeItems || {}).forEach(([name, amount]) => {
|
||||||
|
feeItems[name] = Number((Number(feeItems[name] || 0) + Number(amount || 0)).toFixed(2));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const settlementAmount = feeRows.reduce(
|
||||||
|
(total, row) => total + Number(row.settlementAmountTax || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const freightAmount = feeRows.reduce(
|
||||||
|
(total, row) => total + Number(row.freightAmount || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
Object.assign(detail, {
|
||||||
|
feeItemsJson: JSON.stringify(feeItems),
|
||||||
|
freightAmount: Number(freightAmount.toFixed(2)),
|
||||||
|
originalAmount: Number(settlementAmount.toFixed(2)),
|
||||||
|
adjustAmount: 0,
|
||||||
|
settlementAmountTax: Number(settlementAmount.toFixed(2)),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
async openAdjustDialog(row, readonly) {
|
async openAdjustDialog(row, readonly) {
|
||||||
const detailRow = await this.persistDetailForAdjustment(row);
|
const detailRow = row;
|
||||||
if (!detailRow) return;
|
|
||||||
this.adjustDialog.visible = true;
|
this.adjustDialog.visible = true;
|
||||||
this.adjustDialog.loading = true;
|
this.adjustDialog.loading = true;
|
||||||
this.adjustDialog.readonly = readonly;
|
this.adjustDialog.readonly = readonly;
|
||||||
this.adjustDialog.activeTab = 'adjust';
|
this.adjustDialog.activeTab = 'adjust';
|
||||||
this.adjustDialog.detailId = detailRow.id;
|
this.adjustDialog.detailId = detailRow.id;
|
||||||
this.adjustDialog.detailLineNo = detailRow.lineNo;
|
this.adjustDialog.detailLineNo = detailRow.lineNo;
|
||||||
this.adjustDialog.reason = '';
|
this.adjustDialog.sourceDetailId =
|
||||||
|
detailRow.id && String(detailRow.id) === String(detailRow.sourceDetailId)
|
||||||
|
? detailRow.sourceDetailId
|
||||||
|
: '';
|
||||||
|
const sourceDetailId = String(this.adjustDialog.sourceDetailId || '');
|
||||||
|
const pending = sourceDetailId ? this.pendingAdjustments[sourceDetailId] : null;
|
||||||
|
this.adjustDialog.reason = pending?.reason || '';
|
||||||
try {
|
try {
|
||||||
const { data } = await getDetailFees(detailRow.id);
|
// 已暂存的调整结果优先回显,避免重新打开时又回到调整前的数据。
|
||||||
this.adjustRows = (data?.data || []).map(item => ({
|
if (pending?.rows?.length) {
|
||||||
...item,
|
this.adjustRows = this.cloneAdjustRows(pending.rows);
|
||||||
feeItems: this.parseFeeItems(item.feeItemsJson),
|
return;
|
||||||
billingRule: this.normalizeBillingRule(item),
|
}
|
||||||
}));
|
if (sourceDetailId) {
|
||||||
|
if (!this.sourceFeeSnapshots[sourceDetailId]) {
|
||||||
|
this.sourceFeeSnapshots[sourceDetailId] = await this.loadSourceFeeRows(sourceDetailId);
|
||||||
|
}
|
||||||
|
this.adjustRows = this.cloneAdjustRows(this.sourceFeeSnapshots[sourceDetailId]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = await getDetailFees(detailRow.id);
|
||||||
|
const rows = response.data?.data || response.data || [];
|
||||||
|
this.adjustRows = rows.map(item => this.normalizeAdjustRow(item));
|
||||||
} finally {
|
} finally {
|
||||||
this.adjustDialog.loading = false;
|
this.adjustDialog.loading = false;
|
||||||
}
|
}
|
||||||
@@ -1957,6 +2135,45 @@ export default {
|
|||||||
row.settlementAmountTax = Number(
|
row.settlementAmountTax = Number(
|
||||||
(hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2)
|
(hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2)
|
||||||
);
|
);
|
||||||
|
if (changedField && this.adjustDialog.sourceDetailId && row.id) {
|
||||||
|
this.scheduleSourceAdjustCalculation(row);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scheduleSourceAdjustCalculation(row) {
|
||||||
|
if (row.adjustCalculateTimer) clearTimeout(row.adjustCalculateTimer);
|
||||||
|
row.adjustCalculateVersion += 1;
|
||||||
|
row.calculating = true;
|
||||||
|
const version = row.adjustCalculateVersion;
|
||||||
|
row.adjustCalculateTimer = setTimeout(async () => {
|
||||||
|
row.adjustCalculateTimer = null;
|
||||||
|
try {
|
||||||
|
const response = await calculateAdjustedFee({
|
||||||
|
detailId: this.adjustDialog.sourceDetailId,
|
||||||
|
feeId: row.id,
|
||||||
|
transportQuantity: Number(row.transportQuantity || 0),
|
||||||
|
mileage: Number(row.mileage || 0),
|
||||||
|
freightAmount: Number(row.freightAmount || 0),
|
||||||
|
feeItems: Object.fromEntries(
|
||||||
|
Object.entries(row.feeItems || {}).map(([name, amount]) => [
|
||||||
|
name,
|
||||||
|
Number(amount || 0),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (version !== row.adjustCalculateVersion || !this.adjustDialog.visible) return;
|
||||||
|
const data = response.data?.data || response.data || {};
|
||||||
|
row.freightAmount = Number(data.freightAmount || 0);
|
||||||
|
row.feeItems = Object.fromEntries(
|
||||||
|
Object.entries(data.feeItems || {}).map(([name, amount]) => [name, Number(amount || 0)])
|
||||||
|
);
|
||||||
|
row.adjustAmount = Number(data.adjustAmount || 0);
|
||||||
|
row.settlementAmountTax = Number(data.afterAmount || 0);
|
||||||
|
} catch (error) {
|
||||||
|
row.calculateError = error.message || '费用试算失败';
|
||||||
|
} finally {
|
||||||
|
if (version === row.adjustCalculateVersion) row.calculating = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
},
|
},
|
||||||
isFreightFeeItem(name) {
|
isFreightFeeItem(name) {
|
||||||
return String(name || '').includes('运费') || String(name || '').includes('运输费');
|
return String(name || '').includes('运费') || String(name || '').includes('运输费');
|
||||||
@@ -1966,8 +2183,22 @@ export default {
|
|||||||
this.$message.warning('请输入调整原因');
|
this.$message.warning('请输入调整原因');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.adjustRows.some(row => row.calculating)) {
|
||||||
|
this.$message.warning('费用正在重新计算,请稍候');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.adjustDialog.saving = true;
|
this.adjustDialog.saving = true;
|
||||||
try {
|
try {
|
||||||
|
if (this.adjustDialog.sourceDetailId) {
|
||||||
|
this.pendingAdjustments[this.adjustDialog.sourceDetailId] = {
|
||||||
|
reason: this.adjustDialog.reason,
|
||||||
|
rows: this.adjustRows.map(row => ({ ...row, feeItems: { ...row.feeItems } })),
|
||||||
|
};
|
||||||
|
this.applyLocalAdjustment(this.adjustDialog.sourceDetailId, this.adjustRows);
|
||||||
|
this.adjustDialog.visible = false;
|
||||||
|
this.$message.success('结算明细调整已暂存,保存预结算单后生效');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await adjustDetail({
|
await adjustDetail({
|
||||||
detailId: this.adjustDialog.detailId,
|
detailId: this.adjustDialog.detailId,
|
||||||
changeReason: this.adjustDialog.reason,
|
changeReason: this.adjustDialog.reason,
|
||||||
@@ -1991,6 +2222,92 @@ export default {
|
|||||||
this.adjustDialog.saving = false;
|
this.adjustDialog.saving = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
applyLocalAdjustment(sourceDetailId, rows) {
|
||||||
|
const detail = this.details.find(
|
||||||
|
item => String(item.sourceDetailId || item.id) === String(sourceDetailId)
|
||||||
|
);
|
||||||
|
if (!detail) return;
|
||||||
|
// 原金额以进入预结算单时的费用快照为基准,调整金额 = 结算金额 - 原金额。
|
||||||
|
const baseRows = this.sourceFeeSnapshots[String(sourceDetailId)] || [];
|
||||||
|
const originalAmount = baseRows.length
|
||||||
|
? baseRows.reduce((sum, row) => sum + Number(row.settlementAmountTax || 0), 0)
|
||||||
|
: rows.reduce((sum, row) => sum + Number(row.originalAmount || 0), 0);
|
||||||
|
const settlementAmountTax = rows.reduce(
|
||||||
|
(sum, row) => sum + Number(row.settlementAmountTax || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const freightAmount = rows.reduce((sum, row) => sum + Number(row.freightAmount || 0), 0);
|
||||||
|
const feeItems = {};
|
||||||
|
rows.forEach(row =>
|
||||||
|
Object.entries(row.feeItems || {}).forEach(([name, amount]) => {
|
||||||
|
feeItems[name] = Number((Number(feeItems[name] || 0) + Number(amount || 0)).toFixed(2));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Object.assign(detail, {
|
||||||
|
originalAmount: Number(originalAmount.toFixed(2)),
|
||||||
|
adjustAmount: Number((settlementAmountTax - originalAmount).toFixed(2)),
|
||||||
|
settlementAmountTax: Number(settlementAmountTax.toFixed(2)),
|
||||||
|
freightAmount: Number(freightAmount.toFixed(2)),
|
||||||
|
feeItemsJson: JSON.stringify(feeItems),
|
||||||
|
});
|
||||||
|
this.buildSummaryFeesFromDetails();
|
||||||
|
},
|
||||||
|
async persistPendingAdjustments() {
|
||||||
|
const pendingEntries = Object.entries(this.pendingAdjustments);
|
||||||
|
if (!pendingEntries.length || !this.form.id) return;
|
||||||
|
for (const [sourceDetailId, pending] of pendingEntries) {
|
||||||
|
const detail = this.details.find(
|
||||||
|
item => String(item.sourceDetailId || '') === String(sourceDetailId)
|
||||||
|
);
|
||||||
|
if (!detail) continue;
|
||||||
|
const persistedFees = await getDetailFees(detail.id);
|
||||||
|
const feeRows = persistedFees.data?.data || persistedFees.data || [];
|
||||||
|
const pendingRows = pending.rows || [];
|
||||||
|
const rows = feeRows.map(fee => {
|
||||||
|
const pendingRow = this.findPendingAdjustmentRow(fee, pendingRows);
|
||||||
|
if (!pendingRow) {
|
||||||
|
throw new Error(`费用行“${fee.cargoName || fee.lineNo || fee.id}”保存匹配失败`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: fee.id,
|
||||||
|
transportQuantity: pendingRow.transportQuantity,
|
||||||
|
mileage: pendingRow.mileage,
|
||||||
|
unitPrice: pendingRow.unitPrice,
|
||||||
|
freightAmount: pendingRow.freightAmount,
|
||||||
|
feeItems: pendingRow.feeItems,
|
||||||
|
settlementAmountTax: pendingRow.settlementAmountTax,
|
||||||
|
settlementAmountNoTax: pendingRow.settlementAmountNoTax,
|
||||||
|
remark: pendingRow.remark,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (rows.length) {
|
||||||
|
await adjustDetail({ detailId: detail.id, changeReason: pending.reason, rows });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pendingAdjustments = {};
|
||||||
|
await this.loadDetail();
|
||||||
|
},
|
||||||
|
findPendingAdjustmentRow(fee, pendingRows) {
|
||||||
|
const exactIdMatch = pendingRows.find(
|
||||||
|
row => row.id && fee.sourceFeeId && String(row.id) === String(fee.sourceFeeId)
|
||||||
|
);
|
||||||
|
if (exactIdMatch) return exactIdMatch;
|
||||||
|
const exactLineMatch = pendingRows.find(
|
||||||
|
row =>
|
||||||
|
row.lineNo &&
|
||||||
|
fee.lineNo &&
|
||||||
|
String(row.lineNo) === String(fee.lineNo) &&
|
||||||
|
String(row.cargoName || '') === String(fee.cargoName || '') &&
|
||||||
|
String(row.cargoType || '') === String(fee.cargoType || '')
|
||||||
|
);
|
||||||
|
if (exactLineMatch) return exactLineMatch;
|
||||||
|
const cargoMatches = pendingRows.filter(
|
||||||
|
row =>
|
||||||
|
String(row.cargoName || '') === String(fee.cargoName || '') &&
|
||||||
|
String(row.cargoType || '') === String(fee.cargoType || '')
|
||||||
|
);
|
||||||
|
return cargoMatches.length === 1 ? cargoMatches[0] : null;
|
||||||
|
},
|
||||||
exportDetails() {
|
exportDetails() {
|
||||||
if (!this.filteredDetails.length) {
|
if (!this.filteredDetails.length) {
|
||||||
this.$message.warning('暂无可导出的结算明细');
|
this.$message.warning('暂无可导出的结算明细');
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="visible" :title="title" width="98%" top="2vh" append-to-body destroy-on-close>
|
<component
|
||||||
<div v-loading="loading" class="reconciliation-editor">
|
:is="editorContainer"
|
||||||
|
v-bind="editorContainerProps"
|
||||||
|
@update:model-value="visible = $event"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-loading="loading"
|
||||||
|
class="reconciliation-editor"
|
||||||
|
:class="{ 'reconciliation-editor--page': pageMode }"
|
||||||
|
>
|
||||||
<section-card title="导入外部账单,与内部账单核对">
|
<section-card title="导入外部账单,与内部账单核对">
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
@@ -98,17 +106,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="reconciliation-editor__filter">
|
<div class="reconciliation-editor__filter">
|
||||||
<el-input v-model="internalQuery.documentNo" placeholder="单据号" clearable />
|
<div class="reconciliation-editor__filter-item">
|
||||||
<el-input v-model="internalQuery.vehicleNo" placeholder="车号" clearable />
|
<span class="reconciliation-editor__filter-label">单据号</span>
|
||||||
<el-input v-model="internalQuery.batchNo" placeholder="批次号" clearable />
|
<el-input v-model="internalQuery.documentNo" placeholder="请输入" clearable />
|
||||||
<el-input v-model="internalQuery.cargoName" placeholder="货物名称" clearable />
|
</div>
|
||||||
|
<div class="reconciliation-editor__filter-item">
|
||||||
|
<span class="reconciliation-editor__filter-label">车号</span>
|
||||||
|
<el-input v-model="internalQuery.vehicleNo" placeholder="请输入" clearable />
|
||||||
|
</div>
|
||||||
|
<div class="reconciliation-editor__filter-item">
|
||||||
|
<span class="reconciliation-editor__filter-label">批次号</span>
|
||||||
|
<el-input v-model="internalQuery.batchNo" placeholder="请输入" clearable />
|
||||||
|
</div>
|
||||||
|
<div class="reconciliation-editor__filter-item">
|
||||||
|
<span class="reconciliation-editor__filter-label">货物名称</span>
|
||||||
|
<el-input v-model="internalQuery.cargoName" placeholder="请输入" clearable />
|
||||||
|
</div>
|
||||||
<el-button @click="resetInternalQuery">重置</el-button>
|
<el-button @click="resetInternalQuery">重置</el-button>
|
||||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="filteredInternalRows" border max-height="420">
|
<el-table
|
||||||
|
:data="filteredInternalRows"
|
||||||
|
border
|
||||||
|
max-height="420"
|
||||||
|
:row-class-name="internalRowClassName"
|
||||||
|
>
|
||||||
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-for="column in internalColumns"
|
v-for="column in internalTableColumns"
|
||||||
:key="column.prop"
|
:key="column.prop"
|
||||||
v-bind="column"
|
v-bind="column"
|
||||||
align="center"
|
align="center"
|
||||||
@@ -133,6 +158,9 @@
|
|||||||
<span v-else-if="column.prop === 'transportQuantity'">
|
<span v-else-if="column.prop === 'transportQuantity'">
|
||||||
{{ formatTransportQuantity(row.transportQuantity) }}
|
{{ formatTransportQuantity(row.transportQuantity) }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-else-if="column.feeItemName">
|
||||||
|
{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}
|
||||||
|
</span>
|
||||||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="
|
v-else-if="
|
||||||
@@ -149,17 +177,11 @@
|
|||||||
<div class="reconciliation-editor__links">
|
<div class="reconciliation-editor__links">
|
||||||
<el-link v-if="editable" type="primary" @click="openAdjust(row)">调整</el-link>
|
<el-link v-if="editable" type="primary" @click="openAdjust(row)">调整</el-link>
|
||||||
<el-link
|
<el-link
|
||||||
v-if="editable && row.matchResult === 'matched'"
|
v-if="editable && ['matched', 'partial'].includes(row.matchResult)"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleUnmatch(row)"
|
@click="handleUnmatch(row)"
|
||||||
>取消匹配</el-link
|
>取消匹配</el-link
|
||||||
>
|
>
|
||||||
<el-link
|
|
||||||
v-if="editable && row.matchResult !== 'matched'"
|
|
||||||
type="primary"
|
|
||||||
@click="openManualMatch(row)"
|
|
||||||
>人工匹配</el-link
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -192,7 +214,12 @@
|
|||||||
<el-tab-pane :label="`导入明细(${externalDetails.length})`" name="all" />
|
<el-tab-pane :label="`导入明细(${externalDetails.length})`" name="all" />
|
||||||
<el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" />
|
<el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" />
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
<el-table :data="visibleExternalRows" border max-height="420">
|
<el-table
|
||||||
|
:data="visibleExternalRows"
|
||||||
|
border
|
||||||
|
max-height="420"
|
||||||
|
:row-class-name="externalRowClassName"
|
||||||
|
>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-for="column in externalColumns"
|
v-for="column in externalColumns"
|
||||||
:key="column.prop"
|
:key="column.prop"
|
||||||
@@ -204,12 +231,57 @@
|
|||||||
<el-tag v-if="column.prop === 'matchStatus'" :type="matchTagType(row.matchStatus)" class="status-text">{{
|
<el-tag v-if="column.prop === 'matchStatus'" :type="matchTagType(row.matchStatus)" class="status-text">{{
|
||||||
matchName(row.matchStatus)
|
matchName(row.matchStatus)
|
||||||
}}</el-tag>
|
}}</el-tag>
|
||||||
|
<template v-else-if="isExternalRowEditing(row) && isExternalEditableColumn(column.prop)">
|
||||||
|
<el-input-number
|
||||||
|
v-if="column.feeItemName"
|
||||||
|
v-model="row.feeItems[column.feeItemName]"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:controls="false"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<el-date-picker
|
||||||
|
v-else-if="isExternalDateColumn(column.prop)"
|
||||||
|
v-model="row[column.prop]"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
size="small"
|
||||||
|
placeholder="请选择"
|
||||||
|
/>
|
||||||
|
<el-input-number
|
||||||
|
v-else-if="isExternalNumberColumn(column.prop)"
|
||||||
|
v-model="row[column.prop]"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:controls="false"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<el-select
|
||||||
|
v-else-if="column.prop === 'transportType'"
|
||||||
|
v-model="row[column.prop]"
|
||||||
|
size="small"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in transportTypeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input v-else v-model="row[column.prop]" size="small" />
|
||||||
|
</template>
|
||||||
<span v-else-if="column.prop === 'transportType'">
|
<span v-else-if="column.prop === 'transportType'">
|
||||||
{{ transportTypeLabel(row.transportType) }}
|
{{ transportTypeLabel(row.transportType) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="column.prop === 'transportQuantity'">
|
<span v-else-if="column.prop === 'transportQuantity'">
|
||||||
{{ formatTransportQuantity(row.transportQuantity) }}
|
{{ formatTransportQuantity(row.transportQuantity) }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-else-if="column.feeItemName">
|
||||||
|
{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}
|
||||||
|
</span>
|
||||||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="
|
v-else-if="
|
||||||
@@ -221,21 +293,24 @@
|
|||||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="90" fixed="right" align="center">
|
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||||
<template #default="{ row }"
|
<template #default="{ row }">
|
||||||
><el-link
|
<div class="reconciliation-editor__links">
|
||||||
v-if="editable && row.matchStatus !== 'matched'"
|
<template v-if="editable && isExternalRowEditing(row)">
|
||||||
type="primary"
|
<el-link type="primary" @click="finishExternalAdjust(row)">完成</el-link>
|
||||||
@click="openManualMatchByExternal(row)"
|
<el-link type="primary" @click="cancelExternalAdjust(row)">取消</el-link>
|
||||||
>匹配</el-link
|
</template>
|
||||||
></template
|
<el-link v-else-if="editable" type="primary" @click="openExternalAdjust(row)">
|
||||||
>
|
调整
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</section-card>
|
</section-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template v-if="!pageMode" #footer>
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button @click="visible = false">取消</el-button>
|
||||||
<template v-if="editable">
|
<template v-if="editable">
|
||||||
<el-button type="primary" plain :loading="saving" @click="handleSave">保存草稿</el-button>
|
<el-button type="primary" plain :loading="saving" @click="handleSave">保存草稿</el-button>
|
||||||
@@ -248,6 +323,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div v-if="pageMode" class="reconciliation-editor__page-actions">
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<template v-if="editable">
|
||||||
|
<el-button type="primary" plain :loading="actionLoading" @click="handleUpdate">
|
||||||
|
按匹配结果更新账单
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" :loading="actionLoading" @click="handleComplete">
|
||||||
|
完成对账
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="formalDialog.visible" title="选择正式结算单" width="84%" append-to-body>
|
<el-dialog v-model="formalDialog.visible" title="选择正式结算单" width="84%" append-to-body>
|
||||||
<el-form :model="formalDialog.query" inline label-position="right" label-width="88px">
|
<el-form :model="formalDialog.query" inline label-position="right" label-width="88px">
|
||||||
<el-form-item label="正式结算单号"
|
<el-form-item label="正式结算单号"
|
||||||
@@ -335,34 +422,7 @@
|
|||||||
>
|
>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="manualDialog.visible" title="选择外部账单明细" width="86%" append-to-body>
|
</component>
|
||||||
<el-table :data="unmatchedExternalRows" border @row-click="manualDialog.selected = $event">
|
|
||||||
<el-table-column type="index" label="序号" width="64" />
|
|
||||||
<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"
|
|
||||||
><template #default="{ row }">{{
|
|
||||||
formatTransportQuantity(row.transportQuantity)
|
|
||||||
}}</template></el-table-column
|
|
||||||
>
|
|
||||||
<el-table-column prop="settlementAmount" label="结算金额" width="130"
|
|
||||||
><template #default="{ row }">{{
|
|
||||||
formatMoney(row.settlementAmount)
|
|
||||||
}}</template></el-table-column
|
|
||||||
>
|
|
||||||
<el-table-column label="选择" width="80"
|
|
||||||
><template #default="{ row }"
|
|
||||||
><el-radio v-model="manualDialog.selected" :label="row"> </el-radio></template
|
|
||||||
></el-table-column
|
|
||||||
>
|
|
||||||
</el-table>
|
|
||||||
<template #footer
|
|
||||||
><el-button @click="manualDialog.visible = false">取消</el-button
|
|
||||||
><el-button type="primary" @click="confirmManualMatch">确定</el-button></template
|
|
||||||
>
|
|
||||||
</el-dialog>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -385,6 +445,7 @@ export default {
|
|||||||
recordId: [String, Number],
|
recordId: [String, Number],
|
||||||
settlementType: { type: String, default: 'payable' },
|
settlementType: { type: String, default: 'payable' },
|
||||||
readonly: Boolean,
|
readonly: Boolean,
|
||||||
|
pageMode: Boolean,
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'success'],
|
emits: ['update:modelValue', 'success'],
|
||||||
data() {
|
data() {
|
||||||
@@ -411,7 +472,9 @@ export default {
|
|||||||
page: { current: 1, size: 10, total: 0 },
|
page: { current: 1, size: 10, total: 0 },
|
||||||
},
|
},
|
||||||
adjustDialog: { visible: false, saving: false, rows: [], reason: '' },
|
adjustDialog: { visible: false, saving: false, rows: [], reason: '' },
|
||||||
manualDialog: { visible: false, internal: null, selected: null },
|
externalEditing: {},
|
||||||
|
externalEditSnapshots: {},
|
||||||
|
matchingStarted: false,
|
||||||
rules: {
|
rules: {
|
||||||
formalSettlementNo: [{ required: true, message: '请选择正式结算单', trigger: 'change' }],
|
formalSettlementNo: [{ required: true, message: '请选择正式结算单', trigger: 'change' }],
|
||||||
reconciliationMode: [{ required: true, message: '请选择对账模式', trigger: 'change' }],
|
reconciliationMode: [{ required: true, message: '请选择对账模式', trigger: 'change' }],
|
||||||
@@ -429,6 +492,22 @@ export default {
|
|||||||
this.$emit('update:modelValue', value);
|
this.$emit('update:modelValue', value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
editorContainer() {
|
||||||
|
return this.pageMode ? 'div' : 'el-dialog';
|
||||||
|
},
|
||||||
|
editorContainerProps() {
|
||||||
|
if (this.pageMode) {
|
||||||
|
return { class: 'reconciliation-editor-shell reconciliation-editor-shell--page' };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
modelValue: this.visible,
|
||||||
|
title: this.title,
|
||||||
|
width: '98%',
|
||||||
|
top: '2vh',
|
||||||
|
appendToBody: true,
|
||||||
|
destroyOnClose: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
editable() {
|
editable() {
|
||||||
return !this.readonly;
|
return !this.readonly;
|
||||||
},
|
},
|
||||||
@@ -442,7 +521,7 @@ export default {
|
|||||||
filteredInternalRows() {
|
filteredInternalRows() {
|
||||||
void this.internalFilterTick;
|
void this.internalFilterTick;
|
||||||
const query = this.internalQuery;
|
const query = this.internalQuery;
|
||||||
return this.internalDetails.filter(
|
return this.groupedInternalRows.filter(
|
||||||
row =>
|
row =>
|
||||||
(!query.documentNo || String(row.documentNo || '').includes(query.documentNo)) &&
|
(!query.documentNo || String(row.documentNo || '').includes(query.documentNo)) &&
|
||||||
(!query.vehicleNo || String(row.vehicleNo || '').includes(query.vehicleNo)) &&
|
(!query.vehicleNo || String(row.vehicleNo || '').includes(query.vehicleNo)) &&
|
||||||
@@ -450,10 +529,62 @@ export default {
|
|||||||
(!query.cargoName || String(row.cargoName || '').includes(query.cargoName))
|
(!query.cargoName || String(row.cargoName || '').includes(query.cargoName))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
groupedInternalRows() {
|
||||||
|
const groups = new Map();
|
||||||
|
this.internalDetails.forEach((row, index) => {
|
||||||
|
const key = row.documentNo ? String(row.documentNo) : `__row_${index}`;
|
||||||
|
if (!groups.has(key)) groups.set(key, []);
|
||||||
|
groups.get(key).push(row);
|
||||||
|
});
|
||||||
|
return [...groups.values()].map(rows => this.mergeInternalRows(rows));
|
||||||
|
},
|
||||||
|
internalFeeItemNames() {
|
||||||
|
const names = new Set();
|
||||||
|
this.internalDetails.forEach(row => {
|
||||||
|
Object.keys(row.feeItems || this.parseFeeItems(row.feeItemsJson)).forEach(name => {
|
||||||
|
if (name) names.add(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return [...names];
|
||||||
|
},
|
||||||
|
internalTableColumns() {
|
||||||
|
const columns = internalColumns.filter(column => column.prop !== 'freightAmount');
|
||||||
|
const settlementIndex = columns.findIndex(column => column.prop === 'settlementAmount');
|
||||||
|
const feeColumns = this.internalFeeItemNames.map((name, index) => ({
|
||||||
|
prop: `feeItem_${index}`,
|
||||||
|
label: name,
|
||||||
|
minWidth: 120,
|
||||||
|
money: true,
|
||||||
|
feeItemName: name,
|
||||||
|
}));
|
||||||
|
columns.splice(settlementIndex < 0 ? columns.length : settlementIndex, 0, ...feeColumns);
|
||||||
|
return columns;
|
||||||
|
},
|
||||||
externalColumns() {
|
externalColumns() {
|
||||||
return this.form.reconciliationMode === 'cargo'
|
const columns = [
|
||||||
|
...(this.form.reconciliationMode === 'cargo'
|
||||||
? externalCargoColumns
|
? externalCargoColumns
|
||||||
: externalVehicleColumns;
|
: externalVehicleColumns),
|
||||||
|
];
|
||||||
|
const settlementIndex = columns.findIndex(column => column.prop === 'settlementAmount');
|
||||||
|
const feeColumns = this.externalFeeItemNames.map((name, index) => ({
|
||||||
|
prop: `externalFeeItem_${index}`,
|
||||||
|
label: name,
|
||||||
|
minWidth: 120,
|
||||||
|
money: true,
|
||||||
|
feeItemName: name,
|
||||||
|
}));
|
||||||
|
columns.splice(settlementIndex < 0 ? columns.length : settlementIndex, 0, ...feeColumns);
|
||||||
|
return columns;
|
||||||
|
},
|
||||||
|
externalFeeItemNames() {
|
||||||
|
const names = new Set();
|
||||||
|
this.externalDetails.forEach(row => {
|
||||||
|
Object.keys(row.feeItems || this.parseFeeItems(row.feeItemsJson)).forEach(name => {
|
||||||
|
if (name) names.add(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return [...names];
|
||||||
},
|
},
|
||||||
duplicateRows() {
|
duplicateRows() {
|
||||||
return this.externalDetails.filter(
|
return this.externalDetails.filter(
|
||||||
@@ -466,17 +597,15 @@ export default {
|
|||||||
visibleExternalRows() {
|
visibleExternalRows() {
|
||||||
return this.externalTab === 'duplicate' ? this.duplicateRows : this.externalDetails;
|
return this.externalTab === 'duplicate' ? this.duplicateRows : this.externalDetails;
|
||||||
},
|
},
|
||||||
unmatchedExternalRows() {
|
|
||||||
return this.externalDetails.filter(
|
|
||||||
row => row.matchStatus !== 'matched' && !row.suspectedDuplicate
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
modelValue(value) {
|
modelValue: {
|
||||||
|
immediate: true,
|
||||||
|
handler(value) {
|
||||||
if (value) this.initialize();
|
if (value) this.initialize();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
emptyForm() {
|
emptyForm() {
|
||||||
return {
|
return {
|
||||||
@@ -484,6 +613,7 @@ export default {
|
|||||||
reconciliationMode: 'vehicle',
|
reconciliationMode: 'vehicle',
|
||||||
formalSettlementId: null,
|
formalSettlementId: null,
|
||||||
formalSettlementNo: '',
|
formalSettlementNo: '',
|
||||||
|
customerName: '',
|
||||||
payerName: '',
|
payerName: '',
|
||||||
payeeName: '',
|
payeeName: '',
|
||||||
projectName: '',
|
projectName: '',
|
||||||
@@ -512,6 +642,9 @@ export default {
|
|||||||
this.form = this.emptyForm();
|
this.form = this.emptyForm();
|
||||||
this.internalDetails = [];
|
this.internalDetails = [];
|
||||||
this.externalDetails = [];
|
this.externalDetails = [];
|
||||||
|
this.externalEditing = {};
|
||||||
|
this.externalEditSnapshots = {};
|
||||||
|
this.matchingStarted = false;
|
||||||
this.externalTab = 'all';
|
this.externalTab = 'all';
|
||||||
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
|
this.internalQuery = { documentNo: '', vehicleNo: '', batchNo: '', cargoName: '' };
|
||||||
if (!this.currentId) {
|
if (!this.currentId) {
|
||||||
@@ -530,11 +663,27 @@ export default {
|
|||||||
},
|
},
|
||||||
async loadDetail() {
|
async loadDetail() {
|
||||||
const data = this.unwrapData(await api.getDetail(this.currentId)) || {};
|
const data = this.unwrapData(await api.getDetail(this.currentId)) || {};
|
||||||
this.form = { ...this.emptyForm(), ...data };
|
this.form = {
|
||||||
this.internalDetails =
|
...this.emptyForm(),
|
||||||
data.internalDetails || data.internalBillDetails || data.internals || [];
|
...data,
|
||||||
this.externalDetails =
|
customerName:
|
||||||
data.externalDetails || data.externalBillDetails || data.externals || [];
|
data.customerName ||
|
||||||
|
((data.settlementType || this.settlementType) === 'receivable'
|
||||||
|
? data.payerName
|
||||||
|
: data.payeeName) ||
|
||||||
|
'',
|
||||||
|
};
|
||||||
|
const internalRows = data.internalDetails || data.internalBillDetails || data.internals || [];
|
||||||
|
this.internalDetails = internalRows.map(row => this.normalizeInternalRow(row));
|
||||||
|
const externalRows = data.externalDetails || data.externalBillDetails || data.externals || [];
|
||||||
|
this.externalDetails = externalRows.map((row, index) =>
|
||||||
|
this.normalizeExternalRow(row, index)
|
||||||
|
);
|
||||||
|
this.matchingStarted =
|
||||||
|
this.matchingStarted ||
|
||||||
|
data.matchStatus === 'partial' ||
|
||||||
|
this.internalDetails.some(row => row.matchResult === 'matched') ||
|
||||||
|
this.externalDetails.some(row => row.matchStatus === 'matched');
|
||||||
},
|
},
|
||||||
openFormalDialog() {
|
openFormalDialog() {
|
||||||
if (!this.editable) return;
|
if (!this.editable) return;
|
||||||
@@ -542,6 +691,80 @@ export default {
|
|||||||
this.formalDialog.page.current = 1;
|
this.formalDialog.page.current = 1;
|
||||||
this.loadFormalOptions();
|
this.loadFormalOptions();
|
||||||
},
|
},
|
||||||
|
parseFeeItems(value) {
|
||||||
|
if (!value) return {};
|
||||||
|
if (typeof value === 'object' && !Array.isArray(value)) return value;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeInternalRow(row) {
|
||||||
|
const feeItems =
|
||||||
|
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||||||
|
? row.feeItems
|
||||||
|
: this.parseFeeItems(row.feeItemsJson);
|
||||||
|
return { ...row, feeItems };
|
||||||
|
},
|
||||||
|
normalizeExternalRow(row, index) {
|
||||||
|
const feeItems =
|
||||||
|
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||||||
|
? row.feeItems
|
||||||
|
: this.parseFeeItems(row.feeItemsJson);
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
feeItems,
|
||||||
|
_externalKey: row.id || row.externalLineNo || `external-${index}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mergeInternalRows(rows) {
|
||||||
|
const firstRow = rows[0] || {};
|
||||||
|
const feeItems = {};
|
||||||
|
rows.forEach(row => {
|
||||||
|
Object.entries(row.feeItems || this.parseFeeItems(row.feeItemsJson)).forEach(
|
||||||
|
([name, amount]) => {
|
||||||
|
feeItems[name] = Number(feeItems[name] || 0) + Number(amount || 0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const matchResults = rows.map(row => row.matchResult);
|
||||||
|
const updateResults = rows.map(row => row.updateResult);
|
||||||
|
return {
|
||||||
|
...firstRow,
|
||||||
|
cargoName: this.joinInternalValues(rows, 'cargoName'),
|
||||||
|
cargoType: this.joinInternalValues(rows, 'cargoType'),
|
||||||
|
transportQuantity: this.sumInternalValues(rows, 'transportQuantity'),
|
||||||
|
settlementAmount: this.sumInternalValues(rows, 'settlementAmount'),
|
||||||
|
feeItems,
|
||||||
|
matchedExternalLineNo: this.joinInternalValues(rows, 'matchedExternalLineNo'),
|
||||||
|
matchResult: matchResults.every(value => value === 'matched')
|
||||||
|
? 'matched'
|
||||||
|
: matchResults.some(value => value === 'matched')
|
||||||
|
? 'partial'
|
||||||
|
: 'unmatched',
|
||||||
|
updateResult: updateResults.every(value => value === updateResults[0])
|
||||||
|
? updateResults[0]
|
||||||
|
: 'partial_updated',
|
||||||
|
_sourceRows: rows,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
joinInternalValues(rows, prop) {
|
||||||
|
return [
|
||||||
|
...new Set(
|
||||||
|
rows
|
||||||
|
.map(row => row[prop])
|
||||||
|
.filter(value => value !== null && value !== undefined && value !== '')
|
||||||
|
),
|
||||||
|
].join('、');
|
||||||
|
},
|
||||||
|
sumInternalValues(rows, prop) {
|
||||||
|
return rows.reduce((total, row) => total + Number(row[prop] || 0), 0);
|
||||||
|
},
|
||||||
|
getFeeItemAmount(row, name) {
|
||||||
|
return (row.feeItems || this.parseFeeItems(row.feeItemsJson))[name] ?? 0;
|
||||||
|
},
|
||||||
async loadFormalOptions() {
|
async loadFormalOptions() {
|
||||||
this.formalDialog.loading = true;
|
this.formalDialog.loading = true;
|
||||||
try {
|
try {
|
||||||
@@ -586,16 +809,26 @@ export default {
|
|||||||
...selected,
|
...selected,
|
||||||
formalSettlementId: selected.id,
|
formalSettlementId: selected.id,
|
||||||
formalSettlementNo: selected.formalSettlementNo,
|
formalSettlementNo: selected.formalSettlementNo,
|
||||||
|
customerName:
|
||||||
|
selected.customerName ||
|
||||||
|
((selected.settlementType || this.settlementType) === 'receivable'
|
||||||
|
? selected.payerName
|
||||||
|
: selected.payeeName) ||
|
||||||
|
'',
|
||||||
reconciliationNo: this.form.reconciliationNo,
|
reconciliationNo: this.form.reconciliationNo,
|
||||||
};
|
};
|
||||||
if (formalSettlementChanged) this.externalDetails = [];
|
if (formalSettlementChanged) {
|
||||||
|
this.externalDetails = [];
|
||||||
|
this.matchingStarted = false;
|
||||||
|
}
|
||||||
await this.loadFormalInternalPreview(selected.id);
|
await this.loadFormalInternalPreview(selected.id);
|
||||||
this.formalDialog.visible = false;
|
this.formalDialog.visible = false;
|
||||||
},
|
},
|
||||||
async loadFormalInternalPreview(formalSettlementId) {
|
async loadFormalInternalPreview(formalSettlementId) {
|
||||||
const formal = this.unwrapData(await formalSettlementApi.getDetail(formalSettlementId));
|
const formal = this.unwrapData(await formalSettlementApi.getDetail(formalSettlementId));
|
||||||
const details = formal.details || [];
|
const details = formal.details || [];
|
||||||
const buildInternalRow = (detail, overrides = {}, index = 0) => ({
|
const buildInternalRow = (detail, overrides = {}, index = 0) =>
|
||||||
|
this.normalizeInternalRow({
|
||||||
...detail,
|
...detail,
|
||||||
...overrides,
|
...overrides,
|
||||||
id: null,
|
id: null,
|
||||||
@@ -688,7 +921,7 @@ export default {
|
|||||||
await this.loadDetail();
|
await this.loadDetail();
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
this.$message.success('草稿保存成功');
|
this.$message.success('草稿保存成功');
|
||||||
this.$emit('success');
|
this.$emit('success', this.currentId);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -701,6 +934,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
await api.match(this.currentId);
|
await api.match(this.currentId);
|
||||||
await this.loadDetail();
|
await this.loadDetail();
|
||||||
|
this.matchingStarted = true;
|
||||||
this.$message.success('匹配完成');
|
this.$message.success('匹配完成');
|
||||||
} finally {
|
} finally {
|
||||||
this.actionLoading = false;
|
this.actionLoading = false;
|
||||||
@@ -740,7 +974,7 @@ export default {
|
|||||||
await this.loadDetail();
|
await this.loadDetail();
|
||||||
this.$message.success('对账完成');
|
this.$message.success('对账完成');
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
this.$emit('success');
|
this.$emit('success', this.currentId);
|
||||||
} finally {
|
} finally {
|
||||||
this.actionLoading = false;
|
this.actionLoading = false;
|
||||||
}
|
}
|
||||||
@@ -773,7 +1007,9 @@ export default {
|
|||||||
if (result.code !== 200) throw new Error(result.msg || '导入失败');
|
if (result.code !== 200) throw new Error(result.msg || '导入失败');
|
||||||
this.$message.success('外部账单导入成功');
|
this.$message.success('外部账单导入成功');
|
||||||
}
|
}
|
||||||
|
this.matchingStarted = false;
|
||||||
await this.loadDetail();
|
await this.loadDetail();
|
||||||
|
this.matchingStarted = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$message.error(error.message || '外部账单导入失败');
|
this.$message.error(error.message || '外部账单导入失败');
|
||||||
}
|
}
|
||||||
@@ -794,9 +1030,9 @@ export default {
|
|||||||
visible: true,
|
visible: true,
|
||||||
saving: false,
|
saving: false,
|
||||||
reason: '',
|
reason: '',
|
||||||
rows: this.internalDetails
|
rows: (row._sourceRows || this.internalDetails.filter(
|
||||||
.filter(item => item.formalSettlementDetailId === row.formalSettlementDetailId)
|
item => item.formalSettlementDetailId === row.formalSettlementDetailId
|
||||||
.map(item => ({ ...item })),
|
)).map(item => ({ ...item })),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async saveAdjust() {
|
async saveAdjust() {
|
||||||
@@ -817,26 +1053,85 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async handleUnmatch(row) {
|
async handleUnmatch(row) {
|
||||||
await api.unmatch(row.id);
|
const matchedRows = (row._sourceRows || [row]).filter(item => item.matchResult === 'matched');
|
||||||
|
for (const item of matchedRows) await api.unmatch(item.id);
|
||||||
await this.loadDetail();
|
await this.loadDetail();
|
||||||
},
|
},
|
||||||
openManualMatch(row) {
|
internalRowClassName({ row }) {
|
||||||
this.manualDialog = { visible: true, internal: row, selected: null };
|
return this.matchingStarted && row.matchResult !== 'matched'
|
||||||
|
? 'reconciliation-editor__row--unmatched'
|
||||||
|
: '';
|
||||||
},
|
},
|
||||||
openManualMatchByExternal(row) {
|
externalRowClassName({ row }) {
|
||||||
this.manualDialog = { visible: true, internal: null, selected: row };
|
return this.matchingStarted && row.matchStatus !== 'matched'
|
||||||
|
? 'reconciliation-editor__row--unmatched'
|
||||||
|
: '';
|
||||||
},
|
},
|
||||||
async confirmManualMatch() {
|
externalRowKey(row) {
|
||||||
if (!this.manualDialog.selected) return this.$message.warning('请选择外部账单明细');
|
return String(row.id || row.externalLineNo || row._externalKey || '');
|
||||||
if (!this.manualDialog.internal) return this.$message.warning('请从内部账单行发起人工匹配');
|
},
|
||||||
await api.manualMatch({
|
isExternalRowEditing(row) {
|
||||||
reconciliationId: this.currentId,
|
return Boolean(this.externalEditing[this.externalRowKey(row)]);
|
||||||
internalId: this.manualDialog.internal.id,
|
},
|
||||||
externalId: this.manualDialog.selected.id,
|
isExternalEditableColumn(prop) {
|
||||||
});
|
return !['externalLineNo', 'matchStatus'].includes(prop);
|
||||||
this.manualDialog.visible = false;
|
},
|
||||||
await this.loadDetail();
|
isExternalDateColumn(prop) {
|
||||||
this.$message.success('人工匹配成功');
|
return ['actualDepartureTime', 'actualCompletionTime'].includes(prop);
|
||||||
|
},
|
||||||
|
isExternalNumberColumn(prop) {
|
||||||
|
return ['transportQuantity', 'mileage', 'unitPrice', 'freightAmount', 'settlementAmount'].includes(prop);
|
||||||
|
},
|
||||||
|
openExternalAdjust(row) {
|
||||||
|
const key = this.externalRowKey(row);
|
||||||
|
this.externalEditSnapshots = {
|
||||||
|
...this.externalEditSnapshots,
|
||||||
|
[key]: { ...row },
|
||||||
|
};
|
||||||
|
this.externalEditing = { ...this.externalEditing, [key]: true };
|
||||||
|
},
|
||||||
|
cancelExternalAdjust(row) {
|
||||||
|
const key = this.externalRowKey(row);
|
||||||
|
const snapshot = this.externalEditSnapshots[key];
|
||||||
|
if (snapshot) Object.assign(row, snapshot);
|
||||||
|
const editing = { ...this.externalEditing };
|
||||||
|
const snapshots = { ...this.externalEditSnapshots };
|
||||||
|
delete editing[key];
|
||||||
|
delete snapshots[key];
|
||||||
|
this.externalEditing = editing;
|
||||||
|
this.externalEditSnapshots = snapshots;
|
||||||
|
},
|
||||||
|
finishExternalAdjust(row) {
|
||||||
|
row.feeItemsJson = JSON.stringify(row.feeItems || {});
|
||||||
|
const key = this.externalRowKey(row);
|
||||||
|
const editing = { ...this.externalEditing };
|
||||||
|
const snapshots = { ...this.externalEditSnapshots };
|
||||||
|
delete editing[key];
|
||||||
|
delete snapshots[key];
|
||||||
|
this.externalEditing = editing;
|
||||||
|
this.externalEditSnapshots = snapshots;
|
||||||
|
this.refreshExternalStats();
|
||||||
|
},
|
||||||
|
refreshExternalStats() {
|
||||||
|
const externalQuantity = this.externalDetails.reduce(
|
||||||
|
(total, item) => total + Number(item.transportQuantity || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const externalAmount = this.externalDetails.reduce(
|
||||||
|
(total, item) => total + Number(item.settlementAmount || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const matchedCount = this.externalDetails.filter(item => item.matchStatus === 'matched').length;
|
||||||
|
this.form = {
|
||||||
|
...this.form,
|
||||||
|
externalBillCount: this.externalDetails.length,
|
||||||
|
externalQuantity,
|
||||||
|
externalAmount,
|
||||||
|
differenceQuantity: Math.abs(Number(this.form.internalQuantity || 0) - externalQuantity),
|
||||||
|
differenceAmount: Math.abs(Number(this.form.internalAmount || 0) - externalAmount),
|
||||||
|
matchedCount,
|
||||||
|
unmatchedCount: Math.max(Number(this.form.internalBillCount || 0), this.externalDetails.length) - matchedCount,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
matchName(value) {
|
matchName(value) {
|
||||||
return (
|
return (
|
||||||
@@ -863,6 +1158,7 @@ export default {
|
|||||||
updated: '已更新',
|
updated: '已更新',
|
||||||
skipped_multi_cargo: '跳过更新',
|
skipped_multi_cargo: '跳过更新',
|
||||||
manually_adjusted: '手工调整',
|
manually_adjusted: '手工调整',
|
||||||
|
partial_updated: '部分更新',
|
||||||
not_updated: '未更新',
|
not_updated: '未更新',
|
||||||
}[value] ||
|
}[value] ||
|
||||||
value ||
|
value ||
|
||||||
@@ -872,7 +1168,7 @@ export default {
|
|||||||
updateTagType(value) {
|
updateTagType(value) {
|
||||||
return value === 'updated' || value === 'manually_adjusted'
|
return value === 'updated' || value === 'manually_adjusted'
|
||||||
? 'success'
|
? 'success'
|
||||||
: value === 'skipped_multi_cargo'
|
: value === 'skipped_multi_cargo' || value === 'partial_updated'
|
||||||
? 'warning'
|
? 'warning'
|
||||||
: 'info';
|
: 'info';
|
||||||
},
|
},
|
||||||
@@ -956,11 +1252,50 @@ export default {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
.reconciliation-editor__filter-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.reconciliation-editor__filter-label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #606266;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.reconciliation-editor__filter-item :deep(.el-input) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
.reconciliation-editor__pagination {
|
.reconciliation-editor__pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
.reconciliation-editor-shell--page {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.reconciliation-editor--page {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.reconciliation-editor__page-actions {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 20;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 230px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-height: 64px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid #eff1f7;
|
||||||
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.reconciliation-editor__page-actions .el-button + .el-button {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
.reconciliation-editor__help {
|
.reconciliation-editor__help {
|
||||||
color: #606266;
|
color: #606266;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
@@ -980,6 +1315,20 @@ export default {
|
|||||||
.reconciliation-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
.reconciliation-editor :deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
.reconciliation-editor :deep(.el-table__body tr.reconciliation-editor__row--unmatched > td.el-table__cell) {
|
||||||
|
color: #f56c6c;
|
||||||
|
background: #fff1f0 !important;
|
||||||
|
}
|
||||||
|
.reconciliation-editor
|
||||||
|
:deep(.el-table__body tr.reconciliation-editor__row--unmatched > td.el-table__cell .cell) {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
.reconciliation-editor
|
||||||
|
:deep(.el-table__body tr.reconciliation-editor__row--unmatched .status-text) {
|
||||||
|
color: #f56c6c;
|
||||||
|
background: #fde2e2;
|
||||||
|
border-color: #fbc4c4;
|
||||||
|
}
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.reconciliation-editor__stats {
|
.reconciliation-editor__stats {
|
||||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||||
|
|||||||
@@ -104,8 +104,6 @@ export default {
|
|||||||
},
|
},
|
||||||
rowActions(row) {
|
rowActions(row) {
|
||||||
const actions = [{ type: 'view', label: '查看' }];
|
const actions = [{ type: 'view', label: '查看' }];
|
||||||
if (this.hasPermission('transport_reconciliation_edit') && this.isEditable(row))
|
|
||||||
actions.push({ type: 'edit', label: '编辑' });
|
|
||||||
if (this.hasPermission('transport_reconciliation_delete') && this.isEditable(row))
|
if (this.hasPermission('transport_reconciliation_delete') && this.isEditable(row))
|
||||||
actions.push({ type: 'delete', label: '删除', danger: true });
|
actions.push({ type: 'delete', label: '删除', danger: true });
|
||||||
if (this.hasPermission('transport_reconciliation_complete') && this.isEditable(row))
|
if (this.hasPermission('transport_reconciliation_complete') && this.isEditable(row))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<basic-container class="formal-settlement-form-page">
|
<basic-container class="formal-settlement-form-page">
|
||||||
<div class="formal-settlement-form-page__title">{{ pageTitle }}</div>
|
<div class="formal-settlement-form-page__title">{{ pageTitle }}</div>
|
||||||
|
<!-- 新增来源明细可直接调整:计费试算走独立接口,调整结果先暂存,保存正式结算单时再落库。 -->
|
||||||
<formal-settlement-editor
|
<formal-settlement-editor
|
||||||
v-model="editorVisible"
|
v-model="editorVisible"
|
||||||
page-mode
|
page-mode
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<basic-container class="pre-settlement-form-page">
|
<basic-container class="pre-settlement-form-page">
|
||||||
<div class="pre-settlement-form-page__title">{{ pageTitle }}</div>
|
<div class="pre-settlement-form-page__title">{{ pageTitle }}</div>
|
||||||
|
<!-- 新增来源明细可直接调整:计费试算走独立接口,调整结果先暂存,保存预结算单时再落库。 -->
|
||||||
<pre-settlement-editor
|
<pre-settlement-editor
|
||||||
v-model="editorVisible"
|
v-model="editorVisible"
|
||||||
page-mode
|
page-mode
|
||||||
:record-id="recordId"
|
:record-id="recordId"
|
||||||
:initial-data="transferPayload"
|
:initial-data="transferPayload"
|
||||||
:defer-save="Boolean(transferPayload)"
|
|
||||||
@success="handleSuccess"
|
@success="handleSuccess"
|
||||||
/>
|
/>
|
||||||
</basic-container>
|
</basic-container>
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="transport-reconciliation-form-page">
|
||||||
|
<div class="transport-reconciliation-form-page__title">{{ pageTitle }}</div>
|
||||||
|
<transport-reconciliation-editor
|
||||||
|
v-model="editorVisible"
|
||||||
|
page-mode
|
||||||
|
:record-id="recordId"
|
||||||
|
:settlement-type="settlementType"
|
||||||
|
@success="handleSuccess"
|
||||||
|
/>
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TransportReconciliationEditor from './components/transport-reconciliation-editor.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TransportReconciliationForm',
|
||||||
|
components: { TransportReconciliationEditor },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editorVisible: true,
|
||||||
|
savedRecordId: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
recordId() {
|
||||||
|
return this.$route.query.id || '';
|
||||||
|
},
|
||||||
|
settlementType() {
|
||||||
|
return this.$route.query.settlementType || 'payable';
|
||||||
|
},
|
||||||
|
pageTitle() {
|
||||||
|
return this.recordId || this.savedRecordId ? '编辑运输对账单' : '新增运输对账单';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
editorVisible(value) {
|
||||||
|
if (!value) this.goBack();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.syncTagTitle();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSuccess(recordId) {
|
||||||
|
this.savedRecordId = recordId || this.savedRecordId;
|
||||||
|
this.syncTagTitle();
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.$router.push('/settlement/transport-reconciliation');
|
||||||
|
},
|
||||||
|
syncTagTitle() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$store.commit('SET_TAG', {
|
||||||
|
fullPath: this.$route.fullPath,
|
||||||
|
name: this.pageTitle,
|
||||||
|
});
|
||||||
|
this.$router.$avueRouter.setTitle(this.pageTitle);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.transport-reconciliation-form-page {
|
||||||
|
min-height: 100%;
|
||||||
|
padding-bottom: 72px;
|
||||||
|
background: #f5f6fa;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 4px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
background: #409eff;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transport-reconciliation-form-page.basic-container .basic-container__card > .el-card__body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transport-reconciliation-form-page.basic-container .basic-container__card) {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.avue--collapse) .reconciliation-editor__page-actions {
|
||||||
|
left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.avue-layout--horizontal) .reconciliation-editor__page-actions {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -109,8 +109,8 @@ const emptyQuery = () => ({
|
|||||||
contractNo: '',
|
contractNo: '',
|
||||||
payerName: '',
|
payerName: '',
|
||||||
payeeName: '',
|
payeeName: '',
|
||||||
matchStatus: '',
|
matchStatus: 'all',
|
||||||
reconciliationStatus: '',
|
reconciliationStatus: 'all',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -153,7 +153,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const response = await api.getList(this.page.current, this.page.size, {
|
const response = await api.getList(this.page.current, this.page.size, {
|
||||||
// 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显)
|
// 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显)
|
||||||
...this.normalizeOrganizationSearch({ ...this.query }, 'deptName'),
|
...this.buildSearchParams(),
|
||||||
settlementType: this.settlementType,
|
settlementType: this.settlementType,
|
||||||
});
|
});
|
||||||
const data = response?.data?.data || response?.data || response || {};
|
const data = response?.data?.data || response?.data || response || {};
|
||||||
@@ -183,10 +183,15 @@ export default {
|
|||||||
this.page.size = size;
|
this.page.size = size;
|
||||||
this.loadTable();
|
this.loadTable();
|
||||||
},
|
},
|
||||||
|
buildSearchParams() {
|
||||||
|
const params = this.normalizeOrganizationSearch({ ...this.query }, 'deptName');
|
||||||
|
if (params.matchStatus === 'all') params.matchStatus = '';
|
||||||
|
if (params.reconciliationStatus === 'all') params.reconciliationStatus = '';
|
||||||
|
return params;
|
||||||
|
},
|
||||||
handleAction({ type, row }) {
|
handleAction({ type, row }) {
|
||||||
const map = {
|
const map = {
|
||||||
view: this.openView,
|
view: this.openView,
|
||||||
edit: this.openEdit,
|
|
||||||
delete: this.handleDelete,
|
delete: this.handleDelete,
|
||||||
complete: this.handleComplete,
|
complete: this.handleComplete,
|
||||||
};
|
};
|
||||||
@@ -194,10 +199,10 @@ export default {
|
|||||||
if (fn) fn(row);
|
if (fn) fn(row);
|
||||||
},
|
},
|
||||||
openCreate() {
|
openCreate() {
|
||||||
this.editor = { visible: true, id: null, readonly: false };
|
this.$router.push({
|
||||||
},
|
path: '/settlement/transport-reconciliation/form',
|
||||||
openEdit(row) {
|
query: { mode: 'add', settlementType: this.settlementType, name: '新增运输对账单' },
|
||||||
this.editor = { visible: true, id: row.id, readonly: false };
|
});
|
||||||
},
|
},
|
||||||
openView(row) {
|
openView(row) {
|
||||||
this.editor = { visible: true, id: row.id, readonly: true };
|
this.editor = { visible: true, id: row.id, readonly: true };
|
||||||
@@ -218,7 +223,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async handleExport() {
|
async handleExport() {
|
||||||
const params = {
|
const params = {
|
||||||
...this.normalizeOrganizationSearch({ ...this.query }, 'deptName'),
|
...this.buildSearchParams(),
|
||||||
settlementType: this.settlementType,
|
settlementType: this.settlementType,
|
||||||
};
|
};
|
||||||
if (this.selection.length) params.ids = this.selection.map(item => item.id).join(',');
|
if (this.selection.length) params.ids = this.selection.map(item => item.id).join(',');
|
||||||
|
|||||||
@@ -476,7 +476,7 @@
|
|||||||
<span>{{ row.reviewStatus }}</span>
|
<span>{{ row.reviewStatus }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" min-width="135" align="center">
|
<el-table-column label="操作" width="135" fixed="right" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
|
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
|
||||||
详情
|
详情
|
||||||
@@ -513,7 +513,13 @@
|
|||||||
<span>{{ row.branchName || archiveForm.deptName || '-' }}</span>
|
<span>{{ row.branchName || archiveForm.deptName || '-' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="130" align="center" v-if="!readonly">
|
<el-table-column
|
||||||
|
label="操作"
|
||||||
|
width="130"
|
||||||
|
fixed="right"
|
||||||
|
align="center"
|
||||||
|
v-if="!readonly"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link type="primary" @click="openContact(row.__raw, row.__rawIndex)">
|
<el-link type="primary" @click="openContact(row.__raw, row.__rawIndex)">
|
||||||
修改
|
修改
|
||||||
@@ -542,7 +548,13 @@
|
|||||||
<el-table-column label="收款账号" prop="bankAccount" min-width="180" />
|
<el-table-column label="收款账号" prop="bankAccount" min-width="180" />
|
||||||
<el-table-column label="开户行" prop="bankName" min-width="180" />
|
<el-table-column label="开户行" prop="bankName" min-width="180" />
|
||||||
<el-table-column label="备注" prop="remark" min-width="180" />
|
<el-table-column label="备注" prop="remark" min-width="180" />
|
||||||
<el-table-column label="操作" width="130" align="center" v-if="!readonly">
|
<el-table-column
|
||||||
|
label="操作"
|
||||||
|
width="130"
|
||||||
|
fixed="right"
|
||||||
|
align="center"
|
||||||
|
v-if="!readonly"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link type="primary" @click="openReceipt(row.__raw, row.__rawIndex)">
|
<el-link type="primary" @click="openReceipt(row.__raw, row.__rawIndex)">
|
||||||
修改
|
修改
|
||||||
@@ -578,7 +590,13 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="邮箱" prop="email" min-width="180" />
|
<el-table-column label="邮箱" prop="email" min-width="180" />
|
||||||
<el-table-column label="操作" width="130" align="center" v-if="!readonly">
|
<el-table-column
|
||||||
|
label="操作"
|
||||||
|
width="130"
|
||||||
|
fixed="right"
|
||||||
|
align="center"
|
||||||
|
v-if="!readonly"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link type="primary" @click="openInvoice(row.__raw, row.__rawIndex)">
|
<el-link type="primary" @click="openInvoice(row.__raw, row.__rawIndex)">
|
||||||
修改
|
修改
|
||||||
@@ -1004,13 +1022,15 @@
|
|||||||
<el-input v-model="invoiceForm.registeredPhone" maxlength="20" />
|
<el-input v-model="invoiceForm.registeredPhone" maxlength="20" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="注册地址" prop="registeredRegionName">
|
<el-form-item label="注册地址" prop="registeredRegionName">
|
||||||
<el-input
|
<el-cascader
|
||||||
v-model="invoiceForm.registeredRegionName"
|
ref="invoiceRegisteredRegionCascader"
|
||||||
readonly
|
v-model="invoiceForm.registeredRegionPath"
|
||||||
maxlength="100"
|
:options="regionOptions"
|
||||||
placeholder="点击后弹出地图组件"
|
:props="regionCascaderProps"
|
||||||
suffix-icon="el-icon-location"
|
:placeholder="invoiceForm.registeredRegionName || '请选择省市区'"
|
||||||
@click="handleInvoiceRegisteredMapPick"
|
clearable
|
||||||
|
filterable
|
||||||
|
@change="handleInvoiceRegisteredRegionChange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="详细地址" prop="registeredDetailAddress">
|
<el-form-item label="详细地址" prop="registeredDetailAddress">
|
||||||
@@ -1023,11 +1043,9 @@
|
|||||||
<el-input
|
<el-input
|
||||||
ref="invoiceDetailInput"
|
ref="invoiceDetailInput"
|
||||||
v-model="invoiceForm.registeredDetailAddress"
|
v-model="invoiceForm.registeredDetailAddress"
|
||||||
readonly
|
|
||||||
maxlength="255"
|
maxlength="255"
|
||||||
placeholder="点击后弹出地图组件"
|
placeholder="请输入详细地址"
|
||||||
suffix-icon="el-icon-location"
|
@input="checkOverflow('invoiceDetailInput', 'invoiceDetailOverflow')"
|
||||||
@click="handleInvoiceRegisteredMapPick"
|
|
||||||
/>
|
/>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -1058,26 +1076,21 @@
|
|||||||
<el-form-item label="收件人姓名" prop="receiverName">
|
<el-form-item label="收件人姓名" prop="receiverName">
|
||||||
<el-input v-model="invoiceForm.receiverName" maxlength="30" />
|
<el-input v-model="invoiceForm.receiverName" maxlength="30" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="收件人地址" prop="receiverRegionName">
|
<el-form-item label="收件人地址" prop="receiverAddress">
|
||||||
<div class="invoice-form__address">
|
<el-tooltip
|
||||||
|
:content="invoiceForm.receiverAddress"
|
||||||
|
placement="top"
|
||||||
|
:disabled="!invoiceReceiverOverflow"
|
||||||
|
style="display: block; width: 100%"
|
||||||
|
>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="invoiceForm.receiverRegionName"
|
ref="invoiceReceiverInput"
|
||||||
readonly
|
v-model="invoiceForm.receiverAddress"
|
||||||
maxlength="100"
|
|
||||||
placeholder="点击后弹出地图组件"
|
|
||||||
@click="handleInvoiceReceiverMapPick"
|
|
||||||
/>
|
|
||||||
<el-input
|
|
||||||
v-model="invoiceForm.receiverDetailAddress"
|
|
||||||
maxlength="255"
|
maxlength="255"
|
||||||
placeholder="请输入详细地址"
|
placeholder="请输入收件人地址"
|
||||||
><template #suffix
|
@input="checkOverflow('invoiceReceiverInput', 'invoiceReceiverOverflow')"
|
||||||
><el-icon
|
/>
|
||||||
class="invoice-form__address-location"
|
</el-tooltip>
|
||||||
@click="handleInvoiceReceiverMapPick"
|
|
||||||
><Location /></el-icon></template
|
|
||||||
></el-input>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -1415,7 +1428,7 @@ import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
|||||||
import { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
import { getUploadHeaders } from '@/utils/upload';
|
import { getUploadHeaders } from '@/utils/upload';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
import { ArrowRight, Location } from '@element-plus/icons-vue';
|
import { ArrowRight } from '@element-plus/icons-vue';
|
||||||
import { ElImageViewer, ElLoading } from 'element-plus';
|
import { ElImageViewer, ElLoading } from 'element-plus';
|
||||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||||
import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core';
|
import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core';
|
||||||
@@ -1445,7 +1458,6 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
SectionCard,
|
SectionCard,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Location,
|
|
||||||
ElImageViewer,
|
ElImageViewer,
|
||||||
OpenFileViewer,
|
OpenFileViewer,
|
||||||
PdfPreview,
|
PdfPreview,
|
||||||
@@ -1588,6 +1600,7 @@ export default {
|
|||||||
invoiceForm: this.emptyInvoice(),
|
invoiceForm: this.emptyInvoice(),
|
||||||
registeredDetailOverflow: false,
|
registeredDetailOverflow: false,
|
||||||
invoiceDetailOverflow: false,
|
invoiceDetailOverflow: false,
|
||||||
|
invoiceReceiverOverflow: false,
|
||||||
qualificationFiles: [],
|
qualificationFiles: [],
|
||||||
qualificationUploadFiles: [],
|
qualificationUploadFiles: [],
|
||||||
ocrQualificationUploadFiles: [],
|
ocrQualificationUploadFiles: [],
|
||||||
@@ -1690,15 +1703,13 @@ export default {
|
|||||||
invoiceType: [{ required: true, message: '请选择发票类型', trigger: 'change' }],
|
invoiceType: [{ required: true, message: '请选择发票类型', trigger: 'change' }],
|
||||||
registeredPhone: [{ required: true, message: '请输入注册电话', trigger: 'blur' }],
|
registeredPhone: [{ required: true, message: '请输入注册电话', trigger: 'blur' }],
|
||||||
bankName: [{ required: true, message: '请输入开户行名称', trigger: 'blur' }],
|
bankName: [{ required: true, message: '请输入开户行名称', trigger: 'blur' }],
|
||||||
registeredRegionName: [{ required: true, message: '请选择注册地址', trigger: 'blur' }],
|
registeredRegionName: [{ required: true, message: '请选择注册地址', trigger: 'change' }],
|
||||||
registeredDetailAddress: [
|
registeredDetailAddress: [
|
||||||
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
||||||
{ max: 255, message: '详细地址最多255个字符', trigger: 'blur' },
|
{ max: 255, message: '详细地址最多255个字符', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
bankAccount: [{ required: true, message: '请输入银行账号', trigger: 'blur' }],
|
bankAccount: [{ required: true, message: '请输入银行账号', trigger: 'blur' }],
|
||||||
receiverDetailAddress: [
|
receiverAddress: [{ max: 255, message: '收件人地址最多255个字符', trigger: 'blur' }],
|
||||||
{ max: 255, message: '收件人详细地址最多255个字符', trigger: 'blur' },
|
|
||||||
],
|
|
||||||
email: [{ type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }],
|
email: [{ type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }],
|
||||||
},
|
},
|
||||||
option: {
|
option: {
|
||||||
@@ -2028,6 +2039,9 @@ export default {
|
|||||||
'invoiceForm.registeredDetailAddress'() {
|
'invoiceForm.registeredDetailAddress'() {
|
||||||
this.$nextTick(() => this.checkOverflow('invoiceDetailInput', 'invoiceDetailOverflow'));
|
this.$nextTick(() => this.checkOverflow('invoiceDetailInput', 'invoiceDetailOverflow'));
|
||||||
},
|
},
|
||||||
|
'invoiceForm.receiverAddress'() {
|
||||||
|
this.$nextTick(() => this.checkOverflow('invoiceReceiverInput', 'invoiceReceiverOverflow'));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hasPermission(code) {
|
hasPermission(code) {
|
||||||
@@ -2472,14 +2486,13 @@ export default {
|
|||||||
bankName: '',
|
bankName: '',
|
||||||
registeredPhone: '',
|
registeredPhone: '',
|
||||||
bankAccount: '',
|
bankAccount: '',
|
||||||
|
registeredRegionPath: [],
|
||||||
registeredRegionName: '',
|
registeredRegionName: '',
|
||||||
registeredDetailAddress: '',
|
registeredDetailAddress: '',
|
||||||
registeredAddress: '',
|
registeredAddress: '',
|
||||||
email: '',
|
email: '',
|
||||||
receiverName: '',
|
receiverName: '',
|
||||||
receiverPhone: '',
|
receiverPhone: '',
|
||||||
receiverRegionName: '',
|
|
||||||
receiverDetailAddress: '',
|
|
||||||
receiverAddress: '',
|
receiverAddress: '',
|
||||||
isDefault: 0,
|
isDefault: 0,
|
||||||
};
|
};
|
||||||
@@ -2568,8 +2581,8 @@ export default {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
getRegisteredRegionLabels(value) {
|
getRegisteredRegionLabels(value, refName = 'registeredRegionCascader') {
|
||||||
const nodes = this.$refs.registeredRegionCascader?.getCheckedNodes?.() || [];
|
const nodes = this.$refs[refName]?.getCheckedNodes?.() || [];
|
||||||
if (nodes.length && nodes[0].pathLabels?.length) {
|
if (nodes.length && nodes[0].pathLabels?.length) {
|
||||||
return nodes[0].pathLabels;
|
return nodes[0].pathLabels;
|
||||||
}
|
}
|
||||||
@@ -2588,6 +2601,20 @@ export default {
|
|||||||
this.$refs.archiveForm?.validateField('registeredDetailAddress');
|
this.$refs.archiveForm?.validateField('registeredDetailAddress');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleInvoiceRegisteredRegionChange(value) {
|
||||||
|
if (!value || !value.length) {
|
||||||
|
this.invoiceForm.registeredRegionName = '';
|
||||||
|
this.$refs.invoiceForm?.validateField('registeredRegionName');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.invoiceForm.registeredRegionName = this.getRegisteredRegionLabels(
|
||||||
|
value,
|
||||||
|
'invoiceRegisteredRegionCascader'
|
||||||
|
).join('');
|
||||||
|
this.$refs.invoiceForm?.validateField('registeredRegionName');
|
||||||
|
});
|
||||||
|
},
|
||||||
initBusinessDictionaries() {
|
initBusinessDictionaries() {
|
||||||
this.loadDictOptions('nature_of_client', 'customerNatureOptions', [], options => {
|
this.loadDictOptions('nature_of_client', 'customerNatureOptions', [], options => {
|
||||||
const customerNatureColumn = this.findColumn(this.option.column, 'customerNature');
|
const customerNatureColumn = this.findColumn(this.option.column, 'customerNature');
|
||||||
@@ -2815,21 +2842,22 @@ export default {
|
|||||||
.join('')
|
.join('')
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
// 省市区 + 详细地址为当前录入口径,registeredAddress 仅作为历史数据兜底
|
||||||
formatInvoiceRegisteredAddress(row = {}) {
|
formatInvoiceRegisteredAddress(row = {}) {
|
||||||
return (
|
const address = [row.registeredRegionName, row.registeredDetailAddress]
|
||||||
row.registeredAddress ||
|
.map(item => String(item || '').trim())
|
||||||
[row.registeredRegionName, row.registeredDetailAddress]
|
.filter(Boolean)
|
||||||
.map(item => String(item || '').trim())
|
.join('');
|
||||||
.filter(Boolean)
|
return address || row.registeredAddress || '';
|
||||||
.join('')
|
},
|
||||||
);
|
formatInvoiceReceiverAddress(row = {}) {
|
||||||
},
|
const address = String(row.receiverAddress || '').trim();
|
||||||
formatInvoiceReceiverAddress(row = {}) {
|
if (address) return address;
|
||||||
const address = [row.receiverRegionName, row.receiverDetailAddress]
|
// 历史数据中收件人地址拆分为行政区划 + 详细地址两个字段
|
||||||
|
return [row.receiverRegionName, row.receiverDetailAddress]
|
||||||
.map(item => String(item || '').trim())
|
.map(item => String(item || '').trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('');
|
.join('');
|
||||||
return address || row.receiverAddress || '';
|
|
||||||
},
|
},
|
||||||
normalizeContact(contact = {}) {
|
normalizeContact(contact = {}) {
|
||||||
const contactAddress =
|
const contactAddress =
|
||||||
@@ -2899,28 +2927,6 @@ export default {
|
|||||||
: '可搜索地址或点击地图选点';
|
: '可搜索地址或点击地图选点';
|
||||||
this.contactMapBox = true;
|
this.contactMapBox = true;
|
||||||
},
|
},
|
||||||
handleInvoiceRegisteredMapPick() {
|
|
||||||
this.contactMapTarget = 'invoiceRegistered';
|
|
||||||
this.contactMapKeyword =
|
|
||||||
this.invoiceForm.registeredDetailAddress || this.invoiceForm.registeredRegionName || '';
|
|
||||||
this.contactMapSelected = this.buildContactMapSelection({
|
|
||||||
address: this.invoiceForm.registeredDetailAddress,
|
|
||||||
regionName: this.invoiceForm.registeredRegionName,
|
|
||||||
});
|
|
||||||
this.contactMapStatus = '可搜索地址或点击地图选点';
|
|
||||||
this.contactMapBox = true;
|
|
||||||
},
|
|
||||||
handleInvoiceReceiverMapPick() {
|
|
||||||
this.contactMapTarget = 'invoiceReceiver';
|
|
||||||
this.contactMapKeyword =
|
|
||||||
this.invoiceForm.receiverDetailAddress || this.invoiceForm.receiverRegionName || '';
|
|
||||||
this.contactMapSelected = this.buildContactMapSelection({
|
|
||||||
address: this.invoiceForm.receiverDetailAddress,
|
|
||||||
regionName: this.invoiceForm.receiverRegionName,
|
|
||||||
});
|
|
||||||
this.contactMapStatus = '可搜索地址或点击地图选点';
|
|
||||||
this.contactMapBox = true;
|
|
||||||
},
|
|
||||||
loadAmap() {
|
loadAmap() {
|
||||||
if (window.AMap && window.AMap.Map) {
|
if (window.AMap && window.AMap.Map) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -3062,29 +3068,6 @@ export default {
|
|||||||
this.$message.warning('请先搜索或点击地图完成选点');
|
this.$message.warning('请先搜索或点击地图完成选点');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.contactMapTarget === 'invoiceRegistered') {
|
|
||||||
this.invoiceForm.registeredRegionName =
|
|
||||||
this.contactMapSelected.regionName || this.invoiceForm.registeredRegionName;
|
|
||||||
this.invoiceForm.registeredDetailAddress = this.formatMapDetailAddress(
|
|
||||||
this.contactMapSelected.detailAddress || this.invoiceForm.registeredDetailAddress,
|
|
||||||
this.invoiceForm.registeredRegionName
|
|
||||||
);
|
|
||||||
this.$refs.invoiceForm?.validateField('registeredRegionName');
|
|
||||||
this.contactMapBox = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.contactMapTarget === 'invoiceReceiver') {
|
|
||||||
this.invoiceForm.receiverRegionName =
|
|
||||||
this.contactMapSelected.regionName || this.invoiceForm.receiverRegionName;
|
|
||||||
this.invoiceForm.receiverDetailAddress = this.formatMapDetailAddress(
|
|
||||||
this.contactMapSelected.detailAddress || this.invoiceForm.receiverDetailAddress,
|
|
||||||
this.invoiceForm.receiverRegionName
|
|
||||||
);
|
|
||||||
this.invoiceForm.receiverAddress = this.formatInvoiceReceiverAddress(this.invoiceForm);
|
|
||||||
this.$refs.invoiceForm?.validateField('receiverRegionName');
|
|
||||||
this.contactMapBox = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.contactForm.detailAddress =
|
this.contactForm.detailAddress =
|
||||||
this.contactMapSelected.detailAddress || this.contactForm.detailAddress;
|
this.contactMapSelected.detailAddress || this.contactForm.detailAddress;
|
||||||
this.contactForm.regionName =
|
this.contactForm.regionName =
|
||||||
@@ -3096,14 +3079,6 @@ export default {
|
|||||||
this.$refs.contactForm?.validateField('detailAddress');
|
this.$refs.contactForm?.validateField('detailAddress');
|
||||||
this.contactMapBox = false;
|
this.contactMapBox = false;
|
||||||
},
|
},
|
||||||
formatMapDetailAddress(address, regionName) {
|
|
||||||
const rawAddress = String(address || '').trim();
|
|
||||||
const rawRegionName = String(regionName || '').trim();
|
|
||||||
if (!rawAddress || !rawRegionName) return rawAddress;
|
|
||||||
return rawAddress.startsWith(rawRegionName)
|
|
||||||
? rawAddress.slice(rawRegionName.length).trim()
|
|
||||||
: rawAddress;
|
|
||||||
},
|
|
||||||
buildContactMapSelection({ lng, lat, address, regionName, regionCode }) {
|
buildContactMapSelection({ lng, lat, address, regionName, regionCode }) {
|
||||||
const longitude = this.formatCoordinate(lng);
|
const longitude = this.formatCoordinate(lng);
|
||||||
const latitude = this.formatCoordinate(lat);
|
const latitude = this.formatCoordinate(lat);
|
||||||
@@ -3241,26 +3216,27 @@ export default {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
normalizeInvoice(invoice = {}) {
|
normalizeInvoice(invoice = {}) {
|
||||||
|
// receiverRegionName / receiverDetailAddress 为历史拆分字段,现已合并为单一的收件人地址
|
||||||
|
const { receiverRegionName, receiverDetailAddress, ...rest } = invoice;
|
||||||
const registeredDetailAddress =
|
const registeredDetailAddress =
|
||||||
invoice.registeredDetailAddress ||
|
invoice.registeredDetailAddress ||
|
||||||
(invoice.registeredRegionName ? '' : invoice.registeredAddress) ||
|
(invoice.registeredRegionName ? '' : invoice.registeredAddress) ||
|
||||||
'';
|
'';
|
||||||
const receiverDetailAddress =
|
|
||||||
invoice.receiverDetailAddress ||
|
|
||||||
(invoice.receiverRegionName ? '' : invoice.receiverAddress) ||
|
|
||||||
'';
|
|
||||||
return {
|
return {
|
||||||
...this.emptyInvoice(),
|
...this.emptyInvoice(),
|
||||||
...invoice,
|
...rest,
|
||||||
|
registeredRegionPath: Array.isArray(invoice.registeredRegionPath)
|
||||||
|
? invoice.registeredRegionPath
|
||||||
|
: [],
|
||||||
registeredDetailAddress,
|
registeredDetailAddress,
|
||||||
registeredAddress: this.formatInvoiceRegisteredAddress({
|
registeredAddress: this.formatInvoiceRegisteredAddress({
|
||||||
...invoice,
|
...invoice,
|
||||||
registeredDetailAddress,
|
registeredDetailAddress,
|
||||||
}),
|
}),
|
||||||
receiverDetailAddress,
|
|
||||||
receiverAddress: this.formatInvoiceReceiverAddress({
|
receiverAddress: this.formatInvoiceReceiverAddress({
|
||||||
...invoice,
|
receiverRegionName,
|
||||||
receiverDetailAddress,
|
receiverDetailAddress,
|
||||||
|
receiverAddress: invoice.receiverAddress,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -3269,7 +3245,10 @@ export default {
|
|||||||
this.invoiceForm = row ? this.normalizeInvoice(row) : this.emptyInvoice();
|
this.invoiceForm = row ? this.normalizeInvoice(row) : this.emptyInvoice();
|
||||||
this.invoiceBox = true;
|
this.invoiceBox = true;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$nextTick(() => this.checkOverflow('invoiceDetailInput', 'invoiceDetailOverflow'));
|
this.$nextTick(() => {
|
||||||
|
this.checkOverflow('invoiceDetailInput', 'invoiceDetailOverflow');
|
||||||
|
this.checkOverflow('invoiceReceiverInput', 'invoiceReceiverOverflow');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetInvoice() {
|
resetInvoice() {
|
||||||
@@ -3280,11 +3259,7 @@ export default {
|
|||||||
saveInvoice() {
|
saveInvoice() {
|
||||||
this.$refs.invoiceForm.validate(valid => {
|
this.$refs.invoiceForm.validate(valid => {
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
const invoice = this.normalizeInvoice({
|
const invoice = this.normalizeInvoice(this.invoiceForm);
|
||||||
...this.invoiceForm,
|
|
||||||
registeredAddress: this.formatInvoiceRegisteredAddress(this.invoiceForm),
|
|
||||||
receiverAddress: this.formatInvoiceReceiverAddress(this.invoiceForm),
|
|
||||||
});
|
|
||||||
if (this.invoiceIndex > -1) {
|
if (this.invoiceIndex > -1) {
|
||||||
this.archiveForm.invoices.splice(this.invoiceIndex, 1, invoice);
|
this.archiveForm.invoices.splice(this.invoiceIndex, 1, invoice);
|
||||||
} else {
|
} else {
|
||||||
@@ -3626,15 +3601,6 @@ export default {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
validateQualificationMaterials(
|
|
||||||
files = this.qualificationFiles,
|
|
||||||
customerType = this.archiveForm.customerType
|
|
||||||
) {
|
|
||||||
const missing = this.getMissingQualificationTypes(files, customerType);
|
|
||||||
if (!missing.length) return true;
|
|
||||||
this.$message.warning(`请先上传客商材料:${missing.join('、')}`);
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
downloadAttachments() {
|
downloadAttachments() {
|
||||||
const source = this.selectedQualificationFiles.length
|
const source = this.selectedQualificationFiles.length
|
||||||
? this.selectedQualificationFiles
|
? this.selectedQualificationFiles
|
||||||
@@ -3851,15 +3817,8 @@ export default {
|
|||||||
this.invoiceForm = this.emptyInvoice();
|
this.invoiceForm = this.emptyInvoice();
|
||||||
this.$refs.archiveForm?.clearValidate();
|
this.$refs.archiveForm?.clearValidate();
|
||||||
},
|
},
|
||||||
|
// 客商材料不做提交校验,仅在页面上提示未上传项
|
||||||
validateFormalForApproval(archive) {
|
validateFormalForApproval(archive) {
|
||||||
if (
|
|
||||||
!this.validateQualificationMaterials(
|
|
||||||
this.parseAttachments(archive.qualificationAttachments),
|
|
||||||
archive.customerType
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (archive.accessType !== 'formal') return true;
|
if (archive.accessType !== 'formal') return true;
|
||||||
const hasCompletedScore = (archive.scores || []).some(
|
const hasCompletedScore = (archive.scores || []).some(
|
||||||
item => item.selfStatus === '已完成' || item.reviewStatus === '已完成' || item.finalScore
|
item => item.selfStatus === '已完成' || item.reviewStatus === '已完成' || item.finalScore
|
||||||
@@ -3911,7 +3870,11 @@ export default {
|
|||||||
.map(item => this.normalizeReceipt(item))
|
.map(item => this.normalizeReceipt(item))
|
||||||
.filter(item => item.accountName || item.accountHolderName || item.bankAccount);
|
.filter(item => item.accountName || item.accountHolderName || item.bankAccount);
|
||||||
archive.invoices = (archive.invoices || [])
|
archive.invoices = (archive.invoices || [])
|
||||||
.map(item => this.normalizeInvoice(item))
|
.map(item => {
|
||||||
|
const invoice = this.normalizeInvoice(item);
|
||||||
|
delete invoice.registeredRegionPath;
|
||||||
|
return invoice;
|
||||||
|
})
|
||||||
.filter(item => item.invoiceTitle || item.taxNo || item.bankAccount);
|
.filter(item => item.invoiceTitle || item.taxNo || item.bankAccount);
|
||||||
archive.scores = (archive.scores || []).map(score => {
|
archive.scores = (archive.scores || []).map(score => {
|
||||||
const calculatedScore = this.calculateScore(score);
|
const calculatedScore = this.calculateScore(score);
|
||||||
@@ -5129,26 +5092,6 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-form__address {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.el-input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input:first-child {
|
|
||||||
flex: 0 0 38%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.invoice-form__address-location {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-detail-table {
|
.score-detail-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user