|
|
|
|
@ -159,28 +159,34 @@
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item
|
|
|
|
|
label="发票附件"
|
|
|
|
|
prop="ossId"
|
|
|
|
|
v-if="routeParams.type === 'update' || routeParams.type === 'view' || routeParams.type === 'approval'"
|
|
|
|
|
>
|
|
|
|
|
<FileUpload
|
|
|
|
|
v-model="ossIdString"
|
|
|
|
|
:limit="5"
|
|
|
|
|
:fileSize="20"
|
|
|
|
|
:fileType="['png', 'jpg', 'pdf', 'ofd', 'xml']"
|
|
|
|
|
:isShowTip="true"
|
|
|
|
|
:disabled="!hasInvoiceAttachPer"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</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]" header="开票明细">
|
|
|
|
|
<el-card shadow="never" class="mb-[15px]">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="flex justify-between items-center">
|
|
|
|
|
<span class="font-medium">开票明细</span>
|
|
|
|
|
@ -212,17 +218,22 @@
|
|
|
|
|
<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="请输入开票内容" @change="handleItemChange($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="请输入规格型号" />
|
|
|
|
|
<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="请输入单位" />
|
|
|
|
|
<el-input v-model="row.unitName" placeholder="请输入单位" :disabled="isFormDisabled" />
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="数量" prop="quantity" width="145">
|
|
|
|
|
@ -232,6 +243,7 @@
|
|
|
|
|
:min="0"
|
|
|
|
|
:precision="2"
|
|
|
|
|
controls-position="right"
|
|
|
|
|
:disabled="isFormDisabled"
|
|
|
|
|
@change="calculateItemAmount($index)"
|
|
|
|
|
style="width: 120px"
|
|
|
|
|
/>
|
|
|
|
|
@ -247,6 +259,7 @@
|
|
|
|
|
clearable
|
|
|
|
|
placeholder="税率"
|
|
|
|
|
style="width: 80px"
|
|
|
|
|
:disabled="isFormDisabled"
|
|
|
|
|
@change="calculateItemAmount($index)"
|
|
|
|
|
>
|
|
|
|
|
<el-option v-for="rate in taxRateOptions" :key="rate" :label="`${rate}`" :value="rate" />
|
|
|
|
|
@ -266,6 +279,7 @@
|
|
|
|
|
:min="0"
|
|
|
|
|
:precision="2"
|
|
|
|
|
controls-position="right"
|
|
|
|
|
:disabled="isFormDisabled"
|
|
|
|
|
@change="calculateItemAmountByTotal($index)"
|
|
|
|
|
style="width: 120px"
|
|
|
|
|
/>
|
|
|
|
|
@ -278,6 +292,7 @@
|
|
|
|
|
:min="0"
|
|
|
|
|
:precision="2"
|
|
|
|
|
controls-position="right"
|
|
|
|
|
:disabled="isFormDisabled"
|
|
|
|
|
@change="calculateItemAmountByNoTax($index)"
|
|
|
|
|
style="width: 120px"
|
|
|
|
|
/>
|
|
|
|
|
@ -301,7 +316,7 @@
|
|
|
|
|
<!-- 项目选择弹窗 -->
|
|
|
|
|
|
|
|
|
|
<!-- 项目选择弹窗组件 -->
|
|
|
|
|
<ProjectSelectDialog v-model:visible="projectSelectDialogVisible" projectCategory="1" @project-selected="handleProjectSelected" />
|
|
|
|
|
<ProjectSelectDialog v-model:visible="projectSelectDialogVisible" @project-selected="handleProjectSelected" />
|
|
|
|
|
|
|
|
|
|
<!-- 合同选择弹窗组件 -->
|
|
|
|
|
<ContractSelectDialog v-model:visible="contractSelectDialogVisible" @contract-selected="handleContractSelected" />
|
|
|
|
|
@ -356,7 +371,7 @@ import { ContractPaymentMethodVO } from '@/api/oa/erp/contractPaymentMethod/type
|
|
|
|
|
import { UserVO } from '@/api/system/user/types';
|
|
|
|
|
import ContractSelectDialog from '@/views/oa/components/ContractSelectDialog.vue';
|
|
|
|
|
import FileUpload from '@/components/FileUpload/index.vue';
|
|
|
|
|
import { checkPermi } from '@/utils/permission';
|
|
|
|
|
import { checkPermi, checkRole } from '@/utils/permission';
|
|
|
|
|
import { getContractInfo } from '@/api/oa/erp/contractInfo';
|
|
|
|
|
import { listProjectInfoByContractId } from '@/api/oa/erp/projectInfo';
|
|
|
|
|
|
|
|
|
|
@ -397,6 +412,25 @@ const CONTRACT_FLAG = {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
@ -488,7 +522,7 @@ watch(
|
|
|
|
|
// 使用 ?? 将 null 和 undefined 都统一为 'empty' 字符串
|
|
|
|
|
|
|
|
|
|
// 如果 ossId 从无到有,或者从有到无,或者值改变 (只在初始化完成后,并且值确实发生变化时才执行)
|
|
|
|
|
if (isInitialized.value && form.value.invoiceId) {
|
|
|
|
|
if (isInitialized.value && form.value.invoiceId && !invoiceAttachReadonly.value) {
|
|
|
|
|
const normalizedOld = oldVal ?? 'empty';
|
|
|
|
|
const normalizedNew = newVal ?? 'empty';
|
|
|
|
|
if (normalizedOld !== normalizedNew) {
|
|
|
|
|
@ -529,8 +563,19 @@ const handleUserChange = async (newUserId) => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getContractPaymentMethods = async (contractId) => {
|
|
|
|
|
const res = await getContractPaymentMethodList(contractId);
|
|
|
|
|
contractPaymentMethodVoList.value = res.data;
|
|
|
|
|
if (!contractId) {
|
|
|
|
|
contractPaymentMethodVoList.value = [];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const paymentDescription = computed(() => {
|
|
|
|
|
@ -559,7 +604,8 @@ const filterDisabledDept = (deptList: DeptTreeVO[]) => {
|
|
|
|
|
|
|
|
|
|
// 计算合计金额 - 自动更新 issueAmount 和 issuancePercentage
|
|
|
|
|
const totalInvoiceAmount = computed(() => {
|
|
|
|
|
const total = Number(form.value.erpFinInvoiceDetailList.reduce((sum, item) => sum + (item.totalPrice || 0), 0).toFixed(2));
|
|
|
|
|
const detailList = form.value.erpFinInvoiceDetailList || [];
|
|
|
|
|
const total = Number(detailList.reduce((sum, item) => sum + (item.totalPrice || 0), 0).toFixed(2));
|
|
|
|
|
|
|
|
|
|
// 更新开票金额
|
|
|
|
|
form.value.issueAmount = total;
|
|
|
|
|
@ -648,8 +694,27 @@ onMounted(async () => {
|
|
|
|
|
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);
|
|
|
|
|
form.value.erpFinInvoiceDetailList = res.data.erpFinInvoiceDetailVoList || [];
|
|
|
|
|
form.value.erpFinInvoiceDetailList.forEach((_, index) => calculateItemAmountByTotal(index));
|
|
|
|
|
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 = [];
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const res = await getBaseInfo();
|
|
|
|
|
Object.assign(form.value, res.data);
|
|
|
|
|
@ -786,6 +851,25 @@ 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';
|
|
|
|
|
};
|
|
|
|
|
@ -808,8 +892,10 @@ const calculateTaxAmounts = (item: FinInvoiceDetailVO) => {
|
|
|
|
|
// 通过数量和金额(含税)回算单价
|
|
|
|
|
const calculateItemAmount = (index: number) => {
|
|
|
|
|
const item = form.value.erpFinInvoiceDetailList[index];
|
|
|
|
|
if (item.quantity && item.totalPrice !== undefined && item.totalPrice !== null) {
|
|
|
|
|
item.unitPrice = roundAmount(item.totalPrice / item.quantity);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
@ -831,8 +917,9 @@ const calculateItemAmountByNoTax = (index: number) => {
|
|
|
|
|
}
|
|
|
|
|
const taxRate = Number(item.taxRate || 0);
|
|
|
|
|
item.totalPrice = roundAmount(noTaxAmount * (1 + taxRate / 100)) || 0;
|
|
|
|
|
if (item.quantity && item.quantity > 0) {
|
|
|
|
|
item.unitPrice = roundAmount((item.totalPrice || 0) / item.quantity);
|
|
|
|
|
const qty = toFormNumber(item.quantity);
|
|
|
|
|
if (qty !== undefined && qty > 0) {
|
|
|
|
|
item.unitPrice = roundAmount((item.totalPrice || 0) / qty);
|
|
|
|
|
} else {
|
|
|
|
|
item.unitPrice = undefined;
|
|
|
|
|
}
|
|
|
|
|
@ -903,8 +990,20 @@ const submitCallback = async () => {
|
|
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|