| | |
| | | border |
| | | size="small" |
| | | max-height="260" |
| | | empty-text="暂无可用库存,可在下方生成生产主计划补充" |
| | | style="width: 100%" |
| | | > |
| | | <el-table-column label="批号" prop="batchNo" min-width="180" /> |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <!-- 库存不足时:自动生成生产主计划并下发 --> |
| | | <el-row :gutter="30" v-if="isStockInsufficient"> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <el-alert |
| | | type="warning" |
| | | :closable="false" |
| | | show-icon |
| | | style="width: 100%; margin-bottom: 12px" |
| | | > |
| | | <template #title> |
| | | <template v-if="pendingNeed > 0"> |
| | | 库存不足:可用库存合计 |
| | | <strong>{{ availableStockTotal }}</strong> ,待发货数量 |
| | | <strong>{{ pendingNeed }}</strong> ,缺口 |
| | | <strong>{{ pendingNeed - availableStockTotal }}</strong> |
| | | ,可生成生产主计划并自动下发 |
| | | </template> |
| | | <template v-else> |
| | | 当前暂无可用库存(可用 |
| | | <strong>{{ availableStockTotal }}</strong> |
| | | ),可生成生产主计划并自动下发 |
| | | </template> |
| | | </template> |
| | | </el-alert> |
| | | <div |
| | | style=" |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | width: 100%; |
| | | padding: 12px 16px; |
| | | background: #fdf6ec; |
| | | border-radius: 4px; |
| | | " |
| | | > |
| | | <span style="white-space: nowrap; font-size: 14px" |
| | | >计划生产数量:</span |
| | | > |
| | | <el-input-number |
| | | v-model="planQuantity" |
| | | :min="0.01" |
| | | :precision="2" |
| | | :step="1" |
| | | controls-position="right" |
| | | style="width: 180px" |
| | | /> |
| | | <el-button |
| | | type="warning" |
| | | :loading="generatePlanLoading" |
| | | @click="handleGenerateAndDispatchPlan" |
| | | >生成生产主计划并下发</el-button |
| | | > |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | <!-- 框架订单列表弹窗 --> |
| | | <el-dialog |
| | | v-model="frameworkOrderListVisible" |
| | | title="框架订单列表" |
| | | width="80%" |
| | | top="6vh" |
| | | :close-on-click-modal="false" |
| | | class="framework-order-dialog" |
| | | > |
| | | <el-alert |
| | | v-if="frameworkContractShippedQty !== null" |
| | | type="info" |
| | | :closable="false" |
| | | style="margin-bottom: 12px" |
| | | :title="'父框架合同累计已发货数量:' + frameworkContractShippedQty" |
| | | /> |
| | | <el-table :data="frameworkOrderList" border :row-key="(row) => row.id"> |
| | | <template #header> |
| | | <div class="fo-header"> |
| | | <span class="fo-header__title">框架订单列表</span> |
| | | <span class="fo-header__sub"> |
| | | 父合同:{{ currentFrameworkContract?.salesContractNo || "--" }} |
| | | </span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="fo-summary"> |
| | | <div class="fo-summary__item"> |
| | | <div class="fo-summary__label">订单数</div> |
| | | <div class="fo-summary__value"> |
| | | {{ frameworkOrderListLoading ? "--" : frameworkOrderList.length }} |
| | | </div> |
| | | </div> |
| | | <div class="fo-summary__divider" /> |
| | | <div class="fo-summary__item"> |
| | | <div class="fo-summary__label">产品数</div> |
| | | <div class="fo-summary__value"> |
| | | {{ frameworkOrderListLoading ? "--" : frameworkOrderProductCount }} |
| | | </div> |
| | | </div> |
| | | <div class="fo-summary__divider" /> |
| | | <div class="fo-summary__item"> |
| | | <div class="fo-summary__label">累计已发货数量</div> |
| | | <div class="fo-summary__value fo-summary__value--accent"> |
| | | {{ frameworkContractShippedQty ?? "--" }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-table |
| | | v-loading="frameworkOrderListLoading" |
| | | :data="frameworkOrderList" |
| | | :row-key="(row) => row.id" |
| | | :expand-row-keys="frameworkOrderExpandedKeys" |
| | | class="fo-table" |
| | | > |
| | | <el-table-column type="expand"> |
| | | <template #default="props"> |
| | | <el-table :data="props.row.products" border size="small"> |
| | | <el-table-column label="产品大类" prop="productCategory" /> |
| | | <el-table-column label="规格型号" prop="specificationModel" /> |
| | | <el-table-column label="单位" prop="unit" /> |
| | | <el-table-column label="操作" width="80" align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | @click="shipFrameworkOrderProduct(scope.row, props.row)" |
| | | >发货 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="fo-expand"> |
| | | <div class="fo-expand__title">产品明细</div> |
| | | <el-table :data="props.row.products" border size="small"> |
| | | <el-table-column |
| | | label="产品大类" |
| | | prop="productCategory" |
| | | min-width="120" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="规格型号" |
| | | prop="specificationModel" |
| | | min-width="150" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column |
| | | label="数量" |
| | | prop="quantity" |
| | | width="90" |
| | | align="center" |
| | | > |
| | | <template #default="scope"> |
| | | {{ scope.row.quantity ?? "--" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="单位" |
| | | prop="unit" |
| | | width="80" |
| | | align="center" |
| | | /> |
| | | <el-table-column |
| | | label="交货日期" |
| | | prop="deliveryDate" |
| | | width="130" |
| | | align="center" |
| | | > |
| | | <template #default="scope"> |
| | | {{ scope.row.deliveryDate || "--" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="90" align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | @click="shipFrameworkOrderProduct(scope.row, props.row)" |
| | | >发货 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="订单编号" min-width="180"> |
| | | <template #default="scope"> |
| | | <span class="fo-order-no"> |
| | | {{ scope.row.salesContractNo || "--" }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="产品" width="90" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag size="small" type="info" effect="light"> |
| | | {{ (scope.row.products || []).length }} 项 |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="订单编号" |
| | | prop="salesContractNo" |
| | | min-width="180" |
| | | /> |
| | | <el-table-column label="项目名称" prop="projectName" min-width="160" /> |
| | | <el-table-column label="备注" prop="remarks" min-width="160" /> |
| | | label="项目名称" |
| | | prop="projectName" |
| | | min-width="160" |
| | | show-overflow-tooltip |
| | | > |
| | | <template #default="scope">{{ scope.row.projectName || "--" }}</template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="累计发货数量" |
| | | prop="shippedQuantity" |
| | | width="140" |
| | | align="center" |
| | | /> |
| | | label="备注" |
| | | prop="remarks" |
| | | min-width="160" |
| | | show-overflow-tooltip |
| | | > |
| | | <template #default="scope">{{ scope.row.remarks || "--" }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="发货状态" width="170" align="center"> |
| | | <template #default="scope"> |
| | | <el-tag |
| | | v-if="Number(scope.row.shippedQuantity) > 0" |
| | | type="success" |
| | | size="small" |
| | | effect="light" |
| | | >已发货</el-tag |
| | | > |
| | | <el-tag v-else type="info" size="small" effect="plain" |
| | | >未发货</el-tag |
| | | > |
| | | <span class="fo-qty"> |
| | | 累计 {{ scope.row.shippedQuantity ?? 0 }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <template #empty> |
| | | <el-empty |
| | | :description=" |
| | | frameworkOrderListLoading ? '加载中...' : '暂无框架订单' |
| | | " |
| | | :image-size="72" |
| | | /> |
| | | </template> |
| | | </el-table> |
| | | </el-dialog> |
| | | </div> |
| | |
| | | import ImageUpload from "@/components/AttachmentUpload/image/index.vue"; |
| | | import { getCurrentDate } from "@/utils/index.js"; |
| | | import { listCustomer } from "@/api/basicData/customer.js"; |
| | | import { |
| | | productionPlanAdd, |
| | | productionPlanCombine, |
| | | productionPlanListPage, |
| | | } from "@/api/productionPlan/productionPlan.js"; |
| | | |
| | | const FileList = defineAsyncComponent(() => |
| | | import("@/components/Dialog/FileList.vue") |
| | |
| | | }, |
| | | }); |
| | | const { deliveryForm, deliveryRules } = toRefs(deliveryFormData); |
| | | |
| | | // 发货库存不足时自动生成生产主计划相关 |
| | | // 可用库存合计(所有批次可用数量之和) |
| | | const availableStockTotal = computed(() => |
| | | (deliveryForm.value?.batchNoList || []).reduce( |
| | | (sum, item) => sum + Number(getDeliveryBatchQuantity(item) || 0), |
| | | 0 |
| | | ) |
| | | ); |
| | | // 待发货数量(框架合同等可能为空,此时视为无明确需求量) |
| | | const pendingNeed = computed(() => |
| | | Number(currentDeliveryRow.value?.noQuantity || 0) |
| | | ); |
| | | // 是否显示生成入口: |
| | | // 1) 完全没有可用库存(库存表格为空/全为0)即可生成,兼容框架合同无待发货数量的场景; |
| | | // 2) 或待发货数量 > 0 且可用库存不足以覆盖。 |
| | | const isStockInsufficient = computed(() => { |
| | | if (availableStockTotal.value <= 0) return true; |
| | | return pendingNeed.value > 0 && availableStockTotal.value < pendingNeed.value; |
| | | }); |
| | | // 计划生产数量 |
| | | const planQuantity = ref(0); |
| | | // 生成并下发按钮 loading |
| | | const generatePlanLoading = ref(false); |
| | | |
| | | // 导入相关 |
| | | const importUploadRef = ref(null); |
| | |
| | | batchNoList, |
| | | }; |
| | | deliveryFileList.value = []; |
| | | // 计算缺口,作为计划生产数量默认值 |
| | | const totalStock = batchNoList.reduce( |
| | | (sum, item) => sum + Number(getDeliveryBatchQuantity(item) || 0), |
| | | 0 |
| | | ); |
| | | planQuantity.value = Math.max( |
| | | 0, |
| | | Number(row.noQuantity || 0) - totalStock |
| | | ); |
| | | deliveryFormVisible.value = true; |
| | | }; |
| | | |
| | |
| | | currentDeliveryRow.value = null; |
| | | }; |
| | | |
| | | // 主生产计划剩余可下发数量 |
| | | const remainingOfPlan = (plan) => |
| | | Math.max(0, Number(plan?.qtyRequired || 0) - Number(plan?.quantityIssued || 0)); |
| | | |
| | | // 查询该产品(规格型号)下「尚未下发」的主生产计划列表 |
| | | // 已下发过(哪怕是部分下发)的计划不复用,下次需新建 |
| | | const fetchPendingPlans = async ({ productModelId, productName, model }) => { |
| | | const params = { current: 1, size: 200 }; |
| | | if (productName) params.productName = productName; |
| | | if (model) params.model = model; |
| | | const res = await productionPlanListPage(params); |
| | | const records = res?.data?.records || res?.data?.rows || []; |
| | | return records.filter((r) => { |
| | | const sameModel = productModelId |
| | | ? String(r.productModelId) === String(productModelId) |
| | | : !!model && r.model === model; |
| | | const notDispatched = Number(r.quantityIssued || 0) <= 0; |
| | | return sameModel && notDispatched && remainingOfPlan(r) > 0; |
| | | }); |
| | | }; |
| | | |
| | | // 取最新一条(id 最大) |
| | | const pickNewestPlan = (plans) => |
| | | plans.reduce((a, b) => (Number(b.id) > Number(a.id) ? b : a)); |
| | | |
| | | // 库存不足/无库存时:自动生成生产主计划并全量下发 |
| | | const handleGenerateAndDispatchPlan = async () => { |
| | | const qty = Number(planQuantity.value || 0); |
| | | if (qty <= 0) { |
| | | proxy.$modal.msgWarning("计划生产数量必须大于0"); |
| | | return; |
| | | } |
| | | try { |
| | | await proxy.$modal.confirm( |
| | | `当前库存不足,确认生成/下发生产主计划(计划数量 ${qty})并全量下发?` |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | |
| | | generatePlanLoading.value = true; |
| | | try { |
| | | const row = currentDeliveryRow.value || {}; |
| | | const firstBatch = (deliveryForm.value?.batchNoList || [])[0] || {}; |
| | | const productName = row.productCategory || firstBatch.productName || ""; |
| | | const model = row.specificationModel || firstBatch.model || ""; |
| | | const productModelId = row.productModelId || row.modelId || undefined; |
| | | const requiredDate = |
| | | row.deliveryDate || dayjs().add(7, "day").format("YYYY-MM-DD"); |
| | | const createTime = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | // 订单编号(销售合同号):框架订单行直接带,普通台账行从父记录取 |
| | | const salesContractNo = |
| | | row.salesContractNo || |
| | | findLedgerRecordByRow(row)?.salesContractNo || |
| | | ""; |
| | | |
| | | // productId:行/批次优先,缺失时用产品大类名称在产品树反查(与编辑产品一致) |
| | | let productId = row.productId || firstBatch.productId; |
| | | if (!productId && productName) { |
| | | const options = |
| | | productOptions.value && productOptions.value.length > 0 |
| | | ? productOptions.value |
| | | : await getProductOptions(); |
| | | productId = findNodeIdByLabel(options, productName) || undefined; |
| | | } |
| | | if (!productId || !productModelId) { |
| | | proxy.$modal.msgError( |
| | | "未识别到该产品的产品ID/规格型号ID,无法生成生产主计划,请联系管理员" |
| | | ); |
| | | return; |
| | | } |
| | | |
| | | // ① 先查该产品是否存在「尚未下发」的主生产计划可复用;已下发过的不复用,直接新建 |
| | | let target = null; |
| | | let dispatchQty = qty; |
| | | const existingPlans = await fetchPendingPlans({ |
| | | productModelId, |
| | | productName, |
| | | model, |
| | | }); |
| | | if (existingPlans.length > 0) { |
| | | target = pickNewestPlan(existingPlans); |
| | | dispatchQty = remainingOfPlan(target); |
| | | } else { |
| | | // ② 无已有计划则新建,再通过查询列表取回新计划 id(add 响应不一定带 id) |
| | | await productionPlanAdd({ |
| | | id: null, |
| | | mpsNo: "", |
| | | // 合同号通过台账关联:production_plan.sales_ledger_id → sales_ledger.sales_contract_no |
| | | salesLedgerId: row.salesLedgerId ?? null, |
| | | salesLedgerProductId: row.id ?? null, |
| | | productId, |
| | | productModelId, |
| | | productName, |
| | | model, |
| | | qtyRequired: qty, |
| | | unit: row.unit || firstBatch.unit || "方", |
| | | requiredDate, |
| | | promisedDeliveryDate: "", |
| | | remark: "销售发货自动生成", |
| | | salesContractNo, |
| | | createTime, |
| | | }); |
| | | const createdPlans = await fetchPendingPlans({ |
| | | productModelId, |
| | | productName, |
| | | model, |
| | | }); |
| | | if (createdPlans.length > 0) { |
| | | target = pickNewestPlan(createdPlans); |
| | | dispatchQty = remainingOfPlan(target) || qty; |
| | | } |
| | | } |
| | | |
| | | if (!target || !target.id) { |
| | | proxy.$modal.msgError( |
| | | "生产主计划已创建,但未能查询到计划ID,请前往【生产计划】页面手动下发" |
| | | ); |
| | | return; |
| | | } |
| | | |
| | | // ③ 全量下发 |
| | | const combineRes = await productionPlanCombine({ |
| | | productName, |
| | | model, |
| | | totalAssignedQuantity: dispatchQty, |
| | | planCompleteTime: target.requiredDate || requiredDate, |
| | | productId: target.productId || productId || "", |
| | | salesContractNo: target.salesContractNo || salesContractNo || "", |
| | | createTime, |
| | | ids: [target.id], |
| | | }); |
| | | |
| | | if (combineRes?.code === 200) { |
| | | proxy.$modal.msgSuccess("生产主计划已下发成功"); |
| | | closeDeliveryDia(); |
| | | getList(); |
| | | } else { |
| | | proxy.$modal.msgError( |
| | | `生产主计划下发失败:${ |
| | | combineRes?.msg || "未知错误" |
| | | },请前往【生产计划】页面手动下发` |
| | | ); |
| | | } |
| | | } catch (err) { |
| | | console.error("自动生成/下发生产主计划失败:", err); |
| | | if (!err?.response) { |
| | | proxy.$modal.msgError("自动生成/下发生产主计划失败,请稍后重试或手动操作"); |
| | | } |
| | | } finally { |
| | | generatePlanLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 框架合同 / 框架订单相关 |
| | | const frameworkOrderDialogVisible = ref(false); |
| | | const frameworkOrderListVisible = ref(false); |
| | |
| | | const frameworkOrderProducts = ref([]); |
| | | const frameworkOrderList = ref([]); |
| | | const frameworkContractShippedQty = ref(null); |
| | | const frameworkOrderListLoading = ref(false); |
| | | // 默认展开第一个订单 |
| | | const frameworkOrderExpandedKeys = ref([]); |
| | | const frameworkOrderProductCount = computed(() => |
| | | frameworkOrderList.value.reduce( |
| | | (sum, order) => sum + (order.products?.length || 0), |
| | | 0 |
| | | ) |
| | | ); |
| | | |
| | | const openFrameworkOrderDialog = async (row) => { |
| | | currentFrameworkContract.value = row; |
| | |
| | | currentFrameworkContract.value = row; |
| | | frameworkContractShippedQty.value = null; |
| | | frameworkOrderList.value = []; |
| | | frameworkOrderExpandedKeys.value = []; |
| | | frameworkOrderListLoading.value = true; |
| | | frameworkOrderListVisible.value = true; |
| | | listFrameworkOrders({ parentContractId: row.id }).then(async (res) => { |
| | | const orders = res.data || []; |
| | | const withProducts = []; |
| | | for (const order of orders) { |
| | | let products = []; |
| | | try { |
| | | const productRes = await productList({ salesLedgerId: order.id, type: 1 }); |
| | | products = productRes.data || []; |
| | | } catch (e) { |
| | | products = []; |
| | | listFrameworkOrders({ parentContractId: row.id }) |
| | | .then(async (res) => { |
| | | const orders = res.data || []; |
| | | const withProducts = []; |
| | | for (const order of orders) { |
| | | let products = []; |
| | | try { |
| | | const productRes = await productList({ |
| | | salesLedgerId: order.id, |
| | | type: 1, |
| | | }); |
| | | products = productRes.data || []; |
| | | } catch (e) { |
| | | products = []; |
| | | } |
| | | withProducts.push({ ...order, products }); |
| | | } |
| | | withProducts.push({ ...order, products }); |
| | | } |
| | | frameworkOrderList.value = withProducts; |
| | | }); |
| | | frameworkOrderList.value = withProducts; |
| | | if (withProducts.length > 0) { |
| | | frameworkOrderExpandedKeys.value = [withProducts[0].id]; |
| | | } |
| | | }) |
| | | .finally(() => { |
| | | frameworkOrderListLoading.value = false; |
| | | }); |
| | | frameworkContractShippedQuantity({ parentContractId: row.id }).then((res) => { |
| | | frameworkContractShippedQty.value = res.data ?? 0; |
| | | }); |
| | |
| | | openDeliveryForm({ |
| | | ...productRow, |
| | | salesLedgerId: orderRow.id, |
| | | salesContractNo: orderRow.salesContractNo, |
| | | }); |
| | | }; |
| | | const currentFactoryName = ref(""); |
| | |
| | | } |
| | | } |
| | | |
| | | /* 框架订单列表弹窗 */ |
| | | .framework-order-dialog { |
| | | .fo-header { |
| | | display: flex; |
| | | align-items: baseline; |
| | | gap: 12px; |
| | | |
| | | &__title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | &__sub { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .fo-summary { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 14px 24px; |
| | | margin-bottom: 16px; |
| | | background: linear-gradient(90deg, #eef4ff 0%, #f7fafd 100%); |
| | | border: 1px solid #e2ecfb; |
| | | border-radius: 8px; |
| | | |
| | | &__item { |
| | | flex: 1; |
| | | } |
| | | |
| | | &__label { |
| | | margin-bottom: 6px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | &__value { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | |
| | | &--accent { |
| | | color: var(--el-color-primary); |
| | | } |
| | | } |
| | | |
| | | &__divider { |
| | | width: 1px; |
| | | height: 30px; |
| | | margin: 0 24px; |
| | | background: #dce7f7; |
| | | } |
| | | } |
| | | |
| | | .fo-order-no { |
| | | font-weight: 500; |
| | | color: var(--el-color-primary); |
| | | } |
| | | |
| | | .fo-qty { |
| | | margin-left: 8px; |
| | | font-size: 12px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .fo-expand { |
| | | padding: 12px 16px 16px 48px; |
| | | background: #fafbfd; |
| | | |
| | | &__title { |
| | | margin-bottom: 8px; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | color: #606266; |
| | | } |
| | | } |
| | | |
| | | :deep(.fo-table) { |
| | | th.el-table__cell { |
| | | background-color: #f5f7fa; |
| | | color: #606266; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .el-table__expanded-cell { |
| | | padding: 0 !important; |
| | | } |
| | | } |
| | | } |
| | | |
| | | @media print { |
| | | .app-container { |
| | | display: none; |