| | |
| | | </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" |
| | |
| | | <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> |
| | |
| | | </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"> |
| | |
| | | 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(); |
| | | }; |
| | | |
| | | // 选中报价单后回填到台账表单 |
| | |
| | | // 台账字段 |
| | | 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() |
| | | } |
| | |
| | | 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: "取消", |
| | |
| | | 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; |
| | | } |