| | |
| | | <el-tag v-else-if="scope.row.productStockStatus == 2" |
| | | type="success">已入库</el-tag> |
| | | <el-tag v-else-if="scope.row.productStockStatus == 0" |
| | | type="info">未出库</el-tag> |
| | | type="info">未入库</el-tag> |
| | | <el-tag v-else |
| | | type="danger">不足</el-tag> |
| | | type="danger">未入库</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <!-- <el-table-column label="发货状态" width="140" align="center"> |
| | |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | @keydown.capture="handleTabScrollFollow" |
| | | ref="formRef"> |
| | | <!-- 报价单导入入口:放在表单顶部,选择后反显客户/业务员等 --> |
| | | <el-row v-if="operationType === 'add'" |
| | |
| | | v-model="scope.row.settlePieceArea" |
| | | :min="0" |
| | | :step="1" |
| | | :precision="10" |
| | | :precision="4" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable |
| | | @change="() => handleInlineSettleAreaChange(scope.row)" /> |
| | | <span v-else>{{ scope.row.settlePieceArea ?? "" }}</span> |
| | | <span v-else>{{ scope.row.settlePieceArea ? Number(scope.row.settlePieceArea).toFixed(4) : "" }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="数量" |
| | |
| | | v-model="scope.row.actualTotalArea" |
| | | :min="0" |
| | | :step="1" |
| | | :precision="10" |
| | | :precision="4" |
| | | style="width: 100%" |
| | | placeholder="自动计算" /> |
| | | <span v-else>{{ scope.row.actualTotalArea ?? "" }}</span> |
| | | <span v-else>{{ scope.row.actualTotalArea ? Number(scope.row.actualTotalArea).toFixed(4) : "" }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="含税单价(元)" |
| | |
| | | clearable |
| | | @change="() => handleInlineUnitPriceChange(scope.row)" |
| | | @input="() => handleInlineUnitPriceChange(scope.row)" /> |
| | | <span v-else>{{ formattedNumber(null, null, scope.row.taxInclusiveUnitPrice ?? 0) }}</span> |
| | | <span v-else>{{ formattedNumber(null, null, scope.row.taxInclusiveUnitPrice) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="税率(%)" |
| | |
| | | :disabled="isProductShipped(scope.row)" |
| | | @click="editProductInline(scope.row, scope.$index)"> |
| | | 编辑 |
| | | </el-button> |
| | | <el-button link |
| | | type="primary" |
| | | size="small" |
| | | :disabled="isProductShipped(scope.row) || hasEditingProductRow()" |
| | | @click="copyProductInline(scope.row, scope.$index)"> |
| | | 复制新建 |
| | | </el-button> |
| | | <el-popover :width="560" |
| | | trigger="click" |
| | |
| | | prop="actualPieceArea"> |
| | | <el-input-number v-model="productForm.actualPieceArea" |
| | | :min="0" |
| | | :step="0.00001" |
| | | :precision="5" |
| | | :step="0.0001" |
| | | :precision="4" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable |
| | |
| | | prop="actualTotalArea"> |
| | | <el-input-number v-model="productForm.actualTotalArea" |
| | | :min="0" |
| | | :step="0.00001" |
| | | :precision="5" |
| | | :step="0.0001" |
| | | :precision="4" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable /> |
| | |
| | | prop="settlePieceArea"> |
| | | <el-input-number v-model="productForm.settlePieceArea" |
| | | :min="0" |
| | | :step="0.00001" |
| | | :precision="5" |
| | | :step="0.0001" |
| | | :precision="4" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable |
| | |
| | | prop="settleTotalArea"> |
| | | <el-input-number v-model="productForm.settleTotalArea" |
| | | :min="0" |
| | | :step="0.00001" |
| | | :precision="5" |
| | | :step="0.0001" |
| | | :precision="4" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable /> |
| | |
| | | prop="settleTotalArea"> |
| | | <el-input-number v-model="productForm.settleTotalArea" |
| | | :min="0" |
| | | :step="0.00001" |
| | | :precision="5" |
| | | :step="0.0001" |
| | | :precision="4" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable /> |
| | |
| | | |
| | | const buildLedgerQrCompositeDataUrl = row => |
| | | new Promise((resolve, reject) => { |
| | | const payload = JSON.stringify({ id: row.id }); |
| | | const payload = JSON.stringify({ |
| | | id: row.id, |
| | | salesContractNo: (row.salesContractNo ?? "").trim(), |
| | | type: "XS", |
| | | }); |
| | | QRCode.toDataURL(payload, { width: 220, margin: 2 }) |
| | | .then(qrDataUrl => { |
| | | const contract = (row.salesContractNo ?? "").trim() || "—"; |
| | |
| | | if (!ledgerQrCompositeUrl.value) return; |
| | | const a = document.createElement("a"); |
| | | a.href = ledgerQrCompositeUrl.value; |
| | | a.download = `销售销售订单二维码-${ledgerQrDownloadBaseName.value}.png`; |
| | | a.download = `销售台账二维码-${ledgerQrDownloadBaseName.value}.png`; |
| | | a.click(); |
| | | }; |
| | | |
| | |
| | | return (productData.value || []).some(r => r && r.__editing); |
| | | }; |
| | | |
| | | const buildEmptyInlineProductRow = () => ({ |
| | | id: null, |
| | | __tempKey: `__temp_${Date.now()}_${Math.random().toString(16).slice(2)}`, |
| | | __editing: true, |
| | | __isNew: true, |
| | | __productCategoryId: null, |
| | | productCategory: "", |
| | | productModelId: null, |
| | | specificationModel: "", |
| | | thickness: null, |
| | | quantity: null, |
| | | taxInclusiveUnitPrice: null, |
| | | taxRate: "", |
| | | taxInclusiveTotalPrice: null, |
| | | taxExclusiveTotalPrice: null, |
| | | invoiceType: "", |
| | | width: null, |
| | | height: null, |
| | | perimeter: null, |
| | | actualPieceArea: null, |
| | | actualTotalArea: null, |
| | | settlePieceArea: null, |
| | | settleTotalArea: null, |
| | | processRequirement: "", |
| | | remark: "", |
| | | salesProductProcessList: [], |
| | | processFlowConfigId: null, |
| | | floorCode: "", |
| | | heavyBox: "", |
| | | }); |
| | | |
| | | const addProductInline = async () => { |
| | | if (operationType.value === "view") return; |
| | | if (hasEditingProductRow()) { |
| | |
| | | } |
| | | await getProductOptions(); |
| | | await fetchOtherAmountSelectOptions(true); |
| | | const row = { |
| | | id: null, |
| | | __tempKey: `__temp_${Date.now()}_${Math.random().toString(16).slice(2)}`, |
| | | __editing: true, |
| | | __isNew: true, |
| | | __productCategoryId: null, |
| | | productCategory: "", |
| | | productModelId: null, |
| | | specificationModel: "", |
| | | thickness: null, |
| | | quantity: 0, |
| | | taxInclusiveUnitPrice: 0, |
| | | taxRate: "", |
| | | taxInclusiveTotalPrice: 0, |
| | | taxExclusiveTotalPrice: 0, |
| | | invoiceType: "", |
| | | width: 0, |
| | | height: 0, |
| | | perimeter: 0, |
| | | actualPieceArea: 0, |
| | | actualTotalArea: 0, |
| | | settlePieceArea: 0, |
| | | settleTotalArea: 0, |
| | | processRequirement: "", |
| | | remark: "", |
| | | salesProductProcessList: [], |
| | | processFlowConfigId: null, |
| | | floorCode: "", |
| | | heavyBox: "", |
| | | }; |
| | | const row = buildEmptyInlineProductRow(); |
| | | productData.value.push(row); |
| | | editingProductRow.value = row; |
| | | // 让现有的计算/其他金额逻辑复用当前行 |
| | | productForm.value = row; |
| | | }; |
| | | |
| | | const copyProductInline = async row => { |
| | | if (operationType.value === "view") return; |
| | | if (!row) return; |
| | | if (isProductShipped(row)) { |
| | | proxy.$modal.msgWarning("已发货或审核通过的产品不能复制"); |
| | | return; |
| | | } |
| | | if (hasEditingProductRow()) { |
| | | proxy.$modal.msgWarning("请先保存或取消当前编辑行"); |
| | | return; |
| | | } |
| | | await getProductOptions(); |
| | | await fetchOtherAmountSelectOptions(true); |
| | | |
| | | const copied = buildEmptyInlineProductRow(); |
| | | copied.__productCategoryId = |
| | | row.__productCategoryId ?? |
| | | findNodeIdByLabel(productOptions.value, row.productCategory) ?? |
| | | null; |
| | | copied.productCategory = row.productCategory ?? ""; |
| | | copied.productModelId = row.productModelId ?? null; |
| | | copied.specificationModel = row.specificationModel ?? ""; |
| | | copied.thickness = |
| | | row.thickness !== null && row.thickness !== undefined && row.thickness !== "" |
| | | ? Number(row.thickness) |
| | | : null; |
| | | |
| | | // 复制新建仅带出产品大类与规格型号,其他数字字段全部留空,避免出现 0.00 |
| | | copied.quantity = null; |
| | | copied.taxInclusiveUnitPrice = null; |
| | | copied.taxInclusiveTotalPrice = null; |
| | | copied.taxExclusiveTotalPrice = null; |
| | | copied.width = null; |
| | | copied.height = null; |
| | | copied.perimeter = null; |
| | | copied.actualPieceArea = null; |
| | | copied.actualTotalArea = null; |
| | | copied.settlePieceArea = null; |
| | | copied.settleTotalArea = null; |
| | | |
| | | // 复制时按“产品大类 + 规格型号名称”反查型号 id,确保下拉能正确回显 |
| | | try { |
| | | if (copied.__productCategoryId) { |
| | | const models = await modelList({ id: copied.__productCategoryId }); |
| | | modelOptions.value = models || []; |
| | | const matchedModel = (modelOptions.value || []).find( |
| | | m => m.model === copied.specificationModel |
| | | ); |
| | | copied.productModelId = matchedModel?.id ?? copied.productModelId ?? null; |
| | | } |
| | | } catch (e) { |
| | | console.error("复制时加载产品规格型号失败", e); |
| | | } |
| | | |
| | | productData.value.push(copied); |
| | | editingProductRow.value = copied; |
| | | productForm.value = copied; |
| | | }; |
| | | |
| | | const editProductInline = async (row, index) => { |
| | |
| | | const res = await productList({ salesLedgerId: id, type: 1 }); |
| | | stockProductList.value = []; |
| | | stockProductList.value = |
| | | res.data.filter(item => item.productStockStatus == 0) || []; |
| | | res.data.filter(item => item.productStockStatus == 0 || item.productStockStatus == 1) || []; |
| | | } catch (e) { |
| | | proxy?.$modal?.msgError?.("获取产品列表失败"); |
| | | } finally { |
| | |
| | | }); |
| | | }; |
| | | const formattedNumber = (row, column, cellValue) => { |
| | | return parseFloat(cellValue).toFixed(2); |
| | | if (cellValue === null || cellValue === undefined || cellValue === "") { |
| | | return ""; |
| | | } |
| | | const num = Number(cellValue); |
| | | return Number.isFinite(num) ? num.toFixed(2) : ""; |
| | | }; |
| | | |
| | | const scrollElementIntoVisibleArea = target => { |
| | | if (!target || !(target instanceof HTMLElement)) return; |
| | | let parent = target.parentElement; |
| | | while (parent && parent !== document.body) { |
| | | const style = window.getComputedStyle(parent); |
| | | const canScrollX = |
| | | (style.overflowX === "auto" || |
| | | style.overflowX === "scroll" || |
| | | style.overflowX === "overlay") && |
| | | parent.scrollWidth > parent.clientWidth; |
| | | const canScrollY = |
| | | (style.overflowY === "auto" || |
| | | style.overflowY === "scroll" || |
| | | style.overflowY === "overlay") && |
| | | parent.scrollHeight > parent.clientHeight; |
| | | |
| | | if (canScrollX || canScrollY) { |
| | | const parentRect = parent.getBoundingClientRect(); |
| | | const targetRect = target.getBoundingClientRect(); |
| | | if (canScrollX) { |
| | | const targetCenterX = targetRect.left + targetRect.width / 2; |
| | | const parentCenterX = parentRect.left + parentRect.width / 2; |
| | | const deltaX = targetCenterX - parentCenterX; |
| | | if (Math.abs(deltaX) > 2) { |
| | | parent.scrollLeft += deltaX; |
| | | } |
| | | } |
| | | |
| | | if (canScrollY) { |
| | | const targetCenterY = targetRect.top + targetRect.height / 2; |
| | | const parentCenterY = parentRect.top + parentRect.height / 2; |
| | | const deltaY = targetCenterY - parentCenterY; |
| | | if (Math.abs(deltaY) > 2) { |
| | | parent.scrollTop += deltaY; |
| | | } |
| | | } |
| | | } |
| | | |
| | | parent = parent.parentElement; |
| | | } |
| | | }; |
| | | |
| | | const handleTabScrollFollow = e => { |
| | | if (!e || e.key !== "Tab") return; |
| | | requestAnimationFrame(() => { |
| | | const active = document.activeElement; |
| | | if (active instanceof HTMLElement) { |
| | | scrollElementIntoVisibleArea(active); |
| | | } |
| | | }); |
| | | }; |
| | | // 获取tree子数据 |
| | | const getModels = value => { |
| | |
| | | try { |
| | | const res = await getSalesInvoices(selectedIds); |
| | | const salesInvoiceData = res?.data ?? {}; |
| | | printSalesDeliveryNote(salesInvoiceData, selectedRow); |
| | | await printSalesDeliveryNote(salesInvoiceData, selectedRow, selectedIds); |
| | | } catch (error) { |
| | | console.error("打印销售发货单失败:", error); |
| | | proxy.$modal.msgError("打印失败,请稍后重试"); |
| | |
| | | } else { |
| | | const res = await getProcessCard(selectedId); |
| | | const processCardData = res?.data ?? {}; |
| | | // 补齐二维码所需的台账标识(后端数据有时不带 id) |
| | | if (processCardData && typeof processCardData === "object") { |
| | | processCardData.salesLedgerId = processCardData.salesLedgerId ?? selectedId; |
| | | processCardData.salesContractNo = |
| | | (processCardData.salesContractNo ?? "").trim() || |
| | | String(selectedRow?.salesContractNo ?? "").trim(); |
| | | } |
| | | const routeNodes = processCardData?.routeNodes; |
| | | const isProcessRouteEmpty = |
| | | !Array.isArray(routeNodes) || routeNodes.length === 0; |
| | |
| | | } catch { |
| | | return; |
| | | } |
| | | printFinishedProcessCard(processCardData); |
| | | await printFinishedProcessCard(processCardData); |
| | | } else { |
| | | printFinishedProcessCard(processCardData); |
| | | await printFinishedProcessCard(processCardData); |
| | | } |
| | | } |
| | | } catch (error) { |
| | |
| | | return statusStr === "待发货" || statusStr === "审核拒绝"; |
| | | }; |
| | | |
| | | const getLedgerDisplayName = ledger => |
| | | String(ledger?.salesContractNo || "").trim() || |
| | | String(ledger?.projectName || "").trim() || |
| | | `ID:${ledger?.id ?? "-"}`; |
| | | |
| | | const validateLedgersStockedBeforeDelivery = async ledgers => { |
| | | const invalidLedgers = []; |
| | | for (const ledger of ledgers || []) { |
| | | const ledgerId = ledger?.id; |
| | | const ledgerName = getLedgerDisplayName(ledger); |
| | | if (!ledgerId) { |
| | | invalidLedgers.push(`${ledgerName}(缺少台账ID)`); |
| | | continue; |
| | | } |
| | | let products = []; |
| | | try { |
| | | const res = await productList({ salesLedgerId: ledgerId, type: 1 }); |
| | | products = Array.isArray(res?.data) ? res.data : []; |
| | | } catch (e) { |
| | | invalidLedgers.push(`${ledgerName}(明细加载失败)`); |
| | | continue; |
| | | } |
| | | const unstockedProducts = products.filter( |
| | | item => Number(item?.productStockStatus) !== 2 |
| | | ); |
| | | if (unstockedProducts.length > 0) { |
| | | invalidLedgers.push( |
| | | `${ledgerName}(未全部入库${unstockedProducts.length}条)` |
| | | ); |
| | | } |
| | | } |
| | | return invalidLedgers; |
| | | }; |
| | | |
| | | const handleBulkDelivery = async () => { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("请选择数据"); |
| | |
| | | |
| | | // 只允许【未发货/审批失败】进入发货流程 |
| | | const statusItem = selectedRows.value[0].deliveryStatus; |
| | | const isTrue = true; |
| | | let isTrue = true; |
| | | selectedRows.value.forEach(row => { |
| | | if (row.deliveryStatus != 1 && row.deliveryStatus != 3) { |
| | | proxy.$modal.msgWarning("仅未发货或审批失败的台账可以发货"); |
| | |
| | | } |
| | | }); |
| | | if (!isTrue) { |
| | | return; |
| | | } |
| | | |
| | | proxy.$modal.loading("正在校验明细入库状态,请稍候..."); |
| | | const invalidLedgers = await validateLedgersStockedBeforeDelivery( |
| | | selectedRows.value |
| | | ); |
| | | proxy.$modal.closeLoading(); |
| | | if (invalidLedgers.length > 0) { |
| | | try { |
| | | await ElMessageBox.alert( |
| | | `以下销售台账存在未全部入库的明细,暂不可发货:\n${invalidLedgers.join( |
| | | "\n" |
| | | )}`, |
| | | "提示", |
| | | { |
| | | type: "warning", |
| | | confirmButtonText: "知道了", |
| | | } |
| | | ); |
| | | } catch { |
| | | /* 关闭弹窗 */ |
| | | } |
| | | return; |
| | | } |
| | | |
| | |
| | | }; |
| | | |
| | | // 打开发货弹框(单条) |
| | | const openDeliveryForm = row => { |
| | | const openDeliveryForm = async row => { |
| | | // 只允许【未发货/审批失败】发货;已发货/审批中不允许 |
| | | const status = Number(row.deliveryStatus); |
| | | if (status !== 1 && status !== 3) { |
| | |
| | | return; |
| | | } |
| | | |
| | | proxy.$modal.loading("正在校验明细入库状态,请稍候..."); |
| | | const invalidLedgers = await validateLedgersStockedBeforeDelivery([row]); |
| | | proxy.$modal.closeLoading(); |
| | | if (invalidLedgers.length > 0) { |
| | | try { |
| | | await ElMessageBox.alert( |
| | | `当前销售台账存在未全部入库的明细,暂不可发货:\n${invalidLedgers[0]}`, |
| | | "提示", |
| | | { |
| | | type: "warning", |
| | | confirmButtonText: "知道了", |
| | | } |
| | | ); |
| | | } catch { |
| | | /* 关闭弹窗 */ |
| | | } |
| | | return; |
| | | } |
| | | |
| | | currentDeliveryRows.value = [row]; |
| | | deliveryForm.value = { |
| | | type: "货车", |