yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productissue/MesWmProductIssueLineController.java
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueBomItemRespVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLinePageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineRespVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineReturnableRespVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO; @@ -155,6 +156,15 @@ return success(result); } @GetMapping("/returnable-list") @Operation(summary = "获得领料出库单可退料行列表", description = "返回指定工单下已完成领料单的可退料行,可退数量 = 已领数量 - 已退数量") @Parameter(name = "workOrderId", description = "生产工单ID", required = true, example = "1") @PreAuthorize("@ss.hasPermission('mes:wm-product-issue:query')") public CommonResult<List<MesWmProductIssueLineReturnableRespVO>> getReturnableList(@RequestParam("workOrderId") Long workOrderId) { return success(issueLineService.getReturnableList(workOrderId)); } // ==================== 拼接 VO ==================== private List<MesWmProductIssueLineRespVO> buildRespVOList(List<MesWmProductIssueLineDO> list) { yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnissue/vo/line/MesWmReturnIssueLineRespVO.java
@@ -16,6 +16,9 @@ @Schema(description = "退料单 ID", example = "1") private Long issueId; @Schema(description = "领料单行 ID", example = "1") private Long issueLineId; @Schema(description = "生产任务 ID", example = "1") private Long taskId; yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnissue/vo/line/MesWmReturnIssueLineSaveReqVO.java
@@ -18,6 +18,9 @@ @NotNull(message = "退料单 ID 不能为空") private Long issueId; @Schema(description = "领料单行 ID", example = "1") private Long issueLineId; @Schema(description = "生产任务 ID", example = "1") private Long taskId; yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnissue/MesWmReturnIssueLineDO.java
@@ -39,6 +39,13 @@ */ private Long issueId; /** * 领料单行 ID * * 关联 {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueLineDO#getId()} * 用于追踪退料来源 */ private Long issueLineId; /** * 生产任务 ID * * 关联 {@link MesProTaskDO#getId()} yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productissue/MesWmProductIssueLineMapper.java
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueLineDO; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; /** @@ -29,6 +30,10 @@ return selectList(MesWmProductIssueLineDO::getIssueId, issueId); } default List<MesWmProductIssueLineDO> selectListByIssueIds(Collection<Long> issueIds) { return selectList(MesWmProductIssueLineDO::getIssueId, issueIds); } default void deleteByIssueId(Long issueId) { delete(MesWmProductIssueLineDO::getIssueId, issueId); } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productissue/MesWmProductIssueMapper.java
@@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueDO; import org.apache.ibatis.annotations.Mapper; import java.util.List; /** * MES 领料出库单 Mapper */ @@ -29,4 +31,10 @@ return selectOne(MesWmProductIssueDO::getCode, code); } default List<MesWmProductIssueDO> selectListByWorkOrderIdAndStatus(Long workOrderId, Integer status) { return selectList(new LambdaQueryWrapperX<MesWmProductIssueDO>() .eq(MesWmProductIssueDO::getWorkOrderId, workOrderId) .eq(MesWmProductIssueDO::getStatus, status)); } } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/returnissue/MesWmReturnIssueLineMapper.java
@@ -10,6 +10,7 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.Collection; import java.util.List; /** @@ -31,6 +32,10 @@ return selectList(MesWmReturnIssueLineDO::getIssueId, issueId); } default List<MesWmReturnIssueLineDO> selectListByIssueLineIds(Collection<Long> issueLineIds) { return selectList(MesWmReturnIssueLineDO::getIssueLineId, issueLineIds); } default void deleteByIssueId(Long issueId) { delete(MesWmReturnIssueLineDO::getIssueId, issueId); } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java
@@ -117,6 +117,24 @@ Long batchId, String batchCode, Long vendorId, LocalDateTime receiptTime); /** * 创建新的库存记录(不查找已有记录,始终创建新记录) * * 用于退料等场景,需要为每次退料创建独立的库存记录 * * @param itemId 物料编号 * @param warehouseId 仓库编号 * @param locationId 库区编号 * @param areaId 库位编号 * @param batchId 批次编号 * @param batchCode 批次号 * @param vendorId 供应商编号 * @param receiptTime 入库时间(为空则默认当前时间) * @return 新创建的库存记录 */ MesWmMaterialStockDO createMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId, Long batchId, String batchCode, Long vendorId, LocalDateTime receiptTime); /** * 按组合键查找库存记录(只查不创建) * * @param itemId 物料编号 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java
@@ -203,6 +203,13 @@ } // 2. 不存在则新建 return createMaterialStock(itemId, warehouseId, locationId, areaId, batchId, batchCode, vendorId, receiptTime); } @Override public MesWmMaterialStockDO createMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId, Long batchId, String batchCode, Long vendorId, LocalDateTime receiptTime) { // 始终创建新库存记录 MesMdmItemDO item = mdmItemService.validateItemExists(itemId); MesWmMaterialStockDO newStock = MesWmMaterialStockDO.builder() .itemId(itemId).itemTypeId(item.getCategoryId()) @@ -213,7 +220,7 @@ .frozen(false) .build(); materialStockMapper.insert(newStock); // 3. 重新查询,确保拿到数据库生成的完整记录(id、createTime 等) // 重新查询,确保拿到数据库生成的完整记录(id、createTime 等) MesWmMaterialStockDO inserted = materialStockMapper.selectById(newStock.getId()); return inserted != null ? inserted : newStock; } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueLineService.java
@@ -2,6 +2,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLinePageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineReturnableRespVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueLineDO; import jakarta.validation.Valid; @@ -74,4 +75,12 @@ */ MesWmProductIssueLineDO validateProductIssueLineExists(Long id); /** * 获取指定工单下已完成领料单的可退料行列表 * * @param workOrderId 生产工单ID * @return 可退料行列表(可退数量 > 0) */ List<MesWmProductIssueLineReturnableRespVO> getReturnableList(Long workOrderId); } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueLineServiceImpl.java
@@ -2,25 +2,41 @@ import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLinePageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineRespVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineReturnableRespVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueLineDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLineDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.productissue.MesWmProductIssueLineMapper; import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductIssueStatusEnum; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService; import cn.iocoder.yudao.module.mes.service.wm.productissue.MesWmProductIssueDetailService; import cn.iocoder.yudao.module.mes.service.wm.productissue.MesWmProductIssueLineService; import cn.iocoder.yudao.module.mes.service.wm.productissue.MesWmProductIssueService; import cn.iocoder.yudao.module.mes.service.wm.returnissue.MesWmReturnIssueLineService; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_PRODUCT_ISSUE_LINE_ITEM_NOT_IN_BOM; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_PRODUCT_ISSUE_LINE_NOT_EXISTS; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; /** * MES 领料出库单行 Service 实现类 @@ -36,12 +52,17 @@ @Lazy private MesWmProductIssueService issueService; @Resource private MesMdmItemService mdmItemService; private MesMdItemService mdmItemService; @Resource private MesProWorkOrderBomService workOrderBomService; @Lazy private MesWmReturnIssueLineService returnIssueLineService; @Resource private MesMdUnitMeasureService unitMeasureService; @Resource @Lazy private MesWmProductIssueDetailService issueDetailService; @Resource private MesProWorkOrderBomService workOrderBomService; @Override public Long createProductIssueLine(MesWmProductIssueLineSaveReqVO createReqVO) { @@ -114,7 +135,7 @@ // 校验物料在工单 BOM 中 validateItemInWorkOrderBom(reqVO.getIssueId(), reqVO.getItemId()); // 校验物料存在 mdmItemService.mesValidateItemExists(reqVO.getItemId()); mdmItemService.validateItemExists(reqVO.getItemId()); } private void validateItemInWorkOrderBom(Long issueId, Long itemId) { @@ -132,4 +153,68 @@ } } @Override public List<MesWmProductIssueLineReturnableRespVO> getReturnableList(Long workOrderId) { // 1. 查询该工单下已完成的领料单 List<MesWmProductIssueDO> finishedIssues = issueService.getFinishedIssuesByWorkOrderId(workOrderId); if (CollUtil.isEmpty(finishedIssues)) { return Collections.emptyList(); } // 2. 获取这些领料单的所有行 List<Long> issueIds = CollectionUtils.convertList(finishedIssues, MesWmProductIssueDO::getId); List<MesWmProductIssueLineDO> lines = issueLineMapper.selectListByIssueIds(issueIds); if (CollUtil.isEmpty(lines)) { return Collections.emptyList(); } // 3. 批量查询已退数量(按 issueLineId 汇总) List<Long> lineIds = CollectionUtils.convertList(lines, MesWmProductIssueLineDO::getId); List<MesWmReturnIssueLineDO> returnLines = returnIssueLineService.getReturnIssueLineListByIssueLineIds(lineIds); // 按 issueLineId 汇总退料数量 Map<Long, BigDecimal> returnedQtyMap = new java.util.HashMap<>(); for (MesWmReturnIssueLineDO returnLine : returnLines) { if (returnLine.getIssueLineId() != null && returnLine.getQuantity() != null) { returnedQtyMap.merge(returnLine.getIssueLineId(), returnLine.getQuantity(), BigDecimal::add); } } // 4. 批量查询物料和单位信息 Map<Long, MesMdItemDO> itemMap = mdmItemService.getItemMap( CollectionUtils.convertSet(lines, MesWmProductIssueLineDO::getItemId)); Map<Long, MesMdUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap( CollectionUtils.convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId)); // 5. 组装结果,过滤可退数量 > 0 的行 List<MesWmProductIssueLineReturnableRespVO> result = new ArrayList<>(); for (MesWmProductIssueLineDO line : lines) { BigDecimal issuedQty = line.getQuantity() != null ? line.getQuantity() : BigDecimal.ZERO; BigDecimal returnedQty = returnedQtyMap.getOrDefault(line.getId(), BigDecimal.ZERO); BigDecimal returnableQty = issuedQty.subtract(returnedQty); if (returnableQty.compareTo(BigDecimal.ZERO) > 0) { MesWmProductIssueLineReturnableRespVO vo = new MesWmProductIssueLineReturnableRespVO(); vo.setId(line.getId()); vo.setItemId(line.getItemId()); vo.setBatchId(line.getBatchId()); vo.setQuantity(returnableQty); // 填充物料信息 MesMdItemDO item = itemMap.get(line.getItemId()); if (item != null) { vo.setItemCode(item.getCode()); vo.setItemName(item.getName()); vo.setSpecification(item.getSpecification()); // 填充单位信息 MesMdUnitMeasureDO unit = unitMap.get(item.getUnitMeasureId()); vo.setUnitMeasureName(unit != null ? unit.getName() : null); } result.add(vo); } } return result; } } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueService.java
@@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueDO; import jakarta.validation.Valid; import java.util.List; /** * MES 领料出库单 Service 接口 */ @@ -101,4 +103,12 @@ */ Boolean checkProductIssueQuantity(Long id); /** * 获取指定工单下已完成的领料单列表 * * @param workOrderId 生产工单ID * @return 已完成的领料单列表 */ List<MesWmProductIssueDO> getFinishedIssuesByWorkOrderId(Long workOrderId); } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueServiceImpl.java
@@ -410,4 +410,9 @@ } } @Override public List<MesWmProductIssueDO> getFinishedIssuesByWorkOrderId(Long workOrderId) { return issueMapper.selectListByWorkOrderIdAndStatus(workOrderId, MesWmProductIssueStatusEnum.FINISHED.getStatus()); } } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueLineService.java
@@ -8,6 +8,7 @@ import jakarta.validation.Valid; import java.math.BigDecimal; import java.util.Collection; import java.util.List; /** @@ -104,4 +105,12 @@ void updateReturnIssueLineWhenRqcFinish(Long sourceLineId, Long sourceDocId, Integer checkResult, BigDecimal qualifiedQuantity, BigDecimal unqualifiedQuantity); /** * 根据领料单行ID列表获取退料单行列表 * * @param issueLineIds 领料单行ID列表 * @return 退料单行列表 */ List<MesWmReturnIssueLineDO> getReturnIssueLineListByIssueLineIds(Collection<Long> issueLineIds); } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueLineServiceImpl.java
@@ -20,6 +20,8 @@ import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -205,4 +207,12 @@ } } @Override public List<MesWmReturnIssueLineDO> getReturnIssueLineListByIssueLineIds(Collection<Long> issueLineIds) { if (CollUtil.isEmpty(issueLineIds)) { return Collections.emptyList(); } return issueLineMapper.selectListByIssueLineIds(issueLineIds); } } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueServiceImpl.java
@@ -268,12 +268,13 @@ } } // 2.3 入上架目标仓库(库存增加) // 2.3 入上架目标仓库(库存增加)— 始终创建新的库存记录,不更新已有记录 wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(detail.getItemId()) .setQuantity(detail.getQuantity()) // 库存增加(退料回仓) .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()) .setWarehouseId(targetWarehouseId).setLocationId(targetLocationId).setAreaId(targetAreaId) .setForceCreateNewStock(true) // 强制创建新库存记录 .setBizType(MesBizTypeConstants.WM_RETURN_ISSUE).setBizId(issue.getId()) .setBizCode(issue.getCode()).setBizLineId(detail.getLineId()) .setRelatedTransactionId(outTransactionId)); // 关联出库事务 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java
@@ -81,6 +81,8 @@ MesWmMaterialStockDO materialStock; MesWmTransactionTypeEnum transactionType = MesWmTransactionTypeEnum.valueOf(reqDTO.getType()); boolean isInbound = transactionType != null && transactionType.isInbound(); boolean forceCreateNewStock = Boolean.TRUE.equals(reqDTO.getForceCreateNewStock()); if (reqDTO.getMaterialStockId() != null) { // 传入了 materialStockId,直接使用 materialStock = materialStockService.getMaterialStock(reqDTO.getMaterialStockId()); @@ -95,10 +97,16 @@ throw exception(WM_MATERIAL_STOCK_SELECTION_MISMATCH); } } else if (isInbound) { // 入库事务:找不到则自动创建库存记录 // 入库事务:根据 forceCreateNewStock 标志决定是创建新记录还是查找/创建 if (forceCreateNewStock) { materialStock = materialStockService.createMaterialStock( reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(), reqDTO.getBatchId(), reqDTO.getBatchCode(), reqDTO.getVendorId(), reqDTO.getReceiptTime()); } else { materialStock = materialStockService.getOrCreateMaterialStock( reqDTO.getItemId(), reqDTO.getWarehouseId(), reqDTO.getLocationId(), reqDTO.getAreaId(), reqDTO.getBatchId(), reqDTO.getBatchCode(), reqDTO.getVendorId(), reqDTO.getReceiptTime()); } } else { // 出库事务:库存记录必须已存在,不允许自动创建(除非 checkFlag=false 允许负库存) materialStock = materialStockService.getMaterialStockByCompositeKey( yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java
@@ -107,14 +107,23 @@ private Long materialStockId; /** * 关联事务 ID(成对事务中,入库事务关联出库事务) * 关联事务 ID */ private Long relatedTransactionId; // ===== 可选控制(非存储字段) ===== /** * 是否强制创建新库存记录(仅入库事务有效) * * 为 true 时,入库事务始终创建新的库存记录,不查找已有记录 * 用于退料等场景,需要为每次退料创建独立的库存记录 */ private Boolean forceCreateNewStock; /** * 是否校验库存充足(默认 true;线边库消耗等场景设 false 允许负库存) * 是否校验库存充足(仅出库事务有效) * * 为 true(默认)时,出库数量不能超过现有库存 * 为 false 时,允许负库存(如虚拟线边库场景) */ private Boolean checkFlag;