| | |
| | | <el-input v-model="searchForm.customerName" placeholder="请输入" clearable prefix-icon="Search" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="客户合同号:"> |
| | | <el-input v-model="searchForm.customerContractNo" placeholder="请输入" clearable prefix-icon="Search" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="销售合同号:"> |
| | | <el-input v-model="searchForm.salesContractNo" placeholder="请输入" clearable prefix-icon="Search" |
| | | @change="handleQuery" /> |
| | |
| | | <el-button type="primary" @click="openForm('add')"> |
| | | 新增台账 |
| | | </el-button> |
| | | <el-button type="primary" plain @click="handleImport">导入</el-button> |
| | | <el-button @click="handleOut">导出</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">删除</el-button> |
| | | <el-button type="primary" plain @click="handlePrint">打印</el-button> |
| | | </div> |
| | | </div> |
| | | <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange" |
| | | :expand-row-keys="expandedRowKeys" :row-key="(row) => row.id" show-summary style="width: 100%" |
| | | :expand-row-keys="expandedRowKeys" :row-key="(row) => row.id" :row-class-name="tableRowClassName" show-summary style="width: 100%" |
| | | :summary-method="summarizeMainTable" @expand-change="expandChange" height="calc(100vh - 18.5em)"> |
| | | <el-table-column align="center" type="selection" width="55" fixed="left"/> |
| | | <el-table-column type="expand" width="60" fixed="left"> |
| | |
| | | <el-table-column align="center" label="序号" type="index"/> |
| | | <el-table-column label="产品大类" prop="productCategory" /> |
| | | <el-table-column label="规格型号" prop="specificationModel" /> |
| | | <el-table-column label="批号" prop="batchNo" /> |
| | | <el-table-column label="UID码" prop="uidNo" /> |
| | | <el-table-column label="单位" prop="unit" /> |
| | | <el-table-column label="产品状态" |
| | | width="100px" |
| | |
| | | type="danger">不足</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="发货状态" prop="shippingStatus" width="140" align="center" show-overflow-tooltip /> |
| | | <el-table-column label="发货状态" width="140" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getShippingStatusType(scope.row)" size="small"> |
| | | {{ getShippingStatusText(scope.row) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="快递公司" prop="expressCompany" show-overflow-tooltip /> |
| | | <el-table-column label="快递单号" prop="expressNumber" show-overflow-tooltip /> |
| | | <el-table-column label="发货车牌" minWidth="100px" align="center"> |
| | | <template #default="scope"> |
| | | <div> |
| | | <el-tag type="success" v-if="scope.row.shippingCarNumber">{{ scope.row.shippingCarNumber }}</el-tag> |
| | | <el-tag v-else type="info">未发货</el-tag> |
| | | <el-tag v-else type="info">-</el-tag> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | size="small" |
| | | :disabled="scope.row.approveStatus !== 1 || !!scope.row.shippingDate || !!scope.row.shippingCarNumber" |
| | | size="small" |
| | | :disabled="!canShip(scope.row)" |
| | | @click="openDeliveryForm(scope.row)"> |
| | | 发货 |
| | | </el-button> |
| | |
| | | </el-table-column> |
| | | <el-table-column align="center" label="序号" type="index" width="60" /> |
| | | <el-table-column label="销售合同号" prop="salesContractNo" width="180" show-overflow-tooltip /> |
| | | <el-table-column label="客户合同号" prop="customerContractNo" width="180" show-overflow-tooltip /> |
| | | <el-table-column label="客户名称" prop="customerName" width="300" show-overflow-tooltip /> |
| | | <el-table-column label="业务员" prop="salesman" width="100" show-overflow-tooltip /> |
| | | <el-table-column label="项目名称" prop="projectName" width="180" show-overflow-tooltip /> |
| | |
| | | <el-table-column label="录入人" prop="entryPersonName" width="100" show-overflow-tooltip /> |
| | | <el-table-column label="录入日期" prop="entryDate" width="120" show-overflow-tooltip /> |
| | | <el-table-column label="签订日期" prop="executionDate" width="120" show-overflow-tooltip /> |
| | | <el-table-column label="交付日期" prop="deliveryDate" width="120" show-overflow-tooltip /> |
| | | <el-table-column label="备注" prop="remarks" width="200" show-overflow-tooltip /> |
| | | <el-table-column fixed="right" label="操作" min-width="100" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="openForm('edit', scope.row)">编辑</el-button> |
| | | <el-button link type="primary" size="small" @click="openForm('edit', scope.row)" :disabled="!scope.row.isEdit">编辑</el-button> |
| | | <!-- <el-button link type="primary" size="small" @click="openForm('view', scope.row)">详情</el-button>--> |
| | | <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">附件</el-button> |
| | | <el-button link type="primary" size="small" @click="exportSaleOutbound(scope.row)">打印销售出库单</el-button> |
| | | <!-- <el-button link type="primary" size="small" @click="openDeliveryForm(scope.row)">发货</el-button>--> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | <FormDialog v-model="dialogFormVisible" :title="operationType === 'add' ? '新增销售台账页面' : '编辑销售台账页面'" :width="'70%'" |
| | | :operation-type="operationType" @close="closeDia" @confirm="submitForm" @cancel="closeDia"> |
| | | <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef"> |
| | | <!-- 报价单导入入口:放在表单顶部,选择后反显客户/业务员等 --> |
| | | <el-row v-if="operationType === 'add'" style="margin-bottom: 10px;"> |
| | | <el-col :span="24" style="text-align: right;"> |
| | | <el-button type="primary" plain @click="openQuotationDialog"> |
| | | 从销售报价导入 |
| | | </el-button> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="销售合同号:" prop="salesContractNo"> |
| | |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="客户合同号:" prop="customerContractNo"> |
| | | <el-input v-model="form.customerContractNo" placeholder="请输入" clearable :disabled="operationType === 'view'"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="客户名称:" prop="customerId"> |
| | | <el-select v-model="form.customerId" placeholder="请选择" clearable :disabled="operationType === 'view'"> |
| | | <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id"> |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="项目名称:" prop="projectName"> |
| | | <el-input v-model="form.projectName" placeholder="请输入" clearable :disabled="operationType === 'view'" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="项目名称:" prop="projectName"> |
| | | <el-input v-model="form.projectName" placeholder="请输入" clearable :disabled="operationType === 'view'" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="签订日期:" prop="executionDate"> |
| | | <el-date-picker style="width: 100%" v-model="form.executionDate" value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" type="date" placeholder="请选择" clearable :disabled="operationType === 'view'" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="付款方式"> |
| | | <el-input v-model="form.paymentMethod" placeholder="请输入" clearable :disabled="operationType === 'view'" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="交货日期:" prop="entryDate"> |
| | | <el-date-picker style="width: 100%" v-model="form.deliveryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" |
| | | type="date" placeholder="请选择" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-form-item label="产品信息:" prop="entryDate"> |
| | | <el-button v-if="operationType !== 'view'" type="primary" @click="openProductForm('add')">添加</el-button> |
| | |
| | | </el-row> |
| | | <el-table :data="productData" border @selection-change="productSelected" show-summary |
| | | :summary-method="summarizeMainTable"> |
| | | <el-table-column align="center" type="selection" width="55" v-if="operationType !== 'view'" /> |
| | | <el-table-column align="center" type="selection" width="55" v-if="operationType !== 'view'" |
| | | :selectable="(row) => !isProductShipped(row)" /> |
| | | <el-table-column align="center" label="序号" type="index" width="60" /> |
| | | <el-table-column label="产品大类" prop="productCategory" /> |
| | | <el-table-column label="规格型号" prop="specificationModel" /> |
| | | <el-table-column label="UID码" prop="uidNo" /> |
| | | <el-table-column label="批号" prop="batchNo" /> |
| | | <el-table-column label="单位" prop="unit" /> |
| | | <el-table-column label="数量" prop="quantity" /> |
| | | <el-table-column label="税率(%)" prop="taxRate" /> |
| | |
| | | <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" /> |
| | | <el-table-column fixed="right" label="操作" min-width="60" align="center" v-if="operationType !== 'view'"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="openProductForm('edit', scope.row,scope.$index)">编辑</el-button> |
| | | <el-button link type="primary" size="small" |
| | | :disabled="isProductShipped(scope.row)" |
| | | @click="openProductForm('edit', scope.row,scope.$index)">编辑</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="备注·:" prop="remark"> |
| | | <el-input v-model="form.remark" placeholder="请输入" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" /> |
| | | <el-form-item label="备注:" prop="remarks"> |
| | | <el-input v-model="form.remarks" placeholder="请输入" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="附件材料:" prop="remark"> |
| | | <el-form-item label="附件材料:" prop="salesLedgerFiles"> |
| | | <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload |
| | | :headers="upload.headers" :before-upload="handleBeforeUpload" :on-error="handleUploadError" |
| | | :on-success="handleUploadSuccess" :on-remove="handleRemove"> |
| | |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <pagination |
| | | v-show="quotationPage.total > 0" |
| | | :total="quotationPage.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="quotationPage.current" |
| | | :limit="quotationPage.size" |
| | | @pagination="quotationPaginationChange" |
| | | /> |
| | | |
| | | <template #footer> |
| | | <el-button @click="quotationDialogVisible = false">关闭</el-button> |
| | | </template> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="UID码:" prop="uidNo"> |
| | | <el-input v-model="productForm.uidNo" placeholder="请输入" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="批号:" prop="batchNo"> |
| | | <el-select v-model="productForm.batchNo" |
| | | placeholder="请选择" |
| | | clearable |
| | | filterable |
| | | @change="handleBatchNoChange"> |
| | | <el-option v-for="item in batchNoOptions" :key="item.value" :label="item.label" :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="供应商:" prop="customer"> |
| | | <el-select v-model="productForm.customer" |
| | | placeholder="请选择" |
| | | clearable |
| | | filterable |
| | | :disabled="!supplierOptions.length"> |
| | | <el-option v-for="item in supplierOptions" :key="item.value" :label="item.label" :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="单位:" prop="unit"> |
| | |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </FormDialog> |
| | | <!-- 导入弹窗 --> |
| | | <FormDialog |
| | | v-model="importUpload.open" |
| | | :title="importUpload.title" |
| | | :width="'600px'" |
| | | @close="importUpload.open = false" |
| | | @confirm="submitImportFile" |
| | | @cancel="importUpload.open = false" |
| | | > |
| | | <el-upload |
| | | ref="importUploadRef" |
| | | :limit="1" |
| | | accept=".xlsx,.xls" |
| | | :action="importUpload.url" |
| | | :headers="importUpload.headers" |
| | | :before-upload="importUpload.beforeUpload" |
| | | :on-success="importUpload.onSuccess" |
| | | :on-error="importUpload.onError" |
| | | :on-progress="importUpload.onProgress" |
| | | :on-change="importUpload.onChange" |
| | | :auto-upload="false" |
| | | drag |
| | | > |
| | | <i class="el-icon-upload"></i> |
| | | <div class="el-upload__text"> |
| | | 将文件拖到此处,或<em>点击上传</em> |
| | | </div> |
| | | <template #tip> |
| | | <div class="el-upload__tip"> |
| | | 仅支持 xls/xlsx,大小不超过 10MB。 |
| | | <el-button link type="primary" @click="downloadTemplate">下载导入模板</el-button> |
| | | </div> |
| | | </template> |
| | | </el-upload> |
| | | </FormDialog> |
| | | <!-- 附件列表弹窗 --> |
| | | <FileListDialog |
| | |
| | | <th>产品名称</th> |
| | | <th>规格型号</th> |
| | | <th>单位</th> |
| | | <th>UID码</th> |
| | | <th>单价</th> |
| | | <th>零售数量</th> |
| | | <th>零售金额</th> |
| | |
| | | <td>{{ product.productCategory || '' }}</td> |
| | | <td>{{ product.specificationModel || '' }}</td> |
| | | <td>{{ product.unit || '' }}</td> |
| | | <td>{{ product.uidNo || '' }}</td> |
| | | <td>{{ product.taxInclusiveUnitPrice || '0' }}</td> |
| | | <td>{{ product.quantity || '0' }}</td> |
| | | <td>{{ product.taxInclusiveTotalPrice || '0' }}</td> |
| | |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue'; |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { |
| | | ledgerListPage, |
| | | productList, |
| | | customerList, |
| | | addOrUpdateSalesLedger, |
| | | getSalesLedgerWithProducts, |
| | | delLedger, |
| | | addOrUpdateSalesLedgerProduct, |
| | | delProduct, |
| | | delLedgerFile, getProductInventory, |
| | | ledgerListPage, |
| | | productList, |
| | | customerList, |
| | | addOrUpdateSalesLedger, |
| | | getSalesLedgerWithProducts, |
| | | delLedger, |
| | | addOrUpdateSalesLedgerProduct, |
| | | delProduct, |
| | | delLedgerFile, getProductInventory, saleOutboundExport, |
| | | } from "@/api/salesManagement/salesLedger.js"; |
| | | import { modelList, productTreeList } from "@/api/basicData/product.js"; |
| | | import { getStockInventoryAll } from "@/api/inventoryManagement/stockInventory.js"; |
| | | import useFormData from "@/hooks/useFormData.js"; |
| | | import dayjs from "dayjs"; |
| | | import { getCurrentDate } from "@/utils/index.js"; |
| | | // 由 /stockInventory/getStockInventoryAll 驱动“批号/供应商”联动 |
| | | import {safeTrainingExport} from "@/api/safeProduction/safetyTrainingAssessment.js"; |
| | | |
| | | const userStore = useUserStore(); |
| | | const { proxy } = getCurrentInstance(); |
| | |
| | | const customerOption = ref([]); |
| | | const productOptions = ref([]); |
| | | const modelOptions = ref([]); |
| | | const supplierOptions = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | |
| | | customerId: "", |
| | | entryPerson: "", |
| | | entryDate: "", |
| | | deliveryDate: "", |
| | | maintenanceTime: "", |
| | | productData: [], |
| | | executionDate: "", |
| | |
| | | customerId: [{ required: true, message: "请选择", trigger: "change" }], |
| | | entryPerson: [{ required: true, message: "请选择", trigger: "change" }], |
| | | entryDate: [{ required: true, message: "请选择", trigger: "change" }], |
| | | deliveryDate: [{ required: true, message: "请选择", trigger: "change" }], |
| | | executionDate: [{ required: true, message: "请选择", trigger: "change" }], |
| | | }, |
| | | }); |
| | |
| | | const productFormData = reactive({ |
| | | productForm: { |
| | | productCategory: "", |
| | | customer: "", |
| | | specificationModel: "", |
| | | uidNo: "", |
| | | unit: "", |
| | | quantity: "", |
| | | taxInclusiveUnitPrice: "", |
| | |
| | | taxInclusiveTotalPrice: "", |
| | | taxExclusiveTotalPrice: "", |
| | | invoiceType: "", |
| | | batchNo: "", |
| | | }, |
| | | productRules: { |
| | | productCategory: [{ required: true, message: "请选择", trigger: "change" }], |
| | | productModelId: [{ required: true, message: "请选择", trigger: "change" }], |
| | | batchNo: [{ required: true, message: "请选择", trigger: "change" }], |
| | | customer: [{ required: true, message: "请选择", trigger: "change" }], |
| | | specificationModel: [ |
| | | { required: true, message: "请选择", trigger: "change" }, |
| | | ], |
| | |
| | | const quotationSearchForm = reactive({ |
| | | quotationNo: "", |
| | | customer: "", |
| | | }); |
| | | // 报价单弹框分页 |
| | | const quotationPage = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | const selectedQuotation = ref(null); |
| | | |
| | |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | // 获取产品大类tree数据 |
| | | const getProductOptions = () => { |
| | | let stockInventoryAllTree = []; |
| | | let batchNodeByBatchNo = new Map(); |
| | | |
| | | const normalizeStockInventoryTree = (nodes = []) => { |
| | | const normalizeNodeValue = (node) => { |
| | | // 后端有时会出现 id=null 的层级,这里给一个可用的 key |
| | | if (node?.id !== null && node?.id !== undefined) return String(node.id); |
| | | if (node?.nodeType === "batch") return String(node.batchNo ?? node.label ?? ""); |
| | | if (node?.nodeType === "customer") return String(node.customer ?? node.label ?? ""); |
| | | if (node?.nodeType === "model") return String(node.model ?? node.label ?? ""); |
| | | return String(node.productName ?? node.label ?? ""); |
| | | }; |
| | | |
| | | const normalized = (list) => |
| | | (list || []).map((n) => { |
| | | const value = normalizeNodeValue(n); |
| | | const label = n.label ?? n.productName ?? n.model ?? n.batchNo ?? n.customer ?? ""; |
| | | return { |
| | | ...n, |
| | | value, |
| | | label, |
| | | children: normalized(n.children), |
| | | }; |
| | | }); |
| | | |
| | | return normalized(nodes); |
| | | }; |
| | | |
| | | // 仅展示最多 3 个层级:第 1 层(product) -> 第 2 层(model) -> 第 3 层(batch),更深的节点不展示 |
| | | const filterStockInventoryFirst3Levels = (nodes = []) => { |
| | | const MAX_LEVEL = 3; |
| | | |
| | | const cloneAndFilterByLevel = (list = [], level = 1) => { |
| | | return (list || []) |
| | | .map((n) => { |
| | | // 后续层级里如果还有 customer,直接剔除 |
| | | if (n.nodeType === "customer") return null; |
| | | |
| | | // 到达展示深度后,不再向下挂子节点 |
| | | if (level >= MAX_LEVEL) { |
| | | return { ...n, children: [] }; |
| | | } |
| | | |
| | | // 特例:batch 节点本身也不再展示 children(保持与接口节点语义一致) |
| | | if (n.nodeType === "batch") { |
| | | return { ...n, children: [] }; |
| | | } |
| | | |
| | | return { ...n, children: cloneAndFilterByLevel(n.children, level + 1) }; |
| | | }) |
| | | .filter(Boolean); |
| | | }; |
| | | |
| | | return cloneAndFilterByLevel(nodes, 1); |
| | | }; |
| | | |
| | | const findNodeObjByValue = (nodes = [], value) => { |
| | | for (let i = 0; i < (nodes || []).length; i++) { |
| | | const node = nodes[i]; |
| | | if (String(node?.value) === String(value)) return node; |
| | | const children = node?.children || []; |
| | | if (children.length) { |
| | | const found = findNodeObjByValue(children, value); |
| | | if (found) return found; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | // 获取库存树(用于产品大类/规格型号联动) |
| | | const getProductOptions = async () => { |
| | | // 返回 Promise,便于在编辑产品时等待加载完成 |
| | | return productTreeList().then((res) => { |
| | | productOptions.value = convertIdToValue(res); |
| | | return productOptions.value; |
| | | }); |
| | | const res = await getStockInventoryAll(); |
| | | const data = res?.data || []; |
| | | stockInventoryAllTree = normalizeStockInventoryTree(data); |
| | | productOptions.value = filterStockInventoryFirst3Levels(stockInventoryAllTree); |
| | | return productOptions.value; |
| | | }; |
| | | const formattedNumber = (row, column, cellValue) => { |
| | | return parseFloat(cellValue).toFixed(2); |
| | | }; |
| | | // 获取tree子数据 |
| | | // 获取tree子数据(先选产品,再选规格型号) |
| | | const getModels = (value) => { |
| | | productForm.value.productCategory = findNodeById(productOptions.value, value); |
| | | modelList({ id: value }).then((res) => { |
| | | modelOptions.value = res; |
| | | }); |
| | | const node = findNodeObjByValue(stockInventoryAllTree, value); |
| | | if (!node) return; |
| | | if (node.nodeType !== "product") return; |
| | | |
| | | // 选择产品后,重置下游字段 |
| | | productForm.value.productCategory = node.label; |
| | | modelOptions.value = (node.children || []) |
| | | .filter((c) => c.nodeType === "model") |
| | | .map((m) => ({ |
| | | id: m.value, |
| | | model: m.model ?? m.label ?? "", |
| | | unit: m.unit ?? "", |
| | | uidNo: m.uidNo ?? m.identifierCode ?? "", |
| | | })); |
| | | |
| | | productForm.value.productModelId = null; |
| | | productForm.value.specificationModel = ""; |
| | | productForm.value.uidNo = ""; |
| | | productForm.value.unit = ""; |
| | | productForm.value.batchNo = ""; |
| | | productForm.value.customer = ""; |
| | | productForm.value.taxInclusiveUnitPrice = ""; |
| | | productForm.value.taxInclusiveTotalPrice = ""; |
| | | productForm.value.taxExclusiveTotalPrice = ""; |
| | | |
| | | modelOptions.value = modelOptions.value || []; |
| | | batchNoOptions.value = []; |
| | | supplierOptions.value = []; |
| | | batchNodeByBatchNo = new Map(); |
| | | }; |
| | | |
| | | // 规格型号选择后:回显 UID,并生成“批号下拉” |
| | | const getProductModel = (value) => { |
| | | const index = modelOptions.value.findIndex((item) => item.id === value); |
| | | if (index !== -1) { |
| | | productForm.value.specificationModel = modelOptions.value[index].model; |
| | | productForm.value.unit = modelOptions.value[index].unit; |
| | | const modelNode = findNodeObjByValue(stockInventoryAllTree, value); |
| | | if (!modelNode || modelNode.nodeType !== "model") return; |
| | | |
| | | const prevBatchNo = productForm.value.batchNo; |
| | | const prevCustomer = productForm.value.customer; |
| | | |
| | | productForm.value.specificationModel = modelNode.model ?? modelNode.label ?? ""; |
| | | // 有些接口/树数据里可能不包含 unit,这种情况下不要覆盖编辑时已回显的值 |
| | | const nextUnit = modelNode.unit ?? ""; |
| | | if (nextUnit !== null && nextUnit !== undefined && String(nextUnit).trim() !== "") { |
| | | productForm.value.unit = nextUnit; |
| | | } |
| | | // 有些接口/树数据里可能不包含 uidNo,这种情况下不要覆盖编辑时已回显的值 |
| | | const nextUidNo = modelNode.uidNo ?? modelNode.identifierCode ?? ""; |
| | | if (nextUidNo !== null && nextUidNo !== undefined && String(nextUidNo).trim() !== "") { |
| | | productForm.value.uidNo = nextUidNo; |
| | | } |
| | | |
| | | const batchNodes = (modelNode.children || []).filter((b) => b.nodeType === "batch"); |
| | | batchNodeByBatchNo = new Map( |
| | | batchNodes.map((b) => { |
| | | const key = String(b.batchNo ?? b.value ?? b.label ?? "").trim(); |
| | | return [key, b]; |
| | | }) |
| | | ); |
| | | batchNoOptions.value = batchNodes.map((b) => ({ |
| | | label: String(b.batchNo ?? b.label ?? "").trim(), |
| | | value: String(b.batchNo ?? b.value ?? b.label ?? "").trim(), |
| | | })); |
| | | |
| | | // 批号不再属于新规格时,清空 |
| | | const batchValues = new Set(batchNoOptions.value.map((x) => x.value)); |
| | | if (!prevBatchNo || !batchValues.has(prevBatchNo)) { |
| | | productForm.value.batchNo = ""; |
| | | } |
| | | |
| | | // 需要供应商:批号回显后再生成 |
| | | productForm.value.customer = ""; |
| | | supplierOptions.value = []; |
| | | if (productForm.value.batchNo) { |
| | | handleBatchNoChange(productForm.value.batchNo, prevCustomer); |
| | | } |
| | | }; |
| | | |
| | | const handleBatchNoChange = (batchNo, prevCustomer) => { |
| | | const safeBatchNo = String(batchNo ?? "").trim(); |
| | | if (!safeBatchNo || !batchNodeByBatchNo.size) { |
| | | productForm.value.customer = ""; |
| | | supplierOptions.value = []; |
| | | return; |
| | | } |
| | | |
| | | const batchNode = batchNodeByBatchNo.get(String(safeBatchNo)); |
| | | if (!batchNode) { |
| | | productForm.value.customer = ""; |
| | | supplierOptions.value = []; |
| | | return; |
| | | } |
| | | |
| | | // UID码可能来源于 batch 节点(不同接口字段名不一致时尽量兜底) |
| | | const nextUidNo = batchNode.uidNo ?? batchNode.identifierCode ?? batchNode.uid ?? ""; |
| | | if (nextUidNo !== null && nextUidNo !== undefined && String(nextUidNo).trim() !== "") { |
| | | productForm.value.uidNo = nextUidNo; |
| | | } |
| | | |
| | | const customers = (batchNode.children || []) |
| | | .filter((c) => c.nodeType === "customer") |
| | | .map((c) => c.customer ?? c.label ?? "") |
| | | .filter(Boolean); |
| | | |
| | | const uniq = Array.from(new Set(customers)); |
| | | supplierOptions.value = uniq.map((s) => ({ label: s, value: s })); |
| | | |
| | | // 编辑场景尽量回显;新增场景不回显 |
| | | if (prevCustomer && uniq.includes(prevCustomer)) { |
| | | productForm.value.customer = prevCustomer; |
| | | } else { |
| | | productForm.value.specificationModel = null; |
| | | productForm.value.unit = null; |
| | | productForm.value.customer = ""; |
| | | } |
| | | }; |
| | | const findNodeById = (nodes, productId) => { |
| | | for (let i = 0; i < nodes.length; i++) { |
| | | if (nodes[i].value === productId) { |
| | | return nodes[i].label; // 找到节点,返回该节点 |
| | | } |
| | | if (nodes[i].children && nodes[i].children.length > 0) { |
| | | const foundNode = findNodeById(nodes[i].children, productId); |
| | | if (foundNode) { |
| | | return foundNode; // 在子节点中找到,返回该节点 |
| | | } |
| | | } |
| | | } |
| | | return null; // 没有找到节点,返回null |
| | | }; |
| | | function convertIdToValue(data) { |
| | | return data.map((item) => { |
| | | const { id, children, ...rest } = item; |
| | | const newItem = { |
| | | ...rest, |
| | | value: id, // 将 id 改为 value |
| | | }; |
| | | if (children && children.length > 0) { |
| | | newItem.children = convertIdToValue(children); |
| | | } |
| | | |
| | | return newItem; |
| | | }); |
| | | } |
| | | // 根据名称反查产品大类 id,便于仅存名称时的反显 |
| | | function findNodeIdByLabel(nodes, label) { |
| | | if (!label) return null; |
| | |
| | | } else { |
| | | expandedRowKeys.value = []; |
| | | } |
| | | }; |
| | | |
| | | // 添加表行类名方法 |
| | | const tableRowClassName = ({ row }) => { |
| | | if (!row.deliveryDate) return ''; |
| | | if (row.isFh) return ''; |
| | | |
| | | const diff = row.deliveryDaysDiff; |
| | | if (diff === 15) { |
| | | return 'yellow'; |
| | | } else if (diff === 10) { |
| | | return 'pink'; |
| | | } else if (diff === 2) { |
| | | return 'purple'; |
| | | } else if (diff < 2) { |
| | | return 'red'; |
| | | } |
| | | }; |
| | | // 主表合计方法 |
| | | const summarizeMainTable = (param) => { |
| | |
| | | const openQuotationDialog = async () => { |
| | | if (operationType.value === "view") return; |
| | | quotationDialogVisible.value = true; |
| | | // 打开弹窗时重置分页到第一页 |
| | | quotationPage.current = 1; |
| | | // 先确保客户列表已加载,便于后续回填 customerId |
| | | if (!customerOption.value || customerOption.value.length === 0) { |
| | | try { |
| | |
| | | quotationLoading.value = true; |
| | | try { |
| | | const params = { |
| | | // 兼容后端分页字段:这里沿用报价页面已有可用的字段命名 |
| | | currentPage: 1, |
| | | pageSize: 100, |
| | | // 后端分页字段:current / size |
| | | current: quotationPage.current, |
| | | size: quotationPage.size, |
| | | ...quotationSearchForm, |
| | | status: "通过", |
| | | }; |
| | | const res = await getQuotationList(params); |
| | | quotationList.value = res?.data?.records || []; |
| | | quotationPage.total = res?.data?.total || 0; |
| | | } finally { |
| | | quotationLoading.value = false; |
| | | } |
| | |
| | | const resetQuotationSearch = async () => { |
| | | quotationSearchForm.quotationNo = ""; |
| | | quotationSearchForm.customer = ""; |
| | | quotationPage.current = 1; |
| | | await fetchQuotationList(); |
| | | }; |
| | | |
| | | // 报价单弹框分页切换 |
| | | const quotationPaginationChange = (obj) => { |
| | | quotationPage.current = obj.page; |
| | | quotationPage.size = obj.limit; |
| | | fetchQuotationList(); |
| | | }; |
| | | |
| | | // 选中报价单后回填到台账表单 |
| | |
| | | selectedQuotation.value = row; |
| | | |
| | | // 业务员 |
| | | form.value.salesman = row.salesperson || ""; |
| | | form.value.salesman = (row.salesperson || "").trim(); |
| | | |
| | | // 客户名称 -> customerId |
| | | const customer = (customerOption.value || []).find((c) => c.customerName === row.customer); |
| | | const qCustomerName = String(row.customer || "").trim(); |
| | | const customer = (customerOption.value || []).find((c) => { |
| | | const name = String(c.customerName || "").trim(); |
| | | return name === qCustomerName || name.includes(qCustomerName) || qCustomerName.includes(name); |
| | | }); |
| | | if (customer?.id) { |
| | | form.value.customerId = customer.id; |
| | | } else { |
| | |
| | | // 台账字段 |
| | | productCategory: p.product || p.productName || "", |
| | | specificationModel: p.specification || "", |
| | | uidNo: p.uidNo || "", |
| | | unit: p.unit || "", |
| | | quantity: quantity, |
| | | taxRate: taxRate, |
| | |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const batchNoOptions = ref([]); |
| | | // 关闭弹框 |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | |
| | | const productIndex = ref(0); |
| | | // 打开产品弹框 |
| | | const openProductForm = async (type, row, index) => { |
| | | // 编辑时检查产品是否已发货或审核通过 |
| | | if (type === "edit" && isProductShipped(row)) { |
| | | proxy.$modal.msgWarning("已发货或审核通过的产品不能编辑"); |
| | | return; |
| | | } |
| | | |
| | | productOperationType.value = type; |
| | | productForm.value = {}; |
| | | proxy.resetForm("productFormRef"); |
| | |
| | | productIndex.value = index; |
| | | // 编辑时根据产品大类名称反查 tree 节点 id,并加载规格型号列表 |
| | | try { |
| | | const options = productOptions.value && productOptions.value.length > 0 |
| | | ? productOptions.value |
| | | : await getProductOptions(); |
| | | const categoryId = findNodeIdByLabel(options, productForm.value.productCategory); |
| | | if (categoryId) { |
| | | const models = await modelList({ id: categoryId }); |
| | | modelOptions.value = models || []; |
| | | // 根据当前规格型号名称反查并设置 productModelId,便于下拉框显示已选值 |
| | | const currentModel = (modelOptions.value || []).find( |
| | | (m) => m.model === productForm.value.specificationModel |
| | | ); |
| | | if (!productOptions.value || productOptions.value.length === 0) { |
| | | await getProductOptions(); |
| | | } |
| | | |
| | | // 回显:根据“产品大类”反查产品节点 |
| | | const categoryKey = findNodeIdByLabel(productOptions.value, productForm.value.productCategory); |
| | | if (categoryKey) { |
| | | const categoryNode = findNodeObjByValue(stockInventoryAllTree, categoryKey); |
| | | const models = (categoryNode?.children || []) |
| | | .filter((n) => n.nodeType === "model") |
| | | .map((m) => ({ |
| | | id: m.value, |
| | | model: m.model ?? m.label ?? "", |
| | | unit: m.unit ?? "", |
| | | uidNo: m.uidNo ?? m.identifierCode ?? "", |
| | | })); |
| | | modelOptions.value = models; |
| | | |
| | | // 根据当前规格型号回显 |
| | | const targetSpec = String(productForm.value.specificationModel ?? "").trim(); |
| | | const currentModel = |
| | | (models || []).find((m) => String(m.model ?? "").trim() === targetSpec) || |
| | | (models || []).find((m) => String(m.model ?? "").trim().includes(targetSpec)) || |
| | | (models || []).find((m) => targetSpec.includes(String(m.model ?? "").trim())); |
| | | if (currentModel) { |
| | | productForm.value.customer = productForm.value.customer || row.customer || row.supplierName || ""; |
| | | productForm.value.productModelId = currentModel.id; |
| | | getProductModel(currentModel.id); |
| | | } |
| | | } |
| | | } catch (e) { |
| | | // 加载失败时保持可编辑,不中断弹窗 |
| | | console.error("加载产品规格型号失败", e); |
| | | } |
| | | // 最终兜底:如果中途被重置清空,至少回显行数据里的 UID |
| | | productForm.value.uidNo = row.uidNo ?? productForm.value.uidNo ?? ""; |
| | | // 最终兜底:同样保证单位不会因树数据缺失而被覆盖为空 |
| | | productForm.value.unit = row.unit ?? productForm.value.unit ?? ""; |
| | | } else { |
| | | getProductOptions() |
| | | } |
| | | productFormVisible.value = true; |
| | | }; |
| | |
| | | proxy.$modal.msgWarning("请选择数据"); |
| | | return; |
| | | } |
| | | |
| | | // 检查是否有已发货或审核通过的产品 |
| | | const shippedProducts = productSelectedRows.value.filter(row => isProductShipped(row)); |
| | | if (shippedProducts.length > 0) { |
| | | proxy.$modal.msgWarning("已发货或审核通过的产品不能删除"); |
| | | return; |
| | | } |
| | | |
| | | if (operationType.value === "add") { |
| | | productSelectedRows.value.forEach((selectedRow) => { |
| | | const index = productData.value.findIndex( |
| | |
| | | proxy.$modal.msg("已取消"); |
| | | }); |
| | | }; |
| | | /** 判断单个产品是否已发货(根据shippingStatus判断,已发货或审核通过不可编辑和删除) */ |
| | | const isProductShipped = (product) => { |
| | | if (!product) return false; |
| | | const status = String(product.shippingStatus || "").trim(); |
| | | // 如果发货状态是"已发货"或"审核通过",则不可编辑和删除 |
| | | return status === "已发货" || status === "审核通过"; |
| | | }; |
| | | |
| | | /** 判断销售订单下是否存在已发货/发货完成的产品(不可删除) */ |
| | | const hasShippedProducts = (products) => { |
| | | if (!products || !products.length) return false; |
| | | return products.some((p) => { |
| | | const status = String(p.shippingStatus || "").trim(); |
| | | // 有发货日期或车牌号视为已发货 |
| | | if (p.shippingDate || p.shippingCarNumber) return true; |
| | | // 已进行发货、发货完成、已发货 均不可删除 |
| | | return status === "已进行发货" || status === "发货完成" || status === "已发货"; |
| | | }); |
| | | }; |
| | | |
| | | // 删除 |
| | | const handleDelete = () => { |
| | | let ids = []; |
| | | if (selectedRows.value.length > 0) { |
| | | ids = selectedRows.value.map((item) => item.id); |
| | | } else { |
| | | const handleDelete = async () => { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("请选择数据"); |
| | | return; |
| | | } |
| | | const ids = selectedRows.value.map((item) => item.id); |
| | | |
| | | // 检查是否有已进行发货或发货完成的销售订单,若有则不允许删除 |
| | | const cannotDeleteNames = []; |
| | | for (const row of selectedRows.value) { |
| | | let products = row.children && row.children.length > 0 ? row.children : null; |
| | | if (!products) { |
| | | try { |
| | | const res = await productList({ salesLedgerId: row.id, type: 1 }); |
| | | products = res.data || []; |
| | | } catch { |
| | | products = []; |
| | | } |
| | | } |
| | | if (hasShippedProducts(products)) { |
| | | cannotDeleteNames.push(row.salesContractNo || `ID:${row.id}`); |
| | | } |
| | | } |
| | | if (cannotDeleteNames.length > 0) { |
| | | proxy.$modal.msgWarning("已进行发货或发货完成的销售订单不能删除:" + cannotDeleteNames.join("、")); |
| | | return; |
| | | } |
| | | |
| | | ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "取消", |
| | |
| | | <th>产品名称</th> |
| | | <th>规格型号</th> |
| | | <th>单位</th> |
| | | <th>UID码</th> |
| | | <th>单价</th> |
| | | <th>零售数量</th> |
| | | <th>零售金额</th> |
| | |
| | | <td>${product.productCategory || ''}</td> |
| | | <td>${product.specificationModel || ''}</td> |
| | | <td>${product.unit || ''}</td> |
| | | <td>${product.uidNo || ''}</td> |
| | | <td>${product.taxInclusiveUnitPrice || '0'}</td> |
| | | <td>${product.quantity || '0'}</td> |
| | | <td>${product.taxInclusiveTotalPrice || '0'}</td> |
| | |
| | | isCalculating.value = false; |
| | | }; |
| | | /** |
| | | * 获取发货状态文本 |
| | | * @param row 行数据 |
| | | */ |
| | | const getShippingStatusText = (row) => { |
| | | // 如果已发货(有发货日期或车牌号),显示"已发货" |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return '已发货'; |
| | | } |
| | | |
| | | // 获取发货状态字段 |
| | | const status = row.shippingStatus; |
| | | |
| | | // 如果状态为空或未定义,默认为"待发货" |
| | | if (status === null || status === undefined || status === '') { |
| | | return '待发货'; |
| | | } |
| | | |
| | | // 状态是字符串 |
| | | const statusStr = String(status).trim(); |
| | | const statusTextMap = { |
| | | '待发货': '待发货', |
| | | '待审核': '待审核', |
| | | '审核中': '审核中', |
| | | '审核拒绝': '审核拒绝', |
| | | '审核通过': '审核通过', |
| | | '已发货': '已发货' |
| | | }; |
| | | return statusTextMap[statusStr] || '待发货'; |
| | | }; |
| | | |
| | | /** |
| | | * 获取发货状态标签类型(颜色) |
| | | * @param row 行数据 |
| | | */ |
| | | const getShippingStatusType = (row) => { |
| | | // 如果已发货(有发货日期或车牌号),显示绿色 |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return 'success'; |
| | | } |
| | | |
| | | // 获取发货状态字段 |
| | | const status = row.shippingStatus; |
| | | |
| | | // 如果状态为空或未定义,默认为灰色(待发货) |
| | | if (status === null || status === undefined || status === '') { |
| | | return 'info'; |
| | | } |
| | | |
| | | // 状态是字符串 |
| | | const statusStr = String(status).trim(); |
| | | const typeTextMap = { |
| | | '待发货': 'info', |
| | | '待审核': 'info', |
| | | '审核中': 'warning', |
| | | '审核拒绝': 'danger', |
| | | '审核通过': 'success', |
| | | '已发货': 'success' |
| | | }; |
| | | return typeTextMap[statusStr] || 'info'; |
| | | }; |
| | | |
| | | /** |
| | | * 判断是否可以发货 |
| | | * 只有在产品状态是充足,发货状态是待发货和审核拒绝的时候才可以发货 |
| | | * @param row 行数据 |
| | | */ |
| | | const canShip = (row) => { |
| | | // 产品状态必须是充足(approveStatus === 1) |
| | | if (row.approveStatus !== 1) { |
| | | return false; |
| | | } |
| | | |
| | | // 获取发货状态 |
| | | const shippingStatus = row.shippingStatus; |
| | | |
| | | // 如果已发货(有发货日期或车牌号),不能再次发货 |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return false; |
| | | } |
| | | |
| | | // 发货状态必须是"待发货"或"审核拒绝" |
| | | const statusStr = shippingStatus ? String(shippingStatus).trim() : ''; |
| | | return statusStr === '待发货' || statusStr === '审核拒绝'; |
| | | }; |
| | | |
| | | /** |
| | | * 下载文件 |
| | | * |
| | | * @param row 下载文件的相关信息对象 |
| | |
| | | |
| | | // 打开发货弹框 |
| | | const openDeliveryForm = (row) => { |
| | | // 校验:只有产品状态为充足且未发货时才能发货 |
| | | if (row.approveStatus !== 1) { |
| | | proxy.$modal.msgWarning("产品状态不足,无法发货"); |
| | | // 检查是否可以发货 |
| | | if (!canShip(row)) { |
| | | proxy.$modal.msgWarning("只有在产品状态是充足,发货状态是待发货或审核拒绝的时候才可以发货"); |
| | | return; |
| | | } |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | proxy.$modal.msgWarning("该产品已发货,无法重复发货"); |
| | | return; |
| | | } |
| | | |
| | | currentDeliveryRow.value = row; |
| | | deliveryForm.value = { |
| | | type: "货车", |
| | |
| | | let res = await userStore.getInfo(); |
| | | currentFactoryName.value = res.user.currentFactoryName; |
| | | }; |
| | | |
| | | const exportSaleOutbound = row => { |
| | | saleOutboundExport({id: row.id}) |
| | | .then(res => { |
| | | // 创建Blob对象 |
| | | const blob = new Blob([res], { |
| | | type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
| | | }); |
| | | // 创建下载链接 |
| | | const url = window.URL.createObjectURL(blob); |
| | | const link = document.createElement("a"); |
| | | link.href = url; |
| | | link.download = `销售出库单.docx`; |
| | | |
| | | // 模拟点击下载 |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | |
| | | // 清理临时对象 |
| | | setTimeout(() => { |
| | | document.body.removeChild(link); |
| | | window.URL.revokeObjectURL(url); |
| | | }, 100); |
| | | |
| | | ElMessage.success("导出成功"); |
| | | }) |
| | | .catch(err => { |
| | | console.error("导出失败:", err); |
| | | ElMessage.error("导出失败,请重试"); |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | userListNoPage().then(res => { |
| | |
| | | margin-left: 10px; |
| | | } |
| | | |
| | | ::v-deep .yellow { |
| | | background-color: #FAF0DE; |
| | | } |
| | | |
| | | ::v-deep .pink { |
| | | background-color: #FAE1DE; |
| | | } |
| | | |
| | | ::v-deep .red { |
| | | background-color: #FAE1DE; |
| | | } |
| | | |
| | | ::v-deep .purple{ |
| | | background-color: #F4DEFA; |
| | | } |
| | | |
| | | .table_list { |
| | | margin-top: unset; |
| | | } |