package cn.iocoder.yudao.module.mes.service.wm.returnsales; 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.collection.MapUtils; 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.productsales.vo.MesWmProductSalesPageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnableProductSalesLineRespVO; 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.wm.productsales.MesWmProductSalesDO; import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO; 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.productsales.MesWmProductSalesMapper; 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.MesWmProductSalesStatusEnum; 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.mdm.api.item.MdmItemApi; import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi; import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO; import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesLineService; 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.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; 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.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; 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 MdmItemApi mdmItemApi; @Resource private MdmUnitMeasureApi mdmUnitMeasureApi; @Resource private MesWmTransactionService wmTransactionService; @Resource private MesWmProductSalesMapper productSalesMapper; @Resource private MesWmProductSalesLineService productSalesLineService; @Override @Transactional(rollbackFor = Exception.class) public Long createReturnSales(MesWmReturnSalesSaveReqVO createReqVO) { // 1. 校验关联数据 validateSaveData(createReqVO); // 2. 插入主表 MesWmReturnSalesDO returnSales = BeanUtils.toBean(createReqVO, MesWmReturnSalesDO.class); returnSales.setStatus(MesWmReturnSalesStatusEnum.PREPARE.getStatus()); returnSalesMapper.insert(returnSales); return returnSales.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateReturnSales(MesWmReturnSalesSaveReqVO updateReqVO) { // 1.1 校验存在 + 准备中状态 MesWmReturnSalesDO oldReturnSales = validateReturnSalesExistsAndPrepare(updateReqVO.getId()); // 1.2 校验关联数据 validateSaveData(updateReqVO); // 1.3 已有退货行时,不允许更换销售出库单 if (ObjUtil.notEqual(oldReturnSales.getProductSalesId(), updateReqVO.getProductSalesId()) && CollUtil.isNotEmpty(returnSalesLineService.getReturnSalesLineListByReturnId(updateReqVO.getId()))) { throw exception(WM_RETURN_SALES_PRODUCT_SALES_IMMUTABLE); } // 2. 更新主表 MesWmReturnSalesDO updateObj = BeanUtils.toBean(updateReqVO, MesWmReturnSalesDO.class); returnSalesMapper.updateById(updateObj); } @Override @Transactional(rollbackFor = Exception.class) public void deleteReturnSales(Long id) { // 1. 校验存在 + 准备中状态 validateReturnSalesExistsAndPrepare(id); // 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 public PageResult getReturnableProductSalesPage(MesWmProductSalesPageReqVO pageReqVO) { List permittedClientIds = customerApi.getPermittedCustomerIds(); return productSalesMapper.selectReturnablePage(pageReqVO, MesWmProductSalesStatusEnum.FINISHED.getStatus(), MesWmReturnSalesStatusEnum.CANCELED.getStatus(), permittedClientIds); } @Override public List getReturnableProductSalesLineList(Long productSalesId) { if (productSalesId == null) { return Collections.emptyList(); } MesWmProductSalesDO productSales = productSalesMapper.selectById(productSalesId); if (productSales == null) { throw exception(WM_RETURN_SALES_PRODUCT_SALES_NOT_EXISTS); } List lines = productSalesLineService.getProductSalesLineListBySalesId(productSalesId); if (CollUtil.isEmpty(lines)) { return Collections.emptyList(); } Map returnedMap = returnSalesLineService.getReturnedQuantityMap( convertList(lines, MesWmProductSalesLineDO::getId), null); // 仅保留可退数量大于 0 的出库行 List returnableLines = lines.stream() .filter(line -> line.getQuantity() != null && line.getQuantity().subtract(returnedMap.getOrDefault(line.getId(), BigDecimal.ZERO)) .compareTo(BigDecimal.ZERO) > 0) .toList(); if (CollUtil.isEmpty(returnableLines)) { return Collections.emptyList(); } Map itemMap = mdmItemApi.getItemMap( convertSet(returnableLines, MesWmProductSalesLineDO::getItemId)); Map unitMap = CollectionUtils.convertMap( mdmUnitMeasureApi.getUnitMeasureList(convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)) .getCheckedData(), MdmUnitMeasureRespDTO::getId); return returnableLines.stream().map(line -> { MesWmReturnableProductSalesLineRespVO vo = new MesWmReturnableProductSalesLineRespVO(); BigDecimal returned = returnedMap.getOrDefault(line.getId(), BigDecimal.ZERO); vo.setProductSalesLineId(line.getId()); vo.setSalesId(line.getSalesId()); vo.setItemId(line.getItemId()); vo.setBatchId(line.getBatchId()); vo.setBatchCode(line.getBatchCode()); vo.setOutboundQuantity(line.getQuantity()); vo.setReturnedQuantity(returned); vo.setReturnableQuantity(line.getQuantity().subtract(returned)); MapUtils.findAndThen(itemMap, line.getItemId(), item -> { vo.setItemCode(item.getCode()); vo.setItemName(item.getName()); vo.setItemSpecification(item.getSpecification()); MapUtils.findAndThen(unitMap, item.getUnitMeasureId(), unit -> vo.setItemUnit(unit.getName())); }); return vo; }).toList(); } @Override @Transactional(rollbackFor = Exception.class) public void submitReturnSales(Long id) { // 1.1 校验存在 + 草稿状态 MesWmReturnSalesDO returnSales = validateReturnSalesExistsAndPrepare(id); // 1.2 校验至少有一条行 List lines = returnSalesLineService.getReturnSalesLineListByReturnId(id); if (CollUtil.isEmpty(lines)) { throw exception(WM_RETURN_SALES_NO_LINE); } // 1.3 关联出售库单时,重新校验行归属与可退数量(草稿期间可能已被其它退货单占用) if (returnSales.getProductSalesId() != null) { validateLinesAgainstProductSales(returnSales, lines); } // 2. 提交(草稿 -> 待执行);销售仓库流转不经过质检 returnSalesMapper.updateById(new MesWmReturnSalesDO() .setId(id).setStatus(MesWmReturnSalesStatusEnum.APPROVING.getStatus())); } /** * 校验退货单所有行均来自所选的销售出库单,且本单各行数量之和不超过可退数量 */ private void validateLinesAgainstProductSales(MesWmReturnSalesDO returnSales, List lines) { if (CollUtil.contains(lines, line -> line.getProductSalesLineId() == null)) { throw exception(WM_RETURN_SALES_LINE_PRODUCT_SALES_REQUIRED); } Map outboundLineMap = convertMap( productSalesLineService.getProductSalesLineListBySalesId(returnSales.getProductSalesId()), MesWmProductSalesLineDO::getId); // 汇总本单每个出库行的退货数量 Map lineQuantityMap = new HashMap<>(); for (MesWmReturnSalesLineDO line : lines) { if (!outboundLineMap.containsKey(line.getProductSalesLineId())) { throw exception(WM_RETURN_SALES_LINE_PRODUCT_SALES_NOT_MATCH); } lineQuantityMap.merge(line.getProductSalesLineId(), line.getQuantity(), BigDecimal::add); } // 已退数量排除本单,避免把本单自身计入 Map returnedMap = returnSalesLineService.getReturnedQuantityMap( convertSet(lines, MesWmReturnSalesLineDO::getProductSalesLineId), returnSales.getId()); for (Map.Entry entry : lineQuantityMap.entrySet()) { BigDecimal returnable = outboundLineMap.get(entry.getKey()).getQuantity() .subtract(returnedMap.getOrDefault(entry.getKey(), BigDecimal.ZERO)); if (entry.getValue().compareTo(returnable) > 0) { throw exception(WM_RETURN_SALES_LINE_QUANTITY_EXCEED); } } } @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. 执行退货(待执行 -> 待上架) returnSalesMapper.updateById(new MesWmReturnSalesDO() .setId(id).setStatus(MesWmReturnSalesStatusEnum.APPROVED.getStatus())); } @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) { MdmItemRespDTO item = mdmItemApi.validateItemExistsAndEnable(line.getItemId()).getCheckedData(); throw exception(WM_RETURN_SALES_DETAIL_QUANTITY_MISMATCH, item.getCode() + " " + item.getName() + " 未完成上架"); } } } // 2. 遍历所有明细,创建库存事务(增加库存 + 记录流水) createTransactionList(returnSales); // 3. 更新退货单状态 returnSalesMapper.updateById(new MesWmReturnSalesDO() .setId(id).setStatus(MesWmReturnSalesStatusEnum.FINISHED.getStatus())); } private void createTransactionList(MesWmReturnSalesDO returnSales) { List details = returnSalesDetailService.getReturnSalesDetailListByReturnId(returnSales.getId()); // 明细未带批次时回退到退货行批次,避免历史数据入库丢批次 Map lineMap = convertMap( returnSalesLineService.getReturnSalesLineListByReturnId(returnSales.getId()), MesWmReturnSalesLineDO::getId); wmTransactionService.createTransactionList(convertList(details, detail -> { MesWmReturnSalesLineDO line = lineMap.get(detail.getLineId()); Long batchId = detail.getBatchId() != null ? detail.getBatchId() : line != null ? line.getBatchId() : null; String batchCode = detail.getBatchCode() != null ? detail.getBatchCode() : line != null ? line.getBatchCode() : null; return new MesWmTransactionSaveReqDTO() .setType(MesWmTransactionTypeEnum.IN.getType()).setItemId(detail.getItemId()) .setQuantity(detail.getQuantity()) // 入库数量为正数 .setBatchId(batchId).setBatchCode(batchCode) .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); } // 取消 returnSalesMapper.updateById(new MesWmReturnSalesDO() .setId(id).setStatus(MesWmReturnSalesStatusEnum.CANCELED.getStatus())); } @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) { validateReturnSalesExists(id); returnSalesMapper.updateById(new MesWmReturnSalesDO().setId(id).setStatus(status)); } /** * 校验编码唯一性 */ 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()); // 关联销售出库单时,校验出库单状态与客户一致性,并回填出库单号 if (reqVO.getProductSalesId() == null) { return; } MesWmProductSalesDO productSales = productSalesMapper.selectById(reqVO.getProductSalesId()); if (productSales == null) { throw exception(WM_RETURN_SALES_PRODUCT_SALES_NOT_EXISTS); } if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.FINISHED.getStatus(), productSales.getStatus())) { throw exception(WM_RETURN_SALES_PRODUCT_SALES_NOT_FINISHED); } if (ObjUtil.notEqual(productSales.getClientId(), reqVO.getClientId())) { throw exception(WM_RETURN_SALES_CLIENT_MISMATCH); } reqVO.setProductSalesCode(productSales.getCode()); } }