You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1196 lines
41 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

5<template>
<div class="p-3">
<el-card shadow="never" style="margin-top: 0; margin-bottom: 10px">
<!-- <template #header>-->
<!-- <div style="text-align: left; font-weight: bold; font-size: 24px">合同{{ form.contractId ? ' - 修改' : ' - 新增' }}</div>-->
<!-- </template>-->
<!-- 审批按钮组件 -->
<approvalButton
@submitForm="handleSave"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.invoiceId"
:status="form.flowStatus"
:pageType="routeParams.type"
:mode="false"
/>
</el-card>
<el-card shadow="never" class="mb-[15px]" header="开票信息">
<el-form ref="formRef" :model="form" :rules="rules" label-width="130px" status-icon :disabled="isFormDisabled">
<el-row :gutter="24">
<!-- 第一行 -->
<el-col :span="8">
<el-form-item label="发出人员" prop="requestBy">
<el-select v-model="form.requestBy" placeholder="请选择发出人员" clearable filterable class="w-full" @change="handleUserChange">
<el-option v-for="user in userOptions" :key="user.userId" :label="user.nickName" :value="user.userId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="发出部门" prop="requestDeptName">
<el-input v-model="form.requestDeptName" readonly />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="发出日期" prop="issueDate">
<el-date-picker v-model="form.issueDate" type="date" placeholder="请选择发出日期" value-format="YYYY-MM-DD" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<!-- 第二行:项目信息 -->
<el-col :span="8">
<el-form-item label="项目编号" prop="projectCode">
<el-input v-model="form.projectCode" placeholder="请选择项目编号" @click="handleSelectProject" readonly suffix-icon="Search">
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="项目名称" readonly />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合同编号" prop="contractCode">
<el-input
v-if="form.projectId && form.projectId !== '' && form.contractFlag === CONTRACT_FLAG.NO"
v-model="form.contractCode"
placeholder="请选择合同"
readonly
@click="showContractSelectDialog"
suffix-icon="Search"
/>
<el-input v-else v-model="form.contractCode" readonly />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="form.customerName" placeholder="客户名称" readonly />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合同金额" prop="totalPrice">
<el-input v-model="form.totalPrice" placeholder="合同金额" readonly></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="本次开具发票比例" prop="issuancePercentage">
<el-input-number
v-model="form.issuancePercentage"
:precision="2"
:min="1"
:max="100"
placeholder="请输入开具发票比例"
style="width: 210px"
>
</el-input-number>
<span style="margin-left: 5px">%</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="合同付款条款" prop="paymentMethod">
<span class="el-input__wrapper" style="padding: 0 11px; min-height: 32px; display: flex; align-items: center">
{{ paymentDescription }}
</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="项目类型" prop="invoiceCategory">
<el-radio-group v-model="form.invoiceCategory">
<el-radio v-for="dict in invoice_category" :key="dict.value" :value="dict.value">{{ dict.label }} </el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="累计回款金额" prop="returnedMoney">
<el-input-number
v-model="form.returnedMoney"
:precision="2"
:controls="false"
disabled
placeholder="累计回款金额"
style="width: 210px"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="累计回款比例" prop="returnedRate">
<el-input v-model="form.returnedRate" placeholder="累计回款比例" readonly>
<template #append>%</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="提前开票" prop="earlyFlag">
<el-radio-group v-model="form.earlyFlag">
<el-radio
v-for="dict in early_flag"
:key="dict.value"
:value="dict.value"
:disabled="isSpareInvoiceCategory && dict.value === EARLY_FLAG.YES"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="验收日期" prop="acceptanceDate" v-if="form.earlyFlag !== EARLY_FLAG.YES">
<el-date-picker v-model="form.acceptanceDate" type="date" placeholder="请选择验收日期" value-format="YYYY-MM-DD" class="w-full" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="发货日期" prop="deliveryDate" v-if="form.earlyFlag !== EARLY_FLAG.YES">
<el-date-picker v-model="form.deliveryDate" type="date" placeholder="请选择发货日期" value-format="YYYY-MM-DD" class="w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="提前开票原因" prop="earlyReason" v-if="form.earlyFlag === EARLY_FLAG.YES">
<el-input v-model="form.earlyReason" placeholder="请输入提前开票原因" type="textarea" :rows="1" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="发票备注信息" prop="remark">
<el-input v-model="form.remark" placeholder="请输入发票需备注的信息内容" type="textarea" :rows="1" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 发票附件单独表单:避免主表单在审批/待办时整体 disabled 导致无法上传;与权限角色 invoice 审批场景配合 -->
<el-form
v-if="routeParams.type === 'update' || routeParams.type === 'view' || routeParams.type === 'approval'"
:model="form"
label-width="130px"
status-icon
:disabled="invoiceAttachReadonly"
>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="发票附件" prop="ossId">
<FileUpload
v-model="ossIdString"
:limit="5"
:fileSize="20"
:fileType="['png', 'jpg', 'pdf', 'ofd', 'xml']"
:isShowTip="true"
:disabled="invoiceAttachReadonly"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 开票明细 -->
<el-card shadow="never" class="mb-[15px]">
<template #header>
<div class="flex justify-between items-center">
<span class="font-medium">开票明细</span>
<div>
<el-button type="primary" plain size="small" icon="Plus" @click="handleAddItem" :disabled="isFormDisabled"></el-button>
<el-button
type="danger"
plain
size="small"
icon="Delete"
@click="handleDeleteItems"
:disabled="isFormDisabled || selectedItems.length === 0"
>删除
</el-button>
</div>
</div>
</template>
<el-table
:data="form.erpFinInvoiceDetailList"
border
stripe
@selection-change="handleSelectionChange"
max-height="400"
:header-cell-style="{ textAlign: 'center' }"
:cell-style="{ textAlign: 'center' }"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="开票内容" prop="billingItems" min-width="160">
<template #default="{ row, $index }">
<el-input
v-model="row.billingItems"
placeholder="请输入开票内容"
:disabled="isFormDisabled"
@change="handleItemChange($index)"
/>
</template>
</el-table-column>
<el-table-column label="规格型号" prop="specificationModel" min-width="140">
<template #default="{ row }">
<el-input v-model="row.specificationModel" placeholder="请输入规格型号" :disabled="isFormDisabled" />
</template>
</el-table-column>
<el-table-column label="单位" prop="unitName" width="120">
<template #default="{ row }">
<el-input v-model="row.unitName" placeholder="请输入单位" :disabled="isFormDisabled" />
</template>
</el-table-column>
<el-table-column label="数量" prop="quantity" width="145">
<template #default="{ row, $index }">
<el-input-number
v-model="row.quantity"
:min="0"
:precision="2"
controls-position="right"
:disabled="isFormDisabled"
@change="calculateItemAmount($index)"
style="width: 120px"
/>
</template>
</el-table-column>
<el-table-column label="税率(%)" prop="taxRate" width="110">
<template #default="{ row, $index }">
<el-select
v-model="row.taxRate"
filterable
allow-create
default-first-option
clearable
placeholder="税率"
style="width: 80px"
:disabled="isFormDisabled"
@change="calculateItemAmount($index)"
>
<el-option v-for="rate in taxRateOptions" :key="rate" :label="`${rate}`" :value="rate" />
</el-select>
</template>
</el-table-column>
<el-table-column label="单价(含税)" prop="unitPrice" width="100">
<template #default="{ row }">
<span class="text-red-500 font-medium">{{ formatAmount(row.unitPrice) }}</span>
</template>
</el-table-column>
<el-table-column label="金额(含税)" prop="totalPrice" width="145">
<template #default="{ row, $index }">
<el-input-number
v-model="row.totalPrice"
:min="0"
:precision="2"
controls-position="right"
:disabled="isFormDisabled"
@change="calculateItemAmountByTotal($index)"
style="width: 120px"
/>
</template>
</el-table-column>
<el-table-column label="金额(不含税)" prop="totalPriceNoTax" width="150">
<template #default="{ row, $index }">
<el-input-number
v-model="row.totalPriceNoTax"
:min="0"
:precision="2"
controls-position="right"
:disabled="isFormDisabled"
@change="calculateItemAmountByNoTax($index)"
style="width: 120px"
/>
</template>
</el-table-column>
<el-table-column v-if="!isFormDisabled" label="操作" fixed="right" width="80" align="center">
<template #default="{ row, $index }">
<el-button link type="danger" icon="Delete" @click="handleRemoveItem($index, row)" />
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-3 text-sm">
<div class="mr-4">
合计金额(含税)<span class="text-red-500 font-bold text-base">{{ totalInvoiceAmount }}</span>
</div>
</div>
</el-card>
<!-- 项目选择弹窗 -->
<!-- 项目选择弹窗组件 -->
<ProjectSelectDialog v-model:visible="projectSelectDialogVisible" @project-selected="handleProjectSelected" />
<!-- 合同选择弹窗组件 -->
<ContractSelectDialog v-model:visible="contractSelectDialogVisible" @contract-selected="handleContractSelected" />
<!-- 人员选择弹窗 (可根据实际组件调整) -->
<el-dialog v-model="userDialogVisible" title="选择人员" width="600px" append-to-body>
<div class="p-3 text-center text-gray-500">请根据实际项目集成人员选择组件</div>
<template #footer>
<el-button @click="userDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleUserSelected">确定</el-button>
</template>
</el-dialog>
<!-- 提交审批组件 -->
<submitVerify ref="submitVerifyRef" @submit-callback="submitCallback" />
<!-- 审批记录 -->
<approvalRecord ref="approvalRecordRef" />
</div>
</template>
<script setup lang="ts" name="InvoiceInfoEdit">
import api from '@/api/system/user';
import { ref, reactive, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
import { useRoute, useRouter } from 'vue-router';
import ProjectSelectDialog from '@/views/oa/components/ProjectSelectDialog.vue';
import {
getFinInvoiceInfo,
getBaseInfo,
addFinInvoiceInfo,
updateFinInvoiceInfo,
getContractPaymentMethodList,
updateInvoiceAttach,
getMaxReturnedMoneyByContractId
} from '@/api/oa/erp/finInvoiceInfo';
import { FinInvoiceInfoForm, FinInvoiceInfoQuery } from '@/api/oa/erp/finInvoiceInfo/types';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import { FinInvoiceDetailForm, FinInvoiceDetailVO } from '@/api/oa/erp/finInvoiceDetail/types';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import { ContractPaymentMethodVO } from '@/api/oa/erp/contractPaymentMethod/types';
import { UserVO } from '@/api/system/user/types';
import ContractSelectDialog from '@/views/oa/components/ContractSelectDialog.vue';
import FileUpload from '@/components/FileUpload/index.vue';
import { checkPermi, checkRole } from '@/utils/permission';
import { getContractInfo } from '@/api/oa/erp/contractInfo';
import { getProjectInfo, listProjectInfoByContractId } from '@/api/oa/erp/projectInfo';
const userOptions = ref<UserVO[]>([]);
const contractPaymentMethodVoList = ref<ContractPaymentMethodVO[]>([]);
/** 同合同下历史开票单(累计回款比例最大)的累计回款金额 */
const contractBaseReturnedMoney = ref(0);
const route = useRoute();
const router = useRouter();
const routeParams = ref<Record<string, any>>({});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const buttonLoading = ref(false);
const formRef = ref<FormInstance>();
const BUSINESS_STATUS = reactive({
DRAFT: '1', //暂存
WAITING: '2', //审批中
AVAILABLE: '3' //可用
});
const { early_flag, invoice_category } = toRefs<any>(proxy?.useDict('early_flag', 'invoice_category'));
const EARLY_FLAG = reactive({
YES: '1', //是
NO: '0' //否
});
/** 项目类型1实施类 2备件类 */
const INVOICE_CATEGORY = {
IMPLEMENTATION: '1',
SPARE: '2'
};
const isSpareInvoiceCategory = computed(() => form.value.invoiceCategory === INVOICE_CATEGORY.SPARE);
const FLOW_STATUS = reactive({
DRAFT: 'draft',
WAITING: 'waiting'
});
const CONTRACT_FLAG = {
YES: '1',
NO: '2'
};
const hasInvoiceAttachPer = checkPermi(['oa/erp:finInvoiceInfo:invoiceAttach']);
/** 仅允许下载:查看页;或修改页但流程审批中(与主表单锁定一致)。其余按菜单权限 / invoice 审批角色 */
const invoiceAttachReadonly = computed(() => {
if (routeParams.value.type === 'view') {
return true;
}
if (routeParams.value.type === 'update') {
return true;
}
const canInvoiceRoleAttach =
routeParams.value.type === 'approval' &&
form.value.flowStatus === FLOW_STATUS.WAITING &&
checkRole(['invoice']);
if (hasInvoiceAttachPer || canInvoiceRoleAttach) {
return false;
}
return true;
});
const isFormDisabled = computed(() => {
return routeParams.value.type === 'view' || routeParams.value.type === 'approval' || form.value.flowStatus === FLOW_STATUS.WAITING;
});
// 弹窗可见性
const projectSelectDialogVisible = ref(false);
const userDialogVisible = ref(false);
// 表格选中项
const selectedItems = ref<FinInvoiceDetailVO[]>([]);
const toDeletedInvoiceDetailIdList = ref([]);
const taxRateOptions = [0, 3, 9, 13];
const getTodayDateString = () => {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const initFormData: FinInvoiceInfoForm = {
invoiceId: undefined,
invoiceCode: undefined,
earlyFlag: EARLY_FLAG.NO,
invoiceType: undefined,
issueAmount: undefined,
issuancePercentage: undefined,
redInkFlag: undefined,
projectId: undefined,
projectCode: undefined,
projectName: undefined,
managerId: undefined,
acceptanceDate: undefined,
deliveryDate: undefined,
arrivalDate: undefined,
invoiceVersion: undefined,
invoiceCategory: undefined,
contractId: undefined,
totalPrice: undefined,
customerId: undefined,
customerName: undefined,
paymentMethod: undefined,
returnedMoney: undefined,
returnedRate: undefined,
feedingFlag: undefined,
costCompleteFlag: undefined,
saleOrderCreateFlag: undefined,
flowStatus: undefined,
invoiceStatus: undefined,
remark: undefined,
earlyReason: undefined,
issueDate: getTodayDateString(),
erpFinInvoiceDetailList: []
};
const data = reactive<PageData<FinInvoiceInfoForm, FinInvoiceInfoQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: undefined,
pageSize: undefined,
params: {}
},
rules: {
projectCode: [{ required: true, message: '项目不能为空', trigger: 'blur' }],
invoiceCategory: [{ required: true, message: '项目类型不能为空', trigger: 'blur' }],
earlyFlag: [{ required: true, message: '提前开票不能为空', trigger: 'blur' }]
}
});
const { form, rules } = toRefs(data);
watch(
() => form.value.invoiceCategory,
(category) => {
if (category === INVOICE_CATEGORY.SPARE && form.value.earlyFlag === EARLY_FLAG.YES) {
form.value.earlyFlag = EARLY_FLAG.NO;
form.value.earlyReason = undefined;
}
}
);
// 附件ID字符串转换用于FileUpload组件
const ossIdString = computed({
get: () => {
if (form.value.ossId === undefined || form.value.ossId === null) {
return '';
}
return String(form.value.ossId);
},
set: (val: string) => {
form.value.ossId = val || undefined;
}
});
watch(
() => form.value.ossId,
async (newVal, oldVal) => {
// 使用 ?? 将 null 和 undefined 都统一为 'empty' 字符串
// 如果 ossId 从无到有,或者从有到无,或者值改变 (只在初始化完成后,并且值确实发生变化时才执行)
if (isInitialized.value && form.value.invoiceId && !invoiceAttachReadonly.value) {
const normalizedOld = oldVal ?? 'empty';
const normalizedNew = newVal ?? 'empty';
if (normalizedOld !== normalizedNew) {
try {
buttonLoading.value = true;
// 更新数据库
const invoiceAttachForm = {
invoiceId: form.value.invoiceId,
ossId: form.value.ossId
};
await updateInvoiceAttach(invoiceAttachForm);
} finally {
buttonLoading.value = false;
}
}
}
},
{ immediate: false }
);
const getUserList = async () => {
const query = {} as any;
const res = await api.getUserList(query);
userOptions.value = res.data;
};
const handleUserChange = async (newUserId) => {
const userOption = userOptions.value.find((item) => item.userId == newUserId);
if (!userOption) {
form.value.requestDeptName = undefined;
form.value.requestDept = undefined;
form.value.requestByName = undefined;
return;
}
form.value.requestDeptName = userOption.deptName;
form.value.requestDept = userOption.deptId;
form.value.requestByName = userOption.nickName;
};
const getContractPaymentMethods = async (contractId?: string | number) => {
if (!contractId) {
contractPaymentMethodVoList.value = [];
return null;
}
const [pmRes, contractRes] = await Promise.all([
getContractPaymentMethodList(contractId),
getContractInfo(contractId)
]);
contractPaymentMethodVoList.value = pmRes.data ?? [];
const c = contractRes?.data;
if (c?.contractCode) {
form.value.contractCode = c.contractCode;
}
return c;
};
const paymentDescription = computed(() => {
return contractPaymentMethodVoList.value.map((item) => `${item.paymentDescription}`).join('');
});
// 计算合计金额 - 自动更新 issueAmount 和 issuancePercentage
const totalInvoiceAmount = computed(() => {
const detailList = form.value.erpFinInvoiceDetailList || [];
const total = Number(detailList.reduce((sum, item) => sum + (item.totalPrice || 0), 0).toFixed(2));
// 更新开票金额
form.value.issueAmount = total;
// 更新开票比例如果合同金额存在且不为0
if (form.value.totalPrice && form.value.totalPrice > 0) {
form.value.issuancePercentage = Number(((total / form.value.totalPrice) * 100).toFixed(2));
} else {
form.value.issuancePercentage = undefined;
}
return total;
});
// 监听合同金额变化
watch(
() => form.value.totalPrice,
(newTotalPrice) => {
// 当合同金额变化时,重新计算开票比例
if (newTotalPrice && newTotalPrice > 0 && form.value.issueAmount) {
form.value.issuancePercentage = Number(((form.value.issueAmount / newTotalPrice) * 100).toFixed(2));
} else {
form.value.issuancePercentage = undefined;
}
calculateReturnedRate();
}
);
// 监听本次开票合计金额变化,自动重算累计回款金额
watch(
() => form.value.issueAmount,
() => {
syncReturnedMoney();
}
);
watch(
() => form.value.contractId,
(newVal, oldVal) => {
if (newVal !== oldVal && !isFormDisabled.value) {
fetchContractBaseReturnedMoney();
}
}
);
// 计算回款比例
const calculateReturnedRate = () => {
if (form.value.totalPrice && form.value.totalPrice > 0 && form.value.returnedMoney != null) {
form.value.returnedRate = Number(((form.value.returnedMoney / form.value.totalPrice) * 100).toFixed(2));
} else {
form.value.returnedRate = undefined;
}
};
/** 查询同合同下累计回款比例最大的历史累计回款金额 */
const fetchContractBaseReturnedMoney = async () => {
if (isFormDisabled.value) {
return;
}
if (!form.value.contractId) {
contractBaseReturnedMoney.value = 0;
syncReturnedMoney();
return;
}
try {
const res = await getMaxReturnedMoneyByContractId(form.value.contractId, form.value.invoiceId);
contractBaseReturnedMoney.value = Number(res.data ?? 0);
} catch {
contractBaseReturnedMoney.value = 0;
}
syncReturnedMoney();
};
/** 累计回款金额 = 历史最大累计回款金额 + 本次开票合计金额(含税) */
const syncReturnedMoney = () => {
if (isFormDisabled.value) {
return;
}
const currentAmount = Number(form.value.issueAmount ?? 0);
form.value.returnedMoney = Number((contractBaseReturnedMoney.value + currentAmount).toFixed(2));
calculateReturnedRate();
};
const syncManagerIdFromProject = async (projectId?: string | number) => {
if (!projectId) {
form.value.managerId = undefined;
return;
}
const res = await getProjectInfo(projectId);
form.value.managerId = res.data?.managerId;
};
// 新增开票页:从台账带入 contractId 时自动回填项目与合同信息
const initByContractId = async () => {
const contractId = routeParams.value.contractId as string | number | undefined;
if (!contractId) return;
const [contractRes, projectRes] = await Promise.all([getContractInfo(contractId), listProjectInfoByContractId(contractId)]);
const contract = contractRes?.data;
const projectList = (projectRes?.data || []) as any[];
const routeProjectId = routeParams.value.projectId as string | number | undefined;
const matchedProject =
projectList.find((item) => routeProjectId && String(item.projectId) === String(routeProjectId)) || projectList[0];
Object.assign(form.value, {
contractId,
contractCode: contract?.contractCode,
contractName: contract?.contractName,
customerId: contract?.oneCustomerId,
customerName: contract?.oneCustomerName,
totalPrice: contract?.totalPrice,
projectId: matchedProject?.projectId,
projectCode: matchedProject?.projectCode,
projectName: matchedProject?.projectName,
managerId: matchedProject?.managerId,
contractFlag: matchedProject?.contractFlag
});
await getContractPaymentMethods(contractId);
initInvoiceDetailsFromContract((contract as any)?.contractMaterialList || []);
};
const isInitialized = ref(false);
onMounted(async () => {
nextTick(async () => {
await getUserList(); // 初始化用户数据
// 获取路由参数
routeParams.value = route.query;
const id = routeParams.value.id as string | number;
try {
proxy?.$modal.loading('正在加载数据,请稍后...');
if (id && (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval')) {
const res = await getFinInvoiceInfo(id);
Object.assign(form.value, res.data);
const invoiceVo = res.data;
const detailFromApi =
invoiceVo.erpFinInvoiceDetailVoList ?? (invoiceVo as FinInvoiceInfoForm).erpFinInvoiceDetailList;
form.value.erpFinInvoiceDetailList = normalizeInvoiceDetailListFromApi(
Array.isArray(detailFromApi) ? detailFromApi : []
);
// 仅补算后端未落库的不含税金额/税额,避免整表重算把「无数量但有单价」等数据清空(与台账展示一致)
form.value.erpFinInvoiceDetailList.forEach((item) => {
if (
item.totalPrice != null &&
item.totalPriceNoTax == null &&
item.taxRate != null
) {
calculateTaxAmounts(item);
}
});
if (form.value.contractId) {
await getContractPaymentMethods(form.value.contractId);
} else {
contractPaymentMethodVoList.value = [];
}
if (form.value.projectId) {
await syncManagerIdFromProject(form.value.projectId);
}
if (form.value.contractId && !isFormDisabled.value) {
await fetchContractBaseReturnedMoney();
}
} else {
const res = await getBaseInfo();
Object.assign(form.value, res.data);
form.value.earlyFlag = form.value.earlyFlag || EARLY_FLAG.NO;
form.value.issueDate = form.value.issueDate || getTodayDateString();
await initByContractId();
if (!form.value.erpFinInvoiceDetailList?.length) {
handleAddItem();
}
if (form.value.contractId) {
await fetchContractBaseReturnedMoney();
}
}
// 数据加载完成后,设置初始化标志为 true
await nextTick();
isInitialized.value = true;
} finally {
proxy?.$modal.closeLoading();
}
});
});
// onMounted(() => {
// const id = route.params?.id
// if (id) {
// getDetail(Number(id))
// } else {
// // 新增时默认添加一行空明细
// handleAddItem()
// }
// })
// 项目选择
const handleSelectProject = () => {
projectSelectDialogVisible.value = true;
};
// 处理项目选择
const handleProjectSelected = async (result: any) => {
const project = result.project;
const contractId = result.contract?.contractId;
Object.assign(form.value, {
contractId,
contractFlag: project.contractFlag,
projectId: project.projectId,
projectName: project.projectName,
projectCode: project.projectCode,
managerId: project.managerId,
contractCode: result.contract?.contractCode,
contractName: result.contract?.contractName,
customerName: result.contract?.oneCustomerName,
totalPrice: result.contract?.totalPrice
});
if (contractId) {
const contractDetail = await getContractPaymentMethods(contractId);
initInvoiceDetailsFromContract((contractDetail as any)?.contractMaterialList || []);
} else if (isAddMode()) {
form.value.erpFinInvoiceDetailList = [];
contractBaseReturnedMoney.value = 0;
handleAddItem();
syncReturnedMoney();
}
};
// 合同选择弹窗相关
const contractSelectDialogVisible = ref(false);
// 显示合同选择弹窗
const showContractSelectDialog = () => {
contractSelectDialogVisible.value = true;
};
// 处理合同选择
const handleContractSelected = async (contract: any) => {
Object.assign(form.value, {
contractId: contract.contractId,
contractCode: contract.contractCode,
contractName: contract.contractName,
customerName: contract.oneCustomerName,
totalPrice: contract.totalPrice
});
const contractDetail = await getContractPaymentMethods(contract.contractId);
initInvoiceDetailsFromContract((contractDetail as any)?.contractMaterialList || []);
};
// 人员选择
const handleSelectUser = () => {
userDialogVisible.value = true;
};
const handleUserSelected = () => {
// form.userName = '张三'; // 模拟选择
// form.userId = 1;
userDialogVisible.value = false;
};
// 表格操作
const handleAddItem = () => {
form.value.erpFinInvoiceDetailList.push({
invoiceDetailId: undefined,
invoiceId: undefined,
billingItems: '',
specificationModel: '',
unitName: '',
quantity: undefined,
taxRate: undefined,
unitPrice: undefined,
totalPrice: 0,
totalPriceNoTax: 0,
taxPrice: undefined
} as FinInvoiceDetailForm as FinInvoiceDetailVO);
};
const handleRemoveItem = (index: number, row: FinInvoiceDetailVO) => {
form.value.erpFinInvoiceDetailList.splice(index, 1);
if (row.invoiceDetailId) {
toDeletedInvoiceDetailIdList.value.push(row.invoiceDetailId);
}
};
const handleDeleteItems = () => {
if (selectedItems.value.length === 0) return;
const remainingItems = form.value.erpFinInvoiceDetailList.filter((item) => !selectedItems.value.includes(item));
form.value.erpFinInvoiceDetailList = remainingItems;
selectedItems.value.forEach((row) => {
if (row.invoiceDetailId) {
toDeletedInvoiceDetailIdList.value.push(row.invoiceDetailId);
}
});
selectedItems.value = [];
};
const handleSelectionChange = (selection: FinInvoiceDetailVO[]) => {
selectedItems.value = selection;
};
/** 接口 BigDecimal 等可能为数字或字符串,统一为表单可用的 number便于 el-select / el-input-number 正确回显 */
const toFormNumber = (val: unknown): number | undefined => {
if (val === undefined || val === null || val === '') return undefined;
const n = Number(val);
return Number.isFinite(n) ? n : undefined;
};
const normalizeInvoiceDetailListFromApi = (list: FinInvoiceDetailVO[]): FinInvoiceDetailVO[] => {
return list.map((row) => ({
...row,
quantity: toFormNumber(row.quantity),
unitPrice: toFormNumber(row.unitPrice),
totalPrice: toFormNumber(row.totalPrice),
totalPriceNoTax: toFormNumber(row.totalPriceNoTax),
taxRate: toFormNumber(row.taxRate),
taxPrice: toFormNumber(row.taxPrice)
})) as FinInvoiceDetailVO[];
};
const formatAmount = (value?: number) => {
return value !== undefined && value !== null ? Number(value).toFixed(2) : '0.00';
};
const roundAmount = (value?: number) => {
if (value === undefined || value === null || Number.isNaN(Number(value))) {
return undefined;
}
return Number(Number(value).toFixed(2));
};
const calculateTaxAmounts = (item: FinInvoiceDetailVO) => {
const totalPrice = roundAmount(item.totalPrice) || 0;
const taxRate = Number(item.taxRate || 0);
const divisor = 1 + taxRate / 100;
item.totalPriceNoTax = roundAmount(totalPrice / divisor) || 0;
item.taxPrice = roundAmount(totalPrice - (item.totalPriceNoTax || 0)) || 0;
};
const isAddMode = () => {
const type = routeParams.value.type;
return !routeParams.value.id && type !== 'update' && type !== 'view' && type !== 'approval';
};
/** 合同物料 → 开票明细字段映射 */
const mapContractMaterialToInvoiceDetail = (material: Record<string, any>): FinInvoiceDetailVO => {
const quantity = toFormNumber(material.amount) ?? 0;
const unitPrice = toFormNumber(material.includingPrice);
const taxRate = toFormNumber(material.taxRate);
let totalPrice = toFormNumber(material.subtotal);
if ((totalPrice === undefined || totalPrice <= 0) && unitPrice !== undefined && quantity > 0) {
totalPrice = roundAmount(unitPrice * quantity) ?? 0;
}
const detail = {
invoiceDetailId: undefined,
invoiceId: undefined,
billingItems: material.saleMaterialName || material.materialName || material.productName || '',
specificationModel: material.specificationDescription || '',
unitName: material.unitName || '',
quantity,
taxRate,
unitPrice,
totalPrice: totalPrice ?? 0,
totalPriceNoTax: 0,
taxPrice: undefined
} as FinInvoiceDetailVO;
if (detail.totalPrice != null && detail.taxRate != null) {
calculateTaxAmounts(detail);
} else if (detail.totalPrice != null && detail.unitPrice == null && quantity > 0) {
detail.unitPrice = roundAmount(detail.totalPrice / quantity);
}
return detail;
};
/** 新增开票:用合同物料初始化开票明细 */
const initInvoiceDetailsFromContract = (contractMaterialList: Record<string, any>[]) => {
if (!isAddMode()) {
return;
}
const details = (contractMaterialList || []).map(mapContractMaterialToInvoiceDetail);
if (details.length > 0) {
form.value.erpFinInvoiceDetailList = details;
} else {
form.value.erpFinInvoiceDetailList = [];
handleAddItem();
}
};
// 通过数量和金额(含税)回算单价
const calculateItemAmount = (index: number) => {
const item = form.value.erpFinInvoiceDetailList[index];
const qty = toFormNumber(item.quantity);
const total = toFormNumber(item.totalPrice);
if (qty !== undefined && qty > 0 && total !== undefined) {
item.unitPrice = roundAmount(total / qty);
} else {
item.unitPrice = undefined;
}
calculateTaxAmounts(item);
};
const calculateItemAmountByTotal = (index: number) => {
calculateItemAmount(index);
};
const calculateItemAmountByNoTax = (index: number) => {
const item = form.value.erpFinInvoiceDetailList[index];
const noTaxAmount = roundAmount(item.totalPriceNoTax);
if (noTaxAmount === undefined) {
item.totalPrice = 0;
item.unitPrice = undefined;
item.taxPrice = 0;
return;
}
const taxRate = Number(item.taxRate || 0);
item.totalPrice = roundAmount(noTaxAmount * (1 + taxRate / 100)) || 0;
const qty = toFormNumber(item.quantity);
if (qty !== undefined && qty > 0) {
item.unitPrice = roundAmount((item.totalPrice || 0) / qty);
} else {
item.unitPrice = undefined;
}
item.taxPrice = roundAmount((item.totalPrice || 0) - noTaxAmount) || 0;
};
// 明细变更时触发计算
const handleItemChange = (index: number) => {
calculateItemAmount(index);
};
const buildInvoiceDetailError = (): string | null => {
if (!form.value.erpFinInvoiceDetailList.length) {
return '请至少添加一条开票明细';
}
for (let index = 0; index < form.value.erpFinInvoiceDetailList.length; index++) {
const item = form.value.erpFinInvoiceDetailList[index];
const rowNo = index + 1;
if (!item.billingItems) {
return `${rowNo}行开票内容不能为空`;
}
if (!item.unitName) {
return `${rowNo}行单位不能为空`;
}
if (item.taxRate === undefined || item.taxRate === null) {
return `${rowNo}行税率不能为空`;
}
if (!item.quantity || item.quantity <= 0) {
return `${rowNo}行数量必须大于0`;
}
if (!item.totalPrice || item.totalPrice <= 0) {
return `${rowNo}行金额(含税)必须大于0`;
}
}
return null;
};
const normalizeInvoiceForm = (): FinInvoiceInfoForm => {
const invoiceForm: FinInvoiceInfoForm = {
...form.value,
invoiceStatus: getInvoiceStatus(buttonStatus.value),
flowStatus: getFlowStatus(buttonStatus.value),
toDeletedInvoiceDetailIdList: [...toDeletedInvoiceDetailIdList.value]
};
if (invoiceForm.earlyFlag === EARLY_FLAG.YES) {
invoiceForm.acceptanceDate = undefined;
invoiceForm.deliveryDate = undefined;
} else {
invoiceForm.earlyReason = undefined;
}
return invoiceForm;
};
const buttonStatus = ref('draft');
// 审批相关组件引用
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
// 审批记录
const handleApprovalRecord = () => {
approvalRecordRef.value.init(form.value.invoiceId);
};
// 提交回调
const submitCallback = async () => {
await proxy.$tab.closePage(route);
router.go(-1);
};
// 审批本页回调invoice 角色在审批中打开弹窗前校验发票附件必填)
const approvalVerifyOpen = async () => {
const needInvoiceAttach =
routeParams.value.type === 'approval' &&
form.value.flowStatus === FLOW_STATUS.WAITING &&
checkRole(['invoice']);
if (needInvoiceAttach) {
const raw = form.value.ossId;
const hasFile = raw !== undefined && raw !== null && String(raw).trim() !== '';
if (!hasFile) {
ElMessage.warning('请先上传发票附件后再审批');
return;
}
}
await submitVerifyRef.value.openDialog(routeParams.value.taskId);
};
// 保存
const handleSave = async (status: string, mode: boolean) => {
if (!formRef.value) return;
buttonStatus.value = status;
try {
await formRef.value.validate();
} catch {
ElMessage.warning('请先完善必填信息后再提交');
return;
}
const detailError = buildInvoiceDetailError();
if (detailError) {
ElMessage.warning(detailError);
return;
}
if (form.value.invoiceCategory === INVOICE_CATEGORY.SPARE && form.value.earlyFlag === EARLY_FLAG.YES) {
ElMessage.warning('备件类项目不能选择提前开票');
return;
}
if (form.value.returnedRate != null && Number(form.value.returnedRate) > 100) {
ElMessage.warning('累计回款比例不能超过100%');
return;
}
if (form.value.returnedMoney && form.value.totalPrice && form.value.returnedMoney > form.value.totalPrice) {
ElMessage.warning('累计回款金额不能大于合同金额');
return;
}
if (status !== 'draft' && !form.value.managerId) {
ElMessage.warning('项目项目经理不能为空,请重新选择项目');
return;
}
const invoiceForm = normalizeInvoiceForm();
try {
buttonLoading.value = true;
if (form.value.invoiceId) {
await updateFinInvoiceInfo(invoiceForm);
} else {
await addFinInvoiceInfo(invoiceForm);
}
ElMessage.success(status === 'draft' ? '暂存成功' : '提交成功');
goBack();
} finally {
buttonLoading.value = false;
}
};
const getInvoiceStatus = (status: string): string => {
return status === 'draft' ? BUSINESS_STATUS.DRAFT : BUSINESS_STATUS.WAITING;
};
const getFlowStatus = (status: string): string => {
return status === 'draft' ? 'draft' : 'waiting';
};
const goBack = () => {
const obj = {
path: '/fin/finInvoiceInfo',
query: {
t: Date.now(),
pageNum: routeParams.value.pageNum
}
};
proxy?.$tab.closeOpenPage(obj);
};
// 取消返回
const handleCancel = () => {
router.push('/oa/erp/finInvoiceInfo');
};
</script>