Commit 2f4dfb69ee877e09ff6aff835624321f922fbf5c

Authored by YunaiV
1 parent b1e4a6df

✨ feat(transaction): 添加库存事务管理逻辑及相关 DTO

新增库存事务类型枚举及服务接口,支持创建和批量创建库存事务。更新库存数量时增加校验逻辑,确保库存充足。重构相关服务以支持新功能,提升库存管理的灵活性和准确性。
Showing 18 changed files with 444 additions and 92 deletions
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/transaction/MesWmTransactionDO.java
... ... @@ -40,16 +40,12 @@ public class MesWmTransactionDO extends BaseDO {
40 40  
41 41 // ==================== 业务归属 ====================
42 42  
43   - // codex reply:`transactionType` 收敛成 `type` 是对的,和项目里其他 DO 的命名保持一致。
44   - // codex reply:不过这类改名要连同 `MesWmTransactionSaveReqVO`、Service 和查询条件一起同步,不然 DO 与上下游模型会割裂。
45 43 /**
46 44 * 事务类型
47 45 *
48 46 * 枚举 {@link MesWmTransactionTypeEnum}
49 47 */
50 48 private Integer type;
51   - // codex reply:`transactionQuantity` 收敛成 `quantity` 是对的;在事务流水这个上下文里,`transaction` 前缀没有额外信息。
52   - // codex reply:`transactionFlag` 删除也合理,直接存带符号数量更符合当前写入逻辑,后续聚合查询也可以直接 `SUM(quantity)`。
53 49 /**
54 50 * 本次变动数量
55 51 *
... ... @@ -109,7 +105,6 @@ public class MesWmTransactionDO extends BaseDO {
109 105 * 关联 {@link MesWmBatchDO#getCode()}
110 106 */
111 107 private String batchCode;
112   - // codex reply:`packageId` 当前删除是合理的。事务流水链路没有包装维度的输入输出,先不保留“预留字段”更干净。
113 108  
114 109 // ==================== 库存位置 ====================
115 110  
... ... @@ -134,8 +129,6 @@ public class MesWmTransactionDO extends BaseDO {
134 129  
135 130 // ==================== 时间 ====================
136 131  
137   - // codex reply:这里我不建议继续沿用 `erpTime` / `receiptTime` 这组命名;项目里的同类字段基本统一为 `receiptDate`,单独改成 `Time` 会破坏一致性。
138   - // codex reply:如果这两个字段暂时没有业务赋值,建议宁可先删;若后续保留,ERP 侧更建议 `erpAccountDate`,收货侧保持 `receiptDate`。
139 132 /**
140 133 * 事务发生时间
141 134 */
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java
... ... @@ -20,6 +20,10 @@ import java.util.List;
20 20 @Mapper
21 21 public interface MesWmBatchMapper extends BaseMapperX<MesWmBatchDO> {
22 22  
  23 + default MesWmBatchDO selectByCode(String code) {
  24 + return selectOne(MesWmBatchDO:: getCode, code);
  25 + }
  26 +
23 27 default PageResult<MesWmBatchDO> selectPage(MesWmBatchPageReqVO reqVO) {
24 28 return selectPage(reqVO, new LambdaQueryWrapperX<MesWmBatchDO>()
25 29 .likeIfPresent(MesWmBatchDO::getCode, reqVO.getCode())
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java
... ... @@ -51,15 +51,16 @@ public interface MesWmMaterialStockMapper extends BaseMapperX&lt;MesWmMaterialStock
51 51 /**
52 52 * 增量更新库存数量
53 53 *
54   - * @param id 库存记录编号
55   - * @param count 变动数量(正数=增加,负数=扣减)
56   - * @return 影响行数(扣减时为 0 表示库存不足)
  54 + * @param id 库存记录编号
  55 + * @param count 变动数量(正数=增加,负数=扣减)
  56 + * @param checkFlag 是否校验库存充足(为 true 时扣减不允许变为负数)
  57 + * @return 影响行数
57 58 */
58   - default int updateQuantity(Long id, BigDecimal count) {
  59 + default int updateQuantity(Long id, BigDecimal count, boolean checkFlag) {
59 60 LambdaUpdateWrapper<MesWmMaterialStockDO> updateWrapper = new LambdaUpdateWrapper<MesWmMaterialStockDO>()
60 61 .eq(MesWmMaterialStockDO::getId, id)
61 62 .setSql("quantity = quantity + " + count);
62   - if (count.compareTo(BigDecimal.ZERO) < 0) {
  63 + if (checkFlag && count.compareTo(BigDecimal.ZERO) < 0) {
63 64 updateWrapper.ge(MesWmMaterialStockDO::getQuantity, count.abs()); // CAS 防负库存
64 65 }
65 66 return update(null, updateWrapper);
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
... ... @@ -355,7 +355,7 @@ public interface ErrorCodeConstants {
355 355 // ========== MES 仓库管理-库存(1-040-703-000) ==========
356 356 ErrorCode WM_MATERIAL_STOCK_NOT_EXISTS = new ErrorCode(1_040_703_000, "库存记录不存在");
357 357 ErrorCode WM_MATERIAL_STOCK_INSUFFICIENT = new ErrorCode(1_040_703_001, "库存数量不足");
358   - ErrorCode WM_TRANSACTION_MATERIAL_STOCK_REQUIRED = new ErrorCode(1_040_703_002, "出库事务必须指定库存记录 ID");
  358 + ErrorCode WM_TRANSACTION_TYPE_NOT_EXISTS = new ErrorCode(1_040_703_002, "库存事务类型不存在");
359 359  
360 360 // ========== MES 仓库管理-到货通知单(1-040-704-000) ==========
361 361 ErrorCode WM_ARRIVAL_NOTICE_NOT_EXISTS = new ErrorCode(1_040_704_000, "到货通知单不存在");
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesBizTypeConstants.java
... ... @@ -29,6 +29,21 @@ public final class MesBizTypeConstants {
29 29 public static final int WM_PACKAGE = 105; // 装箱单:MesWmPackageDO
30 30 public static final int WM_STOCK = 106; // 库存:MesWmMaterialStockDO
31 31 public static final int WM_BATCH = 107; // 批次:MesWmBatchDO
  32 + public static final int WM_TRANSACTION = 108; // 库存事务流水:MesWmTransactionDO
  33 + public static final int WM_ITEM_RECEIPT_IN = 110; // 采购入库:MesWmItemReceiptDO
  34 + public static final int WM_TRANSFER_OUT = 111; // 调拨出库:MesWmTransferDO
  35 + public static final int WM_TRANSFER_IN = 112; // 调拨入库:MesWmTransferDO
  36 + public static final int WM_MISC_ISSUE = 113; // 其他出库:MesWmMiscIssueDO
  37 + public static final int WM_MISC_RECPT = 114; // 其他入库:MesWmMiscReceiptDO
  38 + public static final int WM_ISSUE = 115; // 生产领料:MesWmProductionIssueDO
  39 + public static final int WM_RT_ISSUE = 116; // 生产退料:MesWmReturnIssueDO
  40 + public static final int WM_PRODUCT_RECPT = 117; // 生产入库:MesWmProductReceiptDO
  41 + public static final int WM_PRODUCT_SALES = 118; // 销售出库:MesWmProductSalesDO
  42 + public static final int WM_RT_SALES = 119; // 销售退货入库:MesWmReturnSalesDO
  43 + public static final int WM_OUTSOURCE_ISSUE = 120; // 外协发料:MesWmOutsourceIssueDO
  44 + public static final int WM_OUTSOURCE_RECPT = 121; // 外协入库:MesWmOutsourceReceiptDO
  45 + public static final int WM_ITEM_CONSUME = 122; // 生产消耗:MesWmItemConsumeDO
  46 + public static final int WM_PRODUCT_PRODUCE = 123; // 产品产出:MesWmProductProduceDO
32 47  
33 48 // ========== CAL 排班模块 [200, 300) ==========
34 49  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmTransactionTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.enums.wm;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  6 +import lombok.AllArgsConstructor;
  7 +import lombok.Getter;
  8 +
  9 +import java.util.Arrays;
  10 +
  11 +/**
  12 + * MES 库存事务类型枚举
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@Getter
  17 +@AllArgsConstructor
  18 +public enum MesWmTransactionTypeEnum implements ArrayValuable<Integer> {
  19 +
  20 + /**
  21 + * 入库
  22 + */
  23 + IN(1, "入库", true),
  24 + /**
  25 + * 出库
  26 + */
  27 + OUT(2, "出库", false),
  28 + /**
  29 + * 调拨移出
  30 + */
  31 + MOVE_OUT(3, "调拨移出", false),
  32 + /**
  33 + * 调拨移入
  34 + */
  35 + MOVE_IN(4, "调拨移入", true),
  36 + /**
  37 + * 盘盈
  38 + */
  39 + ADJUST_IN(5, "盘盈", true),
  40 + /**
  41 + * 盘亏
  42 + */
  43 + ADJUST_OUT(6, "盘亏", false);
  44 +
  45 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesWmTransactionTypeEnum::getType).toArray(Integer[]::new);
  46 +
  47 + /**
  48 + * 类型值
  49 + */
  50 + private final Integer type;
  51 + /**
  52 + * 类型名
  53 + */
  54 + private final String name;
  55 + /**
  56 + * 是否为入库方向
  57 + */
  58 + private final boolean inbound;
  59 +
  60 + public static MesWmTransactionTypeEnum valueOf(Integer type) {
  61 + return CollUtil.findOne(CollUtil.newArrayList(values()),
  62 + e -> ObjUtil.equal(e.getType(), type));
  63 + }
  64 +
  65 + @Override
  66 + public Integer[] array() {
  67 + return ARRAYS;
  68 + }
  69 +
  70 +}
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java
... ... @@ -23,6 +23,22 @@ public interface MesWmBatchService {
23 23 PageResult<MesWmBatchDO> getBatchPage(MesWmBatchPageReqVO pageReqVO);
24 24  
25 25 /**
  26 + * 获得批次
  27 + *
  28 + * @param id 批次编号
  29 + * @return 批次记录
  30 + */
  31 + MesWmBatchDO getBatch(Long id);
  32 +
  33 + /**
  34 + * 根据批次号获得批次
  35 + *
  36 + * @param code 批次号
  37 + * @return 批次记录
  38 + */
  39 + MesWmBatchDO getBatchByCode(String code);
  40 +
  41 + /**
26 42 * 获取或生成批次编码
27 43 * <p>
28 44 * 根据物料批次配置,查询或生成批次记录
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java
... ... @@ -54,6 +54,16 @@ public class MesWmBatchServiceImpl implements MesWmBatchService {
54 54 }
55 55  
56 56 @Override
  57 + public MesWmBatchDO getBatch(Long id) {
  58 + return batchMapper.selectById(id);
  59 + }
  60 +
  61 + @Override
  62 + public MesWmBatchDO getBatchByCode(String code) {
  63 + return batchMapper.selectByCode(code);
  64 + }
  65 +
  66 + @Override
57 67 @Transactional(rollbackFor = Exception.class)
58 68 public MesWmBatchDO getOrGenerateBatchCode(MesWmBatchGenerateReqVO reqVO) {
59 69 // 1.1 查询物料信息,检查是否启用批次管理
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java
... ... @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterial
8 8 import jakarta.validation.Valid;
9 9  
10 10 import java.math.BigDecimal;
  11 +import java.time.LocalDateTime;
11 12 import java.util.Collection;
12 13 import java.util.List;
13 14 import java.util.Map;
... ... @@ -28,14 +29,6 @@ public interface MesWmMaterialStockService {
28 29 MesWmMaterialStockDO getMaterialStock(Long id);
29 30  
30 31 /**
31   - * 校验库存记录存在
32   - *
33   - * @param id 编号
34   - * @return 库存记录
35   - */
36   - MesWmMaterialStockDO validateMaterialStockExists(Long id);
37   -
38   - /**
39 32 * 获得库存台账分页
40 33 *
41 34 * @param pageReqVO 分页参数
... ... @@ -95,31 +88,27 @@ public interface MesWmMaterialStockService {
95 88 }
96 89  
97 90 /**
98   - * 增加库存
99   - *
100   - * 按 itemId + warehouseId + locationId + areaId + batchId 查找已有库存,
101   - * 存在则增加 quantity;不存在则新建记录。
  91 + * 获取或创建库存记录(按组合键唯一)
102 92 *
103 93 * @param itemId 物料编号
104 94 * @param warehouseId 仓库编号
105 95 * @param locationId 库区编号
106 96 * @param areaId 库位编号
107 97 * @param batchId 批次编号
108   - * @param quantity 增加数量
109 98 * @param vendorId 供应商编号
  99 + * @param receiptTime 入库时间(为空则默认当前时间)
  100 + * @return 库存记录编号
110 101 */
111   - void increaseStock(Long itemId, Long warehouseId, Long locationId, Long areaId,
112   - Long batchId, BigDecimal quantity, Long vendorId);
  102 + Long getOrCreateMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId,
  103 + Long batchId, Long vendorId, LocalDateTime receiptTime);
113 104  
114 105 /**
115   - * 扣减库存
116   - *
117   - * 根据库存记录 ID 直接扣减数量。
  106 + * 更新库存数量
118 107 *
119   - * @param materialStockId 库存记录编号
120   - * @param quantity 扣减数量(正数)
121   - * @param storageCheckFlag 是否校验库存充足
  108 + * @param materialStockId 库存记录编号
  109 + * @param quantity 变动数量(正数=增加,负数=扣减)
  110 + * @param checkFlag 是否校验库存充足(为 true 且扣减后为负则报错)
122 111 */
123   - void decreaseStock(Long materialStockId, BigDecimal quantity, boolean storageCheckFlag);
  112 + void updateMaterialStockQuantity(Long materialStockId, BigDecimal quantity, boolean checkFlag);
124 113  
125 114 }
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java
... ... @@ -46,7 +46,6 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService
46 46 return materialStockMapper.selectById(id);
47 47 }
48 48  
49   - @Override
50 49 public MesWmMaterialStockDO validateMaterialStockExists(Long id) {
51 50 MesWmMaterialStockDO stock = materialStockMapper.selectById(id);
52 51 if (stock == null) {
... ... @@ -79,11 +78,10 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService
79 78 public void updateMaterialStockFrozen(MesWmMaterialStockFreezeReqVO updateReqVO) {
80 79 // 校验存在
81 80 validateMaterialStockExists(updateReqVO.getId());
  81 +
82 82 // 更新冻结状态
83   - MesWmMaterialStockDO updateObj = new MesWmMaterialStockDO();
84   - updateObj.setId(updateReqVO.getId());
85   - updateObj.setFrozenFlag(updateReqVO.getFrozenFlag());
86   - materialStockMapper.updateById(updateObj);
  83 + materialStockMapper.updateById(new MesWmMaterialStockDO()
  84 + .setId(updateReqVO.getId()).setFrozenFlag(updateReqVO.getFrozenFlag()));
87 85 }
88 86  
89 87 @Override
... ... @@ -115,37 +113,34 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService
115 113 }
116 114  
117 115 @Override
118   - public void increaseStock(Long itemId, Long warehouseId, Long locationId, Long areaId,
119   - Long batchId, BigDecimal quantity, Long vendorId) {
  116 + public Long getOrCreateMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId,
  117 + Long batchId, Long vendorId, LocalDateTime receiptTime) {
120 118 // 1. 查找已有库存记录
121 119 MesWmMaterialStockDO stock = materialStockMapper.selectByCompositeKey(
122 120 itemId, warehouseId, locationId, areaId, batchId);
123   -
124   - // 2a. 存在则增加数量
125 121 if (stock != null) {
126   - materialStockMapper.updateQuantity(stock.getId(), quantity);
127   - return;
  122 + return stock.getId();
128 123 }
129 124  
130   - // 2b. 不存在则新建
  125 + // 2. 不存在则新建
131 126 MesMdItemDO item = itemService.validateItemExists(itemId);
132 127 MesWmMaterialStockDO newStock = MesWmMaterialStockDO.builder()
133 128 .itemId(itemId).itemTypeId(item.getItemTypeId())
134 129 .warehouseId(warehouseId).locationId(locationId).areaId(areaId)
135   - .batchId(batchId).vendorId(vendorId).quantity(quantity)
136   - .receiptTime(LocalDateTime.now()).frozenFlag(false)
  130 + .batchId(batchId).vendorId(vendorId)
  131 + .quantity(BigDecimal.ZERO) // 初始数量为 0,由 updateMaterialStockQuantity 更新
  132 + .receiptTime(receiptTime != null ? receiptTime : LocalDateTime.now())
  133 + .frozenFlag(false)
137 134 .build();
138 135 materialStockMapper.insert(newStock);
  136 + return newStock.getId();
139 137 }
140 138  
141 139 @Override
142   - public void decreaseStock(Long materialStockId, BigDecimal quantity, boolean storageCheckFlag) {
143   - // 1. 校验库存记录存在
144   - validateMaterialStockExists(materialStockId);
145   -
146   - // 2. 扣减库存(Mapper 层 CAS 防负库存)
147   - int updatedRows = materialStockMapper.updateQuantity(materialStockId, quantity.negate());
148   - if (storageCheckFlag && updatedRows == 0) {
  140 + public void updateMaterialStockQuantity(Long materialStockId, BigDecimal quantity, boolean checkFlag) {
  141 + // 更新库存数量(Mapper 层 CAS 防负库存;checkFlag=false 时允许负数)
  142 + int updatedRows = materialStockMapper.updateQuantity(materialStockId, quantity, checkFlag);
  143 + if (updatedRows == 0) {
149 144 throw exception(WM_MATERIAL_STOCK_INSUFFICIENT);
150 145 }
151 146 }
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/miscissue/MesWmMiscIssueServiceImpl.java
... ... @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.mes.service.wm.miscissue;
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ObjUtil;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6   -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
7 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
8 7 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
9 8 import cn.iocoder.yudao.module.mes.controller.admin.wm.miscissue.vo.MesWmMiscIssuePageReqVO;
... ... @@ -12,17 +11,20 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.miscissue.MesWmMiscIssueDO;
12 11 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.miscissue.MesWmMiscIssueLineDO;
13 12 import cn.iocoder.yudao.module.mes.dal.mysql.wm.miscissue.MesWmMiscIssueMapper;
14 13 import cn.iocoder.yudao.module.mes.enums.wm.MesWmMiscIssueStatusEnum;
15   -import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
  14 +import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
  15 +import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
  16 +import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
  17 +import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
16 18 import jakarta.annotation.Resource;
17 19 import lombok.extern.slf4j.Slf4j;
18 20 import org.springframework.stereotype.Service;
19 21 import org.springframework.transaction.annotation.Transactional;
20 22 import org.springframework.validation.annotation.Validated;
21 23  
22   -import java.math.BigDecimal;
23 24 import java.util.List;
24 25  
25 26 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  27 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
26 28 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
27 29  
28 30 /**
... ... @@ -38,6 +40,8 @@ public class MesWmMiscIssueServiceImpl implements MesWmMiscIssueService {
38 40  
39 41 @Resource
40 42 private MesWmMiscIssueLineService miscIssueLineService;
  43 + @Resource
  44 + private MesWmTransactionService wmTransactionService;
41 45  
42 46 @Override
43 47 public Long createMiscIssue(MesWmMiscIssueSaveReqVO createReqVO) {
... ... @@ -110,21 +114,23 @@ public class MesWmMiscIssueServiceImpl implements MesWmMiscIssueService {
110 114 throw exception(WM_MISC_ISSUE_STATUS_INVALID);
111 115 }
112 116  
113   - // 遍历所有行,扣减库存
114   - // TODO @AI:需要实现 materialStockService.decreaseStock 方法来扣减库存
115   - List<MesWmMiscIssueLineDO> lines = miscIssueLineService.getMiscIssueLineListByIssueId(id);
116   - for (MesWmMiscIssueLineDO line : lines) {
117   - // materialStockService.decreaseStock(
118   - // line.getItemId(), line.getWarehouseId(), line.getLocationId(), line.getAreaId(),
119   - // line.getBatchId(), line.getQuantity());
120   - log.warn("[finishMiscIssue][杂项出库单({}) 行({}) 需要扣减库存,但 decreaseStock 方法尚未实现]", id, line.getId());
121   - }
  117 + // 2. 遍历所有行,创建库存事务(扣减库存 + 记录流水)
  118 + createTransactionList(issue);
122 119  
123   - // 更新出库单状态(待执行出库 → 已完成)
  120 + // 3. 更新出库单状态(待执行出库 → 已完成)
124 121 miscIssueMapper.updateById(new MesWmMiscIssueDO()
125 122 .setId(id).setStatus(MesWmMiscIssueStatusEnum.FINISHED.getStatus()));
126 123 }
127 124  
  125 + private void createTransactionList(MesWmMiscIssueDO issue) {
  126 + List<MesWmMiscIssueLineDO> lines = miscIssueLineService.getMiscIssueLineListByIssueId(issue.getId());
  127 + wmTransactionService.createTransactionList(convertList(lines, line -> new MesWmTransactionSaveReqDTO()
  128 + .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(line.getItemId())
  129 + .setQuantity(line.getQuantity().negate()) // 出库数量为负数
  130 + .setWarehouseId(line.getWarehouseId()).setLocationId(line.getLocationId()).setAreaId(line.getAreaId())
  131 + .setBizType(MesBizTypeConstants.WM_MISC_ISSUE).setBizId(issue.getId()).setBizCode(issue.getCode()).setBizLineId(line.getId())));
  132 + }
  133 +
128 134 @Override
129 135 @Transactional(rollbackFor = Exception.class)
130 136 public void cancelMiscIssue(Long id) {
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/miscreceipt/MesWmMiscReceiptServiceImpl.java
... ... @@ -11,7 +11,10 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.miscreceipt.MesWmMiscReceip
11 11 import cn.iocoder.yudao.module.mes.dal.mysql.wm.miscreceipt.MesWmMiscReceiptLineMapper;
12 12 import cn.iocoder.yudao.module.mes.dal.mysql.wm.miscreceipt.MesWmMiscReceiptMapper;
13 13 import cn.iocoder.yudao.module.mes.enums.wm.MesWmMiscReceiptStatusEnum;
14   -import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
  14 +import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
  15 +import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
  16 +import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
  17 +import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
15 18 import jakarta.annotation.Resource;
16 19 import lombok.extern.slf4j.Slf4j;
17 20 import org.springframework.stereotype.Service;
... ... @@ -21,6 +24,7 @@ import org.springframework.validation.annotation.Validated;
21 24 import java.util.List;
22 25  
23 26 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  27 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
24 28 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
25 29  
26 30 /**
... ... @@ -33,15 +37,13 @@ public class MesWmMiscReceiptServiceImpl implements MesWmMiscReceiptService {
33 37  
34 38 @Resource
35 39 private MesWmMiscReceiptMapper miscReceiptMapper;
36   -
37 40 @Resource
38 41 private MesWmMiscReceiptLineMapper miscReceiptLineMapper;
39 42  
40 43 @Resource
41 44 private MesWmMiscReceiptDetailService miscReceiptDetailService;
42   -
43 45 @Resource
44   - private MesWmMaterialStockService materialStockService;
  46 + private MesWmTransactionService wmTransactionService;
45 47  
46 48 @Override
47 49 public Long createMiscReceipt(MesWmMiscReceiptSaveReqVO createReqVO) {
... ... @@ -116,27 +118,23 @@ public class MesWmMiscReceiptServiceImpl implements MesWmMiscReceiptService {
116 118 throw exception(WM_MISC_RECEIPT_STATUS_NOT_APPROVED);
117 119 }
118 120  
119   - // 遍历所有行,更新库存台账
120   - List<MesWmMiscReceiptLineDO> lines = miscReceiptLineMapper.selectListByReceiptId(id);
121   - for (MesWmMiscReceiptLineDO line : lines) {
122   - materialStockService.increaseStock(
123   - line.getItemId(),
124   - line.getWarehouseId(),
125   - line.getLocationId(),
126   - line.getAreaId(),
127   - null, // batchId 传 null(杂项入库使用 batchCode)
128   - line.getQuantity(),
129   - null, // vendorId 传 null(杂项入库无供应商)
130   - line.getProductionDate(),
131   - line.getExpireDate()
132   - );
133   - }
  121 + // 2. 遍历所有行,创建库存事务(增加库存 + 记录流水)
  122 + createTransactionList(receipt);
134 123  
135   - // 执行入库(已审批 → 已完成)
  124 + // 3. 执行入库(已审批 → 已完成)
136 125 miscReceiptMapper.updateById(new MesWmMiscReceiptDO()
137 126 .setId(id).setStatus(MesWmMiscReceiptStatusEnum.FINISHED.getStatus()));
138 127 }
139 128  
  129 + private void createTransactionList(MesWmMiscReceiptDO receipt) {
  130 + List<MesWmMiscReceiptLineDO> lines = miscReceiptLineMapper.selectListByReceiptId(receipt.getId());
  131 + wmTransactionService.createTransactionList(convertList(lines, line -> new MesWmTransactionSaveReqDTO()
  132 + .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(line.getItemId())
  133 + .setQuantity(line.getQuantity()) // 入库数量为正数
  134 + .setWarehouseId(line.getWarehouseId()).setLocationId(line.getLocationId()).setAreaId(line.getAreaId())
  135 + .setBizType(MesBizTypeConstants.WM_MISC_RECPT).setBizId(receipt.getId()).setBizCode(receipt.getCode()).setBizLineId(line.getId())));
  136 + }
  137 +
140 138 @Override
141 139 @Transactional(rollbackFor = Exception.class)
142 140 public void cancelMiscReceipt(Long id) {
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskLineServiceImpl.java
... ... @@ -69,7 +69,7 @@ public class MesWmStockTakingTaskLineServiceImpl implements MesWmStockTakingTask
69 69 // 3. 批量生成明细行
70 70 List<MesWmStockTakingTaskLineDO> lines = convertList(stocks, stock -> MesWmStockTakingTaskLineDO.builder()
71 71 .taskId(task.getId()).materialStockId(stock.getId()).itemId(stock.getItemId())
72   - .batchId(stock.getBatchId()).quantity(stock.getQuantityOnhand()).takingQuantity(BigDecimal.ZERO)
  72 + .batchId(stock.getBatchId()).quantity(stock.getQuantity()).takingQuantity(BigDecimal.ZERO)
73 73 .warehouseId(stock.getWarehouseId()).locationId(stock.getLocationId()).areaId(stock.getAreaId())
74 74 .status(MesWmStockTakingTaskLineStatusEnum.LOSS.getStatus()).build());
75 75 stockTakingTaskLineMapper.insertBatch(lines);
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskServiceImpl.java
... ... @@ -211,7 +211,7 @@ public class MesWmStockTakingTaskServiceImpl implements MesWmStockTakingTaskServ
211 211 private void updateMaterialStockFrozen(Long materialStockId, boolean frozen) {
212 212 MesWmMaterialStockFreezeReqVO reqVO = new MesWmMaterialStockFreezeReqVO();
213 213 reqVO.setId(materialStockId);
214   - reqVO.setFrozen(frozen);
  214 + reqVO.setFrozenFlag(frozen);
215 215 materialStockService.updateMaterialStockFrozen(reqVO);
216 216 }
217 217  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.service.wm.transaction;
  2 +
  3 +import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
  4 +import jakarta.validation.Valid;
  5 +
  6 +import java.util.List;
  7 +
  8 +/**
  9 + * MES 库存事务流水 Service 接口
  10 + *
  11 + * 对齐 KTG WmTransactionServiceImpl.processTransaction 实现:
  12 + * 校验 → 库存台账更新 → 事务流水插入。
  13 + */
  14 +public interface MesWmTransactionService {
  15 +
  16 + /**
  17 + * 创建库存事务(含校验 + 库存更新 + 插入流水)
  18 + *
  19 + * @param reqDTO 事务数据
  20 + * @return 事务流水编号
  21 + */
  22 + Long createTransaction(@Valid MesWmTransactionSaveReqDTO reqDTO);
  23 +
  24 + /**
  25 + * 批量创建库存事务
  26 + *
  27 + * @param reqDTOs 事务数据列表
  28 + */
  29 + void createTransactionList(List<MesWmTransactionSaveReqDTO> reqDTOs);
  30 +
  31 +}
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.service.wm.transaction;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
  7 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
  8 +import cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction.MesWmTransactionMapper;
  9 +import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
  10 +import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
  11 +import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
  12 +import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
  13 +import jakarta.annotation.Resource;
  14 +import org.springframework.stereotype.Service;
  15 +import org.springframework.transaction.annotation.Transactional;
  16 +import org.springframework.validation.annotation.Validated;
  17 +
  18 +import java.math.BigDecimal;
  19 +import java.time.LocalDateTime;
  20 +import java.util.List;
  21 +
  22 +/**
  23 + * MES 库存事务流水 Service 实现类
  24 + */
  25 +@Service
  26 +@Validated
  27 +public class MesWmTransactionServiceImpl implements MesWmTransactionService {
  28 +
  29 + @Resource
  30 + private MesWmTransactionMapper transactionMapper;
  31 +
  32 + @Resource
  33 + private MesWmMaterialStockService materialStockService;
  34 + @Resource
  35 + private MesWmBatchService batchService;
  36 +
  37 + @Override
  38 + @Transactional(rollbackFor = Exception.class)
  39 + public Long createTransaction(MesWmTransactionSaveReqDTO reqDTO) {
  40 + // 1.1 校验事务类型
  41 + MesWmTransactionTypeEnum typeEnum = MesWmTransactionTypeEnum.valueOf(reqDTO.getType());
  42 + Assert.notNull(typeEnum, "事务类型({}) 不存在", reqDTO.getType());
  43 + // 1.2 校验 quantity 正负号与事务方向一致(入库必须正数,出库必须负数)
  44 + boolean inbound = typeEnum.isInbound();
  45 + if (inbound) {
  46 + Assert.isTrue(reqDTO.getQuantity().compareTo(BigDecimal.ZERO) > 0,
  47 + "入库事务数量必须为正数,实际值: {}", reqDTO.getQuantity());
  48 + } else {
  49 + Assert.isTrue(reqDTO.getQuantity().compareTo(BigDecimal.ZERO) < 0,
  50 + "出库事务数量必须为负数,实际值: {}", reqDTO.getQuantity());
  51 + }
  52 + // 1.3 batchId / batchCode 互补
  53 + completeBatchInfo(reqDTO);
  54 +
  55 + // 2.1 获取或创建库存记录
  56 + Long materialStockId = materialStockService.getOrCreateMaterialStock(
  57 + reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(),
  58 + reqDTO.getBatchId(), reqDTO.getVendorId(), reqDTO.getReceiptTime());
  59 + // 2.2 更新库存数量
  60 + boolean checkFlag = ObjUtil.defaultIfNull(reqDTO.getCheckFlag(), true);
  61 + materialStockService.updateMaterialStockQuantity(materialStockId, reqDTO.getQuantity(), checkFlag);
  62 +
  63 + // 3. 插入事务流水
  64 + MesWmTransactionDO transaction = MesWmTransactionDO.builder()
  65 + .type(reqDTO.getType()).quantity(reqDTO.getQuantity()).transactionTime(LocalDateTime.now())
  66 + .itemId(reqDTO.getItemId()).batchId(reqDTO.getBatchId()).batchCode(reqDTO.getBatchCode())
  67 + .warehouseId(reqDTO.getWarehouseId()).locationId(reqDTO.getLocationId()).areaId(reqDTO.getAreaId())
  68 + .bizType(reqDTO.getBizType()).bizId(reqDTO.getBizId()).bizCode(reqDTO.getBizCode()).bizLineId(reqDTO.getBizLineId())
  69 + .materialStockId(materialStockId).relatedTransactionId(reqDTO.getRelatedTransactionId())
  70 + .build();
  71 + transactionMapper.insert(transaction);
  72 + return transaction.getId();
  73 + }
  74 +
  75 + @Override
  76 + @Transactional(rollbackFor = Exception.class)
  77 + public void createTransactionList(List<MesWmTransactionSaveReqDTO> reqDTOs) {
  78 + reqDTOs.forEach(this::createTransaction);
  79 + }
  80 +
  81 + /**
  82 + * batchId / batchCode 互补:有 batchId 无 batchCode 时,查出 batchCode;反之亦然。
  83 + *
  84 + * @param reqDTO 事务请求 DTO
  85 + */
  86 + private void completeBatchInfo(MesWmTransactionSaveReqDTO reqDTO) {
  87 + if (reqDTO.getBatchId() != null && StrUtil.isNotEmpty(reqDTO.getBatchCode())) {
  88 + return;
  89 + }
  90 + // 情况一:有 batchId,补 batchCode
  91 + if (reqDTO.getBatchId() != null) {
  92 + MesWmBatchDO batch = batchService.getBatch(reqDTO.getBatchId());
  93 + if (batch != null) {
  94 + reqDTO.setBatchCode(batch.getCode());
  95 + }
  96 + return;
  97 + }
  98 + // 情况二:有 batchCode,补 batchId
  99 + if (StrUtil.isNotEmpty(reqDTO.getBatchCode())) {
  100 + MesWmBatchDO batch = batchService.getBatchByCode(reqDTO.getBatchCode());
  101 + if (batch != null) {
  102 + reqDTO.setBatchId(batch.getId());
  103 + }
  104 + }
  105 + }
  106 +
  107 +}
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.service.wm.transaction.dto;
  2 +
  3 +import jakarta.validation.constraints.NotNull;
  4 +import lombok.Data;
  5 +import lombok.experimental.Accessors;
  6 +
  7 +import java.math.BigDecimal;
  8 +import java.time.LocalDateTime;
  9 +
  10 +/**
  11 + * MES 库存事务 创建 Request DTO
  12 + */
  13 +@Data
  14 +@Accessors(chain = true)
  15 +public class MesWmTransactionSaveReqDTO {
  16 +
  17 + // ===== 事务核心 =====
  18 +
  19 + /**
  20 + * 事务类型
  21 + *
  22 + * 枚举 {@link cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum}
  23 + */
  24 + @NotNull(message = "事务类型不能为空")
  25 + private Integer type;
  26 +
  27 + // ===== 业务来源 =====
  28 +
  29 + /**
  30 + * 业务类型
  31 + *
  32 + * 对应 {@link cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants}
  33 + */
  34 + @NotNull(message = "业务类型不能为空")
  35 + private Integer bizType;
  36 + /**
  37 + * 业务主单 ID
  38 + */
  39 + @NotNull(message = "业务主单 ID 不能为空")
  40 + private Long bizId;
  41 + /**
  42 + * 业务单号
  43 + */
  44 + @NotNull(message = "业务单号不能为空")
  45 + private String bizCode;
  46 + /**
  47 + * 业务行 ID
  48 + */
  49 + @NotNull(message = "业务行 ID 不能为空")
  50 + private Long bizLineId;
  51 +
  52 + // ===== 物料维度 =====
  53 +
  54 + /**
  55 + * 物料 ID
  56 + */
  57 + @NotNull(message = "物料 ID 不能为空")
  58 + private Long itemId;
  59 + /**
  60 + * 变动数量(入库为正数,出库为负数)
  61 + */
  62 + @NotNull(message = "变动数量不能为空")
  63 + private BigDecimal quantity;
  64 + /**
  65 + * 批次 ID
  66 + */
  67 + private Long batchId;
  68 + /**
  69 + * 批次号
  70 + */
  71 + private String batchCode;
  72 +
  73 + // ===== 库存位置 =====
  74 +
  75 + /**
  76 + * 仓库 ID
  77 + */
  78 + @NotNull(message = "仓库 ID 不能为空")
  79 + private Long warehouseId;
  80 + /**
  81 + * 库区 ID
  82 + */
  83 + @NotNull(message = "库区 ID 不能为空")
  84 + private Long locationId;
  85 + /**
  86 + * 库位 ID
  87 + */
  88 + private Long areaId;
  89 +
  90 + // ===== 台账补充字段(入库时用于初始化 MesWmMaterialStockDO) =====
  91 +
  92 + /**
  93 + * 供应商 ID(采购入库场景)
  94 + */
  95 + private Long vendorId;
  96 + /**
  97 + * 入库时间(为空默认当前时间)
  98 + *
  99 + * 对应 KTG 的 recptDate
  100 + */
  101 + private LocalDateTime receiptTime;
  102 +
  103 + // ===== 关联 =====
  104 +
  105 + /**
  106 + * 关联事务 ID(成对事务中,入库事务关联出库事务)
  107 + */
  108 + private Long relatedTransactionId;
  109 +
  110 + // ===== 可选控制(非存储字段) =====
  111 +
  112 + /**
  113 + * 是否校验库存充足(默认 true;线边库消耗等场景设 false 允许负库存)
  114 + */
  115 + private Boolean checkFlag;
  116 +
  117 +}
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transfer/MesWmTransferServiceImpl.java
... ... @@ -274,7 +274,7 @@ public class MesWmTransferServiceImpl implements MesWmTransferService {
274 274 }
275 275 MesWmMaterialStockFreezeReqVO freezeReqVO = new MesWmMaterialStockFreezeReqVO();
276 276 freezeReqVO.setId(line.getMaterialStockId());
277   - freezeReqVO.setFrozen(frozen);
  277 + freezeReqVO.setFrozenFlag(frozen);
278 278 materialStockService.updateMaterialStockFrozen(freezeReqVO);
279 279 }
280 280 }
... ...