diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/itemconsume/MesWmItemConsumeDetailDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/itemconsume/MesWmItemConsumeDetailDO.java new file mode 100644 index 0000000..daae8fb --- /dev/null +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/itemconsume/MesWmItemConsumeDetailDO.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; + +/** + * MES 物料消耗记录明细 DO + * + * 记录 line 级别的消耗按线边库 FIFO 分配到具体批次的明细。 + * 一条 line 可能拆分为多条 detail(当一个批次库存不够,需要从下一个批次继续分配时)。 + * + * @author 芋道源码 + */ +@TableName("mes_wm_item_consume_detail") +@KeySequence("mes_wm_item_consume_detail_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MesWmItemConsumeDetailDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 消耗记录编号 + * + * 关联 {@link MesWmItemConsumeDO#getId()} + */ + private Long consumeId; + /** + * 消耗记录行编号 + * + * 关联 {@link MesWmItemConsumeLineDO#getId()} + */ + private Long lineId; + /** + * 库存台账编号 + * + * 关联 {@link MesWmMaterialStockDO#getId()} + * 当线边库中无可用库存时为 null + */ + private Long materialStockId; + /** + * 物料编号 + * + * 关联 {@link MesMdItemDO#getId()} + */ + private Long itemId; + /** + * 消耗数量 + */ + private BigDecimal quantity; + /** + * 批次编号 + * + * 关联 {@link MesWmBatchDO#getId()} + */ + private Long batchId; + /** + * 批次号 + * + * 冗余 {@link MesWmBatchDO#getCode()} + */ + private String batchCode; + /** + * 仓库编号 + * + * 关联 {@link MesWmWarehouseDO#getId()} + */ + private Long warehouseId; + /** + * 库区编号 + * + * 关联 {@link MesWmWarehouseLocationDO#getId()} + */ + private Long locationId; + /** + * 库位编号 + * + * 关联 {@link MesWmWarehouseAreaDO#getId()} + */ + private Long areaId; + /** + * 备注 + */ + private String remark; + +} diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/itemconsume/MesWmItemConsumeDetailMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/itemconsume/MesWmItemConsumeDetailMapper.java new file mode 100644 index 0000000..f9bf8bf --- /dev/null +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/itemconsume/MesWmItemConsumeDetailMapper.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDetailDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * MES 物料消耗记录明细 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface MesWmItemConsumeDetailMapper extends BaseMapperX { + + default List selectListByConsumeId(Long consumeId) { + return selectList(MesWmItemConsumeDetailDO::getConsumeId, consumeId); + } + + default List selectListByLineId(Long lineId) { + return selectList(MesWmItemConsumeDetailDO::getLineId, lineId); + } + +} diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeDetailService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeDetailService.java new file mode 100644 index 0000000..9a9b9a9 --- /dev/null +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeDetailService.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.mes.service.wm.itemconsume; + +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDetailDO; + +import java.util.List; + +/** + * MES 物料消耗记录明细 Service 接口 + * + * @author 芋道源码 + */ +public interface MesWmItemConsumeDetailService { + + /** + * 批量创建消耗明细 + * + * @param details 消耗明细列表 + */ + void createItemConsumeDetailBatch(List details); + + /** + * 查询指定消耗记录的所有明细 + * + * @param consumeId 消耗记录编号 + * @return 消耗明细列表 + */ + List getItemConsumeDetailListByConsumeId(Long consumeId); + +} diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeDetailServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeDetailServiceImpl.java new file mode 100644 index 0000000..70843e6 --- /dev/null +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeDetailServiceImpl.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.mes.service.wm.itemconsume; + +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDetailDO; +import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeDetailMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +/** + * MES 物料消耗记录明细 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class MesWmItemConsumeDetailServiceImpl implements MesWmItemConsumeDetailService { + + @Resource + private MesWmItemConsumeDetailMapper itemConsumeDetailMapper; + + @Override + public void createItemConsumeDetailBatch(List details) { + itemConsumeDetailMapper.insertBatch(details); + } + + @Override + public List getItemConsumeDetailListByConsumeId(Long consumeId) { + return itemConsumeDetailMapper.selectListByConsumeId(consumeId); + } + +} diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeLineServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeLineServiceImpl.java index cc63943..1aacd6e 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeLineServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeLineServiceImpl.java @@ -5,8 +5,8 @@ import cn.iocoder.yudao.module.mes.controller.admin.wm.itemconsume.vo.MesWmItemC import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeLineDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeLineMapper; -import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeMapper; import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -25,7 +25,8 @@ public class MesWmItemConsumeLineServiceImpl implements MesWmItemConsumeLineServ private MesWmItemConsumeLineMapper itemConsumeLineMapper; @Resource - private MesWmItemConsumeMapper itemConsumeMapper; + @Lazy + private MesWmItemConsumeService itemConsumeService; @Override public void createItemConsumeLineBatch(List lines) { @@ -33,10 +34,9 @@ public class MesWmItemConsumeLineServiceImpl implements MesWmItemConsumeLineServ } @Override - public PageResult getItemConsumeLinePage( - MesWmItemConsumeLinePageReqVO pageReqVO) { - // 1. 先通过 feedbackId 找到消耗头 - MesWmItemConsumeDO consume = itemConsumeMapper.selectByFeedbackId(pageReqVO.getFeedbackId()); + public PageResult getItemConsumeLinePage(MesWmItemConsumeLinePageReqVO pageReqVO) { + // 1. 先通过 feedbackId 找到消耗头(通过 Service 调用,不直接注入 Mapper) + MesWmItemConsumeDO consume = itemConsumeService.getByFeedbackId(pageReqVO.getFeedbackId()); if (consume == null) { return PageResult.empty(); } diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeService.java index 3e66349..305d5ec 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeService.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeService.java @@ -24,10 +24,18 @@ public interface MesWmItemConsumeService { /** * 完成物料消耗(库存扣减) * - * 遍历消耗行,扣减线边库库存,更新消耗单状态为已完成 + * 遍历消耗明细(detail),按批次精确扣减线边库库存,更新消耗单状态为已完成 * * @param consumeId 消耗记录编号 */ void finishItemConsume(Long consumeId); + /** + * 根据报工记录编号获取消耗记录 + * + * @param feedbackId 报工记录编号 + * @return 消耗记录,不存在返回 null + */ + MesWmItemConsumeDO getByFeedbackId(Long feedbackId); + } diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeServiceImpl.java index d7fe5d0..63daf67 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeServiceImpl.java @@ -1,18 +1,22 @@ package cn.iocoder.yudao.module.mes.service.wm.itemconsume; import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDetailDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeLineDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeMapper; -import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemConsumeStatusEnum; import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; +import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemConsumeStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductBomService; +import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService; import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; @@ -22,7 +26,9 @@ import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @@ -41,6 +47,8 @@ public class MesWmItemConsumeServiceImpl implements MesWmItemConsumeService { @Resource private MesWmItemConsumeLineService itemConsumeLineService; @Resource + private MesWmItemConsumeDetailService itemConsumeDetailService; + @Resource private MesProRouteProductBomService routeProductBomService; @Resource private MesWmTransactionService wmTransactionService; @@ -50,6 +58,8 @@ public class MesWmItemConsumeServiceImpl implements MesWmItemConsumeService { private MesWmWarehouseLocationService locationService; @Resource private MesWmWarehouseAreaService areaService; + @Resource + private MesWmMaterialStockService materialStockService; @Override public MesWmItemConsumeDO generateItemConsume(MesProFeedbackDO feedback) { @@ -60,43 +70,126 @@ public class MesWmItemConsumeServiceImpl implements MesWmItemConsumeService { return null; } - // 2.1 生成消耗单头 + // 2. 获取虚拟线边库信息 + MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE); + MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode( + MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION); + MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA); + + // 3. 生成消耗单头 MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder() .workOrderId(feedback.getWorkOrderId()).taskId(feedback.getTaskId()) .workstationId(feedback.getWorkstationId()).processId(feedback.getProcessId()) .feedbackId(feedback.getId()).consumeDate(LocalDateTime.now()) .status(MesWmItemConsumeStatusEnum.PREPARE.getStatus()).build(); itemConsumeMapper.insert(consume); - // 2.2 批量生成消耗行(消耗数量 = BOM 用料比例 × 报工数量) + + // 4. 批量生成消耗行(消耗数量 = BOM 用料比例 × 报工数量) List lines = convertList(boms, bom -> MesWmItemConsumeLineDO.builder() .consumeId(consume.getId()).itemId(bom.getItemId()) .quantity(bom.getQuantity().multiply(feedback.getFeedbackQuantity())).build()); itemConsumeLineService.createItemConsumeLineBatch(lines); - // TODO @AI(from codex):对齐,这里还需要按线边库/FIFO 生成消耗明细,并在库存不足时回退为无批次消耗明细。 + + // 5. 按线边库 FIFO 生成消耗明细 + List allDetails = new ArrayList<>(); + for (MesWmItemConsumeLineDO line : lines) { + generateDetailForLine(line, consume.getId(), + virtualWarehouse, virtualLocation, virtualArea, allDetails); + } + if (CollUtil.isNotEmpty(allDetails)) { + itemConsumeDetailService.createItemConsumeDetailBatch(allDetails); + } return consume; } + /** + * 为单条消耗行按 FIFO 分配线边库批次,生成消耗明细 + * + * 规则: + * 1. 查询该物料在虚拟线边库中的库存记录,按入库时间升序(FIFO) + * 2. 依次从各批次分配,直到消耗量满足 + * 3. 如果线边库无库存或库存不足,剩余数量生成无批次明细(待后续盘库核销) + */ + private void generateDetailForLine(MesWmItemConsumeLineDO line, Long consumeId, + MesWmWarehouseDO warehouse, MesWmWarehouseLocationDO location, MesWmWarehouseAreaDO area, + List allDetails) { + // 查询该物料在线边库中的库存(按 receiptTime 升序 = FIFO;已排除 quantity = 0) + MesWmMaterialStockListReqVO stockQuery = new MesWmMaterialStockListReqVO().setItemId(line.getItemId()) + .setWarehouseId(warehouse.getId()).setLocationId(location.getId()).setAreaId(area.getId()); + List stocks = materialStockService.getMaterialStockList(stockQuery) + .stream().filter(s -> s.getQuantity().compareTo(BigDecimal.ZERO) > 0).toList(); + + if (CollUtil.isEmpty(stocks)) { + // 线边库无该物料库存 → 生成不带批次的明细 + allDetails.add(buildDetail(consumeId, line.getId(), null, line.getItemId(), + line.getQuantity(), null, null, warehouse, location, area)); + return; + } + + // 按 FIFO 依次从各批次分配 + BigDecimal remaining = line.getQuantity(); + for (MesWmMaterialStockDO stock : stocks) { + if (remaining.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + BigDecimal allocate = stock.getQuantity().min(remaining); + allDetails.add(buildDetail(consumeId, line.getId(), stock.getId(), line.getItemId(), + allocate, stock.getBatchId(), stock.getBatchCode(), warehouse, location, area)); + remaining = remaining.subtract(allocate); + } + + // 库存不足时,剩余数量生成无批次明细(待后续盘库手工核销) + if (remaining.compareTo(BigDecimal.ZERO) > 0) { + allDetails.add(buildDetail(consumeId, line.getId(), null, line.getItemId(), + remaining, null, null, warehouse, location, area)); + } + } + + private static MesWmItemConsumeDetailDO buildDetail(Long consumeId, Long lineId, Long materialStockId, + Long itemId, BigDecimal quantity, + Long batchId, String batchCode, + MesWmWarehouseDO warehouse, + MesWmWarehouseLocationDO location, + MesWmWarehouseAreaDO area) { + return MesWmItemConsumeDetailDO.builder() + .consumeId(consumeId).lineId(lineId).materialStockId(materialStockId) + .itemId(itemId).quantity(quantity) + .batchId(batchId).batchCode(batchCode) + .warehouseId(warehouse.getId()).locationId(location.getId()).areaId(area.getId()) + .build(); + } + @Override public void finishItemConsume(Long consumeId) { - MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE); - MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION); - MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA); + // 1. 查询消耗明细(detail 级别,包含精确的批次/仓库信息) + List details = itemConsumeDetailService + .getItemConsumeDetailListByConsumeId(consumeId); - // 遍历消耗行,创建库存事务(从线边库扣减) - List lines = itemConsumeLineService.getItemConsumeLineListByConsumeId(consumeId); - for (MesWmItemConsumeLineDO line : lines) { + // 2. 遍历明细,创建库存事务(按批次精确扣减线边库) + for (MesWmItemConsumeDetailDO detail : details) { wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO() - .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(line.getItemId()) - .setQuantity(line.getQuantity().negate()) // 库存减少 - .setWarehouseId(virtualWarehouse.getId()).setLocationId(virtualLocation.getId()).setAreaId(virtualArea.getId()) + .setType(MesWmTransactionTypeEnum.OUT.getType()) + .setItemId(detail.getItemId()) + .setQuantity(detail.getQuantity().negate()) + .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()) + .setWarehouseId(detail.getWarehouseId()) + .setLocationId(detail.getLocationId()) + .setAreaId(detail.getAreaId()) .setCheckFlag(false) // 线边库允许负库存 - .setBizType(MesBizTypeConstants.WM_ITEM_CONSUME).setBizId(consumeId) - .setBizCode("").setBizLineId(line.getId())); + .setBizType(MesBizTypeConstants.WM_ITEM_CONSUME) + .setBizId(consumeId) + .setBizCode("") + .setBizLineId(detail.getLineId())); } - // 更新消耗单状态为已完成 + // 3. 更新消耗单状态为已完成 itemConsumeMapper.updateById(MesWmItemConsumeDO.builder() .id(consumeId).status(MesWmItemConsumeStatusEnum.FINISHED.getStatus()).build()); } + @Override + public MesWmItemConsumeDO getByFeedbackId(Long feedbackId) { + return itemConsumeMapper.selectByFeedbackId(feedbackId); + } + } diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeServiceImplTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeServiceImplTest.java new file mode 100644 index 0000000..7569111 --- /dev/null +++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/itemconsume/MesWmItemConsumeServiceImplTest.java @@ -0,0 +1,494 @@ +package cn.iocoder.yudao.module.mes.service.wm.itemconsume; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO; +import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDetailDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeLineDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO; +import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemconsume.MesWmItemConsumeMapper; +import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; +import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemConsumeStatusEnum; +import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; +import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductBomService; +import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService; +import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; +import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; +import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; +import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService; +import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.*; + +/** + * {@link MesWmItemConsumeServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +@Import(MesWmItemConsumeServiceImpl.class) +public class MesWmItemConsumeServiceImplTest extends BaseDbUnitTest { + + @Resource + private MesWmItemConsumeServiceImpl itemConsumeService; + + @Resource + private MesWmItemConsumeMapper itemConsumeMapper; + + @MockitoBean + private MesWmItemConsumeLineService itemConsumeLineService; + @MockitoBean + private MesWmItemConsumeDetailService itemConsumeDetailService; + @MockitoBean + private MesProRouteProductBomService routeProductBomService; + @MockitoBean + private MesWmTransactionService wmTransactionService; + @MockitoBean + private MesWmWarehouseService warehouseService; + @MockitoBean + private MesWmWarehouseLocationService locationService; + @MockitoBean + private MesWmWarehouseAreaService areaService; + @MockitoBean + private MesWmMaterialStockService materialStockService; + + // ========== 公共 mock 数据 ========== + + private static final Long WAREHOUSE_ID = 100L; + private static final Long LOCATION_ID = 200L; + private static final Long AREA_ID = 300L; + + private MesWmWarehouseDO virtualWarehouse; + private MesWmWarehouseLocationDO virtualLocation; + private MesWmWarehouseAreaDO virtualArea; + + @BeforeEach + public void setUp() { + virtualWarehouse = new MesWmWarehouseDO(); + virtualWarehouse.setId(WAREHOUSE_ID); + virtualWarehouse.setCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE); + + virtualLocation = new MesWmWarehouseLocationDO(); + virtualLocation.setId(LOCATION_ID); + virtualLocation.setCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION); + + virtualArea = new MesWmWarehouseAreaDO(); + virtualArea.setId(AREA_ID); + virtualArea.setCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA); + + when(warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE)) + .thenReturn(virtualWarehouse); + when(locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION)) + .thenReturn(virtualLocation); + when(areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA)) + .thenReturn(virtualArea); + + // mock createItemConsumeLineBatch:模拟 insertBatch 后填充 ID + AtomicLong lineIdSeq = new AtomicLong(1000L); + doAnswer(invocation -> { + List lines = invocation.getArgument(0); + for (MesWmItemConsumeLineDO line : lines) { + line.setId(lineIdSeq.getAndIncrement()); + } + return null; + }).when(itemConsumeLineService).createItemConsumeLineBatch(any()); + } + + private MesProFeedbackDO buildFeedback() { + return MesProFeedbackDO.builder() + .id(randomLongId()).workOrderId(randomLongId()).taskId(randomLongId()) + .workstationId(randomLongId()).processId(randomLongId()).routeId(randomLongId()) + .itemId(randomLongId()).feedbackQuantity(BigDecimal.TEN) + .build(); + } + + // ========== generateItemConsume ========== + + @Test + public void testGenerateItemConsume_noBom() { + // 准备 + MesProFeedbackDO feedback = buildFeedback(); + when(routeProductBomService.getRouteProductBomList( + feedback.getRouteId(), feedback.getProcessId(), feedback.getItemId())) + .thenReturn(Collections.emptyList()); + + // 调用 + MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback); + + // 断言 + assertNull(result); + verify(itemConsumeLineService, never()).createItemConsumeLineBatch(any()); + verify(itemConsumeDetailService, never()).createItemConsumeDetailBatch(any()); + } + + @Test + public void testGenerateItemConsume_noStock() { + // 准备:1 个 BOM 物料,线边库无库存 + MesProFeedbackDO feedback = buildFeedback(); + Long bomItemId = randomLongId(); + MesProRouteProductBomDO bom = new MesProRouteProductBomDO(); + bom.setItemId(bomItemId); + bom.setQuantity(new BigDecimal("2")); // 用料比例 = 2 + when(routeProductBomService.getRouteProductBomList(any(), any(), any())) + .thenReturn(List.of(bom)); + when(materialStockService.getMaterialStockList(any(MesWmMaterialStockListReqVO.class))) + .thenReturn(Collections.emptyList()); + + // 调用 + MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback); + + // 断言 1:消耗单头已入库 + assertNotNull(result); + assertNotNull(result.getId()); + assertEquals(MesWmItemConsumeStatusEnum.PREPARE.getStatus(), result.getStatus()); + assertEquals(feedback.getId(), result.getFeedbackId()); + + // 断言 2:消耗行正确 + @SuppressWarnings("unchecked") + ArgumentCaptor> lineCaptor = ArgumentCaptor.forClass(List.class); + verify(itemConsumeLineService).createItemConsumeLineBatch(lineCaptor.capture()); + List lines = lineCaptor.getValue(); + assertEquals(1, lines.size()); + assertEquals(bomItemId, lines.get(0).getItemId()); + // 消耗数量 = 2 × 10 = 20 + assertEquals(0, new BigDecimal("20").compareTo(lines.get(0).getQuantity())); + + // 断言 3:生成 1 条无批次的 detail + @SuppressWarnings("unchecked") + ArgumentCaptor> detailCaptor = ArgumentCaptor.forClass(List.class); + verify(itemConsumeDetailService).createItemConsumeDetailBatch(detailCaptor.capture()); + List details = detailCaptor.getValue(); + assertEquals(1, details.size()); + MesWmItemConsumeDetailDO detail = details.get(0); + assertEquals(bomItemId, detail.getItemId()); + assertEquals(0, new BigDecimal("20").compareTo(detail.getQuantity())); + assertNull(detail.getMaterialStockId()); + assertNull(detail.getBatchId()); + assertEquals(WAREHOUSE_ID, detail.getWarehouseId()); + assertEquals(LOCATION_ID, detail.getLocationId()); + assertEquals(AREA_ID, detail.getAreaId()); + } + + @Test + public void testGenerateItemConsume_singleBatchSufficient() { + // 准备:1 个 BOM 物料,线边库有 1 个批次且库存充足 + MesProFeedbackDO feedback = buildFeedback(); + Long bomItemId = randomLongId(); + MesProRouteProductBomDO bom = new MesProRouteProductBomDO(); + bom.setItemId(bomItemId); + bom.setQuantity(new BigDecimal("3")); // 消耗 = 3 × 10 = 30 + when(routeProductBomService.getRouteProductBomList(any(), any(), any())) + .thenReturn(List.of(bom)); + + Long stockId = randomLongId(); + Long batchId = randomLongId(); + MesWmMaterialStockDO stock = new MesWmMaterialStockDO(); + stock.setId(stockId); + stock.setItemId(bomItemId); + stock.setQuantity(new BigDecimal("50")); // 库存 50 > 需求 30 + stock.setBatchId(batchId); + stock.setBatchCode("BATCH-001"); + when(materialStockService.getMaterialStockList(any(MesWmMaterialStockListReqVO.class))) + .thenReturn(List.of(stock)); + + // 调用 + MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback); + + // 断言:生成 1 条带批次的 detail + assertNotNull(result); + @SuppressWarnings("unchecked") + ArgumentCaptor> detailCaptor = ArgumentCaptor.forClass(List.class); + verify(itemConsumeDetailService).createItemConsumeDetailBatch(detailCaptor.capture()); + List details = detailCaptor.getValue(); + assertEquals(1, details.size()); + MesWmItemConsumeDetailDO detail = details.get(0); + assertEquals(0, new BigDecimal("30").compareTo(detail.getQuantity())); + assertEquals(stockId, detail.getMaterialStockId()); + assertEquals(batchId, detail.getBatchId()); + assertEquals("BATCH-001", detail.getBatchCode()); + } + + @Test + public void testGenerateItemConsume_multiBatchFifo() { + // 准备:1 个 BOM 物料,需消耗 30,线边库有 2 个批次(20 + 15) + MesProFeedbackDO feedback = buildFeedback(); + Long bomItemId = randomLongId(); + MesProRouteProductBomDO bom = new MesProRouteProductBomDO(); + bom.setItemId(bomItemId); + bom.setQuantity(new BigDecimal("3")); // 消耗 = 3 × 10 = 30 + when(routeProductBomService.getRouteProductBomList(any(), any(), any())) + .thenReturn(List.of(bom)); + + Long stockId1 = randomLongId(); + Long stockId2 = randomLongId(); + MesWmMaterialStockDO stock1 = new MesWmMaterialStockDO(); + stock1.setId(stockId1); + stock1.setItemId(bomItemId); + stock1.setQuantity(new BigDecimal("20")); // 先进先出:先消耗这批 + stock1.setBatchId(1L); + stock1.setBatchCode("BATCH-001"); + + MesWmMaterialStockDO stock2 = new MesWmMaterialStockDO(); + stock2.setId(stockId2); + stock2.setItemId(bomItemId); + stock2.setQuantity(new BigDecimal("15")); + stock2.setBatchId(2L); + stock2.setBatchCode("BATCH-002"); + + // FIFO 顺序返回 + when(materialStockService.getMaterialStockList(any(MesWmMaterialStockListReqVO.class))) + .thenReturn(Arrays.asList(stock1, stock2)); + + // 调用 + MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback); + + // 断言:生成 2 条带批次的 detail(20 + 10) + assertNotNull(result); + @SuppressWarnings("unchecked") + ArgumentCaptor> detailCaptor = ArgumentCaptor.forClass(List.class); + verify(itemConsumeDetailService).createItemConsumeDetailBatch(detailCaptor.capture()); + List details = detailCaptor.getValue(); + assertEquals(2, details.size()); + + // 第 1 条:批次 1 全部消耗 20 + assertEquals(stockId1, details.get(0).getMaterialStockId()); + assertEquals(0, new BigDecimal("20").compareTo(details.get(0).getQuantity())); + assertEquals("BATCH-001", details.get(0).getBatchCode()); + + // 第 2 条:批次 2 消耗 10(30 - 20 = 10) + assertEquals(stockId2, details.get(1).getMaterialStockId()); + assertEquals(0, new BigDecimal("10").compareTo(details.get(1).getQuantity())); + assertEquals("BATCH-002", details.get(1).getBatchCode()); + } + + @Test + public void testGenerateItemConsume_insufficientStock() { + // 准备:需消耗 30,线边库只有 1 个批次(库存 18),不足部分生成无批次 detail + MesProFeedbackDO feedback = buildFeedback(); + Long bomItemId = randomLongId(); + MesProRouteProductBomDO bom = new MesProRouteProductBomDO(); + bom.setItemId(bomItemId); + bom.setQuantity(new BigDecimal("3")); // 消耗 = 3 × 10 = 30 + when(routeProductBomService.getRouteProductBomList(any(), any(), any())) + .thenReturn(List.of(bom)); + + Long stockId = randomLongId(); + MesWmMaterialStockDO stock = new MesWmMaterialStockDO(); + stock.setId(stockId); + stock.setItemId(bomItemId); + stock.setQuantity(new BigDecimal("18")); // 不够 30 + stock.setBatchId(1L); + stock.setBatchCode("BATCH-001"); + when(materialStockService.getMaterialStockList(any(MesWmMaterialStockListReqVO.class))) + .thenReturn(List.of(stock)); + + // 调用 + MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback); + + // 断言:2 条 detail(18 带批次 + 12 无批次) + assertNotNull(result); + @SuppressWarnings("unchecked") + ArgumentCaptor> detailCaptor = ArgumentCaptor.forClass(List.class); + verify(itemConsumeDetailService).createItemConsumeDetailBatch(detailCaptor.capture()); + List details = detailCaptor.getValue(); + assertEquals(2, details.size()); + + // 第 1 条:批次全部消耗 18 + assertEquals(stockId, details.get(0).getMaterialStockId()); + assertEquals(0, new BigDecimal("18").compareTo(details.get(0).getQuantity())); + assertEquals("BATCH-001", details.get(0).getBatchCode()); + + // 第 2 条:剩余 12 无批次 + assertNull(details.get(1).getMaterialStockId()); + assertEquals(0, new BigDecimal("12").compareTo(details.get(1).getQuantity())); + assertNull(details.get(1).getBatchId()); + } + + @Test + public void testGenerateItemConsume_multipleBomItems() { + // 准备:2 个 BOM 物料 + MesProFeedbackDO feedback = buildFeedback(); + Long itemA = randomLongId(); + Long itemB = randomLongId(); + + MesProRouteProductBomDO bomA = new MesProRouteProductBomDO(); + bomA.setItemId(itemA); + bomA.setQuantity(new BigDecimal("1")); // 消耗 = 1 × 10 = 10 + + MesProRouteProductBomDO bomB = new MesProRouteProductBomDO(); + bomB.setItemId(itemB); + bomB.setQuantity(new BigDecimal("2")); // 消耗 = 2 × 10 = 20 + + when(routeProductBomService.getRouteProductBomList(any(), any(), any())) + .thenReturn(Arrays.asList(bomA, bomB)); + + // 物料 A 有库存,物料 B 无库存 + MesWmMaterialStockDO stockA = new MesWmMaterialStockDO(); + stockA.setId(randomLongId()); + stockA.setItemId(itemA); + stockA.setQuantity(new BigDecimal("100")); + stockA.setBatchId(1L); + stockA.setBatchCode("BATCH-A"); + + when(materialStockService.getMaterialStockList(argThat((MesWmMaterialStockListReqVO req) -> + req != null && itemA.equals(req.getItemId())))) + .thenReturn(List.of(stockA)); + when(materialStockService.getMaterialStockList(argThat((MesWmMaterialStockListReqVO req) -> + req != null && itemB.equals(req.getItemId())))) + .thenReturn(Collections.emptyList()); + + // 调用 + MesWmItemConsumeDO result = itemConsumeService.generateItemConsume(feedback); + + // 断言:2 条消耗行,2 条 detail + assertNotNull(result); + + @SuppressWarnings("unchecked") + ArgumentCaptor> lineCaptor = ArgumentCaptor.forClass(List.class); + verify(itemConsumeLineService).createItemConsumeLineBatch(lineCaptor.capture()); + assertEquals(2, lineCaptor.getValue().size()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> detailCaptor = ArgumentCaptor.forClass(List.class); + verify(itemConsumeDetailService).createItemConsumeDetailBatch(detailCaptor.capture()); + List details = detailCaptor.getValue(); + assertEquals(2, details.size()); + + // 物料 A:有批次 + MesWmItemConsumeDetailDO detailA = details.stream() + .filter(d -> itemA.equals(d.getItemId())).findFirst().orElseThrow(); + assertEquals(0, new BigDecimal("10").compareTo(detailA.getQuantity())); + assertNotNull(detailA.getMaterialStockId()); + assertEquals("BATCH-A", detailA.getBatchCode()); + + // 物料 B:无批次 + MesWmItemConsumeDetailDO detailB = details.stream() + .filter(d -> itemB.equals(d.getItemId())).findFirst().orElseThrow(); + assertEquals(0, new BigDecimal("20").compareTo(detailB.getQuantity())); + assertNull(detailB.getMaterialStockId()); + } + + // ========== finishItemConsume ========== + + @Test + public void testFinishItemConsume_withDetails() { + // 准备:插入一条消耗记录 + MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder() + .workOrderId(randomLongId()).taskId(randomLongId()) + .workstationId(randomLongId()).processId(randomLongId()) + .feedbackId(randomLongId()).consumeDate(java.time.LocalDateTime.now()) + .status(MesWmItemConsumeStatusEnum.PREPARE.getStatus()).build(); + itemConsumeMapper.insert(consume); + + // mock detail 列表 + Long lineId1 = randomLongId(); + Long lineId2 = randomLongId(); + MesWmItemConsumeDetailDO detail1 = MesWmItemConsumeDetailDO.builder() + .consumeId(consume.getId()).lineId(lineId1).itemId(randomLongId()) + .quantity(new BigDecimal("20")).batchId(1L).batchCode("BATCH-001") + .warehouseId(WAREHOUSE_ID).locationId(LOCATION_ID).areaId(AREA_ID).build(); + MesWmItemConsumeDetailDO detail2 = MesWmItemConsumeDetailDO.builder() + .consumeId(consume.getId()).lineId(lineId2).itemId(randomLongId()) + .quantity(new BigDecimal("12")) + .warehouseId(WAREHOUSE_ID).locationId(LOCATION_ID).areaId(AREA_ID).build(); + when(itemConsumeDetailService.getItemConsumeDetailListByConsumeId(consume.getId())) + .thenReturn(Arrays.asList(detail1, detail2)); + + // 调用 + itemConsumeService.finishItemConsume(consume.getId()); + + // 断言 1:状态更新为已完成 + MesWmItemConsumeDO updated = itemConsumeMapper.selectById(consume.getId()); + assertEquals(MesWmItemConsumeStatusEnum.FINISHED.getStatus(), updated.getStatus()); + + // 断言 2:创建了 2 次库存事务 + ArgumentCaptor txCaptor = ArgumentCaptor.forClass(MesWmTransactionSaveReqDTO.class); + verify(wmTransactionService, times(2)).createTransaction(txCaptor.capture()); + List txList = txCaptor.getAllValues(); + + // 验证第 1 条事务 + MesWmTransactionSaveReqDTO tx1 = txList.get(0); + assertEquals(MesWmTransactionTypeEnum.OUT.getType(), tx1.getType()); + assertEquals(detail1.getItemId(), tx1.getItemId()); + assertEquals(0, new BigDecimal("-20").compareTo(tx1.getQuantity())); + assertEquals(1L, tx1.getBatchId()); + assertEquals("BATCH-001", tx1.getBatchCode()); + assertEquals(WAREHOUSE_ID, tx1.getWarehouseId()); + assertFalse(tx1.getCheckFlag()); + assertEquals(MesBizTypeConstants.WM_ITEM_CONSUME, tx1.getBizType()); + assertEquals(consume.getId(), tx1.getBizId()); + assertEquals(lineId1, tx1.getBizLineId()); + + // 验证第 2 条事务(无批次) + MesWmTransactionSaveReqDTO tx2 = txList.get(1); + assertEquals(detail2.getItemId(), tx2.getItemId()); + assertEquals(0, new BigDecimal("-12").compareTo(tx2.getQuantity())); + assertNull(tx2.getBatchId()); + } + + @Test + public void testFinishItemConsume_emptyDetails() { + // 准备:插入一条消耗记录,但没有 detail + MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder() + .workOrderId(randomLongId()).taskId(randomLongId()) + .workstationId(randomLongId()).processId(randomLongId()) + .feedbackId(randomLongId()).consumeDate(java.time.LocalDateTime.now()) + .status(MesWmItemConsumeStatusEnum.PREPARE.getStatus()).build(); + itemConsumeMapper.insert(consume); + + when(itemConsumeDetailService.getItemConsumeDetailListByConsumeId(consume.getId())) + .thenReturn(Collections.emptyList()); + + // 调用 + itemConsumeService.finishItemConsume(consume.getId()); + + // 断言:状态更新为已完成,但无事务创建 + MesWmItemConsumeDO updated = itemConsumeMapper.selectById(consume.getId()); + assertEquals(MesWmItemConsumeStatusEnum.FINISHED.getStatus(), updated.getStatus()); + verify(wmTransactionService, never()).createTransaction(any()); + } + + // ========== getByFeedbackId ========== + + @Test + public void testGetByFeedbackId_exists() { + Long feedbackId = randomLongId(); + MesWmItemConsumeDO consume = MesWmItemConsumeDO.builder() + .workOrderId(randomLongId()).taskId(randomLongId()) + .workstationId(randomLongId()).processId(randomLongId()) + .feedbackId(feedbackId).consumeDate(java.time.LocalDateTime.now()) + .status(MesWmItemConsumeStatusEnum.PREPARE.getStatus()).build(); + itemConsumeMapper.insert(consume); + + MesWmItemConsumeDO result = itemConsumeService.getByFeedbackId(feedbackId); + assertNotNull(result); + assertEquals(consume.getId(), result.getId()); + } + + @Test + public void testGetByFeedbackId_notExists() { + MesWmItemConsumeDO result = itemConsumeService.getByFeedbackId(randomLongId()); + assertNull(result); + } + +} diff --git a/yudao-module-mes/src/test/resources/sql/create_tables.sql b/yudao-module-mes/src/test/resources/sql/create_tables.sql index 7ec75ec..5655285 100644 --- a/yudao-module-mes/src/test/resources/sql/create_tables.sql +++ b/yudao-module-mes/src/test/resources/sql/create_tables.sql @@ -519,3 +519,70 @@ CREATE TABLE IF NOT EXISTS "mes_wm_item_receipt_detail" ( "tenant_id" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id") ); + +-- ---------------------------- +-- MES 物料消耗记录表 +-- ---------------------------- +CREATE TABLE IF NOT EXISTS "mes_wm_item_consume" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "work_order_id" bigint NOT NULL, + "task_id" bigint NOT NULL, + "workstation_id" bigint NOT NULL, + "process_id" bigint NOT NULL, + "feedback_id" bigint NOT NULL, + "consume_date" timestamp NOT NULL, + "status" tinyint NOT NULL DEFAULT 0, + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +-- ---------------------------- +-- MES 物料消耗记录行表 +-- ---------------------------- +CREATE TABLE IF NOT EXISTS "mes_wm_item_consume_line" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "consume_id" bigint NOT NULL, + "item_id" bigint NOT NULL, + "quantity" decimal(14,2) NOT NULL, + "batch_id" bigint DEFAULT NULL, + "batch_code" varchar(128) DEFAULT NULL, + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +-- ---------------------------- +-- MES 物料消耗记录明细表 +-- ---------------------------- +CREATE TABLE IF NOT EXISTS "mes_wm_item_consume_detail" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "consume_id" bigint NOT NULL, + "line_id" bigint NOT NULL, + "material_stock_id" bigint DEFAULT NULL, + "item_id" bigint NOT NULL, + "quantity" decimal(14,2) NOT NULL, + "batch_id" bigint DEFAULT NULL, + "batch_code" varchar(128) DEFAULT NULL, + "warehouse_id" bigint NOT NULL, + "location_id" bigint NOT NULL, + "area_id" bigint NOT NULL, + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +);