gaoluyang
2026-09-02 9227d2b4e4aad18b48ec879b136d3cca12de5613
src/views/salesManagement/salesLedger/index.vue
@@ -117,6 +117,11 @@
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="审批中数量" width="120" align="center">
                <template #default="scope">
                  <span>{{ scope.row.pendingApprovalQuantity || 0 }}</span>
                </template>
              </el-table-column>
              <el-table-column
                label="快递公司"
                prop="expressCompany"
@@ -148,6 +153,7 @@
                </template>
              </el-table-column>
              <el-table-column label="数量" prop="quantity" />
              <el-table-column label="退货数量" prop="returnQuantity" />
              <el-table-column label="待发货数量" prop="noQuantity" />
              <el-table-column label="税率(%)" prop="taxRate" />
              <el-table-column
@@ -171,7 +177,7 @@
                  <el-button
                    link
                    type="primary"
                    :disabled="!canShip(scope.row)"
                    :disabled="!canShip(scope.row) || props.row.contractType === 2"
                    @click="openDeliveryForm(scope.row)"
                  >
                    发货
@@ -219,6 +225,13 @@
          :formatter="formattedNumber"
        />
        <el-table-column
          label="实际合同金额(元)"
          prop="netContractAmount"
          width="220"
          show-overflow-tooltip
          :formatter="formattedNumber"
        />
        <el-table-column
          label="录入人"
          prop="entryPersonName"
          width="100"
@@ -248,7 +261,7 @@
          width="200"
          show-overflow-tooltip
        />
        <el-table-column fixed="right" label="操作" width="220" align="center">
        <el-table-column fixed="right" label="操作" width="320" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="openForm('view', scope.row)"
              >详情
@@ -263,6 +276,20 @@
                !canEditLedger(scope.row)
              "
              >编辑
            </el-button>
            <el-button
              v-if="scope.row.contractType === 2"
              link
              type="success"
              @click="openFrameworkOrderDialog(scope.row)"
              >创建框架订单
            </el-button>
            <el-button
              v-if="scope.row.contractType === 2"
              link
              type="primary"
              @click="openFrameworkOrderList(scope.row)"
              >框架订单
            </el-button>
            <el-button link type="primary" @click="openFileDialog(scope.row)"
              >附件
@@ -307,6 +334,21 @@
            <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="contractType">
              <el-select
                v-model="form.contractType"
                placeholder="请选择"
                clearable
                :disabled="operationType === 'view' || operationType === 'edit'"
              >
                <el-option label="普通销售合同" :value="1" />
                <el-option label="框架合同" :value="2" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
@@ -454,7 +496,7 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
        <el-row :gutter="30" v-if="!isFrameworkContract">
          <el-col :span="12">
            <el-form-item label="交货日期:" prop="entryDate">
              <el-date-picker
@@ -737,7 +779,7 @@
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item label="税率(%):" prop="taxRate">
              <el-select
                v-model="productForm.taxRate"
@@ -770,7 +812,7 @@
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item label="数量:" prop="quantity">
              <el-input-number
                :step="0.1"
@@ -786,7 +828,7 @@
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item label="含税总价(元):" prop="taxInclusiveTotalPrice">
              <el-input
                v-model="productForm.taxInclusiveTotalPrice"
@@ -796,7 +838,7 @@
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item
              label="不含税总价(元):"
              prop="taxExclusiveTotalPrice"
@@ -811,7 +853,7 @@
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item label="发票类型:" prop="invoiceType">
              <el-select
                v-model="productForm.invoiceType"
@@ -823,7 +865,7 @@
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
          <el-col :span="12" v-if="!isFrameworkContract">
            <el-form-item label="是否生产:" prop="isProduction">
              <el-radio-group v-model="productForm.isProduction">
                <el-radio label="是" :value="true" />
@@ -1053,6 +1095,17 @@
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="发货地址:" prop="shippingAddress">
              <el-input
                v-model="deliveryForm.shippingAddress"
                placeholder="请输入发货地址"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24" v-if="deliveryForm.type === '货车'">
            <el-form-item label="发货车牌号:" prop="shippingCarNumber">
              <el-input
@@ -1153,6 +1206,104 @@
        </div>
      </template>
    </el-dialog>
    <!-- 创建框架订单弹窗 -->
    <el-dialog
      v-model="frameworkOrderDialogVisible"
      title="创建框架订单"
      width="70%"
      :close-on-click-modal="false"
    >
      <el-form label-width="120px" label-position="top">
        <el-form-item label="父框架合同:">
          <el-input
            :model-value="currentFrameworkContract?.salesContractNo"
            disabled
          />
        </el-form-item>
        <el-form-item label="备注:">
          <el-input
            v-model="frameworkOrderForm.remarks"
            type="textarea"
            :rows="2"
            placeholder="请输入备注"
          />
        </el-form-item>
        <el-form-item label="产品(填写交货日期):">
          <el-table :data="frameworkOrderProducts" border>
            <el-table-column label="产品大类" prop="productCategory" />
            <el-table-column label="规格型号" prop="specificationModel" />
            <el-table-column label="单位" prop="unit" />
            <el-table-column label="交货日期" align="center">
              <template #default="scope">
                <el-date-picker
                  v-model="scope.row.deliveryDate"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  type="date"
                  placeholder="请选择"
                  clearable
                />
              </template>
            </el-table-column>
          </el-table>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="submitFrameworkOrder"
          >确认创建
        </el-button>
        <el-button @click="frameworkOrderDialogVisible = false">取消</el-button>
      </template>
    </el-dialog>
    <!-- 框架订单列表弹窗 -->
    <el-dialog
      v-model="frameworkOrderListVisible"
      title="框架订单列表"
      width="80%"
      :close-on-click-modal="false"
    >
      <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">
        <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>
          </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" />
        <el-table-column
          label="累计发货数量"
          prop="shippedQuantity"
          width="140"
          align="center"
        />
      </el-table>
    </el-dialog>
  </div>
</template>
@@ -1177,6 +1328,9 @@
  delProduct,
  delLedgerFile,
  getProductInventory,
  createFrameworkOrder,
  listFrameworkOrders,
  frameworkContractShippedQuantity,
} from "@/api/salesManagement/salesLedger.js";
import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js";
import { modelList, productTreeList } from "@/api/basicData/product.js";
@@ -1226,6 +1380,7 @@
  },
  form: {
    salesContractNo: "",
    contractType: 2,
    autoGenerateContractNo: true,
    salesman: "",
    customerId: "",
@@ -1243,11 +1398,31 @@
    customerId: [{ required: true, message: "请选择", trigger: "change" }],
    entryPerson: [{ required: true, message: "请选择", trigger: "change" }],
    entryDate: [{ required: true, message: "请选择", trigger: "change" }],
    deliveryDate: [{ required: true, message: "请选择", trigger: "change" }],
    deliveryDate: [
      {
        validator: (rule, value, callback) => {
          if (form.value.contractType === 2) return callback();
          if (!value) return callback(new Error("请选择"));
          callback();
        },
        trigger: "change",
      },
    ],
    executionDate: [{ required: true, message: "请选择", trigger: "change" }],
  },
});
const { form, rules } = toRefs(data);
const isFrameworkContract = computed(() => form.value.contractType === 2);
const frameworkOptional = (message, trigger = "blur") => ({
  validator: (_rule, value, callback) => {
    if (isFrameworkContract.value) return callback();
    if (value === undefined || value === null || value === "") {
      return callback(new Error(message));
    }
    callback();
  },
  trigger,
});
const { form: searchForm } = useFormData(data.searchForm);
// 产品表单弹框数据
const productFormVisible = ref(false);
@@ -1273,18 +1448,12 @@
      { required: true, message: "请选择", trigger: "change" },
    ],
    unit: [{ required: true, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    taxInclusiveUnitPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    taxRate: [{ required: true, message: "请选择", trigger: "change" }],
    taxInclusiveTotalPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    taxExclusiveTotalPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    invoiceType: [{ required: true, message: "请选择", trigger: "change" }],
    quantity: [frameworkOptional("请输入")],
    taxInclusiveUnitPrice: [{ required: true, message: "请输入", trigger: "blur" }],
    taxRate: [frameworkOptional("请选择", "change")],
    taxInclusiveTotalPrice: [frameworkOptional("请输入")],
    taxExclusiveTotalPrice: [frameworkOptional("请输入")],
    invoiceType: [frameworkOptional("请选择", "change")],
    isProduction: [{ required: true, message: "请选择", trigger: "change" }],
  },
});
@@ -1324,47 +1493,17 @@
    item?.stockQuantity;
  return quantity ?? 0;
};
const getCurrentDeliveryRowQuantity = () => {
  return Number(currentDeliveryRow.value?.noQuantity || 0);
};
const getDeliveryBatchDeliveryMax = (row) => {
  const productQuantity = getCurrentDeliveryRowQuantity();
  const batchQuantity = Number(getDeliveryBatchQuantity(row) || 0);
  const otherBatchTotal = (deliveryForm.value.batchNoList || []).reduce(
    (sum, item) => {
      if (item?.id === row?.id) return sum;
      return sum + Number(item?.deliveryQuantity || 0);
    },
    0
  );
  const remainingProductQuantity = Math.max(
    0,
    productQuantity - otherBatchTotal
  );
  return Math.max(0, Math.min(batchQuantity, remainingProductQuantity));
  return Math.max(0, batchQuantity);
};
const handleDeliveryBatchQuantityChange = (row) => {
  const productQuantity = getCurrentDeliveryRowQuantity();
  const batchQuantity = Number(getDeliveryBatchQuantity(row) || 0);
  const otherBatchTotal = (deliveryForm.value.batchNoList || []).reduce(
    (sum, item) => {
      if (item?.id === row?.id) return sum;
      return sum + Number(item?.deliveryQuantity || 0);
    },
    0
  );
  const remainingProductQuantity = Math.max(
    0,
    productQuantity - otherBatchTotal
  );
  const currentValue = Number(row?.deliveryQuantity || 0);
  if (currentValue > batchQuantity) {
    row.deliveryQuantity = batchQuantity;
    proxy.$modal.msgWarning("发货数量不能大于库存数量");
  } else if (currentValue > remainingProductQuantity) {
    row.deliveryQuantity = remainingProductQuantity;
    proxy.$modal.msgWarning("所有批次发货数量之和不能大于待发货数量");
  }
};
const getSelectedDeliveryBatchRows = () => {
@@ -1409,6 +1548,7 @@
    shippingCarNumber: "",
    expressCompany: "",
    expressNumber: "",
    shippingAddress: "",
    type: "货车", // 货车, 快递
  },
  deliveryRules: {
@@ -1745,11 +1885,12 @@
  form.value.entryPerson = userStore.id;
  if (type === "add") {
    // 新增时设置录入日期为当天
    form.value.contractType = 2;
    form.value.entryDate = getCurrentDate();
    // 签订日期默认为当天
    form.value.executionDate = getCurrentDate();
    // 创建时间默认为当天
    form.value.createTime = getCurrentDate();
    form.value.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
    // 默认自动生成销售合同号
    form.value.autoGenerateContractNo = true;
  } else {
@@ -1897,6 +2038,14 @@
      }
      form.value.storageBlobDTOs = fileList;
      form.value.type = 1;
      if (isFrameworkContract.value) {
        form.value.productData = form.value.productData.map((item) => ({
          ...item,
          quantity: null,
          deliveryDate: null,
          isProduction: false,
        }));
      }
      if (form.value.autoGenerateContractNo) {
        form.value.salesContractNo = "";
      }
@@ -2004,6 +2153,12 @@
  );
  if (shippedProducts.length > 0) {
    proxy.$modal.msgWarning("已发货或审核通过的产品不能删除");
    return;
  }
  // 检查是否只剩一条数据,销售台账至少保留一条产品
  if (productData.value.length <= 1) {
    proxy.$modal.msgWarning("销售台账至少需要保留一条产品数据");
    return;
  }
@@ -2155,7 +2310,7 @@
};
// 打印功能
const handlePrint = async () => {
  const handlePrint = async () => {
  if (selectedRows.value.length === 0) {
    proxy.$modal.msgWarning("请选择要打印的数据");
    return;
@@ -2512,11 +2667,7 @@
// 格式化日期
const formatDate = (dateString) => {
  if (!dateString) return getCurrentDate();
  const date = new Date(dateString);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${year}/${month}/${day}`;
  return dayjs(dateString).format("YYYY/MM/DD HH:mm:ss");
};
// 格式化日期时间
const formatDateTime = (date) => {
@@ -2803,30 +2954,12 @@
/**
 * 判断是否可以发货
 * 只有在产品状态是充足,发货状态是待发货和审核拒绝的时候才可以发货
 * 只有在产品状态是充足,发货状态是待发货、审批中、审核拒绝、部分发货的时候才可以发货
 * @param row 行数据
 */
const canShip = (row) => {
  // 产品状态必须是充足(approveStatus === 1)
  if (row.approveStatus !== 1) {
    return false;
  }
  // 获取发货状态
  const shippingStatus = row.shippingStatus;
  // 如果已发货(有发货日期或车牌号),不能再次发货
  if (shippingStatus === "已发货") {
    return false;
  }
  // 发货状态必须是"待发货"或"审核拒绝"
  const statusStr = shippingStatus ? String(shippingStatus).trim() : "";
  return (
    statusStr === "待发货" ||
    statusStr === "审核拒绝" ||
    statusStr === "部分发货"
  );
  // 不再根据销售产品状态或已发货状态限制发货,发货合法性由库存批次和后端校验控制
  return true;
};
// 打开附件弹窗
@@ -2841,12 +2974,10 @@
// 打开发货弹框
const openDeliveryForm = async (row) => {
  // 检查是否可以发货
  if (!canShip(row)) {
    proxy.$modal.msgWarning(
      "只有在产品状态是充足,发货状态是待发货或审核拒绝的时候才可以发货"
    );
    return;
  // 审批中时提示用户已有待审批的发货记录
  const statusStr = row.shippingStatus ? String(row.shippingStatus).trim() : "";
  if (statusStr === "审批中") {
    proxy.$modal.msgWarning("该产品存在待审批的发货记录,请注意避免重复提交");
  }
  currentDeliveryRow.value = row;
@@ -2857,6 +2988,7 @@
    shippingCarNumber: "",
    expressCompany: "",
    expressNumber: "",
    shippingAddress: "",
    type: "货车",
    batchNo: [],
    batchNoList,
@@ -2872,20 +3004,6 @@
      const selectedBatchRows = getSelectedDeliveryBatchRows();
      if (selectedBatchRows.length === 0) {
        proxy.$modal.msgWarning("请至少填写一个批号的发货数量");
        return;
      }
      const totalDeliveryQuantity = selectedBatchRows.reduce(
        (sum, item) => sum + Number(item.deliveryQuantity || 0),
        0
      );
      const currentRowNoQuantity = Number(
        currentDeliveryRow.value?.noQuantity || 0
      );
      if (
        currentRowNoQuantity > 0 &&
        totalDeliveryQuantity > currentRowNoQuantity
      ) {
        proxy.$modal.msgWarning("批号发货总数不能超过待发货数量");
        return;
      }
      // 保存当前展开的行ID,以便发货后重新加载子表格数据
@@ -2911,6 +3029,7 @@
          deliveryForm.value.type === "快递"
            ? deliveryForm.value.expressNumber
            : "",
        shippingAddress: deliveryForm.value.shippingAddress,
        storageBlobDTOs: deliveryFileList.value || [],
        batchNo: deliveryForm.value.batchNo,
        batchNoDetailList: selectedBatchRows.map((item) => ({
@@ -2945,6 +3064,8 @@
            });
          }
        });
      }).catch(() => {
        // 后端库存不足等错误已由 request 拦截器统一提示
      });
    }
  });
@@ -2966,6 +3087,92 @@
  deliveryFormVisible.value = false;
  currentDeliveryRow.value = null;
};
// 框架合同 / 框架订单相关
const frameworkOrderDialogVisible = ref(false);
const frameworkOrderListVisible = ref(false);
const currentFrameworkContract = ref(null);
const frameworkOrderForm = reactive({
  remarks: "",
});
const frameworkOrderProducts = ref([]);
const frameworkOrderList = ref([]);
const frameworkContractShippedQty = ref(null);
const openFrameworkOrderDialog = async (row) => {
  currentFrameworkContract.value = row;
  frameworkOrderForm.remarks = "";
  frameworkOrderProducts.value = [];
  try {
    const res = await productList({ salesLedgerId: row.id, type: 1 });
    frameworkOrderProducts.value = (res.data || []).map((p) => ({
      parentProductId: p.id,
      productCategory: p.productCategory,
      specificationModel: p.specificationModel,
      unit: p.unit,
      deliveryDate: "",
    }));
  } catch (e) {
    frameworkOrderProducts.value = [];
  }
  frameworkOrderDialogVisible.value = true;
};
const submitFrameworkOrder = async () => {
  const selected = frameworkOrderProducts.value.filter((p) => p.deliveryDate);
  if (selected.length === 0) {
    proxy.$modal.msgWarning("请至少为一个产品填写交货日期");
    return;
  }
  try {
    await createFrameworkOrder({
      id: currentFrameworkContract.value.id,
      remarks: frameworkOrderForm.remarks,
      productData: selected.map((p) => ({
        parentProductId: p.parentProductId,
        deliveryDate: p.deliveryDate,
      })),
    });
    proxy.$modal.msgSuccess("框架订单创建成功");
    frameworkOrderDialogVisible.value = false;
  } catch (e) {
    // 错误已由请求拦截器统一提示
  }
};
const openFrameworkOrderList = async (row) => {
  currentFrameworkContract.value = row;
  frameworkContractShippedQty.value = null;
  frameworkOrderList.value = [];
  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 = [];
      }
      withProducts.push({ ...order, products });
    }
    frameworkOrderList.value = withProducts;
  });
  frameworkContractShippedQuantity({ parentContractId: row.id }).then((res) => {
    frameworkContractShippedQty.value = res.data ?? 0;
  });
};
const shipFrameworkOrderProduct = (productRow, orderRow) => {
  if (!orderRow || !orderRow.id) return;
  frameworkOrderListVisible.value = false;
  openDeliveryForm({
    ...productRow,
    salesLedgerId: orderRow.id,
  });
};
const currentFactoryName = ref("");
const getCurrentFactoryName = async () => {
  let res = await userStore.getInfo();