1.1.30 分款明细新增加合同信息列显示优化

dev
yinq 2 days ago
parent 3fe3d9ad58
commit d0d0e5050e

@ -59,6 +59,15 @@ export interface FinAccountInstallmentDetailVO {
*/ */
remark: string; remark: string;
/** 合同总价(列表/详情 join erp_contract_info */
contractTotalPrice?: number;
/** 当前节点支付比例 %join erp_contract_payment_method */
contractPaymentPercentage?: number;
/** 当前节点合同约定支付金额join erp_contract_payment_method */
contractPaymentAmount?: number;
} }
export interface FinAccountInstallmentDetailForm extends BaseEntity { export interface FinAccountInstallmentDetailForm extends BaseEntity {
@ -117,11 +126,25 @@ export interface FinAccountInstallmentDetailForm extends BaseEntity {
*/ */
detailAmount?: number; detailAmount?: number;
/**
* installment_status
*/
installmentStatus?: string;
/** /**
* *
*/ */
remark?: string; remark?: string;
/** 仅展示/校验:合同总价(来自 join勿依赖提交 */
contractTotalPrice?: number;
/** 仅展示/校验:节点支付比例 % */
contractPaymentPercentage?: number;
/** 仅展示/校验:节点合同约定支付金额 */
contractPaymentAmount?: number;
} }
export interface FinAccountInstallmentDetailQuery extends PageQuery { export interface FinAccountInstallmentDetailQuery extends PageQuery {

@ -147,12 +147,12 @@
<el-table-column label="项目名称" align="center" prop="projectName" width="150" show-overflow-tooltip /> <el-table-column label="项目名称" align="center" prop="projectName" width="150" show-overflow-tooltip />
<el-table-column label="合同编号" align="center" prop="contractCode" width="120" show-overflow-tooltip /> <el-table-column label="合同编号" align="center" prop="contractCode" width="120" show-overflow-tooltip />
<el-table-column label="合同名称" align="center" prop="contractName" width="150" show-overflow-tooltip /> <el-table-column label="合同名称" align="center" prop="contractName" width="150" show-overflow-tooltip />
<el-table-column label="分款阶段" align="center" prop="stageName" width="100" />
<el-table-column label="分款金额" align="center" width="100"> <el-table-column label="分款金额" align="center" width="100">
<template #default="{ row }"> <template #default="{ row }">
<span class="font-medium">¥{{ formatMoney(row.detailAmount) }}</span> <span class="font-medium">¥{{ formatMoney(row.detailAmount) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="分款阶段" align="center" prop="stageName" width="100" />
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip /> <el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="100" fixed="right"> <el-table-column label="操作" align="center" width="100" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
@ -236,8 +236,8 @@
<ContractSelectDialog v-model:visible="showContractDialog" @contract-selected="handleContractSelected" /> <ContractSelectDialog v-model:visible="showContractDialog" @contract-selected="handleContractSelected" />
<!-- 分款明细表单对话框 --> <!-- 分款明细表单对话框 -->
<el-dialog :title="detailDialog.title" v-model="detailDialog.visible" width="500px" append-to-body> <el-dialog :title="detailDialog.title" v-model="detailDialog.visible" width="560px" append-to-body>
<el-form ref="detailFormRef" :model="detailForm" :rules="detailRules" label-width="100px"> <el-form ref="detailFormRef" :model="detailForm" :rules="detailRules" label-width="120px">
<el-form-item label="项目编号"> <el-form-item label="项目编号">
<el-input v-model="detailForm.projectCode" disabled /> <el-input v-model="detailForm.projectCode" disabled />
</el-form-item> </el-form-item>
@ -250,10 +250,23 @@
<el-form-item label="合同名称"> <el-form-item label="合同名称">
<el-input v-model="detailForm.contractName" disabled /> <el-input v-model="detailForm.contractName" disabled />
</el-form-item> </el-form-item>
<el-form-item label="合同额">
<span class="font-medium text-gray-800">{{ contractTotalPriceDisplay }}</span>
</el-form-item>
<el-form-item label="分款节点" prop="paymentStageId"> <el-form-item label="分款节点" prop="paymentStageId">
<el-select v-model="detailForm.paymentStageId" placeholder="请选择分款节点" style="width: 100%"> <div style="display: flex; flex-flow: row nowrap; align-items: center; gap: 12px; width: 100%; min-width: 0; overflow-x: auto">
<el-option v-for="stage in paymentStageList" :key="stage.paymentStageId" :label="stage.stageName" :value="stage.paymentStageId" /> <el-select v-model="detailForm.paymentStageId" placeholder="请选择分款节点" style="width: 150px; flex-shrink: 0">
</el-select> <el-option v-for="stage in paymentStageList" :key="stage.paymentStageId" :label="stage.stageName" :value="stage.paymentStageId" />
</el-select>
<template v-if="detailForm.paymentStageId">
<span style="white-space: nowrap; flex-shrink: 0">
回款比例 <span>{{ selectedStagePaymentRatioText }}</span>
</span>
<span style="white-space: nowrap; flex-shrink: 0">
金额 <span>¥{{ formatMoneyOrDash(selectedStageContractShareAmount) }}</span>
</span>
</template>
</div>
</el-form-item> </el-form-item>
<el-form-item label="分款金额" prop="detailAmount"> <el-form-item label="分款金额" prop="detailAmount">
<el-input-number <el-input-number
@ -264,7 +277,6 @@
placeholder="请输入分款金额" placeholder="请输入分款金额"
style="width: 100%" style="width: 100%"
controls-position="right" controls-position="right"
@change="handleDetailAmountChange"
/> />
</el-form-item> </el-form-item>
<el-form-item label="备注"> <el-form-item label="备注">
@ -297,10 +309,9 @@
</template> </template>
<script setup name="FinAccountInstallment" lang="ts"> <script setup name="FinAccountInstallment" lang="ts">
import { importFinAccountInstallment, listFinAccountInstallment } from '@/api/oa/erp/finAccountInstallment'; import { listFinAccountInstallment } from '@/api/oa/erp/finAccountInstallment';
import { import {
addFinAccountInstallmentDetail, addFinAccountInstallmentDetail,
delFinAccountInstallmentDetail,
getErpFinAccountInstallmentDetailList, getErpFinAccountInstallmentDetailList,
updateFinAccountInstallmentDetail, updateFinAccountInstallmentDetail,
deleteInstallmentDetailByBo deleteInstallmentDetailByBo
@ -308,10 +319,8 @@ import {
import { FinAccountInstallmentQuery, FinAccountInstallmentVO } from '@/api/oa/erp/finAccountInstallment/types'; import { FinAccountInstallmentQuery, FinAccountInstallmentVO } from '@/api/oa/erp/finAccountInstallment/types';
import { FinAccountInstallmentDetailForm, FinAccountInstallmentDetailVO } from '@/api/oa/erp/finAccountInstallmentDetail/types'; import { FinAccountInstallmentDetailForm, FinAccountInstallmentDetailVO } from '@/api/oa/erp/finAccountInstallmentDetail/types';
import { getErpContractPaymentMethodJoinList } from '@/api/oa/erp/finAccountReceivable'; import { getErpContractPaymentMethodJoinList } from '@/api/oa/erp/finAccountReceivable';
import { PaymentStageVO } from '@/api/oa/base/paymentStage/types';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { DArrowLeft, UploadFilled } from '@element-plus/icons-vue'; import { DArrowLeft } from '@element-plus/icons-vue';
import ProjectSelectDialog from '@/views/oa/components/ProjectSelectDialog.vue'; import ProjectSelectDialog from '@/views/oa/components/ProjectSelectDialog.vue';
import ContractSelectDialog from '@/views/oa/components/ContractSelectDialog.vue'; import ContractSelectDialog from '@/views/oa/components/ContractSelectDialog.vue';
import { globalHeaders } from '@/utils/request'; import { globalHeaders } from '@/utils/request';
@ -366,12 +375,10 @@ const formRemainingAmount = computed(() => {
}); });
// //
const showImportDialog = ref(false);
const uploadRef = ref(); const uploadRef = ref();
const importLoading = ref(false); const importLoading = ref(false);
const importPreviewData = ref<any[]>([]); const importPreviewData = ref<any[]>([]);
const duplicateCount = ref(0); const duplicateCount = ref(0);
const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() });
const importFile = ref<File | null>(null); const importFile = ref<File | null>(null);
// //
@ -379,26 +386,84 @@ const showProjectDialog = ref(false);
const showContractDialog = ref(false); const showContractDialog = ref(false);
const selectedProject = ref<any>(null); const selectedProject = ref<any>(null);
const paymentStageList = ref<PaymentStageVO[]>([]); /** 合同付款节点 join 列表(含支付比例、节点约定金额等) */
/** 新增时查询应收款节点信息列表 */ interface ContractPaymentStageJoinRow {
paymentStageId: string | number;
stageName?: string;
paymentPercentage?: number | string;
paymentAmount?: number | string;
}
const paymentStageList = ref<ContractPaymentStageJoinRow[]>([]);
/** 当前表单关联合同的合同额(总价),用于展示与节点金额推算 */
const detailFormContractTotalPrice = ref<number | undefined>(undefined);
const formatMoney = (money: number | undefined | null) => {
if (money === undefined || money === null) return '0.00';
return money.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
const formatMoneyOrDash = (money: number | undefined | null) => {
if (money === undefined || money === null || Number.isNaN(Number(money))) return '—';
return formatMoney(money);
};
const contractTotalPriceDisplay = computed(() => {
const v = detailFormContractTotalPrice.value;
if (v === undefined || v === null || Number.isNaN(Number(v))) return '—';
return `¥${formatMoney(v)}`;
});
const selectedPaymentStageRow = computed(() => {
const id = detailForm.paymentStageId;
if (id == null || id === '') return null;
return paymentStageList.value.find((s) => String(s.paymentStageId) === String(id)) ?? null;
});
const selectedStagePaymentRatioText = computed(() => {
const row = selectedPaymentStageRow.value;
if (row && row.paymentPercentage !== undefined && row.paymentPercentage !== null && row.paymentPercentage !== '') {
const n = Number(row.paymentPercentage);
if (!Number.isNaN(n)) return `${n}%`;
}
const p = detailForm.contractPaymentPercentage;
if (p !== undefined && p !== null && !Number.isNaN(Number(p))) {
const n = Number(p);
if (!Number.isNaN(n)) return `${n}%`;
}
return '—';
});
/** 当前节点约定回款金额:优先付款节点列表;否则用明细 join 带回的合同节点金额/比例推算 */
const selectedStageContractShareAmount = computed(() => {
const row = selectedPaymentStageRow.value;
if (row) {
const backend = row.paymentAmount !== undefined && row.paymentAmount !== null && row.paymentAmount !== '' ? Number(row.paymentAmount) : NaN;
if (!Number.isNaN(backend)) return backend;
const total = Number(detailFormContractTotalPrice.value);
const pct = Number(row.paymentPercentage);
if (!Number.isNaN(total) && !Number.isNaN(pct)) return (total * pct) / 100;
}
const dAmt = detailForm.contractPaymentAmount;
if (dAmt !== undefined && dAmt !== null && !Number.isNaN(Number(dAmt))) {
return Number(dAmt);
}
const total = Number(detailFormContractTotalPrice.value);
const pct = Number(detailForm.contractPaymentPercentage);
if (!Number.isNaN(total) && !Number.isNaN(pct)) return (total * pct) / 100;
return undefined;
});
/** 新增时查询合同付款节点列表 */
const getPaymentStages = async (contractId) => { const getPaymentStages = async (contractId) => {
paymentStageList.value = []; paymentStageList.value = [];
if (contractId && contractId !== '') { if (contractId && contractId !== '') {
const res = await getErpContractPaymentMethodJoinList({ contractId: contractId }); const res = await getErpContractPaymentMethodJoinList({ contractId: contractId });
console.log(res.data); paymentStageList.value = (res.data || []) as ContractPaymentStageJoinRow[];
paymentStageList.value = res.data;
} }
}; };
// Mock
// const paymentStageList = ref([
// { id: 1, stageName: '', stageOrder: 1 },
// { id: 2, stageName: '', stageOrder: 2 },
// { id: 3, stageName: '', stageOrder: 3 },
// { id: 4, stageName: '', stageOrder: 4 },
// { id: 5, stageName: '', stageOrder: 5 }
// ]);
// //
const detailDialog = reactive({ const detailDialog = reactive({
visible: false, visible: false,
@ -417,19 +482,41 @@ const initDetailForm: FinAccountInstallmentDetailForm = {
contractName: undefined, contractName: undefined,
paymentStageId: undefined, paymentStageId: undefined,
detailAmount: undefined, detailAmount: undefined,
remark: undefined installmentStatus: undefined,
remark: undefined,
contractTotalPrice: undefined,
contractPaymentPercentage: undefined,
contractPaymentAmount: undefined
}; };
const detailForm = reactive<FinAccountInstallmentDetailForm>({ ...initDetailForm }); const detailForm = reactive<FinAccountInstallmentDetailForm>({ ...initDetailForm });
function validateDetailAmountNotExceedNode(_rule: unknown, value: unknown, callback: (e?: Error) => void) {
const max = selectedStageContractShareAmount.value;
if (max == null || Number.isNaN(Number(max))) {
callback();
return;
}
const maxN = Number(max);
const v = Number(value);
if (value === undefined || value === null || value === '' || Number.isNaN(v)) {
callback();
return;
}
const vCents = Math.round(v * 100);
const maxCents = Math.round(maxN * 100);
if (vCents > maxCents) {
callback(new Error(`分款金额不能大于节点约定金额(¥${formatMoney(maxN)}`));
return;
}
callback();
}
const detailRules = { const detailRules = {
paymentStageId: [{ required: true, message: '请选择分款节点', trigger: 'change' }], paymentStageId: [{ required: true, message: '请选择分款节点', trigger: 'change' }],
detailAmount: [{ required: true, message: '请输入分款金额', trigger: 'blur' }] detailAmount: [
}; { required: true, message: '请输入分款金额', trigger: 'blur' },
{ validator: validateDetailAmountNotExceedNode, trigger: ['blur', 'change'] }
// ]
const formatMoney = (money: number | undefined | null) => {
if (money === undefined || money === null) return '0.00';
return money.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}; };
const INSTALLMENT_STATUS = { const INSTALLMENT_STATUS = {
@ -438,26 +525,21 @@ const INSTALLMENT_STATUS = {
COMPLETE: '2' // COMPLETE: '2' //
}; };
// /** 明细列表刷新后,按剩余回款与已分款情况同步左侧选中行及列表中的分款状态 */
const getStatusType = (status: string | undefined) => { const syncSelectedInstallmentStatusWithDetailList = () => {
if (!status) return 'info'; if (!selectedInstallment.value) return;
const map: Record<string, string> = { const newStatus =
'0': 'warning', // - remainingAmount.value === 0
'1': 'primary', // - ? INSTALLMENT_STATUS.COMPLETE
'2': 'success' // - 绿 : allocatedAmount.value > 0
}; ? INSTALLMENT_STATUS.PARTIALLY
return map[status] || 'info'; : INSTALLMENT_STATUS.NOT;
}; selectedInstallment.value.installmentStatus = newStatus;
const id = selectedInstallment.value.accountInstallmentId;
// const index = finAccountInstallmentList.value.findIndex((item) => item.accountInstallmentId === id);
const getStatusText = (status: string | undefined) => { if (index !== -1) {
if (!status) return '未知'; finAccountInstallmentList.value[index].installmentStatus = newStatus;
const map: Record<string, string> = { }
'0': '未分款',
'1': '部分分款',
'2': '已分款完成'
};
return map[status] || '未知';
}; };
/*** 用户导入参数 */ /*** 用户导入参数 */
@ -573,8 +655,6 @@ const handleAddDetail = () => {
// //
const handleProjectSelected = (data: any) => { const handleProjectSelected = (data: any) => {
selectedProject.value = data.project; selectedProject.value = data.project;
console.log(data);
// (contractFlag !== '1') // (contractFlag !== '1')
if (data.project.contractFlag !== '1') { if (data.project.contractFlag !== '1') {
showContractDialog.value = true; showContractDialog.value = true;
@ -593,8 +673,11 @@ const handleContractSelected = (contract: any) => {
} }
}; };
// // syncAddDetailAmountFromSelectedStage
const fillDetailForm = (project: any, contract: any) => { const fillDetailForm = (project: any, contract: any) => {
const totalPriceRaw = contract?.totalPrice;
detailFormContractTotalPrice.value =
totalPriceRaw !== undefined && totalPriceRaw !== null && totalPriceRaw !== '' ? Number(totalPriceRaw) : undefined;
Object.assign(detailForm, { Object.assign(detailForm, {
installmentDetailId: undefined, installmentDetailId: undefined,
accountInstallmentId: selectedInstallment.value?.accountInstallmentId, accountInstallmentId: selectedInstallment.value?.accountInstallmentId,
@ -606,23 +689,45 @@ const fillDetailForm = (project: any, contract: any) => {
contractName: contract?.contractName || '', contractName: contract?.contractName || '',
paymentStageId: undefined, paymentStageId: undefined,
detailAmount: undefined, detailAmount: undefined,
remark: undefined remark: undefined,
contractTotalPrice: undefined,
contractPaymentPercentage: undefined,
contractPaymentAmount: undefined
}); });
detailDialog.title = '新增分款明细'; detailDialog.title = '新增分款明细';
detailDialog.visible = true; detailDialog.visible = true;
}; };
// /** 新增明细:选定分款节点后,分款金额默认取当前节点约定金额 */
const handleEditDetail = (row: FinAccountInstallmentDetailVO) => { const syncAddDetailAmountFromSelectedStage = () => {
if (!detailDialog.visible || detailForm.installmentDetailId) return;
let next: number | undefined;
if (detailForm.paymentStageId != null && detailForm.paymentStageId !== '') {
const raw = selectedStageContractShareAmount.value;
const n = raw != null && !Number.isNaN(Number(raw)) ? Number(raw) : NaN;
if (!Number.isNaN(n) && n > 0) {
const rounded = Math.round(n * 100) / 100;
next = rounded < 0.01 ? 0.01 : rounded;
}
}
detailForm.detailAmount = next;
if (detailFormRef.value) {
nextTick(() => detailFormRef.value?.validateField('detailAmount'));
}
};
// / join getContractInfo
const handleEditDetail = async (row: FinAccountInstallmentDetailVO) => {
Object.assign(detailForm, row); Object.assign(detailForm, row);
detailDialog.title = '编辑分款明细'; detailDialog.title = '编辑分款明细';
detailDialog.visible = true; detailDialog.visible = true;
getPaymentStages(detailForm.contractId); const tp = row.contractTotalPrice;
}; detailFormContractTotalPrice.value =
tp !== undefined && tp !== null && !Number.isNaN(Number(tp)) ? Number(tp) : undefined;
// await getPaymentStages(detailForm.contractId);
const handleDetailAmountChange = () => { nextTick(() => detailFormRef.value?.validateField('detailAmount'));
// computedformRemainingAmount
}; };
// //
@ -635,16 +740,7 @@ const handleDeleteDetail = async (row: FinAccountInstallmentDetailVO) => {
await deleteInstallmentDetailByBo(deleteForm); await deleteInstallmentDetailByBo(deleteForm);
proxy?.$modal.msgSuccess('删除成功'); proxy?.$modal.msgSuccess('删除成功');
await getDetailList(); await getDetailList();
// syncSelectedInstallmentStatusWithDetailList();
if (selectedInstallment.value) {
const newStatus = remainingAmount.value === 0 ? INSTALLMENT_STATUS.COMPLETE : (allocatedAmount.value > 0 ? INSTALLMENT_STATUS.PARTIALLY : INSTALLMENT_STATUS.NOT);
selectedInstallment.value.installmentStatus = newStatus;
//
const index = finAccountInstallmentList.value.findIndex((item) => item.accountInstallmentId === selectedInstallment.value?.accountInstallmentId);
if (index !== -1) {
finAccountInstallmentList.value[index].installmentStatus = newStatus;
}
}
}; };
// //
@ -664,16 +760,7 @@ const submitDetailForm = async () => {
} }
detailDialog.visible = false; detailDialog.visible = false;
await getDetailList(); await getDetailList();
// syncSelectedInstallmentStatusWithDetailList();
if (selectedInstallment.value) {
const newStatus = remainingAmount.value === 0 ? INSTALLMENT_STATUS.COMPLETE : (allocatedAmount.value > 0 ? INSTALLMENT_STATUS.PARTIALLY : INSTALLMENT_STATUS.NOT);
selectedInstallment.value.installmentStatus = newStatus;
//
const index = finAccountInstallmentList.value.findIndex((item) => item.accountInstallmentId === selectedInstallment.value?.accountInstallmentId);
if (index !== -1) {
finAccountInstallmentList.value[index].installmentStatus = newStatus;
}
}
} finally { } finally {
detailButtonLoading.value = false; detailButtonLoading.value = false;
} }
@ -682,7 +769,6 @@ const submitDetailForm = async () => {
// //
const cancelDetail = () => { const cancelDetail = () => {
detailDialog.visible = false; detailDialog.visible = false;
Object.assign(detailForm, initDetailForm);
}; };
// Excel // Excel
@ -720,25 +806,31 @@ const submitImport = async () => {
uploadRef.value?.submit(); uploadRef.value?.submit();
}; };
//
watch(showImportDialog, (val) => {
if (!val) {
importPreviewData.value = [];
importFile.value = null;
uploadRef.value?.clearFiles();
}
});
// //
watch( watch(
() => detailDialog.visible, () => detailDialog.visible,
(val) => { (val) => {
if (!val) { if (!val) {
Object.assign(detailForm, initDetailForm); Object.assign(detailForm, initDetailForm);
detailFormContractTotalPrice.value = undefined;
} }
} }
); );
watch(
() => [
detailDialog.visible,
detailForm.installmentDetailId,
detailForm.paymentStageId,
paymentStageList.value,
detailFormContractTotalPrice.value
],
() => {
syncAddDetailAmountFromSelectedStage();
},
{ flush: 'post' }
);
// //
onMounted(() => { onMounted(() => {
getList(); getList();

Loading…
Cancel
Save