package cn.iocoder.yudao.module.mes.service.wm.returnsales; import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; 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.returnsales.vo.MesWmReturnSalesImportExcelVO; import cn.iocoder.yudao.module.mes.api.event.MesReturnSalesProgressEvent; import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesImportRespVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesPageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDetailDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesLineDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.returnsales.MesWmReturnSalesMapper; 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.MesWmReturnSalesStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi; import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; /** * MES 销售退货单 Service 实现类 * * @author 超级管理员 */ @Service @Validated public class MesWmReturnSalesServiceImpl implements MesWmReturnSalesService { @Resource private MesWmReturnSalesMapper returnSalesMapper; @Resource private MesWmReturnSalesLineService returnSalesLineService; @Resource private MesWmReturnSalesDetailService returnSalesDetailService; @Resource private CrmCustomerApi customerApi; @Resource private MesMdmItemService mdmItemService; @Resource private MesWmTransactionService wmTransactionService; @Resource private ApplicationEventPublisher eventPublisher; @Override @Transactional(rollbackFor = Exception.class) public Long createReturnSales(MesWmReturnSalesSaveReqVO createReqVO) { // 1. 校验关联数据 validateSaveData(createReqVO); // 2. 插入主表 MesWmReturnSalesDO returnSales = BeanUtils.toBean(createReqVO, MesWmReturnSalesDO.class); returnSales.setAfterSaleReturnId(createReqVO.getAfterSaleReturnId()); returnSales.setStatus(MesWmReturnSalesStatusEnum.PREPARE.getStatus()); returnSalesMapper.insert(returnSales); return returnSales.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateReturnSales(MesWmReturnSalesSaveReqVO updateReqVO) { // 1.1 校验存在 + 准备中状态 MesWmReturnSalesDO existing = validateReturnSalesExistsAndPrepare(updateReqVO.getId()); if (existing.getAfterSaleReturnId() != null) { throw exception(WM_RETURN_SALES_AFTERSALE_LINKED); } // 1.2 校验关联数据 validateSaveData(updateReqVO); // 2. 更新主表 MesWmReturnSalesDO updateObj = BeanUtils.toBean(updateReqVO, MesWmReturnSalesDO.class); updateObj.setAfterSaleReturnId(null); // 来源申请不可通过编辑接口改绑 returnSalesMapper.updateById(updateObj); } @Override @Transactional(rollbackFor = Exception.class) public void deleteReturnSales(Long id) { // 1. 校验存在 + 准备中状态 MesWmReturnSalesDO returnSales = validateReturnSalesExistsAndPrepare(id); if (returnSales.getAfterSaleReturnId() != null) { throw exception(WM_RETURN_SALES_AFTERSALE_LINKED); } // 2.1 级联删除明细 returnSalesDetailService.deleteReturnSalesDetailByReturnId(id); // 2.2 级联删除行 returnSalesLineService.deleteReturnSalesLineByReturnId(id); // 2.3 删除主表 returnSalesMapper.deleteById(id); } @Override public MesWmReturnSalesDO getReturnSales(Long id) { return returnSalesMapper.selectById(id); } @Override public PageResult getReturnSalesPage(MesWmReturnSalesPageReqVO pageReqVO) { List permittedClientIds = customerApi.getPermittedCustomerIds(); return returnSalesMapper.selectPage(pageReqVO, permittedClientIds); } @Override @Transactional(rollbackFor = Exception.class) public void submitReturnSales(Long id) { // 1.1 校验存在 + 草稿状态 validateReturnSalesExistsAndPrepare(id); // 1.2 校验至少有一条行 List lines = returnSalesLineService.getReturnSalesLineListByReturnId(id); if (CollUtil.isEmpty(lines)) { throw exception(WM_RETURN_SALES_NO_LINE); } // 2.1 确定目标状态:1)有待检验物料 → 待检验状态;2)无待检验物料 → 待执行状态 boolean hasPendingQc = CollUtil.contains(lines, line -> MesWmQualityStatusEnum.PENDING.getStatus().equals(line.getQualityStatus())); Integer targetStatus = hasPendingQc ? MesWmReturnSalesStatusEnum.CONFIRMED.getStatus() : MesWmReturnSalesStatusEnum.APPROVING.getStatus(); // 2.2 提交(草稿 -> 待检验/待执行) if (updateStatusIfCurrent(id, MesWmReturnSalesStatusEnum.PREPARE.getStatus(), targetStatus) != 1) { throw exception(WM_RETURN_SALES_STATUS_NOT_PREPARE); } if (!hasPendingQc && validateReturnSalesExists(id).getAfterSaleReturnId() != null) { eventPublisher.publishEvent(new MesReturnSalesProgressEvent(id, MesReturnSalesProgressEvent.Stage.QUALITY_FINISHED, "免检")); } } @Override @Transactional(rollbackFor = Exception.class) public void finishReturnSales(Long id) { // 1.1 校验存在 + 待执行状态 MesWmReturnSalesDO returnSales = validateReturnSalesExists(id); if (ObjUtil.notEqual(MesWmReturnSalesStatusEnum.APPROVING.getStatus(), returnSales.getStatus())) { throw exception(WM_RETURN_SALES_STATUS_NOT_APPROVING); } // 1.2 校验至少有一条行 List lines = returnSalesLineService.getReturnSalesLineListByReturnId(id); if (CollUtil.isEmpty(lines)) { throw exception(WM_RETURN_SALES_NO_LINE); } // 2. 执行退货(待执行 -> 待上架) if (updateStatusIfCurrent(id, MesWmReturnSalesStatusEnum.APPROVING.getStatus(), MesWmReturnSalesStatusEnum.APPROVED.getStatus()) != 1) { throw exception(WM_RETURN_SALES_STATUS_NOT_APPROVING); } } @Override @Transactional(rollbackFor = Exception.class) public void stockReturnSales(Long id) { // 1.1 校验存在 + 待上架状态 MesWmReturnSalesDO returnSales = validateReturnSalesExists(id); if (ObjUtil.notEqual(MesWmReturnSalesStatusEnum.APPROVED.getStatus(), returnSales.getStatus())) { throw exception(WM_RETURN_SALES_STATUS_NOT_APPROVED); } // 1.2 检查每个行的明细数量是否完成上架 List lines = returnSalesLineService.getReturnSalesLineListByReturnId(id); if (CollUtil.isNotEmpty(lines)) { // 批量查询所有明细 List allDetails = returnSalesDetailService.getReturnSalesDetailListByReturnId(id); Map> detailMap = CollectionUtils.convertMultiMap( allDetails, MesWmReturnSalesDetailDO::getLineId); // 检查每行的明细数量 for (MesWmReturnSalesLineDO line : lines) { List details = detailMap.getOrDefault(line.getId(), Collections.emptyList()); BigDecimal totalDetailQuantity = CollectionUtils.getSumValue(details, MesWmReturnSalesDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO); // 对比行数量与明细总数量,不满足直接抛出 if (line.getQuantity().compareTo(totalDetailQuantity) > 0) { MesMdmItemDO item = mdmItemService.validateItemExistsAndEnable(line.getItemId()); throw exception(WM_RETURN_SALES_DETAIL_QUANTITY_MISMATCH, item.getCode() + " " + item.getName() + " 未完成上架"); } } } // 抢占状态后再记库存流水,避免两个并发上架请求重复入库;事务失败会回滚状态。 if (updateStatusIfCurrent(id, MesWmReturnSalesStatusEnum.APPROVED.getStatus(), MesWmReturnSalesStatusEnum.FINISHED.getStatus()) != 1) { throw exception(WM_RETURN_SALES_STATUS_NOT_APPROVED); } createTransactionList(returnSales); if (returnSales.getAfterSaleReturnId() != null) { eventPublisher.publishEvent(new MesReturnSalesProgressEvent(id, MesReturnSalesProgressEvent.Stage.INBOUND_FINISHED, null)); } } private void createTransactionList(MesWmReturnSalesDO returnSales) { List details = returnSalesDetailService.getReturnSalesDetailListByReturnId(returnSales.getId()); wmTransactionService.createTransactionList(convertList(details, detail -> new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(detail.getItemId()) .setQuantity(detail.getQuantity()) // 入库数量为正数 .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode()) .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId()) .setBizType(MesBizTypeConstants.WM_RETURN_SALES).setBizId(returnSales.getId()) .setBizCode(returnSales.getCode()).setBizLineId(detail.getLineId()))); } @Override @Transactional(rollbackFor = Exception.class) public void cancelReturnSales(Long id) { // 校验存在 MesWmReturnSalesDO returnSales = validateReturnSalesExists(id); // 已完成和已取消不允许取消 if (ObjectUtils.equalsAny(returnSales.getStatus(), MesWmReturnSalesStatusEnum.FINISHED.getStatus(), MesWmReturnSalesStatusEnum.CANCELED.getStatus())) { throw exception(WM_RETURN_SALES_CANCEL_NOT_ALLOWED); } // 取消 if (updateStatusIfCurrent(id, returnSales.getStatus(), MesWmReturnSalesStatusEnum.CANCELED.getStatus()) != 1) { throw exception(WM_RETURN_SALES_CANCEL_NOT_ALLOWED); } if (returnSales.getAfterSaleReturnId() != null) { eventPublisher.publishEvent(new MesReturnSalesProgressEvent(id, MesReturnSalesProgressEvent.Stage.CANCELED, null)); } } @Override public Boolean checkReturnSalesQuantity(Long id) { List lines = returnSalesLineService.getReturnSalesLineListByReturnId(id); for (MesWmReturnSalesLineDO line : lines) { List details = returnSalesDetailService.getReturnSalesDetailListByLineId(line.getId()); BigDecimal totalDetailQty = CollectionUtils.getSumValue(details, MesWmReturnSalesDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO); if (line.getQuantity() != null && totalDetailQty.compareTo(line.getQuantity()) != 0) { return false; } } return true; } @Override public MesWmReturnSalesDO validateReturnSalesExists(Long id) { MesWmReturnSalesDO returnSales = returnSalesMapper.selectById(id); if (returnSales == null) { throw exception(WM_RETURN_SALES_NOT_EXISTS); } return returnSales; } /** * 校验销售退货单存在且为准备中状态 */ @Override public MesWmReturnSalesDO validateReturnSalesExistsAndPrepare(Long id) { MesWmReturnSalesDO returnSales = validateReturnSalesExists(id); if (ObjUtil.notEqual(MesWmReturnSalesStatusEnum.PREPARE.getStatus(), returnSales.getStatus())) { throw exception(WM_RETURN_SALES_STATUS_NOT_PREPARE); } return returnSales; } @Override public void updateReturnSalesStatus(Long id, Integer status) { if (updateStatusIfCurrent(id, MesWmReturnSalesStatusEnum.CONFIRMED.getStatus(), status) != 1) { throw exception(WM_RETURN_SALES_RQC_SOURCE_INVALID); } } private int updateStatusIfCurrent(Long id, Integer currentStatus, Integer targetStatus) { return returnSalesMapper.update(null, new LambdaUpdateWrapper() .eq(MesWmReturnSalesDO::getId, id) .eq(MesWmReturnSalesDO::getStatus, currentStatus) .set(MesWmReturnSalesDO::getStatus, targetStatus)); } /** * 校验编码唯一性 */ private void validateCodeUnique(Long id, String code) { MesWmReturnSalesDO returnSales = returnSalesMapper.selectByCode(code); if (returnSales == null) { return; } if (ObjUtil.notEqual(id, returnSales.getId())) { throw exception(WM_RETURN_SALES_CODE_DUPLICATE); } } /** * 校验保存时的关联数据 */ private void validateSaveData(MesWmReturnSalesSaveReqVO reqVO) { validateCodeUnique(reqVO.getId(), reqVO.getCode()); customerApi.validateCustomer(reqVO.getClientId()); } @Override @Transactional(rollbackFor = Exception.class) public MesWmReturnSalesImportRespVO importReturnSalesList(List importList, boolean updateSupport) { if (CollUtil.isEmpty(importList)) { throw exception(WM_RETURN_SALES_IMPORT_LIST_IS_EMPTY); } MesWmReturnSalesImportRespVO respVO = MesWmReturnSalesImportRespVO.builder() .createCodes(new ArrayList<>()).updateCodes(new ArrayList<>()) .failureCodes(new LinkedHashMap<>()).build(); AtomicInteger index = new AtomicInteger(1); importList.forEach(importItem -> { int currentIndex = index.getAndIncrement(); String code = importItem.getCode(); String key = StrUtil.blankToDefault(code, "第 " + currentIndex + " 行"); if (StrUtil.isBlank(code)) { respVO.getFailureCodes().put(key, "单号不能为空"); return; } if (StrUtil.isBlank(importItem.getName())) { respVO.getFailureCodes().put(key, "名称不能为空"); return; } MesWmReturnSalesDO existing = returnSalesMapper.selectByCode(code); if (existing != null && updateSupport) { if (!MesWmReturnSalesStatusEnum.PREPARE.getStatus().equals(existing.getStatus())) { respVO.getFailureCodes().put(key, "单据非草稿状态,不允许覆盖更新"); return; } MesWmReturnSalesDO updateObj = BeanUtils.toBean(importItem, MesWmReturnSalesDO.class); updateObj.setId(existing.getId()); updateObj.setStatus(existing.getStatus()); returnSalesMapper.updateById(updateObj); respVO.getUpdateCodes().add(code); } else if (existing != null) { respVO.getFailureCodes().put(key, "单号已存在:" + code); } else { MesWmReturnSalesDO createObj = BeanUtils.toBean(importItem, MesWmReturnSalesDO.class); createObj.setId(null); createObj.setStatus(MesWmReturnSalesStatusEnum.PREPARE.getStatus()); returnSalesMapper.insert(createObj); respVO.getCreateCodes().add(code); } }); return respVO; } }