fix:1.生产按照特定工序更改
2.库存按照型号,工序类别,电压进行筛选
3.销售页面展示电压,类别
4.生产入库按照电压入库
已添加2个文件
已修改25个文件
1284 ■■■■ 文件已修改
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedServiceImpl.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/FinishedProductTreeDto.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/StockInRecordExportData.java 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/StockInventoryExportData.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/StockInRecord.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/StockInventory.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/StockInventoryService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 375 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/support/FinishedProductStockDimensionResolver.java 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductWorkOrderMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerMapper.xml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInRecordMapper.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
@@ -103,6 +103,18 @@
        stockInventoryService.addstockInventory(stockInventoryDto);
    }
    public void addStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId,
                         String processCategory, String voltage) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
        stockInventoryDto.setRecordType(String.valueOf(recordType));
        stockInventoryDto.setQualitity(quantity);
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setProcessCategory(processCategory);
        stockInventoryDto.setVoltage(voltage);
        stockInventoryService.addstockInventory(stockInventoryDto);
    }
    /**
     * åˆæ ¼å…¥åº“(不审核)
     *
@@ -120,6 +132,18 @@
        stockInventoryService.addstockInventoryNoReview(stockInventoryDto);
    }
    public void addStockNoReview(Long productModelId, BigDecimal quantity, String recordType, Long recordId,
                                 String processCategory, String voltage) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
        stockInventoryDto.setRecordType(String.valueOf(recordType));
        stockInventoryDto.setQualitity(quantity);
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setProcessCategory(processCategory);
        stockInventoryDto.setVoltage(voltage);
        stockInventoryService.addstockInventoryNoReview(stockInventoryDto);
    }
    /**
     * åˆæ ¼å‡ºåº“
     *
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -30,6 +30,7 @@
import com.ruoyi.quality.mapper.*;
import com.ruoyi.quality.pojo.*;
import com.ruoyi.quality.service.IQualityInspectService;
import com.ruoyi.stock.support.FinishedProductStockDimensionResolver;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -47,6 +48,10 @@
@AllArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class ProductionProductMainServiceImpl extends ServiceImpl<ProductionProductMainMapper, ProductionProductMain> implements ProductionProductMainService {
    private static final String PROCESS_VOLTAGE_SORT = "电压分选";
    private static final String PROCESS_OPTICAL_INSPECTION = "光检外观";
    private static final String PROCESS_PACKAGING = "包装";
    private IQualityInspectService qualityInspectService;
    private ProductionProductMainMapper productionProductMainMapper;
@@ -86,6 +91,11 @@
    private StockUtils stockUtils;
    /**
     * è§£æžç”Ÿäº§æŠ¥å·¥å¯¹åº”的成品入库维度。
     */
    private FinishedProductStockDimensionResolver finishedProductStockDimensionResolver;
    private ISysNoticeService sysNoticeService;
    @Override
@@ -93,6 +103,9 @@
        return productionProductMainMapper.listPageProductionProductMainDto(page, productionProductMainDto);
    }
    /**
     * æ–°å¢žæŠ¥å·¥ï¼Œå¹¶æ ¹æ®å½“前工序处理质检和入库逻辑。
     */
    @Override
    public Boolean addProductMain(ProductionProductMainDto dto) {
        SysUser user = userMapper.selectUserById(dto.getUserId());
@@ -100,9 +113,8 @@
        //当前工艺路线对应的工序详情
        ProductProcessRouteItem productProcessRouteItem = productProcessRouteItemMapper.selectById(dto.getProductProcessRouteItemId());
        if (productProcessRouteItem == null) {
            throw new RuntimeException("工艺路线项不存在");
            throw new RuntimeException("工艺路线工序项不存在");
        }
        // æ˜¯å¦èƒ½æŠ¥å·¥ï¼š 1. ç¬¬ä¸€ä¸ªå·¥åºèƒ½æŠ¥å·¥ 2. ä¸Šä¸€ä¸ªå·¥åºå·²æŠ¥å·¥ 3. ä¹‹å‰çš„工序未被隔离
        //检查上一个工序是否已报工
        Integer currentDragSort = productProcessRouteItem.getDragSort();
        if (currentDragSort != null && currentDragSort > 1) {
@@ -111,7 +123,7 @@
                    currentDragSort
            );
            if (!isPreviousReported) {
                throw new RuntimeException("上一个工序尚未报工,不能进行当前工序报工");
                throw new RuntimeException("上一道工序尚未报工,当前工序不能报工");
            }
            // æŸ¥è¯¢æ‰€æœ‰ä¹‹å‰çš„工序(排序号小于当前工序)
@@ -133,7 +145,7 @@
                // æ£€æŸ¥ä¹‹å‰çš„工序是否有被隔离的不合格记录
                List<QualityUnqualified> unqualifiedList = qualityUnqualifiedMapper.selectUnqualifiedByProcessNames(previousProcessNames);
                if (CollectionUtils.isNotEmpty(unqualifiedList)) {
                    throw new RuntimeException("之前的工序已被隔离,不能进行当前工序报工");
                    throw new RuntimeException("前序工序存在隔离记录,当前工序不能报工");
                }
            }
        }
@@ -219,13 +231,11 @@
            // æ ¸å¿ƒè®¡ç®—
            BigDecimal needQty = childQty.divide(parentQty, 6, RoundingMode.HALF_UP).multiply(dto.getQuantity());
            productionProductInput.setQuantity(needQty);
            productionProductInput.setProductMainId(productionProductMain.getId());
            productionProductInputMapper.insert(productionProductInput);
            stockUtils.substractStock(productStructureDto.getProductModelId(), productionProductInput.getQuantity(), StockOutQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_OUT.getCode(), productionProductMain.getId(), null);
            stockUtils.substractStock(productStructureDto.getProductModelId(), productionProductInput.getQuantity(),
                    StockOutQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_OUT.getCode(), productionProductMain.getId(), null);
        }
        /*新增报工产出表*/
        ProductionProductOutput productionProductOutput = new ProductionProductOutput();
@@ -241,16 +251,27 @@
        //合格数量=报工数量-报废数量
        BigDecimal productQty = productionProductOutput.getQuantity().subtract(productionProductOutput.getScrapQty());
        List<ProductProcessRouteItem> productProcessRouteItems = productProcessRouteItemMapper.selectList(Wrappers.<ProductProcessRouteItem>lambdaQuery().eq(ProductProcessRouteItem::getProductRouteId, productProcessRouteItem.getProductRouteId()));
        //只有合格数量>0才能增加相应数据
        List<ProductProcessRouteItem> productProcessRouteItems = productProcessRouteItemMapper.selectList(
                Wrappers.<ProductProcessRouteItem>lambdaQuery()
                        .eq(ProductProcessRouteItem::getProductRouteId, productProcessRouteItem.getProductRouteId())
        );
        boolean isRouteLastProcess = productProcessRouteItem.getDragSort() == productProcessRouteItems.size();
        ReportStockRule reportStockRule = resolveReportStockRule(productProcessRouteItem, productProcess, productProcessRouteItems);
        String processCategory = null;
        String voltage = null;
        if (productQty.compareTo(BigDecimal.ZERO) > 0 && reportStockRule.isFinishedGoodsStockIn()) {
            processCategory = finishedProductStockDimensionResolver.resolveProcessCategory(productionProductMain.getId());
            voltage = finishedProductStockDimensionResolver.resolveVoltage(productionProductMain.getId());
        }
        if (productQty.compareTo(BigDecimal.ZERO) > 0) {
            /*新增质检*/
            if (productProcessRouteItem.getIsQuality()) {
                //对应的过程检或者出厂检
                int inspectType = 1;
                String process = productProcess.getName();//工序
                if (productProcessRouteItem.getDragSort() == productProcessRouteItems.size()) {
                    //最后一道工序生成出厂检
                String process = productProcess.getName();
                if (reportStockRule.isFinishedGoodsStockIn()) {
                    inspectType = 2;
                    process = null;
                }
@@ -260,11 +281,11 @@
                qualityInspect.setProductName(product.getProductName());
                qualityInspect.setModel(productModel.getModel());
                qualityInspect.setUnit(productModel.getUnit());
                qualityInspect.setQuantity(productQty);
                qualityInspect.setQuantity(productionProductOutput.getQuantity());
                qualityInspect.setProcess(process);
                qualityInspect.setInspectState(0);
                qualityInspect.setInspectType(inspectType);
                qualityInspect.setDefectiveQuantity(BigDecimal.ZERO);
                qualityInspect.setDefectiveQuantity(productionProductOutput.getScrapQty());
                qualityInspect.setProductMainId(productionProductMain.getId());
                qualityInspect.setProductModelId(productModel.getId());
                qualityInspectMapper.insert(qualityInspect);
@@ -272,8 +293,10 @@
                if (qualityTestStandard.size() > 0) {
                    qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
                    qualityInspectMapper.updateById(qualityInspect);
                    qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                                    .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId()))//默认获取最新的
                    qualityTestStandardParamMapper.selectList(
                                    Wrappers.<QualityTestStandardParam>lambdaQuery()
                                            .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId())
                            )
                            .forEach(qualityTestStandardParam -> {
                                QualityInspectParam param = new QualityInspectParam();
                                BeanUtils.copyProperties(qualityTestStandardParam, param);
@@ -283,38 +306,42 @@
                            });
                }
            }else {
                //直接入库
                //最后一道工序为成品
                if (productProcessRouteItem.getDragSort() == productProcessRouteItems.size()) {
                    //成品审核
                    stockUtils.addStock(productProcessRouteItem.getProductModelId(), productQty, StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(), productionProductMain.getId());
                if (reportStockRule.shouldCreateStockIn()) {
                    if (reportStockRule.isFinishedGoodsStockIn()) {
                        stockUtils.addStock(productProcessRouteItem.getProductModelId(), productQty,
                                StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(), productionProductMain.getId(),
                                processCategory, voltage);
                }else {
                    //半成品不审核
                    stockUtils.addStockNoReview(productProcessRouteItem.getProductModelId(), productQty, StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(), productionProductMain.getId());
                        stockUtils.addStockNoReview(productProcessRouteItem.getProductModelId(), productQty,
                                StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(), productionProductMain.getId());
                }
            }
            /*更新工单和生产订单*/
                if (productionProductOutput.getScrapQty().compareTo(BigDecimal.ZERO) > 0) {
                    stockUtils.addUnStockNoReview(productProcessRouteItem.getProductModelId(), productionProductOutput.getScrapQty(),
                            StockInUnQualifiedRecordTypeEnum.QUALITYINSPECT_UNSTOCK_IN.getCode(), productionProductMain.getId());
                }
            }
            productWorkOrder.setCompleteQuantity(productWorkOrder.getCompleteQuantity().add(productQty));
            if (ObjectUtils.isNull(productWorkOrder.getActualStartTime())) {
                productWorkOrder.setActualStartTime(LocalDate.now());//实际开始时间
                productWorkOrder.setActualStartTime(LocalDate.now());
            }
            if (productWorkOrder.getCompleteQuantity().compareTo(productWorkOrder.getPlanQuantity()) == 0) {
                productWorkOrder.setActualEndTime(LocalDate.now());//实际结束时间
                productWorkOrder.setActualEndTime(LocalDate.now());
            }
            productWorkOrderMapper.updateById(productWorkOrder);
            //生产订单
            if (ObjectUtils.isNull(productOrder.getStartTime())) {
                productOrder.setStartTime(LocalDateTime.now());//开始时间
                productOrder.setStartTime(LocalDateTime.now());
            }
            if (productProcessRouteItem.getDragSort() == productProcessRouteItems.size()) {
                //如果是最后一道工序报工之后生产订单完成数量+
            if (isRouteLastProcess) {
                productOrder.setCompleteQuantity(productOrder.getCompleteQuantity().add(productQty));
                if (productOrder.getCompleteQuantity().compareTo(productOrder.getQuantity()) == 0) {
                    productOrder.setEndTime(LocalDateTime.now());//结束时间
                    productOrder.setEndTime(LocalDateTime.now());
                }
            }
            productOrderMapper.updateById(productOrder);
            /*添加生产核算*/
            SalesLedgerProductionAccounting salesLedgerProductionAccounting = SalesLedgerProductionAccounting.builder()
                    .productMainId(productionProductMain.getId())
                    .schedulingUserId(userId)
@@ -327,40 +354,122 @@
                    .build();
            salesLedgerProductionAccountingMapper.insert(salesLedgerProductionAccounting);
        }
        //如果报废数量>0,需要进入报废的库存
        if (ObjectUtils.isNotEmpty(dto.getScrapQty())) {
            if (dto.getScrapQty().compareTo(BigDecimal.ZERO) > 0) {
                if (productProcessRouteItem.getDragSort() == productProcessRouteItems.size()){
                    stockUtils.addUnStock(productModel.getId(), dto.getScrapQty(), StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode(), productionProductMain.getId());
                }else {
                    stockUtils.addUnStockNoReview(productModel.getId(), dto.getScrapQty(), StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode(), productionProductMain.getId());
                }
            }
        }
        return true;
    }
    /**
     * åˆ¤æ–­å½“前报工是否需要入库,以及是否按成品入库处理。
     */
    private ReportStockRule resolveReportStockRule(ProductProcessRouteItem currentRouteItem,
                                                   ProductProcess currentProcess,
                                                   List<ProductProcessRouteItem> routeItems) {
        boolean isRouteLastProcess = currentRouteItem.getDragSort() != null
                && CollectionUtils.isNotEmpty(routeItems)
                && currentRouteItem.getDragSort().equals(routeItems.size());
        String currentProcessName = normalizeProcessName(currentProcess == null ? null : currentProcess.getName());
        if (PROCESS_VOLTAGE_SORT.equals(currentProcessName)) {
            return new ReportStockRule(false, false);
        }
        Map<Long, String> processNameMap = loadRouteProcessNameMap(routeItems);
        boolean hasVoltageSort = containsProcess(routeItems, processNameMap, PROCESS_VOLTAGE_SORT);
        boolean hasOpticalInspection = containsProcess(routeItems, processNameMap, PROCESS_OPTICAL_INSPECTION);
        boolean hasPackaging = containsProcess(routeItems, processNameMap, PROCESS_PACKAGING);
        if (hasPackaging && PROCESS_PACKAGING.equals(currentProcessName)) {
            return new ReportStockRule(true, true);
        }
        if (hasPackaging && PROCESS_OPTICAL_INSPECTION.equals(currentProcessName)) {
            return new ReportStockRule(false, false);
        }
        if (!hasPackaging && hasVoltageSort && hasOpticalInspection && PROCESS_OPTICAL_INSPECTION.equals(currentProcessName)) {
            return new ReportStockRule(true, true);
        }
        return new ReportStockRule(true, isRouteLastProcess);
    }
    /**
     * æ‰¹é‡åŠ è½½å·¥è‰ºè·¯çº¿ä¸­çš„å·¥åºåç§°ã€‚
     */
    private Map<Long, String> loadRouteProcessNameMap(List<ProductProcessRouteItem> routeItems) {
        if (CollectionUtils.isEmpty(routeItems)) {
            return Collections.emptyMap();
        }
        Set<Long> processIds = routeItems.stream()
                .map(ProductProcessRouteItem::getProcessId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (processIds.isEmpty()) {
            return Collections.emptyMap();
        }
        return productProcessMapper.selectBatchIds(processIds).stream()
                .collect(Collectors.toMap(ProductProcess::getId, process -> normalizeProcessName(process.getName())));
    }
    /**
     * åˆ¤æ–­å·¥è‰ºè·¯çº¿ä¸­æ˜¯å¦åŒ…含指定工序。
     */
    private boolean containsProcess(List<ProductProcessRouteItem> routeItems, Map<Long, String> processNameMap, String processName) {
        return routeItems.stream()
                .map(ProductProcessRouteItem::getProcessId)
                .map(processNameMap::get)
                .anyMatch(processName::equals);
    }
    /**
     * ç»Ÿä¸€å·¥åºåç§°æ ¼å¼ï¼Œé¿å…è§„则匹配时受空白字符影响。
     */
    private String normalizeProcessName(String processName) {
        return processName == null ? "" : processName.trim();
    }
    /**
     * å•次报工对应的入库规则。
     */
    private static final class ReportStockRule {
        private final boolean createStockIn;
        private final boolean finishedGoodsStockIn;
        private ReportStockRule(boolean createStockIn, boolean finishedGoodsStockIn) {
            this.createStockIn = createStockIn;
            this.finishedGoodsStockIn = finishedGoodsStockIn;
        }
        private boolean shouldCreateStockIn() {
            return createStockIn;
        }
        private boolean isFinishedGoodsStockIn() {
            return finishedGoodsStockIn;
        }
    }
    @Override
    public Boolean removeProductMain(Long id) {
        //判断该条报工是否不合格处理,如果不合格处理了,则不允许删除
        List<QualityInspect> qualityInspects = qualityInspectMapper.selectList(Wrappers.<QualityInspect>lambdaQuery().eq(QualityInspect::getProductMainId, id));
        List<QualityInspect> qualityInspects = qualityInspectMapper.selectList(
                Wrappers.<QualityInspect>lambdaQuery().eq(QualityInspect::getProductMainId, id)
        );
        if (qualityInspects.size() > 0){
            List<QualityUnqualified> qualityUnqualifieds = qualityUnqualifiedMapper.selectList(Wrappers.<QualityUnqualified>lambdaQuery()
                    .in(QualityUnqualified::getInspectId, qualityInspects.stream().map(QualityInspect::getId).collect(Collectors.toList())));
            List<QualityUnqualified> qualityUnqualifieds = qualityUnqualifiedMapper.selectList(
                    Wrappers.<QualityUnqualified>lambdaQuery()
                            .in(QualityUnqualified::getInspectId, qualityInspects.stream().map(QualityInspect::getId).collect(Collectors.toList()))
            );
            if (qualityUnqualifieds.size() > 0 && qualityUnqualifieds.get(0).getInspectState()==1) {
                throw new ServiceException("该条报工已经不合格处理了,不允许删除");
                throw new ServiceException("该报工已完成不合格处理,不能删除");
            }
        }
        ProductionProductMain productionProductMain = productionProductMainMapper.selectById(id);
        //该报工对应的工艺路线详情
        ProductProcessRouteItem productProcessRouteItem = productProcessRouteItemMapper.selectById(productionProductMain.getProductProcessRouteItemId());
        ProductionProductOutput productionProductOutput = productionProductOutputMapper.selectList(Wrappers.<ProductionProductOutput>lambdaQuery().eq(ProductionProductOutput::getProductMainId, productionProductMain.getId())).get(0);
        /*删除核算*/
        ProductionProductOutput productionProductOutput = productionProductOutputMapper.selectList(
                Wrappers.<ProductionProductOutput>lambdaQuery().eq(ProductionProductOutput::getProductMainId, productionProductMain.getId())
        ).get(0);
        salesLedgerProductionAccountingMapper.delete(
                new LambdaQueryWrapper<SalesLedgerProductionAccounting>()
                        .eq(SalesLedgerProductionAccounting::getProductMainId, productionProductMain.getId())
        );
        /*更新工单和生产订单*/
        ProductWorkOrder productWorkOrder = productWorkOrderMapper.selectById(productionProductMain.getWorkOrderId());
        if (productWorkOrder != null && productionProductOutput != null) {
            BigDecimal outputQty = productionProductOutput.getQuantity() == null ? BigDecimal.ZERO : productionProductOutput.getQuantity();
@@ -368,16 +477,19 @@
            BigDecimal completeQty = productWorkOrder.getCompleteQuantity() == null ? BigDecimal.ZERO : productWorkOrder.getCompleteQuantity();
            BigDecimal validQuantity = outputQty.subtract(scrapQty);
            productWorkOrder.setCompleteQuantity(completeQty.subtract(validQuantity));
            productWorkOrder.setActualEndTime(null);
            productWorkOrderMapper.updateById(productWorkOrder);
        } else {
            throw new ServiceException("操作失败:工单信息或产出记录不存在");
        }
        //判断是否是最后一道工序
        List<ProductProcessRouteItem> productProcessRouteItems = productProcessRouteItemMapper.selectList(Wrappers.<ProductProcessRouteItem>lambdaQuery().eq(ProductProcessRouteItem::getProductRouteId, productProcessRouteItem.getProductRouteId()));
        if (productProcessRouteItem.getDragSort() != null && productProcessRouteItems != null && productProcessRouteItem.getDragSort() == productProcessRouteItems.size()) {
        List<ProductProcessRouteItem> productProcessRouteItems = productProcessRouteItemMapper.selectList(
                Wrappers.<ProductProcessRouteItem>lambdaQuery().eq(ProductProcessRouteItem::getProductRouteId, productProcessRouteItem.getProductRouteId())
        );
        if (productProcessRouteItem.getDragSort() != null
                && productProcessRouteItems != null
                && productProcessRouteItem.getDragSort() == productProcessRouteItems.size()) {
            ProductOrder productOrder = productOrderMapper.selectById(productWorkOrder.getProductOrderId());
            if (productOrder != null) {
                BigDecimal orderCompleteQty = productOrder.getCompleteQuantity() == null ? BigDecimal.ZERO : productOrder.getCompleteQuantity();
@@ -392,31 +504,30 @@
                throw new ServiceException("关联的生产订单不存在");
            }
        }
        //删除质检
        qualityInspectMapper.selectList(
                new LambdaQueryWrapper<QualityInspect>()
                        .eq(QualityInspect::getProductMainId, productionProductMain.getId())
        ).forEach(q -> {
            qualityInspectParamMapper.delete(
                    new LambdaQueryWrapper<QualityInspectParam>()
                            .eq(QualityInspectParam::getInspectId, q.getId()));
                            .eq(QualityInspectParam::getInspectId, q.getId())
            );
            qualityInspectMapper.deleteById(q.getId());
                stockUtils.deleteStockInRecord(q.getId(), StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode());
        });
        // åˆ é™¤äº§å‡ºè®°å½•
        productionProductOutputMapper.delete(new LambdaQueryWrapper<ProductionProductOutput>()
                .eq(ProductionProductOutput::getProductMainId, productionProductMain.getId()));
        //删除投入记录
        productionProductInputMapper.delete(new LambdaQueryWrapper<ProductionProductInput>()
                .eq(ProductionProductInput::getProductMainId, productionProductMain.getId()));
        //删除报废的入库记录
        productionProductOutputMapper.delete(
                new LambdaQueryWrapper<ProductionProductOutput>()
                        .eq(ProductionProductOutput::getProductMainId, productionProductMain.getId())
        );
        productionProductInputMapper.delete(
                new LambdaQueryWrapper<ProductionProductInput>()
                        .eq(ProductionProductInput::getProductMainId, productionProductMain.getId())
        );
        stockUtils.deleteStockInRecord(productionProductMain.getId(), StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode());
        //删除不需要质检的合格入库
        stockUtils.deleteStockInRecord(productionProductMain.getId(), StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode());
        //删除投入对应的出库记录
        stockUtils.deleteStockOutRecord(productionProductMain.getId(), StockOutQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_OUT.getCode());
        // åˆ é™¤ä¸»è¡¨
        productionProductMainMapper.deleteById(productionProductMain.getId());
        return true;
    }
@@ -429,49 +540,39 @@
    @Override
    public List<ProductionProductMainDto> getByProductWorkOrderId(Long productWorkOrderId) {
        List<ProductionProductMainDto> productionProductMainDtos = productionProductMainMapper.getByProductWorkOrderId(productWorkOrderId);
        if (productionProductMainDtos == null || productionProductMainDtos.isEmpty()) {
            return productionProductMainDtos;
        }
        
        // æ”¶é›†æ‰€æœ‰äº§å“ä¸»è®°å½•ID
        List<Long> productMainIds = productionProductMainDtos.stream()
                .map(ProductionProductMainDto::getId)
                .collect(Collectors.toList());
        
        // æ‰¹é‡æŸ¥è¯¢æ‰€æœ‰ç›¸å…³çš„质检记录
        List<QualityInspect> qualityInspects = qualityInspectMapper.selectList(
                Wrappers.<QualityInspect>lambdaQuery()
                        .in(QualityInspect::getProductMainId, productMainIds)
        );
        
        if (!qualityInspects.isEmpty()) {
            // æ”¶é›†æ‰€æœ‰è´¨æ£€è®°å½•ID
            List<Long> inspectIds = qualityInspects.stream()
                    .map(QualityInspect::getId)
                    .collect(Collectors.toList());
            
            // æ‰¹é‡æŸ¥è¯¢æ‰€æœ‰ç›¸å…³çš„不合格处理记录
            List<QualityUnqualified> qualityUnqualifieds = qualityUnqualifiedMapper.selectList(
                    Wrappers.<QualityUnqualified>lambdaQuery()
                            .in(QualityUnqualified::getInspectId, inspectIds)
            );
            
            // æž„建质检ID到不合格处理记录的映射
            Map<Long, QualityUnqualified> inspectIdToUnqualifiedMap = qualityUnqualifieds.stream()
                    .collect(Collectors.toMap(QualityUnqualified::getInspectId, q -> q, (q1, q2) -> q1));
            
            // æž„建产品主ID到质检记录的映射
            Map<Long, QualityInspect> productMainIdToInspectMap = qualityInspects.stream()
                    .collect(Collectors.toMap(QualityInspect::getProductMainId, q -> q, (q1, q2) -> q1));
            
            // å…³è”处理结果到产品主记录
            productionProductMainDtos.forEach(p -> {
                QualityInspect qualityInspect = productMainIdToInspectMap.get(p.getId());
                if (qualityInspect != null) {
                    // ä¸è‰¯æ•°é‡
                    p.setDefectiveQuantity( qualityInspect.getDefectiveQuantity());
                    // åˆæ ¼æ•°é‡ = æŠ¥å·¥æ•°é‡-报废数量-不良数量
                    p.setQualifiedQty(p.getQuantity().subtract(p.getScrapQty()).subtract(p.getDefectiveQuantity()));
                    QualityUnqualified qualityUnqualified = inspectIdToUnqualifiedMap.get(qualityInspect.getId());
                    if (qualityUnqualified != null) {
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -10,6 +10,7 @@
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.HackLoopTableRenderPolicy;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.procurementrecord.service.ProcurementRecordService;
@@ -33,7 +34,7 @@
import com.ruoyi.quality.service.IQualityInspectParamService;
import com.ruoyi.quality.service.IQualityInspectService;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.stock.pojo.StockUninventory;
import com.ruoyi.stock.support.FinishedProductStockDimensionResolver;
import lombok.AllArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -78,6 +79,9 @@
    private ProductOrderMapper productOrderMapper;
    @Autowired
    private ISysNoticeService sysNoticeService;
    // è§£æžæˆå“å…¥åº“维度。
    @Autowired
    private FinishedProductStockDimensionResolver finishedProductStockDimensionResolver;
    @Override
    public int add(QualityInspectDto qualityInspectDto) {
@@ -158,14 +162,28 @@
        } else {
            //合格入库
            Integer inspectType = qualityInspect.getInspectType();
            //生产成品添加审核流程,半成品不审核
            if (inspectType != null && inspectType.equals(2)) {
                stockUtils.addStock(qualityInspect.getProductModelId(), qualityInspect.getQuantity(),
                        StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId());
            boolean isFinishedProduct = inspectType != null && inspectType.equals(2); // æˆå“éœ€è¦å®¡æ ¸
            // æ ¹æ®æ˜¯å¦æˆå“é€‰æ‹©å…¥åº“方式
            String processCategory = null;
            String voltage = null;
            if (isFinishedProduct && qualityInspect.getProductMainId() != null) {
                processCategory = finishedProductStockDimensionResolver.resolveProcessCategory(qualityInspect.getProductMainId());
                voltage = finishedProductStockDimensionResolver.resolveVoltage(qualityInspect.getProductMainId());
            }
            if (isFinishedProduct) {
                stockUtils.addStock(qualityInspect.getProductModelId(), productQty,
                        StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId(),
                        processCategory, voltage);
            } else {
                stockUtils.addStockNoReview(qualityInspect.getProductModelId(), qualityInspect.getQuantity(),
                stockUtils.addStockNoReview(qualityInspect.getProductModelId(), productQty,
                        StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId());
            }
            // ä¸è‰¯æ•°é‡>0的入不合格库存
            if (qualityInspect.getDefectiveQuantity().compareTo(BigDecimal.ZERO) > 0) {
                stockUtils.addUnStockNoReview(qualityInspect.getProductModelId(), qualityInspect.getDefectiveQuantity(),
                        StockInUnQualifiedRecordTypeEnum.QUALITYINSPECT_UNSTOCK_IN.getCode(), qualityInspect.getId());
            }
        }
        qualityInspect.setInspectState(1);//已提交
        return qualityInspectMapper.updateById(qualityInspect);
src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedServiceImpl.java
@@ -26,6 +26,7 @@
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.quality.service.IQualityInspectService;
import com.ruoyi.quality.service.IQualityUnqualifiedService;
import com.ruoyi.stock.support.FinishedProductStockDimensionResolver;
import com.ruoyi.stock.service.StockUninventoryService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
@@ -35,7 +36,6 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
@AllArgsConstructor
@@ -51,6 +51,10 @@
    private ProductProcessRouteItemMapper productProcessRouteItemMapper;
    private ProductWorkOrderMapper productWorkOrderMapper;
    private StockUninventoryService stockUninventoryService;
    /**
     * è§£æžæˆå“å›žæµå…¥åº“维度。
     */
    private FinishedProductStockDimensionResolver finishedProductStockDimensionResolver;
    @Override
    public IPage<QualityUnqualified> qualityUnqualifiedListPage(Page page, QualityUnqualified qualityUnqualified) {
@@ -143,10 +147,12 @@
                case "让步放行":
                    //调用提交合格的接口
                    // 2 æ˜¯æˆå“-需要审核 1 æ˜¯åŠæˆå“-不需要审核
                    if (qualityInspect.getInspectType().equals(2)) {
                    if (qualityInspect.getInspectType().equals(1)) {
                        stockUtils.addStockNoReview(qualityInspect.getProductModelId(), unqualified.getQuantity(), StockInQualifiedRecordTypeEnum.DEFECTIVE_PASS.getCode(), unqualified.getId());
                    } else {
                        stockUtils.addStock(qualityInspect.getProductModelId(), unqualified.getQuantity(), StockInQualifiedRecordTypeEnum.DEFECTIVE_PASS.getCode(), unqualified.getId());
                        String processCategory = finishedProductStockDimensionResolver.resolveProcessCategory(qualityInspect.getProductMainId());
                        String voltage = finishedProductStockDimensionResolver.resolveVoltage(qualityInspect.getProductMainId());
                        stockUtils.addStock(qualityInspect.getProductModelId(), unqualified.getQuantity(), StockInQualifiedRecordTypeEnum.DEFECTIVE_PASS.getCode(), unqualified.getId(), processCategory, voltage);
                    }
                    break;
                case "隔离":
@@ -196,7 +202,9 @@
        // å–消隔离 è°ƒç”¨æäº¤åˆæ ¼çš„æŽ¥å£
        QualityInspect qualityInspect = qualityInspectService.getById(unqualified.getInspectId());
        if (ObjectUtils.isNotNull(qualityInspect) && qualityInspect.getInspectType().equals(2)) {
            stockUtils.addStock(modelId, unqualified.getQuantity(), StockInQualifiedRecordTypeEnum.LIFT_THE_QUARANTINE.getCode(), unqualified.getId());
            String processCategory = finishedProductStockDimensionResolver.resolveProcessCategory(qualityInspect.getProductMainId());
            String voltage = finishedProductStockDimensionResolver.resolveVoltage(qualityInspect.getProductMainId());
            stockUtils.addStock(modelId, unqualified.getQuantity(), StockInQualifiedRecordTypeEnum.LIFT_THE_QUARANTINE.getCode(), unqualified.getId(), processCategory, voltage);
        } else {
            stockUtils.addStockNoReview(modelId, unqualified.getQuantity(), StockInQualifiedRecordTypeEnum.LIFT_THE_QUARANTINE.getCode(), unqualified.getId());
        }
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
@@ -155,13 +155,13 @@
    @PostMapping("/export")
    public void export(HttpServletResponse response, SalesLedgerDto salesLedgerDto) {
        Page page = new Page(-1,-1);
        IPage<SalesLedger> salesLedgerIPage = listPage(page, salesLedgerDto);
        ExcelUtil<SalesLedger> util = new ExcelUtil<SalesLedger>(SalesLedger.class);
        IPage<SalesLedgerDto> salesLedgerIPage = listPage(page, salesLedgerDto);
        ExcelUtil<SalesLedgerDto> util = new ExcelUtil<SalesLedgerDto>(SalesLedgerDto.class);
        if(salesLedgerIPage == null){
            util.exportExcel(response, new ArrayList<>(), "销售台账数据");
            return;
        }
        List<SalesLedger> list = salesLedgerIPage.getRecords();
        List<SalesLedgerDto> list = salesLedgerIPage.getRecords();
        util.exportExcel(response, list, "销售台账数据");
    }
@@ -174,8 +174,8 @@
        Page page = new Page();
        page.setCurrent(-1);
        page.setSize(-1);
        IPage<SalesLedger> salesLedgerIPage = listPage(page, salesLedgerDto);
        ExcelUtil<SalesLedger> util = new ExcelUtil<SalesLedger>(SalesLedger.class);
        IPage<SalesLedgerDto> salesLedgerIPage = listPage(page, salesLedgerDto);
        ExcelUtil<SalesLedgerDto> util = new ExcelUtil<SalesLedgerDto>(SalesLedgerDto.class);
        util.exportExcel(response, salesLedgerIPage == null ? new ArrayList<>() : salesLedgerIPage.getRecords(), "导出开票登记列表");
    }
@@ -270,8 +270,8 @@
     * æŸ¥è¯¢é”€å”®å°è´¦åˆ—表
     */
    @GetMapping("/listPage")
    public IPage<SalesLedger> listPage(Page page, SalesLedgerDto salesLedgerDto) {
        IPage<SalesLedger> iPage = salesLedgerService.selectSalesLedgerListPage(page, salesLedgerDto);
    public IPage<SalesLedgerDto> listPage(Page page, SalesLedgerDto salesLedgerDto) {
        IPage<SalesLedgerDto> iPage = salesLedgerService.selectSalesLedgerListPage(page, salesLedgerDto);
        //  æŸ¥è¯¢ç»“果为空,直接返回
        if (CollectionUtils.isEmpty(iPage.getRecords())) {
src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java
@@ -1,37 +1,23 @@
package com.ruoyi.sales.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
import java.util.Date;
import java.util.List;
@Data
public class SalesLedgerDto {
    private Long id;
    private String salesContractNo;
    private String customerContractNo;
    private String projectName;
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date entryDate;
@EqualsAndHashCode(callSuper = true)
public class SalesLedgerDto extends SalesLedger {
    private String entryDateStart;
    private String entryDateEnd;
    private String salesman;
    private Long customerId;
    private String customerName;
    private String entryPerson;
    private String remarks;
    private String attachmentMaterials;
    @TableField(exist = false)
    @JsonFormat(pattern = "yyyy-MM-dd")
@@ -39,9 +25,9 @@
    @TableField(exist = false)
    private String invoiceNo;
    @TableField(exist = false)
    private String createUser;
    private Boolean hasChildren = false;
    private List<SalesLedgerProduct> productData;
@@ -49,18 +35,17 @@
    private List<CommonFile> SalesLedgerFiles;
    private Integer type;
    @ApiModelProperty(value = "签订日期")
    private LocalDate executionDate;
    private Boolean status;
    @ApiModelProperty(value = "付款方式")
    private String paymentMethod;
    @TableField(exist = false)
    @ApiModelProperty(value = "型号")
    private String model;
    @ApiModelProperty(value = "交货日期")
    private LocalDate deliveryDate;
    @TableField(exist = false)
    @ApiModelProperty(value = "电压")
    private String voltage;
    @ApiModelProperty(value = "是否生产")
    @JsonAlias("isProduce")
    private Boolean produce;
    @TableField(exist = false)
    @ApiModelProperty(value = "c")
    private String qty;
}
src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java
@@ -52,7 +52,7 @@
     * @param salesLedgerDto
     * @return
     */
    IPage<SalesLedger> selectSalesLedgerListPage(Page page, @Param("salesLedgerDto") SalesLedgerDto salesLedgerDto);
    IPage<SalesLedgerDto> selectSalesLedgerListPage(Page page, @Param("salesLedgerDto") SalesLedgerDto salesLedgerDto);
    /**
     * æŒ‰æœˆä»½ç»Ÿè®¡è®¢å•数、销售额(支持产品大类、客户名称筛选)
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java
@@ -1,6 +1,7 @@
package com.ruoyi.sales.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModelProperty;
@@ -180,6 +181,7 @@
     * æ˜¯å¦ç”Ÿäº§
     */
    @ApiModelProperty(value = "是否生产")
    @JsonAlias("isProduce")
    @TableField(value = "is_produce")
    private Boolean produce;
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -606,7 +606,7 @@
    }
    @Override
    public IPage<SalesLedger> selectSalesLedgerListPage(Page page, SalesLedgerDto salesLedgerDto) {
    public IPage<SalesLedgerDto> selectSalesLedgerListPage(Page page, SalesLedgerDto salesLedgerDto) {
        return salesLedgerMapper.selectSalesLedgerListPage(page, salesLedgerDto);
    }
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java
@@ -8,6 +8,7 @@
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.stock.dto.FinishedProductTreeDto;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.execl.StockInventoryExportData;
import com.ruoyi.stock.service.StockInventoryService;
@@ -47,6 +48,13 @@
        return R.ok(stockInventoryDtoIPage);
    }
    @GetMapping("/finishedProductList")
    @ApiOperation("查询成品库存树")
    public R finishedProductList(StockInventoryDto stockInventoryDto) {
        List<FinishedProductTreeDto> list = stockInventoryService.finishedProductList(stockInventoryDto);
        return R.ok(list);
    }
    @GetMapping("/pageListCombinedStockInventory")
    @Operation(summary = "分页查询联合库存列表")
src/main/java/com/ruoyi/stock/dto/FinishedProductTreeDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.stock.dto;
import lombok.Data;
import java.util.List;
@Data
public class FinishedProductTreeDto {
    private Long id;
    private Long parentId;
    private String productName;
    private String label;
    private Long productId;
    private Long productModelId;
    private String model;
    private String materialCode;
    private String processCategory;
    private String voltage;
    private List<FinishedProductTreeDto> children;
}
src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java
@@ -3,40 +3,39 @@
import com.ruoyi.stock.pojo.StockInRecord;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class StockInRecordDto extends StockInRecord {
    /**
     * äº§å“åç§°
     */
    @Schema(description = "产品名称")
    private String productName;
    /**
     * äº§å“è§„æ ¼
     */
    @Schema(description = "产品型号")
    private String model;
    /**
     * äº§å“æ–™å·
     */
    @Schema(description = "物料编码")
    private String materialCode;
    /**
     * äº§å“å•位
     */
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "时间字符串")
    private String timeStr;
    @Schema(description = "创建人")
    private String createBy;
    //现存量
    @Schema(description = "当前库存")
    private String currentStock;
    //订单
    @Schema(description = "订单编号")
    private String npsNo;
    //工单
    @Schema(description = "工单编号")
    private String workOrderNo;
    // é¡¶éƒ¨çˆ¶äº§å“id
    @Schema(description = "顶级父级产品ID")
    private Long topParentProductId;
}
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java
@@ -17,21 +17,17 @@
    private String unit;
    private String materialCode;
    private String productModelName;
    //入库类型
    private String recordType;
    //入库类型对应的id
    private Long recordId;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate reportDate;
    //库存月报查询字段
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate startMonth;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate endMonth;
@@ -39,16 +35,13 @@
    private BigDecimal totalStockIn;
    private BigDecimal totalStockOut;
    private BigDecimal currentStock;
    private BigDecimal  unLockedQuantity;
    //产品id
    private Long productId;
    @Schema(description = "顶部父产品id")
    @Schema(description = "顶级父级产品ID")
    private Long topParentProductId;
    @Schema(description = "库存类型:qualified(合格)、unqualified(不合格)")
    @Schema(description = "库存类型")
    private String stockType;
    @Schema(description = "合格库存数量")
@@ -57,16 +50,16 @@
    @Schema(description = "不合格库存数量")
    private BigDecimal unQualifiedQuantity;
    @Schema(description = "合格库存冻结数量")
    @Schema(description = "合格冻结数量")
    private BigDecimal qualifiedLockedQuantity;
    @Schema(description = "不合格库存冻结数量")
    @Schema(description = "不合格冻结数量")
    private BigDecimal unQualifiedLockedQuantity;
    @Schema(description = "合格库存未冻结数量")
    @Schema(description = "合格可用数量")
    private BigDecimal qualifiedUnLockedQuantity;
    @Schema(description = "不合格库存未冻结数量")
    @Schema(description = "不合格可用数量")
    private BigDecimal unQualifiedUnLockedQuantity;
    @Schema(description = "合格库存ID")
@@ -75,9 +68,10 @@
    @Schema(description = "不合格库存ID")
    private Long unQualifiedId;
    @Schema(description = "合格库存批号")
    @Schema(description = "合格批次号")
    private String qualifiedBatchNo;
    @Schema(description = "不合格库存批号")
    @Schema(description = "不合格批次号")
    private String unQualifiedBatchNo;
}
src/main/java/com/ruoyi/stock/execl/StockInRecordExportData.java
@@ -12,19 +12,32 @@
    @Excel(name = "入库批次")
    private String inboundBatches;
    @Excel(name = "产品名称")
    private String productName;
    @Excel(name = "规格型号")
    @Excel(name = "型号")
    private String model;
    @Excel(name = "料号")
    @Excel(name = "物料编码")
    private String materialCode;
    @Excel(name = "单位")
    private String unit;
    @Excel(name = "入库来源")
    @Excel(name = "成品类别")
    private String processCategory;
    @Excel(name = "电压")
    private String voltage;
    @Excel(name = "记录类型")
    private String recordType;
    @Excel(name = "入库数量")
    private String stockInNum;
    @Excel(name = "入库时间")
    @Excel(name = "创建时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
src/main/java/com/ruoyi/stock/execl/StockInventoryExportData.java
@@ -10,14 +10,20 @@
    @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 = "成品类别")
    private String processCategory;
    @Excel(name = "电压")
    private String voltage;
    @Excel(name = "合格库存批号")
    private String qualifiedBatchNo;
@@ -42,11 +48,4 @@
    @Excel(name = "备注")
    private String remark;
//
//    @Excel(name = "最新更新时间")
//    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
//    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
//    private LocalDateTime updateTime;
}
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java
@@ -52,4 +52,6 @@
    BigDecimal selectTotalByDate(@Param("now") LocalDate now);
    List<StockInventoryDto> selectProductList();
    List<StockInventoryDto> selectFinishedProductInventoryList(@Param("ew") StockInventoryDto stockInventoryDto);
}
src/main/java/com/ruoyi/stock/pojo/StockInRecord.java
@@ -78,4 +78,10 @@
    @ApiModelProperty("批号")
    private String batchNo;
    @ApiModelProperty("工艺类别")
    private String processCategory;
    @ApiModelProperty("电压")
    private String voltage;
}
src/main/java/com/ruoyi/stock/pojo/StockInventory.java
@@ -64,6 +64,12 @@
    @ApiModelProperty("备注")
    private String remark;
    @ApiModelProperty("工艺类别")
    private String processCategory;
    @ApiModelProperty("电压")
    private String voltage;
    @ApiModelProperty("批号")
    private String batchNo;
}
src/main/java/com/ruoyi/stock/service/StockInventoryService.java
@@ -5,6 +5,7 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.stock.dto.FinishedProductTreeDto;
import com.ruoyi.stock.dto.StockInRecordDto;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.pojo.StockInRecord;
@@ -12,6 +13,7 @@
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
@@ -25,6 +27,8 @@
    IPage<StockInventoryDto> pagestockInventory(Page page, StockInventoryDto stockInventoryDto);
    List<FinishedProductTreeDto> finishedProductList(StockInventoryDto stockInventoryDto);
    IPage<StockInventoryDto> pageListCombinedStockInventory(Page page, StockInventoryDto stockInventoryDto);
    Boolean addstockInventory(StockInventoryDto stockInventoryDto);
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
@@ -10,12 +10,11 @@
import com.ruoyi.common.utils.EnumUtil;
import com.ruoyi.common.utils.OrderUtils;
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.stock.dto.StockInRecordDto;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
import com.ruoyi.stock.execl.StockInRecordExportData;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockInventoryMapper;
@@ -30,6 +29,8 @@
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Service
@@ -49,7 +50,6 @@
        return stockInRecordMapper.listPage(page, stockInRecordDto);
    }
    // æ–°å¢žå…¥åº“
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long add(StockInRecordDto stockInRecordDto) {
@@ -57,18 +57,14 @@
        stockInRecordDto.setInboundBatches(no);
        StockInRecord stockInRecord = new StockInRecord();
        BeanUtils.copyProperties(stockInRecordDto, stockInRecord);
        int insertRows = stockInRecordMapper.insert(stockInRecord);
        Long insertId = stockInRecord.getId();
        return insertRows > 0 ? insertId : null;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int update(Long id, StockInRecordDto stockInRecordDto) {
        // åˆ¤æ–­å¯¹è±¡æ˜¯å¦å­˜åœ¨
        StockInRecord stockInRecord = stockInRecordMapper.selectById(id);
        if (stockInRecord == null) {
            throw new BaseException("该入库记录不存在,无法更新!!!");
@@ -84,33 +80,28 @@
    public int batchDelete(List<Long> ids) {
        for (Long id : ids) {
            StockInRecord stockInRecord = stockInRecordMapper.selectById(id);
            if (stockInRecord.getType().equals("0")) {
                StockInventory stockInventory = stockInventoryMapper.selectOne(
                        new LambdaQueryWrapper<StockInventory>()
                                .eq(StockInventory::getProductModelId, stockInRecord.getProductModelId())
                                .eq(StockInventory::getBatchNo, stockInRecord.getBatchNo())
                );
            if ("0".equals(stockInRecord.getType())) {
                StockInventory stockInventory = findQualifiedInventory(stockInRecord);
                if (stockInventory == null) {
                    throw new BaseException("库存记录中没有对应的产品,无法删除!!!");
                } else {
                    StockInventoryDto stockInRecordDto = new StockInventoryDto();
                    stockInRecordDto.setProductModelId(stockInventory.getProductModelId());
                    stockInRecordDto.setQualitity(stockInRecord.getStockInNum());
                    stockInventoryMapper.updateSubtractStockInventory(stockInRecordDto);
                    throw new BaseException("库存记录中没有对应的产品,无法删除!!!");
                }
            } else if (stockInRecord.getType().equals("1")) {
                stockInventory.setQualitity(defaultDecimal(stockInventory.getQualitity()).subtract(defaultDecimal(stockInRecord.getStockInNum())));
                stockInventory.setVersion(stockInventory.getVersion() == null ? 1 : stockInventory.getVersion() + 1);
                stockInventory.setUpdateTime(LocalDateTime.now());
                stockInventoryMapper.updateById(stockInventory);
            } else if ("1".equals(stockInRecord.getType())) {
                StockUninventory stockUninventory = stockUninventoryMapper.selectOne(
                        new LambdaQueryWrapper<StockUninventory>()
                                .eq(StockUninventory::getProductModelId, stockInRecord.getProductModelId())
                                .eq(StockUninventory::getBatchNo, stockInRecord.getBatchNo()));
                                .eq(StockUninventory::getBatchNo, stockInRecord.getBatchNo())
                );
                if (stockUninventory == null) {
                    throw new BaseException("库存记录中没有对应的产品,无法删除!!!");
                } else {
                    StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
                    stockUninventoryDto.setProductModelId(stockUninventory.getProductModelId());
                    stockUninventoryDto.setQualitity(stockInRecord.getStockInNum());
                    stockUninventoryMapper.updateSubtractStockUnInventory(stockUninventoryDto);
                    throw new BaseException("库存记录中没有对应的产品,无法删除!!!");
                }
                stockUninventory.setQualitity(defaultDecimal(stockUninventory.getQualitity()).subtract(defaultDecimal(stockInRecord.getStockInNum())));
                stockUninventory.setVersion(stockUninventory.getVersion() == null ? 1 : stockUninventory.getVersion() + 1);
                stockUninventory.setUpdateTime(LocalDateTime.now());
                stockUninventoryMapper.updateById(stockUninventory);
            }
        }
        return stockInRecordMapper.deleteBatchIds(ids);
@@ -120,10 +111,14 @@
    public void exportStockInRecord(HttpServletResponse response, StockInRecordDto stockInRecordDto) {
        List<StockInRecordExportData> list = stockInRecordMapper.listStockInRecordExportData(stockInRecordDto);
        for (StockInRecordExportData stockInRecordExportData : list) {
            if (stockInRecordExportData.getType().equals("0")) {
                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockInQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
            if ("0".equals(stockInRecordExportData.getType())) {
                stockInRecordExportData.setRecordType(
                        EnumUtil.fromCode(StockInQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue()
                );
            } else {
                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockInUnQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
                stockInRecordExportData.setRecordType(
                        EnumUtil.fromCode(StockInUnQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue()
                );
            }
        }
        ExcelUtil<StockInRecordExportData> util = new ExcelUtil<>(StockInRecordExportData.class);
@@ -146,4 +141,23 @@
        stockInRecordDto.setApproveStatus(1);
        return stockInRecordMapper.updateById(stockInRecordDto);
    }
    // åˆæ ¼å…¥åº“回退按与库存合并一致的唯一键查找。
    private StockInventory findQualifiedInventory(StockInRecord stockInRecord) {
        LambdaQueryWrapper<StockInventory> queryWrapper = new LambdaQueryWrapper<StockInventory>()
                .eq(StockInventory::getProductModelId, stockInRecord.getProductModelId())
                .orderByAsc(StockInventory::getId);
        String processCategory = StringUtils.trimToEmpty(stockInRecord.getProcessCategory());
        String voltage = StringUtils.trimToEmpty(stockInRecord.getVoltage());
        if (StringUtils.isNotBlank(processCategory) || StringUtils.isNotBlank(voltage)) {
            queryWrapper.eq(StockInventory::getProcessCategory, processCategory);
            queryWrapper.eq(StockInventory::getVoltage, voltage);
        }
        List<StockInventory> inventories = stockInventoryMapper.selectList(queryWrapper);
        return inventories.isEmpty() ? null : inventories.get(0);
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
}
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -2,7 +2,6 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -16,9 +15,12 @@
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
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;
@@ -43,18 +45,15 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
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.regex.Matcher;
import java.util.regex.Pattern;
/**
 * <p>
 * åº“存表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-01-21 04:16:36
 */
@Service
public class StockInventoryServiceImpl extends ServiceImpl<StockInventoryMapper, StockInventory> implements StockInventoryService {
@@ -80,17 +79,54 @@
        return stockInventoryMapper.pagestockInventory(page, stockInventoryDto);
    }
    /**
     * æŸ¥è¯¢æˆå“åº“存树。
     * è¿”回结构沿用基础资料产品树,叶子节点补充成品库存维度:型号、工序分类、电压。
     */
    @Override
    public List<FinishedProductTreeDto> finishedProductList(StockInventoryDto stockInventoryDto) {
        List<StockInventoryDto> inventoryList = stockInventoryMapper.selectFinishedProductInventoryList(stockInventoryDto);
        if (inventoryList.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);
        }
        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;
    }
    @Override
    public IPage<StockInventoryDto> pageListCombinedStockInventory(Page page, StockInventoryDto stockInventoryDto) {
        return stockInventoryMapper.pageListCombinedStockInventory(page, stockInventoryDto);
    }
    //入库调用-添加入库记录
    /**
     * åˆæ ¼å…¥åº“:先生成入库记录,再走审批流。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean addstockInventory(StockInventoryDto stockInventoryDto) {
        List<StockInventory> stockInventoryList = stockInventoryMapper.selectList(null);
        //新增入库记录再添加库存
        StockInRecordDto stockInRecordDto = new StockInRecordDto();
        stockInRecordDto.setRecordId(stockInventoryDto.getRecordId());
        stockInRecordDto.setRecordType(stockInventoryDto.getRecordType());
@@ -99,22 +135,17 @@
        stockInRecordDto.setRemark(stockInventoryDto.getRemark());
        stockInRecordDto.setWarnNum(stockInventoryDto.getWarnNum());
        stockInRecordDto.setLockedQuantity(stockInventoryDto.getLockedQuantity());
        stockInRecordDto.setProcessCategory(normalizeDimension(stockInventoryDto.getProcessCategory()));
        stockInRecordDto.setVoltage(normalizeDimension(stockInventoryDto.getVoltage()));
        stockInRecordDto.setApproveStatus(0);
        stockInRecordDto.setType("0");
        if (stockInventoryDto.getBatchNo() == null || stockInventoryDto.getBatchNo().isEmpty()) {
        if (StringUtils.isBlank(stockInventoryDto.getBatchNo())) {
            String batchNo;
            // èŽ·å–å½“å‰æœˆä»½ï¼ˆä¸¤ä½ï¼‰
            LocalDate now = LocalDate.now();
            String monthFlag = now.format(DateTimeFormatter.ofPattern("MM"));
            // èŽ·å–å½“å‰æœˆä»½çš„æœ€å¤§æµæ°´å·
            int maxSeq = getCurrentMonthMaxSeq(stockInventoryDto, monthFlag, stockInventoryList);
            // æ–°æµæ°´å· = æœ€å¤§æµæ°´å· + 1
            int newSeq = maxSeq + 1;
            String seqStr = String.format("%03d", newSeq);
            // ç»„装batchNo
            ProductModel productModel = productModelMapper.selectById(stockInventoryDto.getProductModelId());
            batchNo = stockInventoryDto.getMaterialCode() + productModel.getModel() + "P" + monthFlag + seqStr;
            stockInRecordDto.setBatchNo(batchNo);
@@ -133,21 +164,15 @@
        return true;
    }
    /**
     * æŸ¥è¯¢å½“前月份已存在的最大流水号
     */
    private static int getCurrentMonthMaxSeq(StockInventoryDto dto, String monthFlag, List<StockInventory> existingList) {
        int maxSeq = 0;
        String prefix = dto.getMaterialCode() + dto.getProductModelName() + "P" + monthFlag;
        // æ­£åˆ™åŒ¹é…ï¼šå‰ç¼€ + 3位数字
        Pattern pattern = Pattern.compile(Pattern.quote(prefix) + "(\\d{3})");
        for (StockInventory item : existingList) {
            String batchNo = item.getBatchNo();
            if (batchNo == null) continue;
            if (batchNo == null) {
                continue;
            }
            Matcher matcher = pattern.matcher(batchNo);
            if (matcher.find()) {
                int seq = Integer.parseInt(matcher.group(1));
@@ -156,10 +181,10 @@
                }
            }
        }
        return maxSeq;
    }
    @Override
    public void addApproveByPurchase(LoginUser loginUser, StockInRecordDto stockInRecordDto, Long id) throws Exception {
        ApproveProcessVO approveProcessVO = new ApproveProcessVO();
        approveProcessVO.setApproveType(9);
@@ -175,100 +200,77 @@
    }
    /**
     * å®žé™…入库
     *
     * @param stockInRecord
     * æŒ‰åº“存唯一键合并库存。
     * æˆå“ä½¿ç”¨ product_model_id + processCategory + voltage;其他入库只按 product_model_id。
     */
    @Override
    public void updateOrCreateStockInventory(StockInRecord stockInRecord) {
        // å…ˆæŸ¥è¯¢åº“存表中的产品是否存在
        LambdaQueryWrapper<StockInventory> queryWrapper = new QueryWrapper<StockInventory>().lambda()
                .eq(StockInventory::getProductModelId, stockInRecord.getProductModelId());
        // æ ¹æ® batchNo æ˜¯å¦ä¸ºç©ºæž„建不同的查询条件
        if (stockInRecord.getBatchNo() != null && !stockInRecord.getBatchNo().isEmpty()) {
            queryWrapper.eq(StockInventory::getBatchNo, stockInRecord.getBatchNo());
        } else {
            queryWrapper.isNull(StockInventory::getBatchNo);
        }
        StockInventory oldStockInventory = stockInventoryMapper.selectOne(queryWrapper);
        String processCategory = normalizeDimension(stockInRecord.getProcessCategory());
        String voltage = normalizeDimension(stockInRecord.getVoltage());
        StockInventory oldStockInventory = findInventoryForMerge(
                stockInRecord.getProductModelId(),
                processCategory,
                voltage
        );
        if (ObjectUtils.isEmpty(oldStockInventory)) {
            // ä¸å­˜åœ¨åˆ™æ–°å¢ž
            StockInventory newStockInventory = new StockInventory();
            newStockInventory.setProductModelId(stockInRecord.getProductModelId());
            newStockInventory.setQualitity(stockInRecord.getStockInNum());
            newStockInventory.setQualitity(defaultDecimal(stockInRecord.getStockInNum()));
            newStockInventory.setVersion(1);
            newStockInventory.setRemark(stockInRecord.getRemark());
            newStockInventory.setLockedQuantity(stockInRecord.getLockedQuantity());
            newStockInventory.setLockedQuantity(defaultDecimal(stockInRecord.getLockedQuantity()));
            newStockInventory.setWarnNum(stockInRecord.getWarnNum());
            newStockInventory.setBatchNo(stockInRecord.getBatchNo());
            newStockInventory.setProcessCategory(processCategory);
            newStockInventory.setVoltage(voltage);
            newStockInventory.setBatchNo(StringUtils.trimToEmpty(stockInRecord.getBatchNo()));
            stockInventoryMapper.insert(newStockInventory);
        } else {
            // å­˜åœ¨åˆ™æ›´æ–°
            LambdaUpdateWrapper<StockInventory> updateWrapper = new LambdaUpdateWrapper<>();
            updateWrapper
                    .eq(StockInventory::getProductModelId, stockInRecord.getProductModelId());
            // æ ¹æ® batchNo æ˜¯å¦ä¸ºç©ºæž„建不同的更新条件
            if (stockInRecord.getBatchNo() != null && !stockInRecord.getBatchNo().isEmpty()) {
                updateWrapper.eq(StockInventory::getBatchNo, stockInRecord.getBatchNo());
            } else {
                updateWrapper.isNull(StockInventory::getBatchNo);
            return;
            }
            
            updateWrapper
                    .setSql(stockInRecord.getStockInNum() != null,
                            "qualitity = qualitity + " + stockInRecord.getStockInNum())
                    .setSql(true, "version = version + 1")
                    .set(stockInRecord.getRemark() != null && !stockInRecord.getRemark().isEmpty(),
                            StockInventory::getRemark, stockInRecord.getRemark())
                    .set(stockInRecord.getWarnNum() != null,
                            StockInventory::getWarnNum, stockInRecord.getWarnNum())
                    .setSql(stockInRecord.getLockedQuantity() != null,
                            "locked_quantity = locked_quantity + " + stockInRecord.getLockedQuantity())
                    .set(StockInventory::getUpdateTime, new Date());
            stockInventoryMapper.update(null, updateWrapper);
        oldStockInventory.setQualitity(defaultDecimal(oldStockInventory.getQualitity()).add(defaultDecimal(stockInRecord.getStockInNum())));
        oldStockInventory.setVersion(oldStockInventory.getVersion() == null ? 1 : oldStockInventory.getVersion() + 1);
        if (StringUtils.isNotBlank(stockInRecord.getRemark())) {
            oldStockInventory.setRemark(stockInRecord.getRemark());
        }
        if (stockInRecord.getWarnNum() != null) {
            oldStockInventory.setWarnNum(stockInRecord.getWarnNum());
        }
        if (stockInRecord.getLockedQuantity() != null) {
            oldStockInventory.setLockedQuantity(defaultDecimal(oldStockInventory.getLockedQuantity()).add(stockInRecord.getLockedQuantity()));
        }
        oldStockInventory.setProcessCategory(processCategory);
        oldStockInventory.setVoltage(voltage);
        oldStockInventory.setUpdateTime(LocalDateTime.now());
        stockInventoryMapper.updateById(oldStockInventory);
    }
    //半成品直接入库
    /**
     * ä¸å®¡æ ¸å…¥åº“,直接写入入库记录和库存。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean addstockInventoryNoReview(StockInventoryDto stockInventoryDto) {
        //新增入库记录再添加库存
        StockInRecordDto stockInRecordDto = new StockInRecordDto();
        stockInRecordDto.setRecordId(stockInventoryDto.getRecordId());
        stockInRecordDto.setRecordType(stockInventoryDto.getRecordType());
        stockInRecordDto.setStockInNum(stockInventoryDto.getQualitity());
        stockInRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockInRecordDto.setRemark(stockInventoryDto.getRemark());
        stockInRecordDto.setWarnNum(stockInventoryDto.getWarnNum());
        stockInRecordDto.setLockedQuantity(stockInventoryDto.getLockedQuantity());
        stockInRecordDto.setProcessCategory(normalizeDimension(stockInventoryDto.getProcessCategory()));
        stockInRecordDto.setVoltage(normalizeDimension(stockInventoryDto.getVoltage()));
        stockInRecordDto.setBatchNo(stockInventoryDto.getBatchNo());
        stockInRecordDto.setType("0");
        stockInRecordService.add(stockInRecordDto);
        //再进行新增库存数量库存
        //先查询库存表中的产品是否存在,不存在新增,存在更新
        StockInventory oldStockInventory = stockInventoryMapper.selectOne(new QueryWrapper<StockInventory>().lambda().eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId())
                .eq(StockInventory::getBatchNo, stockInventoryDto.getBatchNo()));
        if (ObjectUtils.isEmpty(oldStockInventory)) {
            StockInventory newStockInventory = new StockInventory();
            newStockInventory.setProductModelId(stockInventoryDto.getProductModelId());
            newStockInventory.setQualitity(stockInventoryDto.getQualitity());
            newStockInventory.setVersion(1);
            newStockInventory.setRemark(stockInventoryDto.getRemark());
            newStockInventory.setLockedQuantity(stockInventoryDto.getLockedQuantity());
            newStockInventory.setWarnNum(stockInventoryDto.getWarnNum());
            stockInventoryMapper.insert(newStockInventory);
        } else {
            stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
        }
        updateOrCreateStockInventory(toStockInRecord(stockInRecordDto));
        return true;
    }
    //出库调用
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean subtractStockInventory(StockInventoryDto stockInventoryDto) {
        //  æ–°å¢žå‡ºåº“记录
        StockOutRecordDto stockOutRecordDto = new StockOutRecordDto();
        stockOutRecordDto.setRecordId(stockInventoryDto.getRecordId());
        stockOutRecordDto.setRecordType(stockInventoryDto.getRecordType());
@@ -276,7 +278,8 @@
        stockOutRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockOutRecordDto.setType("0");
        stockOutRecordService.add(stockOutRecordDto);
        if (ObjectUtils.isEmpty(stockInventoryDto.getBatchNo())) {
        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())
                    .orderByAsc(StockInventory::getId));
@@ -286,14 +289,14 @@
            BigDecimal remainingQty = stockInventoryDto.getQualitity();
            for (StockInventory stockInventory : stockInventories) {
                BigDecimal lockedQty = stockInventory.getLockedQuantity() == null ? BigDecimal.ZERO : stockInventory.getLockedQuantity();
                BigDecimal availableQty = stockInventory.getQualitity().subtract(lockedQty);
                BigDecimal lockedQty = defaultDecimal(stockInventory.getLockedQuantity());
                BigDecimal availableQty = defaultDecimal(stockInventory.getQualitity()).subtract(lockedQty);
                if (availableQty.compareTo(BigDecimal.ZERO) <= 0) {
                    continue;
                }
                BigDecimal deductQty = remainingQty.min(availableQty);
                stockInventory.setQualitity(stockInventory.getQualitity().subtract(deductQty));
                stockInventory.setQualitity(defaultDecimal(stockInventory.getQualitity()).subtract(deductQty));
                stockInventory.setVersion(stockInventory.getVersion() == null ? 1 : stockInventory.getVersion() + 1);
                stockInventory.setUpdateTime(LocalDateTime.now());
                stockInventoryMapper.updateById(stockInventory);
@@ -306,55 +309,51 @@
            ProductModel productModel = productModelMapper.selectById(stockInventoryDto.getProductModelId());
            Product product = productMapper.selectById(productModel.getProductId());
            throw new RuntimeException(product.getProductName() + "/" + productModel.getModel() + "库存不足无法出库");
            throw new RuntimeException(product.getProductName() + "/" + productModel.getModel() + "库存不足,无法出库");
        }
        StockInventory oldStockInventory = stockInventoryMapper.selectOne(new QueryWrapper<StockInventory>().lambda().eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId())
                .eq(StockInventory::getBatchNo, stockInventoryDto.getBatchNo()));
        StockInventory oldStockInventory = findInventoryForMerge(
                stockInventoryDto.getProductModelId(),
                stockInventoryDto.getProcessCategory(),
                stockInventoryDto.getVoltage()
        );
        if (ObjectUtils.isEmpty(oldStockInventory)) {
            throw new RuntimeException("产品库存不存在");
        }
        BigDecimal lockedQty = oldStockInventory.getLockedQuantity();
        if (lockedQty == null) {
            lockedQty = BigDecimal.ZERO;
        }
        if (stockInventoryDto.getQualitity().compareTo(oldStockInventory.getQualitity().subtract(lockedQty)) > 0) {
            // æŸ¥è¯¢äº§å“è§„格名
        BigDecimal lockedQty = defaultDecimal(oldStockInventory.getLockedQuantity());
        if (stockInventoryDto.getQualitity().compareTo(defaultDecimal(oldStockInventory.getQualitity()).subtract(lockedQty)) > 0) {
            ProductModel productModel = productModelMapper.selectById(stockInventoryDto.getProductModelId());
            Product product = productMapper.selectById(productModel.getProductId());
            throw new RuntimeException(product.getProductName() + "/" + productModel.getModel() + "库存不足无法出库");
            throw new RuntimeException(product.getProductName() + "/" + productModel.getModel() + "库存不足,无法出库");
        }
        stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
        oldStockInventory.setQualitity(defaultDecimal(oldStockInventory.getQualitity()).subtract(stockInventoryDto.getQualitity()));
        oldStockInventory.setVersion(oldStockInventory.getVersion() == null ? 1 : oldStockInventory.getVersion() + 1);
        oldStockInventory.setUpdateTime(LocalDateTime.now());
        stockInventoryMapper.updateById(oldStockInventory);
        return true;
    }
    @Override
    public R importStockInventory(MultipartFile file) {
        try {
            // æŸ¥è¯¢æ‰€æœ‰çš„产品并构建映射,提高查找效率
            List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectProduct();
            Map<String, SalesLedgerProduct> productMap = new HashMap<>();
            for (SalesLedgerProduct product : salesLedgerProducts) {
                // ä½¿ç”¨äº§å“ç±»åˆ«å’Œè§„格型号作为键
                String key = product.getProductCategory() + "|" + product.getSpecificationModel();
                productMap.put(key, product);
            }
            ExcelUtil<StockInventoryExportData> util = new ExcelUtil<StockInventoryExportData>(StockInventoryExportData.class);
            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);
                if (matchedProduct != null) {
                    // å¤„理合格库存
                    if (dto.getQualifiedQuantity() != null && dto.getQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                        StockInventoryDto stockInventoryDto = new StockInventoryDto();
                        stockInventoryDto.setRecordId(0L);
@@ -363,8 +362,9 @@
                        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("合格冻结数量不能超过本次导入的合格库存数量");
@@ -379,7 +379,6 @@
                        successCount++;
                    }
                    // å¤„理不合格库存
                    if (dto.getUnQualifiedQuantity() != null && dto.getUnQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                        StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
                        stockUninventoryDto.setRecordId(0L);
@@ -388,7 +387,6 @@
                        stockUninventoryDto.setRemark(dto.getRemark());
                        stockUninventoryDto.setBatchNo(dto.getUnQualifiedBatchNo());
                        // éªŒè¯ä¸åˆæ ¼å†»ç»“数量
                        if (ObjectUtils.isNotEmpty(dto.getUnQualifiedLockedQuantity())) {
                            if (dto.getUnQualifiedLockedQuantity().compareTo(dto.getUnQualifiedQuantity()) > 0) {
                                throw new RuntimeException("不合格冻结数量不能超过本次导入的不合格库存数量");
@@ -403,16 +401,14 @@
                        successCount++;
                    }
                } else {
                    // è®°å½•未匹配的产品
                    String unmatchedRecord = "产品名称:" + dto.getProductName() + ",型号:" + dto.getModel();
                    String unmatchedRecord = "产品名称:" + dto.getProductName() + ",型号:" + dto.getModel() + " æœªåŒ¹é…åˆ°åº“存产品";
                    unmatchedRecords.add(unmatchedRecord);
                }
            }
            // æž„建返回信息
            StringBuilder message = new StringBuilder();
            if (!unmatchedRecords.isEmpty()) {
                message.append("导入成功 " + successCount + " æ¡è®°å½•,以下产品未找到匹配项:\n");
                message.append("导入成功 ").append(successCount).append(" æ¡è®°å½•,以下产品未匹配:\n");
                for (String record : unmatchedRecords) {
                    message.append(record).append("\n");
                }
@@ -421,14 +417,13 @@
            return R.ok("导入成功,共处理 " + successCount + " æ¡è®°å½•");
        } catch (Exception e) {
            log.error("导入库存失败", e);
            return R.fail("导入失败:" + e.getMessage());
            log.error("库存导入失败", e);
            return R.fail("库存导入失败:" + e.getMessage());
        }
    }
    @Override
    public void exportStockInventory(HttpServletResponse response, StockInventoryDto stockInventoryDto) {
        List<StockInventoryExportData> list = stockInventoryMapper.listStockInventoryExportData(stockInventoryDto);
        ExcelUtil<StockInventoryExportData> util = new ExcelUtil<>(StockInventoryExportData.class);
        util.exportExcel(response, list, "库存信息");
@@ -467,4 +462,132 @@
        stockInventory.setLockedQuantity(stockInventory.getLockedQuantity().subtract(stockInventoryDto.getLockedQuantity()));
        return this.updateById(stockInventory);
    }
    // å…¥åº“合并唯一键:成品按类别和电压,其余只按产品规格。
    private StockInventory findInventoryForMerge(Long productModelId, String processCategory, String voltage) {
        LambdaQueryWrapper<StockInventory> queryWrapper = new LambdaQueryWrapper<StockInventory>()
                .eq(StockInventory::getProductModelId, productModelId)
                .orderByAsc(StockInventory::getId);
        if (usesDimensionIdentity(processCategory, voltage)) {
            queryWrapper.eq(StockInventory::getProcessCategory, normalizeDimension(processCategory));
            queryWrapper.eq(StockInventory::getVoltage, normalizeDimension(voltage));
        }
        List<StockInventory> inventories = stockInventoryMapper.selectList(queryWrapper);
        return inventories.isEmpty() ? null : inventories.get(0);
    }
    // å°†å…¥åº“记录 DTO å¤åˆ¶ä¸ºæŒä¹…化实体。
    private StockInRecord toStockInRecord(StockInRecordDto stockInRecordDto) {
        StockInRecord stockInRecord = new StockInRecord();
        stockInRecord.setRecordId(stockInRecordDto.getRecordId());
        stockInRecord.setRecordType(stockInRecordDto.getRecordType());
        stockInRecord.setStockInNum(stockInRecordDto.getStockInNum());
        stockInRecord.setProductModelId(stockInRecordDto.getProductModelId());
        stockInRecord.setRemark(stockInRecordDto.getRemark());
        stockInRecord.setType(stockInRecordDto.getType());
        stockInRecord.setLockedQuantity(stockInRecordDto.getLockedQuantity());
        stockInRecord.setWarnNum(stockInRecordDto.getWarnNum());
        stockInRecord.setApproveStatus(stockInRecordDto.getApproveStatus());
        stockInRecord.setBatchNo(stockInRecordDto.getBatchNo());
        stockInRecord.setProcessCategory(stockInRecordDto.getProcessCategory());
        stockInRecord.setVoltage(stockInRecordDto.getVoltage());
        return stockInRecord;
    }
    // åªè¦å¸¦æœ‰æˆå“ç»´åº¦ï¼Œå°±æŒ‰ç»´åº¦ä½œä¸ºå”¯ä¸€é”®ã€‚
    private boolean usesDimensionIdentity(String processCategory, String voltage) {
        return StringUtils.isNotBlank(processCategory) || StringUtils.isNotBlank(voltage);
    }
    private String normalizeDimension(String value) {
        return StringUtils.trimToEmpty(value);
    }
    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/support/FinishedProductStockDimensionResolver.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,184 @@
package com.ruoyi.stock.support;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.production.mapper.ProductProcessMapper;
import com.ruoyi.production.mapper.ProductProcessRouteItemMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.mapper.ProductionProductOutputMapper;
import com.ruoyi.production.pojo.ProductProcess;
import com.ruoyi.production.pojo.ProductProcessRouteItem;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.pojo.ProductionProductOutput;
import org.springframework.stereotype.Component;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Component
public class FinishedProductStockDimensionResolver {
    private static final String PROCESS_COPPER = "印铜";
    private static final String PROCESS_SILVER = "印银";
    private static final String PROCESS_CATEGORY_COPPER = "铜极";
    private static final String PROCESS_CATEGORY_SILVER = "银极";
    private static final String VOLTAGE_PARAMETER = "电压";
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final ProductProcessRouteItemMapper productProcessRouteItemMapper;
    private final ProductProcessMapper productProcessMapper;
    public FinishedProductStockDimensionResolver(ProductionProductMainMapper productionProductMainMapper,
                                                 ProductionProductOutputMapper productionProductOutputMapper,
                                                 ProductProcessRouteItemMapper productProcessRouteItemMapper,
                                                 ProductProcessMapper productProcessMapper) {
        this.productionProductMainMapper = productionProductMainMapper;
        this.productionProductOutputMapper = productionProductOutputMapper;
        this.productProcessRouteItemMapper = productProcessRouteItemMapper;
        this.productProcessMapper = productProcessMapper;
    }
    /**
     * è§£æžæˆå“å…¥åº“类别。
     * å½“工序为印铜时返回铜极,当工序为印银时返回银极。
     */
    public String resolveProcessCategory(Long productMainId) {
        ProductionProductMain productionProductMain = loadProductionMain(productMainId);
        ProductProcessRouteItem routeItem = loadRouteItem(productionProductMain);
        if (routeItem.getProductRouteId() == null) {
            throw new ServiceException("工艺路线未绑定工艺明细,无法确定成品类别");
        }
        return resolveProcessCategoryByRouteId(routeItem.getProductRouteId());
    }
    /**
     * è§£æžæˆå“å…¥åº“电压,来源于报工 otherData ä¸­çš„“电压”参数。
     */
    public String resolveVoltage(Long productMainId) {
        List<ProductionProductOutput> outputs = loadOutputs(productMainId);
        for (ProductionProductOutput output : outputs) {
            String voltage = resolveVoltageValue(output.getOtherData());
            if (StringUtils.isNotBlank(voltage)) {
                return voltage;
            }
        }
        throw new ServiceException("请配置参数电压");
    }
    private ProductionProductMain loadProductionMain(Long productMainId) {
        if (productMainId == null) {
            throw new ServiceException("报工ID不能为空");
        }
        ProductionProductMain productionProductMain = productionProductMainMapper.selectById(productMainId);
        if (productionProductMain == null) {
            throw new ServiceException("报工记录不存在,无法确定成品库存维度");
        }
        return productionProductMain;
    }
    private ProductProcessRouteItem loadRouteItem(ProductionProductMain productionProductMain) {
        if (productionProductMain.getProductProcessRouteItemId() == null) {
            throw new ServiceException("报工未关联工艺路线明细,无法确定成品库存维度");
        }
        ProductProcessRouteItem routeItem = productProcessRouteItemMapper.selectById(productionProductMain.getProductProcessRouteItemId());
        if (routeItem == null) {
            throw new ServiceException("工艺路线明细不存在,无法确定成品库存维度");
        }
        return routeItem;
    }
    private List<ProductionProductOutput> loadOutputs(Long productMainId) {
        List<ProductionProductOutput> outputs = productionProductOutputMapper.selectList(
                new LambdaQueryWrapper<ProductionProductOutput>()
                        .eq(ProductionProductOutput::getProductMainId, productMainId)
        );
        if (CollectionUtils.isEmpty(outputs)) {
            throw new ServiceException("报工输出记录不存在,无法确定成品库存维度");
        }
        return outputs;
    }
    private String resolveProcessCategoryByRouteId(Long productRouteId) {
        List<ProductProcessRouteItem> routeItems = productProcessRouteItemMapper.selectList(
                new LambdaQueryWrapper<ProductProcessRouteItem>()
                        .eq(ProductProcessRouteItem::getProductRouteId, productRouteId)
        );
        if (CollectionUtils.isEmpty(routeItems)) {
            throw new ServiceException("订单工艺路线未配置工序,无法确定成品类别");
        }
        Set<Long> processIds = routeItems.stream()
                .map(ProductProcessRouteItem::getProcessId)
                .filter(id -> id != null)
                .collect(Collectors.toSet());
        if (processIds.isEmpty()) {
            throw new ServiceException("订单工艺路线未配置工序,无法确定成品类别");
        }
        List<ProductProcess> processes = productProcessMapper.selectBatchIds(processIds);
        Set<String> matchedCategories = processes.stream()
                .map(ProductProcess::getName)
                .filter(name -> PROCESS_COPPER.equals(name) || PROCESS_SILVER.equals(name))
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (matchedCategories.isEmpty()) {
            throw new ServiceException("订单工艺路线未配置印铜/印银工序,无法确定成品类别");
        }
        if (matchedCategories.size() > 1) {
            throw new ServiceException("订单工艺路线同时配置了印铜和印银,无法确定成品类别");
        }
        String matchedProcess = matchedCategories.iterator().next();
        if (PROCESS_COPPER.equals(matchedProcess)) {
            return PROCESS_CATEGORY_COPPER;
        }
        return PROCESS_CATEGORY_SILVER;
    }
    private String resolveVoltageValue(String otherData) {
        if (StringUtils.isBlank(otherData)) {
            return null;
        }
        Object parsed;
        try {
            parsed = JSON.parse(otherData);
        } catch (Exception ex) {
            throw new ServiceException("报工参数格式错误,无法解析电压");
        }
        String voltage = StringUtils.trim(findVoltageValue(parsed));
        return StringUtils.isBlank(voltage) ? null : voltage;
    }
    private String findVoltageValue(Object node) {
        if (node instanceof JSONArray) {
            JSONArray array = (JSONArray) node;
            for (Object item : array) {
                String voltage = findVoltageValue(item);
                if (StringUtils.isNotBlank(voltage)) {
                    return voltage;
                }
            }
            return null;
        }
        if (node instanceof JSONObject) {
            JSONObject object = (JSONObject) node;
            if (VOLTAGE_PARAMETER.equals(StringUtils.trim(object.getString("parameterItem")))) {
                String value = StringUtils.trim(object.getString("value"));
                if (StringUtils.isNotBlank(value)) {
                    return value;
                }
            }
            for (Object value : object.values()) {
                String voltage = findVoltageValue(value);
                if (StringUtils.isNotBlank(voltage)) {
                    return voltage;
                }
            }
        }
        return null;
    }
}
src/main/resources/mapper/production/ProductWorkOrderMapper.xml
@@ -58,6 +58,9 @@
            <if test="c.productOrderNpsNo != null and c.productOrderNpsNo != ''">
                and po.nps_no like concat('%',#{c.productOrderNpsNo},'%')
            </if>
            <if test="c.processName != null and c.processName != ''">
                and pp.NAME like concat('%',#{c.processName},'%')
            </if>
        order by pwo.priority asc, po.id desc, ppri.drag_sort asc
    </select>
src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml
@@ -24,7 +24,7 @@
        ELSE false
        END AS method
        FROM quality_unqualified qu
        LEFT JOIN product_model pm ON qu.model = pm.model
        LEFT JOIN product_model pm ON qu.model = pm.model and qu.product_id = pm.product_id
        where
        1=1
        <if test="qualityUnqualified.inspectType != null ">
src/main/resources/mapper/sales/SalesLedgerMapper.xml
@@ -43,7 +43,7 @@
        </where>
    </select>
    <select id="selectSalesLedgerListPage" resultType="com.ruoyi.sales.pojo.SalesLedger">
    <select id="selectSalesLedgerListPage" resultType="com.ruoyi.sales.dto.SalesLedgerDto">
        SELECT T1.id,
        T1.sales_contract_no,
        T1.customer_contract_no,
@@ -68,6 +68,9 @@
        T1.is_produce AS produce,
        T1.delivery_date,
        DATEDIFF(T1.delivery_date, CURDATE()) AS delivery_days_diff,
        product_summary.model,
        '' AS voltage,
        product_summary.qty,
        CASE
        WHEN shipping_status_counts.total_count = 0 THEN false
        WHEN shipping_status_counts.unshipped_count = 0 THEN true
@@ -83,6 +86,15 @@
        FROM shipping_info
        GROUP BY sales_ledger_id
        ) shipping_status_counts ON T1.id = shipping_status_counts.sales_ledger_id
        LEFT JOIN (
        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
        FROM sales_ledger_product
        WHERE type = 1
        GROUP BY sales_ledger_id
        ) product_summary ON T1.id = product_summary.sales_ledger_id
        <where>
            <if test="salesLedgerDto.customerName != null and salesLedgerDto.customerName != '' ">
                AND  T1.customer_name LIKE CONCAT('%',#{salesLedgerDto.customerName},'%')
src/main/resources/mapper/stock/StockInRecordMapper.xml
@@ -40,6 +40,12 @@
            <if test="params.recordType != null and params.recordType != ''">
                AND sir.record_type = #{params.recordType}
            </if>
            <if test="params.processCategory != null and params.processCategory != ''">
                AND sir.process_category = #{params.processCategory}
            </if>
            <if test="params.voltage != null and params.voltage != ''">
                AND sir.voltage = #{params.voltage}
            </if>
            <if test="params.topParentProductId != null and params.topParentProductId > 0">
                AND p.id in (select id from product_tree)
            </if>
@@ -83,6 +89,12 @@
            <if test="params.recordType != null and params.recordType != ''">
                and sir.record_type = #{params.recordType}
            </if>
            <if test="params.processCategory != null and params.processCategory != ''">
                and sir.process_category = #{params.processCategory}
            </if>
            <if test="params.voltage != null and params.voltage != ''">
                and sir.voltage = #{params.voltage}
            </if>
            <if test="params.topParentProductId != null and params.topParentProductId > 0">
                and p.id in (select id from product_tree)
            </if>
src/main/resources/mapper/stock/StockInventoryMapper.xml
@@ -12,6 +12,9 @@
        <result column="version" property="version"/>
        <result column="locked_quantity" property="lockedQuantity"/>
        <result column="warn_num" property="warnNum"/>
        <result column="process_category" property="processCategory"/>
        <result column="voltage" property="voltage"/>
        <result column="batch_no" property="batchNo"/>
    </resultMap>
    <update id="updateAddStockInventory">
        update stock_inventory
@@ -64,6 +67,8 @@
        (si.qualitity - COALESCE(si.locked_quantity, 0)) AS un_locked_quantity,
        pm.model,
        si.remark,
        si.process_category as processCategory,
        si.voltage,
        pm.unit,
        pm.material_code as materialCode,
        p.product_name,
@@ -78,6 +83,12 @@
        AND (p2.product_name != '半成品' OR p2.product_name IS NULL)
        <if test="ew.productName != null and ew.productName != ''">
            AND p.product_name 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>
    </select>
@@ -109,6 +120,8 @@
        MAX(version) as version,
        model,
        MAX(remark) as remark,
        processCategory,
        voltage,
        unit,
        product_name,
        product_id,
@@ -133,6 +146,8 @@
        (si.qualitity - COALESCE(si.locked_quantity, 0)) as un_locked_quantity,
        pm.model,
        si.remark,
        si.process_category as processCategory,
        si.voltage as voltage,
        pm.unit,
        p.product_name,
        p.id as product_id,
@@ -161,6 +176,8 @@
        (su.qualitity - COALESCE(su.locked_quantity, 0)) as un_locked_quantity,
        pm.model,
        su.remark,
        '' as processCategory,
        '' as voltage,
        pm.unit,
        p.product_name,
        p.id as product_id,
@@ -184,7 +201,7 @@
                and combined.product_id in (select id from product_tree)
            </if>
        </where>
        group by product_model_id, model, unit, product_name, product_id, qualifiedBatchNo, unQualifiedBatchNo, materialCode, product_id, qualifiedBatchNo, unQualifiedBatchNo
        group by product_model_id, model, unit, product_name, product_id, unQualifiedBatchNo, materialCode, processCategory, voltage
    </select>
    <select id="listStockInventoryExportData" resultType="com.ruoyi.stock.execl.StockInventoryExportData">
@@ -207,6 +224,8 @@
        model,
        unit,
        product_name,
        processCategory,
        voltage,
        MAX(warn_num) as warn_num,
        MAX(remark) as remark,
        MAX(update_time) as update_time,
@@ -227,6 +246,8 @@
        pm.model,
        pm.unit,
        p.product_name,
        si.process_category as processCategory,
        si.voltage as voltage,
        p.id as product_id,
        si.batch_no as qualifiedBatchNo,
        null as unQualifiedBatchNo,
@@ -250,6 +271,8 @@
        pm.model,
        pm.unit,
        p.product_name,
        '' as processCategory,
        '' as voltage,
        p.id as product_id,
        null as qualifiedBatchNo,
        su.batch_no as unQualifiedBatchNo,
@@ -271,18 +294,32 @@
                and combined.product_id in (select id from product_tree)
            </if>
        </where>
        group by product_model_id, model, unit, product_name, qualifiedBatchNo, unQualifiedBatchNo, materialCode
        group by product_model_id, model, unit, product_name, unQualifiedBatchNo, materialCode, processCategory, voltage
    </select>
    <select id="stockInventoryPage" resultType="com.ruoyi.stock.dto.StockInRecordDto">
        select sir.*,si.qualitity as current_stock,
        pm.model,
        sir.process_category as processCategory,
        sir.voltage,
        pm.unit,
        pm.material_code as materialCode,
        p.product_name,
        su.nick_name as create_by
        from
        stock_in_record sir
        left join stock_inventory si on sir.product_model_id = si.product_model_id
        left join stock_inventory si
               on sir.product_model_id = si.product_model_id
              and (
                    (ifnull(sir.process_category, '') != '' or ifnull(sir.voltage, '') != '')
                    and ifnull(sir.process_category, '') = ifnull(si.process_category, '')
                    and ifnull(sir.voltage, '') = ifnull(si.voltage, '')
                    or
                    ifnull(sir.process_category, '') = ''
                    and ifnull(sir.voltage, '') = ''
                    and ifnull(si.process_category, '') = ''
                    and ifnull(si.voltage, '') = ''
                    and si.product_model_id = sir.product_model_id
                  )
        left join product_model pm on sir.product_model_id = pm.id
        left join product p on pm.product_id = p.id
        left join sys_user su on sir.create_user = su.user_id
@@ -432,6 +469,8 @@
               (si.qualitity - COALESCE(si.locked_quantity, 0)) as un_locked_quantity,
               pm.model,
               si.remark,
               si.process_category as processCategory,
               si.voltage,
               pm.unit,
               p.product_name,
               p.id as product_id
@@ -440,4 +479,35 @@
                 left join product p on pm.product_id = p.id
    </select>
    <select id="selectFinishedProductInventoryList" resultType="com.ruoyi.stock.dto.StockInventoryDto">
        select si.id,
               si.product_model_id,
               si.process_category as processCategory,
               si.voltage,
               pm.model,
               pm.material_code as materialCode,
               pm.unit,
               p.product_name,
               p.id as product_id
        from stock_inventory si
                 left join product_model pm on si.product_model_id = pm.id
                 left join product p on pm.product_id = p.id
                 left 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>