liu
4 天以前 a5f66f55b2c176e8504b8a077e00acfe2dfb4b00
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -22,7 +22,6 @@
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.production.bean.dto.ProductStructureDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.enums.ProductOrderStatusEnum;
import com.ruoyi.production.mapper.*;
@@ -50,6 +49,7 @@
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -63,6 +63,16 @@
@AllArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class ProductionProductMainServiceImpl extends ServiceImpl<ProductionProductMainMapper, ProductionProductMain> implements ProductionProductMainService {
    /**
     * 生产报工大类:只区分小提琴/大提琴/贝斯,不再区分零件。
     * key为入库规范值,value为产品名称匹配别名(现场数据贝斯写作“贝司”)。
     */
    private static final Map<String, List<String>> MAJOR_CATEGORY_PATTERNS = new LinkedHashMap<>() {{
        put("小提琴", List.of("小提琴"));
        put("大提琴", List.of("大提琴"));
        put("贝斯", List.of("贝斯", "贝司"));
    }};
    private final ProductionProductMainMapper productionProductMainMapper;
    private final SysUserMapper userMapper;
@@ -78,8 +88,6 @@
    private final ProductionAccountMapper productionAccountMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
    private final ProductionOrderRoutingOperationParamMapper productionOrderRoutingOperationParamMapper;
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
@@ -279,13 +287,31 @@
            throw new ServiceException("产品规格不存在");
        }
        // 报工完成时对计数的零件做区分:合格/轻微返工/严重返工/报废四类数量。
        BigDecimal qualifiedQty = defaultDecimal(dto.getQuantity());
        BigDecimal reworkMinorQty = defaultDecimal(dto.getReworkMinorQty());
        BigDecimal reworkMajorQty = defaultDecimal(dto.getReworkMajorQty());
        BigDecimal scrapQtyInput = defaultDecimal(dto.getScrapQty());
        if (qualifiedQty.compareTo(BigDecimal.ZERO) < 0
                || reworkMinorQty.compareTo(BigDecimal.ZERO) < 0
                || reworkMajorQty.compareTo(BigDecimal.ZERO) < 0
                || scrapQtyInput.compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("报工数量不能为负数");
        }
        if (qualifiedQty.add(reworkMinorQty).add(reworkMajorQty).add(scrapQtyInput).compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("请按合格/轻微返工/严重返工/报废分类填写零件数量");
        }
        ProductionProductMain productionProductMain = new ProductionProductMain();
        productionProductMain.setProductNo(generateProductNo());
        productionProductMain.setMajorCategory(resolveMajorCategory(productModel));
        productionProductMain.setUserId(user == null ? dto.getUserId() : user.getUserId());
        productionProductMain.setUserName(user == null ? dto.getUserName() : user.getNickName());
        productionProductMain.setProductionOperationTaskId(taskId);
        productionProductMain.setStatus(0);
        productionProductMain.setReportType(dto.getReportType() == null ? 0 : dto.getReportType());
        productionProductMain.setReportType(dto.getReportType() != null
                ? dto.getReportType()
                : deriveReportType(qualifiedQty, reworkMinorQty, reworkMajorQty, scrapQtyInput));
        productionProductMain.setAuditStatus(dto.getAuditStatus() == null ? 0 : dto.getAuditStatus());
        productionProductMain.setAuditRemark(dto.getAuditRemark());
        productionProductMain.setWorkHour(dto.getWorkHour());
@@ -293,30 +319,21 @@
        syncOperationParamInputValue(dto, routingOperation.getId(), productionProductMain.getId());
        createReportApprovalInstance(productionProductMain);
        List<ProductStructureDto> productStructureDtos = resolveInputStructures(
                productionOrder.getId(), routingOperation, productModel.getId());
       // 如果没有bom子节点了,那么投入就是他本身
        if (productStructureDtos.isEmpty()) {
            ProductStructureDto fallbackInput = new ProductStructureDto();
            fallbackInput.setProductModelId(productModel.getId());
            fallbackInput.setUnitQuantity(BigDecimal.ONE);
            productStructureDtos.add(fallbackInput);
        }
        for (ProductStructureDto item : productStructureDtos) {
            // 当前实现按工序成品直接作为投入,后续若接入领料记录可在这里替换来源。
            ProductionProductInput productionProductInput = new ProductionProductInput();
            productionProductInput.setProductionProductMainId(productionProductMain.getId());
            productionProductInput.setProductModelId(item.getProductModelId());
            productionProductInput.setInputQuantity(item.getUnitQuantity().multiply(defaultDecimal(dto.getQuantity())));
            productionProductInput.setQuantity(productionProductInput.getInputQuantity());
            productionProductInputMapper.insert(productionProductInput);
        }
        // 需求变更:生产报工不再按零件(BOM结构)拆分投入,投入即本次报工的产品本身,按大类统计。
        ProductionProductInput productionProductInput = new ProductionProductInput();
        productionProductInput.setProductionProductMainId(productionProductMain.getId());
        productionProductInput.setProductModelId(productModel.getId());
        productionProductInput.setInputQuantity(qualifiedQty);
        productionProductInput.setQuantity(qualifiedQty);
        productionProductInputMapper.insert(productionProductInput);
        ProductionProductOutput productionProductOutput = new ProductionProductOutput();
        productionProductOutput.setProductionProductMainId(productionProductMain.getId());
        productionProductOutput.setProductModelId(productModel.getId());
        productionProductOutput.setQuantity(defaultDecimal(dto.getQuantity()));
        productionProductOutput.setScrapQty(defaultDecimal(dto.getScrapQty()));
        productionProductOutput.setReworkMinorQty(reworkMinorQty);
        productionProductOutput.setReworkMajorQty(reworkMajorQty);
        productionProductOutputMapper.insert(productionProductOutput);
        BigDecimal reportQty = defaultDecimal(productionProductOutput.getQuantity());
        BigDecimal scrapQty = defaultDecimal(productionProductOutput.getScrapQty());
@@ -360,7 +377,8 @@
                                qualityInspectParamMapper.insert(param);
                            });
                }
            } else {
            } else if(isLastOperation){
                //(最后一道工序才入库)
                StockInventoryDto stockInventoryDto = new StockInventoryDto();
                stockInventoryDto.setRecordId(productionProductMain.getId());
                stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode()));
@@ -427,7 +445,69 @@
                    productionProductMain.getId(),
                    qualifiedBatchNo);
        }
        // 区分出的返工零件生成不合格品待处理单,进入不合格品管理的报工处理闭环。
        createReworkUnqualifiedRecords(productionProductMain, productModel, reworkMinorQty, reworkMajorQty);
        return true;
    }
    private Integer deriveReportType(BigDecimal qualifiedQty, BigDecimal reworkMinorQty,
                                     BigDecimal reworkMajorQty, BigDecimal scrapQty) {
        if (qualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
            return 0;
        }
        if (reworkMinorQty.compareTo(BigDecimal.ZERO) > 0) {
            return 1;
        }
        if (reworkMajorQty.compareTo(BigDecimal.ZERO) > 0) {
            return 2;
        }
        if (scrapQty.compareTo(BigDecimal.ZERO) > 0) {
            return 3;
        }
        return 0;
    }
    private void createReworkUnqualifiedRecords(ProductionProductMain productionProductMain,
                                                ProductModel productModel,
                                                BigDecimal reworkMinorQty,
                                                BigDecimal reworkMajorQty) {
        if (reworkMinorQty.compareTo(BigDecimal.ZERO) <= 0 && reworkMajorQty.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        Product product = productMapper.selectById(productModel.getProductId());
        if (product == null) {
            throw new ServiceException("产品不存在,无法生成报工返工待处理单");
        }
        if (reworkMinorQty.compareTo(BigDecimal.ZERO) > 0) {
            insertReworkUnqualified(productionProductMain, product, productModel, reworkMinorQty, 0);
        }
        if (reworkMajorQty.compareTo(BigDecimal.ZERO) > 0) {
            insertReworkUnqualified(productionProductMain, product, productModel, reworkMajorQty, 1);
        }
    }
    private void insertReworkUnqualified(ProductionProductMain productionProductMain,
                                         Product product,
                                         ProductModel productModel,
                                         BigDecimal quantity,
                                         int dealType) {
        QualityUnqualified qualityUnqualified = new QualityUnqualified();
        qualityUnqualified.setInspectType(1);
        qualityUnqualified.setInspectState(0);
        qualityUnqualified.setCheckTime(new Date());
        qualityUnqualified.setCheckName(productionProductMain.getUserName());
        qualityUnqualified.setProductId(product.getId());
        qualityUnqualified.setProductName(product.getProductName());
        qualityUnqualified.setModel(productModel.getModel());
        qualityUnqualified.setUnit(productModel.getUnit());
        qualityUnqualified.setQuantity(quantity);
        qualityUnqualified.setProductModelId(productModel.getId());
        qualityUnqualified.setProductMainId(productionProductMain.getId());
        qualityUnqualified.setDefectivePhenomena("生产报工【" + productionProductMain.getProductNo()
                + "】计数区分-" + (dealType == 0 ? "轻微返工" : "严重返工"));
        qualityUnqualified.setDealType(dealType);
        qualityUnqualified.setDealResult("报工");
        qualityUnqualifiedMapper.insert(qualityUnqualified);
    }
    private void createReportApprovalInstance(ProductionProductMain productionProductMain) {
@@ -467,6 +547,8 @@
        BigDecimal schedulingNum = task == null ? BigDecimal.ZERO : defaultDecimal(task.getPlanQuantity());
        BigDecimal finishedNum = output == null ? BigDecimal.ZERO : defaultDecimal(output.getQuantity());
        BigDecimal scrapQty = output == null ? BigDecimal.ZERO : defaultDecimal(output.getScrapQty());
        BigDecimal reworkMinorNum = output == null ? BigDecimal.ZERO : defaultDecimal(output.getReworkMinorQty());
        BigDecimal reworkMajorNum = output == null ? BigDecimal.ZERO : defaultDecimal(output.getReworkMajorQty());
        BigDecimal pendingNum = schedulingNum.subtract(finishedNum);
        if (pendingNum.compareTo(BigDecimal.ZERO) < 0) {
            pendingNum = BigDecimal.ZERO;
@@ -479,7 +561,9 @@
        fields.add(buildReportField("workOrderNo", "工单编号", "text", resolveWorkOrderNo(productionProductMain.getProductionOperationTaskId())));
        fields.add(buildReportField("schedulingNum", "排产数量", "number", schedulingNum));
        fields.add(buildReportField("pendingNum", "待生产数量", "number", pendingNum));
        fields.add(buildReportField("finishedNum", "生产合格数量", "number", finishedNum));
        fields.add(buildReportField("finishedNum", "合格数量", "number", finishedNum));
        fields.add(buildReportField("reworkMinorQty", "轻微返工数量", "number", reworkMinorNum));
        fields.add(buildReportField("reworkMajorQty", "严重返工数量", "number", reworkMajorNum));
        fields.add(buildReportField("scrapQty", "报废数量", "number", scrapQty));
        fields.add(buildReportField("reportType", "报工结果", "select", resolveReportTypeLabel(productionProductMain.getReportType())));
        fields.add(buildReportField("workHour", "工时", "number", productionProductMain.getWorkHour()));
@@ -496,6 +580,8 @@
        formPayload.put("pendingNum", pendingNum);
        formPayload.put("finishedNum", finishedNum);
        formPayload.put("scrapQty", scrapQty);
        formPayload.put("reworkMinorQty", reworkMinorNum);
        formPayload.put("reworkMajorQty", reworkMajorNum);
        formPayload.put("reportType", resolveReportTypeLabel(productionProductMain.getReportType()));
        formPayload.put("workHour", productionProductMain.getWorkHour());
        formPayload.put("auditStatus", resolveAuditStatusLabel(productionProductMain.getAuditStatus()));
@@ -654,62 +740,46 @@
        return target;
    }
    private List<ProductStructureDto> resolveInputStructures(Long productionOrderId,
                                                             ProductionOrderRoutingOperation routingOperation,
                                                             Long outputProductModelId) {
        if (productionOrderId == null || routingOperation == null || routingOperation.getTechnologyOperationId() == null) {
            return new ArrayList<>();
    /**
     * 需求:生产报工只区分产品大类(小提琴/大提琴/贝斯),不再区分零件。
     * 从产出产品自身名称开始沿产品树向上做关键字匹配,命中即取对应规范大类;未命中返回 null。
     * 产品树顶层为“成品/半成品/原料”等库存分类,不能直接当作大类,因此必须自底向上取第一个命中的层级。
     */
    private String resolveMajorCategory(ProductModel productModel) {
        if (productModel == null || productModel.getProductId() == null) {
            return null;
        }
        ProductionOrderBom orderBom = productionOrderBomMapper.selectOne(
                Wrappers.<ProductionOrderBom>lambdaQuery()
                        .eq(ProductionOrderBom::getProductionOrderId, productionOrderId)
                        .orderByDesc(ProductionOrderBom::getId)
                        .last("limit 1"));
        if (orderBom == null || orderBom.getId() == null) {
            return new ArrayList<>();
        }
        List<ProductionBomStructure> bomNodeList = productionBomStructureMapper.selectList(
                Wrappers.<ProductionBomStructure>lambdaQuery()
                        .eq(ProductionBomStructure::getProductionOrderBomId, orderBom.getId())
                        .orderByAsc(ProductionBomStructure::getId));
        if (bomNodeList.isEmpty()) {
            return new ArrayList<>();
        }
        Map<Long, ProductionBomStructure> nodeMap = bomNodeList.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left));
        Long currentOutputModelId = routingOperation.getProductModelId() != null
                ? routingOperation.getProductModelId()
                : outputProductModelId;
        Map<Long, BigDecimal> unitQtyByProductModel = new LinkedHashMap<>();
        for (ProductionBomStructure node : bomNodeList) {
            if (node == null || node.getParentId() == null || node.getProductModelId() == null) {
                continue;
        Product product = productMapper.selectById(productModel.getProductId());
        Set<Long> visited = new LinkedHashSet<>();
        while (product != null && visited.add(product.getId())) {
            String category = matchMajorCategory(product.getProductName());
            if (category != null) {
                return category;
            }
            if (!Objects.equals(node.getTechnologyOperationId(), routingOperation.getTechnologyOperationId())) {
                continue;
            if (product.getParentId() == null || product.getParentId() == 0L) {
                break;
            }
            ProductionBomStructure parent = nodeMap.get(node.getParentId());
            if (parent == null || !Objects.equals(parent.getProductModelId(), currentOutputModelId)) {
                continue;
            }
            unitQtyByProductModel.merge(node.getProductModelId(), defaultDecimal(node.getUnitQuantity()), BigDecimal::add);
            product = productMapper.selectById(product.getParentId());
        }
        return null;
    }
        List<ProductStructureDto> result = new ArrayList<>();
        for (Map.Entry<Long, BigDecimal> entry : unitQtyByProductModel.entrySet()) {
            if (entry.getValue().compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            ProductStructureDto item = new ProductStructureDto();
            item.setProductModelId(entry.getKey());
            item.setUnitQuantity(entry.getValue());
            result.add(item);
    static String matchMajorCategory(String productName) {
        if (productName == null || productName.isEmpty()) {
            return null;
        }
        return result;
        // 琴盒/琴套等配件名称可能带“贝司”等字样(如 SA贝司套),排除后避免误归大类。
        if (productName.contains("琴盒") || productName.contains("琴套") || productName.contains("弓盒")) {
            return null;
        }
        for (Map.Entry<String, List<String>> entry : MAJOR_CATEGORY_PATTERNS.entrySet()) {
            for (String alias : entry.getValue()) {
                if (productName.contains(alias)) {
                    return entry.getKey();
                }
            }
        }
        return null;
    }
    private Boolean removeProductMainByProductionTask(ProductionProductMain productionProductMain) {
@@ -725,6 +795,13 @@
            if (qualityUnqualifieds.size() > 0 && qualityUnqualifieds.get(0).getInspectState() == 1) {
                throw new ServiceException("该条报工已经不合格处理了,不允许删除");
            }
        }
        // 报工计数区分直接产生的返工待处理单:已处理的不允许删除报工,未处理的随报工一并删除。
        List<QualityUnqualified> reworkUnqualifieds = qualityUnqualifiedMapper.selectList(
                Wrappers.<QualityUnqualified>lambdaQuery()
                        .eq(QualityUnqualified::getProductMainId, productionProductMain.getId()));
        if (reworkUnqualifieds.stream().anyMatch(q -> Integer.valueOf(1).equals(q.getInspectState()))) {
            throw new ServiceException("该条报工的返工零件已做不合格处理,不允许删除");
        }
        ProductionProductOutput productionProductOutput = productionProductOutputMapper.selectList(
                Wrappers.<ProductionProductOutput>lambdaQuery()
@@ -772,6 +849,8 @@
            }
        }
        qualityUnqualifiedMapper.delete(new LambdaQueryWrapper<QualityUnqualified>()
                .eq(QualityUnqualified::getProductMainId, productionProductMain.getId()));
        qualityInspectMapper.selectList(new LambdaQueryWrapper<QualityInspect>()
                .eq(QualityInspect::getProductMainId, productionProductMain.getId())).forEach(q -> {
            qualityInspectParamMapper.delete(new LambdaQueryWrapper<QualityInspectParam>()