diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
index 35ebc8ff3..8ffd21ccc 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
@@ -288,11 +288,16 @@ public class CollectionUtils {
 
     public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc,
                                                                      BinaryOperator<V> accumulator) {
+        return getSumValue(from, valueFunc, accumulator, null);
+    }
+
+    public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
+                                                                     BinaryOperator<V> accumulator, V defaultValue) {
         if (CollUtil.isEmpty(from)) {
-            return null;
+            return defaultValue;
         }
-        assert from.size() > 0; // 断言,避免告警
-        return from.stream().map(valueFunc).reduce(accumulator).get();
+        assert !from.isEmpty(); // 断言,避免告警
+        return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue);
     }
 
     public static <T> void addIfNotNull(Collection<T> coll, T item) {
@@ -302,8 +307,12 @@ public class CollectionUtils {
         coll.add(item);
     }
 
-    public static <T> Collection<T> singleton(T deptId) {
-        return deptId == null ? Collections.emptyList() : Collections.singleton(deptId);
+    public static <T> Collection<T> singleton(T obj) {
+        return obj == null ? Collections.emptyList() : Collections.singleton(obj);
+    }
+
+    public static <T> List<T> newArrayList(List<List<T>> list) {
+        return list.stream().flatMap(Collection::stream).collect(Collectors.toList());
     }
 
 }
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java
index ccfeb3917..90888d7d3 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/MoneyUtils.java
@@ -13,6 +13,16 @@ import java.math.RoundingMode;
  */
 public class MoneyUtils {
 
+    /**
+     * 金额的小数位数
+     */
+    private static final int PRICE_SCALE = 2;
+
+    /**
+     * 百分比对应的 BigDecimal 对象
+     */
+    public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100);
+
     /**
      * 计算百分比金额,四舍五入
      *
@@ -86,4 +96,36 @@ public class MoneyUtils {
         return new Money(0, fen).toString();
     }
 
+    /**
+     * 金额相乘,默认进行四舍五入
+     *
+     * 位数:{@link #PRICE_SCALE}
+     *
+     * @param price 金额
+     * @param count 数量
+     * @return 金额相乘结果
+     */
+    public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) {
+        if (price == null || count == null) {
+            return null;
+        }
+        return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 金额相乘(百分比),默认进行四舍五入
+     *
+     * 位数:{@link #PRICE_SCALE}
+     *
+     * @param price  金额
+     * @param percent 百分比
+     * @return 金额相乘结果
+     */
+    public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) {
+        if (price == null || percent == null) {
+            return null;
+        }
+        return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP);
+    }
+
 }
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java
index 55ab367a3..ea131e86e 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java
@@ -1,7 +1,10 @@
 package cn.iocoder.yudao.framework.common.util.number;
 
+import cn.hutool.core.util.NumberUtil;
 import cn.hutool.core.util.StrUtil;
 
+import java.math.BigDecimal;
+
 /**
  * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能
  *
@@ -37,4 +40,21 @@ public class NumberUtils {
         return distance;
     }
 
+    /**
+     * 提供精确的乘法运算
+     *
+     * 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null,则返回 null
+     *
+     * @param values 多个被乘值
+     * @return 积
+     */
+    public static BigDecimal mul(BigDecimal... values) {
+        for (BigDecimal value : values) {
+            if (value == null) {
+                return null;
+            }
+        }
+        return NumberUtil.mul(values);
+    }
+
 }
diff --git a/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java b/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
index 1cc2807ad..580539f97 100644
--- a/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
+++ b/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -13,8 +13,73 @@ public interface ErrorCodeConstants {
     ErrorCode SUPPLIER_NOT_EXISTS = new ErrorCode(1_030_100_000, "供应商不存在");
     ErrorCode SUPPLIER_NOT_ENABLE = new ErrorCode(1_030_100_000, "供应商({})未启用");
 
-    // ========== 销售订单(1-030-200-000) ==========
-    ErrorCode SALE_ORDER_NOT_EXISTS = new ErrorCode(1_020_200_000, "销售订单不存在");
+    // ========== ERP 采购订单(1-030-101-000) ==========
+    ErrorCode PURCHASE_ORDER_NOT_EXISTS = new ErrorCode(1_030_101_000, "采购订单不存在");
+    ErrorCode PURCHASE_ORDER_DELETE_FAIL_APPROVE = new ErrorCode(1_030_101_001, "采购订单({})已审核,无法删除");
+    ErrorCode PURCHASE_ORDER_PROCESS_FAIL = new ErrorCode(1_030_101_002, "反审核失败,只有已审核的采购订单才能反审核");
+    ErrorCode PURCHASE_ORDER_APPROVE_FAIL = new ErrorCode(1_030_101_003, "审核失败,只有未审核的采购订单才能审核");
+    ErrorCode PURCHASE_ORDER_NO_EXISTS = new ErrorCode(1_030_101_004, "生成采购单号失败,请重新提交");
+    ErrorCode PURCHASE_ORDER_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_101_005, "采购订单({})已审核,无法修改");
+    ErrorCode PURCHASE_ORDER_NOT_APPROVE = new ErrorCode(1_030_101_006, "采购订单未审核,无法操作");
+    ErrorCode PURCHASE_ORDER_ITEM_IN_FAIL_PRODUCT_EXCEED = new ErrorCode(1_030_101_007, "采购订单项({})超过最大允许入库数量({})");
+    ErrorCode PURCHASE_ORDER_PROCESS_FAIL_EXISTS_IN = new ErrorCode(1_030_101_008, "反审核失败,已存在对应的采购入库单");
+ErrorCode PURCHASE_ORDER_ITEM_RETURN_FAIL_IN_EXCEED = new ErrorCode(1_030_101_009, "采购订单项({})超过最大允许退货数量({})");
+    ErrorCode PURCHASE_ORDER_PROCESS_FAIL_EXISTS_RETURN = new ErrorCode(1_030_101_010, "反审核失败,已存在对应的采购退货单");
+
+    // ========== ERP 采购入库(1-030-102-000) ==========
+    ErrorCode PURCHASE_IN_NOT_EXISTS = new ErrorCode(1_030_102_000, "采购入库单不存在");
+    ErrorCode PURCHASE_IN_DELETE_FAIL_APPROVE = new ErrorCode(1_030_102_001, "采购入库单({})已审核,无法删除");
+    ErrorCode PURCHASE_IN_PROCESS_FAIL = new ErrorCode(1_030_102_002, "反审核失败,只有已审核的入库单才能反审核");
+    ErrorCode PURCHASE_IN_APPROVE_FAIL = new ErrorCode(1_030_102_003, "审核失败,只有未审核的入库单才能审核");
+    ErrorCode PURCHASE_IN_NO_EXISTS = new ErrorCode(1_030_102_004, "生成入库单失败,请重新提交");
+    ErrorCode PURCHASE_IN_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_102_005, "采购入库单({})已审核,无法修改");
+    ErrorCode PURCHASE_IN_NOT_APPROVE = new ErrorCode(1_030_102_006, "采购入库单未审核,无法操作");
+    ErrorCode PURCHASE_IN_FAIL_PAYMENT_PRICE_EXCEED = new ErrorCode(1_030_102_007, "付款金额({})超过采购入库单总金额({})");
+    ErrorCode PURCHASE_IN_PROCESS_FAIL_EXISTS_PAYMENT = new ErrorCode(1_030_102_008, "反审核失败,已存在对应的付款单");
+
+    // ========== ERP 采购退货(1-030-103-000) ==========
+    ErrorCode PURCHASE_RETURN_NOT_EXISTS = new ErrorCode(1_030_103_000, "采购退货单不存在");
+    ErrorCode PURCHASE_RETURN_DELETE_FAIL_APPROVE = new ErrorCode(1_030_103_001, "采购退货单({})已审核,无法删除");
+    ErrorCode PURCHASE_RETURN_PROCESS_FAIL = new ErrorCode(1_030_103_002, "反审核失败,只有已审核的退货单才能反审核");
+    ErrorCode PURCHASE_RETURN_APPROVE_FAIL = new ErrorCode(1_030_103_003, "审核失败,只有未审核的退货单才能审核");
+    ErrorCode PURCHASE_RETURN_NO_EXISTS = new ErrorCode(1_030_103_004, "生成退货单失败,请重新提交");
+    ErrorCode PURCHASE_RETURN_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_103_005, "采购退货单({})已审核,无法修改");
+    ErrorCode PURCHASE_RETURN_NOT_APPROVE = new ErrorCode(1_030_103_006, "采购退货单未审核,无法操作");
+    ErrorCode PURCHASE_RETURN_FAIL_REFUND_PRICE_EXCEED = new ErrorCode(1_030_103_007, "退款金额({})超过采购退货单总金额({})");
+    ErrorCode PURCHASE_RETURN_PROCESS_FAIL_EXISTS_REFUND = new ErrorCode(1_030_103_008, "反审核失败,已存在对应的退款单");
+
+    // ========== ERP 客户(1-030-200-000)==========
+    ErrorCode CUSTOMER_NOT_EXISTS = new ErrorCode(1_020_200_000, "客户不存在");
+    ErrorCode CUSTOMER_NOT_ENABLE = new ErrorCode(1_020_200_001, "客户({})未启用");
+
+    // ========== ERP 销售订单(1-030-201-000) ==========
+    ErrorCode SALE_ORDER_NOT_EXISTS = new ErrorCode(1_020_201_000, "销售订单不存在");
+    ErrorCode SALE_ORDER_DELETE_FAIL_APPROVE = new ErrorCode(1_020_201_001, "销售订单({})已审核,无法删除");
+    ErrorCode SALE_ORDER_PROCESS_FAIL = new ErrorCode(1_020_201_002, "反审核失败,只有已审核的销售订单才能反审核");
+    ErrorCode SALE_ORDER_APPROVE_FAIL = new ErrorCode(1_020_201_003, "审核失败,只有未审核的销售订单才能审核");
+    ErrorCode SALE_ORDER_NO_EXISTS = new ErrorCode(1_020_201_004, "生成销售单号失败,请重新提交");
+    ErrorCode SALE_ORDER_UPDATE_FAIL_APPROVE = new ErrorCode(1_020_201_005, "销售订单({})已审核,无法修改");
+    ErrorCode SALE_ORDER_NOT_APPROVE = new ErrorCode(1_020_201_006, "销售订单未审核,无法操作");
+    ErrorCode SALE_ORDER_ITEM_OUT_FAIL_PRODUCT_EXCEED = new ErrorCode(1_020_201_007, "销售订单项({})超过最大允许出库数量({})");
+    ErrorCode SALE_ORDER_PROCESS_FAIL_EXISTS_OUT = new ErrorCode(1_020_201_008, "反审核失败,已存在对应的销售出库单");
+    ErrorCode SALE_ORDER_ITEM_RETURN_FAIL_OUT_EXCEED = new ErrorCode(1_020_201_009, "销售订单项({})超过最大允许退货数量({})");
+    ErrorCode SALE_ORDER_PROCESS_FAIL_EXISTS_RETURN = new ErrorCode(1_020_201_010, "反审核失败,已存在对应的销售退货单");
+
+    // ========== ERP 销售出库(1-030-202-000) ==========
+    ErrorCode SALE_OUT_NOT_EXISTS = new ErrorCode(1_020_202_000, "销售出库单不存在");
+    ErrorCode SALE_OUT_DELETE_FAIL_APPROVE = new ErrorCode(1_020_202_001, "销售出库单({})已审核,无法删除");
+    ErrorCode SALE_OUT_PROCESS_FAIL = new ErrorCode(1_020_202_002, "反审核失败,只有已审核的出库单才能反审核");
+    ErrorCode SALE_OUT_APPROVE_FAIL = new ErrorCode(1_020_202_003, "审核失败,只有未审核的出库单才能审核");
+    ErrorCode SALE_OUT_NO_EXISTS = new ErrorCode(1_020_202_004, "生成出库单失败,请重新提交");
+    ErrorCode SALE_OUT_UPDATE_FAIL_APPROVE = new ErrorCode(1_020_202_005, "销售出库单({})已审核,无法修改");
+
+    // ========== ERP 销售退货(1-030-203-000) ==========
+    ErrorCode SALE_RETURN_NOT_EXISTS = new ErrorCode(1_020_203_000, "销售退货单不存在");
+    ErrorCode SALE_RETURN_DELETE_FAIL_APPROVE = new ErrorCode(1_020_203_001, "销售退货单({})已审核,无法删除");
+    ErrorCode SALE_RETURN_PROCESS_FAIL = new ErrorCode(1_020_203_002, "反审核失败,只有已审核的退货单才能反审核");
+    ErrorCode SALE_RETURN_APPROVE_FAIL = new ErrorCode(1_020_203_003, "审核失败,只有未审核的退货单才能审核");
+    ErrorCode SALE_RETURN_NO_EXISTS = new ErrorCode(1_020_203_004, "生成退货单失败,请重新提交");
+    ErrorCode SALE_RETURN_UPDATE_FAIL_APPROVE = new ErrorCode(1_020_203_005, "销售退货单({})已审核,无法修改");
 
     // ========== ERP 仓库 1-030-400-000 ==========
     ErrorCode WAREHOUSE_NOT_EXISTS = new ErrorCode(1_030_400_000, "仓库不存在");
@@ -22,6 +87,39 @@ public interface ErrorCodeConstants {
 
     // ========== ERP 其它入库单 1-030-401-000 ==========
     ErrorCode STOCK_IN_NOT_EXISTS = new ErrorCode(1_030_401_000, "其它入库单不存在");
+    ErrorCode STOCK_IN_DELETE_FAIL_APPROVE = new ErrorCode(1_030_401_001, "其它入库单({})已审核,无法删除");
+    ErrorCode STOCK_IN_PROCESS_FAIL = new ErrorCode(1_030_401_002, "反审核失败,只有已审核的入库单才能反审核");
+    ErrorCode STOCK_IN_APPROVE_FAIL = new ErrorCode(1_030_401_003, "审核失败,只有未审核的入库单才能审核");
+    ErrorCode STOCK_IN_NO_EXISTS = new ErrorCode(1_030_401_004, "生成入库单失败,请重新提交");
+    ErrorCode STOCK_IN_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_401_005, "其它入库单({})已审核,无法修改");
+
+    // ========== ERP 其它出库单 1-030-402-000 ==========
+    ErrorCode STOCK_OUT_NOT_EXISTS = new ErrorCode(1_030_402_000, "其它出库单不存在");
+    ErrorCode STOCK_OUT_DELETE_FAIL_APPROVE = new ErrorCode(1_030_402_001, "其它出库单({})已审核,无法删除");
+    ErrorCode STOCK_OUT_PROCESS_FAIL = new ErrorCode(1_030_402_002, "反审核失败,只有已审核的出库单才能反审核");
+    ErrorCode STOCK_OUT_APPROVE_FAIL = new ErrorCode(1_030_402_003, "审核失败,只有未审核的出库单才能审核");
+    ErrorCode STOCK_OUT_NO_EXISTS = new ErrorCode(1_030_402_004, "生成出库单失败,请重新提交");
+    ErrorCode STOCK_OUT_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_402_005, "其它出库单({})已审核,无法修改");
+
+    // ========== ERP 库存调拨单 1-030-403-000 ==========
+    ErrorCode STOCK_MOVE_NOT_EXISTS = new ErrorCode(1_030_402_000, "库存调拨单不存在");
+    ErrorCode STOCK_MOVE_DELETE_FAIL_APPROVE = new ErrorCode(1_030_402_001, "库存调拨单({})已审核,无法删除");
+    ErrorCode STOCK_MOVE_PROCESS_FAIL = new ErrorCode(1_030_402_002, "反审核失败,只有已审核的调拨单才能反审核");
+    ErrorCode STOCK_MOVE_APPROVE_FAIL = new ErrorCode(1_030_402_003, "审核失败,只有未审核的调拨单才能审核");
+    ErrorCode STOCK_MOVE_NO_EXISTS = new ErrorCode(1_030_402_004, "生成调拨号失败,请重新提交");
+    ErrorCode STOCK_MOVE_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_402_005, "库存调拨单({})已审核,无法修改");
+
+    // ========== ERP 库存盘点单 1-030-403-000 ==========
+    ErrorCode STOCK_CHECK_NOT_EXISTS = new ErrorCode(1_030_403_000, "库存盘点单不存在");
+    ErrorCode STOCK_CHECK_DELETE_FAIL_APPROVE = new ErrorCode(1_030_403_001, "库存盘点单({})已审核,无法删除");
+    ErrorCode STOCK_CHECK_PROCESS_FAIL = new ErrorCode(1_030_403_002, "反审核失败,只有已审核的盘点单才能反审核");
+    ErrorCode STOCK_CHECK_APPROVE_FAIL = new ErrorCode(1_030_403_003, "审核失败,只有未审核的盘点单才能审核");
+    ErrorCode STOCK_CHECK_NO_EXISTS = new ErrorCode(1_030_403_004, "生成盘点号失败,请重新提交");
+    ErrorCode STOCK_CHECK_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_403_005, "库存盘点单({})已审核,无法修改");
+
+    // ========== ERP 产品库存 1-030-404-000 ==========
+    ErrorCode STOCK_COUNT_NEGATIVE = new ErrorCode(1_030_404_000, "操作失败,产品({})所在仓库({})的库存:{},小于变更数量:{}");
+    ErrorCode STOCK_COUNT_NEGATIVE2 = new ErrorCode(1_030_404_001, "操作失败,产品({})所在仓库({})的库存不足");
 
     // ========== ERP 产品 1-030-500-000 ==========
     ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_030_500_000, "产品不存在");
@@ -41,4 +139,16 @@ public interface ErrorCodeConstants {
     ErrorCode PRODUCT_UNIT_NAME_DUPLICATE = new ErrorCode(1_030_502_001, "已存在该名字的产品单位");
     ErrorCode PRODUCT_UNIT_EXITS_PRODUCT = new ErrorCode(1_030_502_002, "存在产品使用该单位,无法删除");
 
+    // ========== ERP 结算账户 1-030-600-000 ==========
+    ErrorCode ACCOUNT_NOT_EXISTS = new ErrorCode(1_030_600_000, "结算账户不存在");
+    ErrorCode ACCOUNT_NOT_ENABLE = new ErrorCode(1_030_600_001, "结算账户({})未启用");
+
+    // ========== ERP 付款单 1-030-601-000 ==========
+    ErrorCode FINANCE_PAYMENT_NOT_EXISTS = new ErrorCode(1_030_601_000, "付款单不存在");
+    ErrorCode FINANCE_PAYMENT_DELETE_FAIL_APPROVE = new ErrorCode(1_030_601_001, "付款单({})已审核,无法删除");
+    ErrorCode FINANCE_PAYMENT_PROCESS_FAIL = new ErrorCode(1_030_601_002, "反审核失败,只有已审核的付款单才能反审核");
+    ErrorCode FINANCE_PAYMENT_APPROVE_FAIL = new ErrorCode(1_030_601_003, "审核失败,只有未审核的付款单才能审核");
+    ErrorCode FINANCE_PAYMENT_NO_EXISTS = new ErrorCode(1_030_601_004, "生成付款单号失败,请重新提交");
+    ErrorCode FINANCE_PAYMENT_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_601_005, "付款单({})已审核,无法修改");
+
 }
diff --git a/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/common/ErpBizTypeEnum.java b/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/common/ErpBizTypeEnum.java
new file mode 100644
index 000000000..bba2b309b
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/common/ErpBizTypeEnum.java
@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.erp.enums.common;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * ERP 业务类型枚举
+ *
+ * @author HUIHUI
+ */
+@RequiredArgsConstructor
+@Getter
+public enum ErpBizTypeEnum implements IntArrayValuable {
+
+    PURCHASE_ORDER(10, "采购订单"),
+    PURCHASE_IN(11, "采购入库"),
+    PURCHASE_RETURN(12, "采购退货"),
+
+    SALE_ORDER(20, "销售订单"),
+    SALE_OUT(21, "销售订单"),
+    SALE_RETURN(22, "销售退货"),
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ErpBizTypeEnum::getType).toArray();
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 名称
+     */
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+}
diff --git a/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/sale/ErpSaleOrderStatusEnum.java b/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/sale/ErpSaleOrderStatusEnum.java
deleted file mode 100644
index 4caa7f515..000000000
--- a/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/sale/ErpSaleOrderStatusEnum.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package cn.iocoder.yudao.module.erp.enums.sale;
-
-import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-import java.util.Arrays;
-
-/**
- * ERP 销售订单的状态枚举
- *
- * @author 芋道源码
- */
-@AllArgsConstructor
-@Getter
-public enum ErpSaleOrderStatusEnum implements IntArrayValuable {
-
-    AUDIT_NONE(0, "未审核"),
-    AUDIT_PASS(10, "已审核"),
-    SALE_PART(20, "部分销售"),
-    SALE_ALL(21, "完成销售"),
-    ;
-
-    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ErpSaleOrderStatusEnum::getStatus).toArray();
-
-    /**
-     * 状态
-     */
-    private final Integer status;
-    /**
-     * 状态名
-     */
-    private final String name;
-
-    @Override
-    public int[] array() {
-        return ARRAYS;
-    }
-
-}
diff --git a/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/stock/ErpStockRecordBizTypeEnum.java b/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/stock/ErpStockRecordBizTypeEnum.java
index fb16934d9..559bf4ccf 100644
--- a/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/stock/ErpStockRecordBizTypeEnum.java
+++ b/yudao-module-erp/yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/enums/stock/ErpStockRecordBizTypeEnum.java
@@ -20,6 +20,28 @@ public enum ErpStockRecordBizTypeEnum implements IntArrayValuable {
 
     OTHER_OUT(20, "其它出库"),
     OTHER_OUT_CANCEL(21, "其它出库(作废)"),
+
+    MOVE_IN(30, "调拨入库"),
+    MOVE_IN_CANCEL(31, "调拨入库(作废)"),
+    MOVE_OUT(32, "调拨出库"),
+    MOVE_OUT_CANCEL(33, "调拨出库(作废)"),
+
+    CHECK_MORE_IN(40, "盘盈入库"),
+    CHECK_MORE_IN_CANCEL(41, "盘盈入库(作废)"),
+    CHECK_LESS_OUT(42, "盘亏出库"),
+    CHECK_LESS_OUT_CANCEL(43, "盘亏出库(作废)"),
+
+    SALE_OUT(50, "销售出库"),
+    SALE_OUT_CANCEL(51, "销售出库(作废)"),
+
+    SALE_RETURN(60, "销售退货入库"),
+    SALE_RETURN_CANCEL(61, "销售退货入库(作废)"),
+
+    PURCHASE_IN(70, "采购入库"),
+    PURCHASE_IN_CANCEL(71, "采购入库(作废)"),
+
+    PURCHASE_RETURN(80, "采购退货出库"),
+    PURCHASE_RETURN_CANCEL(81, "采购退货出库(作废)"),
     ;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ErpStockRecordBizTypeEnum::getType).toArray();
diff --git a/yudao-module-erp/yudao-module-erp-biz/pom.xml b/yudao-module-erp/yudao-module-erp-biz/pom.xml
index 83b3900cf..1d0b44162 100644
--- a/yudao-module-erp/yudao-module-erp-biz/pom.xml
+++ b/yudao-module-erp/yudao-module-erp-biz/pom.xml
@@ -51,6 +51,11 @@
             <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-redis</artifactId>
+        </dependency>
+
         <!-- 工具类相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpAccountController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpAccountController.java
new file mode 100644
index 000000000..4c8c98058
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpAccountController.java
@@ -0,0 +1,116 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account.ErpAccountPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account.ErpAccountRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account.ErpAccountSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 结算账户")
+@RestController
+@RequestMapping("/erp/account")
+@Validated
+public class ErpAccountController {
+
+    @Resource
+    private ErpAccountService accountService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建结算账户")
+    @PreAuthorize("@ss.hasPermission('erp:account:create')")
+    public CommonResult<Long> createAccount(@Valid @RequestBody ErpAccountSaveReqVO createReqVO) {
+        return success(accountService.createAccount(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新结算账户")
+    @PreAuthorize("@ss.hasPermission('erp:account:update')")
+    public CommonResult<Boolean> updateAccount(@Valid @RequestBody ErpAccountSaveReqVO updateReqVO) {
+        accountService.updateAccount(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-default-status")
+    @Operation(summary = "更新结算账户默认状态")
+    @Parameters({
+            @Parameter(name = "id", description = "编号", required = true),
+            @Parameter(name = "status", description = "状态", required = true)
+    })
+    public CommonResult<Boolean> updateAccountDefaultStatus(@RequestParam("id") Long id,
+                                                              @RequestParam("defaultStatus") Boolean defaultStatus) {
+        accountService.updateAccountDefaultStatus(id, defaultStatus);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除结算账户")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:account:delete')")
+    public CommonResult<Boolean> deleteAccount(@RequestParam("id") Long id) {
+        accountService.deleteAccount(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得结算账户")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:account:query')")
+    public CommonResult<ErpAccountRespVO> getAccount(@RequestParam("id") Long id) {
+        ErpAccountDO account = accountService.getAccount(id);
+        return success(BeanUtils.toBean(account, ErpAccountRespVO.class));
+    }
+
+    @GetMapping("/simple-list")
+    @Operation(summary = "获得结算账户精简列表", description = "只包含被开启的结算账户,主要用于前端的下拉选项")
+    public CommonResult<List<ErpAccountRespVO>> getWarehouseSimpleList() {
+        List<ErpAccountDO> list = accountService.getAccountListByStatus(CommonStatusEnum.ENABLE.getStatus());
+        return success(convertList(list, account -> new ErpAccountRespVO().setId(account.getId())
+                .setName(account.getName()).setDefaultStatus(account.getDefaultStatus())));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得结算账户分页")
+    @PreAuthorize("@ss.hasPermission('erp:account:query')")
+    public CommonResult<PageResult<ErpAccountRespVO>> getAccountPage(@Valid ErpAccountPageReqVO pageReqVO) {
+        PageResult<ErpAccountDO> pageResult = accountService.getAccountPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, ErpAccountRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出结算账户 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:account:export')")
+    @OperateLog(type = EXPORT)
+    public void exportAccountExcel(@Valid ErpAccountPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpAccountDO> list = accountService.getAccountPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "结算账户.xls", "数据", ErpAccountRespVO.class,
+                        BeanUtils.toBean(list, ErpAccountRespVO.class));
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinancePaymentController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinancePaymentController.java
new file mode 100644
index 000000000..b1f028a28
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/ErpFinancePaymentController.java
@@ -0,0 +1,153 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
+import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
+import cn.iocoder.yudao.module.erp.service.finance.ErpFinancePaymentService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 付款单")
+@RestController
+@RequestMapping("/erp/finance-payment")
+@Validated
+public class ErpFinancePaymentController {
+
+    @Resource
+    private ErpFinancePaymentService financePaymentService;
+    @Resource
+    private ErpSupplierService supplierService;
+    @Resource
+    private ErpAccountService accountService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建付款单")
+    @PreAuthorize("@ss.hasPermission('erp:finance-payment:create')")
+    public CommonResult<Long> createFinancePayment(@Valid @RequestBody ErpFinancePaymentSaveReqVO createReqVO) {
+        return success(financePaymentService.createFinancePayment(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新付款单")
+    @PreAuthorize("@ss.hasPermission('erp:finance-payment:update')")
+    public CommonResult<Boolean> updateFinancePayment(@Valid @RequestBody ErpFinancePaymentSaveReqVO updateReqVO) {
+        financePaymentService.updateFinancePayment(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "更新付款单的状态")
+    @PreAuthorize("@ss.hasPermission('erp:finance-payment:update-status')")
+    public CommonResult<Boolean> updateFinancePaymentStatus(@RequestParam("id") Long id,
+                                                           @RequestParam("status") Integer status) {
+        financePaymentService.updateFinancePaymentStatus(id, status);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除付款单")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:finance-payment:delete')")
+    public CommonResult<Boolean> deleteFinancePayment(@RequestParam("ids") List<Long> ids) {
+        financePaymentService.deleteFinancePayment(ids);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得付款单")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:finance-payment:query')")
+    public CommonResult<ErpFinancePaymentRespVO> getFinancePayment(@RequestParam("id") Long id) {
+        ErpFinancePaymentDO payment = financePaymentService.getFinancePayment(id);
+        if (payment == null) {
+            return success(null);
+        }
+        List<ErpFinancePaymentItemDO> paymentItemList = financePaymentService.getFinancePaymentItemListByPaymentId(id);
+        return success(BeanUtils.toBean(payment, ErpFinancePaymentRespVO.class, financePaymentVO ->
+                financePaymentVO.setItems(BeanUtils.toBean(paymentItemList, ErpFinancePaymentRespVO.Item.class))));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得付款单分页")
+    @PreAuthorize("@ss.hasPermission('erp:finance-payment:query')")
+    public CommonResult<PageResult<ErpFinancePaymentRespVO>> getFinancePaymentPage(@Valid ErpFinancePaymentPageReqVO pageReqVO) {
+        PageResult<ErpFinancePaymentDO> pageResult = financePaymentService.getFinancePaymentPage(pageReqVO);
+        return success(buildFinancePaymentVOPageResult(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出付款单 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:finance-payment:export')")
+    @OperateLog(type = EXPORT)
+    public void exportFinancePaymentExcel(@Valid ErpFinancePaymentPageReqVO pageReqVO,
+                                         HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpFinancePaymentRespVO> list = buildFinancePaymentVOPageResult(financePaymentService.getFinancePaymentPage(pageReqVO)).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "付款单.xls", "数据", ErpFinancePaymentRespVO.class, list);
+    }
+
+    private PageResult<ErpFinancePaymentRespVO> buildFinancePaymentVOPageResult(PageResult<ErpFinancePaymentDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        // 1.1 付款项
+        List<ErpFinancePaymentItemDO> paymentItemList = financePaymentService.getFinancePaymentItemListByPaymentIds(
+                convertSet(pageResult.getList(), ErpFinancePaymentDO::getId));
+        Map<Long, List<ErpFinancePaymentItemDO>> financePaymentItemMap = convertMultiMap(paymentItemList, ErpFinancePaymentItemDO::getPaymentId);
+        // 1.2 供应商信息
+        Map<Long, ErpSupplierDO> supplierMap = supplierService.getSupplierMap(
+                convertSet(pageResult.getList(), ErpFinancePaymentDO::getSupplierId));
+        // 1.3 结算账户信息
+        Map<Long, ErpAccountDO> accountMap = accountService.getAccountMap(
+                convertSet(pageResult.getList(), ErpFinancePaymentDO::getAccountId));
+        // 1.4 管理员信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(),
+                contact -> Stream.of(NumberUtils.parseLong(contact.getCreator()), contact.getFinanceUserId())));
+        // 2. 开始拼接
+        return BeanUtils.toBean(pageResult, ErpFinancePaymentRespVO.class, payment -> {
+            payment.setItems(BeanUtils.toBean(financePaymentItemMap.get(payment.getId()), ErpFinancePaymentRespVO.Item.class));
+            MapUtils.findAndThen(supplierMap, payment.getSupplierId(), supplier -> payment.setSupplierName(supplier.getName()));
+            MapUtils.findAndThen(accountMap, payment.getAccountId(), account -> payment.setAccountName(account.getName()));
+            MapUtils.findAndThen(userMap, Long.parseLong(payment.getCreator()), user -> payment.setCreatorName(user.getNickname()));
+            MapUtils.findAndThen(userMap, payment.getFinanceUserId(), user -> payment.setFinanceUserName(user.getNickname()));
+        });
+    }
+
+}
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountPageReqVO.java
new file mode 100644
index 000000000..3e1fa72f4
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountPageReqVO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - ERP 结算账户分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpAccountPageReqVO extends PageParam {
+
+    @Schema(description = "账户编码", example = "A88")
+    private String no;
+
+    @Schema(description = "账户名称", example = "张三")
+    private String name;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java
new file mode 100644
index 000000000..a1c2e954d
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java
@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - ERP 结算账户 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpAccountRespVO {
+
+    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28684")
+    @ExcelProperty("结算账户编号")
+    private Long id;
+
+    @Schema(description = "账户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @ExcelProperty("账户名称")
+    private String name;
+
+    @Schema(description = "账户编码", example = "A88")
+    @ExcelProperty("账户编码")
+    private String no;
+
+    @Schema(description = "备注", example = "随便")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty("开启状态")
+    @DictFormat(DictTypeConstants.COMMON_STATUS)
+    private Integer status;
+
+    @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty("排序")
+    private Integer sort;
+
+    @Schema(description = "是否默认", example = "1")
+    @ExcelProperty("是否默认")
+    private Boolean defaultStatus;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountSaveReqVO.java
new file mode 100644
index 000000000..6f3556530
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountSaveReqVO.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - ERP 结算账户新增/修改 Request VO")
+@Data
+public class ErpAccountSaveReqVO {
+
+    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28684")
+    private Long id;
+
+    @Schema(description = "账户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @NotEmpty(message = "账户名称不能为空")
+    private String name;
+
+    @Schema(description = "账户编码", example = "A88")
+    private String no;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "开启状态不能为空")
+    @InEnum(value = CommonStatusEnum.class)
+    private Integer status;
+
+    @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "排序不能为空")
+    private Integer sort;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentPageReqVO.java
new file mode 100644
index 000000000..39a0f9505
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentPageReqVO.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - ERP 付款单分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpFinancePaymentPageReqVO extends PageParam {
+
+    @Schema(description = "采购单编号", example = "XS001")
+    private String no;
+
+    @Schema(description = "付款时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] paymentTime;
+
+    @Schema(description = "供应商编号", example = "1724")
+    private Long supplierId;
+
+    @Schema(description = "创建者", example = "666")
+    private String creator;
+
+    @Schema(description = "财务人员编号", example = "888")
+    private String financeUserId;
+
+    @Schema(description = "结算账户编号", example = "31189")
+    private Long accountId;
+
+    @Schema(description = "付款状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "业务编号", example = "123")
+    private String bizNo;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java
new file mode 100644
index 000000000..4dbb4566b
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java
@@ -0,0 +1,97 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 付款单 Response VO")
+@Data
+public class ErpFinancePaymentRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23752")
+    private Long id;
+
+    @Schema(description = "付款单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "FKD888")
+    private String no;
+
+    @Schema(description = "付款状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private Integer status;
+
+    @Schema(description = "付款时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime paymentTime;
+
+    @Schema(description = "财务人员编号", example = "19690")
+    private Long financeUserId;
+    @Schema(description = "财务人员名称", example = "张三")
+    private String financeUserName;
+
+    @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29399")
+    private Long supplierId;
+    @Schema(description = "供应商名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小番茄公司")
+    private String supplierName;
+
+    @Schema(description = "付款账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28989")
+    private Long accountId;
+    @Schema(description = "付款账户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    private String accountName;
+
+    @Schema(description = "合计价格,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "13832")
+    private BigDecimal totalPrice;
+
+    @Schema(description = "优惠金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "11600")
+    private BigDecimal discountPrice;
+
+    @Schema(description = "实际价格,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
+    private BigDecimal paymentPrice;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "创建人", example = "芋道")
+    private String creator;
+    @Schema(description = "创建人名称", example = "芋道")
+    private String creatorName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "付款项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "付款项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        private Integer bizType;
+
+        @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        private Long bizId;
+
+        @Schema(description = "业务单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        private String bizNo;
+
+        @Schema(description = "应付欠款,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
+        private BigDecimal totalPrice;
+
+        @Schema(description = "已付欠款,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
+        private BigDecimal paidPrice;
+
+        @Schema(description = "本次付款,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
+        @NotNull(message = "本次付款不能为空")
+        private BigDecimal paymentPrice;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentSaveReqVO.java
new file mode 100644
index 000000000..d7e3ddb27
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentSaveReqVO.java
@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 付款单新增/修改 Request VO")
+@Data
+public class ErpFinancePaymentSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23752")
+    private Long id;
+
+    @Schema(description = "付款时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "付款时间不能为空")
+    private LocalDateTime paymentTime;
+
+    @Schema(description = "财务人员编号", example = "19690")
+    private Long financeUserId;
+
+    @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29399")
+    @NotNull(message = "供应商编号不能为空")
+    private Long supplierId;
+
+    @Schema(description = "付款账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28989")
+    @NotNull(message = "付款账户编号不能为空")
+    private Long accountId;
+
+    @Schema(description = "优惠金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "11600")
+    @NotNull(message = "优惠金额不能为空")
+    private BigDecimal discountPrice;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "付款项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "付款项列表不能为空")
+    @Valid
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "付款项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+        @NotNull(message = "业务类型不能为空")
+        private Integer bizType;
+
+        @Schema(description = "业务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        @NotNull(message = "业务编号不能为空")
+        private Long bizId;
+
+        @Schema(description = "已付欠款,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
+        @NotNull(message = "已付欠款不能为空")
+        private BigDecimal paidPrice;
+
+        @Schema(description = "本次付款,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
+        @NotNull(message = "本次付款不能为空")
+        private BigDecimal paymentPrice;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInController.java
new file mode 100644
index 000000000..d33c7ae4d
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInController.java
@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in.ErpPurchaseInPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in.ErpPurchaseInRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in.ErpPurchaseInSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockDO;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 采购入库")
+@RestController
+@RequestMapping("/erp/purchase-in")
+@Validated
+public class ErpPurchaseInController {
+
+    @Resource
+    private ErpPurchaseInService purchaseInService;
+    @Resource
+    private ErpStockService stockService;
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpSupplierService supplierService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建采购入库")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-in:create')")
+    public CommonResult<Long> createPurchaseIn(@Valid @RequestBody ErpPurchaseInSaveReqVO createReqVO) {
+        return success(purchaseInService.createPurchaseIn(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新采购入库")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-in:update')")
+    public CommonResult<Boolean> updatePurchaseIn(@Valid @RequestBody ErpPurchaseInSaveReqVO updateReqVO) {
+        purchaseInService.updatePurchaseIn(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "更新采购入库的状态")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-in:update-status')")
+    public CommonResult<Boolean> updatePurchaseInStatus(@RequestParam("id") Long id,
+                                                      @RequestParam("status") Integer status) {
+        purchaseInService.updatePurchaseInStatus(id, status);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除采购入库")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:purchase-in:delete')")
+    public CommonResult<Boolean> deletePurchaseIn(@RequestParam("ids") List<Long> ids) {
+        purchaseInService.deletePurchaseIn(ids);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得采购入库")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-in:query')")
+    public CommonResult<ErpPurchaseInRespVO> getPurchaseIn(@RequestParam("id") Long id) {
+        ErpPurchaseInDO purchaseIn = purchaseInService.getPurchaseIn(id);
+        if (purchaseIn == null) {
+            return success(null);
+        }
+        List<ErpPurchaseInItemDO> purchaseInItemList = purchaseInService.getPurchaseInItemListByInId(id);
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(purchaseInItemList, ErpPurchaseInItemDO::getProductId));
+        return success(BeanUtils.toBean(purchaseIn, ErpPurchaseInRespVO.class, purchaseInVO ->
+                purchaseInVO.setItems(BeanUtils.toBean(purchaseInItemList, ErpPurchaseInRespVO.Item.class, item -> {
+                    ErpStockDO stock = stockService.getStock(item.getProductId(), item.getWarehouseId());
+                    item.setStockCount(stock != null ? stock.getCount() : BigDecimal.ZERO);
+                    MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()));
+                }))));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得采购入库分页")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-in:query')")
+    public CommonResult<PageResult<ErpPurchaseInRespVO>> getPurchaseInPage(@Valid ErpPurchaseInPageReqVO pageReqVO) {
+        PageResult<ErpPurchaseInDO> pageResult = purchaseInService.getPurchaseInPage(pageReqVO);
+        return success(buildPurchaseInVOPageResult(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出采购入库 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-in:export')")
+    @OperateLog(type = EXPORT)
+    public void exportPurchaseInExcel(@Valid ErpPurchaseInPageReqVO pageReqVO,
+                                    HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpPurchaseInRespVO> list = buildPurchaseInVOPageResult(purchaseInService.getPurchaseInPage(pageReqVO)).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "采购入库.xls", "数据", ErpPurchaseInRespVO.class, list);
+    }
+
+    private PageResult<ErpPurchaseInRespVO> buildPurchaseInVOPageResult(PageResult<ErpPurchaseInDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        // 1.1 入库项
+        List<ErpPurchaseInItemDO> purchaseInItemList = purchaseInService.getPurchaseInItemListByInIds(
+                convertSet(pageResult.getList(), ErpPurchaseInDO::getId));
+        Map<Long, List<ErpPurchaseInItemDO>> purchaseInItemMap = convertMultiMap(purchaseInItemList, ErpPurchaseInItemDO::getInId);
+        // 1.2 产品信息
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(purchaseInItemList, ErpPurchaseInItemDO::getProductId));
+        // 1.3 供应商信息
+        Map<Long, ErpSupplierDO> supplierMap = supplierService.getSupplierMap(
+                convertSet(pageResult.getList(), ErpPurchaseInDO::getSupplierId));
+        // 1.4 管理员信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), purchaseIn -> Long.parseLong(purchaseIn.getCreator())));
+        // 2. 开始拼接
+        return BeanUtils.toBean(pageResult, ErpPurchaseInRespVO.class, purchaseIn -> {
+            purchaseIn.setItems(BeanUtils.toBean(purchaseInItemMap.get(purchaseIn.getId()), ErpPurchaseInRespVO.Item.class,
+                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
+            purchaseIn.setProductNames(CollUtil.join(purchaseIn.getItems(), ",", ErpPurchaseInRespVO.Item::getProductName));
+            MapUtils.findAndThen(supplierMap, purchaseIn.getSupplierId(), supplier -> purchaseIn.setSupplierName(supplier.getName()));
+            MapUtils.findAndThen(userMap, Long.parseLong(purchaseIn.getCreator()), user -> purchaseIn.setCreatorName(user.getNickname()));
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
new file mode 100644
index 000000000..203d2fec0
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
@@ -0,0 +1,164 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseOrderService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 采购订单")
+@RestController
+@RequestMapping("/erp/purchase-order")
+@Validated
+public class ErpPurchaseOrderController {
+
+    @Resource
+    private ErpPurchaseOrderService purchaseOrderService;
+    @Resource
+    private ErpStockService stockService;
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpSupplierService supplierService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建采购订单")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-create:create')")
+    public CommonResult<Long> createPurchaseOrder(@Valid @RequestBody ErpPurchaseOrderSaveReqVO createReqVO) {
+        return success(purchaseOrderService.createPurchaseOrder(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新采购订单")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-create:update')")
+    public CommonResult<Boolean> updatePurchaseOrder(@Valid @RequestBody ErpPurchaseOrderSaveReqVO updateReqVO) {
+        purchaseOrderService.updatePurchaseOrder(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "更新采购订单的状态")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-create:update-status')")
+    public CommonResult<Boolean> updatePurchaseOrderStatus(@RequestParam("id") Long id,
+                                                      @RequestParam("status") Integer status) {
+        purchaseOrderService.updatePurchaseOrderStatus(id, status);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除采购订单")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:purchase-create:delete')")
+    public CommonResult<Boolean> deletePurchaseOrder(@RequestParam("ids") List<Long> ids) {
+        purchaseOrderService.deletePurchaseOrder(ids);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得采购订单")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-create:query')")
+    public CommonResult<ErpPurchaseOrderRespVO> getPurchaseOrder(@RequestParam("id") Long id) {
+        ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.getPurchaseOrder(id);
+        if (purchaseOrder == null) {
+            return success(null);
+        }
+        List<ErpPurchaseOrderItemDO> purchaseOrderItemList = purchaseOrderService.getPurchaseOrderItemListByOrderId(id);
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(purchaseOrderItemList, ErpPurchaseOrderItemDO::getProductId));
+        return success(BeanUtils.toBean(purchaseOrder, ErpPurchaseOrderRespVO.class, purchaseOrderVO ->
+                purchaseOrderVO.setItems(BeanUtils.toBean(purchaseOrderItemList, ErpPurchaseOrderRespVO.Item.class, item -> {
+                    BigDecimal purchaseCount = stockService.getStockCount(item.getProductId());
+                    item.setStockCount(purchaseCount != null ? purchaseCount : BigDecimal.ZERO);
+                    MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()));
+                }))));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得采购订单分页")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-create:query')")
+    public CommonResult<PageResult<ErpPurchaseOrderRespVO>> getPurchaseOrderPage(@Valid ErpPurchaseOrderPageReqVO pageReqVO) {
+        PageResult<ErpPurchaseOrderDO> pageResult = purchaseOrderService.getPurchaseOrderPage(pageReqVO);
+        return success(buildPurchaseOrderVOPageResult(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出采购订单 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-create:export')")
+    @OperateLog(type = EXPORT)
+    public void exportPurchaseOrderExcel(@Valid ErpPurchaseOrderPageReqVO pageReqVO,
+                                    HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpPurchaseOrderRespVO> list = buildPurchaseOrderVOPageResult(purchaseOrderService.getPurchaseOrderPage(pageReqVO)).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "采购订单.xls", "数据", ErpPurchaseOrderRespVO.class, list);
+    }
+
+    private PageResult<ErpPurchaseOrderRespVO> buildPurchaseOrderVOPageResult(PageResult<ErpPurchaseOrderDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        // 1.1 订单项
+        List<ErpPurchaseOrderItemDO> purchaseOrderItemList = purchaseOrderService.getPurchaseOrderItemListByOrderIds(
+                convertSet(pageResult.getList(), ErpPurchaseOrderDO::getId));
+        Map<Long, List<ErpPurchaseOrderItemDO>> purchaseOrderItemMap = convertMultiMap(purchaseOrderItemList, ErpPurchaseOrderItemDO::getOrderId);
+        // 1.2 产品信息
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(purchaseOrderItemList, ErpPurchaseOrderItemDO::getProductId));
+        // 1.3 供应商信息
+        Map<Long, ErpSupplierDO> supplierMap = supplierService.getSupplierMap(
+                convertSet(pageResult.getList(), ErpPurchaseOrderDO::getSupplierId));
+        // 1.4 管理员信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), purchaseOrder -> Long.parseLong(purchaseOrder.getCreator())));
+        // 2. 开始拼接
+        return BeanUtils.toBean(pageResult, ErpPurchaseOrderRespVO.class, purchaseOrder -> {
+            purchaseOrder.setItems(BeanUtils.toBean(purchaseOrderItemMap.get(purchaseOrder.getId()), ErpPurchaseOrderRespVO.Item.class,
+                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
+            purchaseOrder.setProductNames(CollUtil.join(purchaseOrder.getItems(), ",", ErpPurchaseOrderRespVO.Item::getProductName));
+            MapUtils.findAndThen(supplierMap, purchaseOrder.getSupplierId(), supplier -> purchaseOrder.setSupplierName(supplier.getName()));
+            MapUtils.findAndThen(userMap, Long.parseLong(purchaseOrder.getCreator()), user -> purchaseOrder.setCreatorName(user.getNickname()));
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseReturnController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseReturnController.java
new file mode 100644
index 000000000..0df31bcf1
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseReturnController.java
@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns.ErpPurchaseReturnPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns.ErpPurchaseReturnRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns.ErpPurchaseReturnSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockDO;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseReturnService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 采购退货")
+@RestController
+@RequestMapping("/erp/purchase-return")
+@Validated
+public class ErpPurchaseReturnController {
+
+    @Resource
+    private ErpPurchaseReturnService purchaseReturnService;
+    @Resource
+    private ErpStockService stockService;
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpSupplierService supplierService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建采购退货")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-return:create')")
+    public CommonResult<Long> createPurchaseReturn(@Valid @RequestBody ErpPurchaseReturnSaveReqVO createReqVO) {
+        return success(purchaseReturnService.createPurchaseReturn(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新采购退货")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-return:update')")
+    public CommonResult<Boolean> updatePurchaseReturn(@Valid @RequestBody ErpPurchaseReturnSaveReqVO updateReqVO) {
+        purchaseReturnService.updatePurchaseReturn(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "更新采购退货的状态")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-return:update-status')")
+    public CommonResult<Boolean> updatePurchaseReturnStatus(@RequestParam("id") Long id,
+                                                      @RequestParam("status") Integer status) {
+        purchaseReturnService.updatePurchaseReturnStatus(id, status);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除采购退货")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:purchase-return:delete')")
+    public CommonResult<Boolean> deletePurchaseReturn(@RequestParam("ids") List<Long> ids) {
+        purchaseReturnService.deletePurchaseReturn(ids);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得采购退货")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-return:query')")
+    public CommonResult<ErpPurchaseReturnRespVO> getPurchaseReturn(@RequestParam("id") Long id) {
+        ErpPurchaseReturnDO purchaseReturn = purchaseReturnService.getPurchaseReturn(id);
+        if (purchaseReturn == null) {
+            return success(null);
+        }
+        List<ErpPurchaseReturnItemDO> purchaseReturnItemList = purchaseReturnService.getPurchaseReturnItemListByReturnId(id);
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(purchaseReturnItemList, ErpPurchaseReturnItemDO::getProductId));
+        return success(BeanUtils.toBean(purchaseReturn, ErpPurchaseReturnRespVO.class, purchaseReturnVO ->
+                purchaseReturnVO.setItems(BeanUtils.toBean(purchaseReturnItemList, ErpPurchaseReturnRespVO.Item.class, item -> {
+                    ErpStockDO stock = stockService.getStock(item.getProductId(), item.getWarehouseId());
+                    item.setStockCount(stock != null ? stock.getCount() : BigDecimal.ZERO);
+                    MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()));
+                }))));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得采购退货分页")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-return:query')")
+    public CommonResult<PageResult<ErpPurchaseReturnRespVO>> getPurchaseReturnPage(@Valid ErpPurchaseReturnPageReqVO pageReqVO) {
+        PageResult<ErpPurchaseReturnDO> pageResult = purchaseReturnService.getPurchaseReturnPage(pageReqVO);
+        return success(buildPurchaseReturnVOPageResult(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出采购退货 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-return:export')")
+    @OperateLog(type = EXPORT)
+    public void exportPurchaseReturnExcel(@Valid ErpPurchaseReturnPageReqVO pageReqVO,
+                                    HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpPurchaseReturnRespVO> list = buildPurchaseReturnVOPageResult(purchaseReturnService.getPurchaseReturnPage(pageReqVO)).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "采购退货.xls", "数据", ErpPurchaseReturnRespVO.class, list);
+    }
+
+    private PageResult<ErpPurchaseReturnRespVO> buildPurchaseReturnVOPageResult(PageResult<ErpPurchaseReturnDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        // 1.1 退货项
+        List<ErpPurchaseReturnItemDO> purchaseReturnItemList = purchaseReturnService.getPurchaseReturnItemListByReturnIds(
+                convertSet(pageResult.getList(), ErpPurchaseReturnDO::getId));
+        Map<Long, List<ErpPurchaseReturnItemDO>> purchaseReturnItemMap = convertMultiMap(purchaseReturnItemList, ErpPurchaseReturnItemDO::getReturnId);
+        // 1.2 产品信息
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(purchaseReturnItemList, ErpPurchaseReturnItemDO::getProductId));
+        // 1.3 供应商信息
+        Map<Long, ErpSupplierDO> supplierMap = supplierService.getSupplierMap(
+                convertSet(pageResult.getList(), ErpPurchaseReturnDO::getSupplierId));
+        // 1.4 管理员信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), purchaseReturn -> Long.parseLong(purchaseReturn.getCreator())));
+        // 2. 开始拼接
+        return BeanUtils.toBean(pageResult, ErpPurchaseReturnRespVO.class, purchaseReturn -> {
+            purchaseReturn.setItems(BeanUtils.toBean(purchaseReturnItemMap.get(purchaseReturn.getId()), ErpPurchaseReturnRespVO.Item.class,
+                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
+            purchaseReturn.setProductNames(CollUtil.join(purchaseReturn.getItems(), ",", ErpPurchaseReturnRespVO.Item::getProductName));
+            MapUtils.findAndThen(supplierMap, purchaseReturn.getSupplierId(), supplier -> purchaseReturn.setSupplierName(supplier.getName()));
+            MapUtils.findAndThen(userMap, Long.parseLong(purchaseReturn.getCreator()), user -> purchaseReturn.setCreatorName(user.getNickname()));
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpSupplierController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpSupplierController.java
index 60893ea57..88253286d 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpSupplierController.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpSupplierController.java
@@ -10,7 +10,7 @@ import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier.ErpSupplierPageReqVO;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier.ErpSupplierRespVO;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier.ErpSupplierSaveReqVO;
-import cn.iocoder.yudao.module.erp.dal.dataobject.supplier.ErpSupplierDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
 import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInPageReqVO.java
new file mode 100644
index 000000000..e84607ce4
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInPageReqVO.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - ERP 采购入库分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpPurchaseInPageReqVO extends PageParam {
+
+    public static final Integer PAYMENT_STATUS_NONE = 0;
+    public static final Integer PAYMENT_STATUS_PART = 1;
+    public static final Integer PAYMENT_STATUS_ALL = 2;
+
+    @Schema(description = "采购单编号", example = "XS001")
+    private String no;
+
+    @Schema(description = "供应商编号", example = "1724")
+    private Long supplierId;
+
+    @Schema(description = "入库时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] inTime;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "入库状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "创建者")
+    private String creator;
+
+    @Schema(description = "产品编号", example = "1")
+    private Long productId;
+
+    @Schema(description = "仓库编号", example = "1")
+    private Long warehouseId;
+
+    @Schema(description = "结算账号编号", example = "1")
+    private Long accountId;
+
+    @Schema(description = "付款状态", example = "1")
+    private Integer paymentStatus;
+
+    @Schema(description = "是否可付款", example = "true")
+    private Boolean paymentEnable; // 对应 paymentStatus = [0, 1]
+
+    @Schema(description = "采购单号", example = "1")
+    private String orderNo;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java
new file mode 100644
index 000000000..beeeab869
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java
@@ -0,0 +1,145 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 采购入库 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpPurchaseInRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "入库单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
+    @ExcelProperty("入库单编号")
+    private String no;
+
+    @Schema(description = "入库状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("入库状态")
+    private Integer status;
+
+    @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1724")
+    private Long supplierId;
+    @Schema(description = "供应商名称", example = "芋道")
+    @ExcelProperty("供应商名称")
+    private String supplierName;
+
+    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "311.89")
+    @ExcelProperty("结算账户编号")
+    private Long accountId;
+
+    @Schema(description = "入库时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("入库时间")
+    private LocalDateTime inTime;
+
+    @Schema(description = "采购订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    private Long orderId;
+    @Schema(description = "采购订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
+    private String orderNo;
+
+    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
+    @ExcelProperty("合计数量")
+    private BigDecimal totalCount;
+    @Schema(description = "最终合计价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "24906")
+    @ExcelProperty("最终合计价格")
+    private BigDecimal totalPrice;
+    @Schema(description = "已付款金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal paymentPrice;
+
+    @Schema(description = "合计产品价格,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalProductPrice;
+
+    @Schema(description = "合计税额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalTaxPrice;
+
+    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "优惠金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal discountPrice;
+
+    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal otherPrice;
+
+    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
+    @ExcelProperty("附件地址")
+    private String fileUrl;
+
+    @Schema(description = "备注", example = "你猜")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "创建人", example = "芋道")
+    private String creator;
+    @Schema(description = "创建人名称", example = "芋道")
+    private String creatorName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "入库项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Item> items;
+
+    @Schema(description = "产品信息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("产品信息")
+    private String productNames;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "入库项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "采购订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        private Long orderItemId;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "税额,单位:元", example = "100.00")
+        private BigDecimal taxPrice;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+        // ========== 关联字段 ==========
+
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
+        private String productBarCode;
+        @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
+        private String productUnitName;
+
+        @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal stockCount; // 该字段仅仅在“详情”和“编辑”时使用
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInSaveReqVO.java
new file mode 100644
index 000000000..80edeec6d
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInSaveReqVO.java
@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 采购入库新增/修改 Request VO")
+@Data
+public class ErpPurchaseInSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    private Long id;
+
+    @Schema(description = "结算账户编号", example = "31189")
+    private Long accountId;
+
+    @Schema(description = "入库时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "入库时间不能为空")
+    private LocalDateTime inTime;
+
+    @Schema(description = "采购订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    @NotNull(message = "采购订单编号不能为空")
+    private Long orderId;
+
+    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "其它金额,单位:元", example = "7127")
+    private BigDecimal otherPrice;
+
+    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
+    private String fileUrl;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "入库清单列表")
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "入库项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "采购订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        @NotNull(message = "采购订单项编号不能为空")
+        private Long orderItemId;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "仓库编号不能为空")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品编号不能为空")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品单位单位不能为空")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderPageReqVO.java
new file mode 100644
index 000000000..8bf70d427
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderPageReqVO.java
@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - ERP 采购订单分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpPurchaseOrderPageReqVO extends PageParam {
+
+    /**
+     * 入库状态 - 无
+     */
+    public static final Integer IN_STATUS_NONE = 0;
+    /**
+     * 入库状态 - 部分
+     */
+    public static final Integer IN_STATUS_PART = 1;
+    /**
+     * 入库状态 - 全部
+     */
+    public static final Integer IN_STATUS_ALL = 2;
+
+    /**
+     * 退货状态 - 无
+     */
+    public static final Integer RETURN_STATUS_NONE = 0;
+    /**
+     * 退货状态 - 部分
+     */
+    public static final Integer RETURN_STATUS_PART = 1;
+    /**
+     * 退货状态 - 全部
+     */
+    public static final Integer RETURN_STATUS_ALL = 2;
+
+    @Schema(description = "采购单编号", example = "XS001")
+    private String no;
+
+    @Schema(description = "供应商编号", example = "1724")
+    private Long supplierId;
+
+    @Schema(description = "采购时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] orderTime;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "采购状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "创建者")
+    private String creator;
+
+    @Schema(description = "产品编号", example = "1")
+    private Long productId;
+
+    @Schema(description = "入库状态", example = "2")
+    private Integer inStatus;
+
+    @Schema(description = "退货状态", example = "2")
+    private Integer returnStatus;
+
+    @Schema(description = "是否可入库", example = "true")
+    private Boolean inEnable;
+
+    @Schema(description = "是否可退货", example = "true")
+    private Boolean returnEnable;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
new file mode 100644
index 000000000..bc76720ee
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
@@ -0,0 +1,152 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 采购订单 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpPurchaseOrderRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "采购单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
+    @ExcelProperty("采购单编号")
+    private String no;
+
+    @Schema(description = "采购状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("采购状态")
+    private Integer status;
+
+    @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1724")
+    private Long supplierId;
+    @Schema(description = "供应商名称", example = "芋道")
+    @ExcelProperty("供应商名称")
+    private String supplierName;
+
+    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "311.89")
+    @ExcelProperty("结算账户编号")
+    private Long accountId;
+
+    @Schema(description = "采购时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("采购时间")
+    private LocalDateTime orderTime;
+
+    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
+    @ExcelProperty("合计数量")
+    private BigDecimal totalCount;
+    @Schema(description = "最终合计价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "24906")
+    @ExcelProperty("最终合计价格")
+    private BigDecimal totalPrice;
+
+    @Schema(description = "合计产品价格,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalProductPrice;
+
+    @Schema(description = "合计税额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalTaxPrice;
+
+    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "优惠金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal discountPrice;
+
+    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal depositPrice;
+
+    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
+    @ExcelProperty("附件地址")
+    private String fileUrl;
+
+    @Schema(description = "备注", example = "你猜")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "创建人", example = "芋道")
+    private String creator;
+    @Schema(description = "创建人名称", example = "芋道")
+    private String creatorName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "订单项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Item> items;
+
+    @Schema(description = "产品信息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("产品信息")
+    private String productNames;
+
+    // ========== 采购入库 ==========
+
+    @Schema(description = "采购入库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+    private BigDecimal inCount;
+
+    // ========== 采购退货(出库)) ==========
+
+    @Schema(description = "采购退货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+    private BigDecimal returnCount;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "订单项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "税额,单位:元", example = "100.00")
+        private BigDecimal taxPrice;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+        // ========== 采购入库 ==========
+
+        @Schema(description = "采购入库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal inCount;
+
+        // ========== 采购退货(入库)) ==========
+
+        @Schema(description = "采购退货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal returnCount;
+
+        // ========== 关联字段 ==========
+
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
+        private String productBarCode;
+        @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
+        private String productUnitName;
+
+        @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal stockCount; // 该字段仅仅在“详情”和“编辑”时使用
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java
new file mode 100644
index 000000000..061ed9dcc
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java
@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 采购订单新增/修改 Request VO")
+@Data
+public class ErpPurchaseOrderSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    private Long id;
+
+    @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1724")
+    @NotNull(message = "供应商编号不能为空")
+    private Long supplierId;
+
+    @Schema(description = "结算账户编号", example = "31189")
+    private Long accountId;
+
+    @Schema(description = "采购时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "采购时间不能为空")
+    private LocalDateTime orderTime;
+
+    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "定金金额,单位:元", example = "7127")
+    private BigDecimal depositPrice;
+
+    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
+    private String fileUrl;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "订单清单列表")
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "订单项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品编号不能为空")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品单位单位不能为空")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnPageReqVO.java
new file mode 100644
index 000000000..a534d2e3e
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnPageReqVO.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - ERP 采购退货分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpPurchaseReturnPageReqVO extends PageParam {
+
+    public static final Integer REFUND_STATUS_NONE = 0;
+    public static final Integer REFUND_STATUS_PART = 1;
+    public static final Integer REFUND_STATUS_ALL = 2;
+
+    @Schema(description = "采购单编号", example = "XS001")
+    private String no;
+
+    @Schema(description = "供应商编号", example = "1724")
+    private Long supplierId;
+
+    @Schema(description = "退货时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] returnTime;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "退货状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "创建者")
+    private String creator;
+
+    @Schema(description = "产品编号", example = "1")
+    private Long productId;
+
+    @Schema(description = "仓库编号", example = "1")
+    private Long warehouseId;
+
+    @Schema(description = "结算账号编号", example = "1")
+    private Long accountId;
+
+    @Schema(description = "采购单号", example = "1")
+    private String orderNo;
+
+    @Schema(description = "退款状态", example = "1")
+    private Integer refundStatus;
+
+    @Schema(description = "是否可退款", example = "true")
+    private Boolean refundEnable; // 对应 refundStatus = [0, 1]
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java
new file mode 100644
index 000000000..223b9327e
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java
@@ -0,0 +1,145 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 采购退货 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpPurchaseReturnRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "退货单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
+    @ExcelProperty("退货单编号")
+    private String no;
+
+    @Schema(description = "退货状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("退货状态")
+    private Integer status;
+
+    @Schema(description = "供应商编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1724")
+    private Long supplierId;
+    @Schema(description = "供应商名称", example = "芋道")
+    @ExcelProperty("供应商名称")
+    private String supplierName;
+
+    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "311.89")
+    @ExcelProperty("结算账户编号")
+    private Long accountId;
+
+    @Schema(description = "退货时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("退货时间")
+    private LocalDateTime returnTime;
+
+    @Schema(description = "采购订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    private Long orderId;
+    @Schema(description = "采购订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
+    private String orderNo;
+
+    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
+    @ExcelProperty("合计数量")
+    private BigDecimal totalCount;
+    @Schema(description = "最终合计价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "24906")
+    @ExcelProperty("最终合计价格")
+    private BigDecimal totalPrice;
+    @Schema(description = "已退款金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal refundPrice;
+
+    @Schema(description = "合计产品价格,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalProductPrice;
+
+    @Schema(description = "合计税额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalTaxPrice;
+
+    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "优惠金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal discountPrice;
+
+    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal otherPrice;
+
+    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
+    @ExcelProperty("附件地址")
+    private String fileUrl;
+
+    @Schema(description = "备注", example = "你猜")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "创建人", example = "芋道")
+    private String creator;
+    @Schema(description = "创建人名称", example = "芋道")
+    private String creatorName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "退货项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Item> items;
+
+    @Schema(description = "产品信息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("产品信息")
+    private String productNames;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "退货项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "采购订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        private Long orderItemId;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "税额,单位:元", example = "100.00")
+        private BigDecimal taxPrice;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+        // ========== 关联字段 ==========
+
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
+        private String productBarCode;
+        @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
+        private String productUnitName;
+
+        @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal stockCount; // 该字段仅仅在“详情”和“编辑”时使用
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnSaveReqVO.java
new file mode 100644
index 000000000..9254bd583
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnSaveReqVO.java
@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 采购退货新增/修改 Request VO")
+@Data
+public class ErpPurchaseReturnSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    private Long id;
+
+    @Schema(description = "结算账户编号", example = "31189")
+    private Long accountId;
+
+    @Schema(description = "退货时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "退货时间不能为空")
+    private LocalDateTime returnTime;
+
+    @Schema(description = "采购订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    @NotNull(message = "采购订单编号不能为空")
+    private Long orderId;
+
+    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "其它金额,单位:元", example = "7127")
+    private BigDecimal otherPrice;
+
+    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
+    private String fileUrl;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "退货清单列表")
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "退货项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "采购订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        @NotNull(message = "采购订单项编号不能为空")
+        private Long orderItemId;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "仓库编号不能为空")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品编号不能为空")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品单位单位不能为空")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpCustomerController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpCustomerController.java
new file mode 100644
index 000000000..2c2886460
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpCustomerController.java
@@ -0,0 +1,102 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer.ErpCustomerPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer.ErpCustomerRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer.ErpCustomerSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
+import cn.iocoder.yudao.module.erp.service.sale.ErpCustomerService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 客户")
+@RestController
+@RequestMapping("/erp/customer")
+@Validated
+public class ErpCustomerController {
+
+    @Resource
+    private ErpCustomerService customerService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建客户")
+    @PreAuthorize("@ss.hasPermission('erp:customer:create')")
+    public CommonResult<Long> createCustomer(@Valid @RequestBody ErpCustomerSaveReqVO createReqVO) {
+        return success(customerService.createCustomer(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新客户")
+    @PreAuthorize("@ss.hasPermission('erp:customer:update')")
+    public CommonResult<Boolean> updateCustomer(@Valid @RequestBody ErpCustomerSaveReqVO updateReqVO) {
+        customerService.updateCustomer(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除客户")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:customer:delete')")
+    public CommonResult<Boolean> deleteCustomer(@RequestParam("id") Long id) {
+        customerService.deleteCustomer(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得客户")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:customer:query')")
+    public CommonResult<ErpCustomerRespVO> getCustomer(@RequestParam("id") Long id) {
+        ErpCustomerDO customer = customerService.getCustomer(id);
+        return success(BeanUtils.toBean(customer, ErpCustomerRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得客户分页")
+    @PreAuthorize("@ss.hasPermission('erp:customer:query')")
+    public CommonResult<PageResult<ErpCustomerRespVO>> getCustomerPage(@Valid ErpCustomerPageReqVO pageReqVO) {
+        PageResult<ErpCustomerDO> pageResult = customerService.getCustomerPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, ErpCustomerRespVO.class));
+    }
+
+    @GetMapping("/simple-list")
+    @Operation(summary = "获得客户精简列表", description = "只包含被开启的客户,主要用于前端的下拉选项")
+    public CommonResult<List<ErpCustomerRespVO>> getCustomerSimpleList() {
+        List<ErpCustomerDO> list = customerService.getCustomerListByStatus(CommonStatusEnum.ENABLE.getStatus());
+        return success(convertList(list, customer -> new ErpCustomerRespVO().setId(customer.getId()).setName(customer.getName())));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出客户 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:customer:export')")
+    @OperateLog(type = EXPORT)
+    public void exportCustomerExcel(@Valid ErpCustomerPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpCustomerDO> list = customerService.getCustomerPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "客户.xls", "数据", ErpCustomerRespVO.class,
+                        BeanUtils.toBean(list, ErpCustomerRespVO.class));
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.http b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.http
deleted file mode 100644
index f8a5c970e..000000000
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.http
+++ /dev/null
@@ -1,4 +0,0 @@
-### 请求 /transfer
-GET {{baseUrl}}/erp/sale-order/demo
-Authorization: Bearer {{token}}
-tenant-id: {{adminTenentId}}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
index ba1345166..0ca56a45e 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -1,16 +1,26 @@
 package cn.iocoder.yudao.module.erp.controller.admin.sale;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
 import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
 import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderRespVO;
 import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.sale.ErpCustomerService;
 import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOrderService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -22,9 +32,13 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.math.BigDecimal;
 import java.util.List;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 
 
@@ -36,65 +50,116 @@ public class ErpSaleOrderController {
 
     @Resource
     private ErpSaleOrderService saleOrderService;
+    @Resource
+    private ErpStockService stockService;
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpCustomerService customerService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
 
-    // TODO 芋艿:待 review
     @PostMapping("/create")
-    @Operation(summary = "创建ERP 销售订单")
-    @PreAuthorize("@ss.hasPermission('erp:sale-order:create')")
+    @Operation(summary = "创建销售订单")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:create')")
     public CommonResult<Long> createSaleOrder(@Valid @RequestBody ErpSaleOrderSaveReqVO createReqVO) {
         return success(saleOrderService.createSaleOrder(createReqVO));
     }
 
-    // TODO 芋艿:待 review
     @PutMapping("/update")
-    @Operation(summary = "更新ERP 销售订单")
-    @PreAuthorize("@ss.hasPermission('erp:sale-order:update')")
+    @Operation(summary = "更新销售订单")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:update')")
     public CommonResult<Boolean> updateSaleOrder(@Valid @RequestBody ErpSaleOrderSaveReqVO updateReqVO) {
         saleOrderService.updateSaleOrder(updateReqVO);
         return success(true);
     }
 
-    // TODO 芋艿:待 review
-    @DeleteMapping("/delete")
-    @Operation(summary = "删除ERP 销售订单")
-    @Parameter(name = "id", description = "编号", required = true)
-    @PreAuthorize("@ss.hasPermission('erp:sale-order:delete')")
-    public CommonResult<Boolean> deleteSaleOrder(@RequestParam("id") Long id) {
-        saleOrderService.deleteSaleOrder(id);
+    @PutMapping("/update-status")
+    @Operation(summary = "更新销售订单的状态")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:update-status')")
+    public CommonResult<Boolean> updateSaleOrderStatus(@RequestParam("id") Long id,
+                                                      @RequestParam("status") Integer status) {
+        saleOrderService.updateSaleOrderStatus(id, status);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除销售订单")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:delete')")
+    public CommonResult<Boolean> deleteSaleOrder(@RequestParam("ids") List<Long> ids) {
+        saleOrderService.deleteSaleOrder(ids);
         return success(true);
     }
 
-    // TODO 芋艿:待 review
     @GetMapping("/get")
-    @Operation(summary = "获得ERP 销售订单")
+    @Operation(summary = "获得销售订单")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
-    @PreAuthorize("@ss.hasPermission('erp:sale-order:query')")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:query')")
     public CommonResult<ErpSaleOrderRespVO> getSaleOrder(@RequestParam("id") Long id) {
         ErpSaleOrderDO saleOrder = saleOrderService.getSaleOrder(id);
-        return success(BeanUtils.toBean(saleOrder, ErpSaleOrderRespVO.class));
+        if (saleOrder == null) {
+            return success(null);
+        }
+        List<ErpSaleOrderItemDO> saleOrderItemList = saleOrderService.getSaleOrderItemListByOrderId(id);
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getProductId));
+        return success(BeanUtils.toBean(saleOrder, ErpSaleOrderRespVO.class, saleOrderVO ->
+                saleOrderVO.setItems(BeanUtils.toBean(saleOrderItemList, ErpSaleOrderRespVO.Item.class, item -> {
+                    BigDecimal stockCount = stockService.getStockCount(item.getProductId());
+                    item.setStockCount(stockCount != null ? stockCount : BigDecimal.ZERO);
+                    MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()));
+                }))));
     }
 
-    // TODO 芋艿:待 review
     @GetMapping("/page")
-    @Operation(summary = "获得ERP 销售订单分页")
-    @PreAuthorize("@ss.hasPermission('erp:sale-order:query')")
+    @Operation(summary = "获得销售订单分页")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:query')")
     public CommonResult<PageResult<ErpSaleOrderRespVO>> getSaleOrderPage(@Valid ErpSaleOrderPageReqVO pageReqVO) {
         PageResult<ErpSaleOrderDO> pageResult = saleOrderService.getSaleOrderPage(pageReqVO);
-        return success(BeanUtils.toBean(pageResult, ErpSaleOrderRespVO.class));
+        return success(buildSaleOrderVOPageResult(pageResult));
     }
 
-    // TODO 芋艿:待 review
     @GetMapping("/export-excel")
-    @Operation(summary = "导出ERP 销售订单 Excel")
-    @PreAuthorize("@ss.hasPermission('erp:sale-order:export')")
+    @Operation(summary = "导出销售订单 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:export')")
     @OperateLog(type = EXPORT)
     public void exportSaleOrderExcel(@Valid ErpSaleOrderPageReqVO pageReqVO,
-              HttpServletResponse response) throws IOException {
+                                    HttpServletResponse response) throws IOException {
         pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
-        List<ErpSaleOrderDO> list = saleOrderService.getSaleOrderPage(pageReqVO).getList();
+        List<ErpSaleOrderRespVO> list = buildSaleOrderVOPageResult(saleOrderService.getSaleOrderPage(pageReqVO)).getList();
         // 导出 Excel
-        ExcelUtils.write(response, "ERP 销售订单.xls", "数据", ErpSaleOrderRespVO.class,
-                        BeanUtils.toBean(list, ErpSaleOrderRespVO.class));
+        ExcelUtils.write(response, "销售订单.xls", "数据", ErpSaleOrderRespVO.class, list);
+    }
+
+    private PageResult<ErpSaleOrderRespVO> buildSaleOrderVOPageResult(PageResult<ErpSaleOrderDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        // 1.1 订单项
+        List<ErpSaleOrderItemDO> saleOrderItemList = saleOrderService.getSaleOrderItemListByOrderIds(
+                convertSet(pageResult.getList(), ErpSaleOrderDO::getId));
+        Map<Long, List<ErpSaleOrderItemDO>> saleOrderItemMap = convertMultiMap(saleOrderItemList, ErpSaleOrderItemDO::getOrderId);
+        // 1.2 产品信息
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getProductId));
+        // 1.3 客户信息
+        Map<Long, ErpCustomerDO> customerMap = customerService.getCustomerMap(
+                convertSet(pageResult.getList(), ErpSaleOrderDO::getCustomerId));
+        // 1.4 管理员信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), saleOrder -> Long.parseLong(saleOrder.getCreator())));
+        // 2. 开始拼接
+        return BeanUtils.toBean(pageResult, ErpSaleOrderRespVO.class, saleOrder -> {
+            saleOrder.setItems(BeanUtils.toBean(saleOrderItemMap.get(saleOrder.getId()), ErpSaleOrderRespVO.Item.class,
+                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
+            saleOrder.setProductNames(CollUtil.join(saleOrder.getItems(), ",", ErpSaleOrderRespVO.Item::getProductName));
+            MapUtils.findAndThen(customerMap, saleOrder.getCustomerId(), supplier -> saleOrder.setCustomerName(supplier.getName()));
+            MapUtils.findAndThen(userMap, Long.parseLong(saleOrder.getCreator()), user -> saleOrder.setCreatorName(user.getNickname()));
+        });
     }
 
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOutController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOutController.java
new file mode 100644
index 000000000..5875ea39f
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOutController.java
@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out.ErpSaleOutPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out.ErpSaleOutRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out.ErpSaleOutSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOutDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOutItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockDO;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.sale.ErpCustomerService;
+import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOutService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 销售出库")
+@RestController
+@RequestMapping("/erp/sale-out")
+@Validated
+public class ErpSaleOutController {
+
+    @Resource
+    private ErpSaleOutService saleOutService;
+    @Resource
+    private ErpStockService stockService;
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpCustomerService customerService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建销售出库")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:create')")
+    public CommonResult<Long> createSaleOut(@Valid @RequestBody ErpSaleOutSaveReqVO createReqVO) {
+        return success(saleOutService.createSaleOut(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新销售出库")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:update')")
+    public CommonResult<Boolean> updateSaleOut(@Valid @RequestBody ErpSaleOutSaveReqVO updateReqVO) {
+        saleOutService.updateSaleOut(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "更新销售出库的状态")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:update-status')")
+    public CommonResult<Boolean> updateSaleOutStatus(@RequestParam("id") Long id,
+                                                      @RequestParam("status") Integer status) {
+        saleOutService.updateSaleOutStatus(id, status);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除销售出库")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:delete')")
+    public CommonResult<Boolean> deleteSaleOut(@RequestParam("ids") List<Long> ids) {
+        saleOutService.deleteSaleOut(ids);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得销售出库")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:query')")
+    public CommonResult<ErpSaleOutRespVO> getSaleOut(@RequestParam("id") Long id) {
+        ErpSaleOutDO saleOut = saleOutService.getSaleOut(id);
+        if (saleOut == null) {
+            return success(null);
+        }
+        List<ErpSaleOutItemDO> saleOutItemList = saleOutService.getSaleOutItemListByOutId(id);
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(saleOutItemList, ErpSaleOutItemDO::getProductId));
+        return success(BeanUtils.toBean(saleOut, ErpSaleOutRespVO.class, saleOutVO ->
+                saleOutVO.setItems(BeanUtils.toBean(saleOutItemList, ErpSaleOutRespVO.Item.class, item -> {
+                    ErpStockDO stock = stockService.getStock(item.getProductId(), item.getWarehouseId());
+                    item.setStockCount(stock != null ? stock.getCount() : BigDecimal.ZERO);
+                    MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()));
+                }))));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得销售出库分页")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:query')")
+    public CommonResult<PageResult<ErpSaleOutRespVO>> getSaleOutPage(@Valid ErpSaleOutPageReqVO pageReqVO) {
+        PageResult<ErpSaleOutDO> pageResult = saleOutService.getSaleOutPage(pageReqVO);
+        return success(buildSaleOutVOPageResult(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出销售出库 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:sale-out:export')")
+    @OperateLog(type = EXPORT)
+    public void exportSaleOutExcel(@Valid ErpSaleOutPageReqVO pageReqVO,
+                                    HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpSaleOutRespVO> list = buildSaleOutVOPageResult(saleOutService.getSaleOutPage(pageReqVO)).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "销售出库.xls", "数据", ErpSaleOutRespVO.class, list);
+    }
+
+    private PageResult<ErpSaleOutRespVO> buildSaleOutVOPageResult(PageResult<ErpSaleOutDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        // 1.1 出库项
+        List<ErpSaleOutItemDO> saleOutItemList = saleOutService.getSaleOutItemListByOutIds(
+                convertSet(pageResult.getList(), ErpSaleOutDO::getId));
+        Map<Long, List<ErpSaleOutItemDO>> saleOutItemMap = convertMultiMap(saleOutItemList, ErpSaleOutItemDO::getOutId);
+        // 1.2 产品信息
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(saleOutItemList, ErpSaleOutItemDO::getProductId));
+        // 1.3 客户信息
+        Map<Long, ErpCustomerDO> customerMap = customerService.getCustomerMap(
+                convertSet(pageResult.getList(), ErpSaleOutDO::getCustomerId));
+        // 1.4 管理员信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), stockOut -> Long.parseLong(stockOut.getCreator())));
+        // 2. 开始拼接
+        return BeanUtils.toBean(pageResult, ErpSaleOutRespVO.class, saleOut -> {
+            saleOut.setItems(BeanUtils.toBean(saleOutItemMap.get(saleOut.getId()), ErpSaleOutRespVO.Item.class,
+                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
+            saleOut.setProductNames(CollUtil.join(saleOut.getItems(), ",", ErpSaleOutRespVO.Item::getProductName));
+            MapUtils.findAndThen(customerMap, saleOut.getCustomerId(), supplier -> saleOut.setCustomerName(supplier.getName()));
+            MapUtils.findAndThen(userMap, Long.parseLong(saleOut.getCreator()), user -> saleOut.setCreatorName(user.getNickname()));
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleReturnController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleReturnController.java
new file mode 100644
index 000000000..0dfba67e9
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleReturnController.java
@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns.ErpSaleReturnPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns.ErpSaleReturnRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns.ErpSaleReturnSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleReturnDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleReturnItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockDO;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.sale.ErpCustomerService;
+import cn.iocoder.yudao.module.erp.service.sale.ErpSaleReturnService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 销售退货")
+@RestController
+@RequestMapping("/erp/sale-return")
+@Validated
+public class ErpSaleReturnController {
+
+    @Resource
+    private ErpSaleReturnService saleReturnService;
+    @Resource
+    private ErpStockService stockService;
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpCustomerService customerService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建销售退货")
+    @PreAuthorize("@ss.hasPermission('erp:sale-return:create')")
+    public CommonResult<Long> createSaleReturn(@Valid @RequestBody ErpSaleReturnSaveReqVO createReqVO) {
+        return success(saleReturnService.createSaleReturn(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新销售退货")
+    @PreAuthorize("@ss.hasPermission('erp:sale-return:update')")
+    public CommonResult<Boolean> updateSaleReturn(@Valid @RequestBody ErpSaleReturnSaveReqVO updateReqVO) {
+        saleReturnService.updateSaleReturn(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "更新销售退货的状态")
+    @PreAuthorize("@ss.hasPermission('erp:sale-return:update-status')")
+    public CommonResult<Boolean> updateSaleReturnStatus(@RequestParam("id") Long id,
+                                                      @RequestParam("status") Integer status) {
+        saleReturnService.updateSaleReturnStatus(id, status);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除销售退货")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:sale-return:delete')")
+    public CommonResult<Boolean> deleteSaleReturn(@RequestParam("ids") List<Long> ids) {
+        saleReturnService.deleteSaleReturn(ids);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得销售退货")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:sale-return:query')")
+    public CommonResult<ErpSaleReturnRespVO> getSaleReturn(@RequestParam("id") Long id) {
+        ErpSaleReturnDO saleReturn = saleReturnService.getSaleReturn(id);
+        if (saleReturn == null) {
+            return success(null);
+        }
+        List<ErpSaleReturnItemDO> saleReturnItemList = saleReturnService.getSaleReturnItemListByReturnId(id);
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(saleReturnItemList, ErpSaleReturnItemDO::getProductId));
+        return success(BeanUtils.toBean(saleReturn, ErpSaleReturnRespVO.class, saleReturnVO ->
+                saleReturnVO.setItems(BeanUtils.toBean(saleReturnItemList, ErpSaleReturnRespVO.Item.class, item -> {
+                    ErpStockDO stock = stockService.getStock(item.getProductId(), item.getWarehouseId());
+                    item.setStockCount(stock != null ? stock.getCount() : BigDecimal.ZERO);
+                    MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()));
+                }))));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得销售退货分页")
+    @PreAuthorize("@ss.hasPermission('erp:sale-return:query')")
+    public CommonResult<PageResult<ErpSaleReturnRespVO>> getSaleReturnPage(@Valid ErpSaleReturnPageReqVO pageReqVO) {
+        PageResult<ErpSaleReturnDO> pageResult = saleReturnService.getSaleReturnPage(pageReqVO);
+        return success(buildSaleReturnVOPageResult(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出销售退货 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:sale-return:export')")
+    @OperateLog(type = EXPORT)
+    public void exportSaleReturnExcel(@Valid ErpSaleReturnPageReqVO pageReqVO,
+                                    HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpSaleReturnRespVO> list = buildSaleReturnVOPageResult(saleReturnService.getSaleReturnPage(pageReqVO)).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "销售退货.xls", "数据", ErpSaleReturnRespVO.class, list);
+    }
+
+    private PageResult<ErpSaleReturnRespVO> buildSaleReturnVOPageResult(PageResult<ErpSaleReturnDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        // 1.1 退货项
+        List<ErpSaleReturnItemDO> saleReturnItemList = saleReturnService.getSaleReturnItemListByReturnIds(
+                convertSet(pageResult.getList(), ErpSaleReturnDO::getId));
+        Map<Long, List<ErpSaleReturnItemDO>> saleReturnItemMap = convertMultiMap(saleReturnItemList, ErpSaleReturnItemDO::getReturnId);
+        // 1.2 产品信息
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(saleReturnItemList, ErpSaleReturnItemDO::getProductId));
+        // 1.3 客户信息
+        Map<Long, ErpCustomerDO> customerMap = customerService.getCustomerMap(
+                convertSet(pageResult.getList(), ErpSaleReturnDO::getCustomerId));
+        // 1.4 管理员信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), saleReturn -> Long.parseLong(saleReturn.getCreator())));
+        // 2. 开始拼接
+        return BeanUtils.toBean(pageResult, ErpSaleReturnRespVO.class, saleReturn -> {
+            saleReturn.setItems(BeanUtils.toBean(saleReturnItemMap.get(saleReturn.getId()), ErpSaleReturnRespVO.Item.class,
+                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
+            saleReturn.setProductNames(CollUtil.join(saleReturn.getItems(), ",", ErpSaleReturnRespVO.Item::getProductName));
+            MapUtils.findAndThen(customerMap, saleReturn.getCustomerId(), supplier -> saleReturn.setCustomerName(supplier.getName()));
+            MapUtils.findAndThen(userMap, Long.parseLong(saleReturn.getCreator()), user -> saleReturn.setCreatorName(user.getNickname()));
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerPageReqVO.java
new file mode 100644
index 000000000..e790cb958
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerPageReqVO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import java.math.BigDecimal;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - ERP 客户分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpCustomerPageReqVO extends PageParam {
+
+    @Schema(description = "客户名称", example = "张三")
+    private String name;
+
+    @Schema(description = "手机号码", example = "15601691300")
+    private String mobile;
+
+    @Schema(description = "联系电话", example = "15601691300")
+    private String telephone;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java
new file mode 100644
index 000000000..f1a58a03d
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import java.math.BigDecimal;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+@Schema(description = "管理后台 - ERP 客户 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpCustomerRespVO {
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "27520")
+    @ExcelProperty("客户编号")
+    private Long id;
+
+    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @ExcelProperty("客户名称")
+    private String name;
+
+    @Schema(description = "联系人", example = "老王")
+    @ExcelProperty("联系人")
+    private String contact;
+
+    @Schema(description = "手机号码", example = "15601691300")
+    @ExcelProperty("手机号码")
+    private String mobile;
+
+    @Schema(description = "联系电话", example = "15601691300")
+    @ExcelProperty("联系电话")
+    private String telephone;
+
+    @Schema(description = "电子邮箱", example = "7685323@qq.com")
+    @ExcelProperty("电子邮箱")
+    private String email;
+
+    @Schema(description = "传真", example = "20 7123 4567")
+    @ExcelProperty("传真")
+    private String fax;
+
+    @Schema(description = "备注", example = "你猜")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "开启状态", converter = DictConvert.class)
+    @DictFormat("common_status") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer status;
+
+    @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @ExcelProperty("排序")
+    private Integer sort;
+
+    @Schema(description = "纳税人识别号", example = "91130803MA098BY05W")
+    @ExcelProperty("纳税人识别号")
+    private String taxNo;
+
+    @Schema(description = "税率", example = "10")
+    @ExcelProperty("税率")
+    private BigDecimal taxPercent;
+
+    @Schema(description = "开户行", example = "芋艿")
+    @ExcelProperty("开户行")
+    private String bankName;
+
+    @Schema(description = "开户账号", example = "622908212277228617")
+    @ExcelProperty("开户账号")
+    private String bankAccount;
+
+    @Schema(description = "开户地址", example = "兴业银行浦东支行")
+    @ExcelProperty("开户地址")
+    private String bankAddress;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerSaveReqVO.java
new file mode 100644
index 000000000..aef0b2df1
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerSaveReqVO.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import jakarta.validation.constraints.*;
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - ERP 客户新增/修改 Request VO")
+@Data
+public class ErpCustomerSaveReqVO {
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "27520")
+    private Long id;
+
+    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @NotEmpty(message = "客户名称不能为空")
+    private String name;
+
+    @Schema(description = "联系人", example = "老王")
+    private String contact;
+
+    @Schema(description = "手机号码", example = "15601691300")
+    private String mobile;
+
+    @Schema(description = "联系电话", example = "15601691300")
+    private String telephone;
+
+    @Schema(description = "电子邮箱", example = "7685323@qq.com")
+    private String email;
+
+    @Schema(description = "传真", example = "20 7123 4567")
+    private String fax;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "开启状态不能为空")
+    private Integer status;
+
+    @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @NotNull(message = "排序不能为空")
+    private Integer sort;
+
+    @Schema(description = "纳税人识别号", example = "91130803MA098BY05W")
+    private String taxNo;
+
+    @Schema(description = "税率", example = "10")
+    private BigDecimal taxPercent;
+
+    @Schema(description = "开户行", example = "芋艿")
+    private String bankName;
+
+    @Schema(description = "开户账号", example = "622908212277228617")
+    private String bankAccount;
+
+    @Schema(description = "开户地址", example = "兴业银行浦东支行")
+    private String bankAddress;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java
index 8e46a64b0..84d92fb67 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderPageReqVO.java
@@ -17,6 +17,32 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class ErpSaleOrderPageReqVO extends PageParam {
 
+    /**
+     * 出库状态 - 无
+     */
+    public static final Integer OUT_STATUS_NONE = 0;
+    /**
+     * 出库状态 - 部分
+     */
+    public static final Integer OUT_STATUS_PART = 1;
+    /**
+     * 出库状态 - 全部
+     */
+    public static final Integer OUT_STATUS_ALL = 2;
+
+    /**
+     * 退货状态 - 无
+     */
+    public static final Integer RETURN_STATUS_NONE = 0;
+    /**
+     * 退货状态 - 部分
+     */
+    public static final Integer RETURN_STATUS_PART = 1;
+    /**
+     * 退货状态 - 全部
+     */
+    public static final Integer RETURN_STATUS_ALL = 2;
+
     @Schema(description = "销售单编号", example = "XS001")
     private String no;
 
@@ -28,7 +54,7 @@ public class ErpSaleOrderPageReqVO extends PageParam {
     private LocalDateTime[] orderTime;
 
     @Schema(description = "备注", example = "你猜")
-    private String description;
+    private String remark;
 
     @Schema(description = "销售状态", example = "2")
     private Integer status;
@@ -36,4 +62,19 @@ public class ErpSaleOrderPageReqVO extends PageParam {
     @Schema(description = "创建者")
     private String creator;
 
+    @Schema(description = "产品编号", example = "1")
+    private Long productId;
+
+    @Schema(description = "出库状态", example = "2")
+    private Integer outStatus;
+
+    @Schema(description = "退货状态", example = "2")
+    private Integer returnStatus;
+
+    @Schema(description = "是否可出库", example = "true")
+    private Boolean outEnable;
+
+    @Schema(description = "是否可退货", example = "true")
+    private Boolean returnEnable;
+
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
index ea24e08ca..e5958a841 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -3,12 +3,13 @@ package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order;
 import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
 import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.List;
 
-// TODO 芋艿:导出最后搞
 @Schema(description = "管理后台 - ERP 销售订单 Response VO")
 @Data
 @ExcelIgnoreUnannotated
@@ -22,57 +23,133 @@ public class ErpSaleOrderRespVO {
     @ExcelProperty("销售单编号")
     private String no;
 
+    @Schema(description = "销售状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("销售状态")
+    private Integer status;
+
     @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1724")
-    @ExcelProperty("客户编号")
     private Long customerId;
+    @Schema(description = "客户名称", example = "芋道")
+    @ExcelProperty("客户名称")
+    private String customerName;
+
+    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "311.89")
+    @ExcelProperty("结算账户编号")
+    private Long accountId;
+
+    @Schema(description = "销售员编号", example = "1888")
+    private Long saleUserId;
 
     @Schema(description = "下单时间", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("下单时间")
     private LocalDateTime orderTime;
 
-    // TODO 芋艿:example 后面
-    @Schema(description = "销售员编号数组")
-    @ExcelProperty("销售员编号数组")
-    private String salePersonIds;
-
-    @Schema(description = "合计价格,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "26094")
-    @ExcelProperty("合计价格,单位:元")
+    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
+    @ExcelProperty("合计数量")
+    private BigDecimal totalCount;
+    @Schema(description = "最终合计价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "24906")
+    @ExcelProperty("最终合计价格")
     private BigDecimal totalPrice;
 
+    @Schema(description = "合计产品价格,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalProductPrice;
+
+    @Schema(description = "合计税额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalTaxPrice;
+
     @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
-    @ExcelProperty("优惠率,百分比")
     private BigDecimal discountPercent;
 
-    @Schema(description = "优惠金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "44.52")
-    @ExcelProperty("优惠金额,单位:元")
+    @Schema(description = "优惠金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
     private BigDecimal discountPrice;
 
-    @Schema(description = "支付金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "322.40")
-    @ExcelProperty("支付金额,单位:元")
-    private BigDecimal payPrice;
-
-    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "71.27")
-    @ExcelProperty("定金金额,单位:元")
+    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
     private BigDecimal depositPrice;
 
     @Schema(description = "附件地址", example = "https://www.iocoder.cn")
     @ExcelProperty("附件地址")
     private String fileUrl;
 
-    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "311.89")
-    @ExcelProperty("结算账户编号")
-    private Long accountId;
-
     @Schema(description = "备注", example = "你猜")
     @ExcelProperty("备注")
-    private String description;
+    private String remark;
 
-    @Schema(description = "销售状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-    @ExcelProperty("销售状态")
-    private Integer status;
+    @Schema(description = "创建人", example = "芋道")
+    private String creator;
+    @Schema(description = "创建人名称", example = "芋道")
+    private String creatorName;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("创建时间")
     private LocalDateTime createTime;
 
+    @Schema(description = "订单项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Item> items;
+
+    @Schema(description = "产品信息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("产品信息")
+    private String productNames;
+
+    // ========== 销售出库 ==========
+
+    @Schema(description = "销售出库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+    private BigDecimal outCount;
+
+    // ========== 销售退货(出库)) ==========
+
+    @Schema(description = "销售退货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+    private BigDecimal returnCount;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "订单项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "税额,单位:元", example = "100.00")
+        private BigDecimal taxPrice;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+        // ========== 销售出库 ==========
+
+        @Schema(description = "销售出库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal outCount;
+
+        // ========== 销售退货(入库)) ==========
+
+        @Schema(description = "销售退货数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal returnCount;
+
+        // ========== 关联字段 ==========
+
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
+        private String productBarCode;
+        @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
+        private String productUnitName;
+
+        @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal stockCount; // 该字段仅仅在“详情”和“编辑”时使用
+
+    }
+
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java
index e40b38902..e23a1fab3 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java
@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
@@ -16,10 +15,6 @@ public class ErpSaleOrderSaveReqVO {
     @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
     private Long id;
 
-    @Schema(description = "销售单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
-    @NotEmpty(message = "销售单编号不能为空")
-    private String no;
-
     @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1724")
     @NotNull(message = "客户编号不能为空")
     private Long customerId;
@@ -28,100 +23,53 @@ public class ErpSaleOrderSaveReqVO {
     @NotNull(message = "下单时间不能为空")
     private LocalDateTime orderTime;
 
-    @Schema(description = "销售员编号数组")
-    private List<Long> salePersonIds;
+    @Schema(description = "销售员编号", example = "1888")
+    private Long saleUserId;
 
-    @Schema(description = "合计价格,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "26094")
-    @NotNull(message = "合计价格,单位:元不能为空")
-    private BigDecimal totalPrice;
+    @Schema(description = "结算账户编号", example = "31189")
+    private Long accountId;
 
     @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
-    @NotNull(message = "优惠率,百分比不能为空")
     private BigDecimal discountPercent;
 
-    @Schema(description = "优惠金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "4452")
-    @NotNull(message = "优惠金额,单位:元不能为空")
-    private BigDecimal discountPrice;
-
-    // TODO 芋艿:后面删除
-//    @Schema(description = "支付金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "32240")
-//    @NotNull(message = "支付金额,单位:元不能为空")
-//    private BigDecimal payPrice;
-
-    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
-    @NotNull(message = "定金金额,单位:元不能为空")
+    @Schema(description = "定金金额,单位:元", example = "7127")
     private BigDecimal depositPrice;
 
     @Schema(description = "附件地址", example = "https://www.iocoder.cn")
     private String fileUrl;
 
-    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31189")
-    @NotNull(message = "结算账户编号不能为空")
-    private Long accountId;
-
     @Schema(description = "备注", example = "你猜")
-    private String description;
+    private String remark;
 
-    // TODO 芋艿:后面删除
-//    @Schema(description = "销售状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
-//    @NotNull(message = "销售状态不能为空")
-//    private Integer status;
+    @Schema(description = "订单清单列表")
+    private List<Item> items;
 
-    @Schema(description = "ERP 销售订单明细列表")
-    private List<Item> salesOrderItems;
-
-    @Schema(description = "管理后台 - ERP 销售订单明细新增/修改 Request VO")
     @Data
-    public class Item {
+    public static class Item {
 
-        @Schema(description = "编号", example = "20704")
+        @Schema(description = "订单项编号", example = "11756")
         private Long id;
 
-        // TODO 芋艿:后面删除
-//        @Schema(description = "销售订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30765")
-//        @NotNull(message = "销售订单编号不能为空")
-//        private Long orderId;
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品编号不能为空")
+        private Long productId;
 
-//        @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5574")
-//        @NotNull(message = "商品 SPU 编号不能为空")
-//        private Long productSpuId;
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品单位单位不能为空")
+        private Long productUnitId;
 
-        @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21273")
-        @NotNull(message = "商品 SKU 编号不能为空")
-        private Long productSkuId;
-
-        @Schema(description = "商品单位", requiredMode = Schema.RequiredMode.REQUIRED)
-        @NotEmpty(message = "商品单位不能为空")
-        private String productUnit;
-
-        @Schema(description = "商品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "6897")
-        @NotNull(message = "商品单价不能为空")
+        @Schema(description = "产品单价", example = "100.00")
         private BigDecimal productPrice;
 
-        @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "22100")
-        @NotNull(message = "数量不能为空")
-        private Integer count;
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
 
-        // TODO 芋艿:后面删除
-//        @Schema(description = "总价", requiredMode = Schema.RequiredMode.REQUIRED, example = "26868")
-//        @NotNull(message = "总价不能为空")
-//        private BigDecimal totalPrice;
-
-        @Schema(description = "备注", example = "你说的对")
-        private String description;
-
-        @Schema(description = "税率,百分比", requiredMode = Schema.RequiredMode.REQUIRED)
-        @NotNull(message = "税率,百分比不能为空")
+        @Schema(description = "税率,百分比", example = "99.88")
         private BigDecimal taxPercent;
 
-        @Schema(description = "税额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "15791")
-        @NotNull(message = "税额,单位:元不能为空")
-        private BigDecimal taxPrice;
-
-        // TODO 芋艿:后面删除
-//        @Schema(description = "支付金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "21930")
-//        @NotNull(message = "支付金额,单位:元不能为空")
-//        private BigDecimal payPrice;
+        @Schema(description = "备注", example = "随便")
+        private String remark;
 
     }
 
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutPageReqVO.java
new file mode 100644
index 000000000..c422a6088
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutPageReqVO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - ERP 销售出库分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpSaleOutPageReqVO extends PageParam {
+
+    @Schema(description = "销售单编号", example = "XS001")
+    private String no;
+
+    @Schema(description = "客户编号", example = "1724")
+    private Long customerId;
+
+    @Schema(description = "出库时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] outTime;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "出库状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "创建者")
+    private String creator;
+
+    @Schema(description = "产品编号", example = "1")
+    private Long productId;
+
+    @Schema(description = "仓库编号", example = "1")
+    private Long warehouseId;
+
+    @Schema(description = "结算账号编号", example = "1")
+    private Long accountId;
+
+    @Schema(description = "是否欠款", example = "true")
+    private Boolean debtStatus;
+
+    @Schema(description = "销售单号", example = "1")
+    private String orderNo;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java
new file mode 100644
index 000000000..4f6cc49ab
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java
@@ -0,0 +1,152 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 销售出库 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpSaleOutRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "出库单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
+    @ExcelProperty("出库单编号")
+    private String no;
+
+    @Schema(description = "出库状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("出库状态")
+    private Integer status;
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1724")
+    private Long customerId;
+    @Schema(description = "客户名称", example = "芋道")
+    @ExcelProperty("客户名称")
+    private String customerName;
+
+    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "311.89")
+    @ExcelProperty("结算账户编号")
+    private Long accountId;
+
+    @Schema(description = "出库员编号", example = "1888")
+    private Long saleUserId;
+
+    @Schema(description = "出库时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("出库时间")
+    private LocalDateTime outTime;
+
+    @Schema(description = "销售订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    private Long orderId;
+    @Schema(description = "销售订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
+    private String orderNo;
+
+    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
+    @ExcelProperty("合计数量")
+    private BigDecimal totalCount;
+    @Schema(description = "最终合计价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "24906")
+    @ExcelProperty("最终合计价格")
+    private BigDecimal totalPrice;
+
+    @Schema(description = "合计产品价格,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalProductPrice;
+
+    @Schema(description = "合计税额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalTaxPrice;
+
+    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "优惠金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal discountPrice;
+
+    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal otherPrice;
+
+    @Schema(description = "本次收款,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal payPrice;
+    @Schema(description = "本次欠款,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
+    private BigDecimal debtPrice;
+
+
+    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
+    @ExcelProperty("附件地址")
+    private String fileUrl;
+
+    @Schema(description = "备注", example = "你猜")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "创建人", example = "芋道")
+    private String creator;
+    @Schema(description = "创建人名称", example = "芋道")
+    private String creatorName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "出库项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Item> items;
+
+    @Schema(description = "产品信息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("产品信息")
+    private String productNames;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "出库项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "销售订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        private Long orderItemId;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "税额,单位:元", example = "100.00")
+        private BigDecimal taxPrice;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+        // ========== 关联字段 ==========
+
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
+        private String productBarCode;
+        @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
+        private String productUnitName;
+
+        @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal stockCount; // 该字段仅仅在“详情”和“编辑”时使用
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutSaveReqVO.java
new file mode 100644
index 000000000..ac09f6bf1
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutSaveReqVO.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 销售出库新增/修改 Request VO")
+@Data
+public class ErpSaleOutSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    private Long id;
+
+    @Schema(description = "结算账户编号", example = "31189")
+    private Long accountId;
+
+    @Schema(description = "销售员编号", example = "1888")
+    private Long saleUserId;
+
+    @Schema(description = "出库时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出库时间不能为空")
+    private LocalDateTime outTime;
+
+    @Schema(description = "销售订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    @NotNull(message = "销售订单编号不能为空")
+    private Long orderId;
+
+    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "其它金额,单位:元", example = "7127")
+    private BigDecimal otherPrice;
+
+    @Schema(description = "本次收款,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    @NotNull(message = "本次收款不能为空")
+    private BigDecimal payPrice;
+
+    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
+    private String fileUrl;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "出库清单列表")
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "出库项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "销售订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        @NotNull(message = "销售订单项编号不能为空")
+        private Long orderItemId;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "仓库编号不能为空")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品编号不能为空")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品单位单位不能为空")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnPageReqVO.java
new file mode 100644
index 000000000..db4f73f5b
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnPageReqVO.java
@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - ERP 销售退货分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpSaleReturnPageReqVO extends PageParam {
+
+    @Schema(description = "销售单编号", example = "XS001")
+    private String no;
+
+    @Schema(description = "客户编号", example = "1724")
+    private Long customerId;
+
+    @Schema(description = "退货时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] returnTime;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "退货状态", example = "2")
+    private Integer status;
+
+    @Schema(description = "创建者")
+    private String creator;
+
+    @Schema(description = "产品编号", example = "1")
+    private Long productId;
+
+    @Schema(description = "仓库编号", example = "1")
+    private Long warehouseId;
+
+    @Schema(description = "结算账号编号", example = "1")
+    private Long accountId;
+
+    @Schema(description = "销售单号", example = "1")
+    private String orderNo;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java
new file mode 100644
index 000000000..0a7801de1
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java
@@ -0,0 +1,152 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 销售退货 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpSaleReturnRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "退货单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
+    @ExcelProperty("退货单编号")
+    private String no;
+
+    @Schema(description = "退货状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("退货状态")
+    private Integer status;
+
+    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1724")
+    private Long customerId;
+    @Schema(description = "客户名称", example = "芋道")
+    @ExcelProperty("客户名称")
+    private String customerName;
+
+    @Schema(description = "结算账户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "311.89")
+    @ExcelProperty("结算账户编号")
+    private Long accountId;
+
+    @Schema(description = "退货员编号", example = "1888")
+    private Long saleUserId;
+
+    @Schema(description = "退货时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("退货时间")
+    private LocalDateTime returnTime;
+
+    @Schema(description = "销售订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    private Long orderId;
+    @Schema(description = "销售订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "XS001")
+    private String orderNo;
+
+    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
+    @ExcelProperty("合计数量")
+    private BigDecimal totalCount;
+    @Schema(description = "最终合计价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "24906")
+    @ExcelProperty("最终合计价格")
+    private BigDecimal totalPrice;
+
+    @Schema(description = "合计产品价格,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalProductPrice;
+
+    @Schema(description = "合计税额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal totalTaxPrice;
+
+    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "优惠金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal discountPrice;
+
+    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal otherPrice;
+
+    @Schema(description = "本次退款,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    private BigDecimal refundPrice;
+    @Schema(description = "本次欠款,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "666")
+    private BigDecimal debtPrice;
+
+
+    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
+    @ExcelProperty("附件地址")
+    private String fileUrl;
+
+    @Schema(description = "备注", example = "你猜")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "创建人", example = "芋道")
+    private String creator;
+    @Schema(description = "创建人名称", example = "芋道")
+    private String creatorName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "退货项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Item> items;
+
+    @Schema(description = "产品信息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("产品信息")
+    private String productNames;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "退货项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "销售订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        private Long orderItemId;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "税额,单位:元", example = "100.00")
+        private BigDecimal taxPrice;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+        // ========== 关联字段 ==========
+
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
+        private String productBarCode;
+        @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
+        private String productUnitName;
+
+        @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal stockCount; // 该字段仅仅在“详情”和“编辑”时使用
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnSaveReqVO.java
new file mode 100644
index 000000000..837a4b509
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnSaveReqVO.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 销售退货新增/修改 Request VO")
+@Data
+public class ErpSaleReturnSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    private Long id;
+
+    @Schema(description = "结算账户编号", example = "31189")
+    private Long accountId;
+
+    @Schema(description = "销售员编号", example = "1888")
+    private Long saleUserId;
+
+    @Schema(description = "退货时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "退货时间不能为空")
+    private LocalDateTime returnTime;
+
+    @Schema(description = "销售订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
+    @NotNull(message = "销售订单编号不能为空")
+    private Long orderId;
+
+    @Schema(description = "优惠率,百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "99.88")
+    private BigDecimal discountPercent;
+
+    @Schema(description = "其它金额,单位:元", example = "7127")
+    private BigDecimal otherPrice;
+
+    @Schema(description = "本次退款,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
+    @NotNull(message = "本次退款不能为空")
+    private BigDecimal refundPrice;
+
+    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
+    private String fileUrl;
+
+    @Schema(description = "备注", example = "你猜")
+    private String remark;
+
+    @Schema(description = "退货清单列表")
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "退货项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "销售订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+        @NotNull(message = "销售订单项编号不能为空")
+        private Long orderItemId;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "仓库编号不能为空")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品编号不能为空")
+        private Long productId;
+
+        @Schema(description = "产品单位单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品单位单位不能为空")
+        private Long productUnitId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "税率,百分比", example = "99.88")
+        private BigDecimal taxPercent;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockCheckController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockCheckController.java
new file mode 100644
index 000000000..298ed54fa
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockCheckController.java
@@ -0,0 +1,149 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check.ErpStockCheckPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check.ErpStockCheckRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check.ErpStockCheckSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockCheckDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockCheckItemDO;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockCheckService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 库存调拨单")
+@RestController
+@RequestMapping("/erp/stock-check")
+@Validated
+public class ErpStockCheckController {
+
+    @Resource
+    private ErpStockCheckService stockCheckService;
+    @Resource
+    private ErpProductService productService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建库存调拨单")
+    @PreAuthorize("@ss.hasPermission('erp:stock-check:create')")
+    public CommonResult<Long> createStockCheck(@Valid @RequestBody ErpStockCheckSaveReqVO createReqVO) {
+        return success(stockCheckService.createStockCheck(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新库存调拨单")
+    @PreAuthorize("@ss.hasPermission('erp:stock-check:update')")
+    public CommonResult<Boolean> updateStockCheck(@Valid @RequestBody ErpStockCheckSaveReqVO updateReqVO) {
+        stockCheckService.updateStockCheck(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "更新库存调拨单的状态")
+    @PreAuthorize("@ss.hasPermission('erp:stock-check:update-status')")
+    public CommonResult<Boolean> updateStockCheckStatus(@RequestParam("id") Long id,
+                                                     @RequestParam("status") Integer status) {
+        stockCheckService.updateStockCheckStatus(id, status);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除库存调拨单")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:stock-check:delete')")
+    public CommonResult<Boolean> deleteStockCheck(@RequestParam("ids") List<Long> ids) {
+        stockCheckService.deleteStockCheck(ids);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得库存调拨单")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:stock-check:query')")
+    public CommonResult<ErpStockCheckRespVO> getStockCheck(@RequestParam("id") Long id) {
+        ErpStockCheckDO stockCheck = stockCheckService.getStockCheck(id);
+        if (stockCheck == null) {
+            return success(null);
+        }
+        List<ErpStockCheckItemDO> stockCheckItemList = stockCheckService.getStockCheckItemListByCheckId(id);
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(stockCheckItemList, ErpStockCheckItemDO::getProductId));
+        return success(BeanUtils.toBean(stockCheck, ErpStockCheckRespVO.class, stockCheckVO ->
+                stockCheckVO.setItems(BeanUtils.toBean(stockCheckItemList, ErpStockCheckRespVO.Item.class, item ->
+                        MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                                .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))))));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得库存调拨单分页")
+    @PreAuthorize("@ss.hasPermission('erp:stock-check:query')")
+    public CommonResult<PageResult<ErpStockCheckRespVO>> getStockCheckPage(@Valid ErpStockCheckPageReqVO pageReqVO) {
+        PageResult<ErpStockCheckDO> pageResult = stockCheckService.getStockCheckPage(pageReqVO);
+        return success(buildStockCheckVOPageResult(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出库存调拨单 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:stock-check:export')")
+    @OperateLog(type = EXPORT)
+    public void exportStockCheckExcel(@Valid ErpStockCheckPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpStockCheckRespVO> list = buildStockCheckVOPageResult(stockCheckService.getStockCheckPage(pageReqVO)).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "库存调拨单.xls", "数据", ErpStockCheckRespVO.class, list);
+    }
+
+    private PageResult<ErpStockCheckRespVO> buildStockCheckVOPageResult(PageResult<ErpStockCheckDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        // 1.1 盘点项
+        List<ErpStockCheckItemDO> stockCheckItemList = stockCheckService.getStockCheckItemListByCheckIds(
+                convertSet(pageResult.getList(), ErpStockCheckDO::getId));
+        Map<Long, List<ErpStockCheckItemDO>> stockCheckItemMap = convertMultiMap(stockCheckItemList, ErpStockCheckItemDO::getCheckId);
+        // 1.2 产品信息
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(stockCheckItemList, ErpStockCheckItemDO::getProductId));
+        // 1.3 管理员信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), stockCheck -> Long.parseLong(stockCheck.getCreator())));
+        // 2. 开始拼接
+        return BeanUtils.toBean(pageResult, ErpStockCheckRespVO.class, stockCheck -> {
+            stockCheck.setItems(BeanUtils.toBean(stockCheckItemMap.get(stockCheck.getId()), ErpStockCheckRespVO.Item.class,
+                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
+            stockCheck.setProductNames(CollUtil.join(stockCheck.getItems(), ",", ErpStockCheckRespVO.Item::getProductName));
+            MapUtils.findAndThen(userMap, Long.parseLong(stockCheck.getCreator()), user -> stockCheck.setCreatorName(user.getNickname()));
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockController.java
index 224468657..912f59731 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockController.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockController.java
@@ -31,6 +31,7 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.io.IOException;
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
 
@@ -66,6 +67,13 @@ public class ErpStockController {
         return success(BeanUtils.toBean(stock, ErpStockRespVO.class));
     }
 
+    @GetMapping("/get-count")
+    @Operation(summary = "获得产品库存数量")
+    @Parameter(name = "productId", description = "产品编号", example = "10")
+    public CommonResult<BigDecimal> getStockCount(@RequestParam("productId") Long productId) {
+        return success(stockService.getStockCount(productId));
+    }
+
     @GetMapping("/page")
     @Operation(summary = "获得产品库存分页")
     @PreAuthorize("@ss.hasPermission('erp:stock:query')")
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockInController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockInController.java
index 1351516ed..8813da89a 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockInController.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockInController.java
@@ -15,7 +15,7 @@ import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.in.ErpStockInSaveRe
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockDO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockInDO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockInItemDO;
-import cn.iocoder.yudao.module.erp.dal.dataobject.supplier.ErpSupplierDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
 import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
 import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
 import cn.iocoder.yudao.module.erp.service.stock.ErpStockInService;
@@ -75,12 +75,21 @@ public class ErpStockInController {
         return success(true);
     }
 
+    @PutMapping("/update-status")
+    @Operation(summary = "更新其它入库单的状态")
+    @PreAuthorize("@ss.hasPermission('erp:stock-in:update-status')")
+    public CommonResult<Boolean> updateStockInStatus(@RequestParam("id") Long id,
+                                                     @RequestParam("status") Integer status) {
+        stockInService.updateStockInStatus(id, status);
+        return success(true);
+    }
+
     @DeleteMapping("/delete")
     @Operation(summary = "删除其它入库单")
-    @Parameter(name = "id", description = "编号", required = true)
+    @Parameter(name = "ids", description = "编号数组", required = true)
     @PreAuthorize("@ss.hasPermission('erp:stock-in:delete')")
-    public CommonResult<Boolean> deleteStockIn(@RequestParam("id") Long id) {
-        stockInService.deleteStockIn(id);
+    public CommonResult<Boolean> deleteStockIn(@RequestParam("ids") List<Long> ids) {
+        stockInService.deleteStockIn(ids);
         return success(true);
     }
 
@@ -93,12 +102,15 @@ public class ErpStockInController {
         if (stockIn == null) {
             return success(null);
         }
-        List<ErpStockInItemDO> stockInItems = stockInService.getStockInItemListByInId(id);
-        // TODO 芋艿:有个锤子;
+        List<ErpStockInItemDO> stockInItemList = stockInService.getStockInItemListByInId(id);
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(stockInItemList, ErpStockInItemDO::getProductId));
         return success(BeanUtils.toBean(stockIn, ErpStockInRespVO.class, stockInVO ->
-                stockInVO.setItems(BeanUtils.toBean(stockInItems, ErpStockInRespVO.Item.class, item -> {
+                stockInVO.setItems(BeanUtils.toBean(stockInItemList, ErpStockInRespVO.Item.class, item -> {
                     ErpStockDO stock = stockService.getStock(item.getProductId(), item.getWarehouseId());
                     item.setStockCount(stock != null ? stock.getCount() : BigDecimal.ZERO);
+                    MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()));
                 }))));
     }
 
@@ -130,7 +142,7 @@ public class ErpStockInController {
         List<ErpStockInItemDO> stockInItemList = stockInService.getStockInItemListByInIds(
                 convertSet(pageResult.getList(), ErpStockInDO::getId));
         Map<Long, List<ErpStockInItemDO>> stockInItemMap = convertMultiMap(stockInItemList, ErpStockInItemDO::getInId);
-        // 1.2 商品信息
+        // 1.2 产品信息
         Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
                 convertSet(stockInItemList, ErpStockInItemDO::getProductId));
         // 1.3 供应商信息
@@ -138,12 +150,12 @@ public class ErpStockInController {
                 convertSet(pageResult.getList(), ErpStockInDO::getSupplierId));
         // 1.4 管理员信息
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
-                convertSet(pageResult.getList(), erpStockRecordDO -> Long.parseLong(erpStockRecordDO.getCreator())));
+                convertSet(pageResult.getList(), stockIn -> Long.parseLong(stockIn.getCreator())));
         // 2. 开始拼接
         return BeanUtils.toBean(pageResult, ErpStockInRespVO.class, stockIn -> {
             stockIn.setItems(BeanUtils.toBean(stockInItemMap.get(stockIn.getId()), ErpStockInRespVO.Item.class,
-                    item -> MapUtils.findAndThen(productMap, item.getProductId(),
-                            product -> item.setProductName(product.getName()).setProductUnitName(product.getUnitName()))));
+                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
             stockIn.setProductNames(CollUtil.join(stockIn.getItems(), ",", ErpStockInRespVO.Item::getProductName));
             MapUtils.findAndThen(supplierMap, stockIn.getSupplierId(), supplier -> stockIn.setSupplierName(supplier.getName()));
             MapUtils.findAndThen(userMap, Long.parseLong(stockIn.getCreator()), user -> stockIn.setCreatorName(user.getNickname()));
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockMoveController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockMoveController.java
new file mode 100644
index 000000000..1df3fd7fc
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockMoveController.java
@@ -0,0 +1,160 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move.ErpStockMovePageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move.ErpStockMoveRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move.ErpStockMoveSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockMoveDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockMoveItemDO;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockMoveService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 库存调拨单")
+@RestController
+@RequestMapping("/erp/stock-move")
+@Validated
+public class ErpStockMoveController {
+
+    @Resource
+    private ErpStockMoveService stockMoveService;
+    @Resource
+    private ErpStockService stockService;
+    @Resource
+    private ErpProductService productService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建库存调拨单")
+    @PreAuthorize("@ss.hasPermission('erp:stock-move:create')")
+    public CommonResult<Long> createStockMove(@Valid @RequestBody ErpStockMoveSaveReqVO createReqVO) {
+        return success(stockMoveService.createStockMove(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新库存调拨单")
+    @PreAuthorize("@ss.hasPermission('erp:stock-move:update')")
+    public CommonResult<Boolean> updateStockMove(@Valid @RequestBody ErpStockMoveSaveReqVO updateReqVO) {
+        stockMoveService.updateStockMove(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "更新库存调拨单的状态")
+    @PreAuthorize("@ss.hasPermission('erp:stock-move:update-status')")
+    public CommonResult<Boolean> updateStockMoveStatus(@RequestParam("id") Long id,
+                                                     @RequestParam("status") Integer status) {
+        stockMoveService.updateStockMoveStatus(id, status);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除库存调拨单")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:stock-move:delete')")
+    public CommonResult<Boolean> deleteStockMove(@RequestParam("ids") List<Long> ids) {
+        stockMoveService.deleteStockMove(ids);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得库存调拨单")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:stock-move:query')")
+    public CommonResult<ErpStockMoveRespVO> getStockMove(@RequestParam("id") Long id) {
+        ErpStockMoveDO stockMove = stockMoveService.getStockMove(id);
+        if (stockMove == null) {
+            return success(null);
+        }
+        List<ErpStockMoveItemDO> stockMoveItemList = stockMoveService.getStockMoveItemListByMoveId(id);
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(stockMoveItemList, ErpStockMoveItemDO::getProductId));
+        return success(BeanUtils.toBean(stockMove, ErpStockMoveRespVO.class, stockMoveVO ->
+                stockMoveVO.setItems(BeanUtils.toBean(stockMoveItemList, ErpStockMoveRespVO.Item.class, item -> {
+                    ErpStockDO stock = stockService.getStock(item.getProductId(), item.getFromWarehouseId());
+                    item.setStockCount(stock != null ? stock.getCount() : BigDecimal.ZERO);
+                    MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()));
+                }))));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得库存调拨单分页")
+    @PreAuthorize("@ss.hasPermission('erp:stock-move:query')")
+    public CommonResult<PageResult<ErpStockMoveRespVO>> getStockMovePage(@Valid ErpStockMovePageReqVO pageReqVO) {
+        PageResult<ErpStockMoveDO> pageResult = stockMoveService.getStockMovePage(pageReqVO);
+        return success(buildStockMoveVOPageResult(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出库存调拨单 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:stock-move:export')")
+    @OperateLog(type = EXPORT)
+    public void exportStockMoveExcel(@Valid ErpStockMovePageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpStockMoveRespVO> list = buildStockMoveVOPageResult(stockMoveService.getStockMovePage(pageReqVO)).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "库存调拨单.xls", "数据", ErpStockMoveRespVO.class, list);
+    }
+
+    private PageResult<ErpStockMoveRespVO> buildStockMoveVOPageResult(PageResult<ErpStockMoveDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        // 1.1 调拨项
+        List<ErpStockMoveItemDO> stockMoveItemList = stockMoveService.getStockMoveItemListByMoveIds(
+                convertSet(pageResult.getList(), ErpStockMoveDO::getId));
+        Map<Long, List<ErpStockMoveItemDO>> stockMoveItemMap = convertMultiMap(stockMoveItemList, ErpStockMoveItemDO::getMoveId);
+        // 1.2 产品信息
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(stockMoveItemList, ErpStockMoveItemDO::getProductId));
+        // 1.3 TODO 芋艿:搞仓库信息
+        // 1.4 管理员信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), stockMove -> Long.parseLong(stockMove.getCreator())));
+        // 2. 开始拼接
+        return BeanUtils.toBean(pageResult, ErpStockMoveRespVO.class, stockMove -> {
+            stockMove.setItems(BeanUtils.toBean(stockMoveItemMap.get(stockMove.getId()), ErpStockMoveRespVO.Item.class,
+                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
+            stockMove.setProductNames(CollUtil.join(stockMove.getItems(), ",", ErpStockMoveRespVO.Item::getProductName));
+            // TODO 芋艿:
+//            MapUtils.findAndThen(customerMap, stockMove.getCustomerId(), supplier -> stockMove.setCustomerName(supplier.getName()));
+            MapUtils.findAndThen(userMap, Long.parseLong(stockMove.getCreator()), user -> stockMove.setCreatorName(user.getNickname()));
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockOutController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockOutController.java
new file mode 100644
index 000000000..9ad592f1a
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockOutController.java
@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out.ErpStockOutPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out.ErpStockOutRespVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out.ErpStockOutSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutItemDO;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.sale.ErpCustomerService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockOutService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - ERP 其它出库单")
+@RestController
+@RequestMapping("/erp/stock-out")
+@Validated
+public class ErpStockOutController {
+
+    @Resource
+    private ErpStockOutService stockOutService;
+    @Resource
+    private ErpStockService stockService;
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpCustomerService customerService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建其它出库单")
+    @PreAuthorize("@ss.hasPermission('erp:stock-out:create')")
+    public CommonResult<Long> createStockOut(@Valid @RequestBody ErpStockOutSaveReqVO createReqVO) {
+        return success(stockOutService.createStockOut(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新其它出库单")
+    @PreAuthorize("@ss.hasPermission('erp:stock-out:update')")
+    public CommonResult<Boolean> updateStockOut(@Valid @RequestBody ErpStockOutSaveReqVO updateReqVO) {
+        stockOutService.updateStockOut(updateReqVO);
+        return success(true);
+    }
+
+    @PutMapping("/update-status")
+    @Operation(summary = "更新其它出库单的状态")
+    @PreAuthorize("@ss.hasPermission('erp:stock-out:update-status')")
+    public CommonResult<Boolean> updateStockOutStatus(@RequestParam("id") Long id,
+                                                     @RequestParam("status") Integer status) {
+        stockOutService.updateStockOutStatus(id, status);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除其它出库单")
+    @Parameter(name = "ids", description = "编号数组", required = true)
+    @PreAuthorize("@ss.hasPermission('erp:stock-out:delete')")
+    public CommonResult<Boolean> deleteStockOut(@RequestParam("ids") List<Long> ids) {
+        stockOutService.deleteStockOut(ids);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得其它出库单")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('erp:stock-out:query')")
+    public CommonResult<ErpStockOutRespVO> getStockOut(@RequestParam("id") Long id) {
+        ErpStockOutDO stockOut = stockOutService.getStockOut(id);
+        if (stockOut == null) {
+            return success(null);
+        }
+        List<ErpStockOutItemDO> stockOutItemList = stockOutService.getStockOutItemListByOutId(id);
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(stockOutItemList, ErpStockOutItemDO::getProductId));
+        return success(BeanUtils.toBean(stockOut, ErpStockOutRespVO.class, stockOutVO ->
+                stockOutVO.setItems(BeanUtils.toBean(stockOutItemList, ErpStockOutRespVO.Item.class, item -> {
+                    ErpStockDO stock = stockService.getStock(item.getProductId(), item.getWarehouseId());
+                    item.setStockCount(stock != null ? stock.getCount() : BigDecimal.ZERO);
+                    MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()));
+                }))));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得其它出库单分页")
+    @PreAuthorize("@ss.hasPermission('erp:stock-out:query')")
+    public CommonResult<PageResult<ErpStockOutRespVO>> getStockOutPage(@Valid ErpStockOutPageReqVO pageReqVO) {
+        PageResult<ErpStockOutDO> pageResult = stockOutService.getStockOutPage(pageReqVO);
+        return success(buildStockOutVOPageResult(pageResult));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出其它出库单 Excel")
+    @PreAuthorize("@ss.hasPermission('erp:stock-out:export')")
+    @OperateLog(type = EXPORT)
+    public void exportStockOutExcel(@Valid ErpStockOutPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<ErpStockOutRespVO> list = buildStockOutVOPageResult(stockOutService.getStockOutPage(pageReqVO)).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "其它出库单.xls", "数据", ErpStockOutRespVO.class, list);
+    }
+
+    private PageResult<ErpStockOutRespVO> buildStockOutVOPageResult(PageResult<ErpStockOutDO> pageResult) {
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return PageResult.empty(pageResult.getTotal());
+        }
+        // 1.1 出库项
+        List<ErpStockOutItemDO> stockOutItemList = stockOutService.getStockOutItemListByOutIds(
+                convertSet(pageResult.getList(), ErpStockOutDO::getId));
+        Map<Long, List<ErpStockOutItemDO>> stockOutItemMap = convertMultiMap(stockOutItemList, ErpStockOutItemDO::getOutId);
+        // 1.2 产品信息
+        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
+                convertSet(stockOutItemList, ErpStockOutItemDO::getProductId));
+        // 1.3 客户信息
+        Map<Long, ErpCustomerDO> customerMap = customerService.getCustomerMap(
+                convertSet(pageResult.getList(), ErpStockOutDO::getCustomerId));
+        // 1.4 管理员信息
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
+                convertSet(pageResult.getList(), stockOut -> Long.parseLong(stockOut.getCreator())));
+        // 2. 开始拼接
+        return BeanUtils.toBean(pageResult, ErpStockOutRespVO.class, stockOut -> {
+            stockOut.setItems(BeanUtils.toBean(stockOutItemMap.get(stockOut.getId()), ErpStockOutRespVO.Item.class,
+                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
+                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
+            stockOut.setProductNames(CollUtil.join(stockOut.getItems(), ",", ErpStockOutRespVO.Item::getProductName));
+            MapUtils.findAndThen(customerMap, stockOut.getCustomerId(), supplier -> stockOut.setCustomerName(supplier.getName()));
+            MapUtils.findAndThen(userMap, Long.parseLong(stockOut.getCreator()), user -> stockOut.setCreatorName(user.getNickname()));
+        });
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockRecordController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockRecordController.java
index 09f9feaf9..6ed453894 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockRecordController.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpStockRecordController.java
@@ -93,7 +93,7 @@ public class ErpStockRecordController {
         Map<Long, ErpWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
                 convertSet(pageResult.getList(), ErpStockRecordDO::getWarehouseId));
         Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
-                convertSet(pageResult.getList(), erpStockRecordDO -> Long.parseLong(erpStockRecordDO.getCreator())));
+                convertSet(pageResult.getList(), record -> Long.parseLong(record.getCreator())));
         return BeanUtils.toBean(pageResult, ErpStockRecordRespVO.class, stock -> {
             MapUtils.findAndThen(productMap, stock.getProductId(), product -> stock.setProductName(product.getName())
                     .setCategoryName(product.getCategoryName()).setUnitName(product.getUnitName()));
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpWarehouseController.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpWarehouseController.java
index 32c33075d..744f439f5 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpWarehouseController.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/ErpWarehouseController.java
@@ -27,6 +27,7 @@ import java.io.IOException;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 
 @Tag(name = "管理后台 - ERP 仓库")
@@ -95,7 +96,8 @@ public class ErpWarehouseController {
     @Operation(summary = "获得仓库精简列表", description = "只包含被开启的仓库,主要用于前端的下拉选项")
     public CommonResult<List<ErpWarehouseRespVO>> getWarehouseSimpleList() {
         List<ErpWarehouseDO> list = warehouseService.getWarehouseListByStatus(CommonStatusEnum.ENABLE.getStatus());
-        return success(BeanUtils.toBean(list, ErpWarehouseRespVO.class));
+        return success(convertList(list, warehouse -> new ErpWarehouseRespVO().setId(warehouse.getId())
+                .setName(warehouse.getName()).setDefaultStatus(warehouse.getDefaultStatus())));
     }
 
     @GetMapping("/export-excel")
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckPageReqVO.java
new file mode 100644
index 000000000..2bae14c1e
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckPageReqVO.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - ERP 库存盘点单分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpStockCheckPageReqVO extends PageParam {
+
+    @Schema(description = "盘点单号", example = "S123")
+    private String no;
+
+    @Schema(description = "仓库编号", example = "3113")
+    private Long warehouseId;
+
+    @Schema(description = "盘点时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] checkTime;
+
+    @Schema(description = "状态", example = "10")
+    @InEnum(ErpAuditStatus.class)
+    private Integer status;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "创建者")
+    private String creator;
+
+    @Schema(description = "产品编号", example = "1")
+    private Long productId;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java
new file mode 100644
index 000000000..af53e3c72
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java
@@ -0,0 +1,111 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.module.erp.enums.DictTypeConstants.AUDIT_STATUS;
+
+@Schema(description = "管理后台 - ERP 库存盘点单 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpStockCheckRespVO {
+
+    @Schema(description = "盘点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+    @ExcelProperty("盘点编号")
+    private Long id;
+
+    @Schema(description = "盘点单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "S123")
+    @ExcelProperty("盘点单号")
+    private String no;
+
+    @Schema(description = "盘点时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("盘点时间")
+    private LocalDateTime checkTime;
+
+    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
+    @ExcelProperty("合计数量")
+    private BigDecimal totalCount;
+
+    @Schema(description = "合计金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "24906")
+    @ExcelProperty("合计金额")
+    private BigDecimal totalPrice;
+
+    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @ExcelProperty(value = "状态", converter = DictConvert.class)
+    @DictFormat(AUDIT_STATUS)
+    private Integer status;
+
+    @Schema(description = "备注", example = "随便")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "附件 URL", example = "https://www.iocoder.cn/1.doc")
+    private String fileUrl;
+
+    @Schema(description = "创建人", example = "芋道")
+    private String creator;
+    @Schema(description = "创建人名称", example = "芋道")
+    private String creatorName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "盘点项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Item> items;
+
+    @Schema(description = "产品信息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("产品信息")
+    private String productNames;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "盘点项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productId;
+
+        @Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "账面数量(当前库存)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "账面数量不能为空")
+        private BigDecimal stockCount;
+
+        @Schema(description = "实际数量(实际库存)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "实际数量不能为空")
+        private BigDecimal actualCount;
+
+        @Schema(description = "盈亏数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "盈亏数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+        // ========== 关联字段 ==========
+
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
+        private String productBarCode;
+        @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
+        private String productUnitName;
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckSaveReqVO.java
new file mode 100644
index 000000000..0af223fb5
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckSaveReqVO.java
@@ -0,0 +1,69 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 其它出库单新增/修改 Request VO")
+@Data
+public class ErpStockCheckSaveReqVO {
+
+    @Schema(description = "出库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+    private Long id;
+
+    @Schema(description = "出库时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出库时间不能为空")
+    private LocalDateTime checkTime;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "附件 URL", example = "https://www.iocoder.cn/1.doc")
+    private String fileUrl;
+
+    @Schema(description = "出库项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "出库项列表不能为空")
+    @Valid
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "出库项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "仓库编号不能为空")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品编号不能为空")
+        private Long productId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "账面数量(当前库存)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "账面数量不能为空")
+        private BigDecimal stockCount;
+
+        @Schema(description = "实际数量(实际库存)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "实际数量不能为空")
+        private BigDecimal actualCount;
+
+        @Schema(description = "盈亏数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "盈亏数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java
index 07897ad30..077b9dd1b 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java
@@ -56,9 +56,9 @@ public class ErpStockInRespVO {
     @Schema(description = "附件 URL", example = "https://www.iocoder.cn/1.doc")
     private String fileUrl;
 
-    @Schema(description = "审核人", example = "芋道")
+    @Schema(description = "创建人", example = "芋道")
     private String creator;
-    @Schema(description = "审核人名称", example = "芋道")
+    @Schema(description = "创建人名称", example = "芋道")
     private String creatorName;
 
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@@ -97,6 +97,8 @@ public class ErpStockInRespVO {
 
         @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
         private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
+        private String productBarCode;
         @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
         private String productUnitName;
 
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInSaveReqVO.java
index c3cc6be95..0187872c8 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInSaveReqVO.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInSaveReqVO.java
@@ -17,10 +17,6 @@ public class ErpStockInSaveReqVO {
     @Schema(description = "入库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
     private Long id;
 
-    @Schema(description = "入库单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "S123")
-    @NotEmpty(message = "入库单号不能为空")
-    private String no;
-
     @Schema(description = "供应商编号", example = "3113")
     private Long supplierId;
 
@@ -53,8 +49,7 @@ public class ErpStockInSaveReqVO {
         @NotNull(message = "产品编号不能为空")
         private Long productId;
 
-        @Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
-        @NotNull(message = "产品单价不能为空")
+        @Schema(description = "产品单价", example = "100.00")
         private BigDecimal productPrice;
 
         @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMovePageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMovePageReqVO.java
new file mode 100644
index 000000000..98a1fe95e
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMovePageReqVO.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - ERP 库存调拨单分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpStockMovePageReqVO extends PageParam {
+
+    @Schema(description = "调拨单号", example = "S123")
+    private String no;
+
+    @Schema(description = "调拨时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] moveTime;
+
+    @Schema(description = "状态", example = "10")
+    @InEnum(ErpAuditStatus.class)
+    private Integer status;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "创建者")
+    private String creator;
+
+    @Schema(description = "产品编号", example = "1")
+    private Long productId;
+
+    @Schema(description = "调出仓库编号", example = "1")
+    private Long fromWarehouseId;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java
new file mode 100644
index 000000000..799ddc3f1
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java
@@ -0,0 +1,107 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.module.erp.enums.DictTypeConstants.AUDIT_STATUS;
+
+@Schema(description = "管理后台 - ERP 库存调拨单 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpStockMoveRespVO {
+
+    @Schema(description = "调拨编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+    @ExcelProperty("调拨编号")
+    private Long id;
+
+    @Schema(description = "调拨单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "S123")
+    @ExcelProperty("调拨单号")
+    private String no;
+
+    @Schema(description = "调拨时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("调拨时间")
+    private LocalDateTime moveTime;
+
+    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
+    @ExcelProperty("合计数量")
+    private BigDecimal totalCount;
+
+    @Schema(description = "合计金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "24906")
+    @ExcelProperty("合计金额")
+    private BigDecimal totalPrice;
+
+    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @ExcelProperty(value = "状态", converter = DictConvert.class)
+    @DictFormat(AUDIT_STATUS)
+    private Integer status;
+
+    @Schema(description = "备注", example = "随便")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "附件 URL", example = "https://www.iocoder.cn/1.doc")
+    private String fileUrl;
+
+    @Schema(description = "创建人", example = "芋道")
+    private String creator;
+    @Schema(description = "创建人名称", example = "芋道")
+    private String creatorName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "调拨项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Item> items;
+
+    @Schema(description = "产品信息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("产品信息")
+    private String productNames;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "调拨项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "调出仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long fromWarehouseId;
+
+        @Schema(description = "调入仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "888")
+        private Long toWarehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productId;
+
+        @Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal count;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+        // ========== 关联字段 ==========
+
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
+        private String productBarCode;
+        @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
+        private String productUnitName;
+
+        @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal stockCount; // 该字段仅仅在“详情”和“编辑”时使用
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveSaveReqVO.java
new file mode 100644
index 000000000..17a431561
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveSaveReqVO.java
@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.AssertTrue;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 库存调拨单新增/修改 Request VO")
+@Data
+public class ErpStockMoveSaveReqVO {
+
+    @Schema(description = "调拨编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+    private Long id;
+
+    @Schema(description = "客户编号", example = "3113")
+    private Long customerId;
+
+    @Schema(description = "调拨时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "调拨时间不能为空")
+    private LocalDateTime moveTime;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "附件 URL", example = "https://www.iocoder.cn/1.doc")
+    private String fileUrl;
+
+    @Schema(description = "调拨项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "调拨项列表不能为空")
+    @Valid
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "调拨项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "调出仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "调出仓库编号不能为空")
+        private Long fromWarehouseId;
+
+        @Schema(description = "调入仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "888")
+        @NotNull(message = "调入仓库编号不能为空")
+        private Long toWarehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品编号不能为空")
+        private Long productId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+        @AssertTrue(message = "调出、调仓仓库不能相同")
+        @JsonIgnore
+        public boolean isWarehouseValid() {
+            return ObjectUtil.notEqual(fromWarehouseId, toWarehouseId);
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutPageReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutPageReqVO.java
new file mode 100644
index 000000000..5f6558b19
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutPageReqVO.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - ERP 其它出库单分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ErpStockOutPageReqVO extends PageParam {
+
+    @Schema(description = "出库单号", example = "S123")
+    private String no;
+
+    @Schema(description = "客户编号", example = "3113")
+    private Long customerId;
+
+    @Schema(description = "出库时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] outTime;
+
+    @Schema(description = "状态", example = "10")
+    @InEnum(ErpAuditStatus.class)
+    private Integer status;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "创建者")
+    private String creator;
+
+    @Schema(description = "产品编号", example = "1")
+    private Long productId;
+
+    @Schema(description = "仓库编号", example = "1")
+    private Long warehouseId;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java
new file mode 100644
index 000000000..22a88e7c9
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java
@@ -0,0 +1,110 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static cn.iocoder.yudao.module.erp.enums.DictTypeConstants.AUDIT_STATUS;
+
+@Schema(description = "管理后台 - ERP 其它出库单 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class ErpStockOutRespVO {
+
+    @Schema(description = "出库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+    @ExcelProperty("出库编号")
+    private Long id;
+
+    @Schema(description = "出库单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "S123")
+    @ExcelProperty("出库单号")
+    private String no;
+
+    @Schema(description = "客户编号", example = "3113")
+    private Long customerId;
+    @Schema(description = "客户名称", example = "芋道")
+    @ExcelProperty("客户名称")
+    private String customerName;
+
+    @Schema(description = "出库时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("出库时间")
+    private LocalDateTime outTime;
+
+    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15663")
+    @ExcelProperty("合计数量")
+    private BigDecimal totalCount;
+
+    @Schema(description = "合计金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "24906")
+    @ExcelProperty("合计金额")
+    private BigDecimal totalPrice;
+
+    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    @ExcelProperty(value = "状态", converter = DictConvert.class)
+    @DictFormat(AUDIT_STATUS)
+    private Integer status;
+
+    @Schema(description = "备注", example = "随便")
+    @ExcelProperty("备注")
+    private String remark;
+
+    @Schema(description = "附件 URL", example = "https://www.iocoder.cn/1.doc")
+    private String fileUrl;
+
+    @Schema(description = "创建人", example = "芋道")
+    private String creator;
+    @Schema(description = "创建人名称", example = "芋道")
+    private String creatorName;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "出库项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<Item> items;
+
+    @Schema(description = "产品信息", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("产品信息")
+    private String productNames;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "出库项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        private Long productId;
+
+        @Schema(description = "产品单价", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal count;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+        // ========== 关联字段 ==========
+
+        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
+        private String productName;
+        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
+        private String productBarCode;
+        @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
+        private String productUnitName;
+
+        @Schema(description = "库存数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        private BigDecimal stockCount; // 该字段仅仅在“详情”和“编辑”时使用
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutSaveReqVO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutSaveReqVO.java
new file mode 100644
index 000000000..5a903798e
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutSaveReqVO.java
@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - ERP 其它出库单新增/修改 Request VO")
+@Data
+public class ErpStockOutSaveReqVO {
+
+    @Schema(description = "出库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11756")
+    private Long id;
+
+    @Schema(description = "客户编号", example = "3113")
+    private Long customerId;
+
+    @Schema(description = "出库时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出库时间不能为空")
+    private LocalDateTime outTime;
+
+    @Schema(description = "备注", example = "随便")
+    private String remark;
+
+    @Schema(description = "附件 URL", example = "https://www.iocoder.cn/1.doc")
+    private String fileUrl;
+
+    @Schema(description = "出库项列表", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "出库项列表不能为空")
+    @Valid
+    private List<Item> items;
+
+    @Data
+    public static class Item {
+
+        @Schema(description = "出库项编号", example = "11756")
+        private Long id;
+
+        @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "仓库编号不能为空")
+        private Long warehouseId;
+
+        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
+        @NotNull(message = "产品编号不能为空")
+        private Long productId;
+
+        @Schema(description = "产品单价", example = "100.00")
+        private BigDecimal productPrice;
+
+        @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
+        @NotNull(message = "产品数量不能为空")
+        private BigDecimal count;
+
+        @Schema(description = "备注", example = "随便")
+        private String remark;
+
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpAccountDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpAccountDO.java
new file mode 100644
index 000000000..fe01cc228
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpAccountDO.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.finance;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * ERP 结算账户 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_account")
+@KeySequence("erp_account_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpAccountDO extends BaseDO {
+
+    /**
+     * 结算账户编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 账户名称
+     */
+    private String name;
+    /**
+     * 账户编码
+     */
+    private String no;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 开启状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 排序
+     */
+    private Integer sort;
+    /**
+     * 是否默认
+     */
+    private Boolean defaultStatus;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentDO.java
new file mode 100644
index 000000000..edb55edbf
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentDO.java
@@ -0,0 +1,86 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.finance;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 付款单 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_finance_payment")
+@KeySequence("erp_finance_payment_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpFinancePaymentDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 付款单号
+     */
+    private String no;
+    /**
+     * 付款状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.ErpAuditStatus}
+     */
+    private Integer status;
+    /**
+     * 付款时间
+     */
+    private LocalDateTime paymentTime;
+    /**
+     * 财务人员编号
+     *
+     * 关联 AdminUserDO 的 id 字段
+     */
+    private Long financeUserId;
+    /**
+     * 供应商编号
+     *
+     * 关联 {@link ErpSupplierDO#getId()}
+     */
+    private Long supplierId;
+    /**
+     * 付款账户编号
+     *
+     * 关联 {@link ErpAccountDO#getId()}
+     */
+    private Long accountId;
+
+    /**
+     * 合计价格,单位:元
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 优惠金额,单位:元
+     */
+    private BigDecimal discountPrice;
+    /**
+     * 实付金额,单位:分
+     *
+     * paymentPrice = totalPrice - discountPrice
+     */
+    private BigDecimal paymentPrice;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentItemDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentItemDO.java
new file mode 100644
index 000000000..fc3ed1ada
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinancePaymentItemDO.java
@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.finance;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * ERP 付款项 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_finance_payment_item")
+@KeySequence("erp_finance_payment_item_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpFinancePaymentItemDO extends BaseDO {
+
+    /**
+     * 入库项编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 付款单编号
+     *
+     * 关联 {@link ErpFinancePaymentDO#getId()}
+     */
+    private Long paymentId;
+
+    /**
+     * 业务类型
+     *
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.common.ErpBizTypeEnum} 的采购入库、退货
+     */
+    private Integer bizType;
+    /**
+     * 业务编号
+     *
+     * 例如说:{@link ErpPurchaseInDO#getId()}
+     */
+    private Long bizId;
+    /**
+     * 业务单号
+     *
+     * 例如说:{@link ErpPurchaseInDO#getNo()}
+     */
+    private String bizNo;
+
+    /**
+     * 应付欠款,单位:分
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 已付欠款,单位:分
+     */
+    private BigDecimal paidPrice;
+    /**
+     * 本次付款,单位:分
+     */
+    private BigDecimal paymentPrice;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInDO.java
new file mode 100644
index 000000000..a16844d3e
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInDO.java
@@ -0,0 +1,122 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 采购入库 DO
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "erp_purchase_in")
+@KeySequence("erp_purchase_in_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpPurchaseInDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 采购入库单号
+     */
+    private String no;
+    /**
+     * 入库状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.ErpAuditStatus}
+     */
+    private Integer status;
+    /**
+     * 供应商编号
+     *
+     * 关联 {@link ErpSupplierDO#getId()}
+     */
+    private Long supplierId;
+    /**
+     * 结算账户编号
+     *
+     * 关联 {@link ErpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 入库时间
+     */
+    private LocalDateTime inTime;
+
+    /**
+     * 采购订单编号
+     *
+     * 关联 {@link ErpPurchaseOrderDO#getId()}
+     */
+    private Long orderId;
+    /**
+     * 采购订单号
+     *
+     * 冗余 {@link ErpPurchaseOrderDO#getNo()}
+     */
+    private String orderNo;
+
+    /**
+     * 合计数量
+     */
+    private BigDecimal totalCount;
+    /**
+     * 最终合计价格,单位:元
+     *
+     * totalPrice = totalProductPrice + totalTaxPrice - discountPrice + otherPrice
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 已支付金额,单位:元
+     *
+     * 目的:和 TODO erp_finance_payment 结合,记录已支付金额
+     */
+    private BigDecimal paymentPrice;
+
+    /**
+     * 合计产品价格,单位:元
+     */
+    private BigDecimal totalProductPrice;
+    /**
+     * 合计税额,单位:元
+     */
+    private BigDecimal totalTaxPrice;
+    /**
+     * 优惠率,百分比
+     */
+    private BigDecimal discountPercent;
+    /**
+     * 优惠金额,单位:元
+     *
+     * discountPrice = (totalProductPrice + totalTaxPrice) * discountPercent
+     */
+    private BigDecimal discountPrice;
+    /**
+     * 其它金额,单位:元
+     */
+    private BigDecimal otherPrice;
+
+    /**
+     * 附件地址
+     */
+    private String fileUrl;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInItemDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInItemDO.java
new file mode 100644
index 000000000..1597bc10b
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInItemDO.java
@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpWarehouseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * ERP 采购入库项 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_purchase_in_items")
+@KeySequence("erp_purchase_in_items_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpPurchaseInItemDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 采购入库编号
+     *
+     * 关联 {@link ErpPurchaseInDO##getId()}
+     */
+    private Long inId;
+    /**
+     * 采购订单项编号
+     *
+     * 关联 {@link ErpPurchaseOrderItemDO#getId()}
+     * 目的:方便更新关联的采购订单项的入库数量
+     */
+    private Long orderItemId;
+    /**
+     * 仓库编号
+     *
+     * 关联 {@link ErpWarehouseDO#getId()}
+     */
+    private Long warehouseId;
+    /**
+     * 产品编号
+     *
+     * 关联 {@link ErpProductDO#getId()}
+     */
+    private Long productId;
+    /**
+     * 产品单位单位
+     *
+     * 冗余 {@link ErpProductDO#getUnitId()}
+     */
+    private Long productUnitId;
+
+    /**
+     * 产品单位单价,单位:元
+     */
+    private BigDecimal productPrice;
+    /**
+     * 数量
+     */
+    private BigDecimal count;
+    /**
+     * 总价,单位:元
+     *
+     * totalPrice = productPrice * count
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 税率,百分比
+     */
+    private BigDecimal taxPercent;
+    /**
+     * 税额,单位:元
+     *
+     * taxPrice = totalPrice * taxPercent
+     */
+    private BigDecimal taxPrice;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java
new file mode 100644
index 000000000..bba1542e3
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java
@@ -0,0 +1,115 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 采购订单 DO
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "erp_purchase_order")
+@KeySequence("erp_purchase_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpPurchaseOrderDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 采购订单号
+     */
+    private String no;
+    /**
+     * 采购状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.ErpAuditStatus}
+     */
+    private Integer status;
+    /**
+     * 供应商编号
+     *
+     * 关联 {@link ErpSupplierDO#getId()}
+     */
+    private Long supplierId;
+    /**
+     * 结算账户编号
+     *
+     * 关联 {@link ErpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 下单时间
+     */
+    private LocalDateTime orderTime;
+
+    /**
+     * 合计数量
+     */
+    private BigDecimal totalCount;
+    /**
+     * 最终合计价格,单位:元
+     *
+     * totalPrice = totalProductPrice + totalTaxPrice - discountPrice
+     */
+    private BigDecimal totalPrice;
+
+    /**
+     * 合计产品价格,单位:元
+     */
+    private BigDecimal totalProductPrice;
+    /**
+     * 合计税额,单位:元
+     */
+    private BigDecimal totalTaxPrice;
+    /**
+     * 优惠率,百分比
+     */
+    private BigDecimal discountPercent;
+    /**
+     * 优惠金额,单位:元
+     *
+     * discountPrice = (totalProductPrice + totalTaxPrice) * discountPercent
+     */
+    private BigDecimal discountPrice;
+    /**
+     * 定金金额,单位:元
+     */
+    private BigDecimal depositPrice;
+
+    /**
+     * 附件地址
+     */
+    private String fileUrl;
+    /**
+     * 备注
+     */
+    private String remark;
+
+    // ========== 采购入库 ==========
+    /**
+     * 采购入库数量
+     */
+    private BigDecimal inCount;
+
+    // ========== 采购退货(出库)) ==========
+    /**
+     * 采购退货数量
+     */
+    private BigDecimal returnCount;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderItemDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderItemDO.java
new file mode 100644
index 000000000..aa54d336b
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderItemDO.java
@@ -0,0 +1,93 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * ERP 采购订单项 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_purchase_order_items")
+@KeySequence("erp_purchase_order_items_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpPurchaseOrderItemDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 采购订单编号
+     *
+     * 关联 {@link ErpPurchaseOrderDO#getId()}
+     */
+    private Long orderId;
+    /**
+     * 产品编号
+     *
+     * 关联 {@link ErpProductDO#getId()}
+     */
+    private Long productId;
+    /**
+     * 产品单位单位
+     *
+     * 冗余 {@link ErpProductDO#getUnitId()}
+     */
+    private Long productUnitId;
+
+    /**
+     * 产品单位单价,单位:元
+     */
+    private BigDecimal productPrice;
+    /**
+     * 数量
+     */
+    private BigDecimal count;
+    /**
+     * 总价,单位:元
+     *
+     * totalPrice = productPrice * count
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 税率,百分比
+     */
+    private BigDecimal taxPercent;
+    /**
+     * 税额,单位:元
+     *
+     * taxPrice = totalPrice * taxPercent
+     */
+    private BigDecimal taxPrice;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    // ========== 采购入库 ==========
+    /**
+     * 采购入库数量
+     */
+    private BigDecimal inCount;
+
+    // ========== 采购退货(出库)) ==========
+    /**
+     * 采购退货数量
+     */
+    private BigDecimal returnCount;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseReturnDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseReturnDO.java
new file mode 100644
index 000000000..a1c8cc4d5
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseReturnDO.java
@@ -0,0 +1,122 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 采购退货 DO
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "erp_purchase_return")
+@KeySequence("erp_purchase_return_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpPurchaseReturnDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 采购退货单号
+     */
+    private String no;
+    /**
+     * 退货状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.ErpAuditStatus}
+     */
+    private Integer status;
+    /**
+     * 供应商编号
+     *
+     * 关联 {@link ErpSupplierDO#getId()}
+     */
+    private Long supplierId;
+    /**
+     * 结算账户编号
+     *
+     * 关联 {@link ErpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 退货时间
+     */
+    private LocalDateTime returnTime;
+
+    /**
+     * 采购订单编号
+     *
+     * 关联 {@link ErpPurchaseOrderDO#getId()}
+     */
+    private Long orderId;
+    /**
+     * 采购订单号
+     *
+     * 冗余 {@link ErpPurchaseOrderDO#getNo()}
+     */
+    private String orderNo;
+
+    /**
+     * 合计数量
+     */
+    private BigDecimal totalCount;
+    /**
+     * 最终合计价格,单位:元
+     *
+     * totalPrice = totalProductPrice + totalTaxPrice - discountPrice + otherPrice
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 已退款金额,单位:元
+     *
+     * 目的:和 TODO erp_finance_payment 结合,记录已退款金额
+     */
+    private BigDecimal refundPrice;
+
+    /**
+     * 合计产品价格,单位:元
+     */
+    private BigDecimal totalProductPrice;
+    /**
+     * 合计税额,单位:元
+     */
+    private BigDecimal totalTaxPrice;
+    /**
+     * 优惠率,百分比
+     */
+    private BigDecimal discountPercent;
+    /**
+     * 优惠金额,单位:元
+     *
+     * discountPrice = (totalProductPrice + totalTaxPrice) * discountPercent
+     */
+    private BigDecimal discountPrice;
+    /**
+     * 其它金额,单位:元
+     */
+    private BigDecimal otherPrice;
+
+    /**
+     * 附件地址
+     */
+    private String fileUrl;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseReturnItemDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseReturnItemDO.java
new file mode 100644
index 000000000..1e1713277
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseReturnItemDO.java
@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpWarehouseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * ERP 采购退货项 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_purchase_return_items")
+@KeySequence("erp_purchase_return_items_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpPurchaseReturnItemDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 采购退货编号
+     *
+     * 关联 {@link ErpPurchaseReturnDO##getId()}
+     */
+    private Long returnId;
+    /**
+     * 采购订单项编号
+     *
+     * 关联 {@link ErpPurchaseOrderItemDO#getId()}
+     * 目的:方便更新关联的采购订单项的退货数量
+     */
+    private Long orderItemId;
+    /**
+     * 仓库编号
+     *
+     * 关联 {@link ErpWarehouseDO#getId()}
+     */
+    private Long warehouseId;
+    /**
+     * 产品编号
+     *
+     * 关联 {@link ErpProductDO#getId()}
+     */
+    private Long productId;
+    /**
+     * 产品单位单位
+     *
+     * 冗余 {@link ErpProductDO#getUnitId()}
+     */
+    private Long productUnitId;
+
+    /**
+     * 产品单位单价,单位:元
+     */
+    private BigDecimal productPrice;
+    /**
+     * 数量
+     */
+    private BigDecimal count;
+    /**
+     * 总价,单位:元
+     *
+     * totalPrice = productPrice * count
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 税率,百分比
+     */
+    private BigDecimal taxPercent;
+    /**
+     * 税额,单位:元
+     *
+     * taxPrice = totalPrice * taxPercent
+     */
+    private BigDecimal taxPrice;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/supplier/ErpSupplierDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpSupplierDO.java
similarity index 96%
rename from yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/supplier/ErpSupplierDO.java
rename to yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpSupplierDO.java
index 108922a22..6e94c6669 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/supplier/ErpSupplierDO.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpSupplierDO.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.erp.dal.dataobject.supplier;
+package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpCustomerDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpCustomerDO.java
new file mode 100644
index 000000000..7bffcc17c
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpCustomerDO.java
@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.sale;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * ERP 客户 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_customer")
+@KeySequence("erp_customer_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpCustomerDO extends BaseDO {
+
+    /**
+     * 客户编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 客户名称
+     */
+    private String name;
+    /**
+     * 联系人
+     */
+    private String contact;
+    /**
+     * 手机号码
+     */
+    private String mobile;
+    /**
+     * 联系电话
+     */
+    private String telephone;
+    /**
+     * 电子邮箱
+     */
+    private String email;
+    /**
+     * 传真
+     */
+    private String fax;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 开启状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 排序
+     */
+    private Integer sort;
+    /**
+     * 纳税人识别号
+     */
+    private String taxNo;
+    /**
+     * 税率
+     */
+    private BigDecimal taxPercent;
+    /**
+     * 开户行
+     */
+    private String bankName;
+    /**
+     * 开户账号
+     */
+    private String bankAccount;
+    /**
+     * 开户地址
+     */
+    private String bankAddress;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java
index 749eabd49..5cdd4344e 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java
@@ -1,24 +1,21 @@
 package cn.iocoder.yudao.module.erp.dal.dataobject.sale;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
-import cn.iocoder.yudao.module.erp.enums.sale.ErpSaleOrderStatusEnum;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
-import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.util.List;
 
 /**
  * ERP 销售订单 DO
  *
  * @author 芋道源码
  */
-@TableName(value = "erp_sale_order", autoResultMap = true)
+@TableName(value = "erp_sale_order")
 @KeySequence("erp_sale_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -34,55 +31,67 @@ public class ErpSaleOrderDO extends BaseDO {
     @TableId
     private Long id;
     /**
-     * 销售单编号
+     * 销售订单号
      */
     private String no;
     /**
      * 销售状态
      *
-     * 枚举 {@link ErpSaleOrderStatusEnum}
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.ErpAuditStatus}
      */
     private Integer status;
     /**
      * 客户编号
      *
-     * TODO 芋艿:关联
+     * 关联 {@link ErpCustomerDO#getId()}
      */
     private Long customerId;
     /**
      * 结算账户编号
      *
-     * TODO 芋艿:关联
+     * 关联 {@link ErpAccountDO#getId()}
      */
     private Long accountId;
     /**
-     * 销售员编号数组
+     * 销售员编号
      *
-     * TODO 芋艿:关联
+     * 关联 AdminUserDO 的 id 字段
      */
-    @TableField(typeHandler = LongListTypeHandler.class)
-    private List<Long> salePersonIds;
+    private Long saleUserId;
     /**
      * 下单时间
      */
     private LocalDateTime orderTime;
 
     /**
-     * 合计价格,单位:元
+     * 合计数量
+     */
+    private BigDecimal totalCount;
+    /**
+     * 最终合计价格,单位:元
+     *
+     * totalPrice = totalProductPrice + totalTaxPrice - discountPrice
      */
     private BigDecimal totalPrice;
+
+    /**
+     * 合计产品价格,单位:元
+     */
+    private BigDecimal totalProductPrice;
+    /**
+     * 合计税额,单位:元
+     */
+    private BigDecimal totalTaxPrice;
     /**
      * 优惠率,百分比
      */
     private BigDecimal discountPercent;
     /**
      * 优惠金额,单位:元
+     *
+     * discountPrice = (totalProductPrice + totalTaxPrice) * discountPercent
      */
     private BigDecimal discountPrice;
-    /**
-     * 支付金额,单位:元
-     */
-    private BigDecimal payPrice;
     /**
      * 定金金额,单位:元
      */
@@ -95,6 +104,18 @@ public class ErpSaleOrderDO extends BaseDO {
     /**
      * 备注
      */
-    private String description;
+    private String remark;
+
+    // ========== 销售出库 ==========
+    /**
+     * 销售出库数量
+     */
+    private BigDecimal outCount;
+
+    // ========== 销售退货(入库)) ==========
+    /**
+     * 销售退货数量
+     */
+    private BigDecimal returnCount;
 
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSalesOrderItemDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderItemDO.java
similarity index 52%
rename from yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSalesOrderItemDO.java
rename to yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderItemDO.java
index 7e1b25f8d..4c829765b 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSalesOrderItemDO.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderItemDO.java
@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.erp.dal.dataobject.sale;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -9,19 +10,19 @@ import lombok.*;
 import java.math.BigDecimal;
 
 /**
- * ERP 销售订单明细 DO
+ * ERP 销售订单项 DO
  *
  * @author 芋道源码
  */
-@TableName("erp_sales_order_items")
-@KeySequence("erp_sales_order_items_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@TableName("erp_sale_order_items")
+@KeySequence("erp_sale_order_items_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class ErpSalesOrderItemDO extends BaseDO {
+public class ErpSaleOrderItemDO extends BaseDO {
 
     /**
      * 编号
@@ -34,55 +35,59 @@ public class ErpSalesOrderItemDO extends BaseDO {
      * 关联 {@link ErpSaleOrderDO#getId()}
      */
     private Long orderId;
+    /**
+     * 产品编号
+     *
+     * 关联 {@link ErpProductDO#getId()}
+     */
+    private Long productId;
+    /**
+     * 产品单位单位
+     *
+     * 冗余 {@link ErpProductDO#getUnitId()}
+     */
+    private Long productUnitId;
 
     /**
-     * 商品 SPU 编号
-     *
-     * TODO 芋艿 关联
-     */
-    private Long productSpuId;
-    /**
-     * 商品 SKU 编号
-     *
-     * TODO 芋艿 关联
-     */
-    private Long productSkuId;
-    /**
-     * 商品单位
-     *
-     * TODO 芋艿 冗余
-     */
-    private String productUnit;
-    /**
-     * 商品单价
-     *
-     * TODO 芋艿 冗余
+     * 产品单位单价,单位:元
      */
     private BigDecimal productPrice;
-
     /**
      * 数量
      */
-    private Integer count;
+    private BigDecimal count;
     /**
-     * 总价
+     * 总价,单位:元
+     *
+     * totalPrice = productPrice * count
      */
     private BigDecimal totalPrice;
-    /**
-     * 备注
-     */
-    private String description;
     /**
      * 税率,百分比
      */
     private BigDecimal taxPercent;
     /**
      * 税额,单位:元
+     *
+     * taxPrice = totalPrice * taxPercent
      */
     private BigDecimal taxPrice;
+
     /**
-     * 支付金额,单位:元
+     * 备注
      */
-    private BigDecimal payPrice;
+    private String remark;
+
+    // ========== 销售出库 ==========
+    /**
+     * 销售出库数量
+     */
+    private BigDecimal outCount;
+
+    // ========== 销售退货(入库)) ==========
+    /**
+     * 销售退货数量
+     */
+    private BigDecimal returnCount;
 
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOutDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOutDO.java
new file mode 100644
index 000000000..4bf567159
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOutDO.java
@@ -0,0 +1,134 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.sale;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 销售出库 DO
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "erp_sale_out")
+@KeySequence("erp_sale_out_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpSaleOutDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 销售出库单号
+     */
+    private String no;
+    /**
+     * 出库状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.ErpAuditStatus}
+     */
+    private Integer status;
+    /**
+     * 客户编号
+     *
+     * 关联 {@link ErpCustomerDO#getId()}
+     */
+    private Long customerId;
+    /**
+     * 结算账户编号
+     *
+     * 关联 {@link ErpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 销售员编号
+     *
+     * 关联 AdminUserDO 的 id 字段
+     */
+    private Long saleUserId;
+    /**
+     * 出库时间
+     */
+    private LocalDateTime outTime;
+
+    /**
+     * 销售订单编号
+     *
+     * 关联 {@link ErpSaleOrderDO#getId()}
+     */
+    private Long orderId;
+    /**
+     * 销售订单号
+     *
+     * 冗余 {@link ErpSaleOrderDO#getNo()}
+     */
+    private String orderNo;
+
+    /**
+     * 合计数量
+     */
+    private BigDecimal totalCount;
+    /**
+     * 最终合计价格,单位:元
+     *
+     * totalPrice = totalProductPrice + totalTaxPrice - discountPrice
+     */
+    private BigDecimal totalPrice;
+
+    /**
+     * 合计产品价格,单位:元
+     */
+    private BigDecimal totalProductPrice;
+    /**
+     * 合计税额,单位:元
+     */
+    private BigDecimal totalTaxPrice;
+    /**
+     * 优惠率,百分比
+     */
+    private BigDecimal discountPercent;
+    /**
+     * 优惠金额,单位:元
+     *
+     * discountPrice = (totalProductPrice + totalTaxPrice) * discountPercent
+     */
+    private BigDecimal discountPrice;
+    /**
+     * 其它金额,单位:元
+     */
+    private BigDecimal otherPrice;
+
+    // TODO 芋艿:receiptPrice
+    /**
+     * 本次收款,单位:元
+     *
+     * payPrice = totalPrice + otherPrice - debtPrice
+     */
+    private BigDecimal payPrice;
+    /**
+     * 本次欠款,单位:元
+     */
+    private BigDecimal debtPrice;
+
+    /**
+     * 附件地址
+     */
+    private String fileUrl;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOutItemDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOutItemDO.java
new file mode 100644
index 000000000..b9b406413
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOutItemDO.java
@@ -0,0 +1,96 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.sale;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpWarehouseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * ERP 销售出库项 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_sale_out_items")
+@KeySequence("erp_sale_out_items_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpSaleOutItemDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 销售出库编号
+     *
+     * 关联 {@link ErpStockOutDO##getId()}
+     */
+    private Long outId;
+    /**
+     * 销售订单项编号
+     *
+     * 关联 {@link ErpSaleOrderItemDO#getId()}
+     * 目的:方便更新关联的销售订单项的出库数量
+     */
+    private Long orderItemId;
+    /**
+     * 仓库编号
+     *
+     * 关联 {@link ErpWarehouseDO#getId()}
+     */
+    private Long warehouseId;
+    /**
+     * 产品编号
+     *
+     * 关联 {@link ErpProductDO#getId()}
+     */
+    private Long productId;
+    /**
+     * 产品单位单位
+     *
+     * 冗余 {@link ErpProductDO#getUnitId()}
+     */
+    private Long productUnitId;
+
+    /**
+     * 产品单位单价,单位:元
+     */
+    private BigDecimal productPrice;
+    /**
+     * 数量
+     */
+    private BigDecimal count;
+    /**
+     * 总价,单位:元
+     *
+     * totalPrice = productPrice * count
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 税率,百分比
+     */
+    private BigDecimal taxPercent;
+    /**
+     * 税额,单位:元
+     *
+     * taxPrice = totalPrice * taxPercent
+     */
+    private BigDecimal taxPrice;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleReturnDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleReturnDO.java
new file mode 100644
index 000000000..2ee54363c
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleReturnDO.java
@@ -0,0 +1,133 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.sale;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 销售退货 DO
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "erp_sale_return")
+@KeySequence("erp_sale_return_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpSaleReturnDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 销售退货单号
+     */
+    private String no;
+    /**
+     * 退货状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.ErpAuditStatus}
+     */
+    private Integer status;
+    /**
+     * 客户编号
+     *
+     * 关联 {@link ErpCustomerDO#getId()}
+     */
+    private Long customerId;
+    /**
+     * 结算账户编号
+     *
+     * 关联 {@link ErpAccountDO#getId()}
+     */
+    private Long accountId;
+    /**
+     * 销售员编号
+     *
+     * 关联 AdminUserDO 的 id 字段
+     */
+    private Long saleUserId;
+    /**
+     * 退货时间
+     */
+    private LocalDateTime returnTime;
+
+    /**
+     * 销售订单编号
+     *
+     * 关联 {@link ErpSaleOrderDO#getId()}
+     */
+    private Long orderId;
+    /**
+     * 销售订单号
+     *
+     * 冗余 {@link ErpSaleOrderDO#getNo()}
+     */
+    private String orderNo;
+
+    /**
+     * 合计数量
+     */
+    private BigDecimal totalCount;
+    /**
+     * 最终合计价格,单位:元
+     *
+     * totalPrice = totalProductPrice + totalTaxPrice - discountPrice
+     */
+    private BigDecimal totalPrice;
+
+    /**
+     * 合计产品价格,单位:元
+     */
+    private BigDecimal totalProductPrice;
+    /**
+     * 合计税额,单位:元
+     */
+    private BigDecimal totalTaxPrice;
+    /**
+     * 优惠率,百分比
+     */
+    private BigDecimal discountPercent;
+    /**
+     * 优惠金额,单位:元
+     *
+     * discountPrice = (totalProductPrice + totalTaxPrice) * discountPercent
+     */
+    private BigDecimal discountPrice;
+    /**
+     * 其它金额,单位:元
+     */
+    private BigDecimal otherPrice;
+
+    /**
+     * 本次收款,单位:元
+     *
+     * refundPrice = totalPrice + otherPrice - debtPrice
+     */
+    private BigDecimal refundPrice;
+    /**
+     * 本次欠款,单位:元
+     */
+    private BigDecimal debtPrice;
+
+    /**
+     * 附件地址
+     */
+    private String fileUrl;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleReturnItemDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleReturnItemDO.java
new file mode 100644
index 000000000..8851d157c
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleReturnItemDO.java
@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.sale;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpWarehouseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * ERP 销售退货项 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_sale_return_items")
+@KeySequence("erp_sale_return_items_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpSaleReturnItemDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 销售退货编号
+     *
+     * 关联 {@link ErpSaleReturnDO##getId()}
+     */
+    private Long returnId;
+    /**
+     * 销售订单项编号
+     *
+     * 关联 {@link ErpSaleOrderItemDO#getId()}
+     * 目的:方便更新关联的销售订单项的退货数量
+     */
+    private Long orderItemId;
+    /**
+     * 仓库编号
+     *
+     * 关联 {@link ErpWarehouseDO#getId()}
+     */
+    private Long warehouseId;
+    /**
+     * 产品编号
+     *
+     * 关联 {@link ErpProductDO#getId()}
+     */
+    private Long productId;
+    /**
+     * 产品单位单位
+     *
+     * 冗余 {@link ErpProductDO#getUnitId()}
+     */
+    private Long productUnitId;
+
+    /**
+     * 产品单位单价,单位:元
+     */
+    private BigDecimal productPrice;
+    /**
+     * 数量
+     */
+    private BigDecimal count;
+    /**
+     * 总价,单位:元
+     *
+     * totalPrice = productPrice * count
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 税率,百分比
+     */
+    private BigDecimal taxPercent;
+    /**
+     * 税额,单位:元
+     *
+     * taxPrice = totalPrice * taxPercent
+     */
+    private BigDecimal taxPrice;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockCheckDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockCheckDO.java
new file mode 100644
index 000000000..e9168275f
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockCheckDO.java
@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.stock;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 库存盘点单 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_stock_check")
+@KeySequence("erp_stock_check_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpStockCheckDO extends BaseDO {
+
+    /**
+     * 盘点编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 盘点单号
+     */
+    private String no;
+    /**
+     * 盘点时间
+     */
+    private LocalDateTime checkTime;
+    /**
+     * 合计数量
+     */
+    private BigDecimal totalCount;
+    /**
+     * 合计金额,单位:元
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.ErpAuditStatus}
+     */
+    private Integer status;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 附件 URL
+     */
+    private String fileUrl;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockCheckItemDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockCheckItemDO.java
new file mode 100644
index 000000000..c3c4dbf99
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockCheckItemDO.java
@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.stock;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * ERP 库存盘点单项 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_stock_check_item")
+@KeySequence("erp_stock_check_item_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpStockCheckItemDO extends BaseDO {
+
+    /**
+     * 盘点项编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 盘点编号
+     *
+     * 关联 {@link ErpStockCheckDO#getId()}
+     */
+    private Long checkId;
+    /**
+     * 仓库编号
+     *
+     * 关联 {@link ErpWarehouseDO#getId()}
+     */
+    private Long warehouseId;
+    /**
+     * 产品编号
+     *
+     * 关联 {@link ErpProductDO#getId()}
+     */
+    private Long productId;
+    /**
+     * 产品单位编号
+     *
+     * 冗余 {@link ErpProductDO#getUnitId()}
+     */
+    private Long productUnitId;
+    /**
+     * 产品单价
+     */
+    private BigDecimal productPrice;
+    /**
+     * 账面数量(当前库存)
+     */
+    private BigDecimal stockCount;
+    /**
+     * 实际数量(实际库存)
+     */
+    private BigDecimal actualCount;
+    /**
+     * 盈亏数量
+     *
+     * count = stockCount - actualCount
+     */
+    private BigDecimal count;
+    /**
+     * 合计金额,单位:元
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockInDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockInDO.java
index 4eaa316e9..ee2512ab6 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockInDO.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockInDO.java
@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.erp.dal.dataobject.stock;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -36,7 +37,7 @@ public class ErpStockInDO extends BaseDO {
     /**
      * 供应商编号
      *
-     * TODO 芋艿:待关联
+     * 关联 {@link ErpSupplierDO#getId()}
      */
     private Long supplierId;
     /**
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockMoveDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockMoveDO.java
new file mode 100644
index 000000000..682b33104
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockMoveDO.java
@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.stock;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 库存调拨单 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_stock_move")
+@KeySequence("erp_stock_move_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpStockMoveDO extends BaseDO {
+
+    /**
+     * 调拨编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 调拨单号
+     */
+    private String no;
+    /**
+     * 调拨时间
+     */
+    private LocalDateTime moveTime;
+    /**
+     * 合计数量
+     */
+    private BigDecimal totalCount;
+    /**
+     * 合计金额,单位:元
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.ErpAuditStatus}
+     */
+    private Integer status;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 附件 URL
+     */
+    private String fileUrl;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockMoveItemDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockMoveItemDO.java
new file mode 100644
index 000000000..aee203670
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockMoveItemDO.java
@@ -0,0 +1,79 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.stock;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * ERP 库存调拨单项 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_stock_move_item")
+@KeySequence("erp_stock_move_item_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpStockMoveItemDO extends BaseDO {
+
+    /**
+     * 调拨项编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 调拨编号
+     *
+     * 关联 {@link ErpStockMoveDO#getId()}
+     */
+    private Long moveId;
+    /**
+     * 调出仓库编号
+     *
+     * 关联 {@link ErpWarehouseDO#getId()}
+     */
+    private Long fromWarehouseId;
+    /**
+     * 调入仓库编号
+     *
+     * 关联 {@link ErpWarehouseDO#getId()}
+     */
+    private Long toWarehouseId;
+    /**
+     * 产品编号
+     *
+     * 关联 {@link ErpProductDO#getId()}
+     */
+    private Long productId;
+    /**
+     * 产品单位编号
+     *
+     * 冗余 {@link ErpProductDO#getUnitId()}
+     */
+    private Long productUnitId;
+    /**
+     * 产品单价
+     */
+    private BigDecimal productPrice;
+    /**
+     * 产品数量
+     */
+    private BigDecimal count;
+    /**
+     * 合计金额,单位:元
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockOutDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockOutDO.java
new file mode 100644
index 000000000..e0b337adb
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockOutDO.java
@@ -0,0 +1,69 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.stock;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 其它出库单 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_stock_out")
+@KeySequence("erp_stock_out_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpStockOutDO extends BaseDO {
+
+    /**
+     * 出库编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 出库单号
+     */
+    private String no;
+    /**
+     * 客户编号
+     *
+     * TODO 芋艿:待关联
+     */
+    private Long customerId;
+    /**
+     * 出库时间
+     */
+    private LocalDateTime outTime;
+    /**
+     * 合计数量
+     */
+    private BigDecimal totalCount;
+    /**
+     * 合计金额,单位:元
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 状态
+     *
+     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.ErpAuditStatus}
+     */
+    private Integer status;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 附件 URL
+     */
+    private String fileUrl;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockOutItemDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockOutItemDO.java
new file mode 100644
index 000000000..065c5255a
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockOutItemDO.java
@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.module.erp.dal.dataobject.stock;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.math.BigDecimal;
+
+/**
+ * ERP 其它出库单项 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("erp_stock_out_item")
+@KeySequence("erp_stock_out_item_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpStockOutItemDO extends BaseDO {
+
+    /**
+     * 出库项编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 出库编号
+     *
+     * 关联 {@link ErpStockOutDO#getId()}
+     */
+    private Long outId;
+    /**
+     * 仓库编号
+     *
+     * 关联 {@link ErpWarehouseDO#getId()}
+     */
+    private Long warehouseId;
+    /**
+     * 产品编号
+     *
+     * 关联 {@link ErpProductDO#getId()}
+     */
+    private Long productId;
+    /**
+     * 产品单位编号
+     *
+     * 冗余 {@link ErpProductDO#getUnitId()}
+     */
+    private Long productUnitId;
+    /**
+     * 产品单价
+     */
+    private BigDecimal productPrice;
+    /**
+     * 产品数量
+     */
+    private BigDecimal count;
+    /**
+     * 合计金额,单位:元
+     */
+    private BigDecimal totalPrice;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockRecordDO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockRecordDO.java
index be010eb04..7bc5e5a01 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockRecordDO.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/stock/ErpStockRecordDO.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.erp.dal.dataobject.stock;
 
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -56,25 +57,25 @@ public class ErpStockRecordDO extends BaseDO {
     /**
      * 业务类型
      *
-     * 枚举 {@link cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum}
+     * 枚举 {@link ErpStockRecordBizTypeEnum}
      */
     private Integer bizType;
     /**
      * 业务编号
      *
-     * 例如说:TODO
+     * 例如说:{@link ErpStockInDO#getId()}
      */
     private Long bizId;
     /**
      * 业务项编号
      *
-     * 例如说:TODO
+     * 例如说:{@link ErpStockInItemDO#getId()}
      */
     private Long bizItemId;
     /**
      * 业务单号
      *
-     * 例如说:TODO
+     * 例如说:{@link ErpStockInDO#getNo()}
      */
     private String bizNo;
 
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpAccountMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpAccountMapper.java
new file mode 100644
index 000000000..2f98147e2
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpAccountMapper.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.finance;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account.ErpAccountPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * ERP 结算账户 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpAccountMapper extends BaseMapperX<ErpAccountDO> {
+
+    default PageResult<ErpAccountDO> selectPage(ErpAccountPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ErpAccountDO>()
+                .likeIfPresent(ErpAccountDO::getName, reqVO.getName())
+                .likeIfPresent(ErpAccountDO::getNo, reqVO.getNo())
+                .eqIfPresent(ErpAccountDO::getRemark, reqVO.getRemark())
+                .orderByDesc(ErpAccountDO::getId));
+    }
+
+    default ErpAccountDO selectByDefaultStatus() {
+        return selectOne(ErpAccountDO::getDefaultStatus, true);
+    }
+
+    default List<ErpAccountDO> selectListByStatus(Integer status) {
+        return selectList(ErpAccountDO::getStatus, status);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentItemMapper.java
new file mode 100644
index 000000000..7787e8d70
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentItemMapper.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.finance;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentItemDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ERP 付款单项 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpFinancePaymentItemMapper extends BaseMapperX<ErpFinancePaymentItemDO> {
+
+    default List<ErpFinancePaymentItemDO> selectListByPaymentId(Long paymentId) {
+        return selectList(ErpFinancePaymentItemDO::getPaymentId, paymentId);
+    }
+
+    default List<ErpFinancePaymentItemDO> selectListByPaymentIds(Collection<Long> paymentIds) {
+        return selectList(ErpFinancePaymentItemDO::getPaymentId, paymentIds);
+    }
+
+    default BigDecimal selectPaymentPriceSumByBizIdAndBizType(Long bizId, Integer bizType) {
+        // SQL sum 查询
+        List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpFinancePaymentItemDO>()
+                .select("SUM(payment_price) AS paymentPriceSum")
+                .eq("biz_id", bizId)
+                .eq("biz_type", bizType));
+        // 获得数量
+        if (CollUtil.isEmpty(result)) {
+            return BigDecimal.ZERO;
+        }
+        return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "paymentPriceSum", 0D));
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java
new file mode 100644
index 000000000..5ad0cccfd
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/finance/ErpFinancePaymentMapper.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.finance;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentItemDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * ERP 付款单 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpFinancePaymentMapper extends BaseMapperX<ErpFinancePaymentDO> {
+
+    default PageResult<ErpFinancePaymentDO> selectPage(ErpFinancePaymentPageReqVO reqVO) {
+        MPJLambdaWrapperX<ErpFinancePaymentDO> query = new MPJLambdaWrapperX<ErpFinancePaymentDO>()
+                .likeIfPresent(ErpFinancePaymentDO::getNo, reqVO.getNo())
+                .betweenIfPresent(ErpFinancePaymentDO::getPaymentTime, reqVO.getPaymentTime())
+                .eqIfPresent(ErpFinancePaymentDO::getSupplierId, reqVO.getSupplierId())
+                .eqIfPresent(ErpFinancePaymentDO::getCreator, reqVO.getCreator())
+                .eqIfPresent(ErpFinancePaymentDO::getFinanceUserId, reqVO.getFinanceUserId())
+                .eqIfPresent(ErpFinancePaymentDO::getAccountId, reqVO.getAccountId())
+                .eqIfPresent(ErpFinancePaymentDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(ErpFinancePaymentDO::getRemark, reqVO.getRemark())
+                .orderByDesc(ErpFinancePaymentDO::getId);
+        if (reqVO.getBizNo() != null) {
+            query.leftJoin(ErpFinancePaymentItemDO.class, ErpFinancePaymentItemDO::getPaymentId, ErpFinancePaymentDO::getId)
+                    .eq(reqVO.getBizNo() != null, ErpFinancePaymentItemDO::getBizNo, reqVO.getBizNo())
+                    .groupBy(ErpFinancePaymentDO::getId); // 避免 1 对多查询,产生相同的 1
+        }
+        return selectJoinPage(reqVO, ErpFinancePaymentDO.class, query);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, ErpFinancePaymentDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpFinancePaymentDO>()
+                .eq(ErpFinancePaymentDO::getId, id).eq(ErpFinancePaymentDO::getStatus, status));
+    }
+
+    default ErpFinancePaymentDO selectByNo(String no) {
+        return selectOne(ErpFinancePaymentDO::getNo, no);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInItemMapper.java
new file mode 100644
index 000000000..9140f9548
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInItemMapper.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInItemDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+
+/**
+ * ERP 采购入库项 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpPurchaseInItemMapper extends BaseMapperX<ErpPurchaseInItemDO> {
+
+    default List<ErpPurchaseInItemDO> selectListByInId(Long inId) {
+        return selectList(ErpPurchaseInItemDO::getInId, inId);
+    }
+
+    default List<ErpPurchaseInItemDO> selectListByInIds(Collection<Long> inIds) {
+        return selectList(ErpPurchaseInItemDO::getInId, inIds);
+    }
+
+    default int deleteByInId(Long inId) {
+        return delete(ErpPurchaseInItemDO::getInId, inId);
+    }
+
+    /**
+     * 基于采购订单编号,查询每个采购订单项的入库数量之和
+     *
+     * @param inIds 入库订单项编号数组
+     * @return key:采购订单项编号;value:入库数量之和
+     */
+    default Map<Long, BigDecimal> selectOrderItemCountSumMapByInIds(Collection<Long> inIds) {
+        if (CollUtil.isEmpty(inIds)) {
+            return Collections.emptyMap();
+        }
+        // SQL sum 查询
+        List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpPurchaseInItemDO>()
+                .select("order_item_id, SUM(count) AS sumCount")
+                .groupBy("order_item_id")
+                .in("in_id", inIds));
+        // 获得数量
+        return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInMapper.java
new file mode 100644
index 000000000..c155d8cbe
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInMapper.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
+
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in.ErpPurchaseInPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInItemDO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * ERP 采购入库 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpPurchaseInMapper extends BaseMapperX<ErpPurchaseInDO> {
+
+    default PageResult<ErpPurchaseInDO> selectPage(ErpPurchaseInPageReqVO reqVO) {
+        MPJLambdaWrapperX<ErpPurchaseInDO> query = new MPJLambdaWrapperX<ErpPurchaseInDO>()
+                .likeIfPresent(ErpPurchaseInDO::getNo, reqVO.getNo())
+                .eqIfPresent(ErpPurchaseInDO::getSupplierId, reqVO.getSupplierId())
+                .betweenIfPresent(ErpPurchaseInDO::getInTime, reqVO.getInTime())
+                .eqIfPresent(ErpPurchaseInDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(ErpPurchaseInDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(ErpPurchaseInDO::getCreator, reqVO.getCreator())
+                .eqIfPresent(ErpPurchaseInDO::getAccountId, reqVO.getAccountId())
+                .likeIfPresent(ErpPurchaseInDO::getOrderNo, reqVO.getOrderNo())
+                .orderByDesc(ErpPurchaseInDO::getId);
+        // 付款状态。为什么需要 t. 的原因,是因为联表查询时,需要指定表名,不然会报字段不存在的错误
+        if (Objects.equals(reqVO.getPaymentStatus(), ErpPurchaseInPageReqVO.PAYMENT_STATUS_NONE)) {
+            query.eq(ErpPurchaseInDO::getPaymentPrice, 0);
+        } else if (Objects.equals(reqVO.getPaymentStatus(), ErpPurchaseInPageReqVO.PAYMENT_STATUS_PART)) {
+            query.gt(ErpPurchaseInDO::getPaymentPrice, 0).apply("t.payment_price < t.total_price");
+        } else if (Objects.equals(reqVO.getPaymentStatus(), ErpPurchaseInPageReqVO.PAYMENT_STATUS_ALL)) {
+            query.apply("t.payment_price = t.total_price");
+        }
+        if (Boolean.TRUE.equals(reqVO.getPaymentEnable())) {
+            query.eq(ErpPurchaseInDO::getStatus, ErpAuditStatus.APPROVE.getStatus())
+                    .apply("t.payment_price < t.total_price");
+        }
+        if (reqVO.getWarehouseId() != null || reqVO.getProductId() != null) {
+            query.leftJoin(ErpPurchaseInItemDO.class, ErpPurchaseInItemDO::getInId, ErpPurchaseInDO::getId)
+                    .eq(reqVO.getWarehouseId() != null, ErpPurchaseInItemDO::getWarehouseId, reqVO.getWarehouseId())
+                    .eq(reqVO.getProductId() != null, ErpPurchaseInItemDO::getProductId, reqVO.getProductId())
+                    .groupBy(ErpPurchaseInDO::getId); // 避免 1 对多查询,产生相同的 1
+        }
+        return selectJoinPage(reqVO, ErpPurchaseInDO.class, query);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, ErpPurchaseInDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpPurchaseInDO>()
+                .eq(ErpPurchaseInDO::getId, id).eq(ErpPurchaseInDO::getStatus, status));
+    }
+
+    default ErpPurchaseInDO selectByNo(String no) {
+        return selectOne(ErpPurchaseInDO::getNo, no);
+    }
+
+    default List<ErpPurchaseInDO> selectListByOrderId(Long orderId) {
+        return selectList(ErpPurchaseInDO::getOrderId, orderId);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderItemMapper.java
new file mode 100644
index 000000000..17f1fe290
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderItemMapper.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 采购订单明项目 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpPurchaseOrderItemMapper extends BaseMapperX<ErpPurchaseOrderItemDO> {
+
+    default List<ErpPurchaseOrderItemDO> selectListByOrderId(Long orderId) {
+        return selectList(ErpPurchaseOrderItemDO::getOrderId, orderId);
+    }
+
+    default List<ErpPurchaseOrderItemDO> selectListByOrderIds(Collection<Long> orderIds) {
+        return selectList(ErpPurchaseOrderItemDO::getOrderId, orderIds);
+    }
+
+    default int deleteByOrderId(Long orderId) {
+        return delete(ErpPurchaseOrderItemDO::getOrderId, orderId);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java
new file mode 100644
index 000000000..01f0303f9
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java
@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Objects;
+
+/**
+ * ERP 采购订单 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpPurchaseOrderMapper extends BaseMapperX<ErpPurchaseOrderDO> {
+
+    default PageResult<ErpPurchaseOrderDO> selectPage(ErpPurchaseOrderPageReqVO reqVO) {
+        MPJLambdaWrapperX<ErpPurchaseOrderDO> query = new MPJLambdaWrapperX<ErpPurchaseOrderDO>()
+                .likeIfPresent(ErpPurchaseOrderDO::getNo, reqVO.getNo())
+                .eqIfPresent(ErpPurchaseOrderDO::getSupplierId, reqVO.getSupplierId())
+                .betweenIfPresent(ErpPurchaseOrderDO::getOrderTime, reqVO.getOrderTime())
+                .eqIfPresent(ErpPurchaseOrderDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(ErpPurchaseOrderDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(ErpPurchaseOrderDO::getCreator, reqVO.getCreator())
+                .orderByDesc(ErpPurchaseOrderDO::getId);
+        // 入库状态。为什么需要 t. 的原因,是因为联表查询时,需要指定表名,不然会报 in_count 错误
+        if (Objects.equals(reqVO.getInStatus(), ErpPurchaseOrderPageReqVO.IN_STATUS_NONE)) {
+            query.eq(ErpPurchaseOrderDO::getInCount, 0);
+        } else if (Objects.equals(reqVO.getInStatus(), ErpPurchaseOrderPageReqVO.IN_STATUS_PART)) {
+            query.gt(ErpPurchaseOrderDO::getInCount, 0).apply("t.in_count < t.total_count");
+        } else if (Objects.equals(reqVO.getInStatus(), ErpPurchaseOrderPageReqVO.IN_STATUS_ALL)) {
+            query.apply("t.in_count = t.total_count");
+        }
+        // 退货状态
+        if (Objects.equals(reqVO.getReturnStatus(), ErpPurchaseOrderPageReqVO.RETURN_STATUS_NONE)) {
+            query.eq(ErpPurchaseOrderDO::getReturnCount, 0);
+        } else if (Objects.equals(reqVO.getReturnStatus(), ErpPurchaseOrderPageReqVO.RETURN_STATUS_PART)) {
+            query.gt(ErpPurchaseOrderDO::getReturnCount, 0).apply("t.return_count < t.total_count");
+        } else if (Objects.equals(reqVO.getReturnStatus(), ErpPurchaseOrderPageReqVO.RETURN_STATUS_ALL)) {
+            query.apply("t.return_count = t.total_count");
+        }
+        // 可采购入库
+        if (Boolean.TRUE.equals(reqVO.getInEnable())) {
+            query.eq(ErpPurchaseOrderDO::getStatus, ErpAuditStatus.APPROVE.getStatus())
+                    .apply("t.in_count < t.total_count");
+        }
+        // 可采购退货
+        if (Boolean.TRUE.equals(reqVO.getReturnEnable())) {
+            query.eq(ErpPurchaseOrderDO::getStatus, ErpAuditStatus.APPROVE.getStatus())
+                    .apply("t.return_count < t.in_count");
+        }
+        if (reqVO.getProductId() != null) {
+            query.leftJoin(ErpPurchaseOrderItemDO.class, ErpPurchaseOrderItemDO::getOrderId, ErpPurchaseOrderDO::getId)
+                    .eq(reqVO.getProductId() != null, ErpPurchaseOrderItemDO::getProductId, reqVO.getProductId())
+                    .groupBy(ErpPurchaseOrderDO::getId); // 避免 1 对多查询,产生相同的 1
+        }
+        return selectJoinPage(reqVO, ErpPurchaseOrderDO.class, query);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, ErpPurchaseOrderDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpPurchaseOrderDO>()
+                .eq(ErpPurchaseOrderDO::getId, id).eq(ErpPurchaseOrderDO::getStatus, status));
+    }
+
+    default ErpPurchaseOrderDO selectByNo(String no) {
+        return selectOne(ErpPurchaseOrderDO::getNo, no);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseReturnItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseReturnItemMapper.java
new file mode 100644
index 000000000..2a8011900
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseReturnItemMapper.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnItemDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+
+/**
+ * ERP 采购退货项 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpPurchaseReturnItemMapper extends BaseMapperX<ErpPurchaseReturnItemDO> {
+
+    default List<ErpPurchaseReturnItemDO> selectListByReturnId(Long returnId) {
+        return selectList(ErpPurchaseReturnItemDO::getReturnId, returnId);
+    }
+
+    default List<ErpPurchaseReturnItemDO> selectListByReturnIds(Collection<Long> returnIds) {
+        return selectList(ErpPurchaseReturnItemDO::getReturnId, returnIds);
+    }
+
+    default int deleteByReturnId(Long returnId) {
+        return delete(ErpPurchaseReturnItemDO::getReturnId, returnId);
+    }
+
+    /**
+     * 基于采购订单编号,查询每个采购订单项的退货数量之和
+     *
+     * @param returnIds 入库订单项编号数组
+     * @return key:采购订单项编号;value:退货数量之和
+     */
+    default Map<Long, BigDecimal> selectOrderItemCountSumMapByReturnIds(Collection<Long> returnIds) {
+        if (CollUtil.isEmpty(returnIds)) {
+            return Collections.emptyMap();
+        }
+        // SQL sum 查询
+        List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpPurchaseReturnItemDO>()
+                .select("order_item_id, SUM(count) AS sumCount")
+                .groupBy("order_item_id")
+                .in("return_id", returnIds));
+        // 获得数量
+        return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseReturnMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseReturnMapper.java
new file mode 100644
index 000000000..689a55dfd
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseReturnMapper.java
@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns.ErpPurchaseReturnPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnItemDO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * ERP 采购退货 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpPurchaseReturnMapper extends BaseMapperX<ErpPurchaseReturnDO> {
+
+    default PageResult<ErpPurchaseReturnDO> selectPage(ErpPurchaseReturnPageReqVO reqVO) {
+        MPJLambdaWrapperX<ErpPurchaseReturnDO> query = new MPJLambdaWrapperX<ErpPurchaseReturnDO>()
+                .likeIfPresent(ErpPurchaseReturnDO::getNo, reqVO.getNo())
+                .eqIfPresent(ErpPurchaseReturnDO::getSupplierId, reqVO.getSupplierId())
+                .betweenIfPresent(ErpPurchaseReturnDO::getReturnTime, reqVO.getReturnTime())
+                .eqIfPresent(ErpPurchaseReturnDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(ErpPurchaseReturnDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(ErpPurchaseReturnDO::getCreator, reqVO.getCreator())
+                .eqIfPresent(ErpPurchaseReturnDO::getAccountId, reqVO.getAccountId())
+                .likeIfPresent(ErpPurchaseReturnDO::getOrderNo, reqVO.getOrderNo())
+                .orderByDesc(ErpPurchaseReturnDO::getId);
+        // 退款状态。为什么需要 t. 的原因,是因为联表查询时,需要指定表名,不然会报字段不存在的错误
+        if (Objects.equals(reqVO.getRefundStatus(), ErpPurchaseReturnPageReqVO.REFUND_STATUS_NONE)) {
+            query.eq(ErpPurchaseReturnDO::getRefundPrice, 0);
+        } else if (Objects.equals(reqVO.getRefundStatus(), ErpPurchaseReturnPageReqVO.REFUND_STATUS_PART)) {
+            query.gt(ErpPurchaseReturnDO::getRefundPrice, 0).apply("t.refund_price < t.total_price");
+        } else if (Objects.equals(reqVO.getRefundStatus(), ErpPurchaseReturnPageReqVO.REFUND_STATUS_ALL)) {
+            query.apply("t.refund_price = t.total_price");
+        }
+        if (Boolean.TRUE.equals(reqVO.getRefundEnable())) {
+            query.eq(ErpPurchaseInDO::getStatus, ErpAuditStatus.APPROVE.getStatus())
+                    .apply("t.refund_price < t.total_price");
+        }
+        if (reqVO.getWarehouseId() != null || reqVO.getProductId() != null) {
+            query.leftJoin(ErpPurchaseReturnItemDO.class, ErpPurchaseReturnItemDO::getReturnId, ErpPurchaseReturnDO::getId)
+                    .eq(reqVO.getWarehouseId() != null, ErpPurchaseReturnItemDO::getWarehouseId, reqVO.getWarehouseId())
+                    .eq(reqVO.getProductId() != null, ErpPurchaseReturnItemDO::getProductId, reqVO.getProductId())
+                    .groupBy(ErpPurchaseReturnDO::getId); // 避免 1 对多查询,产生相同的 1
+        }
+        return selectJoinPage(reqVO, ErpPurchaseReturnDO.class, query);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, ErpPurchaseReturnDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpPurchaseReturnDO>()
+                .eq(ErpPurchaseReturnDO::getId, id).eq(ErpPurchaseReturnDO::getStatus, status));
+    }
+
+    default ErpPurchaseReturnDO selectByNo(String no) {
+        return selectOne(ErpPurchaseReturnDO::getNo, no);
+    }
+
+    default List<ErpPurchaseReturnDO> selectListByOrderId(Long orderId) {
+        return selectList(ErpPurchaseReturnDO::getOrderId, orderId);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpSupplierMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpSupplierMapper.java
index 93f3d6994..c74f1e6f8 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpSupplierMapper.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpSupplierMapper.java
@@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier.ErpSupplierPageReqVO;
-import cn.iocoder.yudao.module.erp.dal.dataobject.supplier.ErpSupplierDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.List;
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpCustomerMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpCustomerMapper.java
new file mode 100644
index 000000000..4970f9ad5
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpCustomerMapper.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.sale;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer.ErpCustomerPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * ERP 客户 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpCustomerMapper extends BaseMapperX<ErpCustomerDO> {
+
+    default PageResult<ErpCustomerDO> selectPage(ErpCustomerPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<ErpCustomerDO>()
+                .likeIfPresent(ErpCustomerDO::getName, reqVO.getName())
+                .eqIfPresent(ErpCustomerDO::getMobile, reqVO.getMobile())
+                .eqIfPresent(ErpCustomerDO::getTelephone, reqVO.getTelephone())
+                .orderByDesc(ErpCustomerDO::getId));
+    }
+
+    default List<ErpCustomerDO> selectListByStatus(Integer status) {
+        return selectList(ErpCustomerDO::getStatus, status);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderItemMapper.java
new file mode 100644
index 000000000..d2825e563
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderItemMapper.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.sale;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 销售订单明项目 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpSaleOrderItemMapper extends BaseMapperX<ErpSaleOrderItemDO> {
+
+    default List<ErpSaleOrderItemDO> selectListByOrderId(Long orderId) {
+        return selectList(ErpSaleOrderItemDO::getOrderId, orderId);
+    }
+
+    default List<ErpSaleOrderItemDO> selectListByOrderIds(Collection<Long> orderIds) {
+        return selectList(ErpSaleOrderItemDO::getOrderId, orderIds);
+    }
+
+    default int deleteByOrderId(Long orderId) {
+        return delete(ErpSaleOrderItemDO::getOrderId, orderId);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
index 07c80c480..8ed3b6fcd 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
@@ -3,11 +3,16 @@ package cn.iocoder.yudao.module.erp.dal.mysql.sale;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
 import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.Objects;
+
 /**
  * ERP 销售订单 Mapper
  *
@@ -17,14 +22,55 @@ import org.apache.ibatis.annotations.Mapper;
 public interface ErpSaleOrderMapper extends BaseMapperX<ErpSaleOrderDO> {
 
     default PageResult<ErpSaleOrderDO> selectPage(ErpSaleOrderPageReqVO reqVO) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<ErpSaleOrderDO>()
+        MPJLambdaWrapperX<ErpSaleOrderDO> query = new MPJLambdaWrapperX<ErpSaleOrderDO>()
                 .likeIfPresent(ErpSaleOrderDO::getNo, reqVO.getNo())
                 .eqIfPresent(ErpSaleOrderDO::getCustomerId, reqVO.getCustomerId())
                 .betweenIfPresent(ErpSaleOrderDO::getOrderTime, reqVO.getOrderTime())
-                .eqIfPresent(ErpSaleOrderDO::getDescription, reqVO.getDescription())
                 .eqIfPresent(ErpSaleOrderDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(ErpSaleOrderDO::getRemark, reqVO.getRemark())
                 .eqIfPresent(ErpSaleOrderDO::getCreator, reqVO.getCreator())
-                .orderByDesc(ErpSaleOrderDO::getId));
+                .orderByDesc(ErpSaleOrderDO::getId);
+        // 入库状态。为什么需要 t. 的原因,是因为联表查询时,需要指定表名,不然会报 out_count 错误
+        if (Objects.equals(reqVO.getOutStatus(), ErpSaleOrderPageReqVO.OUT_STATUS_NONE)) {
+            query.eq(ErpSaleOrderDO::getOutCount, 0);
+        } else if (Objects.equals(reqVO.getOutStatus(), ErpSaleOrderPageReqVO.OUT_STATUS_PART)) {
+            query.gt(ErpSaleOrderDO::getOutCount, 0).apply("t.out_count < t.total_count");
+        } else if (Objects.equals(reqVO.getOutStatus(), ErpSaleOrderPageReqVO.OUT_STATUS_ALL)) {
+            query.apply("t.out_count = t.total_count");
+        }
+        // 退货状态
+        if (Objects.equals(reqVO.getReturnStatus(), ErpSaleOrderPageReqVO.RETURN_STATUS_NONE)) {
+            query.eq(ErpSaleOrderDO::getReturnCount, 0);
+        } else if (Objects.equals(reqVO.getReturnStatus(), ErpSaleOrderPageReqVO.RETURN_STATUS_PART)) {
+            query.gt(ErpSaleOrderDO::getReturnCount, 0).apply("t.return_count < t.total_count");
+        } else if (Objects.equals(reqVO.getReturnStatus(), ErpSaleOrderPageReqVO.RETURN_STATUS_ALL)) {
+            query.apply("t.return_count = t.total_count");
+        }
+        // 可销售出库
+        if (Boolean.TRUE.equals(reqVO.getOutEnable())) {
+            query.eq(ErpSaleOrderDO::getStatus, ErpAuditStatus.APPROVE.getStatus())
+                    .apply("t.out_count < t.total_count");
+        }
+        // 可销售退货
+        if (Boolean.TRUE.equals(reqVO.getReturnEnable())) {
+            query.eq(ErpSaleOrderDO::getStatus, ErpAuditStatus.APPROVE.getStatus())
+                    .apply("t.return_count < t.out_count");
+        }
+        if (reqVO.getProductId() != null) {
+            query.leftJoin(ErpSaleOrderItemDO.class, ErpSaleOrderItemDO::getOrderId, ErpSaleOrderDO::getId)
+                    .eq(reqVO.getProductId() != null, ErpSaleOrderItemDO::getProductId, reqVO.getProductId())
+                    .groupBy(ErpSaleOrderDO::getId); // 避免 1 对多查询,产生相同的 1
+        }
+        return selectJoinPage(reqVO, ErpSaleOrderDO.class, query);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, ErpSaleOrderDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpSaleOrderDO>()
+                .eq(ErpSaleOrderDO::getId, id).eq(ErpSaleOrderDO::getStatus, status));
+    }
+
+    default ErpSaleOrderDO selectByNo(String no) {
+        return selectOne(ErpSaleOrderDO::getNo, no);
     }
 
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOutItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOutItemMapper.java
new file mode 100644
index 000000000..9cd5dede0
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOutItemMapper.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.sale;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOutItemDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+
+/**
+ * ERP 销售出库项 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpSaleOutItemMapper extends BaseMapperX<ErpSaleOutItemDO> {
+
+    default List<ErpSaleOutItemDO> selectListByOutId(Long outId) {
+        return selectList(ErpSaleOutItemDO::getOutId, outId);
+    }
+
+    default List<ErpSaleOutItemDO> selectListByOutIds(Collection<Long> outIds) {
+        return selectList(ErpSaleOutItemDO::getOutId, outIds);
+    }
+
+    default int deleteByOutId(Long outId) {
+        return delete(ErpSaleOutItemDO::getOutId, outId);
+    }
+
+    /**
+     * 基于销售订单编号,查询每个销售订单项的出库数量之和
+     *
+     * @param outIds 出库订单项编号数组
+     * @return key:销售订单项编号;value:出库数量之和
+     */
+    default Map<Long, BigDecimal> selectOrderItemCountSumMapByOutIds(Collection<Long> outIds) {
+        if (CollUtil.isEmpty(outIds)) {
+            return Collections.emptyMap();
+        }
+        // SQL sum 查询
+        List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpSaleOutItemDO>()
+                .select("order_item_id, SUM(count) AS sumCount")
+                .groupBy("order_item_id")
+                .in("out_id", outIds));
+        // 获得数量
+        return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOutMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOutMapper.java
new file mode 100644
index 000000000..e530557fb
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOutMapper.java
@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.sale;
+
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out.ErpSaleOutPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOutDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOutItemDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * ERP 销售出库 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpSaleOutMapper extends BaseMapperX<ErpSaleOutDO> {
+
+    default PageResult<ErpSaleOutDO> selectPage(ErpSaleOutPageReqVO reqVO) {
+        MPJLambdaWrapperX<ErpSaleOutDO> query = new MPJLambdaWrapperX<ErpSaleOutDO>()
+                .likeIfPresent(ErpSaleOutDO::getNo, reqVO.getNo())
+                .eqIfPresent(ErpSaleOutDO::getCustomerId, reqVO.getCustomerId())
+                .betweenIfPresent(ErpSaleOutDO::getOutTime, reqVO.getOutTime())
+                .eqIfPresent(ErpSaleOutDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(ErpSaleOutDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(ErpSaleOutDO::getCreator, reqVO.getCreator())
+                .eqIfPresent(ErpSaleOutDO::getAccountId, reqVO.getAccountId())
+                .likeIfPresent(ErpSaleOutDO::getOrderNo, reqVO.getOrderNo())
+                .orderByDesc(ErpSaleOutDO::getId);
+        if (Boolean.TRUE.equals(reqVO.getDebtStatus())) {
+            query.gt(ErpSaleOutDO::getDebtPrice, BigDecimal.ZERO);
+        } else if (Boolean.FALSE.equals(reqVO.getDebtStatus())) {
+            query.eq(ErpSaleOutDO::getDebtPrice, BigDecimal.ZERO);
+        }
+        if (reqVO.getWarehouseId() != null || reqVO.getProductId() != null) {
+            query.leftJoin(ErpSaleOutItemDO.class, ErpSaleOutItemDO::getOutId, ErpSaleOutDO::getId)
+                    .eq(reqVO.getWarehouseId() != null, ErpSaleOutItemDO::getWarehouseId, reqVO.getWarehouseId())
+                    .eq(reqVO.getProductId() != null, ErpSaleOutItemDO::getProductId, reqVO.getProductId())
+                    .groupBy(ErpSaleOutDO::getId); // 避免 1 对多查询,产生相同的 1
+        }
+        return selectJoinPage(reqVO, ErpSaleOutDO.class, query);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, ErpSaleOutDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpSaleOutDO>()
+                .eq(ErpSaleOutDO::getId, id).eq(ErpSaleOutDO::getStatus, status));
+    }
+
+    default ErpSaleOutDO selectByNo(String no) {
+        return selectOne(ErpSaleOutDO::getNo, no);
+    }
+
+    default List<ErpSaleOutDO> selectListByOrderId(Long orderId) {
+        return selectList(ErpSaleOutDO::getOrderId, orderId);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleReturnItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleReturnItemMapper.java
new file mode 100644
index 000000000..fdc572964
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleReturnItemMapper.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.sale;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleReturnItemDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+
+/**
+ * ERP 销售退货项 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpSaleReturnItemMapper extends BaseMapperX<ErpSaleReturnItemDO> {
+
+    default List<ErpSaleReturnItemDO> selectListByReturnId(Long returnId) {
+        return selectList(ErpSaleReturnItemDO::getReturnId, returnId);
+    }
+
+    default List<ErpSaleReturnItemDO> selectListByReturnIds(Collection<Long> returnIds) {
+        return selectList(ErpSaleReturnItemDO::getReturnId, returnIds);
+    }
+
+    default int deleteByReturnId(Long returnId) {
+        return delete(ErpSaleReturnItemDO::getReturnId, returnId);
+    }
+
+    /**
+     * 基于销售订单编号,查询每个销售订单项的退货数量之和
+     *
+     * @param returnIds 出库订单项编号数组
+     * @return key:销售订单项编号;value:退货数量之和
+     */
+    default Map<Long, BigDecimal> selectOrderItemCountSumMapByReturnIds(Collection<Long> returnIds) {
+        if (CollUtil.isEmpty(returnIds)) {
+            return Collections.emptyMap();
+        }
+        // SQL sum 查询
+        List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpSaleReturnItemDO>()
+                .select("order_item_id, SUM(count) AS sumCount")
+                .groupBy("order_item_id")
+                .in("return_id", returnIds));
+        // 获得数量
+        return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleReturnMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleReturnMapper.java
new file mode 100644
index 000000000..2ca45900c
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleReturnMapper.java
@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.sale;
+
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns.ErpSaleReturnPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleReturnDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleReturnItemDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * ERP 销售退货 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpSaleReturnMapper extends BaseMapperX<ErpSaleReturnDO> {
+
+    default PageResult<ErpSaleReturnDO> selectPage(ErpSaleReturnPageReqVO reqVO) {
+        MPJLambdaWrapperX<ErpSaleReturnDO> query = new MPJLambdaWrapperX<ErpSaleReturnDO>()
+                .likeIfPresent(ErpSaleReturnDO::getNo, reqVO.getNo())
+                .eqIfPresent(ErpSaleReturnDO::getCustomerId, reqVO.getCustomerId())
+                .betweenIfPresent(ErpSaleReturnDO::getReturnTime, reqVO.getReturnTime())
+                .eqIfPresent(ErpSaleReturnDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(ErpSaleReturnDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(ErpSaleReturnDO::getCreator, reqVO.getCreator())
+                .eqIfPresent(ErpSaleReturnDO::getAccountId, reqVO.getAccountId())
+                .likeIfPresent(ErpSaleReturnDO::getOrderNo, reqVO.getOrderNo())
+                .orderByDesc(ErpSaleReturnDO::getId);
+        if (reqVO.getWarehouseId() != null || reqVO.getProductId() != null) {
+            query.leftJoin(ErpSaleReturnItemDO.class, ErpSaleReturnItemDO::getReturnId, ErpSaleReturnDO::getId)
+                    .eq(reqVO.getWarehouseId() != null, ErpSaleReturnItemDO::getWarehouseId, reqVO.getWarehouseId())
+                    .eq(reqVO.getProductId() != null, ErpSaleReturnItemDO::getProductId, reqVO.getProductId())
+                    .groupBy(ErpSaleReturnDO::getId); // 避免 1 对多查询,产生相同的 1
+        }
+        return selectJoinPage(reqVO, ErpSaleReturnDO.class, query);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, ErpSaleReturnDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpSaleReturnDO>()
+                .eq(ErpSaleReturnDO::getId, id).eq(ErpSaleReturnDO::getStatus, status));
+    }
+
+    default ErpSaleReturnDO selectByNo(String no) {
+        return selectOne(ErpSaleReturnDO::getNo, no);
+    }
+
+    default List<ErpSaleReturnDO> selectListByOrderId(Long orderId) {
+        return selectList(ErpSaleReturnDO::getOrderId, orderId);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSalesOrderItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSalesOrderItemMapper.java
deleted file mode 100644
index eb8628291..000000000
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSalesOrderItemMapper.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package cn.iocoder.yudao.module.erp.dal.mysql.sale;
-
-import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSalesOrderItemDO;
-import org.apache.ibatis.annotations.Mapper;
-
-import java.util.List;
-
-/**
- * ERP 销售订单明细 Mapper
- *
- * @author 芋道源码
- */
-@Mapper
-public interface ErpSalesOrderItemMapper extends BaseMapperX<ErpSalesOrderItemDO> {
-
-    default List<ErpSalesOrderItemDO> selectListById(Long id) {
-        return selectList(ErpSalesOrderItemDO::getId, id);
-    }
-
-    default int deleteById(Long id) {
-        return delete(ErpSalesOrderItemDO::getId, id);
-    }
-
-}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockCheckItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockCheckItemMapper.java
new file mode 100644
index 000000000..ae13f9f96
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockCheckItemMapper.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.stock;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockCheckItemDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 库存盘点单项 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpStockCheckItemMapper extends BaseMapperX<ErpStockCheckItemDO> {
+
+    default List<ErpStockCheckItemDO> selectListByCheckId(Long checkId) {
+        return selectList(ErpStockCheckItemDO::getCheckId, checkId);
+    }
+
+    default List<ErpStockCheckItemDO> selectListByCheckIds(Collection<Long> checkIds) {
+        return selectList(ErpStockCheckItemDO::getCheckId, checkIds);
+    }
+
+    default int deleteByCheckId(Long checkId) {
+        return delete(ErpStockCheckItemDO::getCheckId, checkId);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockCheckMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockCheckMapper.java
new file mode 100644
index 000000000..dd976df3d
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockCheckMapper.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.stock;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check.ErpStockCheckPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockCheckDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockCheckItemDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * ERP 库存调拨单 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpStockCheckMapper extends BaseMapperX<ErpStockCheckDO> {
+
+    default PageResult<ErpStockCheckDO> selectPage(ErpStockCheckPageReqVO reqVO) {
+        MPJLambdaWrapperX<ErpStockCheckDO> query = new MPJLambdaWrapperX<ErpStockCheckDO>()
+                .likeIfPresent(ErpStockCheckDO::getNo, reqVO.getNo())
+                .betweenIfPresent(ErpStockCheckDO::getCheckTime, reqVO.getCheckTime())
+                .eqIfPresent(ErpStockCheckDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(ErpStockCheckDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(ErpStockCheckDO::getCreator, reqVO.getCreator())
+                .orderByDesc(ErpStockCheckDO::getId);
+        if (reqVO.getWarehouseId() != null || reqVO.getProductId() != null) {
+            query.leftJoin(ErpStockCheckItemDO.class, ErpStockCheckItemDO::getCheckId, ErpStockCheckDO::getId)
+                    .eq(reqVO.getWarehouseId() != null, ErpStockCheckItemDO::getWarehouseId, reqVO.getWarehouseId())
+                    .eq(reqVO.getProductId() != null, ErpStockCheckItemDO::getProductId, reqVO.getProductId())
+                    .groupBy(ErpStockCheckDO::getId); // 避免 1 对多查询,产生相同的 1
+        }
+        return selectJoinPage(reqVO, ErpStockCheckDO.class, query);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, ErpStockCheckDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpStockCheckDO>()
+                .eq(ErpStockCheckDO::getId, id).eq(ErpStockCheckDO::getStatus, status));
+    }
+
+    default ErpStockCheckDO selectByNo(String no) {
+        return selectOne(ErpStockCheckDO::getNo, no);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockInMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockInMapper.java
index 0b3e71a5f..e815583ac 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockInMapper.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockInMapper.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
 import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.in.ErpStockInPageReqVO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockInDO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockInItemDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 /**
@@ -18,7 +19,7 @@ public interface ErpStockInMapper extends BaseMapperX<ErpStockInDO> {
 
     default PageResult<ErpStockInDO> selectPage(ErpStockInPageReqVO reqVO) {
         MPJLambdaWrapperX<ErpStockInDO> query = new MPJLambdaWrapperX<ErpStockInDO>()
-                .eqIfPresent(ErpStockInDO::getNo, reqVO.getNo())
+                .likeIfPresent(ErpStockInDO::getNo, reqVO.getNo())
                 .eqIfPresent(ErpStockInDO::getSupplierId, reqVO.getSupplierId())
                 .betweenIfPresent(ErpStockInDO::getInTime, reqVO.getInTime())
                 .eqIfPresent(ErpStockInDO::getStatus, reqVO.getStatus())
@@ -34,4 +35,13 @@ public interface ErpStockInMapper extends BaseMapperX<ErpStockInDO> {
         return selectJoinPage(reqVO, ErpStockInDO.class, query);
     }
 
+    default int updateByIdAndStatus(Long id, Integer status, ErpStockInDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpStockInDO>()
+                .eq(ErpStockInDO::getId, id).eq(ErpStockInDO::getStatus, status));
+    }
+
+    default ErpStockInDO selectByNo(String no) {
+        return selectOne(ErpStockInDO::getNo, no);
+    }
+
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockMapper.java
index 5afe42da0..0ebc98597 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockMapper.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockMapper.java
@@ -1,12 +1,20 @@
 package cn.iocoder.yudao.module.erp.dal.mysql.stock;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.stock.ErpStockPageReqVO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
 /**
  * ERP 产品库存 Mapper
  *
@@ -27,4 +35,30 @@ public interface ErpStockMapper extends BaseMapperX<ErpStockDO> {
                 ErpStockDO::getWarehouseId, warehouseId);
     }
 
+    default int updateCountIncrement(Long id, BigDecimal count, boolean negativeEnable) {
+        LambdaUpdateWrapper<ErpStockDO> updateWrapper = new LambdaUpdateWrapper<ErpStockDO>()
+                .eq(ErpStockDO::getId, id);
+        if (count.compareTo(BigDecimal.ZERO) > 0) {
+            updateWrapper.setSql("count = count + " + count);
+        } else if (count.compareTo(BigDecimal.ZERO) < 0) {
+            if (!negativeEnable) {
+                updateWrapper.ge(ErpStockDO::getCount, count.abs());
+            }
+            updateWrapper.setSql("count = count - " + count.abs());
+        }
+        return update(null, updateWrapper);
+    }
+
+    default BigDecimal selectSumByProductId(Long productId) {
+        // SQL sum 查询
+        List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpStockDO>()
+                .select("SUM(count) AS sumCount")
+                .eq("product_id", productId));
+        // 获得数量
+        if (CollUtil.isEmpty(result)) {
+            return BigDecimal.ZERO;
+        }
+        return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "sumCount", 0D));
+    }
+
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockMoveItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockMoveItemMapper.java
new file mode 100644
index 000000000..21a267029
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockMoveItemMapper.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.stock;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockMoveItemDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 库存调拨单项 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpStockMoveItemMapper extends BaseMapperX<ErpStockMoveItemDO> {
+
+    default List<ErpStockMoveItemDO> selectListByMoveId(Long moveId) {
+        return selectList(ErpStockMoveItemDO::getMoveId, moveId);
+    }
+
+    default List<ErpStockMoveItemDO> selectListByMoveIds(Collection<Long> moveIds) {
+        return selectList(ErpStockMoveItemDO::getMoveId, moveIds);
+    }
+
+    default int deleteByMoveId(Long moveId) {
+        return delete(ErpStockMoveItemDO::getMoveId, moveId);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockMoveMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockMoveMapper.java
new file mode 100644
index 000000000..9a8ce0b64
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockMoveMapper.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.stock;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move.ErpStockMovePageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockMoveDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockMoveItemDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * ERP 库存调拨单 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpStockMoveMapper extends BaseMapperX<ErpStockMoveDO> {
+
+    default PageResult<ErpStockMoveDO> selectPage(ErpStockMovePageReqVO reqVO) {
+        MPJLambdaWrapperX<ErpStockMoveDO> query = new MPJLambdaWrapperX<ErpStockMoveDO>()
+                .likeIfPresent(ErpStockMoveDO::getNo, reqVO.getNo())
+                .betweenIfPresent(ErpStockMoveDO::getMoveTime, reqVO.getMoveTime())
+                .eqIfPresent(ErpStockMoveDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(ErpStockMoveDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(ErpStockMoveDO::getCreator, reqVO.getCreator())
+                .orderByDesc(ErpStockMoveDO::getId);
+        if (reqVO.getFromWarehouseId() != null || reqVO.getProductId() != null) {
+            query.leftJoin(ErpStockMoveItemDO.class, ErpStockMoveItemDO::getMoveId, ErpStockMoveDO::getId)
+                    .eq(reqVO.getFromWarehouseId() != null, ErpStockMoveItemDO::getFromWarehouseId, reqVO.getFromWarehouseId())
+                    .eq(reqVO.getProductId() != null, ErpStockMoveItemDO::getProductId, reqVO.getProductId())
+                    .groupBy(ErpStockMoveDO::getId); // 避免 1 对多查询,产生相同的 1
+        }
+        return selectJoinPage(reqVO, ErpStockMoveDO.class, query);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, ErpStockMoveDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpStockMoveDO>()
+                .eq(ErpStockMoveDO::getId, id).eq(ErpStockMoveDO::getStatus, status));
+    }
+
+    default ErpStockMoveDO selectByNo(String no) {
+        return selectOne(ErpStockMoveDO::getNo, no);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockOutItemMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockOutItemMapper.java
new file mode 100644
index 000000000..3b27cd3dc
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockOutItemMapper.java
@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.stock;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutItemDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 其它出库单项 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpStockOutItemMapper extends BaseMapperX<ErpStockOutItemDO> {
+
+    default List<ErpStockOutItemDO> selectListByOutId(Long outId) {
+        return selectList(ErpStockOutItemDO::getOutId, outId);
+    }
+
+    default List<ErpStockOutItemDO> selectListByOutIds(Collection<Long> outIds) {
+        return selectList(ErpStockOutItemDO::getOutId, outIds);
+    }
+
+    default int deleteByOutId(Long outId) {
+        return delete(ErpStockOutItemDO::getOutId, outId);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockOutMapper.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockOutMapper.java
new file mode 100644
index 000000000..a73dd3ccf
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/stock/ErpStockOutMapper.java
@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.erp.dal.mysql.stock;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out.ErpStockOutPageReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutItemDO;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * ERP 其它出库单 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface ErpStockOutMapper extends BaseMapperX<ErpStockOutDO> {
+
+    default PageResult<ErpStockOutDO> selectPage(ErpStockOutPageReqVO reqVO) {
+        MPJLambdaWrapperX<ErpStockOutDO> query = new MPJLambdaWrapperX<ErpStockOutDO>()
+                .likeIfPresent(ErpStockOutDO::getNo, reqVO.getNo())
+                .eqIfPresent(ErpStockOutDO::getCustomerId, reqVO.getCustomerId())
+                .betweenIfPresent(ErpStockOutDO::getOutTime, reqVO.getOutTime())
+                .eqIfPresent(ErpStockOutDO::getStatus, reqVO.getStatus())
+                .likeIfPresent(ErpStockOutDO::getRemark, reqVO.getRemark())
+                .eqIfPresent(ErpStockOutDO::getCreator, reqVO.getCreator())
+                .orderByDesc(ErpStockOutDO::getId);
+        if (reqVO.getWarehouseId() != null || reqVO.getProductId() != null) {
+            query.leftJoin(ErpStockOutItemDO.class, ErpStockOutItemDO::getOutId, ErpStockOutDO::getId)
+                    .eq(reqVO.getWarehouseId() != null, ErpStockOutItemDO::getWarehouseId, reqVO.getWarehouseId())
+                    .eq(reqVO.getProductId() != null, ErpStockOutItemDO::getProductId, reqVO.getProductId())
+                    .groupBy(ErpStockOutDO::getId); // 避免 1 对多查询,产生相同的 1
+        }
+        return selectJoinPage(reqVO, ErpStockOutDO.class, query);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, ErpStockOutDO updateObj) {
+        return update(updateObj, new LambdaUpdateWrapper<ErpStockOutDO>()
+                .eq(ErpStockOutDO::getId, id).eq(ErpStockOutDO::getStatus, status));
+    }
+
+    default ErpStockOutDO selectByNo(String no) {
+        return selectOne(ErpStockOutDO::getNo, no);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/RedisKeyConstants.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/RedisKeyConstants.java
new file mode 100644
index 000000000..f0ba46807
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/RedisKeyConstants.java
@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.erp.dal.redis;
+
+/**
+ * ERP Redis Key 枚举类
+ *
+ * @author 芋道源码
+ */
+public interface RedisKeyConstants {
+
+    /**
+     * 序号的缓存
+     *
+     * KEY 格式:trade_no:{prefix}
+     * VALUE 数据格式:编号自增
+     */
+    String NO = "seq_no:";
+
+}
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
new file mode 100644
index 000000000..d99990133
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
@@ -0,0 +1,92 @@
+package cn.iocoder.yudao.module.erp.dal.redis.no;
+
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.date.DateUtil;
+import cn.iocoder.yudao.module.erp.dal.redis.RedisKeyConstants;
+import jakarta.annotation.Resource;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+
+/**
+ * 订单序号的 Redis DAO
+ *
+ * @author HUIHUI
+ */
+@Repository
+public class ErpNoRedisDAO {
+
+    /**
+     * 其它入库 {@link cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockInDO}
+     */
+    public static final String STOCK_IN_NO_PREFIX = "QTRK";
+    /**
+     * 其它出库 {@link cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutDO}
+     */
+    public static final String STOCK_OUT_NO_PREFIX = "QCKD";
+
+    /**
+     * 库存调拨 {@link cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockMoveDO}
+     */
+    public static final String STOCK_MOVE_NO_PREFIX = "QCDB";
+
+    /**
+     * 库存盘点 {@link cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockCheckDO}
+     */
+    public static final String STOCK_CHECK_NO_PREFIX = "QCPD";
+
+    /**
+     * 销售订单 {@link cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO}
+     */
+    public static final String SALE_ORDER_NO_PREFIX = "XSDD";
+    /**
+     * 销售出库 {@link cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOutDO}
+     */
+    public static final String SALE_OUT_NO_PREFIX = "XSCK";
+    /**
+     * 销售退货 {@link cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleReturnDO}
+     */
+    public static final String SALE_RETURN_NO_PREFIX = "XSTH";
+
+    /**
+     * 采购订单 {@link cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO}
+     */
+    public static final String PURCHASE_ORDER_NO_PREFIX = "CGDD";
+    /**
+     * 采购入库 {@link cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO}
+     */
+    public static final String PURCHASE_IN_NO_PREFIX = "CGRK";
+    /**
+     * 采购退货 {@link cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnDO}
+     */
+    public static final String PURCHASE_RETURN_NO_PREFIX = "CGTH";
+
+    /**
+     * 付款单 {@link cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO}
+     */
+    public static final String FINANCE_PAYMENT_NO_PREFIX = "FKD";
+
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
+    /**
+     * 生成序号,使用当前日期,格式为 {PREFIX} + yyyyMMdd + 6 位自增
+     * 例如说:QTRK 202109 000001 (没有中间空格)
+     *
+     * @param prefix 前缀
+     * @return 序号
+     */
+    public String generate(String prefix) {
+        // 递增序号
+        String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
+        String key = RedisKeyConstants.NO + noPrefix;
+        Long no = stringRedisTemplate.opsForValue().increment(key);
+        // 设置过期时间
+        stringRedisTemplate.expire(key, Duration.ofDays(1L));
+        return noPrefix + String.format("%06d", no);
+    }
+
+}
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountService.java
new file mode 100644
index 000000000..502423fb2
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountService.java
@@ -0,0 +1,102 @@
+package cn.iocoder.yudao.module.erp.service.finance;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account.ErpAccountPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account.ErpAccountSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+
+/**
+ * ERP 结算账户 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpAccountService {
+
+    /**
+     * 创建结算账户
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createAccount(@Valid ErpAccountSaveReqVO createReqVO);
+
+    /**
+     * 更新ERP 结算账户
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateAccount(@Valid ErpAccountSaveReqVO updateReqVO);
+
+    /**
+     * 更新结算账户默认状态
+     *
+     * @param id 编号
+     * @param defaultStatus 默认状态
+     */
+    void updateAccountDefaultStatus(Long id, Boolean defaultStatus);
+
+    /**
+     * 删除结算账户
+     *
+     * @param id 编号
+     */
+    void deleteAccount(Long id);
+
+    /**
+     * 获得结算账户
+     *
+     * @param id 编号
+     * @return 结算账户
+     */
+    ErpAccountDO getAccount(Long id);
+
+    /**
+     * 校验结算账户
+     *
+     * @param id 编号
+     * @return 结算账户
+     */
+    ErpAccountDO validateAccount(Long id);
+
+    /**
+     * 获得指定状态的结算账户列表
+     *
+     * @param status 状态
+     * @return 结算账户
+     */
+    List<ErpAccountDO> getAccountListByStatus(Integer status);
+
+    /**
+     * 获得结算账户列表
+     *
+     * @param ids 编号数组
+     * @return 结算账户列表
+     */
+    List<ErpAccountDO> getAccountList(Collection<Long> ids);
+
+    /**
+     * 获得结算账户 Map
+     *
+     * @param ids 编号数组
+     * @return 结算账户 Map
+     */
+    default Map<Long, ErpAccountDO> getAccountMap(Collection<Long> ids) {
+        return convertMap(getAccountList(ids), ErpAccountDO::getId);
+    }
+
+    /**
+     * 获得结算账户分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 结算账户分页
+     */
+    PageResult<ErpAccountDO> getAccountPage(ErpAccountPageReqVO pageReqVO);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountServiceImpl.java
new file mode 100644
index 000000000..9a8e85828
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpAccountServiceImpl.java
@@ -0,0 +1,112 @@
+package cn.iocoder.yudao.module.erp.service.finance;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account.ErpAccountPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account.ErpAccountSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpAccountMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+/**
+ * ERP 结算账户 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpAccountServiceImpl implements ErpAccountService {
+
+    @Resource
+    private ErpAccountMapper accountMapper;
+
+    @Override
+    public Long createAccount(ErpAccountSaveReqVO createReqVO) {
+        // 插入
+        ErpAccountDO account = BeanUtils.toBean(createReqVO, ErpAccountDO.class);
+        accountMapper.insert(account);
+        // 返回
+        return account.getId();
+    }
+
+    @Override
+    public void updateAccount(ErpAccountSaveReqVO updateReqVO) {
+        // 校验存在
+        validateAccountExists(updateReqVO.getId());
+        // 更新
+        ErpAccountDO updateObj = BeanUtils.toBean(updateReqVO, ErpAccountDO.class);
+        accountMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void updateAccountDefaultStatus(Long id, Boolean defaultStatus) {
+        // 1. 校验存在
+        validateAccountExists(id);
+
+        // 2.1 如果开启,则需要关闭所有其它的默认
+        if (defaultStatus) {
+            ErpAccountDO account = accountMapper.selectByDefaultStatus();
+            if (account != null) {
+                accountMapper.updateById(new ErpAccountDO().setId(account.getId()).setDefaultStatus(false));
+            }
+        }
+        // 2.2 更新对应的默认状态
+        accountMapper.updateById(new ErpAccountDO().setId(id).setDefaultStatus(defaultStatus));
+    }
+
+    @Override
+    public void deleteAccount(Long id) {
+        // 校验存在
+        validateAccountExists(id);
+        // 删除
+        accountMapper.deleteById(id);
+    }
+
+    private void validateAccountExists(Long id) {
+        if (accountMapper.selectById(id) == null) {
+            throw exception(ACCOUNT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ErpAccountDO getAccount(Long id) {
+        return accountMapper.selectById(id);
+    }
+
+    @Override
+    public ErpAccountDO validateAccount(Long id) {
+        ErpAccountDO account = accountMapper.selectById(id);
+        if (account == null) {
+            throw exception(ACCOUNT_NOT_EXISTS);
+        }
+        if (CommonStatusEnum.isDisable(account.getStatus())) {
+            throw exception(ACCOUNT_NOT_ENABLE, account.getName());
+        }
+        return account;
+    }
+
+    @Override
+    public List<ErpAccountDO> getAccountListByStatus(Integer status) {
+        return accountMapper.selectListByStatus(status);
+    }
+
+    @Override
+    public List<ErpAccountDO> getAccountList(Collection<Long> ids) {
+        return accountMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<ErpAccountDO> getAccountPage(ErpAccountPageReqVO pageReqVO) {
+        return accountMapper.selectPage(pageReqVO);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentService.java
new file mode 100644
index 000000000..b2f27917d
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentService.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.erp.service.finance;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentItemDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 付款单 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpFinancePaymentService {
+
+    /**
+     * 创建付款单
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createFinancePayment(@Valid ErpFinancePaymentSaveReqVO createReqVO);
+
+    /**
+     * 更新付款单
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateFinancePayment(@Valid ErpFinancePaymentSaveReqVO updateReqVO);
+
+    /**
+     * 更新付款单的状态
+     *
+     * @param id 编号
+     * @param status 状态
+     */
+    void updateFinancePaymentStatus(Long id, Integer status);
+
+    /**
+     * 删除付款单
+     *
+     * @param ids 编号数组
+     */
+    void deleteFinancePayment(List<Long> ids);
+
+    /**
+     * 获得付款单
+     *
+     * @param id 编号
+     * @return 付款单
+     */
+    ErpFinancePaymentDO getFinancePayment(Long id);
+
+    /**
+     * 获得付款单分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 付款单分页
+     */
+    PageResult<ErpFinancePaymentDO> getFinancePaymentPage(ErpFinancePaymentPageReqVO pageReqVO);
+
+    // ==================== 付款单项 ====================
+
+    /**
+     * 获得付款单项列表
+     *
+     * @param paymentId 付款单编号
+     * @return 付款单项列表
+     */
+    List<ErpFinancePaymentItemDO> getFinancePaymentItemListByPaymentId(Long paymentId);
+
+    /**
+     * 获得付款单项 List
+     *
+     * @param paymentIds 付款单编号数组
+     * @return 付款单项 List
+     */
+    List<ErpFinancePaymentItemDO> getFinancePaymentItemListByPaymentIds(Collection<Long> paymentIds);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
new file mode 100644
index 000000000..68121763c
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
@@ -0,0 +1,273 @@
+package cn.iocoder.yudao.module.erp.service.finance;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment.ErpFinancePaymentSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinancePaymentItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentItemMapper;
+import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.enums.common.ErpBizTypeEnum;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseReturnService;
+import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+// TODO 芋艿:记录操作日志
+
+/**
+ * ERP 付款单 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpFinancePaymentServiceImpl implements ErpFinancePaymentService {
+
+    @Resource
+    private ErpFinancePaymentMapper financePaymentMapper;
+    @Resource
+    private ErpFinancePaymentItemMapper financePaymentItemMapper;
+
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
+    @Resource
+    private ErpSupplierService supplierService;
+    @Resource
+    private ErpAccountService accountService;
+    @Resource
+    private ErpPurchaseInService purchaseInService;
+    @Resource
+    private ErpPurchaseReturnService purchaseReturnService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createFinancePayment(ErpFinancePaymentSaveReqVO createReqVO) {
+        // 1.1 校验订单项的有效性
+        List<ErpFinancePaymentItemDO> paymentItems = validateFinancePaymentItems(
+                createReqVO.getSupplierId(), createReqVO.getItems());
+        // 1.2 校验供应商
+        supplierService.validateSupplier(createReqVO.getSupplierId());
+        // 1.3 校验结算账户
+        if (createReqVO.getAccountId() != null) {
+            accountService.validateAccount(createReqVO.getAccountId());
+        }
+        // 1.4 校验财务人员
+        if (createReqVO.getFinanceUserId() != null) {
+            adminUserApi.validateUser(createReqVO.getFinanceUserId());
+        }
+        // 1.5 生成付款单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.FINANCE_PAYMENT_NO_PREFIX);
+        if (financePaymentMapper.selectByNo(no) != null) {
+            throw exception(FINANCE_PAYMENT_NO_EXISTS);
+        }
+
+        // 2.1 插入付款单
+        ErpFinancePaymentDO payment = BeanUtils.toBean(createReqVO, ErpFinancePaymentDO.class, in -> in
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()));
+        calculateTotalPrice(payment, paymentItems);
+        financePaymentMapper.insert(payment);
+        // 2.2 插入付款单项
+        paymentItems.forEach(o -> o.setPaymentId(payment.getId()));
+        financePaymentItemMapper.insertBatch(paymentItems);
+        return payment.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateFinancePayment(ErpFinancePaymentSaveReqVO updateReqVO) {
+        // 1.1 校验存在
+        ErpFinancePaymentDO payment = validateFinancePaymentExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(payment.getStatus())) {
+            throw exception(FINANCE_PAYMENT_UPDATE_FAIL_APPROVE, payment.getNo());
+        }
+        // 1.2 校验供应商
+        supplierService.validateSupplier(updateReqVO.getSupplierId());
+        // 1.3 校验结算账户
+        if (updateReqVO.getAccountId() != null) {
+            accountService.validateAccount(updateReqVO.getAccountId());
+        }
+        // 1.4 校验财务人员
+        if (updateReqVO.getFinanceUserId() != null) {
+            adminUserApi.validateUser(updateReqVO.getFinanceUserId());
+        }
+        // 1.5 校验付款单项的有效性
+        List<ErpFinancePaymentItemDO> paymentItems = validateFinancePaymentItems(
+                updateReqVO.getSupplierId(), updateReqVO.getItems());
+
+        // 2.1 更新付款单
+        ErpFinancePaymentDO updateObj = BeanUtils.toBean(updateReqVO, ErpFinancePaymentDO.class);
+        calculateTotalPrice(updateObj, paymentItems);
+        financePaymentMapper.updateById(updateObj);
+        // 2.2 更新付款单项
+        updateFinancePaymentItemList(updateReqVO.getId(), paymentItems);
+
+        // 3. 更新采购入库、退货的付款金额情况
+        updatePurchasePrice(paymentItems);
+    }
+
+    private void calculateTotalPrice(ErpFinancePaymentDO payment, List<ErpFinancePaymentItemDO> paymentItems) {
+        payment.setTotalPrice(getSumValue(paymentItems, ErpFinancePaymentItemDO::getPaymentPrice, BigDecimal::add, BigDecimal.ZERO));
+        payment.setPaymentPrice(payment.getTotalPrice().subtract(payment.getDiscountPrice()));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateFinancePaymentStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpFinancePaymentDO payment = validateFinancePaymentExists(id);
+        // 1.2 校验状态
+        if (payment.getStatus().equals(status)) {
+            throw exception(approve ? FINANCE_PAYMENT_APPROVE_FAIL : FINANCE_PAYMENT_PROCESS_FAIL);
+        }
+
+        // 2. 更新状态
+        int updateCount = financePaymentMapper.updateByIdAndStatus(id, payment.getStatus(),
+                new ErpFinancePaymentDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? FINANCE_PAYMENT_APPROVE_FAIL : FINANCE_PAYMENT_PROCESS_FAIL);
+        }
+    }
+
+    private List<ErpFinancePaymentItemDO> validateFinancePaymentItems(
+            Long supplierId,
+            List<ErpFinancePaymentSaveReqVO.Item> list) {
+        return convertList(list, o -> BeanUtils.toBean(o, ErpFinancePaymentItemDO.class, item -> {
+            if (ObjectUtil.equal(item.getBizType(), ErpBizTypeEnum.PURCHASE_IN.getType())) {
+                ErpPurchaseInDO purchaseIn = purchaseInService.validatePurchaseIn(item.getBizId());
+                Assert.equals(purchaseIn.getSupplierId(), supplierId, "供应商必须相同");
+                item.setTotalPrice(purchaseIn.getTotalPrice()).setBizNo(purchaseIn.getNo());
+            } else if (ObjectUtil.equal(item.getBizType(), ErpBizTypeEnum.PURCHASE_RETURN.getType())) {
+                ErpPurchaseReturnDO purchaseReturn = purchaseReturnService.validatePurchaseReturn(item.getBizId());
+                Assert.equals(purchaseReturn.getSupplierId(), supplierId, "供应商必须相同");
+                item.setTotalPrice(purchaseReturn.getTotalPrice().negate()).setBizNo(purchaseReturn.getNo());
+            } else {
+                throw new IllegalArgumentException("业务类型不正确:" + item.getBizType());
+            }
+        }));
+    }
+
+    private void updateFinancePaymentItemList(Long id, List<ErpFinancePaymentItemDO> newList) {
+        // 第一步,对比新老数据,获得添加、修改、删除的列表
+        List<ErpFinancePaymentItemDO> oldList = financePaymentItemMapper.selectListByPaymentId(id);
+        List<List<ErpFinancePaymentItemDO>> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录
+                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+        // 第二步,批量添加、修改、删除
+        if (CollUtil.isNotEmpty(diffList.get(0))) {
+            diffList.get(0).forEach(o -> o.setPaymentId(id));
+            financePaymentItemMapper.insertBatch(diffList.get(0));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(1))) {
+            financePaymentItemMapper.updateBatch(diffList.get(1));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(2))) {
+            financePaymentItemMapper.deleteBatchIds(convertList(diffList.get(2), ErpFinancePaymentItemDO::getId));
+        }
+
+        // 第三步,更新采购入库、退货的付款金额情况
+        updatePurchasePrice(CollectionUtils.newArrayList(diffList));
+    }
+
+    private void updatePurchasePrice(List<ErpFinancePaymentItemDO> paymentItems) {
+        paymentItems.forEach(paymentItem -> {
+            BigDecimal totalPaymentPrice = financePaymentItemMapper.selectPaymentPriceSumByBizIdAndBizType(
+                    paymentItem.getBizId(), paymentItem.getBizType());
+            if (ErpBizTypeEnum.PURCHASE_IN.getType().equals(paymentItem.getBizType())) {
+                purchaseInService.updatePurchaseInPaymentPrice(paymentItem.getBizId(), totalPaymentPrice);
+            } else if (ErpBizTypeEnum.PURCHASE_RETURN.getType().equals(paymentItem.getBizType())) {
+                purchaseReturnService.updatePurchaseReturnRefundPrice(paymentItem.getBizId(), totalPaymentPrice.negate());
+            } else {
+                throw new IllegalArgumentException("业务类型不正确:" + paymentItem.getBizType());
+            }
+        });
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteFinancePayment(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpFinancePaymentDO> payments = financePaymentMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(payments)) {
+            return;
+        }
+        payments.forEach(payment -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(payment.getStatus())) {
+                throw exception(FINANCE_PAYMENT_DELETE_FAIL_APPROVE, payment.getNo());
+            }
+        });
+
+        // 2. 遍历删除,并记录操作日志
+        payments.forEach(payment -> {
+            // 2.1 删除付款单
+            financePaymentMapper.deleteById(payment.getId());
+            // 2.2 删除付款单项
+            List<ErpFinancePaymentItemDO> paymentItems = financePaymentItemMapper.selectListByPaymentId(payment.getId());
+            financePaymentItemMapper.deleteBatchIds(convertSet(paymentItems, ErpFinancePaymentItemDO::getId));
+
+            // 2.3 更新采购入库、退货的付款金额情况
+            updatePurchasePrice(paymentItems);
+        });
+    }
+
+    private ErpFinancePaymentDO validateFinancePaymentExists(Long id) {
+        ErpFinancePaymentDO payment = financePaymentMapper.selectById(id);
+        if (payment == null) {
+            throw exception(FINANCE_PAYMENT_NOT_EXISTS);
+        }
+        return payment;
+    }
+
+    @Override
+    public ErpFinancePaymentDO getFinancePayment(Long id) {
+        return financePaymentMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<ErpFinancePaymentDO> getFinancePaymentPage(ErpFinancePaymentPageReqVO pageReqVO) {
+        return financePaymentMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 付款单项 ====================
+
+    @Override
+    public List<ErpFinancePaymentItemDO> getFinancePaymentItemListByPaymentId(Long paymentId) {
+        return financePaymentItemMapper.selectListByPaymentId(paymentId);
+    }
+
+    @Override
+    public List<ErpFinancePaymentItemDO> getFinancePaymentItemListByPaymentIds(Collection<Long> paymentIds) {
+        if (CollUtil.isEmpty(paymentIds)) {
+            return Collections.emptyList();
+        }
+        return financePaymentItemMapper.selectListByPaymentIds(paymentIds);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductServiceImpl.java
index 53794043f..cbe689c6a 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductServiceImpl.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductServiceImpl.java
@@ -105,6 +105,9 @@ public class ErpProductServiceImpl implements ErpProductService {
 
     @Override
     public List<ErpProductRespVO> getProductVOList(Collection<Long> ids) {
+        if (CollUtil.isEmpty(ids)) {
+            return Collections.emptyList();
+        }
         List<ErpProductDO> list = productMapper.selectBatchIds(ids);
         return buildProductVOList(list);
     }
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductUnitServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductUnitServiceImpl.java
index 87d78a54f..89ba6b08e 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductUnitServiceImpl.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductUnitServiceImpl.java
@@ -74,7 +74,7 @@ public class ErpProductUnitServiceImpl implements ErpProductUnitService {
     public void deleteProductUnit(Long id) {
         // 1.1 校验存在
         validateProductUnitExists(id);
-        // 1.2 校验商品是否使用
+        // 1.2 校验产品是否使用
         if (productService.getProductCountByUnitId(id) > 0) {
             throw exception(PRODUCT_UNIT_EXITS_PRODUCT);
         }
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInService.java
new file mode 100644
index 000000000..37aa452cd
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInService.java
@@ -0,0 +1,101 @@
+package cn.iocoder.yudao.module.erp.service.purchase;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in.ErpPurchaseInPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in.ErpPurchaseInSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInItemDO;
+import jakarta.validation.Valid;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 采购入库 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpPurchaseInService {
+
+    /**
+     * 创建采购入库
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createPurchaseIn(@Valid ErpPurchaseInSaveReqVO createReqVO);
+
+    /**
+     * 更新采购入库
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updatePurchaseIn(@Valid ErpPurchaseInSaveReqVO updateReqVO);
+
+    /**
+     * 更新采购入库的状态
+     *
+     * @param id 编号
+     * @param status 状态
+     */
+    void updatePurchaseInStatus(Long id, Integer status);
+
+    /**
+     * 更新采购入库的付款金额
+     *
+     * @param id 编号
+     * @param paymentPrice 付款金额
+     */
+    void updatePurchaseInPaymentPrice(Long id, BigDecimal paymentPrice);
+
+    /**
+     * 删除采购入库
+     *
+     * @param ids 编号数组
+     */
+    void deletePurchaseIn(List<Long> ids);
+
+    /**
+     * 获得采购入库
+     *
+     * @param id 编号
+     * @return 采购入库
+     */
+    ErpPurchaseInDO getPurchaseIn(Long id);
+
+    /**
+     * 校验采购入库,已经审核通过
+     *
+     * @param id 编号
+     * @return 采购入库
+     */
+    ErpPurchaseInDO validatePurchaseIn(Long id);
+
+    /**
+     * 获得采购入库分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 采购入库分页
+     */
+    PageResult<ErpPurchaseInDO> getPurchaseInPage(ErpPurchaseInPageReqVO pageReqVO);
+
+    // ==================== 采购入库项 ====================
+
+    /**
+     * 获得采购入库项列表
+     *
+     * @param inId 采购入库编号
+     * @return 采购入库项列表
+     */
+    List<ErpPurchaseInItemDO> getPurchaseInItemListByInId(Long inId);
+
+    /**
+     * 获得采购入库项 List
+     *
+     * @param inIds 采购入库编号数组
+     * @return 采购入库项 List
+     */
+    List<ErpPurchaseInItemDO> getPurchaseInItemListByInIds(Collection<Long> inIds);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInServiceImpl.java
new file mode 100644
index 000000000..b2ac729ed
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInServiceImpl.java
@@ -0,0 +1,308 @@
+package cn.iocoder.yudao.module.erp.service.purchase;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in.ErpPurchaseInPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in.ErpPurchaseInSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInItemMapper;
+import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum;
+import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockRecordService;
+import cn.iocoder.yudao.module.erp.service.stock.bo.ErpStockRecordCreateReqBO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+// TODO 芋艿:记录操作日志
+
+/**
+ * ERP 采购入库 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpPurchaseInServiceImpl implements ErpPurchaseInService {
+
+    @Resource
+    private ErpPurchaseInMapper purchaseInMapper;
+    @Resource
+    private ErpPurchaseInItemMapper purchaseInItemMapper;
+
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private ErpPurchaseOrderService purchaseOrderService;
+    @Resource
+    private ErpAccountService accountService;
+    @Resource
+    private ErpStockRecordService stockRecordService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createPurchaseIn(ErpPurchaseInSaveReqVO createReqVO) {
+        // 1.1 校验采购订单已审核
+        ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.validatePurchaseOrder(createReqVO.getOrderId());
+        // 1.2 校验入库项的有效性
+        List<ErpPurchaseInItemDO> purchaseInItems = validatePurchaseInItems(createReqVO.getItems());
+        // 1.3 校验结算账户
+        accountService.validateAccount(createReqVO.getAccountId());
+        // 1.4 生成入库单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_IN_NO_PREFIX);
+        if (purchaseInMapper.selectByNo(no) != null) {
+            throw exception(PURCHASE_IN_NO_EXISTS);
+        }
+
+        // 2.1 插入入库
+        ErpPurchaseInDO purchaseIn = BeanUtils.toBean(createReqVO, ErpPurchaseInDO.class, in -> in
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()))
+                .setOrderNo(purchaseOrder.getNo()).setSupplierId(purchaseOrder.getSupplierId());
+        calculateTotalPrice(purchaseIn, purchaseInItems);
+        purchaseInMapper.insert(purchaseIn);
+        // 2.2 插入入库项
+        purchaseInItems.forEach(o -> o.setInId(purchaseIn.getId()));
+        purchaseInItemMapper.insertBatch(purchaseInItems);
+
+        // 3. 更新采购订单的入库数量
+        updatePurchaseOrderInCount(createReqVO.getOrderId());
+        return purchaseIn.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePurchaseIn(ErpPurchaseInSaveReqVO updateReqVO) {
+        // 1.1 校验存在
+        ErpPurchaseInDO purchaseIn = validatePurchaseInExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(purchaseIn.getStatus())) {
+            throw exception(PURCHASE_IN_UPDATE_FAIL_APPROVE, purchaseIn.getNo());
+        }
+        // 1.2 校验采购订单已审核
+        ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.validatePurchaseOrder(updateReqVO.getOrderId());
+        // 1.3 校验结算账户
+        accountService.validateAccount(updateReqVO.getAccountId());
+        // 1.4 校验订单项的有效性
+        List<ErpPurchaseInItemDO> purchaseInItems = validatePurchaseInItems(updateReqVO.getItems());
+
+        // 2.1 更新入库
+        ErpPurchaseInDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseInDO.class)
+                .setOrderNo(purchaseOrder.getNo()).setSupplierId(purchaseOrder.getSupplierId());
+        calculateTotalPrice(updateObj, purchaseInItems);
+        purchaseInMapper.updateById(updateObj);
+        // 2.2 更新入库项
+        updatePurchaseInItemList(updateReqVO.getId(), purchaseInItems);
+
+        // 3.1 更新采购订单的入库数量
+        updatePurchaseOrderInCount(updateObj.getOrderId());
+        // 3.2 注意:如果采购订单编号变更了,需要更新“老”采购订单的入库数量
+        if (ObjectUtil.notEqual(purchaseIn.getOrderId(), updateObj.getOrderId())) {
+            updatePurchaseOrderInCount(purchaseIn.getOrderId());
+        }
+    }
+
+    private void calculateTotalPrice(ErpPurchaseInDO purchaseIn, List<ErpPurchaseInItemDO> purchaseInItems) {
+        purchaseIn.setTotalCount(getSumValue(purchaseInItems, ErpPurchaseInItemDO::getCount, BigDecimal::add));
+        purchaseIn.setTotalProductPrice(getSumValue(purchaseInItems, ErpPurchaseInItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
+        purchaseIn.setTotalTaxPrice(getSumValue(purchaseInItems, ErpPurchaseInItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
+        purchaseIn.setTotalPrice(purchaseIn.getTotalProductPrice().add(purchaseIn.getTotalTaxPrice()));
+        // 计算优惠价格
+        if (purchaseIn.getDiscountPercent() == null) {
+            purchaseIn.setDiscountPercent(BigDecimal.ZERO);
+        }
+        purchaseIn.setDiscountPrice(MoneyUtils.priceMultiplyPercent(purchaseIn.getTotalPrice(), purchaseIn.getDiscountPercent()));
+        purchaseIn.setTotalPrice(purchaseIn.getTotalPrice().subtract(purchaseIn.getDiscountPrice().add(purchaseIn.getOtherPrice())));
+    }
+
+    private void updatePurchaseOrderInCount(Long orderId) {
+        // 1.1 查询采购订单对应的采购入库单列表
+        List<ErpPurchaseInDO> purchaseIns = purchaseInMapper.selectListByOrderId(orderId);
+        // 1.2 查询对应的采购订单项的入库数量
+        Map<Long, BigDecimal> returnCountMap = purchaseInItemMapper.selectOrderItemCountSumMapByInIds(
+                convertList(purchaseIns, ErpPurchaseInDO::getId));
+        // 2. 更新采购订单的入库数量
+        purchaseOrderService.updatePurchaseOrderInCount(orderId, returnCountMap);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePurchaseInStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpPurchaseInDO purchaseIn = validatePurchaseInExists(id);
+        // 1.2 校验状态
+        if (purchaseIn.getStatus().equals(status)) {
+            throw exception(approve ? PURCHASE_IN_APPROVE_FAIL : PURCHASE_IN_PROCESS_FAIL);
+        }
+        // 1.3 校验已付款
+        if (approve && purchaseIn.getPaymentPrice().compareTo(BigDecimal.ZERO) > 0) {
+            throw exception(PURCHASE_IN_PROCESS_FAIL_EXISTS_PAYMENT);
+        }
+
+        // 2. 更新状态
+        int updateCount = purchaseInMapper.updateByIdAndStatus(id, purchaseIn.getStatus(),
+                new ErpPurchaseInDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? PURCHASE_IN_APPROVE_FAIL : PURCHASE_IN_PROCESS_FAIL);
+        }
+
+        // 3. 变更库存
+        List<ErpPurchaseInItemDO> purchaseInItems = purchaseInItemMapper.selectListByInId(id);
+        Integer bizType = approve ? ErpStockRecordBizTypeEnum.PURCHASE_IN.getType()
+                : ErpStockRecordBizTypeEnum.PURCHASE_IN_CANCEL.getType();
+        purchaseInItems.forEach(purchaseInItem -> {
+            BigDecimal count = approve ? purchaseInItem.getCount() : purchaseInItem.getCount().negate();
+            stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(
+                    purchaseInItem.getProductId(), purchaseInItem.getWarehouseId(), count,
+                    bizType, purchaseInItem.getInId(), purchaseInItem.getId(), purchaseIn.getNo()));
+        });
+    }
+
+    @Override
+    public void updatePurchaseInPaymentPrice(Long id, BigDecimal paymentPrice) {
+        ErpPurchaseInDO purchaseIn = purchaseInMapper.selectById(id);
+        if (purchaseIn.getPaymentPrice().equals(paymentPrice)) {
+            return;
+        }
+        if (paymentPrice.compareTo(purchaseIn.getTotalPrice()) > 0) {
+            throw exception(PURCHASE_IN_FAIL_PAYMENT_PRICE_EXCEED, purchaseIn.getTotalPrice(), paymentPrice);
+        }
+        purchaseInMapper.updateById(new ErpPurchaseInDO().setId(id).setPaymentPrice(paymentPrice));
+    }
+
+    private List<ErpPurchaseInItemDO> validatePurchaseInItems(List<ErpPurchaseInSaveReqVO.Item> list) {
+        // 1. 校验产品存在
+        List<ErpProductDO> productList = productService.validProductList(
+                convertSet(list, ErpPurchaseInSaveReqVO.Item::getProductId));
+        Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
+        // 2. 转化为 ErpPurchaseInItemDO 列表
+        return convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseInItemDO.class, item -> {
+            item.setProductUnitId(productMap.get(item.getProductId()).getUnitId());
+            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
+            if (item.getTotalPrice() == null) {
+                return;
+            }
+            if (item.getTaxPercent() != null) {
+                item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
+            }
+        }));
+    }
+
+    private void updatePurchaseInItemList(Long id, List<ErpPurchaseInItemDO> newList) {
+        // 第一步,对比新老数据,获得添加、修改、删除的列表
+        List<ErpPurchaseInItemDO> oldList = purchaseInItemMapper.selectListByInId(id);
+        List<List<ErpPurchaseInItemDO>> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录
+                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+        // 第二步,批量添加、修改、删除
+        if (CollUtil.isNotEmpty(diffList.get(0))) {
+            diffList.get(0).forEach(o -> o.setInId(id));
+            purchaseInItemMapper.insertBatch(diffList.get(0));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(1))) {
+            purchaseInItemMapper.updateBatch(diffList.get(1));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(2))) {
+            purchaseInItemMapper.deleteBatchIds(convertList(diffList.get(2), ErpPurchaseInItemDO::getId));
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deletePurchaseIn(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpPurchaseInDO> purchaseIns = purchaseInMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(purchaseIns)) {
+            return;
+        }
+        purchaseIns.forEach(purchaseIn -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(purchaseIn.getStatus())) {
+                throw exception(PURCHASE_IN_DELETE_FAIL_APPROVE, purchaseIn.getNo());
+            }
+        });
+
+        // 2. 遍历删除,并记录操作日志
+        purchaseIns.forEach(purchaseIn -> {
+            // 2.1 删除订单
+            purchaseInMapper.deleteById(purchaseIn.getId());
+            // 2.2 删除订单项
+            purchaseInItemMapper.deleteByInId(purchaseIn.getId());
+
+            // 2.3 更新采购订单的入库数量
+            updatePurchaseOrderInCount(purchaseIn.getOrderId());
+        });
+
+    }
+
+    private ErpPurchaseInDO validatePurchaseInExists(Long id) {
+        ErpPurchaseInDO purchaseIn = purchaseInMapper.selectById(id);
+        if (purchaseIn == null) {
+            throw exception(PURCHASE_IN_NOT_EXISTS);
+        }
+        return purchaseIn;
+    }
+
+    @Override
+    public ErpPurchaseInDO getPurchaseIn(Long id) {
+        return purchaseInMapper.selectById(id);
+    }
+
+    @Override
+    public ErpPurchaseInDO validatePurchaseIn(Long id) {
+        ErpPurchaseInDO purchaseIn = validatePurchaseInExists(id);
+        if (ObjectUtil.notEqual(purchaseIn.getStatus(), ErpAuditStatus.APPROVE.getStatus())) {
+            throw exception(PURCHASE_IN_NOT_APPROVE);
+        }
+        return purchaseIn;
+    }
+
+    @Override
+    public PageResult<ErpPurchaseInDO> getPurchaseInPage(ErpPurchaseInPageReqVO pageReqVO) {
+        return purchaseInMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 采购入库项 ====================
+
+    @Override
+    public List<ErpPurchaseInItemDO> getPurchaseInItemListByInId(Long inId) {
+        return purchaseInItemMapper.selectListByInId(inId);
+    }
+
+    @Override
+    public List<ErpPurchaseInItemDO> getPurchaseInItemListByInIds(Collection<Long> inIds) {
+        if (CollUtil.isEmpty(inIds)) {
+            return Collections.emptyList();
+        }
+        return purchaseInItemMapper.selectListByInIds(inIds);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java
new file mode 100644
index 000000000..bfea076bb
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java
@@ -0,0 +1,110 @@
+package cn.iocoder.yudao.module.erp.service.purchase;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
+import jakarta.validation.Valid;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ERP 采购订单 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpPurchaseOrderService {
+
+    /**
+     * 创建采购订单
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createPurchaseOrder(@Valid ErpPurchaseOrderSaveReqVO createReqVO);
+
+    /**
+     * 更新采购订单
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updatePurchaseOrder(@Valid ErpPurchaseOrderSaveReqVO updateReqVO);
+
+    /**
+     * 更新采购订单的状态
+     *
+     * @param id 编号
+     * @param status 状态
+     */
+    void updatePurchaseOrderStatus(Long id, Integer status);
+
+    /**
+     * 更新采购订单的入库数量
+     *
+     * @param id 编号
+     * @param inCountMap 入库数量 Map:key 采购订单项编号;value 入库数量
+     */
+    void updatePurchaseOrderInCount(Long id, Map<Long, BigDecimal> inCountMap);
+
+    /**
+     * 更新采购订单的退货数量
+     *
+     * @param orderId 编号
+     * @param returnCountMap 退货数量 Map:key 采购订单项编号;value 退货数量
+     */
+    void updatePurchaseOrderReturnCount(Long orderId, Map<Long, BigDecimal> returnCountMap);
+
+    /**
+     * 删除采购订单
+     *
+     * @param ids 编号数组
+     */
+    void deletePurchaseOrder(List<Long> ids);
+
+    /**
+     * 获得采购订单
+     *
+     * @param id 编号
+     * @return 采购订单
+     */
+    ErpPurchaseOrderDO getPurchaseOrder(Long id);
+
+    /**
+     * 校验采购订单,已经审核通过
+     *
+     * @param id 编号
+     * @return 采购订单
+     */
+    ErpPurchaseOrderDO validatePurchaseOrder(Long id);
+
+    /**
+     * 获得采购订单分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 采购订单分页
+     */
+    PageResult<ErpPurchaseOrderDO> getPurchaseOrderPage(ErpPurchaseOrderPageReqVO pageReqVO);
+
+    // ==================== 采购订单项 ====================
+
+    /**
+     * 获得采购订单项列表
+     *
+     * @param orderId 采购订单编号
+     * @return 采购订单项列表
+     */
+    List<ErpPurchaseOrderItemDO> getPurchaseOrderItemListByOrderId(Long orderId);
+
+    /**
+     * 获得采购订单项 List
+     *
+     * @param orderIds 采购订单编号数组
+     * @return 采购订单项 List
+     */
+    List<ErpPurchaseOrderItemDO> getPurchaseOrderItemListByOrderIds(Collection<Long> orderIds);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
new file mode 100644
index 000000000..74b5b18d2
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -0,0 +1,295 @@
+package cn.iocoder.yudao.module.erp.service.purchase;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderItemMapper;
+import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseOrderMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+// TODO 芋艿:记录操作日志
+
+/**
+ * ERP 采购订单 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpPurchaseOrderServiceImpl implements ErpPurchaseOrderService {
+
+    @Resource
+    private ErpPurchaseOrderMapper purchaseOrderMapper;
+    @Resource
+    private ErpPurchaseOrderItemMapper purchaseOrderItemMapper;
+
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpSupplierService supplierService;
+    @Resource
+    private ErpAccountService accountService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createPurchaseOrder(ErpPurchaseOrderSaveReqVO createReqVO) {
+        // 1.1 校验订单项的有效性
+        List<ErpPurchaseOrderItemDO> purchaseOrderItems = validatePurchaseOrderItems(createReqVO.getItems());
+        // 1.2 校验供应商
+        supplierService.validateSupplier(createReqVO.getSupplierId());
+        // 1.3 校验结算账户
+        if (createReqVO.getAccountId() != null) {
+            accountService.validateAccount(createReqVO.getAccountId());
+        }
+        // 1.4 生成订单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_ORDER_NO_PREFIX);
+        if (purchaseOrderMapper.selectByNo(no) != null) {
+            throw exception(PURCHASE_ORDER_NO_EXISTS);
+        }
+
+        // 2.1 插入订单
+        ErpPurchaseOrderDO purchaseOrder = BeanUtils.toBean(createReqVO, ErpPurchaseOrderDO.class, in -> in
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()));
+        calculateTotalPrice(purchaseOrder, purchaseOrderItems);
+        purchaseOrderMapper.insert(purchaseOrder);
+        // 2.2 插入订单项
+        purchaseOrderItems.forEach(o -> o.setOrderId(purchaseOrder.getId()));
+        purchaseOrderItemMapper.insertBatch(purchaseOrderItems);
+        return purchaseOrder.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePurchaseOrder(ErpPurchaseOrderSaveReqVO updateReqVO) {
+        // 1.1 校验存在
+        ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(purchaseOrder.getStatus())) {
+            throw exception(PURCHASE_ORDER_UPDATE_FAIL_APPROVE, purchaseOrder.getNo());
+        }
+        // 1.2 校验供应商
+        supplierService.validateSupplier(updateReqVO.getSupplierId());
+        // 1.3 校验结算账户
+        if (updateReqVO.getAccountId() != null) {
+            accountService.validateAccount(updateReqVO.getAccountId());
+        }
+        // 1.4 校验订单项的有效性
+        List<ErpPurchaseOrderItemDO> purchaseOrderItems = validatePurchaseOrderItems(updateReqVO.getItems());
+
+        // 2.1 更新订单
+        ErpPurchaseOrderDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseOrderDO.class);
+        calculateTotalPrice(updateObj, purchaseOrderItems);
+        purchaseOrderMapper.updateById(updateObj);
+        // 2.2 更新订单项
+        updatePurchaseOrderItemList(updateReqVO.getId(), purchaseOrderItems);
+    }
+
+    private void calculateTotalPrice(ErpPurchaseOrderDO purchaseOrder, List<ErpPurchaseOrderItemDO> purchaseOrderItems) {
+        purchaseOrder.setTotalCount(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getCount, BigDecimal::add));
+        purchaseOrder.setTotalProductPrice(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
+        purchaseOrder.setTotalTaxPrice(getSumValue(purchaseOrderItems, ErpPurchaseOrderItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
+        purchaseOrder.setTotalPrice(purchaseOrder.getTotalProductPrice().add(purchaseOrder.getTotalTaxPrice()));
+        // 计算优惠价格
+        if (purchaseOrder.getDiscountPercent() == null) {
+            purchaseOrder.setDiscountPercent(BigDecimal.ZERO);
+        }
+        purchaseOrder.setDiscountPrice(MoneyUtils.priceMultiplyPercent(purchaseOrder.getTotalPrice(), purchaseOrder.getDiscountPercent()));
+        purchaseOrder.setTotalPrice(purchaseOrder.getTotalPrice().subtract(purchaseOrder.getDiscountPrice()));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePurchaseOrderStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
+        // 1.2 校验状态
+        if (purchaseOrder.getStatus().equals(status)) {
+            throw exception(approve ? PURCHASE_ORDER_APPROVE_FAIL : PURCHASE_ORDER_PROCESS_FAIL);
+        }
+        // 1.3 存在采购入单,无法反审核
+        if (!approve && purchaseOrder.getInCount().compareTo(BigDecimal.ZERO) > 0) {
+            throw exception(PURCHASE_ORDER_PROCESS_FAIL_EXISTS_IN);
+        }
+        // 1.4 存在采购退货单,无法反审核
+        if (!approve && purchaseOrder.getReturnCount().compareTo(BigDecimal.ZERO) > 0) {
+            throw exception(PURCHASE_ORDER_PROCESS_FAIL_EXISTS_RETURN);
+        }
+
+        // 2. 更新状态
+        int updateCount = purchaseOrderMapper.updateByIdAndStatus(id, purchaseOrder.getStatus(),
+                new ErpPurchaseOrderDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? PURCHASE_ORDER_APPROVE_FAIL : PURCHASE_ORDER_PROCESS_FAIL);
+        }
+    }
+
+    private List<ErpPurchaseOrderItemDO> validatePurchaseOrderItems(List<ErpPurchaseOrderSaveReqVO.Item> list) {
+        // 1. 校验产品存在
+        List<ErpProductDO> productList = productService.validProductList(
+                convertSet(list, ErpPurchaseOrderSaveReqVO.Item::getProductId));
+        Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
+        // 2. 转化为 ErpPurchaseOrderItemDO 列表
+        return convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseOrderItemDO.class, item -> {
+            item.setProductUnitId(productMap.get(item.getProductId()).getUnitId());
+            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
+            if (item.getTotalPrice() == null) {
+                return;
+            }
+            if (item.getTaxPercent() != null) {
+                item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
+            }
+        }));
+    }
+
+    private void updatePurchaseOrderItemList(Long id, List<ErpPurchaseOrderItemDO> newList) {
+        // 第一步,对比新老数据,获得添加、修改、删除的列表
+        List<ErpPurchaseOrderItemDO> oldList = purchaseOrderItemMapper.selectListByOrderId(id);
+        List<List<ErpPurchaseOrderItemDO>> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录
+                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+        // 第二步,批量添加、修改、删除
+        if (CollUtil.isNotEmpty(diffList.get(0))) {
+            diffList.get(0).forEach(o -> o.setOrderId(id));
+            purchaseOrderItemMapper.insertBatch(diffList.get(0));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(1))) {
+            purchaseOrderItemMapper.updateBatch(diffList.get(1));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(2))) {
+            purchaseOrderItemMapper.deleteBatchIds(convertList(diffList.get(2), ErpPurchaseOrderItemDO::getId));
+        }
+    }
+
+    @Override
+    public void updatePurchaseOrderInCount(Long id, Map<Long, BigDecimal> inCountMap) {
+        List<ErpPurchaseOrderItemDO> orderItems = purchaseOrderItemMapper.selectListByOrderId(id);
+        // 1. 更新每个采购订单项
+        orderItems.forEach(item -> {
+            BigDecimal inCount = inCountMap.getOrDefault(item.getId(), BigDecimal.ZERO);
+            if (item.getInCount().equals(inCount)) {
+                return;
+            }
+            if (inCount.compareTo(item.getCount()) > 0) {
+                throw exception(PURCHASE_ORDER_ITEM_IN_FAIL_PRODUCT_EXCEED,
+                        productService.getProduct(item.getProductId()).getName(), item.getCount());
+            }
+            purchaseOrderItemMapper.updateById(new ErpPurchaseOrderItemDO().setId(item.getId()).setInCount(inCount));
+        });
+        // 2. 更新采购订单
+        BigDecimal totalInCount = getSumValue(inCountMap.values(), value -> value, BigDecimal::add, BigDecimal.ZERO);
+        purchaseOrderMapper.updateById(new ErpPurchaseOrderDO().setId(id).setInCount(totalInCount));
+    }
+
+    @Override
+    public void updatePurchaseOrderReturnCount(Long orderId, Map<Long, BigDecimal> returnCountMap) {
+        List<ErpPurchaseOrderItemDO> orderItems = purchaseOrderItemMapper.selectListByOrderId(orderId);
+        // 1. 更新每个采购订单项
+        orderItems.forEach(item -> {
+            BigDecimal returnCount = returnCountMap.getOrDefault(item.getId(), BigDecimal.ZERO);
+            if (item.getReturnCount().equals(returnCount)) {
+                return;
+            }
+            if (returnCount.compareTo(item.getInCount()) > 0) {
+                throw exception(PURCHASE_ORDER_ITEM_RETURN_FAIL_IN_EXCEED,
+                        productService.getProduct(item.getProductId()).getName(), item.getInCount());
+            }
+            purchaseOrderItemMapper.updateById(new ErpPurchaseOrderItemDO().setId(item.getId()).setReturnCount(returnCount));
+        });
+        // 2. 更新采购订单
+        BigDecimal totalReturnCount = getSumValue(returnCountMap.values(), value -> value, BigDecimal::add, BigDecimal.ZERO);
+        purchaseOrderMapper.updateById(new ErpPurchaseOrderDO().setId(orderId).setReturnCount(totalReturnCount));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deletePurchaseOrder(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpPurchaseOrderDO> purchaseOrders = purchaseOrderMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(purchaseOrders)) {
+            return;
+        }
+        purchaseOrders.forEach(purchaseOrder -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(purchaseOrder.getStatus())) {
+                throw exception(PURCHASE_ORDER_DELETE_FAIL_APPROVE, purchaseOrder.getNo());
+            }
+        });
+
+        // 2. 遍历删除,并记录操作日志
+        purchaseOrders.forEach(purchaseOrder -> {
+            // 2.1 删除订单
+            purchaseOrderMapper.deleteById(purchaseOrder.getId());
+            // 2.2 删除订单项
+            purchaseOrderItemMapper.deleteByOrderId(purchaseOrder.getId());
+        });
+    }
+
+    private ErpPurchaseOrderDO validatePurchaseOrderExists(Long id) {
+        ErpPurchaseOrderDO purchaseOrder = purchaseOrderMapper.selectById(id);
+        if (purchaseOrder == null) {
+            throw exception(PURCHASE_ORDER_NOT_EXISTS);
+        }
+        return purchaseOrder;
+    }
+
+    @Override
+    public ErpPurchaseOrderDO getPurchaseOrder(Long id) {
+        return purchaseOrderMapper.selectById(id);
+    }
+
+    @Override
+    public ErpPurchaseOrderDO validatePurchaseOrder(Long id) {
+        ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
+        if (ObjectUtil.notEqual(purchaseOrder.getStatus(), ErpAuditStatus.APPROVE.getStatus())) {
+            throw exception(PURCHASE_ORDER_NOT_APPROVE);
+        }
+        return purchaseOrder;
+    }
+
+    @Override
+    public PageResult<ErpPurchaseOrderDO> getPurchaseOrderPage(ErpPurchaseOrderPageReqVO pageReqVO) {
+        return purchaseOrderMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 订单项 ====================
+
+    @Override
+    public List<ErpPurchaseOrderItemDO> getPurchaseOrderItemListByOrderId(Long orderId) {
+        return purchaseOrderItemMapper.selectListByOrderId(orderId);
+    }
+
+    @Override
+    public List<ErpPurchaseOrderItemDO> getPurchaseOrderItemListByOrderIds(Collection<Long> orderIds) {
+        if (CollUtil.isEmpty(orderIds)) {
+            return Collections.emptyList();
+        }
+        return purchaseOrderItemMapper.selectListByOrderIds(orderIds);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseReturnService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseReturnService.java
new file mode 100644
index 000000000..b6826fa88
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseReturnService.java
@@ -0,0 +1,101 @@
+package cn.iocoder.yudao.module.erp.service.purchase;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns.ErpPurchaseReturnPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns.ErpPurchaseReturnSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnItemDO;
+import jakarta.validation.Valid;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 采购退货 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpPurchaseReturnService {
+
+    /**
+     * 创建采购退货
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createPurchaseReturn(@Valid ErpPurchaseReturnSaveReqVO createReqVO);
+
+    /**
+     * 更新采购退货
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updatePurchaseReturn(@Valid ErpPurchaseReturnSaveReqVO updateReqVO);
+
+    /**
+     * 更新采购退货的状态
+     *
+     * @param id 编号
+     * @param status 状态
+     */
+    void updatePurchaseReturnStatus(Long id, Integer status);
+
+    /**
+     * 更新采购退货的退款金额
+     *
+     * @param id 编号
+     * @param refundPrice 退款金额
+     */
+    void updatePurchaseReturnRefundPrice(Long id, BigDecimal refundPrice);
+
+    /**
+     * 删除采购退货
+     *
+     * @param ids 编号数组
+     */
+    void deletePurchaseReturn(List<Long> ids);
+
+    /**
+     * 获得采购退货
+     *
+     * @param id 编号
+     * @return 采购退货
+     */
+    ErpPurchaseReturnDO getPurchaseReturn(Long id);
+
+    /**
+     * 校验采购退货,已经审核通过
+     *
+     * @param id 编号
+     * @return 采购退货
+     */
+    ErpPurchaseReturnDO validatePurchaseReturn(Long id);
+
+    /**
+     * 获得采购退货分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 采购退货分页
+     */
+    PageResult<ErpPurchaseReturnDO> getPurchaseReturnPage(ErpPurchaseReturnPageReqVO pageReqVO);
+
+    // ==================== 采购退货项 ====================
+
+    /**
+     * 获得采购退货项列表
+     *
+     * @param returnId 采购退货编号
+     * @return 采购退货项列表
+     */
+    List<ErpPurchaseReturnItemDO> getPurchaseReturnItemListByReturnId(Long returnId);
+
+    /**
+     * 获得采购退货项 List
+     *
+     * @param returnIds 采购退货编号数组
+     * @return 采购退货项 List
+     */
+    List<ErpPurchaseReturnItemDO> getPurchaseReturnItemListByReturnIds(Collection<Long> returnIds);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseReturnServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseReturnServiceImpl.java
new file mode 100644
index 000000000..109916249
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseReturnServiceImpl.java
@@ -0,0 +1,304 @@
+package cn.iocoder.yudao.module.erp.service.purchase;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns.ErpPurchaseReturnPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns.ErpPurchaseReturnSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseReturnItemDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseReturnItemMapper;
+import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseReturnMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum;
+import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockRecordService;
+import cn.iocoder.yudao.module.erp.service.stock.bo.ErpStockRecordCreateReqBO;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+// TODO 芋艿:记录操作日志
+
+/**
+ * ERP 采购退货 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpPurchaseReturnServiceImpl implements ErpPurchaseReturnService {
+
+    @Resource
+    private ErpPurchaseReturnMapper purchaseReturnMapper;
+    @Resource
+    private ErpPurchaseReturnItemMapper purchaseReturnItemMapper;
+
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private ErpPurchaseOrderService purchaseOrderService;
+    @Resource
+    private ErpAccountService accountService;
+    @Resource
+    private ErpStockRecordService stockRecordService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createPurchaseReturn(ErpPurchaseReturnSaveReqVO createReqVO) {
+        // 1.1 校验采购订单已审核
+        ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.validatePurchaseOrder(createReqVO.getOrderId());
+        // 1.2 校验退货项的有效性
+        List<ErpPurchaseReturnItemDO> purchaseReturnItems = validatePurchaseReturnItems(createReqVO.getItems());
+        // 1.3 校验结算账户
+        accountService.validateAccount(createReqVO.getAccountId());
+        // 1.4 生成退货单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_RETURN_NO_PREFIX);
+        if (purchaseReturnMapper.selectByNo(no) != null) {
+            throw exception(PURCHASE_RETURN_NO_EXISTS);
+        }
+
+        // 2.1 插入退货
+        ErpPurchaseReturnDO purchaseReturn = BeanUtils.toBean(createReqVO, ErpPurchaseReturnDO.class, in -> in
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()))
+                .setOrderNo(purchaseOrder.getNo()).setSupplierId(purchaseOrder.getSupplierId());
+        calculateTotalPrice(purchaseReturn, purchaseReturnItems);
+        purchaseReturnMapper.insert(purchaseReturn);
+        // 2.2 插入退货项
+        purchaseReturnItems.forEach(o -> o.setReturnId(purchaseReturn.getId()));
+        purchaseReturnItemMapper.insertBatch(purchaseReturnItems);
+
+        // 3. 更新采购订单的退货数量
+        updatePurchaseOrderReturnCount(createReqVO.getOrderId());
+        return purchaseReturn.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePurchaseReturn(ErpPurchaseReturnSaveReqVO updateReqVO) {
+        // 1.1 校验存在
+        ErpPurchaseReturnDO purchaseReturn = validatePurchaseReturnExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(purchaseReturn.getStatus())) {
+            throw exception(PURCHASE_RETURN_UPDATE_FAIL_APPROVE, purchaseReturn.getNo());
+        }
+        // 1.2 校验采购订单已审核
+        ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.validatePurchaseOrder(updateReqVO.getOrderId());
+        // 1.3 校验结算账户
+        accountService.validateAccount(updateReqVO.getAccountId());
+        // 1.4 校验订单项的有效性
+        List<ErpPurchaseReturnItemDO> purchaseReturnItems = validatePurchaseReturnItems(updateReqVO.getItems());
+
+        // 2.1 更新退货
+        ErpPurchaseReturnDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseReturnDO.class)
+                .setOrderNo(purchaseOrder.getNo()).setSupplierId(purchaseOrder.getSupplierId());
+        calculateTotalPrice(updateObj, purchaseReturnItems);
+        purchaseReturnMapper.updateById(updateObj);
+        // 2.2 更新退货项
+        updatePurchaseReturnItemList(updateReqVO.getId(), purchaseReturnItems);
+
+        // 3.1 更新采购订单的出库数量
+        updatePurchaseOrderReturnCount(updateObj.getOrderId());
+        // 3.2 注意:如果采购订单编号变更了,需要更新“老”采购订单的出库数量
+        if (ObjectUtil.notEqual(purchaseReturn.getOrderId(), updateObj.getOrderId())) {
+            updatePurchaseOrderReturnCount(purchaseReturn.getOrderId());
+        }
+    }
+
+    private void calculateTotalPrice(ErpPurchaseReturnDO purchaseReturn, List<ErpPurchaseReturnItemDO> purchaseReturnItems) {
+        purchaseReturn.setTotalCount(getSumValue(purchaseReturnItems, ErpPurchaseReturnItemDO::getCount, BigDecimal::add));
+        purchaseReturn.setTotalProductPrice(getSumValue(purchaseReturnItems, ErpPurchaseReturnItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
+        purchaseReturn.setTotalTaxPrice(getSumValue(purchaseReturnItems, ErpPurchaseReturnItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
+        purchaseReturn.setTotalPrice(purchaseReturn.getTotalProductPrice().add(purchaseReturn.getTotalTaxPrice()));
+        // 计算优惠价格
+        if (purchaseReturn.getDiscountPercent() == null) {
+            purchaseReturn.setDiscountPercent(BigDecimal.ZERO);
+        }
+        purchaseReturn.setDiscountPrice(MoneyUtils.priceMultiplyPercent(purchaseReturn.getTotalPrice(), purchaseReturn.getDiscountPercent()));
+        purchaseReturn.setTotalPrice(purchaseReturn.getTotalPrice().subtract(purchaseReturn.getDiscountPrice().add(purchaseReturn.getOtherPrice())));
+    }
+
+    private void updatePurchaseOrderReturnCount(Long orderId) {
+        // 1.1 查询采购订单对应的采购出库单列表
+        List<ErpPurchaseReturnDO> purchaseReturns = purchaseReturnMapper.selectListByOrderId(orderId);
+        // 1.2 查询对应的采购订单项的退货数量
+        Map<Long, BigDecimal> returnCountMap = purchaseReturnItemMapper.selectOrderItemCountSumMapByReturnIds(
+                convertList(purchaseReturns, ErpPurchaseReturnDO::getId));
+        // 2. 更新采购订单的出库数量
+        purchaseOrderService.updatePurchaseOrderReturnCount(orderId, returnCountMap);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updatePurchaseReturnStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpPurchaseReturnDO purchaseReturn = validatePurchaseReturnExists(id);
+        // 1.2 校验状态
+        if (purchaseReturn.getStatus().equals(status)) {
+            throw exception(approve ? PURCHASE_RETURN_APPROVE_FAIL : PURCHASE_RETURN_PROCESS_FAIL);
+        }
+        // 1.3 校验已退款
+        if (approve && purchaseReturn.getRefundPrice().compareTo(BigDecimal.ZERO) > 0) {
+            throw exception(PURCHASE_RETURN_PROCESS_FAIL_EXISTS_REFUND);
+        }
+
+        // 2. 更新状态
+        int updateCount = purchaseReturnMapper.updateByIdAndStatus(id, purchaseReturn.getStatus(),
+                new ErpPurchaseReturnDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? PURCHASE_RETURN_APPROVE_FAIL : PURCHASE_RETURN_PROCESS_FAIL);
+        }
+
+        // 3. 变更库存
+        List<ErpPurchaseReturnItemDO> purchaseReturnItems = purchaseReturnItemMapper.selectListByReturnId(id);
+        Integer bizType = approve ? ErpStockRecordBizTypeEnum.PURCHASE_RETURN.getType()
+                : ErpStockRecordBizTypeEnum.PURCHASE_RETURN_CANCEL.getType();
+        purchaseReturnItems.forEach(purchaseReturnItem -> {
+            BigDecimal count = approve ? purchaseReturnItem.getCount().negate() : purchaseReturnItem.getCount();
+            stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(
+                    purchaseReturnItem.getProductId(), purchaseReturnItem.getWarehouseId(), count,
+                    bizType, purchaseReturnItem.getReturnId(), purchaseReturnItem.getId(), purchaseReturn.getNo()));
+        });
+    }
+
+    @Override
+    public void updatePurchaseReturnRefundPrice(Long id, BigDecimal refundPrice) {
+        ErpPurchaseReturnDO purchaseReturn = purchaseReturnMapper.selectById(id);
+        if (purchaseReturn.getRefundPrice().equals(refundPrice)) {
+            return;
+        }
+        if (refundPrice.compareTo(purchaseReturn.getTotalPrice()) > 0) {
+            throw exception(PURCHASE_RETURN_FAIL_REFUND_PRICE_EXCEED, purchaseReturn.getTotalPrice(), refundPrice);
+        }
+        purchaseReturnMapper.updateById(new ErpPurchaseReturnDO().setId(id).setRefundPrice(refundPrice));
+    }
+
+    private List<ErpPurchaseReturnItemDO> validatePurchaseReturnItems(List<ErpPurchaseReturnSaveReqVO.Item> list) {
+        // 1. 校验产品存在
+        List<ErpProductDO> productList = productService.validProductList(
+                convertSet(list, ErpPurchaseReturnSaveReqVO.Item::getProductId));
+        Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
+        // 2. 转化为 ErpPurchaseReturnItemDO 列表
+        return convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseReturnItemDO.class, item -> {
+            item.setProductUnitId(productMap.get(item.getProductId()).getUnitId());
+            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
+            if (item.getTotalPrice() == null) {
+                return;
+            }
+            if (item.getTaxPercent() != null) {
+                item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
+            }
+        }));
+    }
+
+    private void updatePurchaseReturnItemList(Long id, List<ErpPurchaseReturnItemDO> newList) {
+        // 第一步,对比新老数据,获得添加、修改、删除的列表
+        List<ErpPurchaseReturnItemDO> oldList = purchaseReturnItemMapper.selectListByReturnId(id);
+        List<List<ErpPurchaseReturnItemDO>> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录
+                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+        // 第二步,批量添加、修改、删除
+        if (CollUtil.isNotEmpty(diffList.get(0))) {
+            diffList.get(0).forEach(o -> o.setReturnId(id));
+            purchaseReturnItemMapper.insertBatch(diffList.get(0));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(1))) {
+            purchaseReturnItemMapper.updateBatch(diffList.get(1));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(2))) {
+            purchaseReturnItemMapper.deleteBatchIds(convertList(diffList.get(2), ErpPurchaseReturnItemDO::getId));
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deletePurchaseReturn(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpPurchaseReturnDO> purchaseReturns = purchaseReturnMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(purchaseReturns)) {
+            return;
+        }
+        purchaseReturns.forEach(purchaseReturn -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(purchaseReturn.getStatus())) {
+                throw exception(PURCHASE_RETURN_DELETE_FAIL_APPROVE, purchaseReturn.getNo());
+            }
+        });
+
+        // 2. 遍历删除,并记录操作日志
+        purchaseReturns.forEach(purchaseReturn -> {
+            // 2.1 删除订单
+            purchaseReturnMapper.deleteById(purchaseReturn.getId());
+            // 2.2 删除订单项
+            purchaseReturnItemMapper.deleteByReturnId(purchaseReturn.getId());
+
+            // 2.3 更新采购订单的出库数量
+            updatePurchaseOrderReturnCount(purchaseReturn.getOrderId());
+        });
+
+    }
+
+    private ErpPurchaseReturnDO validatePurchaseReturnExists(Long id) {
+        ErpPurchaseReturnDO purchaseReturn = purchaseReturnMapper.selectById(id);
+        if (purchaseReturn == null) {
+            throw exception(PURCHASE_RETURN_NOT_EXISTS);
+        }
+        return purchaseReturn;
+    }
+
+    @Override
+    public ErpPurchaseReturnDO getPurchaseReturn(Long id) {
+        return purchaseReturnMapper.selectById(id);
+    }
+
+    @Override
+    public ErpPurchaseReturnDO validatePurchaseReturn(Long id) {
+        ErpPurchaseReturnDO purchaseReturn = getPurchaseReturn(id);
+        if (ObjectUtil.notEqual(purchaseReturn.getStatus(), ErpAuditStatus.APPROVE.getStatus())) {
+            throw exception(PURCHASE_RETURN_NOT_APPROVE);
+        }
+        return purchaseReturn;
+    }
+
+    @Override
+    public PageResult<ErpPurchaseReturnDO> getPurchaseReturnPage(ErpPurchaseReturnPageReqVO pageReqVO) {
+        return purchaseReturnMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 采购退货项 ====================
+
+    @Override
+    public List<ErpPurchaseReturnItemDO> getPurchaseReturnItemListByReturnId(Long returnId) {
+        return purchaseReturnItemMapper.selectListByReturnId(returnId);
+    }
+
+    @Override
+    public List<ErpPurchaseReturnItemDO> getPurchaseReturnItemListByReturnIds(Collection<Long> returnIds) {
+        if (CollUtil.isEmpty(returnIds)) {
+            return Collections.emptyList();
+        }
+        return purchaseReturnItemMapper.selectListByReturnIds(returnIds);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierService.java
index a2f6547c2..495474d13 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierService.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierService.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.erp.service.purchase;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier.ErpSupplierPageReqVO;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier.ErpSupplierSaveReqVO;
-import cn.iocoder.yudao.module.erp.dal.dataobject.supplier.ErpSupplierDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
 import jakarta.validation.Valid;
 
 import java.util.Collection;
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierServiceImpl.java
index 09c789c57..1c90ff3f6 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierServiceImpl.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpSupplierServiceImpl.java
@@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier.ErpSupplierPageReqVO;
 import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier.ErpSupplierSaveReqVO;
-import cn.iocoder.yudao.module.erp.dal.dataobject.supplier.ErpSupplierDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
 import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpSupplierMapper;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
@@ -68,10 +68,10 @@ public class ErpSupplierServiceImpl implements ErpSupplierService {
     public ErpSupplierDO validateSupplier(Long id) {
         ErpSupplierDO supplier = supplierMapper.selectById(id);
         if (supplier == null) {
-            throw exception(WAREHOUSE_NOT_EXISTS);
+            throw exception(SUPPLIER_NOT_EXISTS);
         }
         if (CommonStatusEnum.isDisable(supplier.getStatus())) {
-            throw exception(WAREHOUSE_NOT_ENABLE, supplier.getName());
+            throw exception(SUPPLIER_NOT_ENABLE, supplier.getName());
         }
         return supplier;
     }
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerService.java
new file mode 100644
index 000000000..d047300e8
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerService.java
@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.erp.service.sale;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer.ErpCustomerPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer.ErpCustomerSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+
+/**
+ * ERP 客户 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpCustomerService {
+
+    /**
+     * 创建客户
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createCustomer(@Valid ErpCustomerSaveReqVO createReqVO);
+
+    /**
+     * 更新客户
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateCustomer(@Valid ErpCustomerSaveReqVO updateReqVO);
+
+    /**
+     * 删除客户
+     *
+     * @param id 编号
+     */
+    void deleteCustomer(Long id);
+
+    /**
+     * 获得客户
+     *
+     * @param id 编号
+     * @return 客户
+     */
+    ErpCustomerDO getCustomer(Long id);
+
+    /**
+     * 校验客户
+     *
+     * @param id 编号
+     * @return 客户
+     */
+    ErpCustomerDO validateCustomer(Long id);
+
+    /**
+     * 获得客户列表
+     *
+     * @param ids 编号列表
+     * @return 客户列表
+     */
+    List<ErpCustomerDO> getCustomerList(Collection<Long> ids);
+
+    /**
+     * 获得客户 Map
+     *
+     * @param ids 编号列表
+     * @return 客户 Map
+     */
+    default Map<Long, ErpCustomerDO> getCustomerMap(Collection<Long> ids) {
+        return convertMap(getCustomerList(ids), ErpCustomerDO::getId);
+    }
+
+    /**
+     * 获得客户分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 客户分页
+     */
+    PageResult<ErpCustomerDO> getCustomerPage(ErpCustomerPageReqVO pageReqVO);
+
+    /**
+     * 获得指定状态的客户列表
+     *
+     * @param status 状态
+     * @return 客户列表
+     */
+    List<ErpCustomerDO> getCustomerListByStatus(Integer status);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerServiceImpl.java
new file mode 100644
index 000000000..73d2aa9d5
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpCustomerServiceImpl.java
@@ -0,0 +1,97 @@
+package cn.iocoder.yudao.module.erp.service.sale;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer.ErpCustomerPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer.ErpCustomerSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpCustomerMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.Collection;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.CUSTOMER_NOT_ENABLE;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.CUSTOMER_NOT_EXISTS;
+
+/**
+ * ERP 客户 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpCustomerServiceImpl implements ErpCustomerService {
+
+    @Resource
+    private ErpCustomerMapper customerMapper;
+
+    @Override
+    public Long createCustomer(ErpCustomerSaveReqVO createReqVO) {
+        // 插入
+        ErpCustomerDO customer = BeanUtils.toBean(createReqVO, ErpCustomerDO.class);
+        customerMapper.insert(customer);
+        // 返回
+        return customer.getId();
+    }
+
+    @Override
+    public void updateCustomer(ErpCustomerSaveReqVO updateReqVO) {
+        // 校验存在
+        validateCustomerExists(updateReqVO.getId());
+        // 更新
+        ErpCustomerDO updateObj = BeanUtils.toBean(updateReqVO, ErpCustomerDO.class);
+        customerMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteCustomer(Long id) {
+        // 校验存在
+        validateCustomerExists(id);
+        // 删除
+        customerMapper.deleteById(id);
+    }
+
+    private void validateCustomerExists(Long id) {
+        if (customerMapper.selectById(id) == null) {
+            throw exception(CUSTOMER_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public ErpCustomerDO getCustomer(Long id) {
+        return customerMapper.selectById(id);
+    }
+
+    @Override
+    public ErpCustomerDO validateCustomer(Long id) {
+        ErpCustomerDO customer = customerMapper.selectById(id);
+        if (customer == null) {
+            throw exception(CUSTOMER_NOT_EXISTS);
+        }
+        if (CommonStatusEnum.isDisable(customer.getStatus())) {
+            throw exception(CUSTOMER_NOT_ENABLE, customer.getName());
+        }
+        return customer;
+    }
+
+    @Override
+    public List<ErpCustomerDO> getCustomerList(Collection<Long> ids) {
+        return customerMapper.selectBatchIds(ids);
+    }
+
+    @Override
+    public PageResult<ErpCustomerDO> getCustomerPage(ErpCustomerPageReqVO pageReqVO) {
+        return customerMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<ErpCustomerDO> getCustomerListByStatus(Integer status) {
+        return customerMapper.selectListByStatus(status);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderService.java
index 6554f2c18..c75f201f3 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderService.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderService.java
@@ -4,8 +4,14 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
 import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderSaveReqVO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
 import jakarta.validation.Valid;
 
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
 /**
  * ERP 销售订单 Service 接口
  *
@@ -14,7 +20,7 @@ import jakarta.validation.Valid;
 public interface ErpSaleOrderService {
 
     /**
-     * 创建ERP 销售订单
+     * 创建销售订单
      *
      * @param createReqVO 创建信息
      * @return 编号
@@ -22,33 +28,83 @@ public interface ErpSaleOrderService {
     Long createSaleOrder(@Valid ErpSaleOrderSaveReqVO createReqVO);
 
     /**
-     * 更新ERP 销售订单
+     * 更新销售订单
      *
      * @param updateReqVO 更新信息
      */
     void updateSaleOrder(@Valid ErpSaleOrderSaveReqVO updateReqVO);
 
     /**
-     * 删除ERP 销售订单
+     * 更新销售订单的状态
      *
      * @param id 编号
+     * @param status 状态
      */
-    void deleteSaleOrder(Long id);
+    void updateSaleOrderStatus(Long id, Integer status);
 
     /**
-     * 获得ERP 销售订单
+     * 更新销售订单的出库数量
      *
      * @param id 编号
-     * @return ERP 销售订单
+     * @param outCountMap 出库数量 Map:key 销售订单项编号;value 出库数量
+     */
+    void updateSaleOrderOutCount(Long id, Map<Long, BigDecimal> outCountMap);
+
+    /**
+     * 更新销售订单的退货数量
+     *
+     * @param orderId 编号
+     * @param returnCountMap 退货数量 Map:key 销售订单项编号;value 退货数量
+     */
+    void updateSaleOrderReturnCount(Long orderId, Map<Long, BigDecimal> returnCountMap);
+
+    /**
+     * 删除销售订单
+     *
+     * @param ids 编号数组
+     */
+    void deleteSaleOrder(List<Long> ids);
+
+    /**
+     * 获得销售订单
+     *
+     * @param id 编号
+     * @return 销售订单
      */
     ErpSaleOrderDO getSaleOrder(Long id);
 
     /**
-     * 获得ERP 销售订单分页
+     * 校验销售订单,已经审核通过
+     *
+     * @param id 编号
+     * @return 销售订单
+     */
+    ErpSaleOrderDO validateSaleOrder(Long id);
+
+    /**
+     * 获得销售订单分页
      *
      * @param pageReqVO 分页查询
-     * @return ERP 销售订单分页
+     * @return 销售订单分页
      */
     PageResult<ErpSaleOrderDO> getSaleOrderPage(ErpSaleOrderPageReqVO pageReqVO);
 
+    // ==================== 销售订单项 ====================
+
+    /**
+     * 获得销售订单项列表
+     *
+     * @param orderId 销售订单编号
+     * @return 销售订单项列表
+     */
+    List<ErpSaleOrderItemDO> getSaleOrderItemListByOrderId(Long orderId);
+
+    /**
+     * 获得销售订单项 List
+     *
+     * @param orderIds 销售订单编号数组
+     * @return 销售订单项 List
+     */
+    List<ErpSaleOrderItemDO> getSaleOrderItemListByOrderIds(Collection<Long> orderIds);
+
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
index 429633f23..592fe5fe7 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -1,23 +1,39 @@
 package cn.iocoder.yudao.module.erp.service.sale;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
 import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
-import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSalesOrderItemDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderItemMapper;
 import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderMapper;
-import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSalesOrderItemMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
 import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
 
+// TODO 芋艿:记录操作日志
+
 /**
  * ERP 销售订单 Service 实现类
  *
@@ -30,61 +46,228 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
     @Resource
     private ErpSaleOrderMapper saleOrderMapper;
     @Resource
-    private ErpSalesOrderItemMapper salesOrderItemMapper;
+    private ErpSaleOrderItemMapper saleOrderItemMapper;
+
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpCustomerService customerService;
+    @Resource
+    private ErpAccountService accountService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createSaleOrder(ErpSaleOrderSaveReqVO createReqVO) {
-        // 插入
-        ErpSaleOrderDO saleOrder = BeanUtils.toBean(createReqVO, ErpSaleOrderDO.class);
+        // 1.1 校验订单项的有效性
+        List<ErpSaleOrderItemDO> saleOrderItems = validateSaleOrderItems(createReqVO.getItems());
+        // 1.2 校验客户
+        customerService.validateCustomer(createReqVO.getCustomerId());
+        // 1.3 校验结算账户
+        if (createReqVO.getAccountId() != null) {
+            accountService.validateAccount(createReqVO.getAccountId());
+        }
+        // 1.4 校验销售人员
+        if (createReqVO.getSaleUserId() != null) {
+            adminUserApi.validateUser(createReqVO.getSaleUserId());
+        }
+        // 1.5 生成订单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.SALE_ORDER_NO_PREFIX);
+        if (saleOrderMapper.selectByNo(no) != null) {
+            throw exception(SALE_ORDER_NO_EXISTS);
+        }
+
+        // 2.1 插入订单
+        ErpSaleOrderDO saleOrder = BeanUtils.toBean(createReqVO, ErpSaleOrderDO.class, in -> in
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()));
+        calculateTotalPrice(saleOrder, saleOrderItems);
         saleOrderMapper.insert(saleOrder);
-
-        // 插入子表
-//        createSalesOrderItemsList(saleOrder.getId(), createReqVO.getSalesOrderItems());
-        // 返回
+        // 2.2 插入订单项
+        saleOrderItems.forEach(o -> o.setOrderId(saleOrder.getId()));
+        saleOrderItemMapper.insertBatch(saleOrderItems);
         return saleOrder.getId();
     }
 
-    private void createSalesOrderItemsList(Long id, List<ErpSalesOrderItemDO> list) {
-        list.forEach(o -> o.setId(id));
-        salesOrderItemMapper.insertBatch(list);
-    }
-
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void updateSaleOrder(ErpSaleOrderSaveReqVO updateReqVO) {
-        // 校验存在
-        validateSaleOrderExists(updateReqVO.getId());
-        // 更新
-        ErpSaleOrderDO updateObj = BeanUtils.toBean(updateReqVO, ErpSaleOrderDO.class);
-        saleOrderMapper.updateById(updateObj);
+        // 1.1 校验存在
+        ErpSaleOrderDO saleOrder = validateSaleOrderExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(saleOrder.getStatus())) {
+            throw exception(SALE_ORDER_UPDATE_FAIL_APPROVE, saleOrder.getNo());
+        }
+        // 1.2 校验客户
+        customerService.validateCustomer(updateReqVO.getCustomerId());
+        // 1.3 校验结算账户
+        if (updateReqVO.getAccountId() != null) {
+            accountService.validateAccount(updateReqVO.getAccountId());
+        }
+        // 1.4 校验销售人员
+        if (updateReqVO.getSaleUserId() != null) {
+            adminUserApi.validateUser(updateReqVO.getSaleUserId());
+        }
+        // 1.5 校验订单项的有效性
+        List<ErpSaleOrderItemDO> saleOrderItems = validateSaleOrderItems(updateReqVO.getItems());
 
-        // 更新子表
-//        updateSalesOrderItemsList(updateReqVO.getId(), updateReqVO.getSalesOrderItems());
+        // 2.1 更新订单
+        ErpSaleOrderDO updateObj = BeanUtils.toBean(updateReqVO, ErpSaleOrderDO.class);
+        calculateTotalPrice(updateObj, saleOrderItems);
+        saleOrderMapper.updateById(updateObj);
+        // 2.2 更新订单项
+        updateSaleOrderItemList(updateReqVO.getId(), saleOrderItems);
     }
 
-    private void updateSalesOrderItemsList(Long id, List<ErpSalesOrderItemDO> list) {
-        deleteSalesOrderItemsById(id);
-        list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新
-        createSalesOrderItemsList(id, list);
+    private void calculateTotalPrice(ErpSaleOrderDO saleOrder, List<ErpSaleOrderItemDO> saleOrderItems) {
+        saleOrder.setTotalCount(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getCount, BigDecimal::add));
+        saleOrder.setTotalProductPrice(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
+        saleOrder.setTotalTaxPrice(getSumValue(saleOrderItems, ErpSaleOrderItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
+        saleOrder.setTotalPrice(saleOrder.getTotalProductPrice().add(saleOrder.getTotalTaxPrice()));
+        // 计算优惠价格
+        if (saleOrder.getDiscountPercent() == null) {
+            saleOrder.setDiscountPercent(BigDecimal.ZERO);
+        }
+        saleOrder.setDiscountPrice(MoneyUtils.priceMultiplyPercent(saleOrder.getTotalPrice(), saleOrder.getDiscountPercent()));
+        saleOrder.setTotalPrice(saleOrder.getTotalPrice().subtract(saleOrder.getDiscountPrice()));
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void deleteSaleOrder(Long id) {
-        // 校验存在
-        validateSaleOrderExists(id);
-        // 删除
-        saleOrderMapper.deleteById(id);
+    public void updateSaleOrderStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpSaleOrderDO saleOrder = validateSaleOrderExists(id);
+        // 1.2 校验状态
+        if (saleOrder.getStatus().equals(status)) {
+            throw exception(approve ? SALE_ORDER_APPROVE_FAIL : SALE_ORDER_PROCESS_FAIL);
+        }
+        // 1.3 存在销售出库单,无法反审核
+        if (!approve && saleOrder.getOutCount().compareTo(BigDecimal.ZERO) > 0) {
+            throw exception(SALE_ORDER_PROCESS_FAIL_EXISTS_OUT);
+        }
+        // 1.4 存在销售退货单,无法反审核
+        if (!approve && saleOrder.getReturnCount().compareTo(BigDecimal.ZERO) > 0) {
+            throw exception(SALE_ORDER_PROCESS_FAIL_EXISTS_RETURN);
+        }
 
-        // 删除子表
-        deleteSalesOrderItemsById(id);
+        // 2. 更新状态
+        int updateCount = saleOrderMapper.updateByIdAndStatus(id, saleOrder.getStatus(),
+                new ErpSaleOrderDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? SALE_ORDER_APPROVE_FAIL : SALE_ORDER_PROCESS_FAIL);
+        }
     }
 
-    private void validateSaleOrderExists(Long id) {
-        if (saleOrderMapper.selectById(id) == null) {
+    private List<ErpSaleOrderItemDO> validateSaleOrderItems(List<ErpSaleOrderSaveReqVO.Item> list) {
+        // 1. 校验产品存在
+        List<ErpProductDO> productList = productService.validProductList(
+                convertSet(list, ErpSaleOrderSaveReqVO.Item::getProductId));
+        Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
+        // 2. 转化为 ErpSaleOrderItemDO 列表
+        return convertList(list, o -> BeanUtils.toBean(o, ErpSaleOrderItemDO.class, item -> {
+            item.setProductUnitId(productMap.get(item.getProductId()).getUnitId());
+            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
+            if (item.getTotalPrice() == null) {
+                return;
+            }
+            if (item.getTaxPercent() != null) {
+                item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
+            }
+        }));
+    }
+
+    private void updateSaleOrderItemList(Long id, List<ErpSaleOrderItemDO> newList) {
+        // 第一步,对比新老数据,获得添加、修改、删除的列表
+        List<ErpSaleOrderItemDO> oldList = saleOrderItemMapper.selectListByOrderId(id);
+        List<List<ErpSaleOrderItemDO>> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录
+                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+        // 第二步,批量添加、修改、删除
+        if (CollUtil.isNotEmpty(diffList.get(0))) {
+            diffList.get(0).forEach(o -> o.setOrderId(id));
+            saleOrderItemMapper.insertBatch(diffList.get(0));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(1))) {
+            saleOrderItemMapper.updateBatch(diffList.get(1));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(2))) {
+            saleOrderItemMapper.deleteBatchIds(convertList(diffList.get(2), ErpSaleOrderItemDO::getId));
+        }
+    }
+
+    @Override
+    public void updateSaleOrderOutCount(Long id, Map<Long, BigDecimal> outCountMap) {
+        List<ErpSaleOrderItemDO> orderItems = saleOrderItemMapper.selectListByOrderId(id);
+        // 1. 更新每个销售订单项
+        orderItems.forEach(item -> {
+            BigDecimal outCount = outCountMap.getOrDefault(item.getId(), BigDecimal.ZERO);
+            if (item.getOutCount().equals(outCount)) {
+                return;
+            }
+            if (outCount.compareTo(item.getCount()) > 0) {
+                throw exception(SALE_ORDER_ITEM_OUT_FAIL_PRODUCT_EXCEED,
+                        productService.getProduct(item.getProductId()).getName(), item.getCount());
+            }
+            saleOrderItemMapper.updateById(new ErpSaleOrderItemDO().setId(item.getId()).setOutCount(outCount));
+        });
+        // 2. 更新销售订单
+        BigDecimal totalOutCount = getSumValue(outCountMap.values(), value -> value, BigDecimal::add, BigDecimal.ZERO);
+        saleOrderMapper.updateById(new ErpSaleOrderDO().setId(id).setOutCount(totalOutCount));
+    }
+
+    @Override
+    public void updateSaleOrderReturnCount(Long orderId, Map<Long, BigDecimal> returnCountMap) {
+        List<ErpSaleOrderItemDO> orderItems = saleOrderItemMapper.selectListByOrderId(orderId);
+        // 1. 更新每个销售订单项
+        orderItems.forEach(item -> {
+            BigDecimal returnCount = returnCountMap.getOrDefault(item.getId(), BigDecimal.ZERO);
+            if (item.getReturnCount().equals(returnCount)) {
+                return;
+            }
+            if (returnCount.compareTo(item.getOutCount()) > 0) {
+                throw exception(SALE_ORDER_ITEM_RETURN_FAIL_OUT_EXCEED,
+                        productService.getProduct(item.getProductId()).getName(), item.getOutCount());
+            }
+            saleOrderItemMapper.updateById(new ErpSaleOrderItemDO().setId(item.getId()).setReturnCount(returnCount));
+        });
+        // 2. 更新销售订单
+        BigDecimal totalReturnCount = getSumValue(returnCountMap.values(), value -> value, BigDecimal::add, BigDecimal.ZERO);
+        saleOrderMapper.updateById(new ErpSaleOrderDO().setId(orderId).setReturnCount(totalReturnCount));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteSaleOrder(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpSaleOrderDO> saleOrders = saleOrderMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(saleOrders)) {
+            return;
+        }
+        saleOrders.forEach(saleOrder -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(saleOrder.getStatus())) {
+                throw exception(SALE_ORDER_DELETE_FAIL_APPROVE, saleOrder.getNo());
+            }
+        });
+
+        // 2. 遍历删除,并记录操作日志
+        saleOrders.forEach(saleOrder -> {
+            // 2.1 删除订单
+            saleOrderMapper.deleteById(saleOrder.getId());
+            // 2.2 删除订单项
+            saleOrderItemMapper.deleteByOrderId(saleOrder.getId());
+        });
+    }
+
+    private ErpSaleOrderDO validateSaleOrderExists(Long id) {
+        ErpSaleOrderDO saleOrder = saleOrderMapper.selectById(id);
+        if (saleOrder == null) {
             throw exception(SALE_ORDER_NOT_EXISTS);
         }
+        return saleOrder;
     }
 
     @Override
@@ -92,15 +275,33 @@ public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
         return saleOrderMapper.selectById(id);
     }
 
+    @Override
+    public ErpSaleOrderDO validateSaleOrder(Long id) {
+        ErpSaleOrderDO saleOrder = validateSaleOrderExists(id);
+        if (ObjectUtil.notEqual(saleOrder.getStatus(), ErpAuditStatus.APPROVE.getStatus())) {
+            throw exception(SALE_ORDER_NOT_APPROVE);
+        }
+        return saleOrder;
+    }
+
     @Override
     public PageResult<ErpSaleOrderDO> getSaleOrderPage(ErpSaleOrderPageReqVO pageReqVO) {
         return saleOrderMapper.selectPage(pageReqVO);
     }
 
-    // ==================== 子表(ERP 销售订单明细) ====================
+    // ==================== 订单项 ====================
 
-    private void deleteSalesOrderItemsById(Long id) {
-        salesOrderItemMapper.deleteById(id);
+    @Override
+    public List<ErpSaleOrderItemDO> getSaleOrderItemListByOrderId(Long orderId) {
+        return saleOrderItemMapper.selectListByOrderId(orderId);
+    }
+
+    @Override
+    public List<ErpSaleOrderItemDO> getSaleOrderItemListByOrderIds(Collection<Long> orderIds) {
+        if (CollUtil.isEmpty(orderIds)) {
+            return Collections.emptyList();
+        }
+        return saleOrderItemMapper.selectListByOrderIds(orderIds);
     }
 
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOutService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOutService.java
new file mode 100644
index 000000000..60ea7912f
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOutService.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.erp.service.sale;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out.ErpSaleOutPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out.ErpSaleOutSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOutDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOutItemDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 销售出库 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpSaleOutService {
+
+    /**
+     * 创建销售出库
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createSaleOut(@Valid ErpSaleOutSaveReqVO createReqVO);
+
+    /**
+     * 更新销售出库
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateSaleOut(@Valid ErpSaleOutSaveReqVO updateReqVO);
+
+    /**
+     * 更新销售出库的状态
+     *
+     * @param id 编号
+     * @param status 状态
+     */
+    void updateSaleOutStatus(Long id, Integer status);
+
+    /**
+     * 删除销售出库
+     *
+     * @param ids 编号数组
+     */
+    void deleteSaleOut(List<Long> ids);
+
+    /**
+     * 获得销售出库
+     *
+     * @param id 编号
+     * @return 销售出库
+     */
+    ErpSaleOutDO getSaleOut(Long id);
+
+    /**
+     * 获得销售出库分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 销售出库分页
+     */
+    PageResult<ErpSaleOutDO> getSaleOutPage(ErpSaleOutPageReqVO pageReqVO);
+
+    // ==================== 销售出库项 ====================
+
+    /**
+     * 获得销售出库项列表
+     *
+     * @param outId 销售出库编号
+     * @return 销售出库项列表
+     */
+    List<ErpSaleOutItemDO> getSaleOutItemListByOutId(Long outId);
+
+    /**
+     * 获得销售出库项 List
+     *
+     * @param outIds 销售出库编号数组
+     * @return 销售出库项 List
+     */
+    List<ErpSaleOutItemDO> getSaleOutItemListByOutIds(Collection<Long> outIds);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOutServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOutServiceImpl.java
new file mode 100644
index 000000000..ab0496892
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOutServiceImpl.java
@@ -0,0 +1,294 @@
+package cn.iocoder.yudao.module.erp.service.sale;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out.ErpSaleOutPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out.ErpSaleOutSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOutDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOutItemDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOutItemMapper;
+import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOutMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum;
+import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockRecordService;
+import cn.iocoder.yudao.module.erp.service.stock.bo.ErpStockRecordCreateReqBO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+// TODO 芋艿:记录操作日志
+
+/**
+ * ERP 销售出库 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpSaleOutServiceImpl implements ErpSaleOutService {
+
+    @Resource
+    private ErpSaleOutMapper saleOutMapper;
+    @Resource
+    private ErpSaleOutItemMapper saleOutItemMapper;
+
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private ErpSaleOrderService saleOrderService;
+    @Resource
+    private ErpAccountService accountService;
+    @Resource
+    private ErpStockRecordService stockRecordService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createSaleOut(ErpSaleOutSaveReqVO createReqVO) {
+        // 1.1 校验销售订单已审核
+        ErpSaleOrderDO saleOrder = saleOrderService.validateSaleOrder(createReqVO.getOrderId());
+        // 1.2 校验出库项的有效性
+        List<ErpSaleOutItemDO> saleOutItems = validateSaleOutItems(createReqVO.getItems());
+        // 1.3 校验结算账户
+        accountService.validateAccount(createReqVO.getAccountId());
+        // 1.4 校验销售人员
+        if (createReqVO.getSaleUserId() != null) {
+            adminUserApi.validateUser(createReqVO.getSaleUserId());
+        }
+        // 1.5 生成出库单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.SALE_OUT_NO_PREFIX);
+        if (saleOutMapper.selectByNo(no) != null) {
+            throw exception(SALE_OUT_NO_EXISTS);
+        }
+
+        // 2.1 插入出库
+        ErpSaleOutDO saleOut = BeanUtils.toBean(createReqVO, ErpSaleOutDO.class, in -> in
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()))
+                .setOrderNo(saleOrder.getNo()).setCustomerId(saleOrder.getCustomerId());
+        calculateTotalPrice(saleOut, saleOutItems);
+        saleOutMapper.insert(saleOut);
+        // 2.2 插入出库项
+        saleOutItems.forEach(o -> o.setOutId(saleOut.getId()));
+        saleOutItemMapper.insertBatch(saleOutItems);
+
+        // 3. 更新销售订单的出库数量
+        updateSaleOrderOutCount(createReqVO.getOrderId());
+        return saleOut.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateSaleOut(ErpSaleOutSaveReqVO updateReqVO) {
+        // 1.1 校验存在
+        ErpSaleOutDO saleOut = validateSaleOutExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(saleOut.getStatus())) {
+            throw exception(SALE_OUT_UPDATE_FAIL_APPROVE, saleOut.getNo());
+        }
+        // 1.2 校验销售订单已审核
+        ErpSaleOrderDO saleOrder = saleOrderService.validateSaleOrder(updateReqVO.getOrderId());
+        // 1.3 校验结算账户
+        accountService.validateAccount(updateReqVO.getAccountId());
+        // 1.4 校验销售人员
+        if (updateReqVO.getSaleUserId() != null) {
+            adminUserApi.validateUser(updateReqVO.getSaleUserId());
+        }
+        // 1.5 校验订单项的有效性
+        List<ErpSaleOutItemDO> saleOutItems = validateSaleOutItems(updateReqVO.getItems());
+
+        // 2.1 更新出库
+        ErpSaleOutDO updateObj = BeanUtils.toBean(updateReqVO, ErpSaleOutDO.class)
+                .setOrderNo(saleOrder.getNo()).setCustomerId(saleOrder.getCustomerId());
+        calculateTotalPrice(updateObj, saleOutItems);
+        saleOutMapper.updateById(updateObj);
+        // 2.2 更新出库项
+        updateSaleOutItemList(updateReqVO.getId(), saleOutItems);
+
+        // 3.1 更新销售订单的出库数量
+        updateSaleOrderOutCount(updateObj.getOrderId());
+        // 3.2 注意:如果销售订单编号变更了,需要更新“老”销售订单的出库数量
+        if (ObjectUtil.notEqual(saleOut.getOrderId(), updateObj.getOrderId())) {
+            updateSaleOrderOutCount(saleOut.getOrderId());
+        }
+    }
+
+    private void calculateTotalPrice(ErpSaleOutDO saleOut, List<ErpSaleOutItemDO> saleOutItems) {
+        saleOut.setTotalCount(getSumValue(saleOutItems, ErpSaleOutItemDO::getCount, BigDecimal::add));
+        saleOut.setTotalProductPrice(getSumValue(saleOutItems, ErpSaleOutItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
+        saleOut.setTotalTaxPrice(getSumValue(saleOutItems, ErpSaleOutItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
+        saleOut.setTotalPrice(saleOut.getTotalProductPrice().add(saleOut.getTotalTaxPrice()));
+        // 计算优惠价格
+        if (saleOut.getDiscountPercent() == null) {
+            saleOut.setDiscountPercent(BigDecimal.ZERO);
+        }
+        saleOut.setDiscountPrice(MoneyUtils.priceMultiplyPercent(saleOut.getTotalPrice(), saleOut.getDiscountPercent()));
+        saleOut.setTotalPrice(saleOut.getTotalPrice().subtract(saleOut.getDiscountPrice()));
+        // 计算应收金额
+        BigDecimal allPrice = saleOut.getTotalPrice().add(saleOut.getOtherPrice());
+        saleOut.setDebtPrice(allPrice.subtract(saleOut.getPayPrice()));
+    }
+
+    private void updateSaleOrderOutCount(Long orderId) {
+        // 1.1 查询销售订单对应的销售出库单列表
+        List<ErpSaleOutDO> saleOuts = saleOutMapper.selectListByOrderId(orderId);
+        // 1.2 查询对应的销售订单项的出库数量
+        Map<Long, BigDecimal> returnCountMap = saleOutItemMapper.selectOrderItemCountSumMapByOutIds(
+                convertList(saleOuts, ErpSaleOutDO::getId));
+        // 2. 更新销售订单的出库数量
+        saleOrderService.updateSaleOrderOutCount(orderId, returnCountMap);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateSaleOutStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpSaleOutDO saleOut = validateSaleOutExists(id);
+        // 1.2 校验状态
+        if (saleOut.getStatus().equals(status)) {
+            throw exception(approve ? SALE_OUT_APPROVE_FAIL : SALE_OUT_PROCESS_FAIL);
+        }
+
+        // 2. 更新状态
+        int updateCount = saleOutMapper.updateByIdAndStatus(id, saleOut.getStatus(),
+                new ErpSaleOutDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? SALE_OUT_APPROVE_FAIL : SALE_OUT_PROCESS_FAIL);
+        }
+
+        // 3. 变更库存
+        List<ErpSaleOutItemDO> saleOutItems = saleOutItemMapper.selectListByOutId(id);
+        Integer bizType = approve ? ErpStockRecordBizTypeEnum.SALE_OUT.getType()
+                : ErpStockRecordBizTypeEnum.SALE_OUT_CANCEL.getType();
+        saleOutItems.forEach(saleOutItem -> {
+            BigDecimal count = approve ? saleOutItem.getCount().negate() : saleOutItem.getCount();
+            stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(
+                    saleOutItem.getProductId(), saleOutItem.getWarehouseId(), count,
+                    bizType, saleOutItem.getOutId(), saleOutItem.getId(), saleOut.getNo()));
+        });
+    }
+
+    private List<ErpSaleOutItemDO> validateSaleOutItems(List<ErpSaleOutSaveReqVO.Item> list) {
+        // 1. 校验产品存在
+        List<ErpProductDO> productList = productService.validProductList(
+                convertSet(list, ErpSaleOutSaveReqVO.Item::getProductId));
+        Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
+        // 2. 转化为 ErpSaleOutItemDO 列表
+        return convertList(list, o -> BeanUtils.toBean(o, ErpSaleOutItemDO.class, item -> {
+            item.setProductUnitId(productMap.get(item.getProductId()).getUnitId());
+            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
+            if (item.getTotalPrice() == null) {
+                return;
+            }
+            if (item.getTaxPercent() != null) {
+                item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
+            }
+        }));
+    }
+
+    private void updateSaleOutItemList(Long id, List<ErpSaleOutItemDO> newList) {
+        // 第一步,对比新老数据,获得添加、修改、删除的列表
+        List<ErpSaleOutItemDO> oldList = saleOutItemMapper.selectListByOutId(id);
+        List<List<ErpSaleOutItemDO>> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录
+                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+        // 第二步,批量添加、修改、删除
+        if (CollUtil.isNotEmpty(diffList.get(0))) {
+            diffList.get(0).forEach(o -> o.setOutId(id));
+            saleOutItemMapper.insertBatch(diffList.get(0));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(1))) {
+            saleOutItemMapper.updateBatch(diffList.get(1));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(2))) {
+            saleOutItemMapper.deleteBatchIds(convertList(diffList.get(2), ErpSaleOutItemDO::getId));
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteSaleOut(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpSaleOutDO> saleOuts = saleOutMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(saleOuts)) {
+            return;
+        }
+        saleOuts.forEach(saleOut -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(saleOut.getStatus())) {
+                throw exception(SALE_OUT_DELETE_FAIL_APPROVE, saleOut.getNo());
+            }
+        });
+
+        // 2. 遍历删除,并记录操作日志
+        saleOuts.forEach(saleOut -> {
+            // 2.1 删除订单
+            saleOutMapper.deleteById(saleOut.getId());
+            // 2.2 删除订单项
+            saleOutItemMapper.deleteByOutId(saleOut.getId());
+
+            // 2.3 更新销售订单的出库数量
+            updateSaleOrderOutCount(saleOut.getOrderId());
+        });
+
+    }
+
+    private ErpSaleOutDO validateSaleOutExists(Long id) {
+        ErpSaleOutDO saleOut = saleOutMapper.selectById(id);
+        if (saleOut == null) {
+            throw exception(SALE_OUT_NOT_EXISTS);
+        }
+        return saleOut;
+    }
+
+    @Override
+    public ErpSaleOutDO getSaleOut(Long id) {
+        return saleOutMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<ErpSaleOutDO> getSaleOutPage(ErpSaleOutPageReqVO pageReqVO) {
+        return saleOutMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 销售出库项 ====================
+
+    @Override
+    public List<ErpSaleOutItemDO> getSaleOutItemListByOutId(Long outId) {
+        return saleOutItemMapper.selectListByOutId(outId);
+    }
+
+    @Override
+    public List<ErpSaleOutItemDO> getSaleOutItemListByOutIds(Collection<Long> outIds) {
+        if (CollUtil.isEmpty(outIds)) {
+            return Collections.emptyList();
+        }
+        return saleOutItemMapper.selectListByOutIds(outIds);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleReturnService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleReturnService.java
new file mode 100644
index 000000000..2186bba54
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleReturnService.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.erp.service.sale;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns.ErpSaleReturnPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns.ErpSaleReturnSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleReturnDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleReturnItemDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 销售退货 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpSaleReturnService {
+
+    /**
+     * 创建销售退货
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createSaleReturn(@Valid ErpSaleReturnSaveReqVO createReqVO);
+
+    /**
+     * 更新销售退货
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateSaleReturn(@Valid ErpSaleReturnSaveReqVO updateReqVO);
+
+    /**
+     * 更新销售退货的状态
+     *
+     * @param id 编号
+     * @param status 状态
+     */
+    void updateSaleReturnStatus(Long id, Integer status);
+
+    /**
+     * 删除销售退货
+     *
+     * @param ids 编号数组
+     */
+    void deleteSaleReturn(List<Long> ids);
+
+    /**
+     * 获得销售退货
+     *
+     * @param id 编号
+     * @return 销售退货
+     */
+    ErpSaleReturnDO getSaleReturn(Long id);
+
+    /**
+     * 获得销售退货分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 销售退货分页
+     */
+    PageResult<ErpSaleReturnDO> getSaleReturnPage(ErpSaleReturnPageReqVO pageReqVO);
+
+    // ==================== 销售退货项 ====================
+
+    /**
+     * 获得销售退货项列表
+     *
+     * @param returnId 销售退货编号
+     * @return 销售退货项列表
+     */
+    List<ErpSaleReturnItemDO> getSaleReturnItemListByReturnId(Long returnId);
+
+    /**
+     * 获得销售退货项 List
+     *
+     * @param returnIds 销售退货编号数组
+     * @return 销售退货项 List
+     */
+    List<ErpSaleReturnItemDO> getSaleReturnItemListByReturnIds(Collection<Long> returnIds);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleReturnServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleReturnServiceImpl.java
new file mode 100644
index 000000000..2c680ac45
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleReturnServiceImpl.java
@@ -0,0 +1,294 @@
+package cn.iocoder.yudao.module.erp.service.sale;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns.ErpSaleReturnPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns.ErpSaleReturnSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleReturnDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleReturnItemDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleReturnItemMapper;
+import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleReturnMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum;
+import cn.iocoder.yudao.module.erp.service.finance.ErpAccountService;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.stock.ErpStockRecordService;
+import cn.iocoder.yudao.module.erp.service.stock.bo.ErpStockRecordCreateReqBO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+// TODO 芋艿:记录操作日志
+
+/**
+ * ERP 销售退货 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpSaleReturnServiceImpl implements ErpSaleReturnService {
+
+    @Resource
+    private ErpSaleReturnMapper saleReturnMapper;
+    @Resource
+    private ErpSaleReturnItemMapper saleReturnItemMapper;
+
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    @Lazy // 延迟加载,避免循环依赖
+    private ErpSaleOrderService saleOrderService;
+    @Resource
+    private ErpAccountService accountService;
+    @Resource
+    private ErpStockRecordService stockRecordService;
+
+    @Resource
+    private AdminUserApi adminUserApi;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createSaleReturn(ErpSaleReturnSaveReqVO createReqVO) {
+        // 1.1 校验销售订单已审核
+        ErpSaleOrderDO saleOrder = saleOrderService.validateSaleOrder(createReqVO.getOrderId());
+        // 1.2 校验退货项的有效性
+        List<ErpSaleReturnItemDO> saleReturnItems = validateSaleReturnItems(createReqVO.getItems());
+        // 1.3 校验结算账户
+        accountService.validateAccount(createReqVO.getAccountId());
+        // 1.4 校验销售人员
+        if (createReqVO.getSaleUserId() != null) {
+            adminUserApi.validateUser(createReqVO.getSaleUserId());
+        }
+        // 1.5 生成退货单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.SALE_RETURN_NO_PREFIX);
+        if (saleReturnMapper.selectByNo(no) != null) {
+            throw exception(SALE_RETURN_NO_EXISTS);
+        }
+
+        // 2.1 插入退货
+        ErpSaleReturnDO saleReturn = BeanUtils.toBean(createReqVO, ErpSaleReturnDO.class, in -> in
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()))
+                .setOrderNo(saleOrder.getNo()).setCustomerId(saleOrder.getCustomerId());
+        calculateTotalPrice(saleReturn, saleReturnItems);
+        saleReturnMapper.insert(saleReturn);
+        // 2.2 插入退货项
+        saleReturnItems.forEach(o -> o.setReturnId(saleReturn.getId()));
+        saleReturnItemMapper.insertBatch(saleReturnItems);
+
+        // 3. 更新销售订单的退货数量
+        updateSaleOrderReturnCount(createReqVO.getOrderId());
+        return saleReturn.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateSaleReturn(ErpSaleReturnSaveReqVO updateReqVO) {
+        // 1.1 校验存在
+        ErpSaleReturnDO saleReturn = validateSaleReturnExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(saleReturn.getStatus())) {
+            throw exception(SALE_RETURN_UPDATE_FAIL_APPROVE, saleReturn.getNo());
+        }
+        // 1.2 校验销售订单已审核
+        ErpSaleOrderDO saleOrder = saleOrderService.validateSaleOrder(updateReqVO.getOrderId());
+        // 1.3 校验结算账户
+        accountService.validateAccount(updateReqVO.getAccountId());
+        // 1.4 校验销售人员
+        if (updateReqVO.getSaleUserId() != null) {
+            adminUserApi.validateUser(updateReqVO.getSaleUserId());
+        }
+        // 1.5 校验订单项的有效性
+        List<ErpSaleReturnItemDO> saleReturnItems = validateSaleReturnItems(updateReqVO.getItems());
+
+        // 2.1 更新退货
+        ErpSaleReturnDO updateObj = BeanUtils.toBean(updateReqVO, ErpSaleReturnDO.class)
+                .setOrderNo(saleOrder.getNo()).setCustomerId(saleOrder.getCustomerId());
+        calculateTotalPrice(updateObj, saleReturnItems);
+        saleReturnMapper.updateById(updateObj);
+        // 2.2 更新退货项
+        updateSaleReturnItemList(updateReqVO.getId(), saleReturnItems);
+
+        // 3.1 更新销售订单的出库数量
+        updateSaleOrderReturnCount(updateObj.getOrderId());
+        // 3.2 注意:如果销售订单编号变更了,需要更新“老”销售订单的出库数量
+        if (ObjectUtil.notEqual(saleReturn.getOrderId(), updateObj.getOrderId())) {
+            updateSaleOrderReturnCount(saleReturn.getOrderId());
+        }
+    }
+
+    private void calculateTotalPrice(ErpSaleReturnDO saleReturn, List<ErpSaleReturnItemDO> saleReturnItems) {
+        saleReturn.setTotalCount(getSumValue(saleReturnItems, ErpSaleReturnItemDO::getCount, BigDecimal::add));
+        saleReturn.setTotalProductPrice(getSumValue(saleReturnItems, ErpSaleReturnItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
+        saleReturn.setTotalTaxPrice(getSumValue(saleReturnItems, ErpSaleReturnItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
+        saleReturn.setTotalPrice(saleReturn.getTotalProductPrice().add(saleReturn.getTotalTaxPrice()));
+        // 计算优惠价格
+        if (saleReturn.getDiscountPercent() == null) {
+            saleReturn.setDiscountPercent(BigDecimal.ZERO);
+        }
+        saleReturn.setDiscountPrice(MoneyUtils.priceMultiplyPercent(saleReturn.getTotalPrice(), saleReturn.getDiscountPercent()));
+        saleReturn.setTotalPrice(saleReturn.getTotalPrice().subtract(saleReturn.getDiscountPrice()));
+        // 计算应退金额
+        BigDecimal allPrice = saleReturn.getTotalPrice().add(saleReturn.getOtherPrice());
+        saleReturn.setDebtPrice(allPrice.subtract(saleReturn.getRefundPrice()));
+    }
+
+    private void updateSaleOrderReturnCount(Long orderId) {
+        // 1.1 查询销售订单对应的销售出库单列表
+        List<ErpSaleReturnDO> saleReturns = saleReturnMapper.selectListByOrderId(orderId);
+        // 1.2 查询对应的销售订单项的退货数量
+        Map<Long, BigDecimal> returnCountMap = saleReturnItemMapper.selectOrderItemCountSumMapByReturnIds(
+                convertList(saleReturns, ErpSaleReturnDO::getId));
+        // 2. 更新销售订单的出库数量
+        saleOrderService.updateSaleOrderReturnCount(orderId, returnCountMap);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateSaleReturnStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpSaleReturnDO saleReturn = validateSaleReturnExists(id);
+        // 1.2 校验状态
+        if (saleReturn.getStatus().equals(status)) {
+            throw exception(approve ? SALE_RETURN_APPROVE_FAIL : SALE_RETURN_PROCESS_FAIL);
+        }
+
+        // 2. 更新状态
+        int updateCount = saleReturnMapper.updateByIdAndStatus(id, saleReturn.getStatus(),
+                new ErpSaleReturnDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? SALE_RETURN_APPROVE_FAIL : SALE_RETURN_PROCESS_FAIL);
+        }
+
+        // 3. 变更库存
+        List<ErpSaleReturnItemDO> saleReturnItems = saleReturnItemMapper.selectListByReturnId(id);
+        Integer bizType = approve ? ErpStockRecordBizTypeEnum.SALE_RETURN.getType()
+                : ErpStockRecordBizTypeEnum.SALE_RETURN_CANCEL.getType();
+        saleReturnItems.forEach(saleReturnItem -> {
+            BigDecimal count = approve ? saleReturnItem.getCount() : saleReturnItem.getCount().negate();
+            stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(
+                    saleReturnItem.getProductId(), saleReturnItem.getWarehouseId(), count,
+                    bizType, saleReturnItem.getReturnId(), saleReturnItem.getId(), saleReturn.getNo()));
+        });
+    }
+
+    private List<ErpSaleReturnItemDO> validateSaleReturnItems(List<ErpSaleReturnSaveReqVO.Item> list) {
+        // 1. 校验产品存在
+        List<ErpProductDO> productList = productService.validProductList(
+                convertSet(list, ErpSaleReturnSaveReqVO.Item::getProductId));
+        Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
+        // 2. 转化为 ErpSaleReturnItemDO 列表
+        return convertList(list, o -> BeanUtils.toBean(o, ErpSaleReturnItemDO.class, item -> {
+            item.setProductUnitId(productMap.get(item.getProductId()).getUnitId());
+            item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
+            if (item.getTotalPrice() == null) {
+                return;
+            }
+            if (item.getTaxPercent() != null) {
+                item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
+            }
+        }));
+    }
+
+    private void updateSaleReturnItemList(Long id, List<ErpSaleReturnItemDO> newList) {
+        // 第一步,对比新老数据,获得添加、修改、删除的列表
+        List<ErpSaleReturnItemDO> oldList = saleReturnItemMapper.selectListByReturnId(id);
+        List<List<ErpSaleReturnItemDO>> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录
+                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+        // 第二步,批量添加、修改、删除
+        if (CollUtil.isNotEmpty(diffList.get(0))) {
+            diffList.get(0).forEach(o -> o.setReturnId(id));
+            saleReturnItemMapper.insertBatch(diffList.get(0));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(1))) {
+            saleReturnItemMapper.updateBatch(diffList.get(1));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(2))) {
+            saleReturnItemMapper.deleteBatchIds(convertList(diffList.get(2), ErpSaleReturnItemDO::getId));
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteSaleReturn(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpSaleReturnDO> saleReturns = saleReturnMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(saleReturns)) {
+            return;
+        }
+        saleReturns.forEach(saleReturn -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(saleReturn.getStatus())) {
+                throw exception(SALE_RETURN_DELETE_FAIL_APPROVE, saleReturn.getNo());
+            }
+        });
+
+        // 2. 遍历删除,并记录操作日志
+        saleReturns.forEach(saleReturn -> {
+            // 2.1 删除订单
+            saleReturnMapper.deleteById(saleReturn.getId());
+            // 2.2 删除订单项
+            saleReturnItemMapper.deleteByReturnId(saleReturn.getId());
+
+            // 2.3 更新销售订单的出库数量
+            updateSaleOrderReturnCount(saleReturn.getOrderId());
+        });
+
+    }
+
+    private ErpSaleReturnDO validateSaleReturnExists(Long id) {
+        ErpSaleReturnDO saleReturn = saleReturnMapper.selectById(id);
+        if (saleReturn == null) {
+            throw exception(SALE_RETURN_NOT_EXISTS);
+        }
+        return saleReturn;
+    }
+
+    @Override
+    public ErpSaleReturnDO getSaleReturn(Long id) {
+        return saleReturnMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<ErpSaleReturnDO> getSaleReturnPage(ErpSaleReturnPageReqVO pageReqVO) {
+        return saleReturnMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 销售退货项 ====================
+
+    @Override
+    public List<ErpSaleReturnItemDO> getSaleReturnItemListByReturnId(Long returnId) {
+        return saleReturnItemMapper.selectListByReturnId(returnId);
+    }
+
+    @Override
+    public List<ErpSaleReturnItemDO> getSaleReturnItemListByReturnIds(Collection<Long> returnIds) {
+        if (CollUtil.isEmpty(returnIds)) {
+            return Collections.emptyList();
+        }
+        return saleReturnItemMapper.selectListByReturnIds(returnIds);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockCheckService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockCheckService.java
new file mode 100644
index 000000000..2f8a0f9ee
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockCheckService.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.erp.service.stock;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check.ErpStockCheckPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check.ErpStockCheckSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockCheckDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockCheckItemDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 库存盘点单 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpStockCheckService {
+
+    /**
+     * 创建库存盘点单
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createStockCheck(@Valid ErpStockCheckSaveReqVO createReqVO);
+
+    /**
+     * 更新库存盘点单
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateStockCheck(@Valid ErpStockCheckSaveReqVO updateReqVO);
+
+    /**
+     * 更新库存盘点单的状态
+     *
+     * @param id 编号
+     * @param status 状态
+     */
+    void updateStockCheckStatus(Long id, Integer status);
+
+    /**
+     * 删除库存盘点单
+     *
+     * @param ids 编号数组
+     */
+    void deleteStockCheck(List<Long> ids);
+
+    /**
+     * 获得库存盘点单
+     *
+     * @param id 编号
+     * @return 库存盘点单
+     */
+    ErpStockCheckDO getStockCheck(Long id);
+
+    /**
+     * 获得库存盘点单分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 库存盘点单分页
+     */
+    PageResult<ErpStockCheckDO> getStockCheckPage(ErpStockCheckPageReqVO pageReqVO);
+
+    // ==================== 盘点项 ====================
+
+    /**
+     * 获得库存盘点单项列表
+     *
+     * @param checkId 盘点编号
+     * @return 库存盘点单项列表
+     */
+    List<ErpStockCheckItemDO> getStockCheckItemListByCheckId(Long checkId);
+
+    /**
+     * 获得库存盘点单项 List
+     *
+     * @param checkIds 盘点编号数组
+     * @return 库存盘点单项 List
+     */
+    List<ErpStockCheckItemDO> getStockCheckItemListByCheckIds(Collection<Long> checkIds);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockCheckServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockCheckServiceImpl.java
new file mode 100644
index 000000000..49aca94d3
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockCheckServiceImpl.java
@@ -0,0 +1,232 @@
+package cn.iocoder.yudao.module.erp.service.stock;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check.ErpStockCheckPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check.ErpStockCheckSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockCheckDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockCheckItemDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpStockCheckItemMapper;
+import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpStockCheckMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.stock.bo.ErpStockRecordCreateReqBO;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+// TODO 芋艿:记录操作日志
+
+/**
+ * ERP 库存盘点单 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpStockCheckServiceImpl implements ErpStockCheckService {
+
+    @Resource
+    private ErpStockCheckMapper stockCheckMapper;
+    @Resource
+    private ErpStockCheckItemMapper stockCheckItemMapper;
+
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpWarehouseService warehouseService;
+    @Resource
+    private ErpStockRecordService stockRecordService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createStockCheck(ErpStockCheckSaveReqVO createReqVO) {
+        // 1.1 校验盘点项的有效性
+        List<ErpStockCheckItemDO> stockCheckItems = validateStockCheckItems(createReqVO.getItems());
+        // 1.2 生成盘点单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.STOCK_CHECK_NO_PREFIX);
+        if (stockCheckMapper.selectByNo(no) != null) {
+            throw exception(STOCK_CHECK_NO_EXISTS);
+        }
+
+        // 2.1 插入盘点单
+        ErpStockCheckDO stockCheck = BeanUtils.toBean(createReqVO, ErpStockCheckDO.class, in -> in
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
+                .setTotalCount(getSumValue(stockCheckItems, ErpStockCheckItemDO::getCount, BigDecimal::add))
+                .setTotalPrice(getSumValue(stockCheckItems, ErpStockCheckItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO)));
+        stockCheckMapper.insert(stockCheck);
+        // 2.2 插入盘点单项
+        stockCheckItems.forEach(o -> o.setCheckId(stockCheck.getId()));
+        stockCheckItemMapper.insertBatch(stockCheckItems);
+        return stockCheck.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStockCheck(ErpStockCheckSaveReqVO updateReqVO) {
+        // 1.1 校验存在
+        ErpStockCheckDO stockCheck = validateStockCheckExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(stockCheck.getStatus())) {
+            throw exception(STOCK_CHECK_UPDATE_FAIL_APPROVE, stockCheck.getNo());
+        }
+        // 1.2 校验盘点项的有效性
+        List<ErpStockCheckItemDO> stockCheckItems = validateStockCheckItems(updateReqVO.getItems());
+
+        // 2.1 更新盘点单
+        ErpStockCheckDO updateObj = BeanUtils.toBean(updateReqVO, ErpStockCheckDO.class, in -> in
+                .setTotalCount(getSumValue(stockCheckItems, ErpStockCheckItemDO::getCount, BigDecimal::add))
+                .setTotalPrice(getSumValue(stockCheckItems, ErpStockCheckItemDO::getTotalPrice, BigDecimal::add)));
+        stockCheckMapper.updateById(updateObj);
+        // 2.2 更新盘点单项
+        updateStockCheckItemList(updateReqVO.getId(), stockCheckItems);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStockCheckStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpStockCheckDO stockCheck = validateStockCheckExists(id);
+        // 1.2 校验状态
+        if (stockCheck.getStatus().equals(status)) {
+            throw exception(approve ? STOCK_CHECK_APPROVE_FAIL : STOCK_CHECK_PROCESS_FAIL);
+        }
+
+        // 2. 更新状态
+        int updateCount = stockCheckMapper.updateByIdAndStatus(id, stockCheck.getStatus(),
+                new ErpStockCheckDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? STOCK_CHECK_APPROVE_FAIL : STOCK_CHECK_PROCESS_FAIL);
+        }
+
+        // 3. 变更库存
+        List<ErpStockCheckItemDO> stockCheckItems = stockCheckItemMapper.selectListByCheckId(id);
+        stockCheckItems.forEach(stockCheckItem -> {
+            // 没有盈亏,不用出入库
+            if (stockCheckItem.getCount().compareTo(BigDecimal.ZERO) == 0) {
+                return;
+            }
+            // 10;12;-2()
+            BigDecimal count = approve ? stockCheckItem.getCount(): stockCheckItem.getCount().negate();
+            Integer bizType;
+            if (approve) {
+                bizType = count.compareTo(BigDecimal.ZERO) > 0 ? ErpStockRecordBizTypeEnum.CHECK_MORE_IN.getType()
+                        : ErpStockRecordBizTypeEnum.CHECK_LESS_OUT.getType();
+            } else {
+                bizType = count.compareTo(BigDecimal.ZERO) > 0 ? ErpStockRecordBizTypeEnum.CHECK_MORE_IN_CANCEL.getType()
+                        : ErpStockRecordBizTypeEnum.CHECK_LESS_OUT_CANCEL.getType();
+            }
+            stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(
+                    stockCheckItem.getProductId(), stockCheckItem.getWarehouseId(), count,
+                    bizType, stockCheckItem.getCheckId(), stockCheckItem.getId(), stockCheck.getNo()));
+        });
+    }
+
+    private List<ErpStockCheckItemDO> validateStockCheckItems(List<ErpStockCheckSaveReqVO.Item> list) {
+        // 1.1 校验产品存在
+        List<ErpProductDO> productList = productService.validProductList(
+                convertSet(list, ErpStockCheckSaveReqVO.Item::getProductId));
+        Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
+        // 1.2 校验仓库存在
+        warehouseService.validWarehouseList(convertSet(list, ErpStockCheckSaveReqVO.Item::getWarehouseId));
+        // 2. 转化为 ErpStockCheckItemDO 列表
+        return convertList(list, o -> BeanUtils.toBean(o, ErpStockCheckItemDO.class, item -> item
+                .setProductUnitId(productMap.get(item.getProductId()).getUnitId())
+                .setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()))));
+    }
+
+    private void updateStockCheckItemList(Long id, List<ErpStockCheckItemDO> newList) {
+        // 第一步,对比新老数据,获得添加、修改、删除的列表
+        List<ErpStockCheckItemDO> oldList = stockCheckItemMapper.selectListByCheckId(id);
+        List<List<ErpStockCheckItemDO>> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录
+                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+        // 第二步,批量添加、修改、删除
+        if (CollUtil.isNotEmpty(diffList.get(0))) {
+            diffList.get(0).forEach(o -> o.setCheckId(id));
+            stockCheckItemMapper.insertBatch(diffList.get(0));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(1))) {
+            stockCheckItemMapper.updateBatch(diffList.get(1));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(2))) {
+            stockCheckItemMapper.deleteBatchIds(convertList(diffList.get(2), ErpStockCheckItemDO::getId));
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteStockCheck(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpStockCheckDO> stockChecks = stockCheckMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(stockChecks)) {
+            return;
+        }
+        stockChecks.forEach(stockCheck -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(stockCheck.getStatus())) {
+                throw exception(STOCK_CHECK_DELETE_FAIL_APPROVE, stockCheck.getNo());
+            }
+        });
+
+        // 2. 遍历删除,并记录操作日志
+        stockChecks.forEach(stockCheck -> {
+            // 2.1 删除盘点单
+            stockCheckMapper.deleteById(stockCheck.getId());
+            // 2.2 删除盘点单项
+            stockCheckItemMapper.deleteByCheckId(stockCheck.getId());
+        });
+    }
+
+    private ErpStockCheckDO validateStockCheckExists(Long id) {
+        ErpStockCheckDO stockCheck = stockCheckMapper.selectById(id);
+        if (stockCheck == null) {
+            throw exception(STOCK_CHECK_NOT_EXISTS);
+        }
+        return stockCheck;
+    }
+
+    @Override
+    public ErpStockCheckDO getStockCheck(Long id) {
+        return stockCheckMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<ErpStockCheckDO> getStockCheckPage(ErpStockCheckPageReqVO pageReqVO) {
+        return stockCheckMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 盘点项 ====================
+
+    @Override
+    public List<ErpStockCheckItemDO> getStockCheckItemListByCheckId(Long checkId) {
+        return stockCheckItemMapper.selectListByCheckId(checkId);
+    }
+
+    @Override
+    public List<ErpStockCheckItemDO> getStockCheckItemListByCheckIds(Collection<Long> checkIds) {
+        if (CollUtil.isEmpty(checkIds)) {
+            return Collections.emptyList();
+        }
+        return stockCheckItemMapper.selectListByCheckIds(checkIds);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockInService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockInService.java
index 92555135f..4e51545e4 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockInService.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockInService.java
@@ -33,11 +33,19 @@ public interface ErpStockInService {
     void updateStockIn(@Valid ErpStockInSaveReqVO updateReqVO);
 
     /**
-     * 删除其它入库单
+     * 更新其它入库单的状态
      *
      * @param id 编号
+     * @param status 状态
      */
-    void deleteStockIn(Long id);
+    void updateStockInStatus(Long id, Integer status);
+
+    /**
+     * 删除其它入库单
+     *
+     * @param ids 编号数组
+     */
+    void deleteStockIn(List<Long> ids);
 
     /**
      * 获得其它入库单
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockInServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockInServiceImpl.java
index 0cf716018..213fca8b8 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockInServiceImpl.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockInServiceImpl.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.erp.service.stock;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.in.ErpStockInPageReqVO;
 import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.in.ErpStockInSaveReqVO;
@@ -10,9 +11,12 @@ import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockInDO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockInItemDO;
 import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpStockInItemMapper;
 import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpStockInMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
 import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum;
 import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
 import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
+import cn.iocoder.yudao.module.erp.service.stock.bo.ErpStockRecordCreateReqBO;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -26,9 +30,9 @@ import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
-import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.STOCK_IN_NOT_EXISTS;
-
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
 
+// TODO 芋艿:记录操作日志
 /**
  * ERP 其它入库单 Service 实现类
  *
@@ -43,12 +47,17 @@ public class ErpStockInServiceImpl implements ErpStockInService {
     @Resource
     private ErpStockInItemMapper stockInItemMapper;
 
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
     @Resource
     private ErpProductService productService;
     @Resource
     private ErpWarehouseService warehouseService;
     @Resource
     private ErpSupplierService supplierService;
+    @Resource
+    private ErpStockRecordService stockRecordService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -57,14 +66,19 @@ public class ErpStockInServiceImpl implements ErpStockInService {
         List<ErpStockInItemDO> stockInItems = validateStockInItems(createReqVO.getItems());
         // 1.2 校验供应商
         supplierService.validateSupplier(createReqVO.getSupplierId());
+        // 1.3 生成入库单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.STOCK_IN_NO_PREFIX);
+        if (stockInMapper.selectByNo(no) != null) {
+            throw exception(STOCK_IN_NO_EXISTS);
+        }
 
         // 2.1 插入入库单
         ErpStockInDO stockIn = BeanUtils.toBean(createReqVO, ErpStockInDO.class, in -> in
-                .setStatus(ErpAuditStatus.PROCESS.getStatus())
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
                 .setTotalCount(getSumValue(stockInItems, ErpStockInItemDO::getCount, BigDecimal::add))
-                .setTotalPrice(getSumValue(stockInItems, ErpStockInItemDO::getTotalPrice, BigDecimal::add)));
+                .setTotalPrice(getSumValue(stockInItems, ErpStockInItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO)));
         stockInMapper.insert(stockIn);
-        // 2. 插入子表
+        // 2.2 插入入库单项
         stockInItems.forEach(o -> o.setInId(stockIn.getId()));
         stockInItemMapper.insertBatch(stockInItems);
         return stockIn.getId();
@@ -74,7 +88,10 @@ public class ErpStockInServiceImpl implements ErpStockInService {
     @Transactional(rollbackFor = Exception.class)
     public void updateStockIn(ErpStockInSaveReqVO updateReqVO) {
         // 1.1 校验存在
-        validateStockInExists(updateReqVO.getId());
+        ErpStockInDO stockIn = validateStockInExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(stockIn.getStatus())) {
+            throw exception(STOCK_IN_UPDATE_FAIL_APPROVE, stockIn.getNo());
+        }
         // 1.2 校验供应商
         supplierService.validateSupplier(updateReqVO.getSupplierId());
         // 1.3 校验入库项的有效性
@@ -89,16 +106,48 @@ public class ErpStockInServiceImpl implements ErpStockInService {
         updateStockInItemList(updateReqVO.getId(), stockInItems);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStockInStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpStockInDO stockIn = validateStockInExists(id);
+        // 1.2 校验状态
+        if (stockIn.getStatus().equals(status)) {
+            throw exception(approve ? STOCK_IN_APPROVE_FAIL : STOCK_IN_PROCESS_FAIL);
+        }
+
+        // 2. 更新状态
+        int updateCount = stockInMapper.updateByIdAndStatus(id, stockIn.getStatus(),
+                new ErpStockInDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? STOCK_IN_APPROVE_FAIL : STOCK_IN_PROCESS_FAIL);
+        }
+
+        // 3. 变更库存
+        List<ErpStockInItemDO> stockInItems = stockInItemMapper.selectListByInId(id);
+        Integer bizType = approve ? ErpStockRecordBizTypeEnum.OTHER_IN.getType()
+                : ErpStockRecordBizTypeEnum.OTHER_IN_CANCEL.getType();
+        stockInItems.forEach(stockInItem -> {
+            BigDecimal count = approve ? stockInItem.getCount() : stockInItem.getCount().negate();
+            stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(
+                    stockInItem.getProductId(), stockInItem.getWarehouseId(), count,
+                    bizType, stockInItem.getInId(), stockInItem.getId(), stockIn.getNo()));
+        });
+    }
+
     private List<ErpStockInItemDO> validateStockInItems(List<ErpStockInSaveReqVO.Item> list) {
         // 1.1 校验产品存在
-        List<ErpProductDO> productList = productService.validProductList(convertSet(list, ErpStockInSaveReqVO.Item::getProductId));
+        List<ErpProductDO> productList = productService.validProductList(
+                convertSet(list, ErpStockInSaveReqVO.Item::getProductId));
         Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
         // 1.2 校验仓库存在
-        warehouseService.validWarehouseList(convertSet(list, ErpStockInSaveReqVO.Item::getWarehouseId));
+        warehouseService.validWarehouseList(convertSet(
+                list, ErpStockInSaveReqVO.Item::getWarehouseId));
         // 2. 转化为 ErpStockInItemDO 列表
         return convertList(list, o -> BeanUtils.toBean(o, ErpStockInItemDO.class, item -> item
                 .setProductUnitId(productMap.get(item.getProductId()).getUnitId())
-                .setTotalPrice(item.getProductPrice().multiply(item.getCount()))));
+                .setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()))));
     }
 
     private void updateStockInItemList(Long id, List<ErpStockInItemDO> newList) {
@@ -122,21 +171,33 @@ public class ErpStockInServiceImpl implements ErpStockInService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void deleteStockIn(Long id) {
-        // 1. 校验存在
-        validateStockInExists(id);
-        // TODO 芋艿:校验一下;
+    public void deleteStockIn(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpStockInDO> stockIns = stockInMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(stockIns)) {
+            return;
+        }
+        stockIns.forEach(stockIn -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(stockIn.getStatus())) {
+                throw exception(STOCK_IN_DELETE_FAIL_APPROVE, stockIn.getNo());
+            }
+        });
 
-        // 2.1 删除
-        stockInMapper.deleteById(id);
-        // 2.2 删除子表
-        stockInItemMapper.deleteByInId(id);
+        // 2. 遍历删除,并记录操作日志
+        stockIns.forEach(stockIn -> {
+            // 2.1 删除入库单
+            stockInMapper.deleteById(stockIn.getId());
+            // 2.2 删除入库单项
+            stockInItemMapper.deleteByInId(stockIn.getId());
+        });
     }
 
-    private void validateStockInExists(Long id) {
-        if (stockInMapper.selectById(id) == null) {
+    private ErpStockInDO validateStockInExists(Long id) {
+        ErpStockInDO stockIn = stockInMapper.selectById(id);
+        if (stockIn == null) {
             throw exception(STOCK_IN_NOT_EXISTS);
         }
+        return stockIn;
     }
 
     @Override
@@ -149,7 +210,7 @@ public class ErpStockInServiceImpl implements ErpStockInService {
         return stockInMapper.selectPage(pageReqVO);
     }
 
-    // ==================== 子表(ERP 其它入库单项) ====================
+    // ==================== 入库项 ====================
 
     @Override
     public List<ErpStockInItemDO> getStockInItemListByInId(Long inId) {
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockMoveService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockMoveService.java
new file mode 100644
index 000000000..8af13a639
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockMoveService.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.erp.service.stock;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move.ErpStockMovePageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move.ErpStockMoveSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockMoveDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockMoveItemDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 库存调拨单 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpStockMoveService {
+
+    /**
+     * 创建库存调拨单
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createStockMove(@Valid ErpStockMoveSaveReqVO createReqVO);
+
+    /**
+     * 更新库存调拨单
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateStockMove(@Valid ErpStockMoveSaveReqVO updateReqVO);
+
+    /**
+     * 更新库存调拨单的状态
+     *
+     * @param id 编号
+     * @param status 状态
+     */
+    void updateStockMoveStatus(Long id, Integer status);
+
+    /**
+     * 删除库存调拨单
+     *
+     * @param ids 编号数组
+     */
+    void deleteStockMove(List<Long> ids);
+
+    /**
+     * 获得库存调拨单
+     *
+     * @param id 编号
+     * @return 库存调拨单
+     */
+    ErpStockMoveDO getStockMove(Long id);
+
+    /**
+     * 获得库存调拨单分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 库存调拨单分页
+     */
+    PageResult<ErpStockMoveDO> getStockMovePage(ErpStockMovePageReqVO pageReqVO);
+
+    // ==================== 调拨项 ====================
+
+    /**
+     * 获得库存调拨单项列表
+     *
+     * @param moveId 调拨编号
+     * @return 库存调拨单项列表
+     */
+    List<ErpStockMoveItemDO> getStockMoveItemListByMoveId(Long moveId);
+
+    /**
+     * 获得库存调拨单项 List
+     *
+     * @param moveIds 调拨编号数组
+     * @return 库存调拨单项 List
+     */
+    List<ErpStockMoveItemDO> getStockMoveItemListByMoveIds(Collection<Long> moveIds);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockMoveServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockMoveServiceImpl.java
new file mode 100644
index 000000000..a0d8f79d4
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockMoveServiceImpl.java
@@ -0,0 +1,229 @@
+package cn.iocoder.yudao.module.erp.service.stock;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move.ErpStockMovePageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move.ErpStockMoveSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockMoveDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockMoveItemDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpStockMoveItemMapper;
+import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpStockMoveMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.stock.bo.ErpStockRecordCreateReqBO;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+// TODO 芋艿:记录操作日志
+
+/**
+ * ERP 库存调拨单 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpStockMoveServiceImpl implements ErpStockMoveService {
+
+    @Resource
+    private ErpStockMoveMapper stockMoveMapper;
+    @Resource
+    private ErpStockMoveItemMapper stockMoveItemMapper;
+
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpWarehouseService warehouseService;
+    @Resource
+    private ErpStockRecordService stockRecordService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createStockMove(ErpStockMoveSaveReqVO createReqVO) {
+        // 1.1 校验出库项的有效性
+        List<ErpStockMoveItemDO> stockMoveItems = validateStockMoveItems(createReqVO.getItems());
+        // 1.2 生成调拨单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.STOCK_MOVE_NO_PREFIX);
+        if (stockMoveMapper.selectByNo(no) != null) {
+            throw exception(STOCK_MOVE_NO_EXISTS);
+        }
+
+        // 2.1 插入出库单
+        ErpStockMoveDO stockMove = BeanUtils.toBean(createReqVO, ErpStockMoveDO.class, in -> in
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
+                .setTotalCount(getSumValue(stockMoveItems, ErpStockMoveItemDO::getCount, BigDecimal::add))
+                .setTotalPrice(getSumValue(stockMoveItems, ErpStockMoveItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO)));
+        stockMoveMapper.insert(stockMove);
+        // 2.2 插入出库单项
+        stockMoveItems.forEach(o -> o.setMoveId(stockMove.getId()));
+        stockMoveItemMapper.insertBatch(stockMoveItems);
+        return stockMove.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStockMove(ErpStockMoveSaveReqVO updateReqVO) {
+        // 1.1 校验存在
+        ErpStockMoveDO stockMove = validateStockMoveExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(stockMove.getStatus())) {
+            throw exception(STOCK_MOVE_UPDATE_FAIL_APPROVE, stockMove.getNo());
+        }
+        // 1.2 校验出库项的有效性
+        List<ErpStockMoveItemDO> stockMoveItems = validateStockMoveItems(updateReqVO.getItems());
+
+        // 2.1 更新出库单
+        ErpStockMoveDO updateObj = BeanUtils.toBean(updateReqVO, ErpStockMoveDO.class, in -> in
+                .setTotalCount(getSumValue(stockMoveItems, ErpStockMoveItemDO::getCount, BigDecimal::add))
+                .setTotalPrice(getSumValue(stockMoveItems, ErpStockMoveItemDO::getTotalPrice, BigDecimal::add)));
+        stockMoveMapper.updateById(updateObj);
+        // 2.2 更新出库单项
+        updateStockMoveItemList(updateReqVO.getId(), stockMoveItems);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStockMoveStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpStockMoveDO stockMove = validateStockMoveExists(id);
+        // 1.2 校验状态
+        if (stockMove.getStatus().equals(status)) {
+            throw exception(approve ? STOCK_MOVE_APPROVE_FAIL : STOCK_MOVE_PROCESS_FAIL);
+        }
+
+        // 2. 更新状态
+        int updateCount = stockMoveMapper.updateByIdAndStatus(id, stockMove.getStatus(),
+                new ErpStockMoveDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? STOCK_MOVE_APPROVE_FAIL : STOCK_MOVE_PROCESS_FAIL);
+        }
+
+        // 3. 变更库存
+        List<ErpStockMoveItemDO> stockMoveItems = stockMoveItemMapper.selectListByMoveId(id);
+        Integer fromBizType = approve ? ErpStockRecordBizTypeEnum.MOVE_OUT.getType()
+                : ErpStockRecordBizTypeEnum.MOVE_OUT_CANCEL.getType();
+        Integer toBizType = approve ? ErpStockRecordBizTypeEnum.MOVE_IN.getType()
+                : ErpStockRecordBizTypeEnum.MOVE_IN_CANCEL.getType();
+        stockMoveItems.forEach(stockMoveItem -> {
+            BigDecimal fromCount = approve ? stockMoveItem.getCount().negate() : stockMoveItem.getCount();
+            BigDecimal toCount = approve ? stockMoveItem.getCount() : stockMoveItem.getCount().negate();
+            stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(
+                    stockMoveItem.getProductId(), stockMoveItem.getFromWarehouseId(), fromCount,
+                    fromBizType, stockMoveItem.getMoveId(), stockMoveItem.getId(), stockMove.getNo()));
+            stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(
+                    stockMoveItem.getProductId(), stockMoveItem.getToWarehouseId(), toCount,
+                    toBizType, stockMoveItem.getMoveId(), stockMoveItem.getId(), stockMove.getNo()));
+        });
+    }
+
+    private List<ErpStockMoveItemDO> validateStockMoveItems(List<ErpStockMoveSaveReqVO.Item> list) {
+        // 1.1 校验产品存在
+        List<ErpProductDO> productList = productService.validProductList(
+                convertSet(list, ErpStockMoveSaveReqVO.Item::getProductId));
+        Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
+        // 1.2 校验仓库存在
+        warehouseService.validWarehouseList(convertSetByFlatMap(list,
+                item -> Stream.of(item.getFromWarehouseId(),  item.getToWarehouseId())));
+        // 2. 转化为 ErpStockMoveItemDO 列表
+        return convertList(list, o -> BeanUtils.toBean(o, ErpStockMoveItemDO.class, item -> item
+                .setProductUnitId(productMap.get(item.getProductId()).getUnitId())
+                .setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()))));
+    }
+
+    private void updateStockMoveItemList(Long id, List<ErpStockMoveItemDO> newList) {
+        // 第一步,对比新老数据,获得添加、修改、删除的列表
+        List<ErpStockMoveItemDO> oldList = stockMoveItemMapper.selectListByMoveId(id);
+        List<List<ErpStockMoveItemDO>> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录
+                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+        // 第二步,批量添加、修改、删除
+        if (CollUtil.isNotEmpty(diffList.get(0))) {
+            diffList.get(0).forEach(o -> o.setMoveId(id));
+            stockMoveItemMapper.insertBatch(diffList.get(0));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(1))) {
+            stockMoveItemMapper.updateBatch(diffList.get(1));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(2))) {
+            stockMoveItemMapper.deleteBatchIds(convertList(diffList.get(2), ErpStockMoveItemDO::getId));
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteStockMove(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpStockMoveDO> stockMoves = stockMoveMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(stockMoves)) {
+            return;
+        }
+        stockMoves.forEach(stockMove -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(stockMove.getStatus())) {
+                throw exception(STOCK_MOVE_DELETE_FAIL_APPROVE, stockMove.getNo());
+            }
+        });
+
+        // 2. 遍历删除,并记录操作日志
+        stockMoves.forEach(stockMove -> {
+            // 2.1 删除出库单
+            stockMoveMapper.deleteById(stockMove.getId());
+            // 2.2 删除出库单项
+            stockMoveItemMapper.deleteByMoveId(stockMove.getId());
+        });
+    }
+
+    private ErpStockMoveDO validateStockMoveExists(Long id) {
+        ErpStockMoveDO stockMove = stockMoveMapper.selectById(id);
+        if (stockMove == null) {
+            throw exception(STOCK_MOVE_NOT_EXISTS);
+        }
+        return stockMove;
+    }
+
+    @Override
+    public ErpStockMoveDO getStockMove(Long id) {
+        return stockMoveMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<ErpStockMoveDO> getStockMovePage(ErpStockMovePageReqVO pageReqVO) {
+        return stockMoveMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 出库项 ====================
+
+    @Override
+    public List<ErpStockMoveItemDO> getStockMoveItemListByMoveId(Long moveId) {
+        return stockMoveItemMapper.selectListByMoveId(moveId);
+    }
+
+    @Override
+    public List<ErpStockMoveItemDO> getStockMoveItemListByMoveIds(Collection<Long> moveIds) {
+        if (CollUtil.isEmpty(moveIds)) {
+            return Collections.emptyList();
+        }
+        return stockMoveItemMapper.selectListByMoveIds(moveIds);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockOutService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockOutService.java
new file mode 100644
index 000000000..558fb6948
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockOutService.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.erp.service.stock;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out.ErpStockOutPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out.ErpStockOutSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutItemDO;
+import jakarta.validation.Valid;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * ERP 其它出库单 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface ErpStockOutService {
+
+    /**
+     * 创建其它出库单
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createStockOut(@Valid ErpStockOutSaveReqVO createReqVO);
+
+    /**
+     * 更新其它出库单
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateStockOut(@Valid ErpStockOutSaveReqVO updateReqVO);
+
+    /**
+     * 更新其它出库单的状态
+     *
+     * @param id 编号
+     * @param status 状态
+     */
+    void updateStockOutStatus(Long id, Integer status);
+
+    /**
+     * 删除其它出库单
+     *
+     * @param ids 编号数组
+     */
+    void deleteStockOut(List<Long> ids);
+
+    /**
+     * 获得其它出库单
+     *
+     * @param id 编号
+     * @return 其它出库单
+     */
+    ErpStockOutDO getStockOut(Long id);
+
+    /**
+     * 获得其它出库单分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 其它出库单分页
+     */
+    PageResult<ErpStockOutDO> getStockOutPage(ErpStockOutPageReqVO pageReqVO);
+
+    // ==================== 出库项 ====================
+
+    /**
+     * 获得其它出库单项列表
+     *
+     * @param outId 出库编号
+     * @return 其它出库单项列表
+     */
+    List<ErpStockOutItemDO> getStockOutItemListByOutId(Long outId);
+
+    /**
+     * 获得其它出库单项 List
+     *
+     * @param outIds 出库编号数组
+     * @return 其它出库单项 List
+     */
+    List<ErpStockOutItemDO> getStockOutItemListByOutIds(Collection<Long> outIds);
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockOutServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockOutServiceImpl.java
new file mode 100644
index 000000000..9fa192465
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockOutServiceImpl.java
@@ -0,0 +1,228 @@
+package cn.iocoder.yudao.module.erp.service.stock;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out.ErpStockOutPageReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out.ErpStockOutSaveReqVO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutDO;
+import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockOutItemDO;
+import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpStockOutItemMapper;
+import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpStockOutMapper;
+import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
+import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
+import cn.iocoder.yudao.module.erp.enums.stock.ErpStockRecordBizTypeEnum;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
+import cn.iocoder.yudao.module.erp.service.sale.ErpCustomerService;
+import cn.iocoder.yudao.module.erp.service.stock.bo.ErpStockRecordCreateReqBO;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
+
+// TODO 芋艿:记录操作日志
+
+/**
+ * ERP 其它出库单 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class ErpStockOutServiceImpl implements ErpStockOutService {
+
+    @Resource
+    private ErpStockOutMapper stockOutMapper;
+    @Resource
+    private ErpStockOutItemMapper stockOutItemMapper;
+
+    @Resource
+    private ErpNoRedisDAO noRedisDAO;
+
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpWarehouseService warehouseService;
+    @Resource
+    private ErpCustomerService customerService;
+    @Resource
+    private ErpStockRecordService stockRecordService;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createStockOut(ErpStockOutSaveReqVO createReqVO) {
+        // 1.1 校验出库项的有效性
+        List<ErpStockOutItemDO> stockOutItems = validateStockOutItems(createReqVO.getItems());
+        // 1.2 校验客户
+        customerService.validateCustomer(createReqVO.getCustomerId());
+        // 1.3 生成出库单号,并校验唯一性
+        String no = noRedisDAO.generate(ErpNoRedisDAO.STOCK_OUT_NO_PREFIX);
+        if (stockOutMapper.selectByNo(no) != null) {
+            throw exception(STOCK_OUT_NO_EXISTS);
+        }
+
+        // 2.1 插入出库单
+        ErpStockOutDO stockOut = BeanUtils.toBean(createReqVO, ErpStockOutDO.class, in -> in
+                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
+                .setTotalCount(getSumValue(stockOutItems, ErpStockOutItemDO::getCount, BigDecimal::add))
+                .setTotalPrice(getSumValue(stockOutItems, ErpStockOutItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO)));
+        stockOutMapper.insert(stockOut);
+        // 2.2 插入出库单项
+        stockOutItems.forEach(o -> o.setOutId(stockOut.getId()));
+        stockOutItemMapper.insertBatch(stockOutItems);
+        return stockOut.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStockOut(ErpStockOutSaveReqVO updateReqVO) {
+        // 1.1 校验存在
+        ErpStockOutDO stockOut = validateStockOutExists(updateReqVO.getId());
+        if (ErpAuditStatus.APPROVE.getStatus().equals(stockOut.getStatus())) {
+            throw exception(STOCK_OUT_UPDATE_FAIL_APPROVE, stockOut.getNo());
+        }
+        // 1.2 校验客户
+        customerService.validateCustomer(updateReqVO.getCustomerId());
+        // 1.3 校验出库项的有效性
+        List<ErpStockOutItemDO> stockOutItems = validateStockOutItems(updateReqVO.getItems());
+
+        // 2.1 更新出库单
+        ErpStockOutDO updateObj = BeanUtils.toBean(updateReqVO, ErpStockOutDO.class, in -> in
+                .setTotalCount(getSumValue(stockOutItems, ErpStockOutItemDO::getCount, BigDecimal::add))
+                .setTotalPrice(getSumValue(stockOutItems, ErpStockOutItemDO::getTotalPrice, BigDecimal::add)));
+        stockOutMapper.updateById(updateObj);
+        // 2.2 更新出库单项
+        updateStockOutItemList(updateReqVO.getId(), stockOutItems);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStockOutStatus(Long id, Integer status) {
+        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
+        // 1.1 校验存在
+        ErpStockOutDO stockOut = validateStockOutExists(id);
+        // 1.2 校验状态
+        if (stockOut.getStatus().equals(status)) {
+            throw exception(approve ? STOCK_OUT_APPROVE_FAIL : STOCK_OUT_PROCESS_FAIL);
+        }
+
+        // 2. 更新状态
+        int updateCount = stockOutMapper.updateByIdAndStatus(id, stockOut.getStatus(),
+                new ErpStockOutDO().setStatus(status));
+        if (updateCount == 0) {
+            throw exception(approve ? STOCK_OUT_APPROVE_FAIL : STOCK_OUT_PROCESS_FAIL);
+        }
+
+        // 3. 变更库存
+        List<ErpStockOutItemDO> stockOutItems = stockOutItemMapper.selectListByOutId(id);
+        Integer bizType = approve ? ErpStockRecordBizTypeEnum.OTHER_OUT.getType()
+                : ErpStockRecordBizTypeEnum.OTHER_OUT_CANCEL.getType();
+        stockOutItems.forEach(stockOutItem -> {
+            BigDecimal count = approve ? stockOutItem.getCount().negate() : stockOutItem.getCount();
+            stockRecordService.createStockRecord(new ErpStockRecordCreateReqBO(
+                    stockOutItem.getProductId(), stockOutItem.getWarehouseId(), count,
+                    bizType, stockOutItem.getOutId(), stockOutItem.getId(), stockOut.getNo()));
+        });
+    }
+
+    private List<ErpStockOutItemDO> validateStockOutItems(List<ErpStockOutSaveReqVO.Item> list) {
+        // 1.1 校验产品存在
+        List<ErpProductDO> productList = productService.validProductList(
+                convertSet(list, ErpStockOutSaveReqVO.Item::getProductId));
+        Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
+        // 1.2 校验仓库存在
+        warehouseService.validWarehouseList(convertSet(list, ErpStockOutSaveReqVO.Item::getWarehouseId));
+        // 2. 转化为 ErpStockOutItemDO 列表
+        return convertList(list, o -> BeanUtils.toBean(o, ErpStockOutItemDO.class, item -> item
+                .setProductUnitId(productMap.get(item.getProductId()).getUnitId())
+                .setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()))));
+    }
+
+    private void updateStockOutItemList(Long id, List<ErpStockOutItemDO> newList) {
+        // 第一步,对比新老数据,获得添加、修改、删除的列表
+        List<ErpStockOutItemDO> oldList = stockOutItemMapper.selectListByOutId(id);
+        List<List<ErpStockOutItemDO>> diffList = diffList(oldList, newList, // id 不同,就认为是不同的记录
+                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
+
+        // 第二步,批量添加、修改、删除
+        if (CollUtil.isNotEmpty(diffList.get(0))) {
+            diffList.get(0).forEach(o -> o.setOutId(id));
+            stockOutItemMapper.insertBatch(diffList.get(0));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(1))) {
+            stockOutItemMapper.updateBatch(diffList.get(1));
+        }
+        if (CollUtil.isNotEmpty(diffList.get(2))) {
+            stockOutItemMapper.deleteBatchIds(convertList(diffList.get(2), ErpStockOutItemDO::getId));
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteStockOut(List<Long> ids) {
+        // 1. 校验不处于已审批
+        List<ErpStockOutDO> stockOuts = stockOutMapper.selectBatchIds(ids);
+        if (CollUtil.isEmpty(stockOuts)) {
+            return;
+        }
+        stockOuts.forEach(stockOut -> {
+            if (ErpAuditStatus.APPROVE.getStatus().equals(stockOut.getStatus())) {
+                throw exception(STOCK_OUT_DELETE_FAIL_APPROVE, stockOut.getNo());
+            }
+        });
+
+        // 2. 遍历删除,并记录操作日志
+        stockOuts.forEach(stockOut -> {
+            // 2.1 删除出库单
+            stockOutMapper.deleteById(stockOut.getId());
+            // 2.2 删除出库单项
+            stockOutItemMapper.deleteByOutId(stockOut.getId());
+        });
+    }
+
+    private ErpStockOutDO validateStockOutExists(Long id) {
+        ErpStockOutDO stockOut = stockOutMapper.selectById(id);
+        if (stockOut == null) {
+            throw exception(STOCK_OUT_NOT_EXISTS);
+        }
+        return stockOut;
+    }
+
+    @Override
+    public ErpStockOutDO getStockOut(Long id) {
+        return stockOutMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<ErpStockOutDO> getStockOutPage(ErpStockOutPageReqVO pageReqVO) {
+        return stockOutMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 出库项 ====================
+
+    @Override
+    public List<ErpStockOutItemDO> getStockOutItemListByOutId(Long outId) {
+        return stockOutItemMapper.selectListByOutId(outId);
+    }
+
+    @Override
+    public List<ErpStockOutItemDO> getStockOutItemListByOutIds(Collection<Long> outIds) {
+        if (CollUtil.isEmpty(outIds)) {
+            return Collections.emptyList();
+        }
+        return stockOutItemMapper.selectListByOutIds(outIds);
+    }
+
+}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockRecordService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockRecordService.java
index 08acb89b5..506b992a4 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockRecordService.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockRecordService.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.erp.service.stock;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.record.ErpStockRecordPageReqVO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockRecordDO;
+import cn.iocoder.yudao.module.erp.service.stock.bo.ErpStockRecordCreateReqBO;
+import jakarta.validation.Valid;
 
 /**
  * ERP 产品库存明细 Service 接口
@@ -27,4 +29,11 @@ public interface ErpStockRecordService {
      */
     PageResult<ErpStockRecordDO> getStockRecordPage(ErpStockRecordPageReqVO pageReqVO);
 
+    /**
+     * 创建库存明细
+     *
+     * @param createReqBO 创建库存明细 BO
+     */
+    void createStockRecord(@Valid ErpStockRecordCreateReqBO createReqBO);
+
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockRecordServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockRecordServiceImpl.java
index 6f50b91cc..d84951c28 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockRecordServiceImpl.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockRecordServiceImpl.java
@@ -1,13 +1,18 @@
 package cn.iocoder.yudao.module.erp.service.stock;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.record.ErpStockRecordPageReqVO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockRecordDO;
 import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpStockRecordMapper;
+import cn.iocoder.yudao.module.erp.service.stock.bo.ErpStockRecordCreateReqBO;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
+import java.math.BigDecimal;
+
 /**
  * ERP 产品库存明细 Service 实现类
  *
@@ -20,6 +25,9 @@ public class ErpStockRecordServiceImpl implements ErpStockRecordService {
     @Resource
     private ErpStockRecordMapper stockRecordMapper;
 
+    @Resource
+    private ErpStockService stockService;
+
     @Override
     public ErpStockRecordDO getStockRecord(Long id) {
         return stockRecordMapper.selectById(id);
@@ -30,4 +38,16 @@ public class ErpStockRecordServiceImpl implements ErpStockRecordService {
         return stockRecordMapper.selectPage(pageReqVO);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void createStockRecord(ErpStockRecordCreateReqBO createReqBO) {
+        // 1. 更新库存
+        BigDecimal totalCount = stockService.updateStockCountIncrement(
+                createReqBO.getProductId(), createReqBO.getWarehouseId(), createReqBO.getCount());
+        // 2. 创建库存明细
+        ErpStockRecordDO stockRecord = BeanUtils.toBean(createReqBO, ErpStockRecordDO.class)
+                .setTotalCount(totalCount);
+        stockRecordMapper.insert(stockRecord);
+    }
+
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockService.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockService.java
index f9cd26158..63ad5fefa 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockService.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockService.java
@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.stock.ErpStockPageReqVO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockDO;
 
+import java.math.BigDecimal;
+
 /**
  * ERP 产品库存 Service 接口
  *
@@ -28,6 +30,16 @@ public interface ErpStockService {
      */
     ErpStockDO getStock(Long productId, Long warehouseId);
 
+    /**
+     * 获得产品库存数量
+     *
+     * 如果不存在库存记录,则返回 0
+     *
+     * @param productId 产品编号
+     * @return 产品库存数量
+     */
+    BigDecimal getStockCount(Long productId);
+
     /**
      * 获得产品库存分页
      *
@@ -36,4 +48,14 @@ public interface ErpStockService {
      */
     PageResult<ErpStockDO> getStockPage(ErpStockPageReqVO pageReqVO);
 
+    /**
+     * 增量更新产品库存数量
+     *
+     * @param productId 产品编号
+     * @param warehouseId 仓库编号
+     * @param count 增量数量:正数,表示增加;负数,表示减少
+     * @return 更新后的库存
+     */
+    BigDecimal updateStockCountIncrement(Long productId, Long warehouseId, BigDecimal count);
+
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockServiceImpl.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockServiceImpl.java
index 423774265..654117047 100644
--- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockServiceImpl.java
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/ErpStockServiceImpl.java
@@ -4,10 +4,17 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.stock.ErpStockPageReqVO;
 import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpStockDO;
 import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpStockMapper;
+import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
+import java.math.BigDecimal;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.STOCK_COUNT_NEGATIVE;
+import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.STOCK_COUNT_NEGATIVE2;
+
 /**
  * ERP 产品库存 Service 实现类
  *
@@ -17,6 +24,18 @@ import org.springframework.validation.annotation.Validated;
 @Validated
 public class ErpStockServiceImpl implements ErpStockService {
 
+    /**
+     * 允许库存为负数
+     *
+     * TODO 芋艿:后续做成 db 配置
+     */
+    private static final Boolean NEGATIVE_STOCK_COUNT_ENABLE = false;
+
+    @Resource
+    private ErpProductService productService;
+    @Resource
+    private ErpWarehouseService warehouseService;
+
     @Resource
     private ErpStockMapper stockMapper;
 
@@ -30,9 +49,41 @@ public class ErpStockServiceImpl implements ErpStockService {
         return stockMapper.selectByProductIdAndWarehouseId(productId, warehouseId);
     }
 
+    @Override
+    public BigDecimal getStockCount(Long productId) {
+        BigDecimal count = stockMapper.selectSumByProductId(productId);
+        return count != null ? count : BigDecimal.ZERO;
+    }
+
     @Override
     public PageResult<ErpStockDO> getStockPage(ErpStockPageReqVO pageReqVO) {
         return stockMapper.selectPage(pageReqVO);
     }
 
+    @Override
+    public BigDecimal updateStockCountIncrement(Long productId, Long warehouseId, BigDecimal count) {
+        // 1.1 查询当前库存
+        ErpStockDO stock = stockMapper.selectByProductIdAndWarehouseId(productId, warehouseId);
+        if (stock == null) {
+            stock = new ErpStockDO().setProductId(productId).setWarehouseId(warehouseId).setCount(BigDecimal.ZERO);
+            stockMapper.insert(stock);
+        }
+        // 1.2 校验库存是否充足
+        if (!NEGATIVE_STOCK_COUNT_ENABLE && stock.getCount().add(count).compareTo(BigDecimal.ZERO) < 0) {
+            throw exception(STOCK_COUNT_NEGATIVE, productService.getProduct(productId).getName(),
+                    warehouseService.getWarehouse(warehouseId).getName(), stock.getCount(), count);
+        }
+
+        // 2. 库存变更
+        int updateCount = stockMapper.updateCountIncrement(stock.getId(), count, NEGATIVE_STOCK_COUNT_ENABLE);
+        if (updateCount == 0) {
+            // 此时不好去查询最新库存,所以直接抛出该提示,不提供具体库存数字
+            throw exception(STOCK_COUNT_NEGATIVE2, productService.getProduct(productId).getName(),
+                    warehouseService.getWarehouse(warehouseId).getName());
+        }
+
+        // 3. 返回最新库存
+        return stock.getCount().add(count);
+    }
+
 }
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/bo/ErpStockRecordCreateReqBO.java b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/bo/ErpStockRecordCreateReqBO.java
new file mode 100644
index 000000000..7006f10b5
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/service/stock/bo/ErpStockRecordCreateReqBO.java
@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.module.erp.service.stock.bo;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+/**
+ * 库存明细的创建 Request BO
+ *
+ * @author 芋道源码
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErpStockRecordCreateReqBO {
+
+    /**
+     * 产品编号
+     */
+    @NotNull(message = "产品编号不能为空")
+    private Long productId;
+    /**
+     * 仓库编号
+     */
+    @NotNull(message = "仓库编号不能为空")
+    private Long warehouseId;
+    /**
+     * 出入库数量
+     *
+     * 正数,表示入库;负数,表示出库
+     */
+    @NotNull(message = "出入库数量不能为空")
+    private BigDecimal count;
+
+    /**
+     * 业务类型
+     */
+    @NotNull(message = "业务类型不能为空")
+    private Integer bizType;
+    /**
+     * 业务编号
+     */
+    @NotNull(message = "业务编号不能为空")
+    private Long bizId;
+    /**
+     * 业务项编号
+     */
+    @NotNull(message = "业务项编号不能为空")
+    private Long bizItemId;
+    /**
+     * 业务单号
+     */
+    @NotNull(message = "业务单号不能为空")
+    private String bizNo;
+
+}
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/resources/mapper/finance/ErpAccountMapper.xml b/yudao-module-erp/yudao-module-erp-biz/src/main/resources/mapper/finance/ErpAccountMapper.xml
new file mode 100644
index 000000000..a3cf6877b
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/resources/mapper/finance/ErpAccountMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpAccountMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/resources/mapper/sale/ErpCustomerMapper.xml b/yudao-module-erp/yudao-module-erp-biz/src/main/resources/mapper/sale/ErpCustomerMapper.xml
new file mode 100644
index 000000000..c012f4467
--- /dev/null
+++ b/yudao-module-erp/yudao-module-erp-biz/src/main/resources/mapper/sale/ErpCustomerMapper.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpCustomerMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/product/ErpProductUnitServiceImplTest.java b/yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/product/ErpProductUnitServiceImplTest.java
deleted file mode 100644
index c4023df0e..000000000
--- a/yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/product/ErpProductUnitServiceImplTest.java
+++ /dev/null
@@ -1,131 +0,0 @@
-package cn.iocoder.yudao.module.erp.service.product;
-
-import cn.iocoder.yudao.module.erp.controller.admin.product.vo.unit.ErpProductUnitPageReqVO;
-import cn.iocoder.yudao.module.erp.controller.admin.product.vo.unit.ErpProductUnitSaveReqVO;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-
-import jakarta.annotation.Resource;
-
-import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-
-import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductUnitDO;
-import cn.iocoder.yudao.module.erp.dal.mysql.product.ErpProductUnitMapper;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import org.springframework.context.annotation.Import;
-
-import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
-import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
-import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * {@link ErpProductUnitServiceImpl} 的单元测试类
- *
- * @author 芋道源码
- */
-@Import(ErpProductUnitServiceImpl.class)
-public class ErpProductUnitServiceImplTest extends BaseDbUnitTest {
-
-    @Resource
-    private ErpProductUnitServiceImpl productUnitService;
-
-    @Resource
-    private ErpProductUnitMapper productUnitMapper;
-
-    @Test
-    public void testCreateProductUnit_success() {
-        // 准备参数
-        ErpProductUnitSaveReqVO createReqVO = randomPojo(ErpProductUnitSaveReqVO.class).setId(null);
-
-        // 调用
-        Long productUnitId = productUnitService.createProductUnit(createReqVO);
-        // 断言
-        assertNotNull(productUnitId);
-        // 校验记录的属性是否正确
-        ErpProductUnitDO productUnit = productUnitMapper.selectById(productUnitId);
-        assertPojoEquals(createReqVO, productUnit, "id");
-    }
-
-    @Test
-    public void testUpdateProductUnit_success() {
-        // mock 数据
-        ErpProductUnitDO dbProductUnit = randomPojo(ErpProductUnitDO.class);
-        productUnitMapper.insert(dbProductUnit);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        ErpProductUnitSaveReqVO updateReqVO = randomPojo(ErpProductUnitSaveReqVO.class, o -> {
-            o.setId(dbProductUnit.getId()); // 设置更新的 ID
-        });
-
-        // 调用
-        productUnitService.updateProductUnit(updateReqVO);
-        // 校验是否更新正确
-        ErpProductUnitDO productUnit = productUnitMapper.selectById(updateReqVO.getId()); // 获取最新的
-        assertPojoEquals(updateReqVO, productUnit);
-    }
-
-    @Test
-    public void testUpdateProductUnit_notExists() {
-        // 准备参数
-        ErpProductUnitSaveReqVO updateReqVO = randomPojo(ErpProductUnitSaveReqVO.class);
-
-        // 调用, 并断言异常
-        assertServiceException(() -> productUnitService.updateProductUnit(updateReqVO), PRODUCT_UNIT_NOT_EXISTS);
-    }
-
-    @Test
-    public void testDeleteProductUnit_success() {
-        // mock 数据
-        ErpProductUnitDO dbProductUnit = randomPojo(ErpProductUnitDO.class);
-        productUnitMapper.insert(dbProductUnit);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        Long id = dbProductUnit.getId();
-
-        // 调用
-        productUnitService.deleteProductUnit(id);
-       // 校验数据不存在了
-       assertNull(productUnitMapper.selectById(id));
-    }
-
-    @Test
-    public void testDeleteProductUnit_notExists() {
-        // 准备参数
-        Long id = randomLongId();
-
-        // 调用, 并断言异常
-        assertServiceException(() -> productUnitService.deleteProductUnit(id), PRODUCT_UNIT_NOT_EXISTS);
-    }
-
-    @Test
-    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
-    public void testGetProductUnitPage() {
-       // mock 数据
-       ErpProductUnitDO dbProductUnit = randomPojo(ErpProductUnitDO.class, o -> { // 等会查询到
-           o.setName(null);
-           o.setStatus(null);
-           o.setCreateTime(null);
-       });
-       productUnitMapper.insert(dbProductUnit);
-       // 测试 name 不匹配
-       productUnitMapper.insert(cloneIgnoreId(dbProductUnit, o -> o.setName(null)));
-       // 测试 status 不匹配
-       productUnitMapper.insert(cloneIgnoreId(dbProductUnit, o -> o.setStatus(null)));
-       // 测试 createTime 不匹配
-       productUnitMapper.insert(cloneIgnoreId(dbProductUnit, o -> o.setCreateTime(null)));
-       // 准备参数
-       ErpProductUnitPageReqVO reqVO = new ErpProductUnitPageReqVO();
-       reqVO.setName(null);
-       reqVO.setStatus(null);
-       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
-
-       // 调用
-       PageResult<ErpProductUnitDO> pageResult = productUnitService.getProductUnitPage(reqVO);
-       // 断言
-       assertEquals(1, pageResult.getTotal());
-       assertEquals(1, pageResult.getList().size());
-       assertPojoEquals(dbProductUnit, pageResult.getList().get(0));
-    }
-
-}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/product/ProductServiceImplTest.java b/yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/product/ProductServiceImplTest.java
deleted file mode 100644
index 5045dbfb0..000000000
--- a/yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/product/ProductServiceImplTest.java
+++ /dev/null
@@ -1,131 +0,0 @@
-package cn.iocoder.yudao.module.erp.service.product;
-
-import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductPageReqVO;
-import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ProductSaveReqVO;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-
-import jakarta.annotation.Resource;
-
-import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-
-import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
-import cn.iocoder.yudao.module.erp.dal.mysql.product.ErpProductMapper;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import org.springframework.context.annotation.Import;
-
-import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
-import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
-import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * {@link ErpProductServiceImpl} 的单元测试类
- *
- * @author 芋道源码
- */
-@Import(ErpProductServiceImpl.class)
-public class ProductServiceImplTest extends BaseDbUnitTest {
-
-    @Resource
-    private ErpProductServiceImpl productService;
-
-    @Resource
-    private ErpProductMapper productMapper;
-
-    @Test
-    public void testCreateProduct_success() {
-        // 准备参数
-        ProductSaveReqVO createReqVO = randomPojo(ProductSaveReqVO.class).setId(null);
-
-        // 调用
-        Long productId = productService.createProduct(createReqVO);
-        // 断言
-        assertNotNull(productId);
-        // 校验记录的属性是否正确
-        ErpProductDO product = productMapper.selectById(productId);
-        assertPojoEquals(createReqVO, product, "id");
-    }
-
-    @Test
-    public void testUpdateProduct_success() {
-        // mock 数据
-        ErpProductDO dbProduct = randomPojo(ErpProductDO.class);
-        productMapper.insert(dbProduct);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        ProductSaveReqVO updateReqVO = randomPojo(ProductSaveReqVO.class, o -> {
-            o.setId(dbProduct.getId()); // 设置更新的 ID
-        });
-
-        // 调用
-        productService.updateProduct(updateReqVO);
-        // 校验是否更新正确
-        ErpProductDO product = productMapper.selectById(updateReqVO.getId()); // 获取最新的
-        assertPojoEquals(updateReqVO, product);
-    }
-
-    @Test
-    public void testUpdateProduct_notExists() {
-        // 准备参数
-        ProductSaveReqVO updateReqVO = randomPojo(ProductSaveReqVO.class);
-
-        // 调用, 并断言异常
-        assertServiceException(() -> productService.updateProduct(updateReqVO), PRODUCT_NOT_EXISTS);
-    }
-
-    @Test
-    public void testDeleteProduct_success() {
-        // mock 数据
-        ErpProductDO dbProduct = randomPojo(ErpProductDO.class);
-        productMapper.insert(dbProduct);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        Long id = dbProduct.getId();
-
-        // 调用
-        productService.deleteProduct(id);
-       // 校验数据不存在了
-       assertNull(productMapper.selectById(id));
-    }
-
-    @Test
-    public void testDeleteProduct_notExists() {
-        // 准备参数
-        Long id = randomLongId();
-
-        // 调用, 并断言异常
-        assertServiceException(() -> productService.deleteProduct(id), PRODUCT_NOT_EXISTS);
-    }
-
-    @Test
-    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
-    public void testGetProductPage() {
-       // mock 数据
-       ErpProductDO dbProduct = randomPojo(ErpProductDO.class, o -> { // 等会查询到
-           o.setName(null);
-           o.setCategoryId(null);
-           o.setCreateTime(null);
-       });
-       productMapper.insert(dbProduct);
-       // 测试 name 不匹配
-       productMapper.insert(cloneIgnoreId(dbProduct, o -> o.setName(null)));
-       // 测试 categoryId 不匹配
-       productMapper.insert(cloneIgnoreId(dbProduct, o -> o.setCategoryId(null)));
-       // 测试 createTime 不匹配
-       productMapper.insert(cloneIgnoreId(dbProduct, o -> o.setCreateTime(null)));
-       // 准备参数
-       ErpProductPageReqVO reqVO = new ErpProductPageReqVO();
-       reqVO.setName(null);
-       reqVO.setCategoryId(null);
-       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
-
-       // 调用
-       PageResult<ErpProductDO> pageResult = productService.getProductVOPage(reqVO);
-       // 断言
-       assertEquals(1, pageResult.getTotal());
-       assertEquals(1, pageResult.getList().size());
-       assertPojoEquals(dbProduct, pageResult.getList().get(0));
-    }
-
-}
\ No newline at end of file
diff --git a/yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/stock/ErpWarehouseServiceImplTest.java b/yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/stock/ErpWarehouseServiceImplTest.java
deleted file mode 100644
index fa74620e2..000000000
--- a/yudao-module-erp/yudao-module-erp-biz/src/test/java/cn/iocoder/yudao/module/erp/service/stock/ErpWarehouseServiceImplTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package cn.iocoder.yudao.module.erp.service.stock;
-
-import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.warehouse.ErpWarehousePageReqVO;
-import cn.iocoder.yudao.module.erp.controller.admin.stock.vo.warehouse.ErpWarehouseSaveReqVO;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-
-import jakarta.annotation.Resource;
-
-import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-
-import cn.iocoder.yudao.module.erp.dal.dataobject.stock.ErpWarehouseDO;
-import cn.iocoder.yudao.module.erp.dal.mysql.stock.ErpWarehouseMapper;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import org.springframework.context.annotation.Import;
-
-import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
-import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * {@link ErpWarehouseServiceImpl} 的单元测试类
- *
- * @author 芋道源码
- */
-@Import(ErpWarehouseServiceImpl.class)
-public class ErpWarehouseServiceImplTest extends BaseDbUnitTest {
-
-    @Resource
-    private ErpWarehouseServiceImpl warehouseService;
-
-    @Resource
-    private ErpWarehouseMapper warehouseMapper;
-
-    @Test
-    public void testCreateWarehouse_success() {
-        // 准备参数
-        ErpWarehouseSaveReqVO createReqVO = randomPojo(ErpWarehouseSaveReqVO.class).setId(null);
-
-        // 调用
-        Long warehouseId = warehouseService.createWarehouse(createReqVO);
-        // 断言
-        assertNotNull(warehouseId);
-        // 校验记录的属性是否正确
-        ErpWarehouseDO warehouse = warehouseMapper.selectById(warehouseId);
-        assertPojoEquals(createReqVO, warehouse, "id");
-    }
-
-    @Test
-    public void testUpdateWarehouse_success() {
-        // mock 数据
-        ErpWarehouseDO dbWarehouse = randomPojo(ErpWarehouseDO.class);
-        warehouseMapper.insert(dbWarehouse);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        ErpWarehouseSaveReqVO updateReqVO = randomPojo(ErpWarehouseSaveReqVO.class, o -> {
-            o.setId(dbWarehouse.getId()); // 设置更新的 ID
-        });
-
-        // 调用
-        warehouseService.updateWarehouse(updateReqVO);
-        // 校验是否更新正确
-        ErpWarehouseDO warehouse = warehouseMapper.selectById(updateReqVO.getId()); // 获取最新的
-        assertPojoEquals(updateReqVO, warehouse);
-    }
-
-    @Test
-    public void testUpdateWarehouse_notExists() {
-        // 准备参数
-        ErpWarehouseSaveReqVO updateReqVO = randomPojo(ErpWarehouseSaveReqVO.class);
-
-        // 调用, 并断言异常
-        assertServiceException(() -> warehouseService.updateWarehouse(updateReqVO), WAREHOUSE_NOT_EXISTS);
-    }
-
-    @Test
-    public void testDeleteWarehouse_success() {
-        // mock 数据
-        ErpWarehouseDO dbWarehouse = randomPojo(ErpWarehouseDO.class);
-        warehouseMapper.insert(dbWarehouse);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        Long id = dbWarehouse.getId();
-
-        // 调用
-        warehouseService.deleteWarehouse(id);
-       // 校验数据不存在了
-       assertNull(warehouseMapper.selectById(id));
-    }
-
-    @Test
-    public void testDeleteWarehouse_notExists() {
-        // 准备参数
-        Long id = randomLongId();
-
-        // 调用, 并断言异常
-        assertServiceException(() -> warehouseService.deleteWarehouse(id), WAREHOUSE_NOT_EXISTS);
-    }
-
-    @Test
-    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
-    public void testGetWarehousePage() {
-       // mock 数据
-       ErpWarehouseDO dbWarehouse = randomPojo(ErpWarehouseDO.class, o -> { // 等会查询到
-           o.setName(null);
-           o.setStatus(null);
-       });
-       warehouseMapper.insert(dbWarehouse);
-       // 测试 name 不匹配
-       warehouseMapper.insert(cloneIgnoreId(dbWarehouse, o -> o.setName(null)));
-       // 测试 status 不匹配
-       warehouseMapper.insert(cloneIgnoreId(dbWarehouse, o -> o.setStatus(null)));
-       // 准备参数
-       ErpWarehousePageReqVO reqVO = new ErpWarehousePageReqVO();
-       reqVO.setName(null);
-       reqVO.setStatus(null);
-
-       // 调用
-       PageResult<ErpWarehouseDO> pageResult = warehouseService.getWarehousePage(reqVO);
-       // 断言
-       assertEquals(1, pageResult.getTotal());
-       assertEquals(1, pageResult.getList().size());
-       assertPojoEquals(dbWarehouse, pageResult.getList().get(0));
-    }
-
-}
\ No newline at end of file
diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java
index b6cab3030..cea3c3663 100644
--- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java
+++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java
@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -65,6 +66,17 @@ public interface AdminUserApi {
         return CollectionUtils.convertMap(users, AdminUserRespDTO::getId);
     }
 
+    /**
+     * 校验用户是否有效。如下情况,视为无效:
+     * 1. 用户编号不存在
+     * 2. 用户被禁用
+     *
+     * @param id 用户编号
+     */
+    default void validateUser(Long id) {
+        validateUserList(Collections.singleton(id));
+    }
+
     /**
      * 校验用户们是否有效。如下情况,视为无效:
      * 1. 用户编号不存在