5 天以前 193377e7fb0bbbcfe89dda4274abbabeb293ae07
feat(salesLedger): 新增库存不足时自动生成生产主计划功能

- 在销售台账发货界面添加库存不足提示和生产计划生成入口
- 实现自动计算库存缺口并设置默认计划生产数量
- 集成生产主计划创建和下发流程,支持一键生成并下发
- 优化框架订单列表弹窗UI,增加订单统计摘要信息
- 添加库存表格空状态提示文本
- 增强框架订单产品明细展示和发货状态标识
已修改1个文件
574 ■■■■■ 文件已修改
src/views/salesManagement/salesLedger/index.vue 574 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue
@@ -1151,6 +1151,7 @@
                border
                size="small"
                max-height="260"
                empty-text="暂无可用库存,可在下方生成生产主计划补充"
                style="width: 100%"
              >
                <el-table-column label="批号" prop="batchNo" min-width="180" />
@@ -1193,6 +1194,63 @@
                  </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>
@@ -1258,50 +1316,162 @@
    <!-- 框架订单列表弹窗 -->
    <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>
@@ -1340,6 +1510,11 @@
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")
@@ -1562,6 +1737,30 @@
  },
});
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);
@@ -2994,6 +3193,15 @@
    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;
};
@@ -3088,6 +3296,160 @@
  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);
@@ -3098,6 +3460,15 @@
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;
@@ -3144,22 +3515,34 @@
  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;
  });
@@ -3171,6 +3554,7 @@
  openDeliveryForm({
    ...productRow,
    salesLedgerId: orderRow.id,
    salesContractNo: orderRow.salesContractNo,
  });
};
const currentFactoryName = ref("");
@@ -3369,6 +3753,98 @@
  }
}
/* 框架订单列表弹窗 */
.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;