feat(stock): 优化库存管理和成品树结构功能
1- 为ApproveProcessMapper.xml和ProductBomMapper.xml添加排序功能
2- 在ProductionProductMainDto中新增bomInputQty字段用于产品结构投入数量
3- 修改ProductionProductMainServiceImpl中投入数量计算逻辑,使用前端传入的bomInputQty值
4- 在ProductWorkOrderDto中添加bomInputQty字段并在服务实现中计算标准投入数量
5- 更新SalesLedgerMapper.xml查询逻辑,从product_summary获取电压信息
6- 为SalesLedgerProduct添加stockId字段并修改库存扣减逻辑使用具体库存ID
7- 重构StockInventoryController中的成品库存树查询接口和导入导出功能
8- 新增成品和非成品库存导入导出的数据模型和Excel工具类
9- 优化StockInventoryServiceImpl中的库存扣减逻辑,支持按特定库存ID操作
10- 更新库存导入导出功能,区分成品和非成品类型并提供相应模板
已添加2个文件
已修改23个文件
787 ■■■■ 文件已修改
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/dto/ProductWorkOrderDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/dto/ProductionProductMainDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductWorkOrderServiceImpl.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/FinishedProductTreeDto.java 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockOutRecordDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/FinishedProductInventoryExportData.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/NonFinishedProductInventoryExportData.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/StockInventoryExportData.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/StockOutRecordExportData.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/StockInventoryService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 382 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApproveProcessMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductBomMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockOutRecordMapper.xml 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
@@ -152,7 +152,7 @@
     * @param recordType
     * @param recordId
     */
    public void substractStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId,String batchNo) {
    public void substractStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId,String batchNo,Long stockId) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
        stockInventoryDto.setRecordType(String.valueOf(recordType));
@@ -161,6 +161,7 @@
        if (batchNo !=null && !batchNo.isEmpty()) {
            stockInventoryDto.setBatchNo(batchNo);
        }
        stockInventoryDto.setStockId(stockId);
        stockInventoryService.subtractStockInventory(stockInventoryDto);
    }
@@ -185,4 +186,18 @@
            stockOutRecordService.batchDelete(idList);
        }
    }
    /**
     * ä»…按recordId删除出库记录(用于recordType为动态值的场景)
     */
    public void deleteStockOutRecordByRecordId(Long recordId) {
        List<StockOutRecord> one = stockOutRecordService.list(new QueryWrapper<StockOutRecord>()
                .lambda().eq(StockOutRecord::getRecordId, recordId));
        if (ObjectUtils.isNotEmpty(one)) {
            List<Long> idList = one.stream()
                    .map(StockOutRecord::getId)
                    .collect(Collectors.toList());
            stockOutRecordService.batchDelete(idList);
        }
    }
}
src/main/java/com/ruoyi/production/dto/ProductWorkOrderDto.java
@@ -62,6 +62,10 @@
    @ApiModelProperty(value = "投入数量")
    private BigDecimal inputQty;
    // äº§å“ç»“构投入数量
    @ApiModelProperty(value = "产品结构投入数量")
    private BigDecimal bomInputQty;
    @ApiModelProperty(value = "工单类型 æ­£å¸¸ /返工返修")
    private String workOrderType;
src/main/java/com/ruoyi/production/dto/ProductionProductMainDto.java
@@ -86,4 +86,7 @@
    @ApiModelProperty(value = "生产批号")
    private String batchNo;
    @ApiModelProperty(value = "产品结构投入数量")
    private BigDecimal bomInputQty;
}
src/main/java/com/ruoyi/production/service/impl/ProductWorkOrderServiceImpl.java
@@ -11,6 +11,7 @@
import com.deepoove.poi.data.Pictures;
import com.ruoyi.common.utils.MatrixToImageWriter;
import com.ruoyi.production.dto.ProductWorkOrderDto;
import com.ruoyi.production.dto.ProductStructureDto;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.pojo.*;
import com.ruoyi.production.service.ProductWorkOrderService;
@@ -30,6 +31,7 @@
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@@ -44,6 +46,10 @@
    private ProductProcessRouteItemMapper productProcessRouteItemMapper;
    @Autowired
    private ProductProcessMapper productProcessMapper;
    @Autowired
    private ProductProcessRouteMapper productProcessRouteMapper;
    @Autowired
    private ProductStructureMapper productStructureMapper;
    @Autowired
    private ProductionProductMainMapper productionProductMainMapper;
    @Autowired
@@ -61,6 +67,7 @@
    @Override
    public IPage<ProductWorkOrderDto> listPage(Page<ProductWorkOrderDto> page, ProductWorkOrderDto productWorkOrder) {
        IPage<ProductWorkOrderDto> productWorkOrderDtoIPage = productWorkOrdermapper.pageProductWorkOrder(page, productWorkOrder);
        Map<String, BigDecimal> bomInputQtyCache = new HashMap<>();
        productWorkOrderDtoIPage.getRecords().forEach(record -> {
            // æ˜¯å¦èƒ½æŠ¥å·¥ï¼š 1. ç¬¬ä¸€ä¸ªå·¥åºèƒ½æŠ¥å·¥ 2. ä¸Šä¸€ä¸ªå·¥åºå·²æŠ¥å·¥ 3. ä¹‹å‰çš„工序未被隔离
            Integer currentDragSort = record.getDragSort();
@@ -126,6 +133,62 @@
            } else {
                record.setActualQualifiedRate((record.getCompleteQuantity().subtract(totalScrapQty)).multiply(BigDecimal.valueOf(100)));
            }
            // æŸ¥è¯¢å½“前工单对应产品结构中的标准投入数量
            String bomInputQtyCacheKey = record.getProductRouteId() + "_" + record.getProcessId() + "_" + record.getPlanQuantity();
            BigDecimal bomInputQty = bomInputQtyCache.get(bomInputQtyCacheKey);
            if (bomInputQty == null) {
                bomInputQty = BigDecimal.ZERO;
                ProductProcessRoute productProcessRoute = productProcessRouteMapper.selectById(record.getProductRouteId());
                if (productProcessRoute != null && productProcessRoute.getBomId() != null && record.getProcessId() != null) {
                    List<ProductStructureDto> productStructureDtos = productStructureMapper.listBybomAndProcess(
                            productProcessRoute.getBomId(),
                            record.getProcessId()
                    );
                    if (CollectionUtils.isEmpty(productStructureDtos)) {
                        bomInputQty = record.getPlanQuantity() == null ? BigDecimal.ZERO : record.getPlanQuantity();
                    } else {
                        Set<Long> parentIds = productStructureDtos.stream()
                                .map(ProductStructureDto::getParentId)
                                .filter(Objects::nonNull)
                                .collect(Collectors.toSet());
                        Map<Long, ProductStructureDto> parentMap = new HashMap<>();
                        if (CollectionUtils.isNotEmpty(parentIds)) {
                            parentMap = productStructureMapper.selectByIds(parentIds)
                                    .stream()
                                    .collect(Collectors.toMap(ProductStructureDto::getId, Function.identity(), (a, b) -> a));
                        }
                        BigDecimal planQty = record.getPlanQuantity() == null ? BigDecimal.ZERO : record.getPlanQuantity();
                        for (ProductStructureDto productStructureDto : productStructureDtos) {
                            BigDecimal childQty = productStructureDto.getUnitQuantity();
                            if (childQty == null || childQty.compareTo(BigDecimal.ZERO) <= 0) {
                                continue;
                            }
                            BigDecimal parentQty = BigDecimal.ONE;
                            if (productStructureDto.getParentId() != null) {
                                ProductStructureDto parent = parentMap.get(productStructureDto.getParentId());
                                if (parent != null && parent.getUnitQuantity() != null && parent.getUnitQuantity().compareTo(BigDecimal.ZERO) > 0) {
                                    parentQty = parent.getUnitQuantity();
                                }
                            }
                            if (parentQty.compareTo(BigDecimal.ZERO) <= 0) {
                                continue;
                            }
                            bomInputQty = bomInputQty.add(
                                    childQty.divide(parentQty, 6, RoundingMode.HALF_UP).multiply(planQty)
                            );
                        }
                    }
                }
                bomInputQtyCache.put(bomInputQtyCacheKey, bomInputQty);
            }
            record.setBomInputQty(bomInputQty);
        });
        return productWorkOrderDtoIPage;
    }
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -37,7 +37,6 @@
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -99,6 +98,7 @@
        BigDecimal reportQty = dto.getQuantity();
        BigDecimal scrapQty = dto.getScrapQty() == null ? BigDecimal.ZERO : dto.getScrapQty();
        BigDecimal bomInputQty = dto.getBomInputQty();
        if (reportQty == null || reportQty.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("报工数量必须大于0");
        }
@@ -107,6 +107,9 @@
        }
        if (scrapQty.compareTo(reportQty) > 0) {
            throw new ServiceException("报废数量不能大于报工数量");
        }
        if (bomInputQty == null || bomInputQty.compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("产品结构投入数量不能小于0");
        }
        // ç¬¬äºŒæ­¥ï¼šåŠ è½½å½“å‰å·¥åºã€å·¥å•ã€å·¥è‰ºè·¯çº¿å’Œè®¢å•æ•°æ®ï¼Œå¹¶æ ¡éªŒåŸºç¡€å…³è”å…³ç³»
@@ -313,6 +316,9 @@
                    .collect(Collectors.toMap(ProductStructureDto::getId, Function.identity(), (a, b) -> a));
        }
        // ç¬¬ä¸ƒæ­¥-1:投入数量强制取前端传入的 bomInputQty
        BigDecimal inputBaseQty = bomInputQty;
        for (ProductStructureDto productStructureDto : productStructureDtos) {
            if (productStructureDto.getProductModelId() == null) {
                throw new ServiceException("投入物料产品型号不能为空");
@@ -335,9 +341,7 @@
                throw new ServiceException("父级物料用量必须大于0");
            }
            BigDecimal needQty = childQty
                    .divide(parentQty, 6, RoundingMode.HALF_UP)
                    .multiply(reportQty);
            BigDecimal needQty = inputBaseQty;
            ProductionProductInput productionProductInput = new ProductionProductInput();
            productionProductInput.setProductModelId(productStructureDto.getProductModelId());
@@ -350,6 +354,7 @@
                    needQty,
                    StockOutQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_OUT.getCode(),
                    productionProductMain.getId(),
                    null,
                    null
            );
        }
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java
@@ -13,7 +13,6 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
/**
 * äº§å“ä¿¡æ¯å¯¹è±¡ sales_ledger_product
@@ -261,4 +260,7 @@
    @ApiModelProperty("批号")
    private String batchNo;
    @ApiModelProperty("库存id")
    private Long stockId;
}
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -66,7 +66,8 @@
        //扣减库存
        if(!"已发货".equals(byId.getStatus())){
            SalesLedgerProduct salesLedgerProduct = salesLedgerProductMapper.selectById(byId.getSalesLedgerProductId());
            stockUtils.substractStock(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), req.getId(),null);
            stockUtils.substractStock(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(),
                    StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), req.getId(),null,salesLedgerProduct.getStockId());
        }
        byId.setExpressNumber(req.getExpressNumber());
        byId.setExpressCompany(req.getExpressCompany());
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java
@@ -10,6 +10,8 @@
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.stock.dto.FinishedProductTreeDto;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.execl.FinishedProductInventoryExportData;
import com.ruoyi.stock.execl.NonFinishedProductInventoryExportData;
import com.ruoyi.stock.execl.StockInventoryExportData;
import com.ruoyi.stock.service.StockInventoryService;
import io.swagger.annotations.Api;
@@ -48,11 +50,13 @@
        return R.ok(stockInventoryDtoIPage);
    }
    @GetMapping("/finishedProductList")
    /**
     * æŸ¥è¯¢æˆå“åº“存树
     */
    @ApiOperation("查询成品库存树")
    public R finishedProductList(StockInventoryDto stockInventoryDto) {
        List<FinishedProductTreeDto> list = stockInventoryService.finishedProductList(stockInventoryDto);
        return R.ok(list);
    @GetMapping("/finishedProductList")
    public List<FinishedProductTreeDto> finishedProductList(StockInventoryDto stockInventoryDto) {
        return stockInventoryService.finishedProductList(stockInventoryDto);
    }
@@ -85,22 +89,33 @@
    @PostMapping("importStockInventory")
    @ApiOperation("导入库存")
    public R importStockInventory(MultipartFile file) {
        return stockInventoryService.importStockInventory(file);
    public R importStockInventory(MultipartFile file,
                                  @RequestParam Integer productType) {
        return stockInventoryService.importStockInventory(file, productType);
    }
    @Log(title = "下载库存导入模板", businessType = BusinessType.EXPORT)
    @PostMapping("/downloadStockInventory")
    public void downloadStockInventory(HttpServletResponse response) {
        List<StockInventoryExportData> list = new ArrayList<>();
        ExcelUtil<StockInventoryExportData> util = new ExcelUtil<>(StockInventoryExportData.class);
        util.exportExcel(response, list, "库存模板");
    public void downloadStockInventory(HttpServletResponse response,
                                        @RequestParam Integer productType) {
        // productType: 1=成品, 0=非成品
        if (productType == 1) {
            List<FinishedProductInventoryExportData> list = new ArrayList<>();
            ExcelUtil<FinishedProductInventoryExportData> util = new ExcelUtil<>(FinishedProductInventoryExportData.class);
            util.exportExcel(response, list, "成品库存模板");
        } else {
            List<NonFinishedProductInventoryExportData> list = new ArrayList<>();
            ExcelUtil<NonFinishedProductInventoryExportData> util = new ExcelUtil<>(NonFinishedProductInventoryExportData.class);
            util.exportExcel(response, list, "非成品库存模板");
        }
    }
    @PostMapping("/exportStockInventory")
    @ApiOperation("导出库存")
    public void exportStockInventory(HttpServletResponse response, StockInventoryDto stockInventoryDto) {
        stockInventoryService.exportStockInventory(response, stockInventoryDto);
    public void exportStockInventory(HttpServletResponse response,
                                      StockInventoryDto stockInventoryDto,
                                      @RequestParam Integer productType) {
        stockInventoryService.exportStockInventory(response, stockInventoryDto, productType);
    }
    @GetMapping("stockInventoryPage")
src/main/java/com/ruoyi/stock/dto/FinishedProductTreeDto.java
@@ -1,31 +1,45 @@
package com.ruoyi.stock.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel(value = "FinishedProductTreeDto", description = "成品树形DTO")
public class FinishedProductTreeDto {
    private Long id;
    private Long parentId;
    private String productName;
    private String label;
    @ApiModelProperty(value = "产品ID")
    private Long productId;
    @ApiModelProperty(value = "产品名称")
    private String productName;
    @ApiModelProperty(value = "标签/显示名称")
    private String label;
    @ApiModelProperty(value = "库存ID")
    private Long stockId;
    @ApiModelProperty(value = "产品型号ID")
    private Long productModelId;
    @ApiModelProperty(value = "型号")
    private String model;
    private String materialCode;
    @ApiModelProperty(value = "工序类别")
    private String processCategory;
    @ApiModelProperty(value = "电压")
    private String voltage;
    @ApiModelProperty(value = "物料编码")
    private String materialCode;
    @ApiModelProperty(value = "单位")
    private String unit;
    @ApiModelProperty(value = "子节点列表")
    private List<FinishedProductTreeDto> children;
}
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java
@@ -2,13 +2,16 @@
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.stock.pojo.StockInventory;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDate;
@EqualsAndHashCode(callSuper = true)
@Data
public class StockInventoryDto extends StockInventory {
@@ -74,4 +77,6 @@
    @Schema(description = "不合格批次号")
    private String unQualifiedBatchNo;
    @ApiModelProperty("库存id")
    private Long stockId;
}
src/main/java/com/ruoyi/stock/dto/StockOutRecordDto.java
@@ -30,6 +30,10 @@
    private String createBy;
    private String processName;
    private String recordTypeName;
    // é¡¶éƒ¨çˆ¶äº§å“id
    private Long topParentProductId;
}
src/main/java/com/ruoyi/stock/execl/FinishedProductInventoryExportData.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package com.ruoyi.stock.execl;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import lombok.Data;
import java.math.BigDecimal;
/**
 * æˆå“åº“存导入导出实体(包含工序类别和电压)
 */
@Data
public class FinishedProductInventoryExportData {
    @Excel(name = "产品名称")
    private String productName;
    @Excel(name = "规格型号")
    private String model;
    @Excel(name = "单位")
    private String unit;
    @Excel(name = "料号")
    private String materialCode;
    @Excel(name = "工序类别")
    private String processCategory;
    @Excel(name = "电压")
    private String voltage;
    // @Excel(name = "合格库存批号")
    private String qualifiedBatchNo;
    // @Excel(name = "不合格库存批号")
    private String unQualifiedBatchNo;
    @Excel(name = "合格库存数量")
    private BigDecimal qualifiedQuantity;
    @Excel(name = "不合格库存数量")
    private BigDecimal unQualifiedQuantity;
    @Excel(name = "预警数量")
    private BigDecimal warnNum;
    @Excel(name = "合格冻结数量")
    private BigDecimal qualifiedLockedQuantity;
    @Excel(name = "不合格冻结数量")
    private BigDecimal unQualifiedLockedQuantity;
    @Excel(name = "备注")
    private String remark;
}
src/main/java/com/ruoyi/stock/execl/NonFinishedProductInventoryExportData.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package com.ruoyi.stock.execl;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import lombok.Data;
import java.math.BigDecimal;
/**
 * éžæˆå“åº“存导入导出实体(不包含工序类别和电压)
 */
@Data
public class NonFinishedProductInventoryExportData {
    @Excel(name = "产品名称")
    private String productName;
    @Excel(name = "规格型号")
    private String model;
    @Excel(name = "单位")
    private String unit;
    @Excel(name = "料号")
    private String materialCode;
    // @Excel(name = "合格库存批号")
    private String qualifiedBatchNo;
    // @Excel(name = "不合格库存批号")
    private String unQualifiedBatchNo;
    @Excel(name = "合格库存数量")
    private BigDecimal qualifiedQuantity;
    @Excel(name = "不合格库存数量")
    private BigDecimal unQualifiedQuantity;
    @Excel(name = "预警数量")
    private BigDecimal warnNum;
    @Excel(name = "合格冻结数量")
    private BigDecimal qualifiedLockedQuantity;
    @Excel(name = "不合格冻结数量")
    private BigDecimal unQualifiedLockedQuantity;
    @Excel(name = "备注")
    private String remark;
}
src/main/java/com/ruoyi/stock/execl/StockInventoryExportData.java
@@ -10,25 +10,25 @@
    @Excel(name = "产品名称")
    private String productName;
    @Excel(name = "型号")
    @Excel(name = "规格型号")
    private String model;
    @Excel(name = "单位")
    private String unit;
    @Excel(name = "物料编码")
    @Excel(name = "料号")
    private String materialCode;
    @Excel(name = "成品类别")
    @Excel(name = "工序类别")
    private String processCategory;
    @Excel(name = "电压")
    private String voltage;
    @Excel(name = "合格库存批号")
    // @Excel(name = "合格库存批号")
    private String qualifiedBatchNo;
    @Excel(name = "不合格库存批号")
    // @Excel(name = "不合格库存批号")
    private String unQualifiedBatchNo;
    @Excel(name = "合格库存数量")
src/main/java/com/ruoyi/stock/execl/StockOutRecordExportData.java
@@ -30,4 +30,7 @@
    @Excel(isExport = false)
    private String type;
    @Excel(isExport = false)
    private String processName;
}
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.stock.dto.FinishedProductTreeDto;
import com.ruoyi.stock.dto.StockInRecordDto;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.execl.StockInventoryExportData;
@@ -54,4 +55,6 @@
    List<StockInventoryDto> selectProductList();
    List<StockInventoryDto> selectFinishedProductInventoryList(@Param("ew") StockInventoryDto stockInventoryDto);
    List<FinishedProductTreeDto> selectFinishedProductList(@Param("ew") StockInventoryDto stockInventoryDto);
}
src/main/java/com/ruoyi/stock/service/StockInventoryService.java
@@ -35,9 +35,9 @@
    Boolean subtractStockInventory(StockInventoryDto stockInventoryDto);
    R importStockInventory(MultipartFile file);
    R importStockInventory(MultipartFile file, Integer productType);
    void exportStockInventory(HttpServletResponse response, StockInventoryDto stockInventoryDto);
    void exportStockInventory(HttpServletResponse response, StockInventoryDto stockInventoryDto, Integer productType);
    IPage<StockInRecordDto> stockInventoryPage(StockInventoryDto stockInventoryDto,Page page);
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -16,17 +16,14 @@
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.stock.dto.FinishedProductTreeDto;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.stock.dto.StockInRecordDto;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockOutRecordDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
import com.ruoyi.stock.dto.*;
import com.ruoyi.stock.execl.FinishedProductInventoryExportData;
import com.ruoyi.stock.execl.NonFinishedProductInventoryExportData;
import com.ruoyi.stock.execl.StockInventoryExportData;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInRecord;
@@ -45,12 +42,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -79,40 +71,40 @@
        return stockInventoryMapper.pagestockInventory(page, stockInventoryDto);
    }
    /**
     * æŸ¥è¯¢æˆå“åº“存树。
     * è¿”回结构沿用基础资料产品树,叶子节点补充成品库存维度:型号、工序分类、电压。
     */
    @Override
    public List<FinishedProductTreeDto> finishedProductList(StockInventoryDto stockInventoryDto) {
        List<StockInventoryDto> inventoryList = stockInventoryMapper.selectFinishedProductInventoryList(stockInventoryDto);
        if (inventoryList.isEmpty()) {
        // æŸ¥è¯¢åº“存数据
        List<FinishedProductTreeDto> dataList = stockInventoryMapper.selectFinishedProductList(stockInventoryDto);
        if (dataList.isEmpty()) {
            return new ArrayList<>();
        }
        List<Product> allProducts = productMapper.selectList(null);
        Map<Long, Product> productMap = new HashMap<>();
        Map<Long, List<Product>> childrenMap = new HashMap<>();
        for (Product product : allProducts) {
            productMap.put(product.getId(), product);
            childrenMap.computeIfAbsent(product.getParentId(), key -> new ArrayList<>()).add(product);
        // æŒ‰äº§å“ID分组,构建树形结构
        Map<Long, FinishedProductTreeDto> productMap = new LinkedHashMap<>();
        for (FinishedProductTreeDto data : dataList) {
            Long productId = data.getProductId();
            if (!productMap.containsKey(productId)) {
                // åˆ›å»ºäº§å“å¤§ç±»èŠ‚ç‚¹
                FinishedProductTreeDto productNode = new FinishedProductTreeDto();
                productNode.setProductId(productId);
                productNode.setProductName(data.getProductName());
                productNode.setLabel(data.getLabel());
                productNode.setChildren(new ArrayList<>());
                productMap.put(productId, productNode);
            }
            // æ·»åŠ åº“å­˜å¶å­èŠ‚ç‚¹
            FinishedProductTreeDto leafNode = new FinishedProductTreeDto();
            leafNode.setStockId(data.getStockId());
            leafNode.setProductModelId(data.getProductModelId());
            leafNode.setModel(data.getModel());
            leafNode.setProcessCategory(data.getProcessCategory());
            leafNode.setVoltage(data.getVoltage());
            leafNode.setMaterialCode(data.getMaterialCode());
            leafNode.setUnit(data.getUnit());
            productMap.get(productId).getChildren().add(leafNode);
        }
        Map<Long, List<FinishedProductTreeDto>> leafMap = buildFinishedProductLeafMap(inventoryList);
        Set<Long> visibleProductIds = collectVisibleProductIds(leafMap.keySet(), productMap);
        List<FinishedProductTreeDto> result = new ArrayList<>();
        for (Product rootProduct : allProducts) {
            if (!isFinishedRoot(rootProduct)) {
                continue;
            }
            FinishedProductTreeDto rootNode = buildProductNode(rootProduct);
            rootNode.setChildren(buildFinishedChildren(rootProduct.getId(), childrenMap, leafMap, visibleProductIds));
            if (!rootNode.getChildren().isEmpty()) {
                result.add(rootNode);
            }
        }
        return result;
        return new ArrayList<>(productMap.values());
    }
    @Override
@@ -279,6 +271,25 @@
        stockOutRecordDto.setType("0");
        stockOutRecordService.add(stockOutRecordDto);
        //销售出库按照保存的库存id
        if (stockInventoryDto.getStockId() != null) {
            StockInventory stockInventory = stockInventoryMapper.selectById(stockInventoryDto.getStockId());
            if (ObjectUtils.isEmpty(stockInventory)) {
                throw new RuntimeException("产品库存不存在");
            }
            BigDecimal lockedQty = defaultDecimal(stockInventory.getLockedQuantity());
            if (stockInventoryDto.getQualitity().compareTo(defaultDecimal(stockInventory.getQualitity()).subtract(lockedQty)) > 0) {
                ProductModel productModel = productModelMapper.selectById(stockInventoryDto.getProductModelId());
                Product product = productMapper.selectById(productModel.getProductId());
                throw new RuntimeException(product.getProductName() + "/" + productModel.getModel() + "库存不足,无法出库");
            }
            stockInventory.setQualitity(defaultDecimal(stockInventory.getQualitity()).subtract(stockInventoryDto.getQualitity()));
            stockInventory.setVersion(stockInventory.getVersion() == null ? 1 : stockInventory.getVersion() + 1);
            stockInventory.setUpdateTime(LocalDateTime.now());
            stockInventoryMapper.updateById(stockInventory);
            return true;
        }
        if (StringUtils.isBlank(stockInventoryDto.getBatchNo()) && !usesDimensionIdentity(normalizeDimension(stockInventoryDto.getProcessCategory()), normalizeDimension(stockInventoryDto.getVoltage()))) {
            List<StockInventory> stockInventories = stockInventoryMapper.selectList(new QueryWrapper<StockInventory>().lambda()
                    .eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId())
@@ -335,7 +346,7 @@
    }
    @Override
    public R importStockInventory(MultipartFile file) {
    public R importStockInventory(MultipartFile file, Integer productType) {
        try {
            List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectProduct();
            Map<String, SalesLedgerProduct> productMap = new HashMap<>();
@@ -344,65 +355,129 @@
                productMap.put(key, product);
            }
            ExcelUtil<StockInventoryExportData> util = new ExcelUtil<>(StockInventoryExportData.class);
            List<StockInventoryExportData> list = util.importExcel(file.getInputStream());
            List<String> unmatchedRecords = new ArrayList<>();
            int successCount = 0;
            for (StockInventoryExportData dto : list) {
                String key = dto.getProductName() + "|" + dto.getModel();
                SalesLedgerProduct matchedProduct = productMap.get(key);
            // productType: 1=成品, 0=非成品
            if (productType == 1) {
                // æˆå“å¯¼å…¥ï¼ˆåŒ…含工序类别和电压)
                ExcelUtil<FinishedProductInventoryExportData> util = new ExcelUtil<>(FinishedProductInventoryExportData.class);
                List<FinishedProductInventoryExportData> list = util.importExcel(file.getInputStream());
                if (matchedProduct != null) {
                    if (dto.getQualifiedQuantity() != null && dto.getQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                        StockInventoryDto stockInventoryDto = new StockInventoryDto();
                        stockInventoryDto.setRecordId(0L);
                        stockInventoryDto.setRecordType(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_STOCK_IN.getCode());
                        stockInventoryDto.setQualitity(dto.getQualifiedQuantity());
                        stockInventoryDto.setRemark(dto.getRemark());
                        stockInventoryDto.setWarnNum(dto.getWarnNum());
                        stockInventoryDto.setBatchNo(dto.getQualifiedBatchNo());
                        stockInventoryDto.setProcessCategory(dto.getProcessCategory());
                        stockInventoryDto.setVoltage(dto.getVoltage());
                for (FinishedProductInventoryExportData dto : list) {
                    String key = dto.getProductName() + "|" + dto.getModel();
                    SalesLedgerProduct matchedProduct = productMap.get(key);
                        if (ObjectUtils.isNotEmpty(dto.getQualifiedLockedQuantity())) {
                            if (dto.getQualifiedLockedQuantity().compareTo(dto.getQualifiedQuantity()) > 0) {
                                throw new RuntimeException("合格冻结数量不能超过本次导入的合格库存数量");
                    if (matchedProduct != null) {
                        if (dto.getQualifiedQuantity() != null && dto.getQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                            StockInventoryDto stockInventoryDto = new StockInventoryDto();
                            stockInventoryDto.setRecordId(0L);
                            stockInventoryDto.setRecordType(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_STOCK_IN.getCode());
                            stockInventoryDto.setQualitity(dto.getQualifiedQuantity());
                            stockInventoryDto.setRemark(dto.getRemark());
                            stockInventoryDto.setWarnNum(dto.getWarnNum());
                            stockInventoryDto.setBatchNo(dto.getQualifiedBatchNo());
                            stockInventoryDto.setProcessCategory(dto.getProcessCategory());
                            stockInventoryDto.setVoltage(dto.getVoltage());
                            if (ObjectUtils.isNotEmpty(dto.getQualifiedLockedQuantity())) {
                                if (dto.getQualifiedLockedQuantity().compareTo(dto.getQualifiedQuantity()) > 0) {
                                    throw new RuntimeException("合格冻结数量不能超过本次导入的合格库存数量");
                                }
                                stockInventoryDto.setLockedQuantity(dto.getQualifiedLockedQuantity());
                            } else {
                                stockInventoryDto.setLockedQuantity(BigDecimal.ZERO);
                            }
                            stockInventoryDto.setLockedQuantity(dto.getQualifiedLockedQuantity());
                        } else {
                            stockInventoryDto.setLockedQuantity(BigDecimal.ZERO);
                            stockInventoryDto.setProductModelId(matchedProduct.getProductModelId());
                            this.addstockInventory(stockInventoryDto);
                            successCount++;
                        }
                        stockInventoryDto.setProductModelId(matchedProduct.getProductModelId());
                        this.addstockInventory(stockInventoryDto);
                        successCount++;
                    }
                        if (dto.getUnQualifiedQuantity() != null && dto.getUnQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                            StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
                            stockUninventoryDto.setRecordId(0L);
                            stockUninventoryDto.setRecordType(StockInUnQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_IN.getCode());
                            stockUninventoryDto.setQualitity(dto.getUnQualifiedQuantity());
                            stockUninventoryDto.setRemark(dto.getRemark());
                            stockUninventoryDto.setBatchNo(dto.getUnQualifiedBatchNo());
                    if (dto.getUnQualifiedQuantity() != null && dto.getUnQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                        StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
                        stockUninventoryDto.setRecordId(0L);
                        stockUninventoryDto.setRecordType(StockInUnQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_IN.getCode());
                        stockUninventoryDto.setQualitity(dto.getUnQualifiedQuantity());
                        stockUninventoryDto.setRemark(dto.getRemark());
                        stockUninventoryDto.setBatchNo(dto.getUnQualifiedBatchNo());
                        if (ObjectUtils.isNotEmpty(dto.getUnQualifiedLockedQuantity())) {
                            if (dto.getUnQualifiedLockedQuantity().compareTo(dto.getUnQualifiedQuantity()) > 0) {
                                throw new RuntimeException("不合格冻结数量不能超过本次导入的不合格库存数量");
                            if (ObjectUtils.isNotEmpty(dto.getUnQualifiedLockedQuantity())) {
                                if (dto.getUnQualifiedLockedQuantity().compareTo(dto.getUnQualifiedQuantity()) > 0) {
                                    throw new RuntimeException("不合格冻结数量不能超过本次导入的不合格库存数量");
                                }
                                stockUninventoryDto.setLockedQuantity(dto.getUnQualifiedLockedQuantity());
                            } else {
                                stockUninventoryDto.setLockedQuantity(BigDecimal.ZERO);
                            }
                            stockUninventoryDto.setLockedQuantity(dto.getUnQualifiedLockedQuantity());
                        } else {
                            stockUninventoryDto.setLockedQuantity(BigDecimal.ZERO);
                            stockUninventoryDto.setProductModelId(matchedProduct.getProductModelId());
                            stockUninventoryService.addStockUninventory(stockUninventoryDto);
                            successCount++;
                        }
                    } else {
                        String unmatchedRecord = "产品名称:" + dto.getProductName() + ",型号:" + dto.getModel() + " æœªåŒ¹é…åˆ°åº“存产品";
                        unmatchedRecords.add(unmatchedRecord);
                    }
                }
            } else {
                // éžæˆå“å¯¼å…¥ï¼ˆä¸åŒ…含工序类别和电压)
                ExcelUtil<NonFinishedProductInventoryExportData> util = new ExcelUtil<>(NonFinishedProductInventoryExportData.class);
                List<NonFinishedProductInventoryExportData> list = util.importExcel(file.getInputStream());
                for (NonFinishedProductInventoryExportData dto : list) {
                    String key = dto.getProductName() + "|" + dto.getModel();
                    SalesLedgerProduct matchedProduct = productMap.get(key);
                    if (matchedProduct != null) {
                        if (dto.getQualifiedQuantity() != null && dto.getQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                            StockInventoryDto stockInventoryDto = new StockInventoryDto();
                            stockInventoryDto.setRecordId(0L);
                            stockInventoryDto.setRecordType(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_STOCK_IN.getCode());
                            stockInventoryDto.setQualitity(dto.getQualifiedQuantity());
                            stockInventoryDto.setRemark(dto.getRemark());
                            stockInventoryDto.setWarnNum(dto.getWarnNum());
                            stockInventoryDto.setBatchNo(dto.getQualifiedBatchNo());
                            if (ObjectUtils.isNotEmpty(dto.getQualifiedLockedQuantity())) {
                                if (dto.getQualifiedLockedQuantity().compareTo(dto.getQualifiedQuantity()) > 0) {
                                    throw new RuntimeException("合格冻结数量不能超过本次导入的合格库存数量");
                                }
                                stockInventoryDto.setLockedQuantity(dto.getQualifiedLockedQuantity());
                            } else {
                                stockInventoryDto.setLockedQuantity(BigDecimal.ZERO);
                            }
                            stockInventoryDto.setProductModelId(matchedProduct.getProductModelId());
                            this.addstockInventory(stockInventoryDto);
                            successCount++;
                        }
                        stockUninventoryDto.setProductModelId(matchedProduct.getProductModelId());
                        stockUninventoryService.addStockUninventory(stockUninventoryDto);
                        successCount++;
                        if (dto.getUnQualifiedQuantity() != null && dto.getUnQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                            StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
                            stockUninventoryDto.setRecordId(0L);
                            stockUninventoryDto.setRecordType(StockInUnQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_IN.getCode());
                            stockUninventoryDto.setQualitity(dto.getUnQualifiedQuantity());
                            stockUninventoryDto.setRemark(dto.getRemark());
                            stockUninventoryDto.setBatchNo(dto.getUnQualifiedBatchNo());
                            if (ObjectUtils.isNotEmpty(dto.getUnQualifiedLockedQuantity())) {
                                if (dto.getUnQualifiedLockedQuantity().compareTo(dto.getUnQualifiedQuantity()) > 0) {
                                    throw new RuntimeException("不合格冻结数量不能超过本次导入的不合格库存数量");
                                }
                                stockUninventoryDto.setLockedQuantity(dto.getUnQualifiedLockedQuantity());
                            } else {
                                stockUninventoryDto.setLockedQuantity(BigDecimal.ZERO);
                            }
                            stockUninventoryDto.setProductModelId(matchedProduct.getProductModelId());
                            stockUninventoryService.addStockUninventory(stockUninventoryDto);
                            successCount++;
                        }
                    } else {
                        String unmatchedRecord = "产品名称:" + dto.getProductName() + ",型号:" + dto.getModel() + " æœªåŒ¹é…åˆ°åº“存产品";
                        unmatchedRecords.add(unmatchedRecord);
                    }
                } else {
                    String unmatchedRecord = "产品名称:" + dto.getProductName() + ",型号:" + dto.getModel() + " æœªåŒ¹é…åˆ°åº“存产品";
                    unmatchedRecords.add(unmatchedRecord);
                }
            }
@@ -423,10 +498,55 @@
    }
    @Override
    public void exportStockInventory(HttpServletResponse response, StockInventoryDto stockInventoryDto) {
    public void exportStockInventory(HttpServletResponse response, StockInventoryDto stockInventoryDto, Integer productType) {
        List<StockInventoryExportData> list = stockInventoryMapper.listStockInventoryExportData(stockInventoryDto);
        ExcelUtil<StockInventoryExportData> util = new ExcelUtil<>(StockInventoryExportData.class);
        util.exportExcel(response, list, "库存信息");
        // productType: 1=成品, 0=非成品
        if (productType == 1) {
            // æˆå“å¯¼å‡ºï¼ˆåŒ…含工序类别和电压)
            List<FinishedProductInventoryExportData> finishedList = new ArrayList<>();
            for (StockInventoryExportData data : list) {
                FinishedProductInventoryExportData finishedData = new FinishedProductInventoryExportData();
                finishedData.setProductName(data.getProductName());
                finishedData.setModel(data.getModel());
                finishedData.setUnit(data.getUnit());
                finishedData.setMaterialCode(data.getMaterialCode());
                finishedData.setProcessCategory(data.getProcessCategory());
                finishedData.setVoltage(data.getVoltage());
                finishedData.setQualifiedBatchNo(data.getQualifiedBatchNo());
                finishedData.setUnQualifiedBatchNo(data.getUnQualifiedBatchNo());
                finishedData.setQualifiedQuantity(data.getQualifiedQuantity());
                finishedData.setUnQualifiedQuantity(data.getUnQualifiedQuantity());
                finishedData.setWarnNum(data.getWarnNum());
                finishedData.setQualifiedLockedQuantity(data.getQualifiedLockedQuantity());
                finishedData.setUnQualifiedLockedQuantity(data.getUnQualifiedLockedQuantity());
                finishedData.setRemark(data.getRemark());
                finishedList.add(finishedData);
            }
            ExcelUtil<FinishedProductInventoryExportData> util = new ExcelUtil<>(FinishedProductInventoryExportData.class);
            util.exportExcel(response, finishedList, "成品库存信息");
        } else {
            // éžæˆå“å¯¼å‡ºï¼ˆä¸åŒ…含工序类别和电压)
            List<NonFinishedProductInventoryExportData> nonFinishedList = new ArrayList<>();
            for (StockInventoryExportData data : list) {
                NonFinishedProductInventoryExportData nonFinishedData = new NonFinishedProductInventoryExportData();
                nonFinishedData.setProductName(data.getProductName());
                nonFinishedData.setModel(data.getModel());
                nonFinishedData.setUnit(data.getUnit());
                nonFinishedData.setMaterialCode(data.getMaterialCode());
                nonFinishedData.setQualifiedBatchNo(data.getQualifiedBatchNo());
                nonFinishedData.setUnQualifiedBatchNo(data.getUnQualifiedBatchNo());
                nonFinishedData.setQualifiedQuantity(data.getQualifiedQuantity());
                nonFinishedData.setUnQualifiedQuantity(data.getUnQualifiedQuantity());
                nonFinishedData.setWarnNum(data.getWarnNum());
                nonFinishedData.setQualifiedLockedQuantity(data.getQualifiedLockedQuantity());
                nonFinishedData.setUnQualifiedLockedQuantity(data.getUnQualifiedLockedQuantity());
                nonFinishedData.setRemark(data.getRemark());
                nonFinishedList.add(nonFinishedData);
            }
            ExcelUtil<NonFinishedProductInventoryExportData> util = new ExcelUtil<>(NonFinishedProductInventoryExportData.class);
            util.exportExcel(response, nonFinishedList, "非成品库存信息");
        }
    }
    @Override
@@ -505,89 +625,5 @@
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private Map<Long, List<FinishedProductTreeDto>> buildFinishedProductLeafMap(List<StockInventoryDto> inventoryList) {
        Map<Long, List<FinishedProductTreeDto>> leafMap = new HashMap<>();
        for (StockInventoryDto inventory : inventoryList) {
            FinishedProductTreeDto leafNode = new FinishedProductTreeDto();
            leafNode.setId(inventory.getId() == null ? null : -inventory.getId());
            leafNode.setParentId(inventory.getProductId());
            leafNode.setProductId(inventory.getProductId());
            leafNode.setProductModelId(inventory.getProductModelId());
            leafNode.setProductName(inventory.getProductName());
            leafNode.setLabel(buildLeafLabel(inventory));
            leafNode.setModel(inventory.getModel());
            leafNode.setMaterialCode(inventory.getMaterialCode());
            leafNode.setProcessCategory(inventory.getProcessCategory());
            leafNode.setVoltage(inventory.getVoltage());
            leafNode.setChildren(new ArrayList<>());
            leafMap.computeIfAbsent(inventory.getProductId(), key -> new ArrayList<>()).add(leafNode);
        }
        return leafMap;
    }
    private Set<Long> collectVisibleProductIds(Set<Long> leafProductIds, Map<Long, Product> productMap) {
        Set<Long> visibleIds = new HashSet<>();
        for (Long productId : leafProductIds) {
            Long currentId = productId;
            while (currentId != null) {
                Product current = productMap.get(currentId);
                if (current == null) {
                    break;
                }
                visibleIds.add(currentId);
                currentId = current.getParentId();
            }
        }
        return visibleIds;
    }
    private List<FinishedProductTreeDto> buildFinishedChildren(Long parentId,
                                                               Map<Long, List<Product>> childrenMap,
                                                               Map<Long, List<FinishedProductTreeDto>> leafMap,
                                                               Set<Long> visibleProductIds) {
        List<FinishedProductTreeDto> children = new ArrayList<>();
        List<Product> childProducts = childrenMap.getOrDefault(parentId, new ArrayList<>());
        for (Product childProduct : childProducts) {
            if (!visibleProductIds.contains(childProduct.getId())) {
                continue;
            }
            FinishedProductTreeDto childNode = buildProductNode(childProduct);
            List<FinishedProductTreeDto> subChildren = buildFinishedChildren(childProduct.getId(), childrenMap, leafMap, visibleProductIds);
            subChildren.addAll(leafMap.getOrDefault(childProduct.getId(), new ArrayList<>()));
            childNode.setChildren(subChildren);
            children.add(childNode);
        }
        return children;
    }
    private FinishedProductTreeDto buildProductNode(Product product) {
        FinishedProductTreeDto node = new FinishedProductTreeDto();
        BeanUtils.copyProperties(product, node);
        node.setLabel(product.getProductName());
        node.setChildren(new ArrayList<>());
        return node;
    }
    private boolean isFinishedRoot(Product product) {
        return product.getParentId() == null && "成品".equals(StringUtils.trimToEmpty(product.getProductName()));
    }
    private String buildLeafLabel(StockInventoryDto inventory) {
        List<String> parts = new ArrayList<>();
        if (StringUtils.isNotBlank(inventory.getProductName())) {
            parts.add(StringUtils.trim(inventory.getProductName()));
        }
        if (StringUtils.isNotBlank(inventory.getModel())) {
            parts.add(StringUtils.trim(inventory.getModel()));
        }
        if (StringUtils.isNotBlank(inventory.getProcessCategory())) {
            parts.add(StringUtils.trim(inventory.getProcessCategory()));
        }
        if (StringUtils.isNotBlank(inventory.getVoltage())) {
            parts.add(StringUtils.trim(inventory.getVoltage()));
        }
        return String.join(" / ", parts);
    }
}
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java
@@ -10,6 +10,7 @@
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.stock.dto.StockInventoryDto;
@@ -49,7 +50,11 @@
    @Override
    public IPage<StockOutRecordDto> listPage(Page page, StockOutRecordDto stockOutRecordDto) {
        return stockOutRecordMapper.listPage(page, stockOutRecordDto);
        IPage<StockOutRecordDto> result = stockOutRecordMapper.listPage(page, stockOutRecordDto);
        for (StockOutRecordDto record : result.getRecords()) {
            record.setRecordTypeName(resolveRecordTypeName(record.getType(), record.getRecordType(), record.getProcessName()));
        }
        return result;
    }
    @Override
@@ -131,7 +136,7 @@
        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());
                stockInRecordExportData.setRecordType(resolveQualifiedRecordType(stockInRecordExportData.getRecordType(), stockInRecordExportData.getProcessName()));
            }else {
                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockOutUnQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
            }
@@ -139,4 +144,26 @@
        ExcelUtil<StockOutRecordExportData> util = new ExcelUtil<>(StockOutRecordExportData.class);
        util.exportExcel(response,list, "出库记录信息");
    }
}
    private String resolveRecordTypeName(String type, String recordType, String processName) {
        if ("0".equals(type)) {
            return resolveQualifiedRecordType(recordType, processName);
        }
        return EnumUtil.fromCode(StockOutUnQualifiedRecordTypeEnum.class, Integer.parseInt(recordType)).getValue();
    }
    private String resolveQualifiedRecordType(String recordType, String processName) {
        if (StockOutQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_OUT.getCode().equals(recordType)
                && StringUtils.isNotEmpty(processName)) {
            return "生产报工-" + processName + "-出库";
        }
        if (recordType == null) {
            return null;
        }
        try {
            return EnumUtil.fromCode(StockOutQualifiedRecordTypeEnum.class, Integer.parseInt(recordType)).getValue();
        } catch (NumberFormatException ex) {
            return recordType;
        }
    }
}
src/main/resources/mapper/approve/ApproveProcessMapper.xml
@@ -38,5 +38,6 @@
        <if test="req.approveType != null ">
            and approve_type = #{req.approveType}
        </if>
        order by approve_time desc
    </select>
</mapper>
src/main/resources/mapper/production/ProductBomMapper.xml
@@ -36,6 +36,7 @@
        <if test="c.version != null">
            and version = #{c.version}
        </if>
        order by create_time desc
    </select>
    <select id="getById" resultType="com.ruoyi.production.dto.ProductBomDto">
        select pb.*,
src/main/resources/mapper/sales/SalesLedgerMapper.xml
@@ -69,7 +69,7 @@
        T1.delivery_date,
        DATEDIFF(T1.delivery_date, CURDATE()) AS delivery_days_diff,
        product_summary.model,
        '' AS voltage,
        product_summary.voltage,
        product_summary.qty,
        CASE
        WHEN shipping_status_counts.total_count = 0 THEN false
@@ -90,7 +90,8 @@
        SELECT
        sales_ledger_id,
        GROUP_CONCAT(IFNULL(specification_model, '') ORDER BY id SEPARATOR ',') AS model,
        GROUP_CONCAT(IFNULL(CAST(quantity AS CHAR), '') ORDER BY id SEPARATOR ',') AS qty
        GROUP_CONCAT(IFNULL(CAST(quantity AS CHAR), '') ORDER BY id SEPARATOR ',') AS qty,
        GROUP_CONCAT(IFNULL(SUBSTRING_INDEX(specification_model, '-', -1), '') ORDER BY id SEPARATOR ',') AS voltage
        FROM sales_ledger_product
        WHERE type = 1
        GROUP BY sales_ledger_id
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml
@@ -14,10 +14,9 @@
        END as has_sufficient_stock
        FROM
        sales_ledger_product T1
        LEFT JOIN (
        select product_model_id,SUM(qualitity) as qualitity,sum(locked_quantity) as locked_quantity from stock_inventory
        group by product_model_id
        )  t2 ON T1.product_model_id = t2.product_model_id
        LEFT JOIN stock_inventory t2
        ON T1.product_model_id = t2.product_model_id
        AND t2.id = T1.stock_id
        LEFT JOIN product_model T3 ON T1.product_model_id = T3.id
        <where>
            <if test="salesLedgerProduct.salesLedgerId != null">
src/main/resources/mapper/stock/StockInventoryMapper.xml
@@ -90,6 +90,7 @@
        <if test="ew.voltage != null and ew.voltage != ''">
            AND si.voltage = #{ew.voltage}
        </if>
        order by si.create_time desc
    </select>
    <select id="pageListCombinedStockInventory" resultType="com.ruoyi.stock.dto.StockInventoryDto">
@@ -510,4 +511,36 @@
        order by p.id asc, pm.id asc, si.id asc
    </select>
    <select id="selectFinishedProductList" resultType="com.ruoyi.stock.dto.FinishedProductTreeDto">
        select si.id as stockId,
               p.id as productId,
               p.product_name as productName,
               p.product_name as label,
               pm.id as productModelId,
               pm.model,
               si.process_category as processCategory,
               si.voltage,
               pm.material_code as materialCode,
               pm.unit
        from stock_inventory si
        inner join product_model pm on si.product_model_id = pm.id
        inner join product p on pm.product_id = p.id
        inner join product pp on p.parent_id = pp.id
        <where>
            pp.product_name = '成品'
            <if test="ew.productName != null and ew.productName != ''">
                and (p.product_name like concat('%', #{ew.productName}, '%')
                or pm.model like concat('%', #{ew.productName}, '%')
                or pm.material_code like concat('%', #{ew.productName}, '%'))
            </if>
            <if test="ew.processCategory != null and ew.processCategory != ''">
                and si.process_category = #{ew.processCategory}
            </if>
            <if test="ew.voltage != null and ew.voltage != ''">
                and si.voltage = #{ew.voltage}
            </if>
        </where>
        order by p.id asc, pm.id asc, si.id asc
    </select>
</mapper>
src/main/resources/mapper/stock/StockOutRecordMapper.xml
@@ -35,11 +35,15 @@
        pm.model,
        pm.unit,
        pm.material_code as materialCode,
        u.nick_name as createBy
        u.nick_name as createBy,
        pp.name as processName
        FROM stock_out_record as sor
        LEFT JOIN product_model as pm on sor.product_model_id = pm.id
        LEFT JOIN product as p on pm.product_id = p.id
        LEFT JOIN sys_user as u on sor.create_user = u.user_id
        LEFT JOIN production_product_main as ppm on sor.record_type = '3' and ppm.id = sor.record_id
        LEFT JOIN product_process_route_item as ppri on ppri.id = ppm.product_process_route_item_id
        LEFT JOIN product_process as pp on pp.id = ppri.process_id
        <where>
            <if test="params.timeStr != null and params.timeStr != ''">
                and sor.create_time like concat('%',#{params.timeStr},'%')
@@ -77,11 +81,15 @@
        pm.model,
        pm.unit,
        pm.material_code as materialCode,
        u.nick_name as createBy
        u.nick_name as createBy,
        pp.name as processName
        FROM stock_out_record as sor
        LEFT JOIN product_model as pm on sor.product_model_id = pm.id
        LEFT JOIN product as p on pm.product_id = p.id
        LEFT JOIN sys_user as u on sor.create_user = u.user_id
        LEFT JOIN production_product_main as ppm on sor.record_type = '3' and ppm.id = sor.record_id
        LEFT JOIN product_process_route_item as ppri on ppri.id = ppm.product_process_route_item_id
        LEFT JOIN product_process as pp on pp.id = ppri.process_id
        <where>
            <if test="params.timeStr != null and params.timeStr != ''">
                and sor.create_time like concat('%',#{params.timeStr},'%')
@@ -102,4 +110,4 @@
        order by sor.id desc
    </select>
</mapper>
</mapper>