xiaoyi
2 天以前 14c80fbd16a51a355138345ecf69df9ca37746bf
完善售后退货联动和订单作废保护
已添加3个文件
已修改15个文件
221 ■■■■■ 文件已修改
sql/mysql/after_sale_finance_draft_menu_20261105.sql 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/after_sale_return_mes_link_20261105.sql 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/after_sale_return_qc_menu_20261105.sql 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApi.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApiImpl.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApi.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesLineCreateDTO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/pom.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApiImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesLineServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/after_sale_finance_draft_menu_20261105.sql
@@ -1,9 +1,24 @@
-- åœ¨è´¢åŠ¡ç®¡ç†ä¸‹å¢žåŠ ç‹¬ç«‹å®¡æ ¸è‰ç¨¿å…¥å£ï¼›è§’è‰²æƒé™åœ¨ç›®æ ‡ç³»ç»Ÿä¸­æ˜¾å¼åˆ†é…ã€‚
SET @finance_parent := (SELECT id FROM system_menu WHERE path='finance' AND type=1 AND deleted=b'0' AND name='财务管理' LIMIT 1);
-- å…¥å£å½’入既有售后管理一级菜单,不新增或修改一级菜单。
-- ç›´æŽ¥æ‰§è¡ŒåŽéœ€æŒ‰ç›®æ ‡çŽ¯å¢ƒè¿ç»´æµç¨‹åˆ·æ–°ç›¸å…³èœå•åŠè§’è‰²æƒé™ç¼“å­˜ã€‚
SET @finance_parent := (SELECT id FROM system_menu WHERE path='/aftersales' AND parent_id=0 AND type=1 AND deleted=b'0' LIMIT 1);
INSERT INTO system_menu (name,permission,type,sort,parent_id,path,icon,component,component_name,status,visible,keep_alive,always_show,creator,updater)
SELECT '售后财务草稿','',2,30,@finance_parent,'after-sale-draft','lucide:file-check','erp/finance/after-sale-draft/index','ErpAfterSaleFinanceDraft',0,b'1',b'1',b'1','1','1'
WHERE @finance_parent IS NOT NULL AND NOT EXISTS(SELECT 1 FROM system_menu WHERE component='erp/finance/after-sale-draft/index' AND deleted=b'0');
SET @finance_page := (SELECT id FROM system_menu WHERE component='erp/finance/after-sale-draft/index' AND deleted=b'0' LIMIT 1);
-- å…¼å®¹å…ˆå‰å®‰è£…到 ERP è´¢åŠ¡ç®¡ç†ä¸‹çš„ç‰ˆæœ¬ï¼Œä»…ç§»åŠ¨å·²çŸ¥çš„æ—§å…¥å£ã€‚
UPDATE system_menu SET parent_id=@finance_parent, path='after-sale-draft', updater='1', update_time=NOW()
WHERE id=@finance_page AND @finance_parent IS NOT NULL AND parent_id IN
 (SELECT id FROM (SELECT f.id FROM system_menu f JOIN system_menu e ON f.parent_id=e.id
   WHERE f.path='finance' AND e.path='/erp' AND e.parent_id=0 AND f.deleted=b'0' AND e.deleted=b'0') old_parent);
-- åŽŸæœ‰è‰ç¨¿ä½¿ç”¨è§’è‰²éœ€è¦å”®åŽç®¡ç†ç›®å½•æ‰èƒ½çœ‹åˆ°å·²æŽˆæƒçš„é¡µé¢ã€‚
INSERT INTO system_role_menu (role_id,menu_id,creator,updater,tenant_id)
SELECT DISTINCT rm.role_id,@finance_parent,'1','1',rm.tenant_id
FROM system_role_menu rm
WHERE @finance_parent IS NOT NULL AND @finance_page IS NOT NULL AND rm.deleted=b'0'
  AND rm.menu_id=@finance_page
  AND NOT EXISTS (SELECT 1 FROM system_role_menu existing
    WHERE existing.role_id=rm.role_id AND existing.tenant_id=rm.tenant_id
      AND existing.menu_id=@finance_parent AND existing.deleted=b'0');
INSERT INTO system_menu (name,permission,type,sort,parent_id,path,icon,status,visible,keep_alive,always_show,creator,updater)
SELECT p.name,p.permission,3,p.sort_no,@finance_page,'','',0,b'1',b'1',b'1','1','1'
FROM (SELECT '草稿查询' name,'erp:after-sale-finance:query' permission,1 sort_no UNION ALL SELECT '草稿审核','erp:after-sale-finance:review',2) p
sql/mysql/after_sale_return_mes_link_20261105.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
-- MES é€€è´§å•关联售后申请。长辉线上和本地已存在此列;
-- å…¶ä»–项目迁移时安全补列,不回填历史数据。
SET @as_return_link_sql := IF(
  (SELECT COUNT(*) FROM information_schema.columns
   WHERE table_schema=DATABASE() AND table_name='mes_wm_return_sales'
     AND column_name='after_sale_return_id')=0,
  'ALTER TABLE mes_wm_return_sales ADD COLUMN after_sale_return_id BIGINT NULL COMMENT ''来源售后退货申请''',
  'SELECT 1');
PREPARE as_return_link_stmt FROM @as_return_link_sql;
EXECUTE as_return_link_stmt;
DEALLOCATE PREPARE as_return_link_stmt;
sql/mysql/after_sale_return_qc_menu_20261105.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
-- å”®åŽå®žç‰©é€€è´§éœ€è¦ RQC æ£€éªŒã€‚仅启用质量管理下已有的退货检验页面,
-- ä¸è°ƒæ•´ä¸€çº§èœå•,不新增用户或角色授权。执行后按环境运维流程刷新权限缓存。
UPDATE system_menu
SET status=0, visible=b'1', updater='1', update_time=NOW()
WHERE component='mes/qc/rqc/index' AND type=2 AND deleted=b'0'
  AND (status<>0 OR visible<>b'1')
  AND parent_id IN (SELECT id FROM (SELECT id FROM system_menu
       WHERE parent_id=0 AND path='/qc' AND deleted=b'0') qc_parent);
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/service/returnobj/AfterSaleReturnServiceImpl.java
@@ -20,6 +20,8 @@
import cn.iocoder.yudao.module.aftersales.service.permission.bo.AfterSalePermissionCreateReqBO;
import cn.iocoder.yudao.module.mes.api.returnsales.MesWmReturnSalesApi;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesLineCreateDTO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -33,6 +35,7 @@
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.module.aftersales.enums.ErrorCodeConstants.*;
/**
@@ -62,6 +65,8 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createReturn(AfterSaleReturnSaveReqVO createReqVO, Long userId) {
        if (Integer.valueOf(3).equals(createReqVO.getReturnType()))
            throw invalidParamException("仅退款流程尚未接入财务审核,暂不能创建退货申请");
        // 1. æ ¡éªŒå·¥å•存在
        AfterSaleTicketDO ticket = ticketMapper.selectById(createReqVO.getTicketId());
        if (ticket == null) {
@@ -122,6 +127,18 @@
        if (!AfterSaleReturnStatusEnum.DRAFT.getStatus().equals(returnDO.getStatus())) {
            throw exception(RETURN_APPROVE_FAIL);
        }
        if (Integer.valueOf(3).equals(returnDO.getReturnType()))
            throw invalidParamException("仅退款流程尚未接入财务审核,暂不能同步为实物退货单");
        List<AfterSaleReturnItemDO> items = returnItemMapper.selectListByReturnId(id);
        if (CollUtil.isEmpty(items) || items.stream().anyMatch(item -> item.getItemId() == null
                || item.getReturnQuantity() == null || item.getReturnQuantity().signum() <= 0))
            throw invalidParamException("请先填写有效的退货物料及数量");
        // åœ¨äº‹åŠ¡ä¸­æŠ¢å è‰ç¨¿çŠ¶æ€ï¼Œé¿å…å¹¶å‘å®¡æ ¸é‡å¤ç”Ÿæˆé€€è´§å•ã€‚
        if (returnMapper.update(null, new LambdaUpdateWrapper<AfterSaleReturnDO>()
                .eq(AfterSaleReturnDO::getId, id)
                .eq(AfterSaleReturnDO::getStatus, AfterSaleReturnStatusEnum.DRAFT.getStatus())
                .set(AfterSaleReturnDO::getStatus, AfterSaleReturnStatusEnum.AUDITING.getStatus())) != 1)
            throw exception(RETURN_APPROVE_FAIL);
        // 2. èŽ·å–å…³è”å·¥å•ï¼ˆç”¨äºŽèŽ·å–é”€å”®è®¢å•å·ç­‰å†—ä½™ä¿¡æ¯ï¼‰
        AfterSaleTicketDO ticket = ticketMapper.selectById(returnDO.getTicketId());
@@ -138,13 +155,22 @@
        mesReqDTO.setRemark("售后工单退货 - å·¥å•号: " + (ticket != null ? ticket.getNo() : ""));
        mesReqDTO.setAfterSaleReturnId(returnDO.getId());
        Long mesReturnId = mesWmReturnSalesApi.createReturnSales(mesReqDTO);
        if (mesReturnId == null) throw invalidParamException("MES é€€è´§å•创建失败");
        for (AfterSaleReturnItemDO item : items) {
            var line = new MesWmReturnSalesLineCreateDTO();
            line.setReturnId(mesReturnId); line.setItemId(item.getItemId()); line.setQuantity(item.getReturnQuantity());
            line.setBatchId(item.getBatchId()); line.setBatchCode(item.getBatchCode()); line.setRemark(item.getRemark());
            Long lineId = mesWmReturnSalesApi.createReturnSalesLine(line);
            if (lineId == null || returnItemMapper.updateById(new AfterSaleReturnItemDO().setId(item.getId()).setMesReturnLineId(lineId)) != 1)
                throw invalidParamException("MES é€€è´§æ˜Žç»†åŒæ­¥å¤±è´¥");
        }
        // 4. æ›´æ–°é€€è´§ç”³è¯·çŠ¶æ€
        returnMapper.updateById(new AfterSaleReturnDO().setId(id)
        if (returnMapper.updateById(new AfterSaleReturnDO().setId(id)
                .setStatus(AfterSaleReturnStatusEnum.APPROVED.getStatus())
                .setMesReturnId(mesReturnId)
                .setMesReturnCode(returnDO.getNo())
                .setAuditTime(LocalDateTime.now()));
                .setAuditTime(LocalDateTime.now())) != 1) throw invalidParamException("更新退货申请失败");
    }
    @Override
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApi.java
@@ -33,6 +33,9 @@
     */
    CommonResult<ErpSaleOrderRespDTO> getSaleOrder(Long id);
    /** åœ¨è°ƒç”¨æ–¹äº‹åŠ¡ä¸­é”å®šå¹¶æ ¡éªŒå¯ç”¨äºŽå‘è´§çš„é”€å”®è®¢å•ã€‚ */
    CommonResult<ErpSaleOrderRespDTO> lockSaleOrderForNotice(Long id, String orderNo);
    /**
     * æ ¹æ®åˆåŒç¼–号获取销售订单
     *
@@ -80,4 +83,4 @@
     */
    CommonResult<Boolean> updateOrderOutStatusTo(Long orderId, Integer outStatus);
}
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApiImpl.java
@@ -13,6 +13,10 @@
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOrderService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import java.util.Objects;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -21,6 +25,7 @@
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
/**
 * ERP é”€å”®è®¢å• API å®žçŽ°ç±»
@@ -57,6 +62,18 @@
    @Override
    public CommonResult<ErpSaleOrderRespDTO> getSaleOrder(Long id) {
        ErpSaleOrderDO order = saleOrderService.getSaleOrder(id);
        return success(BeanUtils.toBean(order, ErpSaleOrderRespDTO.class));
    }
    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public CommonResult<ErpSaleOrderRespDTO> lockSaleOrderForNotice(Long id, String orderNo) {
        if (id == null && (orderNo == null || orderNo.isBlank())) return success(null);
        ErpSaleOrderDO order = id != null ? saleOrderMapper.selectByIdForUpdate(id) : saleOrderMapper.selectByNoForUpdate(orderNo);
        if (order == null || (orderNo != null && !orderNo.isBlank() && !Objects.equals(orderNo, order.getNo())))
            throw invalidParamException("关联销售订单不存在或订单编号不匹配");
        if (!Objects.equals(order.getStatus(), ErpAuditStatus.APPROVE.getStatus()))
            throw invalidParamException("销售订单未审核或已作废,不能生成发货通知");
        return success(BeanUtils.toBean(order, ErpSaleOrderRespDTO.class));
    }
@@ -121,4 +138,4 @@
        return success(true);
    }
}
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
@@ -80,8 +81,16 @@
        return selectOne(ErpSaleOrderDO::getNo, no);
    }
    default ErpSaleOrderDO selectByIdForUpdate(Long id) {
        return selectOne(new LambdaQueryWrapperX<ErpSaleOrderDO>().eq(ErpSaleOrderDO::getId, id).last("FOR UPDATE"));
    }
    default ErpSaleOrderDO selectByNoForUpdate(String no) {
        return selectOne(new LambdaQueryWrapperX<ErpSaleOrderDO>().eq(ErpSaleOrderDO::getNo, no).last("FOR UPDATE"));
    }
    default ErpSaleOrderDO selectByContractId(Long contractId) {
        return selectOne(ErpSaleOrderDO::getContractId, contractId);
    }
}
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -102,6 +102,8 @@
    ErrorCode SALE_ORDER_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_020_201_020, "销售订单审核分类下没有可用的流程模型,请先创建并发布流程");
    ErrorCode SALE_ORDER_CANCEL_FAIL_ACTIVE_MPS = new ErrorCode(1_020_201_021, "作废失败,订单仍有关联生产计划,请先处理并取消生产计划");
    ErrorCode SALE_ORDER_CANCEL_FAIL_MPS_UNKNOWN = new ErrorCode(1_020_201_022, "无法确认关联生产计划状态,请稍后重试");
    ErrorCode SALE_ORDER_CANCEL_FAIL_NOTICE_EXISTS = new ErrorCode(1_020_201_023, "作废失败,订单已生成发货通知,请先处理相关通知单");
    ErrorCode SALE_ORDER_CANCEL_FAIL_NOTICE_UNKNOWN = new ErrorCode(1_020_201_024, "无法确认发货通知状态,请稍后重试");
    // ========== ERP é”€å”®å‡ºåº“(1-030-202-000) ==========
    ErrorCode SALE_OUT_NOT_EXISTS = new ErrorCode(1_020_202_000, "销售出库单不存在");
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -35,6 +35,7 @@
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO;
import cn.iocoder.yudao.module.mes.api.salesnotice.MesWmSalesNoticeApi;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.mzt.logapi.starter.annotation.LogRecord;
@@ -109,6 +110,8 @@
    private MdmItemApi mdmItemApi;
    @Resource
    private MesProMpsApi mpsApi;
    @Resource
    private MesWmSalesNoticeApi salesNoticeApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
@@ -594,7 +597,8 @@
    @Transactional(rollbackFor = Exception.class)
    public void cancelSaleOrder(Long id) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpSaleOrderDO saleOrder = validateSaleOrderExists(id);
        ErpSaleOrderDO saleOrder = saleOrderMapper.selectByIdForUpdate(id);
        if (saleOrder == null) throw exception(SALE_ORDER_NOT_EXISTS);
        // 1.2 æ ¡éªŒçŠ¶æ€ï¼šåªæœ‰å·²å®¡æ ¸çš„è®¢å•æ‰èƒ½ä½œåºŸ
        if (!ErpAuditStatus.APPROVE.getStatus().equals(saleOrder.getStatus())) {
            throw exception(SALE_ORDER_CANCEL_FAIL_NOT_APPROVE, saleOrder.getNo());
@@ -615,6 +619,9 @@
                throw exception(SALE_ORDER_CANCEL_FAIL_ACTIVE_MPS);
            }
        }
        Map<Long, Boolean> noticeStatus = salesNoticeApi.checkSalesNoticeBySaleOrderIds(List.of(id));
        if (noticeStatus == null || noticeStatus.get(id) == null) throw exception(SALE_ORDER_CANCEL_FAIL_NOTICE_UNKNOWN);
        if (Boolean.TRUE.equals(noticeStatus.get(id))) throw exception(SALE_ORDER_CANCEL_FAIL_NOTICE_EXISTS);
        // 2. æ›´æ–°çŠ¶æ€ä¸ºå·²ä½œåºŸ
        int updateCount = saleOrderMapper.updateByIdAndStatus(id, saleOrder.getStatus(),
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApi.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.api.returnsales;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesLineCreateDTO;
/**
 * MES é”€å”®é€€è´§å• API æŽ¥å£
@@ -17,4 +18,6 @@
     */
    Long createReturnSales(MesWmReturnSalesCreateReqDTO reqDTO);
    Long createReturnSalesLine(MesWmReturnSalesLineCreateDTO reqDTO);
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/dto/MesWmReturnSalesLineCreateDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.api.returnsales.dto;
import lombok.Data;
import java.math.BigDecimal;
/** å”®åŽç”³è¯·åŒæ­¥åˆ° MES çš„退货物料行。 */
@Data
public class MesWmReturnSalesLineCreateDTO {
    private Long returnId;
    private Long itemId;
    private BigDecimal quantity;
    private Long batchId;
    private String batchCode;
    private String remark;
}
yudao-module-mes/pom.xml
@@ -20,6 +20,11 @@
    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-aftersales-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-system</artifactId>
            <version>${revision}</version>
        </dependency>
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/returnsales/MesWmReturnSalesApiImpl.java
@@ -2,8 +2,11 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.returnsales.dto.MesWmReturnSalesLineCreateDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.line.MesWmReturnSalesLineSaveReqVO;
import cn.iocoder.yudao.module.mes.service.wm.returnsales.MesWmReturnSalesService;
import cn.iocoder.yudao.module.mes.service.wm.returnsales.MesWmReturnSalesLineService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -17,6 +20,8 @@
    @Resource
    private MesWmReturnSalesService returnSalesService;
    @Resource
    private MesWmReturnSalesLineService returnSalesLineService;
    @Override
    public Long createReturnSales(MesWmReturnSalesCreateReqDTO reqDTO) {
@@ -24,4 +29,12 @@
        return returnSalesService.createReturnSales(createReqVO);
    }
    @Override
    public Long createReturnSalesLine(MesWmReturnSalesLineCreateDTO reqDTO) {
        MesWmReturnSalesLineSaveReqVO line = BeanUtils.toBean(reqDTO, MesWmReturnSalesLineSaveReqVO.class);
        // å”®åŽé€€å›žç‰©æ–™é¡»ç»è¿‡æ£€éªŒï¼Œä¸èƒ½æŒ‰â€œä¸éœ€æ£€éªŒâ€è‡ªåŠ¨åˆ¤ä¸ºåˆæ ¼ã€‚
        line.setRqcCheckFlag(true);
        return returnSalesLineService.createReturnSalesLine(line);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesSaveReqVO.java
@@ -4,6 +4,7 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.time.LocalDate;
@@ -45,6 +46,10 @@
    @NotBlank(message = "退货原因不能为空")
    private String returnReason;
    /** ä»…供服务端售后联动设置,不能从普通仓库退货单接口传入。 */
    @JsonIgnore
    private Long afterSaleReturnId;
    @Schema(description = "备注", example = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java
@@ -60,6 +60,8 @@
     * é€€è´§åŽŸå› 
     */
    private String returnReason;
    /** æ¥æºå”®åŽé€€è´§ç”³è¯·ï¼Œæ™®é€šä»“库退货单为空。 */
    private Long afterSaleReturnId;
    /**
     * çŠ¶æ€
     *
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesLineServiceImpl.java
@@ -14,16 +14,20 @@
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnSalesStatusEnum;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
import cn.iocoder.yudao.module.aftersales.api.AfterSaleTicketApi;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.MD_ITEM_BATCH_REQUIRED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_RETURN_SALES_LINE_NOT_EXISTS;
@@ -49,6 +53,8 @@
    @Resource
    @Lazy
    private MesWmReturnSalesDetailService returnSalesDetailService;
    @Resource
    private ObjectProvider<AfterSaleTicketApi> afterSaleTicketApiProvider;
    @Override
    public Long createReturnSalesLine(MesWmReturnSalesLineSaveReqVO createReqVO) {
@@ -169,6 +175,7 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateReturnSalesLineWhenRqcFinish(Long sourceLineId, Long sourceDocId, Integer checkResult,
                                                    BigDecimal qualifiedQuantity, BigDecimal unqualifiedQuantity) {
        MesWmReturnSalesLineDO sourceLine = validateReturnSalesLineExists(sourceLineId);
@@ -207,10 +214,21 @@
        // 2. æ£€æŸ¥é€€è´§å•下是否还有待检验的行,若全检完则将退货单状态设为"待上架"
        if (sourceDocId != null) {
            List<MesWmReturnSalesLineDO> allLines = returnSalesLineMapper.selectListByReturnId(sourceDocId);
            boolean allInspected = !CollUtil.contains(allLines,
                    line -> Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus()));
            boolean allInspected = CollUtil.isNotEmpty(allLines) && allLines.stream().allMatch(line ->
                    Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.PASS.getStatus())
                    || Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus()));
            if (allInspected) {
                returnSalesService.updateReturnSalesStatus(sourceDocId, MesWmReturnSalesStatusEnum.APPROVING.getStatus());
                MesWmReturnSalesDO source = returnSalesService.validateReturnSalesExists(sourceDocId);
                if (source.getAfterSaleReturnId() != null) {
                    AfterSaleTicketApi api = afterSaleTicketApiProvider.getIfAvailable();
                    if (api == null) throw invalidParamException("售后服务不可用,无法同步退货检验结果");
                    boolean hasFailed = allLines.stream().anyMatch(line ->
                            Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus()));
                    var response = api.onReturnQualityFinished(source.getAfterSaleReturnId(), hasFailed ? "存在不合格" : "合格", null);
                    if (response == null || !Boolean.TRUE.equals(response.getCheckedData()))
                        throw invalidParamException("售后退货检验结果同步失败");
                }
            }
        }
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java
@@ -19,12 +19,14 @@
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.aftersales.api.AfterSaleTicketApi;
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.transaction.annotation.Transactional;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
@@ -33,6 +35,7 @@
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -58,6 +61,8 @@
    private MesMdmItemService mdmItemService;
    @Resource
    private MesWmTransactionService wmTransactionService;
    @Resource
    private ObjectProvider<AfterSaleTicketApi> afterSaleTicketApiProvider;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -183,6 +188,13 @@
        // 3. æ›´æ–°é€€è´§å•状态
        returnSalesMapper.updateById(new MesWmReturnSalesDO()
                .setId(id).setStatus(MesWmReturnSalesStatusEnum.FINISHED.getStatus()));
        if (returnSales.getAfterSaleReturnId() != null) {
            AfterSaleTicketApi api = afterSaleTicketApiProvider.getIfAvailable();
            if (api == null) throw invalidParamException("售后服务不可用,无法同步退货入库状态");
            var response = api.onReturnInboundFinished(returnSales.getAfterSaleReturnId());
            if (response == null || !Boolean.TRUE.equals(response.getCheckedData()))
                throw invalidParamException("售后退货入库状态同步失败");
        }
    }
    private void createTransactionList(MesWmReturnSalesDO returnSales) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/salesnotice/MesWmSalesNoticeServiceImpl.java
@@ -66,7 +66,9 @@
    private MesWmSalesNoticeLineMapper salesNoticeLineMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createSalesNotice(MesWmSalesNoticeSaveReqVO createReqVO) {
        lockReferencedSaleOrder(createReqVO, null);
        // æ ¡éªŒæ•°æ®
        validateSalesNoticeSave(createReqVO);
@@ -78,9 +80,11 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateSalesNotice(MesWmSalesNoticeSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        validateSalesNoticeExistsAndDraft(updateReqVO.getId());
        MesWmSalesNoticeDO existing = validateSalesNoticeExistsAndDraft(updateReqVO.getId());
        lockReferencedSaleOrder(updateReqVO, existing);
        // æ ¡éªŒæ•°æ®
        validateSalesNoticeSave(updateReqVO);
@@ -170,6 +174,21 @@
        }
    }
    /** ä¸Žè®¢å•作废使用同一订单行锁;支持历史请求仅传订单号。 */
    private void lockReferencedSaleOrder(MesWmSalesNoticeSaveReqVO request, MesWmSalesNoticeDO existing) {
        Long orderId = request.getSaleOrderId();
        String orderNo = request.getSaleOrderCode();
        if (orderId == null && (orderNo == null || orderNo.isBlank()) && existing != null) {
            orderId = existing.getSaleOrderId();
            orderNo = existing.getSaleOrderCode();
        }
        if (orderId == null && (orderNo == null || orderNo.isBlank())) return;
        ErpSaleOrderRespDTO order = saleOrderApi.lockSaleOrderForNotice(orderId, orderNo).getCheckedData();
        if (order == null) throw exception(WM_SALES_NOTICE_SALE_ORDER_NOT_EXISTS);
        request.setSaleOrderId(order.getId());
        request.setSaleOrderCode(order.getNo());
    }
    private void validateNoticeCodeUnique(Long id, String code) {
        MesWmSalesNoticeDO notice = salesNoticeMapper.selectByCode(code);
        if (notice == null) {
@@ -183,16 +202,13 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long generateSalesNoticeFromSaleOrder(Long saleOrderId) {
        // å…ˆé”å®šè®¢å•,再检查通知;与订单作废串行执行。
        ErpSaleOrderRespDTO saleOrder = saleOrderApi.lockSaleOrderForNotice(saleOrderId, null).getCheckedData();
        if (saleOrder == null) throw exception(WM_SALES_NOTICE_SALE_ORDER_NOT_EXISTS);
        // 1. æ ¡éªŒæ˜¯å¦å·²ç”Ÿæˆè¿‡å‘货通知单(防止重复生成)
        MesWmSalesNoticeDO existNotice = salesNoticeMapper.selectBySaleOrderId(saleOrderId);
        if (existNotice != null) {
            throw exception(WM_SALES_NOTICE_ALREADY_EXISTS);
        }
        // 2. èŽ·å–é”€å”®è®¢å•ä¿¡æ¯
        ErpSaleOrderRespDTO saleOrder = saleOrderApi.getSaleOrder(saleOrderId).getCheckedData();
        if (saleOrder == null) {
            throw exception(WM_SALES_NOTICE_SALE_ORDER_NOT_EXISTS);
        }
        // 3. èŽ·å–é”€å”®è®¢å•æ˜Žç»†