From 3e5e22f5358156ff7430ee117546123ffe042611 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期日, 20 九月 2026 20:23:52 +0800
Subject: [PATCH] feat(stock): 实现出品出库绑定销售台账功能

---
 src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java |  585 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 568 insertions(+), 17 deletions(-)

diff --git a/src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java b/src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java
index d405413..4a2f241 100644
--- a/src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java
+++ b/src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java
@@ -2,23 +2,56 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.account.mapper.sales.AccountInvoiceApplicationMapper;
+import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
+import com.ruoyi.basic.pojo.Vehicle;
+import com.ruoyi.basic.service.IVehicleService;
+import com.ruoyi.common.enums.ReviewStatusEnum;
+import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
+import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
 import com.ruoyi.common.exception.base.BaseException;
+import com.ruoyi.common.utils.EnumUtil;
 import com.ruoyi.common.utils.OrderUtils;
+import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.bean.BeanUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.outsourcing.enums.OutsourcingOrderStatusEnum;
+import com.ruoyi.outsourcing.mapper.OutsourcingOrderMapper;
+import com.ruoyi.outsourcing.mapper.OutsourcingOrderProductMapper;
+import com.ruoyi.outsourcing.pojo.OutsourcingOrder;
+import com.ruoyi.outsourcing.pojo.OutsourcingOrderProduct;
+import com.ruoyi.sales.dto.SalesLedgerDto;
+import com.ruoyi.sales.mapper.SalesLedgerMapper;
+import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
+import com.ruoyi.sales.pojo.SalesLedger;
+import com.ruoyi.sales.pojo.SalesLedgerProduct;
+
+import java.time.LocalDateTime;
+import com.ruoyi.stock.dto.OilOutAvailableDto;
 import com.ruoyi.stock.dto.StockInventoryDto;
 import com.ruoyi.stock.dto.StockOutRecordDto;
+import com.ruoyi.stock.dto.StockUninventoryDto;
+import com.ruoyi.stock.execl.StockOutRecordExportData;
 import com.ruoyi.stock.mapper.StockInventoryMapper;
 import com.ruoyi.stock.mapper.StockOutRecordMapper;
-import com.ruoyi.stock.pojo.StockInRecord;
+import com.ruoyi.stock.mapper.StockUninventoryMapper;
 import com.ruoyi.stock.pojo.StockInventory;
 import com.ruoyi.stock.pojo.StockOutRecord;
+import com.ruoyi.stock.pojo.StockUninventory;
 import com.ruoyi.stock.service.StockOutRecordService;
-import org.springframework.beans.factory.annotation.Autowired;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 
+import java.math.BigDecimal;
+import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * <p>
@@ -29,11 +62,38 @@
  * @since 2026-01-21 05:27:04
  */
 @Service
+@RequiredArgsConstructor
 public class StockOutRecordServiceImpl extends ServiceImpl<StockOutRecordMapper, StockOutRecord> implements StockOutRecordService {
-    @Autowired
-    private StockOutRecordMapper stockOutRecordMapper;
-    @Autowired
-    private StockInventoryMapper stockInventoryMapper;
+    /**
+     * 鍑哄簱绫诲埆锛氶攢鍞�傞攢鍞被娌瑰搧鍑哄簱鍦ㄥ鎵归�氳繃鏃舵寜鎵规鎵e噺搴撳瓨
+     */
+    private static final String OUT_CATEGORY_SALE = "閿�鍞�";
+
+    /**
+     * 鍑哄簱绫诲埆锛氫唬鍌ㄣ�傚鎴锋妸鑷繁鐨勬补瀵勫瓨鍦ㄦ垜浠殑缃愰噷锛屾彁璧板彧鏄褰曚竴绗旓紝涓嶆墸搴撳瓨銆佷笉杩涘彂璐у彴璐�
+     */
+    private static final String OUT_CATEGORY_DEPOSIT = "浠e偍";
+
+    /**
+     * 閿�鍞彴璐︾殑閿�鍞被鍨嬶細浠e偍锛堝崠瀛樺偍浣嶇疆锛夈�傝繖绫诲彴璐︽病鏈変骇鍝佹槑缁嗭紝鍙湁娌瑰簱+鍌ㄧ綈锛岃 SalesLedger.ledgerType
+     */
+    private static final String LEDGER_TYPE_DEPOSIT = "浠e偍";
+
+    /**
+     * 閿�鍞彴璐︿骇鍝佹槑缁嗙殑琛岀被鍨嬶細閿�鍞�傝 sales_ledger_product.type
+     */
+    private static final Integer LEDGER_PRODUCT_TYPE_SALE = 1;
+
+    private final StockOutRecordMapper stockOutRecordMapper;
+    private final IVehicleService vehicleService;
+    private final StockInventoryMapper stockInventoryMapper;
+    private final StockUninventoryMapper stockUninventoryMapper;
+    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
+    private final AccountInvoiceApplicationMapper accountInvoiceApplicationMapper;
+    private final OutsourcingOrderProductMapper outsourcingOrderProductMapper;
+    private final OutsourcingOrderMapper outsourcingOrderMapper;
+    private final SalesLedgerMapper salesLedgerMapper;
+    private final SalesLedgerProductMapper salesLedgerProductMapper;
 
     @Override
     public IPage<StockOutRecordDto> listPage(Page page, StockOutRecordDto stockOutRecordDto) {
@@ -42,10 +102,25 @@
 
     @Override
     public int add(StockOutRecordDto stockOutRecordDto) {
-        String no = OrderUtils.countTodayByCreateTime(stockOutRecordMapper, "CK");
+        resolveSalesLedgerBinding(stockOutRecordDto);
+        if (OUT_CATEGORY_SALE.equals(stockOutRecordDto.getOutCategory())) {
+            // 閿�鍞被娌瑰搧鍑哄簱瀹℃壒閫氳繃鏃惰鎸夋壒娆℃墸鍑忓簱瀛橈紝鎵规涓嶈兘涓虹┖
+            if (StringUtils.isEmpty(stockOutRecordDto.getBatchNo())) {
+                throw new BaseException("閿�鍞被鍑哄簱蹇呴』閫夋嫨鍑哄簱鎵规");
+            }
+            resolveVehicle(stockOutRecordDto);
+            checkSaleOutAvailable(stockOutRecordDto);
+        }
+        LocalDateTime createTime = stockOutRecordDto.getCreateTime();
+        if (createTime == null) {
+            createTime = LocalDateTime.now();
+        }
+        String no = OrderUtils.countTodayByCreateTime(stockOutRecordMapper, "CK","outbound_batches", createTime);
         stockOutRecordDto.setOutboundBatches(no);
-        StockInRecord stockInRecord = new StockInRecord();
-        BeanUtils.copyProperties(stockOutRecordDto, stockInRecord);
+        stockOutRecordDto.setCreateTime(createTime);
+        if (StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode().equals(stockOutRecordDto.getRecordType())){
+            stockOutRecordDto.setApprovalStatus(3);
+        }
         return stockOutRecordMapper.insert(stockOutRecordDto);
     }
 
@@ -57,25 +132,501 @@
             throw new BaseException("璇ュ嚭搴撹褰曚笉瀛樺湪,鏃犳硶鏇存柊!!!");
         }
 
+        resolveSalesLedgerBinding(stockOutRecordDto);
         String[] ignoreProperties = {"id", "outbound_batches"};//鎺掗櫎id灞炴��
         BeanUtils.copyProperties(stockOutRecordDto, stockOutRecord, ignoreProperties);
         return stockOutRecordMapper.updateById(stockOutRecord);
     }
 
+    /**
+     * 缁戝畾閿�鍞彴璐︼細瀹㈡埛涓�寰嬩互鍙拌处涓哄噯瑕嗙洊锛岄伩鍏嶅嚭搴撹褰曚笌鍙拌处鐨勫鎴峰涓嶄笂锛涘嚭搴撶被鍒寜鍙拌处鐨勯攢鍞被鍨嬭嚜鍔ㄥ畾锛�
+     * 鍏嶅緱鍑虹幇銆岀粦浜嗗崠娌瑰彴璐﹀嵈鎸変唬鍌ㄤ笉鎵e簱瀛樸�嶈繖绫荤煕鐩炬暟鎹��
+     * <ul>
+     *     <li>鍗栨补锛堝惈 ledgerType 涓虹┖锛夛細鍑哄簱绫诲埆缃�岄攢鍞�嶏紝骞舵牎楠屾墍閫夎鏍肩‘瀹炲湪璇ュ彴璐︾殑閿�鍞槑缁嗛噷</li>
+     *     <li>浠e偍锛氬嚭搴撶被鍒疆銆屼唬鍌ㄣ�嶏紝甯﹀叆鍙拌处鐨勬补搴�/鍌ㄧ綈锛岃烦杩囦骇鍝佹牎楠� 鈥斺��
+     *         浠e偍鍙拌处鏈韩灏辨槸鍗栧瓨鍌ㄤ綅缃紝娌℃湁浜у搧鏄庣粏</li>
+     * </ul>
+     * salesLedgerId 涓虹┖鏃舵暣浣撹烦杩囷紝涓嶇粦鍙拌处鐨勫嚭搴撹涓轰笌鏀瑰姩鍓嶅畬鍏ㄤ竴鑷�
+     */
+    private void resolveSalesLedgerBinding(StockOutRecordDto stockOutRecordDto) {
+        if (stockOutRecordDto.getSalesLedgerId() == null) {
+            return;
+        }
+        SalesLedger salesLedger = salesLedgerMapper.selectById(stockOutRecordDto.getSalesLedgerId());
+        if (salesLedger == null) {
+            throw new BaseException("鎵�閫夐攢鍞彴璐︿笉瀛樺湪,閿�鍞彴璐d:" + stockOutRecordDto.getSalesLedgerId());
+        }
+        stockOutRecordDto.setCustomerId(salesLedger.getCustomerId());
+        stockOutRecordDto.setCustomerName(salesLedger.getCustomerName());
+        if (LEDGER_TYPE_DEPOSIT.equals(salesLedger.getLedgerType())) {
+            stockOutRecordDto.setOutCategory(OUT_CATEGORY_DEPOSIT);
+            stockOutRecordDto.setOilDepotId(salesLedger.getOilDepotId());
+            stockOutRecordDto.setOilDepotName(salesLedger.getOilDepotName());
+            stockOutRecordDto.setTankId(salesLedger.getTankId());
+            stockOutRecordDto.setTankNo(salesLedger.getTankNo());
+            return;
+        }
+        stockOutRecordDto.setOutCategory(OUT_CATEGORY_SALE);
+        if (stockOutRecordDto.getProductModelId() == null) {
+            throw new BaseException("缁戝畾閿�鍞彴璐︽椂蹇呴』閫夋嫨浜у搧瑙勬牸,閿�鍞悎鍚屽彿:" + salesLedger.getSalesContractNo());
+        }
+        boolean belongsToLedger = salesLedgerProductMapper.selectList(
+                        Wrappers.<SalesLedgerProduct>lambdaQuery()
+                                .eq(SalesLedgerProduct::getSalesLedgerId, stockOutRecordDto.getSalesLedgerId())
+                                .eq(SalesLedgerProduct::getType, LEDGER_PRODUCT_TYPE_SALE))
+                .stream()
+                .anyMatch(item -> stockOutRecordDto.getProductModelId().equals(item.getProductModelId()));
+        if (!belongsToLedger) {
+            throw new BaseException("鎵�閫変骇鍝佷笉鍦ㄨ閿�鍞彴璐︽槑缁嗕腑,閿�鍞悎鍚屽彿:" + salesLedger.getSalesContractNo());
+        }
+    }
+
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int batchDelete(List<Long> ids) {
+        //todo 濡傛灉鍑哄簱涓庡紑绁ㄦ敹娆炬湁鍏宠仈鍒欐棤娉曞垹闄�
+        if (accountSalesCollectionMapper.existsByStockOutRecordId(ids) ||
+                accountInvoiceApplicationMapper.existsByStockOutRecordId(ids)) {
+            throw new BaseException("鍑哄簱璁板綍瀛樺湪寮�绁ㄦ敹娆惧叧鑱旀暟鎹�,鏃犳硶鍒犻櫎!!!");
+        }
         for (Long id : ids) {
             StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
-            StockInventory stockInventory = stockInventoryMapper.selectOne(new LambdaQueryWrapper<StockInventory>().eq(StockInventory::getProductModelId, id));
-            if (stockInventory == null) {
-                throw new BaseException("璇ュ叆搴撹褰曚笅娌℃湁瀵瑰簲鐨勪骇鍝�,鏃犳硶鍒犻櫎!!!");
-            }else {
-                StockInventoryDto stockInRecordDto = new StockInventoryDto();
-                stockInRecordDto.setProductModelId(stockInventory.getProductModelId());
-                stockInRecordDto.setQualitity(stockOutRecord.getStockOutNum());
-                stockInventoryMapper.updateAddStockInventory(stockInRecordDto);
+            if (stockOutRecord.getType().equals("0")) {
+                LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<StockInventory>()
+                    .eq(StockInventory::getProductModelId, stockOutRecord.getProductModelId());
+                if (StringUtils.isEmpty(stockOutRecord.getBatchNo())) {
+                    wrapper.isNull(StockInventory::getBatchNo);
+                } else {
+                    wrapper.eq(StockInventory::getBatchNo, stockOutRecord.getBatchNo());
+                }
+                StockInventory stockInventory = stockInventoryMapper.selectOne(wrapper);
+                if (stockInventory == null) {
+                    throw new BaseException("搴撳瓨璁板綍涓病鏈夊搴旂殑浜у搧,鏃犳硶鍒犻櫎!!!");
+                }else {
+                    StockInventoryDto stockInRecordDto = new StockInventoryDto();
+                    stockInRecordDto.setProductModelId(stockInventory.getProductModelId());
+                    stockInRecordDto.setQualitity(stockOutRecord.getStockOutNum());
+                    stockInRecordDto.setBatchNo(stockInventory.getBatchNo());
+                    stockInventoryMapper.updateAddStockInventory(stockInRecordDto);
+                }
+            }
+            else if (stockOutRecord.getType().equals("1")) {
+                LambdaQueryWrapper<StockUninventory> wrapper = new LambdaQueryWrapper<StockUninventory>()
+                        .eq(StockUninventory::getProductModelId, stockOutRecord.getProductModelId());
+                if (StringUtils.isEmpty(stockOutRecord.getBatchNo())) {
+                    wrapper.isNull(StockUninventory::getBatchNo);
+                } else {
+                    wrapper.eq(StockUninventory::getBatchNo, stockOutRecord.getBatchNo());
+                }
+                StockUninventory stockUninventory = stockUninventoryMapper.selectOne(wrapper);
+                if (stockUninventory == null) {
+                    throw new BaseException("搴撳瓨璁板綍涓病鏈夊搴旂殑浜у搧,鏃犳硶鍒犻櫎!!!");
+                }else {
+                    StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
+                    stockUninventoryDto.setProductModelId(stockUninventory.getProductModelId());
+                    stockUninventoryDto.setQualitity(stockOutRecord.getStockOutNum());
+                    stockUninventoryDto.setBatchNo(stockUninventory.getBatchNo());
+                    stockUninventoryMapper.updateAddStockUnInventory(stockUninventoryDto);
+                }
+            }
+            else if (OUT_CATEGORY_SALE.equals(stockOutRecord.getOutCategory())) {
+                // 閿�鍞被娌瑰搧鍑哄簱鍙湁瀹℃壒閫氳繃鏃舵墠鎵h繃搴撳瓨锛屽垹闄ゅ凡閫氳繃鐨勮褰曢渶鎸夊悓鎵规褰掕繕
+                returnSaleOilOutStock(stockOutRecord);
             }
         }
         return stockOutRecordMapper.deleteBatchIds(ids);
     }
+
+    /**
+     * 閿�鍞被娌瑰搧鍑哄簱褰掕繕搴撳瓨锛屼粎瀵瑰鎵归�氳繃锛堝嵆鎵h繃搴撳瓨锛夌殑璁板綍鐢熸晥銆�
+     * 褰掕繕閲忓彇 deduct_quantity锛堝疄闄呮墸鍑忛噺锛夛紱鏀归�犲墠瀹℃壒閫氳繃鐨勮褰曟病鏈夎鍊硷紝鎸夊叏棰濆綊杩�
+     */
+    private void returnSaleOilOutStock(StockOutRecord stockOutRecord) {
+        if (!ReviewStatusEnum.APPROVED.getCode().equals(stockOutRecord.getApprovalStatus())
+                || StringUtils.isEmpty(stockOutRecord.getBatchNo())
+                || stockOutRecord.getStockOutNum() == null) {
+            return;
+        }
+        BigDecimal returnQuantity = stockOutRecord.getDeductQuantity() != null
+                ? stockOutRecord.getDeductQuantity()
+                : stockOutRecord.getStockOutNum();
+        if (returnQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+            return;
+        }
+        StockInventoryDto stockInventoryDto = new StockInventoryDto();
+        stockInventoryDto.setProductModelId(stockOutRecord.getProductModelId());
+        stockInventoryDto.setBatchNo(stockOutRecord.getBatchNo());
+        stockInventoryDto.setQualitity(returnQuantity);
+        stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
+    }
+
+    @Override
+    public void exportStockOutRecord(HttpServletResponse response, StockOutRecordDto stockOutRecordDto) {
+        List<StockOutRecordExportData> list = stockOutRecordMapper.listStockOutRecordExportData(stockOutRecordDto);
+        for (StockOutRecordExportData stockInRecordExportData : list) {
+            if (stockInRecordExportData.getType().equals("0")) {
+                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockOutQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
+            }else {
+                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockInQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
+            }
+        }
+        ExcelUtil<StockOutRecordExportData> util = new ExcelUtil<>(StockOutRecordExportData.class);
+        util.exportExcel(response,list, "鍑哄簱璁板綍淇℃伅");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int batchDeletePending(List<Long> ids) {
+        for (Long id : ids) {
+            StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
+            if (stockOutRecord == null) {
+                throw new BaseException("鍑哄簱璁板綍涓嶅瓨鍦�,鏃犳硶鍒犻櫎!!!");
+            }
+            if (stockOutRecord.getApprovalStatus() != null && !ReviewStatusEnum.PENDING_REVIEW.getCode().equals(stockOutRecord.getApprovalStatus())) {
+                throw new BaseException("鍙湁寰呭鎵圭姸鎬佺殑璁板綍鎵嶈兘鍒犻櫎,鍑哄簱鎵规:" + stockOutRecord.getOutboundBatches());
+            }
+        }
+        return stockOutRecordMapper.deleteBatchIds(ids);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int batchApprove(List<Long> ids, Integer approvalStatus) {
+        if (CollectionUtils.isEmpty(ids)) {
+            throw new BaseException("璇烽�夋嫨鑷冲皯涓�鏉℃暟鎹�");
+        }
+        if (approvalStatus == null || (!ReviewStatusEnum.APPROVED.getCode().equals(approvalStatus) && !ReviewStatusEnum.REJECTED.getCode().equals(approvalStatus))) {
+            throw new BaseException("瀹℃壒鐘舵�佸�兼棤鏁�");
+        }
+        for (Long id : ids) {
+            StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
+            if (stockOutRecord == null) {
+                throw new BaseException("鍑哄簱璁板綍涓嶅瓨鍦�,鏃犳硶瀹℃壒!!!");
+            }
+            if (stockOutRecord.getApprovalStatus() != null && !ReviewStatusEnum.PENDING_REVIEW.getCode().equals(stockOutRecord.getApprovalStatus())) {
+                throw new BaseException("鍙湁寰呭鎵圭姸鎬佺殑璁板綍鎵嶈兘瀹℃壒,鍑哄簱鎵规:" + stockOutRecord.getOutboundBatches());
+            }
+            // 瀹℃壒閫氳繃鏃跺厛鎵e噺搴撳瓨锛屾墸鍑忛噺锛坉educt_quantity锛夎闅忓鎵圭姸鎬佷竴璧疯惤搴擄紝鎵�浠ユ斁鍦ㄦ洿鏂颁箣鍓�
+            if (ReviewStatusEnum.APPROVED.getCode().equals(approvalStatus)) {
+                if ("0".equals(stockOutRecord.getType())) {
+                    // 鍚堟牸鍑哄簱 -> 鍏堟煡搴撳瓨鏄惁瀛樺湪锛屽瓨鍦ㄦ墠鎵e噺
+                    StockInventory stockInventory = getStockInventory(stockOutRecord.getProductModelId(), stockOutRecord.getBatchNo());
+                    if (stockInventory == null) {
+                        throw new BaseException("鍚堟牸搴撳瓨璁板綍涓嶅瓨鍦�,鍑哄簱鎵规:" + stockOutRecord.getOutboundBatches());
+                    }
+                    StockInventoryDto stockInventoryDto = new StockInventoryDto();
+                    stockInventoryDto.setProductModelId(stockOutRecord.getProductModelId());
+                    stockInventoryDto.setBatchNo(stockOutRecord.getBatchNo());
+                    stockInventoryDto.setQualitity(stockOutRecord.getStockOutNum());
+                    stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
+                } else if ("1".equals(stockOutRecord.getType())) {
+                    // 涓嶅悎鏍煎嚭搴� -> 鍏堟煡搴撳瓨鏄惁瀛樺湪锛屽瓨鍦ㄦ墠鎵e噺
+                    StockUninventory stockUninventory = getStockUninventory(stockOutRecord.getProductModelId(), stockOutRecord.getBatchNo());
+                    if (stockUninventory == null) {
+                        throw new BaseException("涓嶅悎鏍煎簱瀛樿褰曚笉瀛樺湪,鍑哄簱鎵规:" + stockOutRecord.getOutboundBatches());
+                    }
+                    StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
+                    stockUninventoryDto.setProductModelId(stockOutRecord.getProductModelId());
+                    stockUninventoryDto.setBatchNo(stockOutRecord.getBatchNo());
+                    stockUninventoryDto.setQualitity(stockOutRecord.getStockOutNum());
+                    stockUninventoryMapper.updateSubtractStockUnInventory(stockUninventoryDto);
+                } else if (OUT_CATEGORY_SALE.equals(stockOutRecord.getOutCategory())) {
+                    // 閿�鍞被娌瑰搧鍑哄簱锛坱ype 涓虹┖锛夊鎵归�氳繃鏃舵寜鎵规鎵e噺搴撳瓨
+                    deductSaleOilOutStock(stockOutRecord);
+                }
+            }
+            stockOutRecord.setApprovalStatus(approvalStatus);
+            stockOutRecordMapper.updateById(stockOutRecord);
+            if (ReviewStatusEnum.APPROVED.getCode().equals(approvalStatus)
+                    && StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode()
+                            .equals(stockOutRecord.getRecordType())) {
+                tryCompleteOutsourcingOrder(stockOutRecord);
+            }
+        }
+        return ids.size();
+    }
+
+    /**
+     * 濮斿鎶ュ簾鍝佸嚭搴撳鎵归�氳繃鍚庯紝鑻ヨ濮斿璁㈠崟鐨勬姤搴熷搧閫夋枡琛屽凡<b>鍏ㄩ儴</b>鍑哄簱瀹℃壒閫氳繃锛�
+     * 鑷姩鎶婅鍗曚粠銆屽凡纭銆嶆帹鍒般�屽凡瀹屾垚銆嶏紝鐪佹帀浜哄伐鍐嶇偣涓�娆″畬鎴愩��
+     *
+     * <p>鐩存帴鐢� mapper 鑰屼笉鏄� {@code IOutsourcingOrderService#complete}锛氬悗鑰呬緷璧栨湰鏈嶅姟锛�
+     * 涓よ竟閮借蛋鏋勯�犲櫒娉ㄥ叆浼氬舰鎴愬惊鐜緷璧栥�傞┏鍥�/鍙嶅涓嶅洖閫�璁㈠崟鐘舵�侊紙澶勭悊閿欒瀹℃壒璧颁汉宸ラ噸鏂板鎵癸級銆�</p>
+     */
+    private void tryCompleteOutsourcingOrder(StockOutRecord record) {
+        if (record.getRecordId() == null) {
+            return;
+        }
+        OutsourcingOrderProduct line = outsourcingOrderProductMapper.selectById(record.getRecordId());
+        if (line == null || line.getOutsourcingOrderId() == null) {
+            return;
+        }
+        Long orderId = line.getOutsourcingOrderId();
+        // isScrap 鍦� Java 渚ц繃婊わ紝涓庡澶栨ā鍧楄嚜宸辩殑 scrapLineIds 涓�鑷达紝涓嶄緷璧� Boolean 瀛楁鐨勫垪鍚嶆帹鏂�
+        List<Long> lineIds = outsourcingOrderProductMapper.selectList(
+                        Wrappers.<OutsourcingOrderProduct>lambdaQuery()
+                                .eq(OutsourcingOrderProduct::getOutsourcingOrderId, orderId))
+                .stream()
+                .filter(item -> Boolean.TRUE.equals(item.getIsScrap()))
+                .map(OutsourcingOrderProduct::getId)
+                .toList();
+        if (lineIds.isEmpty()) {
+            return;
+        }
+        // 鍑哄簱璁板綍涓�鏉″搴斾竴琛屾槑缁嗭紱缂鸿褰曪紙琚弽瀹″垹闄ょ瓑锛夊氨涓嶅畬鎴愶紝涓庝汉宸ュ畬鎴愮殑鏍¢獙鍙e緞涓�鑷�
+        long approvedCount = listByRecordIds(lineIds, record.getRecordType()).stream()
+                .filter(item -> ReviewStatusEnum.APPROVED.getCode().equals(item.getApprovalStatus()))
+                .count();
+        if (approvedCount < lineIds.size()) {
+            return;
+        }
+        // 鏉′欢鏇存柊锛屽彧鏈夈�屽凡纭銆嶈兘鎺ㄨ繘锛涘凡浣滃簾/宸插畬鎴愮殑璁㈠崟涓嶅姩
+        outsourcingOrderMapper.update(null, Wrappers.<OutsourcingOrder>lambdaUpdate()
+                .eq(OutsourcingOrder::getId, orderId)
+                .eq(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CONFIRMED.getCode())
+                .set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.COMPLETED.getCode()));
+    }
+
+    /**
+     * 閿�鍞被娌瑰搧鍑哄簱鎵e噺搴撳瓨銆傚嚭搴撴暟閲忔槸鏈瑁呰溅鎬婚噺锛岃溅涓婁笂娆″墿鐨勬补鏈缁х画鐢紝
+     * 鎵�浠ュ彧浠庡簱瀛樿ˉ瓒炽�屽嚭搴撴暟閲� 鈭� 涓婃鍓╀綑娌归噺銆嶇殑宸銆�
+     * updateSubtractStockInventory 鑷甫 qualitity &gt;= 鏁伴噺 鐨勫畧鍗紝褰卞搷琛屾暟涓� 0 鍗冲簱瀛樹笉瓒筹紝
+     * 鎶涢敊鐢卞灞備簨鍔″洖婊�
+     */
+    private void deductSaleOilOutStock(StockOutRecord stockOutRecord) {
+        String suffix = ",鍑哄簱鎵规:" + stockOutRecord.getOutboundBatches();
+        if (StringUtils.isEmpty(stockOutRecord.getBatchNo())) {
+            throw new BaseException("閿�鍞被鍑哄簱蹇呴』閫夋嫨鍑哄簱鎵规" + suffix);
+        }
+        BigDecimal quantity = stockOutRecord.getStockOutNum();
+        if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new BaseException("閿�鍞被鍑哄簱鏁伴噺蹇呴』澶т簬0" + suffix);
+        }
+        BigDecimal lastResidual = getLastResidualOil(stockOutRecord.getVehicleId(), stockOutRecord.getProductModelId(), stockOutRecord.getId());
+        if (lastResidual.compareTo(quantity) > 0) {
+            throw new BaseException("鍑哄簱鏁伴噺涓嶈兘灏忎簬璇ヨ溅涓婃鍓╀綑娌归噺" + toPlain(lastResidual) + suffix);
+        }
+        BigDecimal deduct = quantity.subtract(lastResidual);
+        if (deduct.compareTo(BigDecimal.ZERO) > 0) {
+            StockInventoryDto stockInventoryDto = new StockInventoryDto();
+            stockInventoryDto.setProductModelId(stockOutRecord.getProductModelId());
+            stockInventoryDto.setBatchNo(stockOutRecord.getBatchNo());
+            stockInventoryDto.setQualitity(deduct);
+            if (stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto) == 0) {
+                throw new BaseException("搴撳瓨涓嶈冻鏃犳硶鍑哄簱" + suffix);
+            }
+        }
+        // 杞︿笂鍓╂补鍒氬ソ澶燂紙deduct=0锛夋椂涓嶆秹鍙婂簱瀛樻洿鏂帮紝鍚屾牱钀藉簱渚夸簬鍒犻櫎鏃舵寜瀹為檯鎵e噺閲忓綊杩�
+        stockOutRecord.setDeductQuantity(deduct);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int batchReAudit(List<Long> ids) {
+        if (CollectionUtils.isEmpty(ids)) {
+            throw new BaseException("璇烽�夋嫨鑷冲皯涓�鏉℃暟鎹�");
+        }
+        for (Long id : ids) {
+            StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
+            if (stockOutRecord == null) {
+                throw new BaseException("鍑哄簱璁板綍涓嶅瓨鍦�,鏃犳硶閲嶆柊瀹℃牳!!!");
+            }
+            if (!ReviewStatusEnum.REJECTED.getCode().equals(stockOutRecord.getApprovalStatus())) {
+                throw new BaseException("鍙湁椹冲洖鐘舵�佺殑璁板綍鎵嶈兘閲嶆柊瀹℃牳,鍑哄簱鎵规:" + stockOutRecord.getOutboundBatches());
+            }
+            stockOutRecord.setApprovalStatus(ReviewStatusEnum.PENDING_REVIEW.getCode());
+            stockOutRecordMapper.updateById(stockOutRecord);
+        }
+        return ids.size();
+    }
+
+    @Override
+    public int updateResidualOil(Long id, BigDecimal residualOil) {
+        StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
+        if (stockOutRecord == null) {
+            throw new BaseException("璇ュ嚭搴撹褰曚笉瀛樺湪,鏃犳硶鏇存柊!!!");
+        }
+        if (residualOil == null || residualOil.compareTo(BigDecimal.ZERO) < 0) {
+            throw new BaseException("鍓╀綑娌归噺涓嶈兘涓虹┖涓斾笉鑳藉皬浜�0");
+        }
+        if (stockOutRecord.getStockOutNum() != null && residualOil.compareTo(stockOutRecord.getStockOutNum()) > 0) {
+            throw new BaseException("鍓╀綑娌归噺涓嶈兘澶т簬鍑哄簱鏁伴噺" + toPlain(stockOutRecord.getStockOutNum()));
+        }
+        StockOutRecord update = new StockOutRecord();
+        update.setId(id);
+        update.setResidualOil(residualOil);
+        return stockOutRecordMapper.updateById(update);
+    }
+
+    private StockInventory getStockInventory(Long productModelId, String batchNo) {
+        LambdaQueryWrapper<StockInventory> eq = new LambdaQueryWrapper<>();
+        eq.eq(StockInventory::getProductModelId, productModelId);
+        if (StringUtils.isEmpty(batchNo)) {
+            eq.isNull(StockInventory::getBatchNo);
+        } else {
+            eq.eq(StockInventory::getBatchNo, batchNo);
+        }
+        return stockInventoryMapper.selectOne(eq);
+    }
+
+    private StockUninventory getStockUninventory(Long productModelId, String batchNo) {
+        LambdaQueryWrapper<StockUninventory> eq = new LambdaQueryWrapper<>();
+        eq.eq(StockUninventory::getProductModelId, productModelId);
+        if (StringUtils.isEmpty(batchNo)) {
+            eq.isNull(StockUninventory::getBatchNo);
+        } else {
+            eq.eq(StockUninventory::getBatchNo, batchNo);
+        }
+        return stockUninventoryMapper.selectOne(eq);
+    }
+
+    /**
+     * 杞﹁締鍥炲~锛氬墠绔彲浠ョ洿鎺ヤ紶 vehicleId锛屼篃鍙互鍙紶杞︾墝鍙凤紝
+     * 鍚庤�呮寜杞﹁締涓绘暟鎹弽鏌ュ洖濉�備袱鑰呴兘鎷夸笉鍒板氨鏃犳硶瀹氫綅銆屼笂娆″墿浣欐补閲忋�嶏紝鐩存帴鎶ラ敊
+     */
+    private void resolveVehicle(StockOutRecordDto stockOutRecordDto) {
+        if (stockOutRecordDto.getVehicleId() != null) {
+            return;
+        }
+        if (StringUtils.isEmpty(stockOutRecordDto.getTruckPlateNo())) {
+            throw new BaseException("閿�鍞被鍑哄簱蹇呴』閫夋嫨杞﹁締");
+        }
+        LambdaQueryWrapper<Vehicle> wrapper = new LambdaQueryWrapper<Vehicle>()
+                .eq(Vehicle::getPlateNumber, stockOutRecordDto.getTruckPlateNo());
+        Vehicle vehicle = vehicleService.getOne(wrapper, false);
+        if (vehicle == null) {
+            throw new BaseException("杞︾墝鍙� " + stockOutRecordDto.getTruckPlateNo() + " 涓嶅湪杞﹁締绠$悊涓紝璇峰厛鍒拌溅杈嗙鐞嗘坊鍔�");
+        }
+        stockOutRecordDto.setVehicleId(vehicle.getId());
+    }
+
+    /**
+     * 鍑哄簱鏁伴噺鏄湰娆¤杞︽�婚噺锛屽繀椤昏惤鍦ㄣ�屼笂娆″墿浣欐补閲� ~ 涓婃鍓╀綑娌归噺 + 搴撳瓨銆嶅尯闂村唴
+     */
+    private void checkSaleOutAvailable(StockOutRecordDto stockOutRecordDto) {
+        String suffix = ",鍑哄簱鎵规:" + stockOutRecordDto.getOutboundBatches();
+        BigDecimal quantity = stockOutRecordDto.getStockOutNum();
+        if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new BaseException("閿�鍞被鍑哄簱鏁伴噺蹇呴』澶т簬0" + suffix);
+        }
+        BigDecimal lastResidual = getLastResidualOil(stockOutRecordDto.getVehicleId(), stockOutRecordDto.getProductModelId(), null);
+        if (lastResidual.compareTo(quantity) > 0) {
+            throw new BaseException("鍑哄簱鏁伴噺涓嶈兘灏忎簬璇ヨ溅涓婃鍓╀綑娌归噺" + toPlain(lastResidual) + suffix);
+        }
+        BigDecimal stockQuantity = getStockQuantity(stockOutRecordDto.getProductModelId(), stockOutRecordDto.getBatchNo());
+        BigDecimal availableQty = lastResidual.add(stockQuantity);
+        if (quantity.compareTo(availableQty) > 0) {
+            throw new BaseException("鍑哄簱鏁伴噺瓒呭嚭鍙敤閲�(涓婃鍓╀綑" + toPlain(lastResidual)
+                    + ",搴撳瓨" + toPlain(stockQuantity) + ",鍙敤" + toPlain(availableQty) + ")" + suffix);
+        }
+    }
+
+    /**
+     * 璇ヨ溅璇ヨ鏍间笂娆¢�佸畬璐у悗鍓╀綑鐨勬补閲忥紝鍙栨渶杩戜竴鏉″~杩囨畫娌逛笖宸插鎵圭殑璁板綍銆�
+     * excludeId 鐢ㄤ簬鎺掗櫎銆屾湰娆¤瀹℃壒鐨勮繖鏉¤嚜宸便�嶁�斺�斿畠鑷繁鐨� residual_oil 鏄�佸畬涔嬪悗鐨勫�硷紝涓嶈兘鐢ㄦ潵绠楁湰娆℃墸鍑�
+     */
+    private BigDecimal getLastResidualOil(Long vehicleId, Long productModelId, Long excludeId) {
+        if (vehicleId == null || productModelId == null) {
+            return BigDecimal.ZERO;
+        }
+        LambdaQueryWrapper<StockOutRecord> wrapper = new LambdaQueryWrapper<StockOutRecord>()
+                .eq(StockOutRecord::getVehicleId, vehicleId)
+                .eq(StockOutRecord::getProductModelId, productModelId)
+                .isNotNull(StockOutRecord::getResidualOil)
+                .eq(StockOutRecord::getApprovalStatus, ReviewStatusEnum.APPROVED.getCode());
+        if (excludeId != null) {
+            wrapper.lt(StockOutRecord::getId, excludeId);
+        }
+        wrapper.orderByDesc(StockOutRecord::getId).last("limit 1");
+        StockOutRecord last = stockOutRecordMapper.selectOne(wrapper);
+        return last == null || last.getResidualOil() == null ? BigDecimal.ZERO : last.getResidualOil();
+    }
+
+    /**
+     * 褰撳墠搴撳瓨閲忥紝鎸夎鏍煎悎璁★紱浼犱簡鎵规鍙峰啀鍙犲姞鎵规鏉′欢
+     */
+    private BigDecimal getStockQuantity(Long productModelId, String batchNo) {
+        if (productModelId == null) {
+            return BigDecimal.ZERO;
+        }
+        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<StockInventory>()
+                .eq(StockInventory::getProductModelId, productModelId);
+        if (!StringUtils.isEmpty(batchNo)) {
+            wrapper.eq(StockInventory::getBatchNo, batchNo);
+        }
+        return stockInventoryMapper.selectList(wrapper).stream()
+                .map(StockInventory::getQualitity)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+
+    @Override
+    public OilOutAvailableDto getOilOutAvailableQty(StockOutRecordDto query) {
+        if (query == null || query.getVehicleId() == null || query.getProductModelId() == null) {
+            throw new BaseException("璇峰厛閫夋嫨杞﹁締鍜屼骇鍝佽鏍�");
+        }
+        BigDecimal lastResidualOil = getLastResidualOil(query.getVehicleId(), query.getProductModelId(), query.getId());
+        BigDecimal stockQuantity = getStockQuantity(query.getProductModelId(), query.getBatchNo());
+        OilOutAvailableDto result = new OilOutAvailableDto();
+        result.setLastResidualOil(lastResidualOil);
+        result.setStockQuantity(stockQuantity);
+        result.setAvailableQty(lastResidualOil.add(stockQuantity));
+        return result;
+    }
+
+    private String toPlain(BigDecimal value) {
+        return value == null ? "0" : value.stripTrailingZeros().toPlainString();
+    }
+
+    @Override
+    public List<StockOutRecord> listByRecordIds(List<Long> recordIds, String recordType) {
+        if (CollectionUtils.isEmpty(recordIds) || StringUtils.isEmpty(recordType)) {
+            return Collections.emptyList();
+        }
+        return stockOutRecordMapper.selectByRecordIds(recordIds, recordType);
+    }
+
+    @Override
+    public IPage<SalesLedger> listBindableSalesLedger(Page page, SalesLedgerDto salesLedgerDto) {
+        // 璇ユ煡璇㈢敤 /*data_scope*/ 璺宠繃鏁版嵁鏉冮檺锛岃�� MP 榛樿浼氭妸 count 璇彞浼樺寲鎴愩�孲ELECT COUNT(*) FROM sales_ledger
+        // WHERE ...銆嶏紝浼樺寲鍚庢爣璁颁涪澶憋紝count 鍙嶈�岃杩藉姞鏉冮檺鏉′欢锛宼otal 浼氭瘮瀹為檯璁板綍灏戙�傚叧鎺� count 浼樺寲鍚�
+        // count 閫�鍖栨垚銆孲ELECT COUNT(*) FROM (鍘熷彞) TOTAL銆嶏紝鏍囪鍘熸牱淇濈暀
+        page.setOptimizeCountSql(false);
+        return stockOutRecordMapper.bindableSalesLedgerPage(page, salesLedgerDto);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int releaseByRecordIds(List<Long> recordIds, String recordType) {
+        List<StockOutRecord> records = listByRecordIds(recordIds, recordType);
+        if (records.isEmpty()) {
+            return 0;
+        }
+        for (StockOutRecord record : records) {
+            if (!ReviewStatusEnum.APPROVED.getCode().equals(record.getApprovalStatus())) {
+                continue;
+            }
+            // type=0 褰掕繕鍚堟牸搴撳瓨锛坰tock_inventory锛夛紝type=1 褰掕繕涓嶅悎鏍煎簱瀛橈紙stock_uninventory锛�
+            if ("0".equals(record.getType())) {
+                StockInventoryDto stockInventoryDto = new StockInventoryDto();
+                stockInventoryDto.setProductModelId(record.getProductModelId());
+                stockInventoryDto.setBatchNo(record.getBatchNo());
+                stockInventoryDto.setQualitity(record.getStockOutNum());
+                if (stockInventoryMapper.updateAddStockInventory(stockInventoryDto) == 0) {
+                    throw new BaseException("搴撳瓨璁板綍涓病鏈夊搴旂殑浜у搧,鏃犳硶褰掕繕,鍑哄簱鎵规:" + record.getOutboundBatches());
+                }
+            } else if ("1".equals(record.getType())) {
+                StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
+                stockUninventoryDto.setProductModelId(record.getProductModelId());
+                stockUninventoryDto.setBatchNo(record.getBatchNo());
+                stockUninventoryDto.setQualitity(record.getStockOutNum());
+                if (stockUninventoryMapper.updateAddStockUnInventory(stockUninventoryDto) == 0) {
+                    throw new BaseException("搴撳瓨璁板綍涓病鏈夊搴旂殑浜у搧,鏃犳硶褰掕繕,鍑哄簱鎵规:" + record.getOutboundBatches());
+                }
+            } else {
+                throw new BaseException("鍑哄簱璁板綍绫诲瀷寮傚父锛屾棤娉曞綊杩樺簱瀛�,鍑哄簱鎵规:" + record.getOutboundBatches());
+            }
+        }
+        return stockOutRecordMapper.deleteBatchIds(records.stream().map(StockOutRecord::getId).toList());
+    }
 }

--
Gitblit v1.9.3