<template>
|
<div class="app-container">
|
<el-form :model="filters" :inline="true">
|
<el-form-item label="发票号码:">
|
<el-input v-model="filters.invoiceNumber" placeholder="请输入发票号码" clearable style="width: 200px;" />
|
</el-form-item>
|
<el-form-item label="客户:">
|
<el-select v-model="filters.customerId" placeholder="请选择客户" clearable style="width: 200px;">
|
<el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="开票日期:">
|
<el-date-picker
|
v-model="filters.dateRange"
|
type="daterange"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
clearable
|
style="width: 240px;"
|
/>
|
</el-form-item>
|
<el-form-item label="状态:">
|
<el-select v-model="filters.status" placeholder="请选择状态" clearable style="width: 150px;">
|
<el-option label="正常" :value="0" />
|
<el-option label="作废" :value="1" />
|
</el-select>
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary" @click="onSearch">搜索</el-button>
|
<el-button @click="resetFilters">重置</el-button>
|
</el-form-item>
|
</el-form>
|
<div class="table_list">
|
<div class="actions">
|
<div></div>
|
<div>
|
<!-- <el-button type="primary" @click="add" icon="Plus">录入发票</el-button> -->
|
<el-button type="success" @click="handleExport" icon="Download">导出</el-button>
|
</div>
|
</div>
|
<PIMTable
|
rowKey="id"
|
v-loading="tableLoading"
|
:column="columns"
|
:tableData="dataList"
|
:page="{
|
current: pagination.currentPage,
|
size: pagination.pageSize,
|
total: pagination.total,
|
}"
|
@pagination="changePage"
|
>
|
<template #amount="{ row }">
|
<span class="text-primary">¥{{ formatMoney(row.amount) }}</span>
|
</template>
|
<template #taxAmount="{ row }">
|
<span class="text-danger">¥{{ formatMoney(row.taxAmount) }}</span>
|
</template>
|
<template #totalAmount="{ row }">
|
<span class="text-success">¥{{ formatMoney(row.totalAmount) }}</span>
|
</template>
|
<template #status="{ row }">
|
<el-tag :type="getStatusType(row.status)" effect="light" round>
|
{{ getStatusLabel(row.status) }}
|
</el-tag>
|
</template>
|
<template #operation="{ row }">
|
<el-button type="primary" link @click="view(row)">查看</el-button>
|
<el-button
|
type="primary"
|
link
|
@click="openFileDialog(row)"
|
v-if="row.accountInvoiceApplicationId"
|
>
|
附件
|
</el-button>
|
<el-button type="warning" link @click="handleCancel(row)" v-if="isNormalStatus(row.status)">作废</el-button>
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
</template>
|
</PIMTable>
|
</div>
|
|
<FormDialog
|
:title="dialogTitle"
|
v-model="dialogVisible"
|
width="800px"
|
:operation-type="isView ? 'detail' : ''"
|
@confirm="submitForm"
|
@cancel="closeDialog"
|
>
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
<el-row v-if="isView" :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="状态">
|
<el-tag :type="getStatusType(form.status)" effect="light" round>
|
{{ getStatusLabel(form.status) }}
|
</el-tag>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="发票号码" prop="invoiceNo">
|
<el-input v-model="form.invoiceNo" placeholder="请输入发票号码" :disabled="isView" />
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="客户" prop="customerId">
|
<el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%;" :disabled="isView">
|
<el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="开票日期" prop="invoiceDate">
|
<el-date-picker
|
v-model="form.invoiceDate"
|
type="date"
|
placeholder="选择日期"
|
value-format="YYYY-MM-DD"
|
style="width: 100%;"
|
:disabled="isView"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="发票类型" prop="invoiceType">
|
<el-select
|
v-model="form.invoiceType"
|
placeholder="请选择发票类型"
|
style="width: 100%;"
|
:disabled="isView"
|
@change="handleInvoiceTypeChange"
|
>
|
<el-option label="增值税专用发票" value="增值税专用发票" />
|
<el-option label="增值税普通发票" value="增值税普通发票" />
|
<el-option label="电子发票" value="电子发票" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="税率" prop="taxRate">
|
<el-select
|
v-model="form.taxRate"
|
placeholder="请选择税率"
|
style="width: 100%;"
|
:disabled="isView"
|
@change="calculateTax"
|
>
|
<el-option
|
v-for="dict in tax_rate"
|
:key="dict.value"
|
:label="dict.label"
|
:value="Number(dict.value)"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20">
|
<el-col :span="8">
|
<el-form-item label="金额(不含税)" prop="amount">
|
<el-input-number
|
v-model="form.amount"
|
:min="0"
|
:precision="2"
|
style="width: 100%;"
|
:disabled="isView"
|
@change="calculateTax"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="8">
|
<el-form-item label="税额">
|
<el-input v-model="form.taxAmount" disabled />
|
</el-form-item>
|
</el-col>
|
<el-col :span="8">
|
<el-form-item label="价税合计">
|
<el-input v-model="form.totalAmount" disabled />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-form-item label="发票内容" prop="content">
|
<el-input v-model="form.content" type="textarea" :rows="3" placeholder="请输入发票内容" :disabled="isView" />
|
</el-form-item>
|
<el-form-item label="备注" prop="remark">
|
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" :disabled="isView" />
|
</el-form-item>
|
</el-form>
|
<template v-if="!isView" #footer>
|
<el-button type="primary" :loading="submitLoading" @click="submitForm">确定</el-button>
|
<el-button @click="closeDialog">取消</el-button>
|
</template>
|
</FormDialog>
|
|
<FileList
|
v-if="fileDialogVisible"
|
v-model:visible="fileDialogVisible"
|
record-type="account_invoice_application"
|
:record-id="currentRecordId"
|
:editable="false"
|
/>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted, getCurrentInstance, defineAsyncComponent } from "vue";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import FormDialog from "@/components/Dialog/FormDialog.vue";
|
import { listCustomer } from "@/api/basicData/customer.js";
|
import {
|
addAccountSalesInvoice,
|
listPageAccountSalesInvoice,
|
cancelAccountSalesInvoice,
|
deleteAccountSalesInvoice,
|
} from "@/api/financialManagement/accountSalesInvoice.js";
|
|
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
|
|
defineOptions({
|
name: "销项发票",
|
});
|
|
const { proxy } = getCurrentInstance();
|
const { tax_rate } = proxy.useDict("tax_rate");
|
|
const filters = reactive({
|
invoiceNumber: "",
|
customerId: "",
|
dateRange: [],
|
status: "",
|
});
|
|
const pagination = reactive({
|
currentPage: 1,
|
pageSize: 10,
|
total: 0,
|
});
|
|
const columns = [
|
{ label: "发票号码", prop: "invoiceNo", width: "140" },
|
{ label: "客户名称", prop: "customerName", width: "180" },
|
{ label: "开票日期", prop: "invoiceDate", width: "120" },
|
{ label: "金额", prop: "amount", dataType: "slot", slot: "amount" },
|
{ label: "税额", prop: "taxAmount", dataType: "slot", slot: "taxAmount" },
|
{ label: "价税合计", prop: "totalAmount", dataType: "slot", slot: "totalAmount" },
|
{ label: "发票类型", prop: "invoiceType", width: "130" },
|
{ label: "状态", prop: "status", dataType: "slot", slot: "status", width: "90", align: "center" },
|
{ label: "操作", prop: "operation", dataType: "slot", slot: "operation", width: "260", fixed: "right" },
|
];
|
|
const dataList = ref([]);
|
const tableLoading = ref(false);
|
const dialogVisible = ref(false);
|
const dialogTitle = ref("");
|
const formRef = ref(null);
|
const isView = ref(false);
|
const submitLoading = ref(false);
|
|
const customerList = ref([]);
|
const fileDialogVisible = ref(false);
|
const currentRecordId = ref(0);
|
|
const openFileDialog = (row) => {
|
if (!row.accountInvoiceApplicationId) {
|
ElMessage.warning("未关联开票申请,无法查看附件");
|
return;
|
}
|
currentRecordId.value = row.accountInvoiceApplicationId;
|
fileDialogVisible.value = true;
|
};
|
|
/** 状态:0正常 1作废 */
|
const STATUS_LABEL_MAP = { 0: "正常", 1: "作废" };
|
const STATUS_TYPE_MAP = { 0: "success", 1: "info" };
|
|
const normalizeStatus = (status) => {
|
if (status === undefined || status === null || status === "") return 0;
|
const num = Number(status);
|
return Number.isNaN(num) ? 0 : num;
|
};
|
|
const isNormalStatus = (status) => normalizeStatus(status) === 0;
|
|
const getStatusLabel = (status) => {
|
const num = normalizeStatus(status);
|
return STATUS_LABEL_MAP[num] ?? "正常";
|
};
|
|
const getStatusType = (status) => {
|
const num = normalizeStatus(status);
|
return STATUS_TYPE_MAP[num] ?? "success";
|
};
|
|
const form = reactive({
|
invoiceNo: "",
|
customerId: "",
|
invoiceDate: "",
|
invoiceType: "增值税专用发票",
|
taxRate: 13,
|
amount: 0,
|
taxAmount: 0,
|
totalAmount: 0,
|
content: "",
|
remark: "",
|
accountInvoiceApplicationId: undefined,
|
storageAttachmentId: undefined,
|
});
|
|
const rules = {
|
invoiceNo: [{ required: true, message: "请输入发票号码", trigger: "blur" }],
|
customerId: [{ required: true, message: "请选择客户", trigger: "change" }],
|
invoiceDate: [{ required: true, message: "请选择开票日期", trigger: "change" }],
|
invoiceType: [{ required: true, message: "请选择发票类型", trigger: "change" }],
|
taxRate: [{ required: true, message: "请选择税率", trigger: "change" }],
|
amount: [{ required: true, message: "请输入金额", trigger: "blur" }],
|
};
|
|
const formatMoney = (value) => {
|
if (value === undefined || value === null) return "0.00";
|
return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
};
|
|
const calculateTax = () => {
|
form.taxAmount = Number((form.amount * form.taxRate / 100).toFixed(2));
|
form.totalAmount = Number((form.amount + form.taxAmount).toFixed(2));
|
};
|
|
const handleInvoiceTypeChange = () => {
|
calculateTax();
|
};
|
|
const normalizeTableRow = (row) => ({
|
...row,
|
invoiceNo: row.invoiceNumber ?? row.invoiceNo,
|
invoiceDate: row.issueDate ?? row.invoiceDate,
|
amount: row.taxExclusivelPrice ?? row.amount,
|
taxAmount: row.taxPrice ?? row.taxAmount,
|
totalAmount: row.taxInclusivePrice ?? row.totalAmount,
|
content: row.invoiceContent ?? row.content,
|
status: normalizeStatus(row.status),
|
});
|
|
const fillFormFromRow = (row) => {
|
Object.assign(form, {
|
invoiceNo: row.invoiceNo ?? row.invoiceNumber ?? "",
|
customerId: row.customerId,
|
invoiceDate: row.invoiceDate ?? row.issueDate ?? "",
|
invoiceType: row.invoiceType ?? "增值税专用发票",
|
taxRate: row.taxRate ?? 13,
|
amount: row.amount ?? row.taxExclusivelPrice ?? 0,
|
taxAmount: row.taxAmount ?? row.taxPrice ?? 0,
|
totalAmount: row.totalAmount ?? row.taxInclusivePrice ?? 0,
|
content: row.content ?? row.invoiceContent ?? "",
|
remark: row.remark ?? "",
|
accountInvoiceApplicationId: row.accountInvoiceApplicationId,
|
storageAttachmentId: row.storageAttachmentId,
|
status: normalizeStatus(row.status),
|
});
|
};
|
|
const buildCancelPayload = (row) => ({
|
id: row.id,
|
accountInvoiceApplicationId: row.accountInvoiceApplicationId,
|
invoiceNumber: row.invoiceNumber ?? row.invoiceNo,
|
taxRate: row.taxRate,
|
invoiceType: row.invoiceType,
|
issueDate: row.issueDate ?? row.invoiceDate,
|
taxExclusivelPrice: row.taxExclusivelPrice ?? row.amount,
|
taxPrice: row.taxPrice ?? row.taxAmount,
|
taxInclusivePrice: row.taxInclusivePrice ?? row.totalAmount,
|
remark: row.remark ?? "",
|
invoiceContent: row.invoiceContent ?? row.content,
|
customerId: row.customerId,
|
storageAttachmentId: row.storageAttachmentId,
|
status: 1,
|
});
|
|
const buildSubmitPayload = () => ({
|
invoiceNumber: form.invoiceNo,
|
customerId: form.customerId,
|
issueDate: form.invoiceDate,
|
invoiceType: form.invoiceType,
|
taxRate: form.taxRate,
|
taxExclusivelPrice: form.amount,
|
taxPrice: form.taxAmount,
|
taxInclusivePrice: form.totalAmount,
|
invoiceContent: form.content,
|
remark: form.remark || "",
|
accountInvoiceApplicationId: form.accountInvoiceApplicationId,
|
storageAttachmentId: form.storageAttachmentId,
|
});
|
|
const getCustomerList = () => {
|
listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
|
if (res.code === 200) {
|
customerList.value = res.data?.records || [];
|
}
|
});
|
};
|
|
const appendFilterParams = (params) => {
|
if (filters.invoiceNumber) {
|
params.invoiceNumber = filters.invoiceNumber;
|
}
|
if (filters.customerId) {
|
params.customerId = filters.customerId;
|
}
|
if (filters.dateRange?.length === 2) {
|
params.startDate = filters.dateRange[0];
|
params.endDate = filters.dateRange[1];
|
}
|
if (filters.status !== "" && filters.status != null) {
|
params.status = filters.status;
|
}
|
return params;
|
};
|
|
const buildListParams = () =>
|
appendFilterParams({
|
current: pagination.currentPage,
|
size: pagination.pageSize,
|
});
|
|
const buildExportParams = () => appendFilterParams({});
|
|
const handleExport = () => {
|
const params = buildExportParams();
|
proxy.download("/accountSalesInvoice/exportAccountSalesInvoice", params, `销项发票_${Date.now()}.xlsx`);
|
};
|
|
const getTableData = () => {
|
tableLoading.value = true;
|
listPageAccountSalesInvoice(buildListParams())
|
.then((res) => {
|
if (res.code === 200) {
|
const records = res.data?.records ?? [];
|
dataList.value = records.map(normalizeTableRow);
|
pagination.total = res.data?.total ?? 0;
|
} else {
|
dataList.value = [];
|
pagination.total = 0;
|
ElMessage.error(res.msg || "查询失败");
|
}
|
})
|
.catch(() => {
|
dataList.value = [];
|
pagination.total = 0;
|
ElMessage.error("查询失败");
|
})
|
.finally(() => {
|
tableLoading.value = false;
|
});
|
};
|
|
const onSearch = () => {
|
pagination.currentPage = 1;
|
getTableData();
|
};
|
|
const resetFilters = () => {
|
filters.invoiceNumber = "";
|
filters.customerId = "";
|
filters.dateRange = [];
|
filters.status = "";
|
pagination.currentPage = 1;
|
getTableData();
|
};
|
|
const changePage = ({ current, size }) => {
|
pagination.currentPage = current;
|
pagination.pageSize = size;
|
getTableData();
|
};
|
|
const closeDialog = () => {
|
dialogVisible.value = false;
|
isView.value = false;
|
};
|
|
const add = () => {
|
isView.value = false;
|
dialogTitle.value = "录入发票";
|
Object.assign(form, {
|
invoiceNo: "",
|
customerId: "",
|
invoiceDate: new Date().toISOString().split("T")[0],
|
invoiceType: "增值税专用发票",
|
taxRate: 13,
|
amount: 0,
|
taxAmount: 0,
|
totalAmount: 0,
|
content: "",
|
remark: "",
|
accountInvoiceApplicationId: undefined,
|
storageAttachmentId: undefined,
|
});
|
dialogVisible.value = true;
|
};
|
|
const view = (row) => {
|
isView.value = true;
|
dialogTitle.value = "查看发票";
|
fillFormFromRow(row);
|
dialogVisible.value = true;
|
};
|
|
const handleCancel = (row) => {
|
ElMessageBox.confirm(`确认作废发票「${row.invoiceNo ?? row.invoiceNumber}」吗?`, "作废确认", {
|
confirmButtonText: "确定",
|
cancelButtonText: "取消",
|
type: "warning",
|
}).then(() => {
|
cancelAccountSalesInvoice(buildCancelPayload(row))
|
.then((res) => {
|
if (res.code === 200) {
|
ElMessage.success("作废成功");
|
getTableData();
|
} else {
|
ElMessage.error(res.msg || "作废失败");
|
}
|
})
|
.catch(() => {
|
ElMessage.error("作废失败");
|
});
|
});
|
};
|
|
const handleDelete = (row) => {
|
ElMessageBox.confirm(`确认删除发票「${row.invoiceNo ?? row.invoiceNumber}」吗?删除后不可恢复。`, "删除确认", {
|
confirmButtonText: "确定",
|
cancelButtonText: "取消",
|
type: "warning",
|
}).then(() => {
|
deleteAccountSalesInvoice([row.id])
|
.then((res) => {
|
if (res.code === 200) {
|
ElMessage.success("删除成功");
|
getTableData();
|
} else {
|
ElMessage.error(res.msg || "删除失败");
|
}
|
})
|
.catch(() => {
|
ElMessage.error("删除失败");
|
});
|
});
|
};
|
|
const submitForm = () => {
|
formRef.value.validate((valid) => {
|
if (!valid) return;
|
submitLoading.value = true;
|
addAccountSalesInvoice(buildSubmitPayload())
|
.then((res) => {
|
if (res.code === 200) {
|
ElMessage.success("录入成功");
|
closeDialog();
|
getTableData();
|
} else {
|
ElMessage.error(res.msg || "录入失败");
|
}
|
})
|
.catch(() => {
|
ElMessage.error("录入失败");
|
})
|
.finally(() => {
|
submitLoading.value = false;
|
});
|
});
|
};
|
|
onMounted(() => {
|
getCustomerList();
|
getTableData();
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.actions {
|
display: flex;
|
justify-content: space-between;
|
margin-bottom: 15px;
|
}
|
|
.text-primary {
|
color: #409eff;
|
font-weight: bold;
|
}
|
|
.text-danger {
|
color: #f56c6c;
|
font-weight: bold;
|
}
|
|
.text-success {
|
color: #67c23a;
|
font-weight: bold;
|
}
|
</style>
|