From 251be431ad12399fe4ebce599c353d05dcec7888 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 29 十月 2026 12:28:34 +0800
Subject: [PATCH] feat(mes): 支持追溯二维码解析功能

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java |  182 ++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 160 insertions(+), 22 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java
index 3435611..557e623 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesServiceImpl.java
+++ b/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());
     }
 
 }

--
Gitblit v1.9.3