package cn.iocoder.yudao.module.mes.service.wm.returnissue; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.mes.controller.admin.wm.returnissue.vo.MesWmReturnIssuePageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.returnissue.vo.MesWmReturnIssueSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskIssueDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueDetailDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLineDO; import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskIssueMapper; import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnissue.MesWmReturnIssueMapper; import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnIssueStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService; import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; 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 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.dataobject.wm.transaction.MesWmTransactionDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction.MesWmTransactionMapper; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.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.*; /** * MES 生产退料单 Service 实现类 */ @Service @Validated public class MesWmReturnIssueServiceImpl implements MesWmReturnIssueService { @Resource private MesWmReturnIssueMapper issueMapper; @Resource private MesWmReturnIssueLineService issueLineService; @Resource private MesWmReturnIssueDetailService issueDetailService; @Resource private MesMdWorkstationService workstationService; @Resource private MesProWorkOrderService workOrderService; @Resource private MesProTaskService taskService; @Resource private MesProTaskIssueMapper taskIssueMapper; @Resource private MesMdmItemService mdmItemService; @Resource private MesWmTransactionService wmTransactionService; @Resource private MesWmWarehouseService warehouseService; @Resource private MesWmWarehouseLocationService locationService; @Resource private MesWmWarehouseAreaService areaService; @Resource private MesWmTransactionMapper transactionMapper; @Override @Transactional(rollbackFor = Exception.class) public Long createReturnIssue(MesWmReturnIssueSaveReqVO createReqVO) { // 1. 校验关联数据 validateReturnIssueSaveData(createReqVO); // 2. 从 task 自动带入 workOrderId 和 workstationId(taskId 非必填) if (createReqVO.getTaskId() != null) { MesProTaskDO task = taskService.validateTaskExists(createReqVO.getTaskId()); if (createReqVO.getWorkOrderId() == null) { createReqVO.setWorkOrderId(task.getWorkOrderId()); } if (createReqVO.getWorkstationId() == null) { createReqVO.setWorkstationId(task.getWorkstationId()); } } // 3. 插入主表 MesWmReturnIssueDO issue = BeanUtils.toBean(createReqVO, MesWmReturnIssueDO.class); issue.setStatus(MesWmReturnIssueStatusEnum.PREPARE.getStatus()); issueMapper.insert(issue); return issue.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateReturnIssue(MesWmReturnIssueSaveReqVO updateReqVO) { // 1.1 校验存在 + 准备中状态 MesWmReturnIssueDO oldIssue = validateReturnIssueExistsAndPrepare(updateReqVO.getId()); // 1.2 校验关联数据 validateReturnIssueSaveData(updateReqVO); // 2. 从 task 自动带入 workOrderId 和 workstationId(taskId 非必填) if (updateReqVO.getTaskId() != null) { MesProTaskDO task = taskService.validateTaskExists(updateReqVO.getTaskId()); if (updateReqVO.getWorkOrderId() == null) { updateReqVO.setWorkOrderId(task.getWorkOrderId()); } if (updateReqVO.getWorkstationId() == null) { updateReqVO.setWorkstationId(task.getWorkstationId()); } } // 3. 更新主表 MesWmReturnIssueDO updateObj = BeanUtils.toBean(updateReqVO, MesWmReturnIssueDO.class); issueMapper.updateById(updateObj); // 4. 退料类型变更时,刷新所有行的质量状态 if (ObjUtil.notEqual(oldIssue.getType(), updateReqVO.getType())) { issueLineService.updateReturnIssueQualityStatusByIssueId(updateReqVO.getId(), updateReqVO.getType()); } } @Override @Transactional(rollbackFor = Exception.class) public void deleteReturnIssue(Long id) { // 1. 校验存在 + 准备中状态 validateReturnIssueExistsAndPrepare(id); // 2.1 级联删除明细 issueDetailService.deleteReturnIssueDetailByIssueId(id); // 2.2 级联删除行 issueLineService.deleteReturnIssueLineByIssueId(id); // 2.3 删除主表 issueMapper.deleteById(id); } @Override public MesWmReturnIssueDO getReturnIssue(Long id) { return issueMapper.selectById(id); } @Override public PageResult getReturnIssuePage(MesWmReturnIssuePageReqVO pageReqVO) { return issueMapper.selectPage(pageReqVO); } @Override @Transactional(rollbackFor = Exception.class) public void submitReturnIssue(Long id) { // 1.1 校验存在 + 草稿状态 validateReturnIssueExistsAndPrepare(id); // 1.2 校验至少有一条行 List lines = issueLineService.getReturnIssueLineListByIssueId(id); if (CollUtil.isEmpty(lines)) { throw exception(WM_RETURN_ISSUE_NO_LINE); } // 2.1 确定目标状态:1)有待检验物料 → 待检验状态;2)无待检验物料 → 待上架状态 boolean hasPendingQc = CollUtil.contains(lines, line -> MesWmQualityStatusEnum.PENDING.getStatus().equals(line.getQualityStatus())); Integer targetStatus = hasPendingQc ? MesWmReturnIssueStatusEnum.CONFIRMED.getStatus() : MesWmReturnIssueStatusEnum.APPROVING.getStatus(); // 2.2 提交(草稿 → 待检验/待上架) issueMapper.updateById(new MesWmReturnIssueDO().setId(id).setStatus(targetStatus)); } @Override @Transactional(rollbackFor = Exception.class) public void stockReturnIssue(Long id) { // 1.1 校验存在 + 待上架状态 MesWmReturnIssueDO issue = validateReturnIssueExists(id); if (ObjUtil.notEqual(MesWmReturnIssueStatusEnum.APPROVING.getStatus(), issue.getStatus())) { throw exception(WM_RETURN_ISSUE_NOT_APPROVING); } // 1.2 检查每个行的明细数量是否完成上架 List lines = issueLineService.getReturnIssueLineListByIssueId(id); if (CollUtil.isNotEmpty(lines)) { // 批量查询所有明细 List allDetails = issueDetailService.getReturnIssueDetailListByIssueId(id); Map> detailMap = CollectionUtils.convertMultiMap( allDetails, MesWmReturnIssueDetailDO::getLineId); // 检查每行的明细数量 for (MesWmReturnIssueLineDO line : lines) { List details = detailMap.getOrDefault(line.getId(), Collections.emptyList()); BigDecimal totalDetailQuantity = CollectionUtils.getSumValue(details, MesWmReturnIssueDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO); // 对比行数量与明细总数量,不满足直接抛出 if (line.getQuantity().compareTo(totalDetailQuantity) > 0) { MesMdmItemDO item = mdmItemService.validateItemExistsAndEnable(line.getItemId()); throw exception(WM_RETURN_ISSUE_DETAIL_QUANTITY_MISMATCH, item.getCode() + " " + item.getName() + " 未完成上架"); } } } // 2. 入库上架(待上架 → 待执行退料) issueMapper.updateById(new MesWmReturnIssueDO() .setId(id).setStatus(MesWmReturnIssueStatusEnum.APPROVED.getStatus())); } @Override @Transactional(rollbackFor = Exception.class) public void finishReturnIssue(Long id) { // 1. 校验存在 + 待执行退料状态 MesWmReturnIssueDO issue = validateReturnIssueExists(id); if (ObjUtil.notEqual(MesWmReturnIssueStatusEnum.APPROVED.getStatus(), issue.getStatus())) { throw exception(WM_RETURN_ISSUE_NOT_APPROVED); } // 2. 遍历所有明细,创建库存事务(增加库存 + 记录流水) createTransactionList(issue); // 3. 扣减任务投料记录的可用量 updateTaskIssueOnReturn(issue); // 4. 更新退料单状态 issueMapper.updateById(new MesWmReturnIssueDO() .setId(id).setStatus(MesWmReturnIssueStatusEnum.FINISHED.getStatus())); } private void createTransactionList(MesWmReturnIssueDO issue) { // 1. 查询虚拟线边库 MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE); MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION); MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA); // 2. 遍历明细,每条明细产生 OUT(虚拟线边库扣减)+ IN(上架目标仓库增加) List details = issueDetailService.getReturnIssueDetailListByIssueId(issue.getId()); for (MesWmReturnIssueDetailDO detail : details) { // 2.1 先从虚拟线边库出库(库存减少) Long outTransactionId = wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId()) .setQuantity(detail.getQuantity().negate()) // 库存减少 .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()) .setWarehouseId(virtualWarehouse.getId()).setLocationId(virtualLocation.getId()).setAreaId(virtualArea.getId()) .setMaterialStockId(detail.getMaterialStockId()) // 指定虚拟线边库库存记录 .setCheckFlag(false) // 线边库允许负库存 .setBizType(MesBizTypeConstants.WM_RETURN_ISSUE).setBizId(issue.getId()) .setBizCode(issue.getCode()).setBizLineId(detail.getLineId())); // 2.2 确定上架目标仓库:detail 的 warehouseId/locationId/areaId 是上架目标(实际仓库) // 如果前端仍传了虚拟线边库(兼容旧数据),则通过事务流水反查原始仓库 Long targetWarehouseId = detail.getWarehouseId(); Long targetLocationId = detail.getLocationId(); Long targetAreaId = detail.getAreaId(); if (virtualWarehouse.getId().equals(targetWarehouseId) && detail.getMaterialStockId() != null) { MesWmTransactionDO originalOutTx = transactionMapper.selectOriginalOutTransaction(detail.getMaterialStockId()); if (originalOutTx != null) { targetWarehouseId = originalOutTx.getWarehouseId(); targetLocationId = originalOutTx.getLocationId(); targetAreaId = originalOutTx.getAreaId(); } } // 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) .setBizType(MesBizTypeConstants.WM_RETURN_ISSUE).setBizId(issue.getId()) .setBizCode(issue.getCode()).setBizLineId(detail.getLineId()) .setRelatedTransactionId(outTransactionId)); // 关联出库事务 } } /** * 退料完成时,扣减任务投料记录的可用量 * 按行遍历,找到对应的 task_issue 记录,扣减 availableQuantity、issuedQuantity */ private void updateTaskIssueOnReturn(MesWmReturnIssueDO issue) { List lines = issueLineService.getReturnIssueLineListByIssueId(issue.getId()); if (CollUtil.isEmpty(lines)) { return; } for (MesWmReturnIssueLineDO line : lines) { Long taskId = line.getTaskId() != null ? line.getTaskId() : issue.getTaskId(); if (taskId == null) { continue; } // 查找该 task + item + 来源为领料出库单 的投料记录 List taskIssues = taskIssueMapper.selectListByTaskId(taskId); MesProTaskIssueDO matchedIssue = CollUtil.findOne(taskIssues, ti -> ti.getItemId().equals(line.getItemId()) && "PRODUCT_ISSUE".equals(ti.getSourceDocType())); if (matchedIssue != null) { // 扣减投料量和可用量 BigDecimal newIssuedQty = matchedIssue.getIssuedQuantity().subtract(line.getQuantity()); BigDecimal newAvailableQty = matchedIssue.getAvailableQuantity().subtract(line.getQuantity()); matchedIssue.setIssuedQuantity(newIssuedQty.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : newIssuedQty); matchedIssue.setAvailableQuantity(newAvailableQty.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : newAvailableQty); taskIssueMapper.updateById(matchedIssue); } } } @Override @Transactional(rollbackFor = Exception.class) public void cancelReturnIssue(Long id) { // 校验存在 MesWmReturnIssueDO issue = validateReturnIssueExists(id); // 已完成和已取消不允许取消 if (ObjectUtils.equalsAny(issue.getStatus(), MesWmReturnIssueStatusEnum.FINISHED.getStatus(), MesWmReturnIssueStatusEnum.CANCELED.getStatus())) { throw exception(WM_RETURN_ISSUE_CANCEL_NOT_ALLOWED); } // 取消 issueMapper.updateById(new MesWmReturnIssueDO() .setId(id).setStatus(MesWmReturnIssueStatusEnum.CANCELED.getStatus())); } @Override public MesWmReturnIssueDO validateReturnIssueExists(Long id) { MesWmReturnIssueDO issue = issueMapper.selectById(id); if (issue == null) { throw exception(WM_RETURN_ISSUE_NOT_EXISTS); } return issue; } /** * 校验生产退料单存在且为准备中状态 */ @Override public MesWmReturnIssueDO validateReturnIssueExistsAndPrepare(Long id) { MesWmReturnIssueDO issue = validateReturnIssueExists(id); if (ObjUtil.notEqual(MesWmReturnIssueStatusEnum.PREPARE.getStatus(), issue.getStatus())) { throw exception(WM_RETURN_ISSUE_NOT_PREPARE); } return issue; } @Override public void updateReturnIssueStatus(Long id, Integer status) { validateReturnIssueExists(id); issueMapper.updateById(new MesWmReturnIssueDO().setId(id).setStatus(status)); } /** * 校验编码唯一性 */ private void validateCodeUnique(Long id, String code) { MesWmReturnIssueDO issue = issueMapper.selectByCode(code); if (issue == null) { return; } if (ObjUtil.notEqual(id, issue.getId())) { throw exception(WM_RETURN_ISSUE_CODE_DUPLICATE); } } /** * 校验保存时的关联数据 */ private void validateReturnIssueSaveData(MesWmReturnIssueSaveReqVO reqVO) { validateCodeUnique(reqVO.getId(), reqVO.getCode()); // 校验 task 存在(taskId 非必填,传了才校验) if (reqVO.getTaskId() != null) { MesProTaskDO task = taskService.validateTaskExists(reqVO.getTaskId()); // 校验 task 关联的工单已确认 Long workOrderId = reqVO.getWorkOrderId() != null ? reqVO.getWorkOrderId() : task.getWorkOrderId(); if (workOrderId != null) { workOrderService.validateWorkOrderConfirmed(workOrderId); } } else if (reqVO.getWorkOrderId() != null) { // 没传 taskId 但传了 workOrderId,校验工单已确认 workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId()); } if (reqVO.getWorkstationId() != null) { workstationService.validateWorkstationExistsAndEnable(reqVO.getWorkstationId()); } } }