Commit b1e4a6df6620aab1f023b50a29566e59621f5015

Authored by YunaiV
1 parent f709822c

✨ feat(mes): 更新库存管理逻辑,重命名字段并添加事务记录

重构库存管理相关的请求和响应对象,更新字段名称以提高语义清晰度。新增库存事务流水记录,支持库存增减事件的追踪。此变更旨在提升库存管理的可维护性和可读性。
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java
... ... @@ -14,7 +14,6 @@ import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMat
14 14 import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
15 15 import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
16 16 import cn.iocoder.yudao.module.mes.dal.dataobject.md.vendor.MesMdVendorDO;
17   -import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
18 17 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
19 18 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
20 19 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
... ... @@ -22,7 +21,6 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLoc
22 21 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
23 22 import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
24 23 import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
25   -import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
26 24 import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
27 25 import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
28 26 import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
... ... @@ -73,9 +71,6 @@ public class MesWmMaterialStockController {
73 71 @Resource
74 72 private MesMdVendorService vendorService;
75 73  
76   - @Resource
77   - private MesProWorkOrderService workOrderService;
78   -
79 74 @GetMapping("/get")
80 75 @Operation(summary = "获得库存记录")
81 76 @Parameter(name = "id", description = "编号", required = true, example = "1024")
... ... @@ -128,7 +123,7 @@ public class MesWmMaterialStockController {
128 123 Map<Long, MesMdItemDO> itemMap = itemService.getItemMap(
129 124 convertSet(list, MesWmMaterialStockDO::getItemId));
130 125 Map<Long, MesMdUnitMeasureDO> unitMeasureMap = unitMeasureService.getUnitMeasureMap(
131   - convertSet(list, MesWmMaterialStockDO::getUnitMeasureId));
  126 + convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
132 127 Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
133 128 convertSet(list, MesWmMaterialStockDO::getWarehouseId));
134 129 Map<Long, MesWmWarehouseLocationDO> locationMap = locationService.getWarehouseLocationMap(
... ... @@ -137,16 +132,13 @@ public class MesWmMaterialStockController {
137 132 convertSet(list, MesWmMaterialStockDO::getAreaId));
138 133 Map<Long, MesMdVendorDO> vendorMap = vendorService.getVendorMap(
139 134 convertSet(list, MesWmMaterialStockDO::getVendorId));
140   - Map<Long, MesProWorkOrderDO> workOrderMap = workOrderService.getWorkOrderMap(
141   - convertSet(list, MesWmMaterialStockDO::getWorkOrderId));
142   - // TODO DONE @芋艿:待 mes_wm_batch 模块迁移后,补充 batchCode 的拼接
143   -
144 135 // 2. 构建结果
145 136 return BeanUtils.toBean(list, MesWmMaterialStockRespVO.class, vo -> {
146   - MapUtils.findAndThen(itemMap, vo.getItemId(), item ->
147   - vo.setItemCode(item.getCode()).setItemName(item.getName()).setSpecification(item.getSpecification()));
148   - MapUtils.findAndThen(unitMeasureMap, vo.getUnitMeasureId(),
149   - unitMeasure -> vo.setUnitMeasureName(unitMeasure.getName()));
  137 + MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
  138 + vo.setItemCode(item.getCode()).setItemName(item.getName()).setSpecification(item.getSpecification());
  139 + MapUtils.findAndThen(unitMeasureMap, item.getUnitMeasureId(),
  140 + unitMeasure -> vo.setUnitMeasureName(unitMeasure.getName()));
  141 + });
150 142 MapUtils.findAndThen(warehouseMap, vo.getWarehouseId(),
151 143 warehouse -> vo.setWarehouseName(warehouse.getName()));
152 144 MapUtils.findAndThen(locationMap, vo.getLocationId(),
... ... @@ -155,8 +147,6 @@ public class MesWmMaterialStockController {
155 147 area -> vo.setAreaName(area.getName()));
156 148 MapUtils.findAndThen(vendorMap, vo.getVendorId(),
157 149 vendor -> vo.setVendorName(vendor.getName()));
158   - MapUtils.findAndThen(workOrderMap, vo.getWorkOrderId(),
159   - workOrder -> vo.setWorkOrderCode(workOrder.getCode()));
160 150 });
161 151 }
162 152  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockFreezeReqVO.java
... ... @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
4 4 import jakarta.validation.constraints.NotNull;
5 5 import lombok.Data;
6 6  
7   -// TODO DONE @AI:已重命名为 FreezeReqVO,语义与冻结/解冻操作一致
8 7 @Schema(description = "管理后台 - MES 库存台账冻结/解冻 Request VO")
9 8 @Data
10 9 public class MesWmMaterialStockFreezeReqVO {
... ... @@ -15,6 +14,7 @@ public class MesWmMaterialStockFreezeReqVO {
15 14  
16 15 @Schema(description = "是否冻结", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
17 16 @NotNull(message = "冻结状态不能为空")
18   - private Boolean frozen;
  17 + private Boolean frozenFlag;
19 18  
20 19 }
  20 +
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockRespVO.java
... ... @@ -35,9 +35,6 @@ public class MesWmMaterialStockRespVO {
35 35 @ExcelProperty("规格型号")
36 36 private String specification;
37 37  
38   - @Schema(description = "计量单位编号", example = "1")
39   - private Long unitMeasureId;
40   -
41 38 @Schema(description = "计量单位名称", example = "千克")
42 39 @ExcelProperty("计量单位")
43 40 private String unitMeasureName;
... ... @@ -77,32 +74,17 @@ public class MesWmMaterialStockRespVO {
77 74 @ExcelProperty("供应商")
78 75 private String vendorName;
79 76  
80   - @Schema(description = "生产工单编号", example = "1")
81   - private Long workOrderId;
82   -
83   - @Schema(description = "生产工单编码", example = "WO20260101")
84   - @ExcelProperty("生产工单")
85   - private String workOrderCode;
86   -
87 77 @Schema(description = "在库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.0000")
88 78 @ExcelProperty("在库数量")
89   - private BigDecimal quantityOnhand;
  79 + private BigDecimal quantity;
90 80  
91 81 @Schema(description = "入库时间")
92 82 @ExcelProperty("入库时间")
93   - private LocalDateTime recptDate;
94   -
95   - @Schema(description = "生产日期")
96   - @ExcelProperty("生产日期")
97   - private LocalDateTime productionDate;
98   -
99   - @Schema(description = "库存有效期")
100   - @ExcelProperty("有效期")
101   - private LocalDateTime expireDate;
  83 + private LocalDateTime receiptTime;
102 84  
103 85 @Schema(description = "是否冻结", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
104 86 @ExcelProperty("是否冻结")
105   - private Boolean frozen;
  87 + private Boolean frozenFlag;
106 88  
107 89 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
108 90 @ExcelProperty("创建时间")
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/materialstock/MesWmMaterialStockDO.java
... ... @@ -3,9 +3,8 @@ package cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock;
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 4 import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
5 5 import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
6   -import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
7 6 import cn.iocoder.yudao.module.mes.dal.dataobject.md.vendor.MesMdVendorDO;
8   -import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
  7 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
9 8 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
10 9 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
11 10 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
... ... @@ -17,7 +16,6 @@ import lombok.*;
17 16 import java.math.BigDecimal;
18 17 import java.time.LocalDateTime;
19 18  
20   -// TODO DONE @芋艿:当前包名 materialstock 与表名 mes_wm_material_stock 保持一致,语义清晰,暂不调整
21 19 /**
22 20 * MES 库存台账(仓库现有量)DO
23 21 */
... ... @@ -36,6 +34,25 @@ public class MesWmMaterialStockDO extends BaseDO {
36 34 */
37 35 @TableId
38 36 private Long id;
  37 +
  38 + /**
  39 + * 在库数量
  40 + */
  41 + private BigDecimal quantity;
  42 + /**
  43 + * 入库时间
  44 + */
  45 + private LocalDateTime receiptTime;
  46 +
  47 + /**
  48 + * 供应商编号
  49 + *
  50 + * 关联 {@link MesMdVendorDO#getId()}
  51 + */
  52 + private Long vendorId;
  53 +
  54 + // ==================== 物料维度 ====================
  55 +
39 56 /**
40 57 * 物料分类编号
41 58 *
... ... @@ -49,17 +66,20 @@ public class MesWmMaterialStockDO extends BaseDO {
49 66 */
50 67 private Long itemId;
51 68 /**
52   - * 计量单位编号
  69 + * 批次编号
53 70 *
54   - * 关联 {@link MesMdUnitMeasureDO#getId()}
  71 + * 关联 {@link MesWmBatchDO#getId()}
55 72 */
56   - private Long unitMeasureId;
  73 + private Long batchId;
57 74 /**
58   - * 批次编号
  75 + * 批次号
59 76 *
60   - * TODO DONE @芋艿:保留。待 mes_wm_batch 模块迁移后补充 @link 关联
  77 + * 关联 {@link MesWmBatchDO#getCode()}
61 78 */
62   - private Long batchId;
  79 + private String batchCode;
  80 +
  81 + // ==================== 库存位置 ====================
  82 +
63 83 /**
64 84 * 仓库编号
65 85 *
... ... @@ -78,39 +98,12 @@ public class MesWmMaterialStockDO extends BaseDO {
78 98 * 关联 {@link MesWmWarehouseAreaDO#getId()}
79 99 */
80 100 private Long areaId;
81   - /**
82   - * 供应商编号
83   - *
84   - * 关联 {@link MesMdVendorDO#getId()}
85   - */
86   - private Long vendorId;
87   - // TODO @AI:这个字段,应该没有的。
88   - /**
89   - * 生产工单编号
90   - *
91   - * 关联 {@link MesProWorkOrderDO#getId()}
92   - */
93   - private Long workOrderId;
94   - // TODO @AI:onhand_quantity 更合理;更符合现在项目的风格。改下;
95   - /**
96   - * 在库数量
97   - */
98   - private BigDecimal quantityOnhand;
99   - /**
100   - * 入库时间
101   - */
102   - private LocalDateTime recptDate;
103   - /**
104   - * 库存有效期
105   - */
106   - private LocalDateTime expireDate;
107   - /**
108   - * 生产日期
109   - */
110   - private LocalDateTime productionDate;
  101 +
  102 + // ==================== 状态 ====================
  103 +
111 104 /**
112 105 * 是否冻结
113 106 */
114   - private Boolean frozen;
  107 + private Boolean frozenFlag;
115 108  
116 109 }
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/transaction/MesWmTransactionDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
  5 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
  6 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
  7 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
  8 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
  9 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
  10 +import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
  11 +import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
  12 +import com.baomidou.mybatisplus.annotation.KeySequence;
  13 +import com.baomidou.mybatisplus.annotation.TableId;
  14 +import com.baomidou.mybatisplus.annotation.TableName;
  15 +import lombok.*;
  16 +
  17 +import java.math.BigDecimal;
  18 +import java.time.LocalDateTime;
  19 +
  20 +/**
  21 + * MES 库存事务流水 DO
  22 + *
  23 + * 记录每一笔库存增减事件,系统自动生成,只读查询,不允许人工维护。
  24 + */
  25 +@TableName("mes_wm_transaction")
  26 +@KeySequence("mes_wm_transaction_seq")
  27 +@Data
  28 +@EqualsAndHashCode(callSuper = true)
  29 +@ToString(callSuper = true)
  30 +@Builder
  31 +@NoArgsConstructor
  32 +@AllArgsConstructor
  33 +public class MesWmTransactionDO extends BaseDO {
  34 +
  35 + /**
  36 + * 编号
  37 + */
  38 + @TableId
  39 + private Long id;
  40 +
  41 + // ==================== 业务归属 ====================
  42 +
  43 + // codex reply:`transactionType` 收敛成 `type` 是对的,和项目里其他 DO 的命名保持一致。
  44 + // codex reply:不过这类改名要连同 `MesWmTransactionSaveReqVO`、Service 和查询条件一起同步,不然 DO 与上下游模型会割裂。
  45 + /**
  46 + * 事务类型
  47 + *
  48 + * 枚举 {@link MesWmTransactionTypeEnum}
  49 + */
  50 + private Integer type;
  51 + // codex reply:`transactionQuantity` 收敛成 `quantity` 是对的;在事务流水这个上下文里,`transaction` 前缀没有额外信息。
  52 + // codex reply:`transactionFlag` 删除也合理,直接存带符号数量更符合当前写入逻辑,后续聚合查询也可以直接 `SUM(quantity)`。
  53 + /**
  54 + * 本次变动数量
  55 + *
  56 + * 正数=入库,负数=出库
  57 + */
  58 + private BigDecimal quantity;
  59 +
  60 + /**
  61 + * 业务类型
  62 + *
  63 + * 对应 {@link MesBizTypeConstants} 中的常量
  64 + */
  65 + private Integer bizType;
  66 + /**
  67 + * 来源业务主单 ID
  68 + */
  69 + private Long bizId;
  70 + /**
  71 + * 来源业务单号
  72 + */
  73 + private String bizCode;
  74 + /**
  75 + * 来源业务行 ID
  76 + */
  77 + private Long bizLineId;
  78 +
  79 + /**
  80 + * 库存记录 ID
  81 + *
  82 + * 关联 {@link MesWmMaterialStockDO#getId()}
  83 + */
  84 + private Long materialStockId;
  85 + /**
  86 + * 关联的事务 ID
  87 + *
  88 + * 关联 {@link #getId()},用于调拨等成对流水关联
  89 + */
  90 + private Long relatedTransactionId;
  91 +
  92 + // ==================== 物料维度 ====================
  93 +
  94 + /**
  95 + * 物料 ID
  96 + *
  97 + * 关联 {@link MesMdItemDO#getId()}
  98 + */
  99 + private Long itemId;
  100 + /**
  101 + * 批次 ID
  102 + *
  103 + * 关联 {@link MesWmBatchDO#getId()}
  104 + */
  105 + private Long batchId;
  106 + /**
  107 + * 批次号
  108 + *
  109 + * 关联 {@link MesWmBatchDO#getCode()}
  110 + */
  111 + private String batchCode;
  112 + // codex reply:`packageId` 当前删除是合理的。事务流水链路没有包装维度的输入输出,先不保留“预留字段”更干净。
  113 +
  114 + // ==================== 库存位置 ====================
  115 +
  116 + /**
  117 + * 仓库 ID
  118 + *
  119 + * 关联 {@link MesWmWarehouseDO#getId()}
  120 + */
  121 + private Long warehouseId;
  122 + /**
  123 + * 库区 ID
  124 + *
  125 + * 关联 {@link MesWmWarehouseLocationDO#getId()}
  126 + */
  127 + private Long locationId;
  128 + /**
  129 + * 库位 ID
  130 + *
  131 + * 关联 {@link MesWmWarehouseAreaDO#getId()}
  132 + */
  133 + private Long areaId;
  134 +
  135 + // ==================== 时间 ====================
  136 +
  137 + // codex reply:这里我不建议继续沿用 `erpTime` / `receiptTime` 这组命名;项目里的同类字段基本统一为 `receiptDate`,单独改成 `Time` 会破坏一致性。
  138 + // codex reply:如果这两个字段暂时没有业务赋值,建议宁可先删;若后续保留,ERP 侧更建议 `erpAccountDate`,收货侧保持 `receiptDate`。
  139 + /**
  140 + * 事务发生时间
  141 + */
  142 + private LocalDateTime transactionTime;
  143 + /**
  144 + * ERP 账期
  145 + */
  146 + private LocalDateTime erpTime;
  147 + /**
  148 + * 入库时间
  149 + */
  150 + private LocalDateTime receiptTime;
  151 +
  152 +}
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/itemconsume/MesWmItemConsumeLineMapper.java
... ... @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.mes.controller.admin.wm.itemconsume.vo.MesWmItemC
7 7 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeLineDO;
8 8 import org.apache.ibatis.annotations.Mapper;
9 9  
  10 +import java.util.List;
  11 +
10 12 /**
11 13 * MES 物料消耗记录行 Mapper
12 14 *
... ... @@ -22,4 +24,8 @@ public interface MesWmItemConsumeLineMapper extends BaseMapperX&lt;MesWmItemConsume
22 24 .orderByDesc(MesWmItemConsumeLineDO::getId));
23 25 }
24 26  
  27 + default List<MesWmItemConsumeLineDO> selectListByConsumeId(Long consumeId) {
  28 + return selectList(MesWmItemConsumeLineDO::getConsumeId, consumeId);
  29 + }
  30 +
25 31 }
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java
... ... @@ -27,9 +27,9 @@ public interface MesWmMaterialStockMapper extends BaseMapperX&lt;MesWmMaterialStock
27 27 .inIfPresent(MesWmMaterialStockDO::getItemId, itemIds)
28 28 .eqIfPresent(MesWmMaterialStockDO::getWarehouseId, reqVO.getWarehouseId())
29 29 .eqIfPresent(MesWmMaterialStockDO::getLocationId, reqVO.getLocationId())
30   - .eqIfPresent(MesWmMaterialStockDO::getFrozen, reqVO.getFrozen())
31   - .ne(MesWmMaterialStockDO::getQuantityOnhand, BigDecimal.ZERO)
32   - .orderByAsc(MesWmMaterialStockDO::getRecptDate));
  30 + .eqIfPresent(MesWmMaterialStockDO::getFrozenFlag, reqVO.getFrozen())
  31 + .ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO)
  32 + .orderByAsc(MesWmMaterialStockDO::getReceiptTime));
33 33 }
34 34  
35 35 default Long selectCountByWarehouseId(Long warehouseId) {
... ... @@ -48,11 +48,21 @@ public interface MesWmMaterialStockMapper extends BaseMapperX&lt;MesWmMaterialStock
48 48 return selectByIds(ids);
49 49 }
50 50  
51   - // TODO @AI:参考别的模块,最好使用 update某个字段,有例子的;然后看看,有没可能,尽量不写字段,而是通过 lamba 表达式获得名字。
52   - default void incrQuantityOnhand(Long id, BigDecimal quantity) {
53   - update(null, new LambdaUpdateWrapper<MesWmMaterialStockDO>()
  51 + /**
  52 + * 增量更新库存数量
  53 + *
  54 + * @param id 库存记录编号
  55 + * @param count 变动数量(正数=增加,负数=扣减)
  56 + * @return 影响行数(扣减时为 0 表示库存不足)
  57 + */
  58 + default int updateQuantity(Long id, BigDecimal count) {
  59 + LambdaUpdateWrapper<MesWmMaterialStockDO> updateWrapper = new LambdaUpdateWrapper<MesWmMaterialStockDO>()
54 60 .eq(MesWmMaterialStockDO::getId, id)
55   - .setSql("quantity_onhand = quantity_onhand + " + quantity));
  61 + .setSql("quantity = quantity + " + count);
  62 + if (count.compareTo(BigDecimal.ZERO) < 0) {
  63 + updateWrapper.ge(MesWmMaterialStockDO::getQuantity, count.abs()); // CAS 防负库存
  64 + }
  65 + return update(null, updateWrapper);
56 66 }
57 67  
58 68 default MesWmMaterialStockDO selectByCompositeKey(Long itemId, Long warehouseId, Long locationId,
... ... @@ -74,8 +84,8 @@ public interface MesWmMaterialStockMapper extends BaseMapperX&lt;MesWmMaterialStock
74 84 .eqIfPresent(MesWmMaterialStockDO::getBatchId, reqVO.getBatchId())
75 85 .geIfPresent(MesWmMaterialStockDO::getUpdateTime, reqVO.getStartTime())
76 86 .leIfPresent(MesWmMaterialStockDO::getUpdateTime, reqVO.getEndTime())
77   - .ne(MesWmMaterialStockDO::getQuantityOnhand, BigDecimal.ZERO) // TODO @芋艿:需要在考虑下,要不要支持;
78   - .orderByAsc(MesWmMaterialStockDO::getRecptDate));
  87 + .ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO) // TODO @芋艿:需要在考虑下,要不要支持;
  88 + .orderByAsc(MesWmMaterialStockDO::getReceiptTime));
79 89 }
80 90  
81 91 }
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +/**
  8 + * MES 库存事务流水 Mapper
  9 + */
  10 +@Mapper
  11 +public interface MesWmTransactionMapper extends BaseMapperX<MesWmTransactionDO> {
  12 +
  13 +}
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
... ... @@ -355,6 +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 359  
359 360 // ========== MES 仓库管理-到货通知单(1-040-704-000) ==========
360 361 ErrorCode WM_ARRIVAL_NOTICE_NOT_EXISTS = new ErrorCode(1_040_704_000, "到货通知单不存在");
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java
... ... @@ -8,7 +8,6 @@ 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;
12 11 import java.util.Collection;
13 12 import java.util.List;
14 13 import java.util.Map;
... ... @@ -99,7 +98,7 @@ public interface MesWmMaterialStockService {
99 98 * 增加库存
100 99 *
101 100 * 按 itemId + warehouseId + locationId + areaId + batchId 查找已有库存,
102   - * 存在则增加 quantityOnhand;不存在则新建记录。
  101 + * 存在则增加 quantity;不存在则新建记录。
103 102 *
104 103 * @param itemId 物料编号
105 104 * @param warehouseId 仓库编号
... ... @@ -108,11 +107,19 @@ public interface MesWmMaterialStockService {
108 107 * @param batchId 批次编号
109 108 * @param quantity 增加数量
110 109 * @param vendorId 供应商编号
111   - * @param productionDate 生产日期
112   - * @param expireDate 过期日期
113 110 */
114 111 void increaseStock(Long itemId, Long warehouseId, Long locationId, Long areaId,
115   - Long batchId, BigDecimal quantity, Long vendorId,
116   - LocalDateTime productionDate, LocalDateTime expireDate);
  112 + Long batchId, BigDecimal quantity, Long vendorId);
  113 +
  114 + /**
  115 + * 扣减库存
  116 + *
  117 + * 根据库存记录 ID 直接扣减数量。
  118 + *
  119 + * @param materialStockId 库存记录编号
  120 + * @param quantity 扣减数量(正数)
  121 + * @param storageCheckFlag 是否校验库存充足
  122 + */
  123 + void decreaseStock(Long materialStockId, BigDecimal quantity, boolean storageCheckFlag);
117 124  
118 125 }
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java
... ... @@ -22,6 +22,7 @@ import java.util.*;
22 22  
23 23 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
24 24 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  25 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_MATERIAL_STOCK_INSUFFICIENT;
25 26 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_MATERIAL_STOCK_NOT_EXISTS;
26 27  
27 28 /**
... ... @@ -81,7 +82,7 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService
81 82 // 更新冻结状态
82 83 MesWmMaterialStockDO updateObj = new MesWmMaterialStockDO();
83 84 updateObj.setId(updateReqVO.getId());
84   - updateObj.setFrozen(updateReqVO.getFrozen());
  85 + updateObj.setFrozenFlag(updateReqVO.getFrozenFlag());
85 86 materialStockMapper.updateById(updateObj);
86 87 }
87 88  
... ... @@ -113,30 +114,40 @@ public class MesWmMaterialStockServiceImpl implements MesWmMaterialStockService
113 114 return materialStockMapper.selectListForStockTaking(reqVO);
114 115 }
115 116  
116   - // TODO DONE @AI:increaseStock 方法命名合理,语义清晰
117 117 @Override
118 118 public void increaseStock(Long itemId, Long warehouseId, Long locationId, Long areaId,
119   - Long batchId, BigDecimal quantity, Long vendorId,
120   - LocalDateTime productionDate, LocalDateTime expireDate) {
  119 + Long batchId, BigDecimal quantity, Long vendorId) {
121 120 // 1. 查找已有库存记录
122 121 MesWmMaterialStockDO stock = materialStockMapper.selectByCompositeKey(
123 122 itemId, warehouseId, locationId, areaId, batchId);
124 123  
125 124 // 2a. 存在则增加数量
126 125 if (stock != null) {
127   - materialStockMapper.incrQuantityOnhand(stock.getId(), quantity);
  126 + materialStockMapper.updateQuantity(stock.getId(), quantity);
128 127 return;
129 128 }
130 129  
131   - // 2. 不存在则新建
  130 + // 2b. 不存在则新建
132 131 MesMdItemDO item = itemService.validateItemExists(itemId);
133 132 MesWmMaterialStockDO newStock = MesWmMaterialStockDO.builder()
134   - .itemId(itemId).itemTypeId(item.getItemTypeId()).unitMeasureId(item.getUnitMeasureId())
  133 + .itemId(itemId).itemTypeId(item.getItemTypeId())
135 134 .warehouseId(warehouseId).locationId(locationId).areaId(areaId)
136   - .batchId(batchId).vendorId(vendorId).quantityOnhand(quantity)
137   - .recptDate(LocalDateTime.now()).productionDate(productionDate).expireDate(expireDate).frozen(false)
  135 + .batchId(batchId).vendorId(vendorId).quantity(quantity)
  136 + .receiptTime(LocalDateTime.now()).frozenFlag(false)
138 137 .build();
139 138 materialStockMapper.insert(newStock);
140 139 }
141 140  
  141 + @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) {
  149 + throw exception(WM_MATERIAL_STOCK_INSUFFICIENT);
  150 + }
  151 + }
  152 +
142 153 }
... ...