| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="filters" :inline="true"> |
| | | <el-form-item label="发票代码:"> |
| | | <el-input v-model="filters.invoiceCode" placeholder="请输入发票代码" clearable style="width: 200px;" /> |
| | | </el-form-item> |
| | | <el-form-item label="发票号码:"> |
| | | <el-input v-model="filters.invoiceNo" placeholder="请输入发票号码" clearable style="width: 200px;" /> |
| | | <el-input v-model="filters.invoiceNumber" placeholder="请输入发票号码" clearable style="width: 200px;" /> |
| | | </el-form-item> |
| | | <el-form-item label="供应商:"> |
| | | <el-select v-model="filters.supplierId" placeholder="请选择供应商" clearable style="width: 200px;"> |
| | | <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /> |
| | | <el-select v-model="filters.supplierId" placeholder="请选择供应商" clearable filterable style="width: 200px;"> |
| | | <el-option |
| | | v-for="item in supplierList" |
| | | :key="item.id" |
| | | :label="item.supplierName" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="认证状态:"> |
| | | <el-select v-model="filters.certifyStatus" placeholder="请选择认证状态" clearable style="width: 150px;"> |
| | | <el-option label="未认证" value="uncertified" /> |
| | | <el-option label="已认证" value="certified" /> |
| | | <el-option label="认证失败" value="failed" /> |
| | | <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="getTableData">搜索</el-button> |
| | | <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> |
| | | <el-button type="success" @click="handleBatchCertify" icon="Check" :disabled="selectedRows.length === 0">批量认证</el-button> |
| | | </div> |
| | | <div></div> |
| | | <div> |
| | | <el-button type="primary" @click="add" icon="Plus">录入发票</el-button> |
| | | <el-button @click="handleOut" icon="Download">导出</el-button> |
| | | <el-button @click="handleExport" icon="Download">导出</el-button> |
| | | </div> |
| | | </div> |
| | | <PIMTable |
| | | rowKey="id" |
| | | isSelection |
| | | :column="columns" |
| | | :tableData="dataList" |
| | | :tableLoading="tableLoading" |
| | | :page="{ |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | | total: pagination.total, |
| | | }" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="changePage" |
| | | > |
| | | <template #amount="{ row }"> |
| | |
| | | <template #totalAmount="{ row }"> |
| | | <span class="text-success">¥{{ formatMoney(row.totalAmount) }}</span> |
| | | </template> |
| | | <template #certifyStatus="{ row }"> |
| | | <el-tag :type="getCertifyStatusType(row.certifyStatus)">{{ getCertifyStatusLabel(row.certifyStatus) }}</el-tag> |
| | | <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="edit(row)">编辑</el-button> |
| | | <el-button type="success" link @click="handleCertify(row)" v-if="row.certifyStatus === 'uncertified'">认证</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" @confirm="submitForm" @cancel="dialogVisible = false"> |
| | | <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 :gutter="20"> |
| | | <el-row v-if="isView" :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="发票代码" prop="invoiceCode"> |
| | | <el-input v-model="form.invoiceCode" placeholder="请输入发票代码" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="发票号码" prop="invoiceNo"> |
| | | <el-input v-model="form.invoiceNo" placeholder="请输入发票号码" /> |
| | | <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="supplierId"> |
| | | <el-select v-model="form.supplierId" placeholder="请选择供应商" style="width: 100%;"> |
| | | <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" /> |
| | | <el-select |
| | | v-model="form.supplierId" |
| | | placeholder="请选择供应商" |
| | | style="width: 100%;" |
| | | filterable |
| | | :disabled="isView" |
| | | @change="handleSupplierChange" |
| | | > |
| | | <el-option |
| | | v-for="item in supplierList" |
| | | :key="item.id" |
| | | :label="item.supplierName" |
| | | :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="stockInRecordIds"> |
| | | <el-input |
| | | :model-value="inboundBatchDisplayText" |
| | | placeholder="请先选择供应商" |
| | | readonly |
| | | :disabled="!form.supplierId || isView" |
| | | class="inbound-batch-input" |
| | | @click="handleInboundInputClick" |
| | | > |
| | | <template v-if="!isView" #append> |
| | | <el-button |
| | | :disabled="!form.supplierId" |
| | | :loading="inboundBatchLoading" |
| | | @click.stop="openInboundSelectDialog" |
| | | > |
| | | 选择 |
| | | </el-button> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <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%;" /> |
| | | <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-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%;" @change="calculateTax" /> |
| | | <el-col :span="12"> |
| | | <el-form-item label="发票类型" prop="invoiceType"> |
| | | <el-select |
| | | v-model="form.invoiceType" |
| | | placeholder="请选择发票类型" |
| | | style="width: 100%;" |
| | | :disabled="isView" |
| | | > |
| | | <el-option label="增值税专用发票" value="增值税专用发票" /> |
| | | <el-option label="增值税普通发票" value="增值税普通发票" /> |
| | | <el-option label="电子发票" value="电子发票" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="税率" prop="taxRate"> |
| | | <el-select v-model="form.taxRate" placeholder="请选择税率" style="width: 100%;" @change="calculateTax"> |
| | | <el-select |
| | | v-model="form.taxRate" |
| | | placeholder="请选择税率" |
| | | style="width: 100%;" |
| | | :disabled="isView" |
| | | @change="handleTaxRateChange" |
| | | > |
| | | <el-option |
| | | v-for="dict in tax_rate" |
| | | :key="dict.value" |
| | |
| | | </el-select> |
| | | </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-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="认证状态" prop="certifyStatus"> |
| | | <el-select v-model="form.certifyStatus" placeholder="请选择认证状态" style="width: 100%;" disabled> |
| | | <el-option label="未认证" value="uncertified" /> |
| | | <el-option label="已认证" value="certified" /> |
| | | <el-option label="认证失败" value="failed" /> |
| | | </el-select> |
| | | <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" |
| | | placeholder="根据入库单含税金额自动换算,可修改" |
| | | @change="calculateTaxFromExclusive" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="认证日期" prop="certifyDate"> |
| | | <el-date-picker v-model="form.certifyDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%;" disabled /> |
| | | <el-col :span="8"> |
| | | <el-form-item label="税额"> |
| | | <el-input-number |
| | | v-model="form.taxAmount" |
| | | :min="0" |
| | | :precision="2" |
| | | :controls="false" |
| | | style="width: 100%;" |
| | | disabled |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="价税合计"> |
| | | <el-input-number |
| | | v-model="form.totalAmount" |
| | | :min="0" |
| | | :precision="2" |
| | | :controls="false" |
| | | style="width: 100%;" |
| | | 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="请输入发票内容" /> |
| | | <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="请输入备注" /> |
| | | <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" :disabled="isView" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="primary" @click="submitForm">确定</el-button> |
| | | <el-button @click="dialogVisible = false">取消</el-button> |
| | | <template v-if="!isView" #footer> |
| | | <el-button type="primary" :loading="submitLoading" @click="submitForm">确定</el-button> |
| | | <el-button @click="closeDialog">取消</el-button> |
| | | </template> |
| | | </FormDialog> |
| | | |
| | | <el-dialog |
| | | v-model="inboundSelectVisible" |
| | | title="选择入库单号" |
| | | width="1100px" |
| | | append-to-body |
| | | destroy-on-close |
| | | :close-on-click-modal="false" |
| | | @closed="handleInboundDialogClosed" |
| | | > |
| | | <el-table |
| | | ref="inboundTableRef" |
| | | v-loading="inboundBatchLoading" |
| | | :data="inboundBatchList" |
| | | row-key="id" |
| | | border |
| | | stripe |
| | | max-height="480" |
| | | @selection-change="handleInboundDialogSelectionChange" |
| | | > |
| | | <el-table-column type="selection" width="55" align="center" /> |
| | | <el-table-column prop="inboundBatches" label="入库单号" min-width="140" show-overflow-tooltip /> |
| | | <el-table-column prop="supplierName" label="供应商" min-width="120" show-overflow-tooltip /> |
| | | <el-table-column prop="productName" label="产品名称" min-width="120" show-overflow-tooltip /> |
| | | <el-table-column prop="specificationModel" label="规格型号" min-width="140" show-overflow-tooltip /> |
| | | <el-table-column prop="purchaseContractNumber" label="采购订单号" min-width="140" show-overflow-tooltip /> |
| | | <el-table-column prop="inboundDate" label="入库日期" width="110" align="center" /> |
| | | <el-table-column prop="inboundAmount" label="入库金额(含税)" width="120" align="right"> |
| | | <template #default="{ row }">¥{{ formatMoney(getInboundRowTaxInclusiveAmount(row)) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <template #footer> |
| | | <el-button type="primary" @click="confirmInboundSelection">确定</el-button> |
| | | <el-button @click="inboundSelectVisible = false">取消</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, getCurrentInstance } from "vue"; |
| | | import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { getOptions } from "@/api/procurementManagement/procurementLedger.js"; |
| | | import { |
| | | getInboundBatchesBySupplier, |
| | | addAccountPurchaseInvoice, |
| | | listPageAccountPurchaseInvoice, |
| | | cancelAccountPurchaseInvoice, |
| | | deleteAccountPurchaseInvoice, |
| | | } from "@/api/financialManagement/accountPurchaseInvoice.js"; |
| | | |
| | | defineOptions({ |
| | | name: "进项发票", |
| | |
| | | const { tax_rate } = proxy.useDict("tax_rate"); |
| | | |
| | | const filters = reactive({ |
| | | invoiceCode: "", |
| | | invoiceNo: "", |
| | | invoiceNumber: "", |
| | | supplierId: "", |
| | | certifyStatus: "", |
| | | dateRange: [], |
| | | status: "", |
| | | }); |
| | | |
| | | const pagination = reactive({ |
| | |
| | | }); |
| | | |
| | | const columns = [ |
| | | { label: "发票代码", prop: "invoiceCode", width: "130" }, |
| | | { label: "发票号码", prop: "invoiceNo", width: "120" }, |
| | | { label: "发票号码", prop: "invoiceNo", width: "140" }, |
| | | { label: "供应商", prop: "supplierName", width: "180" }, |
| | | { label: "开票日期", prop: "invoiceDate", width: "120" }, |
| | | { label: "金额", prop: "amount", slot: "amount" }, |
| | | { label: "税额", prop: "taxAmount", slot: "taxAmount" }, |
| | | { label: "价税合计", prop: "totalAmount", slot: "totalAmount" }, |
| | | { label: "认证状态", prop: "certifyStatus", slot: "certifyStatus" }, |
| | | { label: "操作", prop: "operation", slot: "operation", width: "180", fixed: "right" }, |
| | | { 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: "200", fixed: "right" }, |
| | | ]; |
| | | |
| | | const dataList = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | | const dialogVisible = ref(false); |
| | | const dialogTitle = ref(""); |
| | | const formRef = ref(null); |
| | | const isEdit = ref(false); |
| | | const currentId = ref(null); |
| | | const isView = ref(false); |
| | | const submitLoading = ref(false); |
| | | const supplierList = ref([]); |
| | | |
| | | const supplierList = [ |
| | | { id: 1, name: "北京原材料供应商" }, |
| | | { id: 2, name: "上海电子元器件公司" }, |
| | | { id: 3, name: "广州包装材料厂" }, |
| | | { id: 4, name: "深圳五金配件公司" }, |
| | | ]; |
| | | const inboundBatchList = ref([]); |
| | | const inboundBatchOptions = ref([]); |
| | | const inboundBatchLoading = ref(false); |
| | | const inboundSelectVisible = ref(false); |
| | | const inboundTableRef = ref(null); |
| | | const dialogInboundSelection = ref([]); |
| | | |
| | | const STATUS_LABEL_MAP = { 0: "正常", 1: "作废" }; |
| | | const STATUS_TYPE_MAP = { 0: "success", 1: "info" }; |
| | | |
| | | const form = reactive({ |
| | | invoiceCode: "", |
| | | invoiceNo: "", |
| | | supplierId: "", |
| | | invoiceDate: "", |
| | | amount: 0, |
| | | invoiceType: "增值税专用发票", |
| | | taxRate: 13, |
| | | amount: 0, |
| | | taxAmount: 0, |
| | | totalAmount: 0, |
| | | certifyStatus: "uncertified", |
| | | certifyDate: "", |
| | | content: "", |
| | | remark: "", |
| | | stockInRecordIds: [], |
| | | inboundBatches: "", |
| | | storageAttachmentId: undefined, |
| | | status: 0, |
| | | }); |
| | | |
| | | const rules = { |
| | | invoiceCode: [{ required: true, message: "请输入发票代码", trigger: "blur" }], |
| | | invoiceNo: [{ required: true, message: "请输入发票号码", trigger: "blur" }], |
| | | supplierId: [{ required: true, message: "请选择供应商", trigger: "change" }], |
| | | stockInRecordIds: [{ required: true, type: "array", min: 1, message: "请选择关联入库单", trigger: "change" }], |
| | | invoiceDate: [{ required: true, message: "请选择开票日期", trigger: "change" }], |
| | | amount: [{ required: true, message: "请输入金额", trigger: "blur" }], |
| | | invoiceType: [{ required: true, message: "请选择发票类型", trigger: "change" }], |
| | | taxRate: [{ required: true, message: "请选择税率", trigger: "change" }], |
| | | amount: [{ required: true, message: "请输入金额", trigger: "blur" }], |
| | | }; |
| | | |
| | | const mockData = [ |
| | | { id: 1, invoiceCode: "0440021001", invoiceNo: "12345678", supplierId: 1, supplierName: "北京原材料供应商", invoiceDate: "2024-01-08", amount: 8000, taxRate: 13, taxAmount: 1040, totalAmount: 9040, certifyStatus: "certified", certifyDate: "2024-01-15", content: "原材料采购", remark: "" }, |
| | | { id: 2, invoiceCode: "0440021002", invoiceNo: "87654321", supplierId: 2, supplierName: "上海电子元器件公司", invoiceDate: "2024-01-10", amount: 12000, taxRate: 13, taxAmount: 1560, totalAmount: 13560, certifyStatus: "uncertified", certifyDate: "", content: "电子元器件", remark: "" }, |
| | | { id: 3, invoiceCode: "0440021003", invoiceNo: "11112222", supplierId: 3, supplierName: "广州包装材料厂", invoiceDate: "2024-01-12", amount: 3500, taxRate: 13, taxAmount: 455, totalAmount: 3955, certifyStatus: "certified", certifyDate: "2024-01-18", content: "包装材料", remark: "" }, |
| | | ]; |
| | | |
| | | const formatMoney = (value) => { |
| | | if (value === undefined || value === null) return "0.00"; |
| | | return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | | }; |
| | | |
| | | const calculateTax = () => { |
| | | 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) => STATUS_LABEL_MAP[normalizeStatus(status)] ?? "正常"; |
| | | |
| | | const getStatusType = (status) => STATUS_TYPE_MAP[normalizeStatus(status)] ?? "success"; |
| | | |
| | | const parseStockInRecordIds = (value) => { |
| | | if (!value) return []; |
| | | if (Array.isArray(value)) return value; |
| | | return String(value) |
| | | .split(/[,,]/) |
| | | .map((s) => s.trim()) |
| | | .filter(Boolean) |
| | | .map((s) => (/^\d+$/.test(s) ? Number(s) : s)); |
| | | }; |
| | | |
| | | const formatInboundBatches = (value) => { |
| | | if (value === undefined || value === null || value === "") return ""; |
| | | if (Array.isArray(value)) return value.filter(Boolean).join("、"); |
| | | return String(value) |
| | | .split(/[,,]/) |
| | | .map((s) => s.trim()) |
| | | .filter(Boolean) |
| | | .join("、"); |
| | | }; |
| | | |
| | | const isSameInboundId = (a, b) => String(a) === String(b); |
| | | |
| | | const getInboundRowId = (row) => row?.id ?? row?.stockInRecordId; |
| | | |
| | | /** 入库单金额为含税价 */ |
| | | const getInboundRowTaxInclusiveAmount = (row) => |
| | | Number(row?.inboundAmount ?? row?.taxInclusivePrice ?? row?.totalAmount ?? 0); |
| | | |
| | | const normalizeInboundBatchOptions = (data) => { |
| | | const list = Array.isArray(data) ? data : []; |
| | | return list.map((item, index) => { |
| | | if (typeof item === "string" || typeof item === "number") { |
| | | const text = String(item); |
| | | return { label: text, value: text, inboundAmount: 0 }; |
| | | } |
| | | const label = |
| | | item.inboundBatches ?? item.batchNo ?? item.inboundNo ?? item.label ?? `入库单${index + 1}`; |
| | | const value = item.id ?? item.stockInRecordId ?? label; |
| | | return { |
| | | label: String(label), |
| | | value, |
| | | inboundAmount: getInboundRowTaxInclusiveAmount(item), |
| | | }; |
| | | }); |
| | | }; |
| | | |
| | | /** 不含税金额变更:税额、价税合计正向计算 */ |
| | | const calculateTaxFromExclusive = () => { |
| | | form.taxAmount = Number((form.amount * form.taxRate / 100).toFixed(2)); |
| | | form.totalAmount = Number((form.amount + form.taxAmount).toFixed(2)); |
| | | }; |
| | | |
| | | const getCertifyStatusLabel = (status) => { |
| | | const map = { uncertified: "未认证", certified: "已认证", failed: "认证失败" }; |
| | | return map[status] || status; |
| | | /** 价税合计变更:按税率反算不含税金额、税额 */ |
| | | const calculateTaxFromInclusive = (inclusiveTotal) => { |
| | | const total = Number(inclusiveTotal ?? form.totalAmount ?? 0); |
| | | if (total <= 0) { |
| | | form.amount = 0; |
| | | form.taxAmount = 0; |
| | | form.totalAmount = 0; |
| | | return; |
| | | } |
| | | const rate = Number(form.taxRate) / 100; |
| | | form.totalAmount = Number(total.toFixed(2)); |
| | | form.amount = Number((form.totalAmount / (1 + rate)).toFixed(2)); |
| | | form.taxAmount = Number((form.totalAmount - form.amount).toFixed(2)); |
| | | }; |
| | | |
| | | const getCertifyStatusType = (status) => { |
| | | const map = { uncertified: "info", certified: "success", failed: "danger" }; |
| | | return map[status] || ""; |
| | | const handleTaxRateChange = () => { |
| | | if (form.totalAmount > 0) { |
| | | calculateTaxFromInclusive(form.totalAmount); |
| | | } else { |
| | | calculateTaxFromExclusive(); |
| | | } |
| | | }; |
| | | |
| | | /** 根据已选入库单汇总含税金额,反算不含税金额与税额 */ |
| | | const syncInvoiceAmount = () => { |
| | | const selected = form.stockInRecordIds || []; |
| | | const sumFromOptions = inboundBatchOptions.value |
| | | .filter((opt) => selected.some((id) => isSameInboundId(id, opt.value))) |
| | | .reduce((acc, opt) => acc + (Number(opt.inboundAmount) || 0), 0); |
| | | |
| | | let taxInclusiveSum = sumFromOptions; |
| | | if (taxInclusiveSum <= 0 && selected.length) { |
| | | taxInclusiveSum = inboundBatchList.value |
| | | .filter((row) => selected.some((id) => isSameInboundId(id, getInboundRowId(row)))) |
| | | .reduce((acc, row) => acc + getInboundRowTaxInclusiveAmount(row), 0); |
| | | } |
| | | |
| | | calculateTaxFromInclusive(taxInclusiveSum > 0 ? Number(taxInclusiveSum.toFixed(2)) : 0); |
| | | }; |
| | | |
| | | const inboundBatchDisplayText = computed(() => { |
| | | if (form.inboundBatches) return form.inboundBatches; |
| | | const ids = form.stockInRecordIds || []; |
| | | if (!ids.length) return ""; |
| | | const labels = inboundBatchOptions.value |
| | | .filter((opt) => ids.some((id) => isSameInboundId(id, opt.value))) |
| | | .map((opt) => opt.label); |
| | | if (labels.length) return labels.join("、"); |
| | | return ids.join("、"); |
| | | }); |
| | | |
| | | 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), |
| | | stockInRecordIds: row.stockInRecordIds ?? "", |
| | | inboundBatches: formatInboundBatches(row.inboundBatches), |
| | | }); |
| | | |
| | | const toFormNumber = (val) => { |
| | | const n = Number(val); |
| | | return Number.isFinite(n) ? n : 0; |
| | | }; |
| | | |
| | | const resolveFormAmounts = (row) => { |
| | | let amount = toFormNumber(row.taxExclusivelPrice ?? row.amount); |
| | | let taxAmount = toFormNumber(row.taxPrice ?? row.taxAmount); |
| | | let totalAmount = toFormNumber(row.taxInclusivePrice ?? row.totalAmount); |
| | | const taxRate = toFormNumber(row.taxRate) || 13; |
| | | |
| | | if (totalAmount > 0 && amount === 0 && taxAmount === 0) { |
| | | amount = Number((totalAmount / (1 + taxRate / 100)).toFixed(2)); |
| | | taxAmount = Number((totalAmount - amount).toFixed(2)); |
| | | } else if (totalAmount > 0 && amount > 0 && taxAmount === 0) { |
| | | taxAmount = Number((totalAmount - amount).toFixed(2)); |
| | | } else if (amount > 0 && taxAmount === 0 && totalAmount === 0) { |
| | | taxAmount = Number((amount * taxRate / 100).toFixed(2)); |
| | | totalAmount = Number((amount + taxAmount).toFixed(2)); |
| | | } else if (amount > 0 && taxAmount > 0 && totalAmount === 0) { |
| | | totalAmount = Number((amount + taxAmount).toFixed(2)); |
| | | } |
| | | |
| | | return { amount, taxAmount, totalAmount }; |
| | | }; |
| | | |
| | | const fillFormFromRow = (row) => { |
| | | const stockInRecordIds = parseStockInRecordIds(row.stockInRecordIds); |
| | | const { amount, taxAmount, totalAmount } = resolveFormAmounts(row); |
| | | Object.assign(form, { |
| | | invoiceNo: row.invoiceNo ?? row.invoiceNumber ?? "", |
| | | supplierId: row.supplierId, |
| | | invoiceDate: row.invoiceDate ?? row.issueDate ?? "", |
| | | invoiceType: row.invoiceType ?? "增值税专用发票", |
| | | taxRate: row.taxRate ?? 13, |
| | | amount, |
| | | taxAmount, |
| | | totalAmount, |
| | | content: row.content ?? row.invoiceContent ?? "", |
| | | remark: row.remark ?? "", |
| | | stockInRecordIds, |
| | | inboundBatches: formatInboundBatches(row.inboundBatches), |
| | | storageAttachmentId: row.storageAttachmentId, |
| | | status: normalizeStatus(row.status), |
| | | }); |
| | | }; |
| | | |
| | | const buildCancelPayload = (row) => ({ |
| | | id: row.id, |
| | | 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, |
| | | supplierId: row.supplierId, |
| | | storageAttachmentId: row.storageAttachmentId, |
| | | stockInRecordIds: row.stockInRecordIds ?? "", |
| | | status: 1, |
| | | }); |
| | | |
| | | const buildSubmitPayload = () => ({ |
| | | invoiceNumber: form.invoiceNo, |
| | | supplierId: form.supplierId, |
| | | issueDate: form.invoiceDate, |
| | | invoiceType: form.invoiceType, |
| | | taxRate: form.taxRate, |
| | | taxExclusivelPrice: form.amount, |
| | | taxPrice: form.taxAmount, |
| | | taxInclusivePrice: form.totalAmount, |
| | | invoiceContent: form.content, |
| | | remark: form.remark || "", |
| | | stockInRecordIds: (form.stockInRecordIds || []).join(","), |
| | | status: 0, |
| | | storageAttachmentId: form.storageAttachmentId, |
| | | }); |
| | | |
| | | const getSupplierList = () => { |
| | | getOptions().then((res) => { |
| | | if (res.code === 200) { |
| | | supplierList.value = res.data ?? []; |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const appendFilterParams = (params) => { |
| | | if (filters.invoiceNumber) { |
| | | params.invoiceNumber = filters.invoiceNumber; |
| | | } |
| | | if (filters.supplierId) { |
| | | params.supplierId = filters.supplierId; |
| | | } |
| | | 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 = () => { |
| | | proxy.download( |
| | | "/accountPurchaseInvoice/exportAccountPurchaseInvoice", |
| | | buildExportParams(), |
| | | `进项发票_${Date.now()}.xlsx` |
| | | ); |
| | | }; |
| | | |
| | | const getTableData = () => { |
| | | let result = [...mockData]; |
| | | if (filters.invoiceCode) { |
| | | result = result.filter(item => item.invoiceCode.includes(filters.invoiceCode)); |
| | | } |
| | | if (filters.invoiceNo) { |
| | | result = result.filter(item => item.invoiceNo.includes(filters.invoiceNo)); |
| | | } |
| | | if (filters.supplierId) { |
| | | result = result.filter(item => item.supplierId === filters.supplierId); |
| | | } |
| | | if (filters.certifyStatus) { |
| | | result = result.filter(item => item.certifyStatus === filters.certifyStatus); |
| | | } |
| | | pagination.total = result.length; |
| | | dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize); |
| | | tableLoading.value = true; |
| | | listPageAccountPurchaseInvoice(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 resetFilters = () => { |
| | | filters.invoiceCode = ""; |
| | | filters.invoiceNo = ""; |
| | | filters.supplierId = ""; |
| | | filters.certifyStatus = ""; |
| | | const onSearch = () => { |
| | | pagination.currentPage = 1; |
| | | getTableData(); |
| | | }; |
| | | |
| | | const changePage = ({ current, size }) => { |
| | | pagination.currentPage = current; |
| | | pagination.pageSize = size; |
| | | const resetFilters = () => { |
| | | filters.invoiceNumber = ""; |
| | | filters.supplierId = ""; |
| | | filters.dateRange = []; |
| | | filters.status = ""; |
| | | pagination.currentPage = 1; |
| | | getTableData(); |
| | | }; |
| | | |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | | const changePage = ({ page, limit }) => { |
| | | pagination.currentPage = page; |
| | | pagination.pageSize = limit; |
| | | getTableData(); |
| | | }; |
| | | |
| | | const closeDialog = () => { |
| | | dialogVisible.value = false; |
| | | isView.value = false; |
| | | inboundSelectVisible.value = false; |
| | | }; |
| | | |
| | | const resetForm = () => { |
| | | Object.assign(form, { |
| | | invoiceNo: "", |
| | | supplierId: "", |
| | | invoiceDate: new Date().toISOString().split("T")[0], |
| | | invoiceType: "增值税专用发票", |
| | | taxRate: 13, |
| | | amount: 0, |
| | | taxAmount: 0, |
| | | totalAmount: 0, |
| | | content: "", |
| | | remark: "", |
| | | stockInRecordIds: [], |
| | | inboundBatches: "", |
| | | storageAttachmentId: undefined, |
| | | status: 0, |
| | | }); |
| | | inboundBatchList.value = []; |
| | | inboundBatchOptions.value = []; |
| | | }; |
| | | |
| | | const add = () => { |
| | | isEdit.value = false; |
| | | isView.value = false; |
| | | dialogTitle.value = "录入发票"; |
| | | Object.assign(form, { |
| | | invoiceCode: "", |
| | | invoiceNo: "", |
| | | supplierId: "", |
| | | invoiceDate: new Date().toISOString().split('T')[0], |
| | | amount: 0, |
| | | taxRate: 13, |
| | | taxAmount: 0, |
| | | totalAmount: 0, |
| | | certifyStatus: "uncertified", |
| | | certifyDate: "", |
| | | content: "", |
| | | remark: "", |
| | | }); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | const edit = (row) => { |
| | | isEdit.value = true; |
| | | currentId.value = row.id; |
| | | dialogTitle.value = "编辑发票"; |
| | | Object.assign(form, row); |
| | | resetForm(); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | const view = (row) => { |
| | | ElMessage.info(`查看发票: ${row.invoiceCode}-${row.invoiceNo}`); |
| | | isView.value = true; |
| | | dialogTitle.value = "查看发票"; |
| | | fillFormFromRow(row); |
| | | if (row.supplierId) { |
| | | loadInboundBatches(row.supplierId, true, false); |
| | | } |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | const handleCertify = (row) => { |
| | | ElMessageBox.confirm("确认认证该发票吗?", "提示", { |
| | | confirmButtonText: "确认", |
| | | const handleCancel = (row) => { |
| | | ElMessageBox.confirm(`确认作废发票「${row.invoiceNo ?? row.invoiceNumber}」吗?`, "作废确认", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "info", |
| | | type: "warning", |
| | | }).then(() => { |
| | | const index = mockData.findIndex(item => item.id === row.id); |
| | | if (index !== -1) { |
| | | mockData[index].certifyStatus = "certified"; |
| | | mockData[index].certifyDate = new Date().toISOString().split('T')[0]; |
| | | } |
| | | ElMessage.success("认证成功"); |
| | | getTableData(); |
| | | cancelAccountPurchaseInvoice(buildCancelPayload(row)) |
| | | .then((res) => { |
| | | if (res.code === 200) { |
| | | ElMessage.success("作废成功"); |
| | | getTableData(); |
| | | } else { |
| | | ElMessage.error(res.msg || "作废失败"); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.error("作废失败"); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const handleBatchCertify = () => { |
| | | ElMessageBox.confirm(`确认批量认证选中的 ${selectedRows.value.length} 张发票吗?`, "提示", { |
| | | confirmButtonText: "确认", |
| | | const handleDelete = (row) => { |
| | | ElMessageBox.confirm(`确认删除发票「${row.invoiceNo ?? row.invoiceNumber}」吗?`, "删除确认", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "info", |
| | | type: "warning", |
| | | }).then(() => { |
| | | selectedRows.value.forEach(row => { |
| | | const index = mockData.findIndex(item => item.id === row.id); |
| | | if (index !== -1 && mockData[index].certifyStatus === "uncertified") { |
| | | mockData[index].certifyStatus = "certified"; |
| | | mockData[index].certifyDate = new Date().toISOString().split('T')[0]; |
| | | } |
| | | }); |
| | | ElMessage.success("批量认证成功"); |
| | | getTableData(); |
| | | deleteAccountPurchaseInvoice([row.id]) |
| | | .then((res) => { |
| | | if (res.code === 200) { |
| | | ElMessage.success("删除成功"); |
| | | getTableData(); |
| | | } else { |
| | | ElMessage.error(res.msg || "删除失败"); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.error("删除失败"); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | ElMessage.success("导出成功"); |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value.validate((valid) => { |
| | | if (valid) { |
| | | const supplier = supplierList.find(item => item.id === form.supplierId); |
| | | if (isEdit.value) { |
| | | const index = mockData.findIndex(item => item.id === currentId.value); |
| | | if (index !== -1) { |
| | | mockData[index] = { ...mockData[index], ...form, supplierName: supplier?.name }; |
| | | formRef.value?.validate((valid) => { |
| | | if (!valid) return; |
| | | submitLoading.value = true; |
| | | addAccountPurchaseInvoice(buildSubmitPayload()) |
| | | .then((res) => { |
| | | if (res.code === 200) { |
| | | ElMessage.success("录入成功"); |
| | | closeDialog(); |
| | | pagination.currentPage = 1; |
| | | getTableData(); |
| | | } else { |
| | | ElMessage.error(res.msg || "录入失败"); |
| | | } |
| | | ElMessage.success("编辑成功"); |
| | | } else { |
| | | const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1; |
| | | mockData.push({ id: newId, ...form, supplierName: supplier?.name }); |
| | | ElMessage.success("录入成功"); |
| | | } |
| | | dialogVisible.value = false; |
| | | getTableData(); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.error("录入失败"); |
| | | }) |
| | | .finally(() => { |
| | | submitLoading.value = false; |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const ensureInboundOptionsForSelected = () => { |
| | | const ids = form.stockInRecordIds || []; |
| | | ids.forEach((id) => { |
| | | const exists = inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, id)); |
| | | if (exists) return; |
| | | const fromList = inboundBatchList.value.find((row) => isSameInboundId(getInboundRowId(row), id)); |
| | | if (fromList) { |
| | | const [option] = normalizeInboundBatchOptions([fromList]); |
| | | if (option) inboundBatchOptions.value.push(option); |
| | | return; |
| | | } |
| | | inboundBatchOptions.value.push({ |
| | | label: String(id), |
| | | value: id, |
| | | inboundAmount: 0, |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const restoreInboundTableSelection = () => { |
| | | nextTick(() => { |
| | | const table = inboundTableRef.value; |
| | | if (!table) return; |
| | | table.clearSelection(); |
| | | const selectedIds = new Set((form.stockInRecordIds || []).map((id) => String(id))); |
| | | inboundBatchList.value.forEach((row) => { |
| | | const rowId = getInboundRowId(row); |
| | | if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) { |
| | | table.toggleRowSelection(row, true); |
| | | } |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const loadInboundBatches = (supplierId, keepSelected = false, syncAmount = true) => { |
| | | if (!supplierId) { |
| | | inboundBatchList.value = []; |
| | | inboundBatchOptions.value = []; |
| | | if (!keepSelected) { |
| | | form.stockInRecordIds = []; |
| | | form.inboundBatches = ""; |
| | | form.amount = 0; |
| | | form.taxAmount = 0; |
| | | form.totalAmount = 0; |
| | | } |
| | | return Promise.resolve(); |
| | | } |
| | | inboundBatchLoading.value = true; |
| | | return getInboundBatchesBySupplier({ supplierId }) |
| | | .then((res) => { |
| | | if (res.code === 200) { |
| | | const list = res.data?.records ?? res.data ?? []; |
| | | inboundBatchList.value = Array.isArray(list) ? list : []; |
| | | inboundBatchOptions.value = normalizeInboundBatchOptions(list); |
| | | } else { |
| | | inboundBatchList.value = []; |
| | | inboundBatchOptions.value = []; |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | inboundBatchList.value = []; |
| | | inboundBatchOptions.value = []; |
| | | }) |
| | | .finally(() => { |
| | | inboundBatchLoading.value = false; |
| | | if (keepSelected) { |
| | | ensureInboundOptionsForSelected(); |
| | | restoreInboundTableSelection(); |
| | | if (syncAmount && !isView.value) { |
| | | syncInvoiceAmount(); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const handleSupplierChange = (supplierId) => { |
| | | form.stockInRecordIds = []; |
| | | form.inboundBatches = ""; |
| | | form.amount = 0; |
| | | form.taxAmount = 0; |
| | | form.totalAmount = 0; |
| | | loadInboundBatches(supplierId); |
| | | }; |
| | | |
| | | const handleInboundInputClick = () => { |
| | | if (isView.value) return; |
| | | openInboundSelectDialog(); |
| | | }; |
| | | |
| | | const openInboundSelectDialog = () => { |
| | | if (!form.supplierId || isView.value) return; |
| | | inboundSelectVisible.value = true; |
| | | loadInboundBatches(form.supplierId, true).then(() => { |
| | | restoreInboundTableSelection(); |
| | | }); |
| | | }; |
| | | |
| | | const handleInboundDialogSelectionChange = (selection) => { |
| | | dialogInboundSelection.value = selection; |
| | | }; |
| | | |
| | | const confirmInboundSelection = () => { |
| | | if (dialogInboundSelection.value.length === 0) { |
| | | ElMessage.warning("请至少选择一条入库单"); |
| | | return; |
| | | } |
| | | form.stockInRecordIds = dialogInboundSelection.value |
| | | .map((row) => getInboundRowId(row)) |
| | | .filter((id) => id !== undefined && id !== null); |
| | | form.inboundBatches = dialogInboundSelection.value |
| | | .map((row) => row.inboundBatches ?? row.batchNo ?? "") |
| | | .filter(Boolean) |
| | | .join("、"); |
| | | dialogInboundSelection.value.forEach((row) => { |
| | | const [option] = normalizeInboundBatchOptions([row]); |
| | | if (option && !inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, option.value))) { |
| | | inboundBatchOptions.value.push(option); |
| | | } |
| | | }); |
| | | inboundSelectVisible.value = false; |
| | | syncInvoiceAmount(); |
| | | formRef.value?.validateField("stockInRecordIds"); |
| | | }; |
| | | |
| | | const handleInboundDialogClosed = () => { |
| | | dialogInboundSelection.value = []; |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getSupplierList(); |
| | | getTableData(); |
| | | }); |
| | | </script> |
| | |
| | | color: #67c23a; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .inbound-batch-input :deep(.el-input__wrapper) { |
| | | cursor: pointer; |
| | | } |
| | | </style> |