From f02d0df2f1dfc7a8b775cf50d45375e57628a5ef Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 10 十一月 2026 11:26:22 +0800
Subject: [PATCH] feat(ncr): 完善NCR处置联动逻辑与关闭校验

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java |   74 ++++++++++++++++++++++++++++++-------
 1 files changed, 60 insertions(+), 14 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
index 6593aa3..69d2d69 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -2,6 +2,8 @@
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
 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;
@@ -9,11 +11,13 @@
 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesPageReqVO;
 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesSaveReqVO;
 import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesShippingReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDetailDO;
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesLineDO;
 import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesMapper;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper;
 import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
 import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductSalesStatusEnum;
@@ -22,6 +26,7 @@
 import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockReserveBizTypeEnum;
 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.md.item.MesMdItemService;
 import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
 import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeService;
 import cn.iocoder.yudao.module.mes.service.wm.stockreserve.MesWmStockReserveService;
@@ -39,6 +44,7 @@
 
 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.util.collection.CollectionUtils.convertList;
@@ -64,6 +70,8 @@
     @Resource
     private MesWmProductSalesDetailService productSalesDetailService;
     @Resource
+    private MesWmOqcConcessionGuard oqcConcessionGuard;
+    @Resource
     private CrmCustomerApi customerApi;
     @Resource
     private MesWmSalesNoticeService salesNoticeService;
@@ -79,6 +87,10 @@
 
     @Resource
     private ErpSaleOrderApi saleOrderApi;
+    @Resource
+    private MesWmSalesReelGuard salesReelGuard;
+    @Resource
+    private MesMdItemService mdItemService;
 
     @Override
     public Long createProductSales(MesWmProductSalesSaveReqVO createReqVO) {
@@ -151,19 +163,7 @@
         boolean needOqc = CollectionUtils.anyMatch(lines,
                 line -> Boolean.TRUE.equals(line.getOqcCheckFlag()));
         if (needOqc) {
-            // 涓洪渶瑕� OQC 妫�楠屼笖灏氭湭鍒涘缓妫�楠屽崟鐨勬槑缁嗗垱寤� OQC 妫�楠屽崟
-            List<MesWmProductSalesLineDO> oqcLines = CollectionUtils.filterList(lines,
-                    line -> Boolean.TRUE.equals(line.getOqcCheckFlag()) && line.getOqcId() == null);
-            for (MesWmProductSalesLineDO line : oqcLines) {
-                // 鐩存帴璋冪敤锛岃寮傚父鎶涘嚭锛屼繚璇佹暟鎹竴鑷存��
-                Long oqcId = oqcService.createOqcFromProductSales(
-                        sales.getId(), line, sales.getCode(), sales.getClientId());
-                // 鏇存柊鍑哄簱鏄庣粏鐨� oqcId
-                productSalesLineMapper.updateById(new MesWmProductSalesLineDO()
-                        .setId(line.getId())
-                        .setOqcId(oqcId));
-            }
-
+            // 妫�楠屽崟鐢辫川妫�浜哄憳鍏堣褰曞叆锛屽緟妫�鏄庣粏閫氳繃鐩樺彿鍏宠仈宸插畬鎴愮殑妫�楠屽崟銆�
             // 鎵归噺鍒濆鍖� OQC 琛岀殑璐ㄩ噺鐘舵�佷负"寰呮楠�"
             List<Long> oqcLineIds = convertList(
                     CollectionUtils.filterList(lines, line -> Boolean.TRUE.equals(line.getOqcCheckFlag())),
@@ -201,7 +201,10 @@
     @Transactional(rollbackFor = Exception.class)
     public void stockProductSales(Long id) {
         // 鏍¢獙瀛樺湪
-        MesWmProductSalesDO sales = validateProductSalesExists(id);
+        validateProductSalesExists(id);
+        MesWmProductSalesDO sales = productSalesMapper.selectOne(new LambdaQueryWrapperX<MesWmProductSalesDO>()
+                .eq(MesWmProductSalesDO::getId, id).last("FOR UPDATE"));
+        if (sales == null) throw exception(WM_PRODUCT_SALES_NOT_EXISTS);
         if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.APPROVING.getStatus(), sales.getStatus())) {
             throw exception(WM_PRODUCT_SALES_CANNOT_PICK);
         }
@@ -212,8 +215,12 @@
             if (CollUtil.isEmpty(details)) {
                 throw exception(WM_PRODUCT_SALES_DETAILS_EMPTY);
             }
+            oqcConcessionGuard.validateQuantity(line, CollectionUtils.getSumValue(details,
+                    MesWmProductSalesDetailDO::getQuantity, BigDecimal::add, BigDecimal.ZERO));
             // 鏍¢獙搴撳瓨鍏呰冻骞跺垱寤哄崰鐢�
             for (MesWmProductSalesDetailDO detail : details) {
+                salesReelGuard.validate(line, detail.getBatchId());
+                oqcConcessionGuard.validate(line, detail.getMaterialStockId(), detail.getBatchCode(), detail.getQuantity());
                 materialStockService.validateSelectedStock(
                         detail.getMaterialStockId(),
                         detail.getItemId(),
@@ -343,6 +350,20 @@
 
     private void createTransactionList(MesWmProductSalesDO sales) {
         List<MesWmProductSalesDetailDO> details = productSalesDetailService.getProductSalesDetailListBySalesId(sales.getId());
+        for (MesWmProductSalesLineDO line : productSalesLineService.getProductSalesLineListBySalesId(sales.getId())) {
+            BigDecimal quantity = details.stream().filter(detail -> ObjUtil.equal(detail.getLineId(), line.getId()))
+                    .map(MesWmProductSalesDetailDO::getQuantity).filter(Objects::nonNull)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            oqcConcessionGuard.validateQuantity(line, quantity);
+        }
+        for (MesWmProductSalesDetailDO detail : details) {
+            MesWmProductSalesLineDO line = productSalesLineService.getProductSalesLine(detail.getLineId());
+            if (line == null || !ObjUtil.equal(line.getSalesId(), sales.getId())) {
+                throw exception(WM_PRODUCT_SALES_DETAIL_LINE_NOT_MATCH);
+            }
+            salesReelGuard.validate(line, detail.getBatchId());
+            oqcConcessionGuard.validate(line, detail.getMaterialStockId(), detail.getBatchCode(), detail.getQuantity());
+        }
         wmTransactionService.createTransactionList(convertList(details, detail -> new MesWmTransactionSaveReqDTO()
                 .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId())
                 .setQuantity(detail.getQuantity().negate()) // 鍑哄簱鏁伴噺涓鸿礋鏁�
@@ -378,11 +399,36 @@
         if (ObjUtil.notEqual(MesWmProductSalesStatusEnum.CONFIRMED.getStatus(), sales.getStatus())) {
             throw exception(WM_PRODUCT_SALES_CANNOT_CONFIRM);
         }
+        // 鍑鸿揣妫�楠屼笉鍚堟牸鐨勫嚭搴撹蹇呴』鍏堝畬鎴愪笉鍚堟牸鍝佸缃紝绂佹鎵嬪伐纭缁曡繃杩斿伐/璁╂闂幆
+        List<MesWmProductSalesLineDO> lines = productSalesLineMapper.selectListBySalesId(id);
+        if (CollUtil.isNotEmpty(lines)) {
+            int rowNo = 0;
+            for (MesWmProductSalesLineDO line : lines) {
+                rowNo++;
+                if (Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())
+                        || line.getNcrDispositionId() != null) {
+                    throw new ServiceException(400, String.format(
+                            "閿�鍞嚭搴撳崟[%s]绗琜%d]琛岀墿鏂橻%s]鍑鸿揣妫�楠屼笉鍚堟牸锛屽皻鏈畬鎴愪笉鍚堟牸鍝佸缃紱"
+                                    + "璇峰厛鍦ㄤ笉鍚堟牸鍝佸鐞嗗崟涓畬鎴愯繑宸�/杩斾慨/閲嶆柊鐢熶骇鎴栬姝ユ斁琛屽悗鍐嶇‘璁ゅ嚭搴�",
+                            sales.getCode(), rowNo, itemLabel(line.getItemId())));
+                }
+            }
+        }
         // OQC 妫�楠屽畬鎴愶紝娴佽浆鍒板緟鎷h揣
         productSalesMapper.updateById(new MesWmProductSalesDO()
                 .setId(id).setStatus(MesWmProductSalesStatusEnum.APPROVING.getStatus()));
     }
 
+    private String itemLabel(Long itemId) {
+        MesMdItemDO item = itemId == null ? null : mdItemService.getItem(itemId);
+        if (item == null) {
+            return "ID=" + itemId;
+        }
+        String label = (StrUtil.blankToDefault(item.getCode(), "") + " "
+                + StrUtil.blankToDefault(item.getName(), "")).trim();
+        return label.isEmpty() ? "ID=" + itemId : label;
+    }
+
     @Override
     public List<MesWmProductSalesDO> getProductSalesListByClientId(Long clientId) {
         return productSalesMapper.selectListByClientId(clientId);

--
Gitblit v1.9.3