云
4 天以前 5d818f39c862884b7087488d588fef5b7ce47386
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java
@@ -5,21 +5,30 @@
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.mdm.MesMdmItemDO;
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.MesWmQualityStatusEnum;
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.mes.service.mdm.MesMdmItemService;
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;
@@ -29,17 +38,20 @@
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 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
@@ -55,9 +67,15 @@
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private MesMdmItemService mdmItemService;
    private MdmItemApi mdmItemApi;
    @Resource
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesWmTransactionService wmTransactionService;
    @Resource
    private MesWmProductSalesMapper productSalesMapper;
    @Resource
    private MesWmProductSalesLineService productSalesLineService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -76,9 +94,14 @@
    @Transactional(rollbackFor = Exception.class)
    public void updateReturnSales(MesWmReturnSalesSaveReqVO updateReqVO) {
        // 1.1 校验存在 + 准备中状态
        validateReturnSalesExistsAndPrepare(updateReqVO.getId());
        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);
@@ -111,24 +134,113 @@
    }
    @Override
    public PageResult<MesWmProductSalesDO> getReturnableProductSalesPage(MesWmProductSalesPageReqVO pageReqVO) {
        List<Long> permittedClientIds = customerApi.getPermittedCustomerIds();
        return productSalesMapper.selectReturnablePage(pageReqVO,
                MesWmProductSalesStatusEnum.FINISHED.getStatus(),
                MesWmReturnSalesStatusEnum.CANCELED.getStatus(), permittedClientIds);
    }
    @Override
    public List<MesWmReturnableProductSalesLineRespVO> 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<MesWmProductSalesLineDO> lines = productSalesLineService.getProductSalesLineListBySalesId(productSalesId);
        if (CollUtil.isEmpty(lines)) {
            return Collections.emptyList();
        }
        Map<Long, BigDecimal> returnedMap = returnSalesLineService.getReturnedQuantityMap(
                convertList(lines, MesWmProductSalesLineDO::getId), null);
        // 仅保留可退数量大于 0 的出库行
        List<MesWmProductSalesLineDO> 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<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(returnableLines, MesWmProductSalesLineDO::getItemId));
        Map<Long, MdmUnitMeasureRespDTO> 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 校验存在 + 草稿状态
        validateReturnSalesExistsAndPrepare(id);
        MesWmReturnSalesDO returnSales = validateReturnSalesExistsAndPrepare(id);
        // 1.2 校验至少有一条行
        List<MesWmReturnSalesLineDO> 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.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 提交(草稿 -> 待检验/待执行)
        // 2. 提交(草稿 -> 待执行);销售仓库流转不经过质检
        returnSalesMapper.updateById(new MesWmReturnSalesDO()
                .setId(id).setStatus(targetStatus));
                .setId(id).setStatus(MesWmReturnSalesStatusEnum.APPROVING.getStatus()));
    }
    /**
     * 校验退货单所有行均来自所选的销售出库单,且本单各行数量之和不超过可退数量
     */
    private void validateLinesAgainstProductSales(MesWmReturnSalesDO returnSales,
                                                  List<MesWmReturnSalesLineDO> lines) {
        if (CollUtil.contains(lines, line -> line.getProductSalesLineId() == null)) {
            throw exception(WM_RETURN_SALES_LINE_PRODUCT_SALES_REQUIRED);
        }
        Map<Long, MesWmProductSalesLineDO> outboundLineMap = convertMap(
                productSalesLineService.getProductSalesLineListBySalesId(returnSales.getProductSalesId()),
                MesWmProductSalesLineDO::getId);
        // 汇总本单每个出库行的退货数量
        Map<Long, BigDecimal> 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<Long, BigDecimal> returnedMap = returnSalesLineService.getReturnedQuantityMap(
                convertSet(lines, MesWmReturnSalesLineDO::getProductSalesLineId), returnSales.getId());
        for (Map.Entry<Long, BigDecimal> 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
@@ -171,7 +283,7 @@
                        MesWmReturnSalesDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO);
                // 对比行数量与明细总数量,不满足直接抛出
                if (line.getQuantity().compareTo(totalDetailQuantity) > 0) {
                    MesMdmItemDO item = mdmItemService.validateItemExistsAndEnable(line.getItemId());
                    MdmItemRespDTO item = mdmItemApi.validateItemExistsAndEnable(line.getItemId()).getCheckedData();
                    throw exception(WM_RETURN_SALES_DETAIL_QUANTITY_MISMATCH,
                            item.getCode() + " " + item.getName() + " 未完成上架");
                }
@@ -188,13 +300,24 @@
    private void createTransactionList(MesWmReturnSalesDO returnSales) {
        List<MesWmReturnSalesDetailDO> 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())));
        // 明细未带批次时回退到退货行批次,避免历史数据入库丢批次
        Map<Long, MesWmReturnSalesLineDO> 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
@@ -274,6 +397,21 @@
    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());
    }
}