gongchunyi
10 小时以前 e80eae3bcd9f1ee0eecd4072f943f2519762f164
feat: 工序添加是否领料,用于后续生产订单领料
已添加1个文件
已修改6个文件
222 ■■■■■ 文件已修改
doc/20261020_工序是否领料与生产BOM结构领料标记.sql 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionBomStructure.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/pojo/TechnologyOperation.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionBomStructureMapper.xml 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyOperationMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20261020_¹¤ÐòÊÇ·ñÁìÁÏÓëÉú²úBOM½á¹¹ÁìÁϱê¼Ç.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
-- ============================================================================
-- å·¥åºå¢žåŠ â€œæ˜¯å¦é¢†æ–™â€æ ‡è®°ï¼Œå¹¶åŒæ­¥åˆ°ç”Ÿäº§BOM结构(生产订单领料台账使用)
--
-- èƒŒæ™¯ï¼š
--   ç”Ÿäº§è®¢å•的领料台账原来按 BOM çš„叶子节点反推需要领料的物料,BOM å±‚级一多,
--   é¢†æ–™ç»“果和实际情况对不上(BOM é‡Œä¸€å¤§å †ç‰©æ–™éœ€è¦é¢†æ–™ï¼Œç®—出来只剩几条)。
--   çŽ°æ”¹ä¸ºç”±å·¥åºä¸Šçš„â€œæ˜¯å¦é¢†æ–™â€æ ‡è®°å†³å®šé¢†æ–™èŒƒå›´ï¼š
--     1) å·¥åºè¡¨ technology_operation å¢žåŠ  is_pick(工序管理-新增/编辑工序 å‹¾é€‰â€œæ˜¯å¦é¢†æ–™â€ï¼‰ï¼›
--     2) ç”Ÿäº§è®¡åˆ’下发(生成生产订单快照)时,把工序的 is_pick å¸¦åˆ° production_bom_structure;
--     3) ç”Ÿäº§è®¢å•-领料台账按 is_pick = 1 çš„工序物料取数,读取时会用工序主数据的最新标记刷新快照值,
--        å› æ­¤å·¥åºä¸Šè°ƒæ•´â€œæ˜¯å¦é¢†æ–™â€åŽæ— éœ€é‡æ–°ä¸‹å‘计划即可生效;
--        production_bom_structure.is_pick ä¸»è¦ç”¨äºŽå¿«ç…§ç•™ç—•、报表和工序被删除后的兜底。
--
-- æ‰§è¡Œè¯´æ˜Žï¼š
--   1. è„šæœ¬å¯é‡å¤æ‰§è¡Œï¼Œåˆ—å·²å­˜åœ¨æ—¶ä¼šè‡ªåŠ¨è·³è¿‡ï¼›
--   2. å»ºè®®å…ˆæ‰§è¡Œæœ¬è„šæœ¬ï¼Œå†å‡çº§ç¨‹åºï¼Œæœ€åŽåœ¨å·¥åºç®¡ç†é‡Œå‹¾é€‰éœ€è¦é¢†æ–™çš„å·¥åºï¼›
--   3. ç¬¬ 3 æ­¥å›žå¡«ä»…用于把存量订单BOM快照里的 is_pick åˆ·æˆå·¥åºæœ€æ–°æ ‡è®°ï¼ˆæŠ¥è¡¨/追溯口径),
--      ä¸å½±å“é¢†æ–™å°è´¦çš„实时取数。
-- ============================================================================
-- ----------------------------------------------------------------------------
-- 1. å·¥åºè¡¨ï¼šå¢žåŠ â€œæ˜¯å¦é¢†æ–™â€å­—æ®µ
-- ----------------------------------------------------------------------------
SET
@col_exists = (
    SELECT COUNT(1)
    FROM information_schema.COLUMNS
    WHERE TABLE_SCHEMA = DATABASE()
      AND TABLE_NAME = 'technology_operation'
      AND COLUMN_NAME = 'is_pick'
);
SET
@ddl = IF(@col_exists = 0,
    'ALTER TABLE technology_operation ADD COLUMN is_pick TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否领料:0否,1是'' AFTER is_production',
    'SELECT ''technology_operation.is_pick å·²å­˜åœ¨ï¼Œè·³è¿‡'' AS msg');
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ----------------------------------------------------------------------------
-- 2. ç”Ÿäº§è®¢å•BOM结构表:增加“是否领料”字段(快照来自工序)
-- ----------------------------------------------------------------------------
SET
@col_exists = (
    SELECT COUNT(1)
    FROM information_schema.COLUMNS
    WHERE TABLE_SCHEMA = DATABASE()
      AND TABLE_NAME = 'production_bom_structure'
      AND COLUMN_NAME = 'is_pick'
);
SET
@ddl = IF(@col_exists = 0,
    'ALTER TABLE production_bom_structure ADD COLUMN is_pick TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否领料:来自工序,0否,1是'' AFTER technology_operation_id',
    'SELECT ''production_bom_structure.is_pick å·²å­˜åœ¨ï¼Œè·³è¿‡'' AS msg');
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ----------------------------------------------------------------------------
-- 3. å­˜é‡ç”Ÿäº§BOM结构回填:按工序当前的“是否领料”标记同步快照值
--    é¦–次执行(工序还没开始标记)时不会改变任何数据;
--    åŽç»­åœ¨å·¥åºç®¡ç†é‡Œå‹¾é€‰/取消“是否领料”后,可再次执行本语句,
--    è®©å·²ç»ä¸‹å‘的订单BOM快照同步最新标记(用于报表/追溯,领料台账读取时本身也会取工序最新标记)。
-- ----------------------------------------------------------------------------
UPDATE production_bom_structure pbs
    INNER JOIN technology_operation top1 ON top1.id = pbs.technology_operation_id
SET pbs.is_pick = IFNULL(top1.is_pick, 0)
WHERE IFNULL(pbs.is_pick, 0) <> IFNULL(top1.is_pick, 0);
-- å¯é€‰ï¼ˆæœ‰é£Žé™©ï¼Œé»˜è®¤ä¸æ‰§è¡Œï¼‰ï¼šæŠŠå­˜é‡å·¥åºå…¨éƒ¨æ ‡è®°ä¸ºéœ€è¦é¢†æ–™ã€‚
-- æ³¨æ„ï¼šæ‰§è¡ŒåŽï¼Œæ‰€æœ‰åŽ†å²è®¢å•çš„é¢†æ–™å°è´¦éƒ½ä¼šå˜æˆâ€œæŒ‰å…¨éƒ¨å·¥åºé¢†æ–™â€ï¼Œè¯·ç¡®è®¤å£å¾„åŽå†æ”¾å¼€ã€‚
-- UPDATE technology_operation SET is_pick = 1 WHERE is_pick = 0;
-- ----------------------------------------------------------------------------
-- 4. æ ¡éªŒSQL(按需执行,不影响数据)
-- ----------------------------------------------------------------------------
-- 4.1 æŸ¥çœ‹å“ªäº›å·¥åºåœ¨BOM结构中被使用,帮助决定给哪些工序开启“是否领料”
-- SELECT top1.id,
--        top1.no,
--        top1.name,
--        top1.is_pick,
--        COUNT(pbs.id) AS bom_row_count
-- FROM technology_operation top1
--          LEFT JOIN production_bom_structure pbs ON pbs.technology_operation_id = top1.id
-- GROUP BY top1.id, top1.no, top1.name, top1.is_pick
-- ORDER BY bom_row_count DESC;
-- 4.2 æŸ¥çœ‹æŸä¸ªç”Ÿäº§è®¢å•çš„BOM结构领料标记(把 :productionOrderId æ¢æˆå®žé™…订单ID)
-- SELECT pbs.id,
--        pbs.parent_id,
--        pbs.technology_operation_id,
--        top1.name AS operation_name,
--        pbs.product_model_id,
--        pm.model,
--        pbs.unit_quantity,
--        pbs.demanded_quantity,
--        pbs.is_pick
-- FROM production_bom_structure pbs
--          LEFT JOIN technology_operation top1 ON top1.id = pbs.technology_operation_id
--          LEFT JOIN product_model pm ON pm.id = pbs.product_model_id
-- WHERE pbs.production_order_id = :productionOrderId
-- ORDER BY pbs.id;
-- ----------------------------------------------------------------------------
-- 5. å›žæ»šï¼ˆå¦‚需):
-- ALTER TABLE production_bom_structure DROP COLUMN is_pick;
-- ALTER TABLE technology_operation DROP COLUMN is_pick;
-- ----------------------------------------------------------------------------
src/main/java/com/ruoyi/production/pojo/ProductionBomStructure.java
@@ -40,6 +40,9 @@
    @Schema(description = "工序id")
    private Long technologyOperationId;
    @Schema(description = "是否领料 æ¥æºå·¥åºçš„“是否领料”标记,领料台账只领取标记了领料的工序物料")
    private Boolean isPick;
    @Schema(description = "生产订单BOMid")
    private Long productionOrderBomId;
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
@@ -131,10 +131,15 @@
        List<ProductionBomStructure> updateList = new ArrayList<>();
        Map<String, ProductionBomStructure> tempEntityMap = new HashMap<>();
        Map<Long, Boolean> operationPickMap = loadOperationPickMap(flatDtoList.stream()
                .map(ProductionBomStructureDto::getTechnologyOperationId)
                .collect(Collectors.toList()));
        for (ProductionBomStructureDto item : flatDtoList) {
            ProductionBomStructure entity = new ProductionBomStructure();
            BeanUtils.copyProperties(item, entity);
            entity.setProductionOrderBomId(orderBomId);
            entity.setIsPick(operationPickMap.getOrDefault(item.getTechnologyOperationId(), Boolean.FALSE));
            if (item.getId() == null) {
                entity.setParentId(null);
                insertList.add(entity);
@@ -853,6 +858,26 @@
        return technologyOperationMapper.selectById(technologyOperationId);
    }
    /**
     * æ‰¹é‡è¯»å–工序的“是否领料”标记,保证生产BOM结构中的领料标记与工序主数据一致。
     */
    private Map<Long, Boolean> loadOperationPickMap(List<Long> technologyOperationIds) {
        if (technologyOperationIds == null || technologyOperationIds.isEmpty()) {
            return Collections.emptyMap();
        }
        Set<Long> operationIdSet = technologyOperationIds.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (operationIdSet.isEmpty()) {
            return Collections.emptyMap();
        }
        return technologyOperationMapper.selectBatchIds(operationIdSet).stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(TechnologyOperation::getId,
                        item -> Boolean.TRUE.equals(item.getIsPick()),
                        (left, right) -> left));
    }
    private String generateNextTaskNo() {
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        ProductionOperationTask latestTask = productionOperationTaskMapper.selectOne(
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -371,6 +371,9 @@
        productionOrderBomMapper.insert(orderBom);
        Map<Long, Long> idMap = new HashMap<>();
        Map<Long, Boolean> operationPickMap = loadOperationPickMap(structureList.stream()
                .map(TechnologyBomStructure::getOperationId)
                .collect(Collectors.toList()));
        for (TechnologyBomStructure source : structureList) {
            // å­èŠ‚ç‚¹ parentId éœ€è¦æ˜ å°„成新快照节点 id,才能保留原始 BOM å±‚级。
            ProductionBomStructure target = new ProductionBomStructure();
@@ -379,6 +382,7 @@
            target.setParentId(source.getParentId() == null ? null : idMap.get(source.getParentId()));
            target.setProductModelId(source.getProductModelId());
            target.setTechnologyOperationId(source.getOperationId());
            target.setIsPick(operationPickMap.getOrDefault(source.getOperationId(), Boolean.FALSE));
            target.setUnitQuantity(source.getUnitQuantity());
            target.setDemandedQuantity(defaultDecimal(orderQuantity)
                    .multiply(defaultDecimal(source.getUnitQuantity())));
@@ -387,6 +391,26 @@
            idMap.put(source.getId(), target.getId());
        }
        return orderBom;
    }
    /**
     * æ‰¹é‡è¯»å–工序的“是否领料”标记,避免BOM快照逐行查询工序表。
     */
    private Map<Long, Boolean> loadOperationPickMap(List<Long> technologyOperationIds) {
        if (technologyOperationIds == null || technologyOperationIds.isEmpty()) {
            return Collections.emptyMap();
        }
        Set<Long> operationIdSet = technologyOperationIds.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (operationIdSet.isEmpty()) {
            return Collections.emptyMap();
        }
        return technologyOperationMapper.selectBatchIds(operationIdSet).stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(TechnologyOperation::getId,
                        item -> Boolean.TRUE.equals(item.getIsPick()),
                        (left, right) -> left));
    }
    private void clearProductionSnapshot(Long productionOrderId) {
@@ -1076,32 +1100,62 @@
            }
        }
        // æ”¶é›†è®¢å•成品规格ID:包含生产订单成品规格与BOM根节点规格,用于排除“自己”这一物料
        // production_bom_structure.is_pick æ˜¯ç”Ÿäº§è®¡åˆ’下发时的工序领料标记快照;
        // é¢†æ–™å°è´¦è¯»å–时以工序主数据的最新标记为准,保证工序上的“是否领料”调整后无需重新下发即可生效;
        // å·¥åºå·²è¢«åˆ é™¤æ—¶ä¿ç•™å¿«ç…§å€¼ï¼Œé¿å…åŽ†å²è®¢å•çš„é¢†æ–™èŒƒå›´ä¸¢å¤±ã€‚
        Map<Long, Boolean> operationPickMap = loadOperationPickMap(bomStructureList.stream()
                .filter(Objects::nonNull)
                .map(ProductionBomStructureVo::getTechnologyOperationId)
                .collect(Collectors.toList()));
        for (ProductionBomStructureVo structure : bomStructureList) {
            if (structure == null || structure.getTechnologyOperationId() == null) {
                continue;
            }
            Boolean latestPick = operationPickMap.get(structure.getTechnologyOperationId());
            if (latestPick != null) {
                structure.setIsPick(latestPick);
            }
        }
        // æ”¶é›†è®¢å•成品规格ID:生产订单成品规格 + è®¢å•BOM快照的根节点规格,用于排除“自己”这一物料。
        // æ³¨æ„ï¼šBOM结构里领料物料本身也可能是 parent_id ä¸ºç©ºçš„独立节点,这里不能再把所有根节点都当成成品排除,
        // å¦åˆ™â€œé¢†æ–™â€å·¥åºä¸‹çš„物料会被整体过滤掉。
        Set<Long> finishedProductModelIds = new HashSet<>();
        if (productionOrder != null && productionOrder.getProductModelId() != null) {
            finishedProductModelIds.add(productionOrder.getProductModelId());
        }
        bomStructureList.stream()
                .filter(s -> s != null && (s.getParentId() == null || s.getParentId() == 0))
                .map(ProductionBomStructureVo::getProductModelId)
                .filter(Objects::nonNull)
                .forEach(finishedProductModelIds::add);
        if (orderBom.getProductModelId() != null) {
            finishedProductModelIds.add(orderBom.getProductModelId());
        }
        // æ”¶é›†æ‰€æœ‰è¢«å¼•用为父级的节点ID:BOM从最后一道工序往里配置前一道工序,
        // ä¸­é—´èŠ‚ç‚¹æ˜¯ä¸Šä¸€é“å·¥åºçš„äº§å‡ºï¼ˆè‡ªåˆ¶ï¼‰ï¼Œåªæœ‰æ¯æ¡åˆ†æ”¯æœ€é‡Œå±‚çš„å¶å­èŠ‚ç‚¹æ‰æ˜¯çœŸæ­£éœ€è¦é¢†ç”¨çš„ç‰©æ–™ã€‚
        // ä¸­é—´èŠ‚ç‚¹æ˜¯ä¸Šä¸€é“å·¥åºçš„äº§å‡ºï¼ˆè‡ªåˆ¶ï¼‰ï¼Œæ—§å£å¾„åªæœ‰æ¯æ¡åˆ†æ”¯æœ€é‡Œå±‚çš„å¶å­èŠ‚ç‚¹æ‰æ˜¯çœŸæ­£éœ€è¦é¢†ç”¨çš„ç‰©æ–™ã€‚
        Set<Long> parentNodeIds = bomStructureList.stream()
                .filter(s -> s != null && s.getParentId() != null && s.getParentId() != 0)
                .map(ProductionBomStructureVo::getParentId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        // è¿‡æ»¤å‡ºå¶å­èŠ‚ç‚¹ï¼ˆå®žé™…é¢†æ–™é¡¹ï¼‰ï¼šéžæ ¹èŠ‚ç‚¹ã€éžä¸­é—´èŠ‚ç‚¹ï¼Œå¹¶æŽ’é™¤ä¸Žè®¢å•æˆå“åŒè§„æ ¼çš„ç‰©æ–™ï¼ˆè‡ªåˆ¶æˆå“æ— éœ€é¢†æ–™ï¼‰
        List<ProductionBomStructureVo> childStructureList = bomStructureList.stream()
                .filter(s -> s != null && s.getParentId() != null && s.getParentId() != 0)
                .filter(s -> s.getId() == null || !parentNodeIds.contains(s.getId()))
        // é¢†æ–™èŒƒå›´ä»¥å·¥åºçš„“是否领料”标记为准:只有标记了领料的工序,其消耗的物料才进入领料台账。
        // æ³¨æ„ï¼šé¢†æ–™ç‰©æ–™åœ¨BOM结构中可能是独立节点(parent_id ä¸ºç©ºï¼‰ï¼Œæ‰€ä»¥è¿™é‡Œä¸é™åˆ¶å±‚级,只看工序标记;
        // åªæŽ’除与订单成品同规格的物料(成品本身无需领料)。
        List<ProductionBomStructureVo> markedPickStructureList = bomStructureList.stream()
                .filter(s -> s != null && Boolean.TRUE.equals(s.getIsPick()))
                .filter(s -> s.getProductModelId() == null || !finishedProductModelIds.contains(s.getProductModelId()))
                .collect(Collectors.toList());
        // å®žé™…领料项:优先使用工序领料标记;
        // å…¼å®¹åŽ†å²æ•°æ®ï¼šå½“è¯¥è®¢å•BOM结构里没有任何工序标记领料时,回退为原来的“叶子节点领料”规则,
        // é¿å…å°šæœªå¯ç”¨â€œæ˜¯å¦é¢†æ–™â€æ ‡è®°çš„客户打开领料台账时数据突然为空。
        List<ProductionBomStructureVo> childStructureList = markedPickStructureList;
        if (childStructureList.isEmpty()) {
            childStructureList = bomStructureList.stream()
                    .filter(s -> s != null && s.getParentId() != null && s.getParentId() != 0)
                    .filter(s -> s.getId() == null || !parentNodeIds.contains(s.getId()))
                    .filter(s -> s.getProductModelId() == null || !finishedProductModelIds.contains(s.getProductModelId()))
                    .collect(Collectors.toList());
        }
        // éåŽ†å¤„ç†æ•°æ®å¹¶ç»„è£…ç»“æžœ
        List<Long> productModelIds = childStructureList.stream()
                .map(ProductionBomStructureVo::getProductModelId)
src/main/java/com/ruoyi/technology/pojo/TechnologyOperation.java
@@ -65,4 +65,7 @@
    @Schema(description = "是否生产")
    private Boolean isProduction;
    @Schema(description = "是否领料 æ ‡è®°è¯¥å·¥åºæ˜¯å¦éœ€è¦é¢†æ–™")
    private Boolean isPick;
}
src/main/resources/mapper/production/ProductionBomStructureMapper.xml
@@ -9,6 +9,7 @@
        <result column="production_order_id" property="productionOrderId" />
        <result column="product_model_id" property="productModelId" />
        <result column="technology_operation_id" property="technologyOperationId" />
        <result column="is_pick" property="isPick" />
        <result column="production_order_bom_id" property="productionOrderBomId" />
        <result column="unit_quantity" property="unitQuantity" />
        <result column="demanded_quantity" property="demandedQuantity" />
src/main/resources/mapper/technology/TechnologyOperationMapper.xml
@@ -12,6 +12,8 @@
        <result column="remark" property="remark" />
        <result column="salary_quota" property="salaryQuota" />
        <result column="is_quality" property="isQuality" />
        <result column="is_production" property="isProduction" />
        <result column="is_pick" property="isPick" />
        <result column="type" property="type" />
        <result column="device_ledger_id" property="deviceLedgerId" />
        <result column="create_user" property="createUser" />