""" 第四步:将bcl计算结果补充到json文件中 """ import json import os from copy import deepcopy import uuid import re class ProjectExpenseProcessor: def __init__(self): self.project_data = None def initialize_project_data(self, project_data): """初始化项目数据,深拷贝避免修改原始数据""" self.project_data = deepcopy(project_data) if project_data else {} # 添加调试信息 print(f"项目数据结构: {list(self.project_data.keys())}") # 检查projectData是否存在 if self.project_data and "projectData" in self.project_data: print("projectData 结构存在") print(f"projectData 子结构: {list(self.project_data['projectData'].keys())}") # 检查expensePreview是否存在于projectData中 if "expensePreview" in self.project_data["projectData"]: print("expensePreview 结构存在于 projectData 中") expense_preview = self.project_data["projectData"]["expensePreview"] print(f"expensePreview 类别: {list(expense_preview.keys())}") for category_name, category in expense_preview.items(): print(f"类别: {category_name}, 类型: {type(category)}") # 处理category是字典的情况 if isinstance(category, dict): for group_name, group in category.items(): print(f" 组: {group_name}, 类型: {type(group)}") if isinstance(group, list): for i, item in enumerate(group): if isinstance(item, dict) and "GUID" in item: print(f" 项目 {i} GUID: {item['GUID']}") # 处理category是列表的情况 elif isinstance(category, list): print(f" 类别 {category_name} 是列表类型,包含 {len(category)} 个项目") for i, item in enumerate(category): if isinstance(item, dict) and "GUID" in item: print(f" 项目 {i} GUID: {item['GUID']}") else: print("警告: expensePreview 结构不存在于 projectData 中!") else: print("警告: projectData 结构不存在!") return self def check_guids_in_division(self, node=None, path=""): """检查项目划分中的所有GUID""" if node is None: if not self.project_data or "projectData" not in self.project_data: print("警告: project_data 为空或不包含 projectData") return project_division = self.project_data.get("projectDivision", {}) if isinstance(project_division, dict): for category_name, category in project_division.items(): self.check_guids_in_division(category, category_name) return if isinstance(node, list): for i, item in enumerate(node): self.check_guids_in_division(item, f"{path}[{i}]") elif isinstance(node, dict): if ( "GUID" in node or "guid" in node or ( "id" in node and isinstance(node["id"], str) and node["id"].startswith("{") and node["id"].endswith("}") ) ): guid = self.get_node_guid(node) if guid: guid_with_braces = "{" + guid + "}" print(f"项目划分GUID: {guid_with_braces} 在路径: {path}") # 尝试在费用预览中查找 expense_node = self.find_expense_preview_node(guid) if expense_node: print(f" ✓ 在费用预览中找到对应节点") else: print(f" ✗ 在费用预览中未找到对应节点") if "children" in node: self.check_guids_in_division(node["children"], f"{path}.children") def find_project_division_node(self, target_guid, node=None, path=""): """查找指定GUID的最子级项目划分节点""" # 将目标GUID转换为大写并去掉花括号 target_guid = target_guid.strip("{}").upper() if node is None: # 从 projectData.projectDivision 开始搜索 if not self.project_data or "projectData" not in self.project_data: print("警告: project_data 为空或不包含 projectData") return None if "projectDivision" not in self.project_data["projectData"]: print("警告: project_data 不包含 projectData.projectDivision") return None # print(f"开始在项目划分中查找GUID: {target_guid}") for category_name, category in self.project_data["projectData"]["projectDivision"].items(): # print(f"搜索项目划分类别: {category_name}") # 递归处理category,无论它是什么类型 result = self.find_project_division_node( target_guid, category, f"projectData.projectDivision.{category_name}" ) if result: return result # print(f"在项目划分中未找到GUID: {target_guid}") return None if isinstance(node, list): for i, item in enumerate(node): new_path = f"{path}[{i}]" result = self.find_project_division_node(target_guid, item, new_path) if result: return result elif isinstance(node, dict): # 检查当前节点的GUID current_guid = self.get_node_guid(node) if current_guid: # print(f"比较项目划分GUID: {current_guid} vs {target_guid} 在路径: {path}") pass if current_guid == target_guid: # print(f"找到匹配的项目划分GUID: {current_guid} 在路径: {path}") return node # 递归检查所有子节点,包括children和其他字典值 for key, value in node.items(): if isinstance(value, (dict, list)): new_path = f"{path}.{key}" result = self.find_project_division_node(target_guid, value, new_path) if result: return result return None def find_expense_preview_node(self, target_guid, node=None, path=""): """ 在 expensePreview 中递归查找指定 GUID 的节点 :param target_guid: 要查找的 GUID(字符串,不带 {}) :param node: 当前查找的子节点(默认从 project_data 开始) :param path: 当前搜索路径(用于调试) :return: 找到的节点或 None """ # 将目标GUID转换为大写并去掉花括号 target_guid = target_guid.strip("{}").upper() if ( not self.project_data or "projectData" not in self.project_data or "expensePreview" not in self.project_data["projectData"] ): print("警告: project_data 为空或不包含 projectData.expensePreview") return None # 初始调用时从顶层开始 if node is None: # print(f"开始查找GUID: {target_guid}") expense_preview = self.project_data["projectData"]["expensePreview"] for category_name, category in expense_preview.items(): # 处理category是字典的情况 if isinstance(category, dict): for group_name, group in category.items(): if isinstance(group, list): for i, item in enumerate(group): new_path = f"projectData.expensePreview.{category_name}.{group_name}[{i}]" result = self.find_expense_preview_node(target_guid, item, new_path) if result: return result # 处理category是列表的情况 elif isinstance(category, list): for i, item in enumerate(category): new_path = f"projectData.expensePreview.{category_name}[{i}]" result = self.find_expense_preview_node(target_guid, item, new_path) if result: return result # print(f"在顶层搜索中未找到GUID: {target_guid}") return None # 检查当前节点 current_guid = self.get_node_guid(node) if current_guid: # print(f"比较GUID: {current_guid} vs {target_guid} 在路径: {path}") pass if current_guid == target_guid: # print(f"找到匹配的GUID: {current_guid} 在路径: {path}") return node # 递归检查子节点 if "children" in node and isinstance(node["children"], list): for i, child in enumerate(node["children"]): new_path = f"{path}.children[{i}]" result = self.find_expense_preview_node(target_guid, child, new_path) if result: return result return None @staticmethod def generate_id(text): """生成简单的ID(只保留字母数字和中文)""" return "".join(c for c in text if c.isalnum() or "\u4e00" <= c <= "\u9fa5").upper() def convert_calculation_results_to_children(self, calculation_results): """将工程量计算结果转换为标准格式""" children = [] for node_name, costs in calculation_results.items(): node_data = {"name": node_name, "type": "工程量节点", "children": []} for cost_type, amount in costs.items(): node_data["children"].append( { "id": f"{cost_type}_{self.generate_id(cost_type)}", "cost": str(amount), } ) children.append(node_data) return children def generate_guid(self): """生成新的GUID""" return "{" + str(uuid.uuid4()).upper() + "}" @staticmethod def _determine_project_type(project_data): """ 根据division字段判断工程类型 :param project_data: 项目数据 :return: 'inventory' 表示清单工程,'budget' 表示预算工程 """ # 清单工程关键词 inventory_keywords = ["清单", "结算", "招标控制价", "招投标工程", "清单计价"] # 预算工程关键词 budget_keywords = ["概预算", "定额", "定额计价", "概算", "概预算工程"] # 尝试从数据中获取division字段 division = None if "division" in project_data: division = project_data["division"] parts = division.split("-") # 如果找到division字段 if division: # 去掉"主网-"前缀 if len(parts) == 2: division_type = parts[1].strip() else: division_type = parts[2].strip() # 判断是否为清单工程 for keyword in inventory_keywords: if keyword in division_type: print(f"根据division字段 '{division}' 判断为清单工程") return "inventory" # 判断是否为预算工程 for keyword in budget_keywords: if keyword in division_type: print(f"根据division字段 '{division}' 判断为预算工程") return "budget" # 如果无法通过division字段判断,则尝试通过数据结构判断 is_inventory_project = False for key in project_data.keys(): if re.search(r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", key, re.IGNORECASE): is_inventory_project = True print("通过数据结构判断为清单工程") break return "inventory" if is_inventory_project else "budget" def add_quantity_node_expense_data(self, project_guid, calculation_results): """添加工程量节点费用预览数据""" try: # 1. 查找项目划分节点 division_node = self.find_project_division_node(project_guid) if not division_node: print(f"未找到GUID为 {project_guid} 的项目划分节点") return False # 2. 查找费用预览节点 expense_node = self.find_expense_preview_node(project_guid) if not expense_node: print(f"未找到GUID为 {project_guid} 的费用预览节点") return False # 3. 确保节点有标准格式 self.ensure_standard_format(expense_node) # 4. 判断工程类型 project_type = ProjectExpenseProcessor._determine_project_type(calculation_results) if project_type == "inventory": # 清单工程处理逻辑 print("使用清单工程处理逻辑") return self._process_inventory_project(division_node, expense_node, calculation_results) else: # 预算工程处理逻辑 print("使用预算工程处理逻辑") return self._process_budget_project(division_node, expense_node, calculation_results) except Exception as e: print(f"添加工程量节点费用预览数据失败: {str(e)}") import traceback traceback.print_exc() return False def _process_budget_project(self, division_node, expense_node, calculation_results): """处理预算工程的费用数据""" try: # 清空children准备添加工程量节点 expense_node["children"] = [] # 处理每个工程量计算结果 for node_name, costs in calculation_results.items(): # 查找项目划分中对应的工程量节点 quantity_nodes = [] # 修改为列表,存储所有匹配的节点 # 检查node_name是否包含GUID格式 guid_match = re.search( r"_([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})$", node_name, re.IGNORECASE, ) node_base_name = node_name node_guid = None node_id = None # 如果节点名称包含GUID格式 if guid_match: node_guid = guid_match.group(1) node_base_name = node_name[: node_name.rfind("_")] print(f"从名称中提取GUID: {node_guid}, 基础名称: {node_base_name}") else: # 检查是否为"工程量名称_id"格式(纯数字ID) node_name_parts = node_name.split("_") if len(node_name_parts) > 1 and node_name_parts[-1].isdigit(): node_id = node_name_parts[-1] node_base_name = "_".join(node_name_parts[:-1]) print(f"从名称中提取ID: {node_id}, 基础名称: {node_base_name}") if "children" in division_node: guid_match_nodes = [] # 存储GUID匹配的节点 exact_match_nodes = [] # 存储完全匹配的节点 id_match_nodes = [] # 存储ID匹配的节点 name_match_nodes = [] # 存储基础名称匹配的节点 # 如果从名称中提取了GUID,先尝试为没有GUID的节点添加这个GUID if node_guid: for child in division_node["children"]: child_name = child.get("项目名称") or child.get("name", "") # 如果节点名称与基础名称匹配,但没有任何形式的GUID,则添加从名称中提取的GUID if child_name == node_base_name and self.get_node_guid(child) is None: self.set_node_guid(child, node_guid) print(f"为匹配的节点 {child_name} 添加从名称中提取的GUID: {node_guid}") for child in division_node["children"]: child_name = child.get("项目名称") or child.get("name", "") child_id = child.get("id", "") child_guid = self.get_node_guid(child) # 情况0: GUID匹配(最高优先级) if node_guid and child_guid and node_guid.upper() == child_guid: guid_match_nodes.append(child) print(f"找到GUID匹配的工程量节点: {child_name} (GUID: {child_guid})") # 情况1: 完全匹配(次高优先级) if child_name == node_name: exact_match_nodes.append(child) # 情况2: 检查项目划分节点名称是否为"工程量名称_id"格式 child_name_parts = child_name.split("_") child_base_name = child_name child_id_from_name = None if len(child_name_parts) > 1 and child_name_parts[-1].isdigit(): child_id_from_name = child_name_parts[-1] child_base_name = "_".join(child_name_parts[:-1]) # 情况3: 名称和ID都匹配 if node_id and (child_id == node_id or child_id_from_name == node_id): if child_base_name == node_base_name: id_match_nodes.append(child) # 情况4: 基础名称匹配(最低优先级) elif child_base_name == node_base_name: name_match_nodes.append(child) # 按优先级选择匹配节点 if guid_match_nodes: quantity_nodes = guid_match_nodes print(f"使用GUID匹配的工程量节点: {node_name}, 找到 {len(guid_match_nodes)} 个节点") elif exact_match_nodes: quantity_nodes = exact_match_nodes print(f"使用完全匹配的工程量节点: {node_name}, 找到 {len(exact_match_nodes)} 个节点") elif id_match_nodes: quantity_nodes = id_match_nodes print(f"使用ID匹配的工程量节点: {node_name} (ID: {node_id}), 找到 {len(id_match_nodes)} 个节点") elif name_match_nodes: quantity_nodes = name_match_nodes print(f"使用基础名称匹配的工程量节点: {node_base_name}, 找到 {len(name_match_nodes)} 个节点") if not quantity_nodes: print(f"未找到名称为 {node_name} 的工程量节点") continue # 处理所有匹配的节点 for quantity_node in quantity_nodes: # 如果工程量节点没有GUID,使用从名称中提取的GUID或生成一个新的 existing_guid = self.get_node_guid(quantity_node) if existing_guid is None: if node_guid: self.set_node_guid(quantity_node, node_guid) print(f"使用名称中的GUID: {node_guid}") else: new_guid = self.generate_guid() self.set_node_guid(quantity_node, new_guid) print(f"生成新的GUID: {new_guid}") else: print(f"使用节点已有的GUID: {existing_guid}") # 使用get_node_guid确保获取正确的GUID,无论是大写还是小写 quantity_guid = self.get_node_guid(quantity_node) if not quantity_guid: # 这种情况不应该发生,因为前面已经确保了节点有GUID print(f"警告: 节点 {node_name} 没有GUID") continue # 创建工程量费用预览节点(使用标准格式) quantity_expense = {"GUID": "{" + quantity_guid.upper() + "}", "sum": [], "children": [], "rcj": []} # 添加费用项到sum for cost_type, amount in costs.items(): quantity_expense["sum"].append({"id": f"{cost_type}", "cost": str(amount)}) # 将工程量费用预览节点添加到项目划分费用预览的children中 expense_node["children"].append(quantity_expense) print(f"成功添加GUID为 {division_node.get('GUID', '')} 的工程量节点费用预览数据") return True except Exception as e: print(f"处理预算工程费用数据失败: {str(e)}") return False def _process_inventory_project(self, division_node, expense_node, calculation_results): """处理清单工程的费用数据""" try: # 确保expense_node有children数组 if "children" not in expense_node: expense_node["children"] = [] # 从calculation_results中移除division字段(如果存在) if "division" in calculation_results: division_value = calculation_results.pop("division") print(f"移除division字段: {division_value}") # 保存原有的清单级节点 existing_inventory_nodes = {} if "children" in expense_node: for child in expense_node["children"]: if "GUID" in child: existing_inventory_nodes[child["GUID"].strip("{}").upper()] = child # 创建清单级节点字典,用于存储新的或更新的清单级节点 inventory_nodes = {} # 处理计算结果中的每个清单级节点 for inventory_key, inventory_data in calculation_results.items(): # 跳过非清单级节点 if inventory_key == "division": continue # 从清单键中提取GUID inventory_guid_match = re.search( r"([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})", inventory_key, re.IGNORECASE ) if not inventory_guid_match: print(f"无法从键 {inventory_key} 中提取GUID,跳过") continue inventory_guid = inventory_guid_match.group(1) # 在项目划分中查找对应的清单节点 inventory_node = self._find_node_by_guid(division_node, inventory_guid) if not inventory_node: print(f"未找到GUID为 {inventory_guid} 的清单节点") continue # 获取或创建清单级费用节点 inventory_guid_upper = inventory_guid.strip("{}").upper() if inventory_guid_upper in existing_inventory_nodes: # 使用现有的清单级节点 inventory_expense = existing_inventory_nodes[inventory_guid_upper] print(f"使用现有的清单级节点: {inventory_guid}") else: # 创建新的清单级费用节点 inventory_expense = {"GUID": "{" + inventory_guid_upper + "}", "sum": [], "children": [], "rcj": []} print(f"创建新的清单级节点: {inventory_guid}") # 确保清单级费用节点有标准格式 self.ensure_standard_format(inventory_expense) # 创建工程量级节点字典,用于存储新的或更新的工程量级节点 quantity_nodes = {} # 如果清单级节点已有工程量级节点,保存它们 if "children" in inventory_expense: for child in inventory_expense["children"]: if "GUID" in child: quantity_nodes[child["GUID"].strip("{}").upper()] = child # 处理清单下的每个工程量节点 for quantity_name, quantity_costs in inventory_data.items(): # 检查quantity_name是否包含GUID格式 guid_match = re.search( r"_([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})$", quantity_name, re.IGNORECASE, ) quantity_base_name = quantity_name quantity_guid = None quantity_id = None # 如果节点名称包含GUID格式 if guid_match: quantity_guid = guid_match.group(1) quantity_base_name = quantity_name[: quantity_name.rfind("_")] print(f"从名称中提取GUID: {quantity_guid}, 基础名称: {quantity_base_name}") else: # 检查是否为"工程量名称_id"格式(纯数字ID) quantity_name_parts = quantity_name.split("_") if len(quantity_name_parts) > 1 and quantity_name_parts[-1].isdigit(): quantity_id = quantity_name_parts[-1] quantity_base_name = "_".join(quantity_name_parts[:-1]) print(f"从名称中提取ID: {quantity_id}, 基础名称: {quantity_base_name}") # 在清单节点的children中查找工程量节点 quantity_node = None guid_match_node = None exact_match = None id_match = None name_match = None if "children" in inventory_node: # 如果从名称中提取了GUID,先尝试为没有GUID的节点添加这个GUID if quantity_guid: for child in inventory_node["children"]: child_name = child.get("项目名称") or child.get("name", "") # 如果节点名称与基础名称匹配,但没有任何形式的GUID,则添加从名称中提取的GUID if child_name == quantity_base_name and self.get_node_guid(child) is None: self.set_node_guid(child, quantity_guid) print(f"为匹配的节点 {child_name} 添加从名称中提取的GUID: {quantity_guid}") for child in inventory_node["children"]: child_name = child.get("项目名称") or child.get("name", "") child_id = child.get("id", "") child_guid = self.get_node_guid(child) # 情况0: GUID匹配(最高优先级) if quantity_guid and child_guid and quantity_guid.upper() == child_guid: guid_match_node = child print(f"找到GUID匹配的工程量节点: {child_name} (GUID: {child_guid})") break # 找到GUID匹配,立即使用 # 情况1: 完全匹配(次高优先级) if child_name == quantity_name: exact_match = child # 不立即break,继续寻找可能的GUID匹配 # 情况2: 检查项目划分节点名称是否为"工程量名称_id"格式 child_name_parts = child_name.split("_") child_base_name = child_name child_id_from_name = None if len(child_name_parts) > 1 and child_name_parts[-1].isdigit(): child_id_from_name = child_name_parts[-1] child_base_name = "_".join(child_name_parts[:-1]) # 情况3: 名称和ID都匹配 if quantity_id and (child_id == quantity_id or child_id_from_name == quantity_id): if child_base_name == quantity_base_name: id_match = child # 不立即break,继续寻找可能的更精确匹配 # 情况4: 基础名称匹配(最低优先级) elif child_base_name == quantity_base_name: name_match = child # 不立即break,继续寻找更精确的匹配 # 按优先级选择最佳匹配 if guid_match_node: quantity_node = guid_match_node print(f"使用GUID匹配的工程量节点: {quantity_name}") elif exact_match: quantity_node = exact_match print(f"使用完全匹配的工程量节点: {quantity_name}") elif id_match: quantity_node = id_match print(f"使用ID匹配的工程量节点: {quantity_name} (ID: {quantity_id})") elif name_match: quantity_node = name_match print(f"使用基础名称匹配的工程量节点: {quantity_base_name}") if not quantity_node: print(f"未找到名称为 {quantity_name} 的工程量节点") continue # 如果工程量节点没有GUID,使用从名称中提取的GUID或生成一个新的 existing_guid = self.get_node_guid(quantity_node) if existing_guid is None: if quantity_guid: self.set_node_guid(quantity_node, quantity_guid) print(f"使用名称中的GUID: {quantity_guid}") else: new_guid = self.generate_guid() self.set_node_guid(quantity_node, new_guid) print(f"生成新的GUID: {new_guid}") else: print(f"使用节点已有的GUID: {existing_guid}") # 使用get_node_guid确保获取正确的GUID,无论是大写还是小写 quantity_guid = self.get_node_guid(quantity_node) if quantity_guid: quantity_guid_upper = quantity_guid else: # 这种情况不应该发生,因为前面已经确保了节点有GUID print(f"警告: 节点 {quantity_name} 没有GUID") continue # 获取或创建工程量级费用节点 if quantity_guid_upper in quantity_nodes: # 使用现有的工程量级节点 quantity_expense = quantity_nodes[quantity_guid_upper] print(f"使用现有的工程量级节点: {quantity_guid}") else: # 创建新的工程量级费用节点 quantity_expense = {"GUID": quantity_guid, "sum": [], "children": [], "rcj": []} print(f"创建新的工程量级节点: {quantity_guid}") # 添加费用项到sum for cost_type, amount in quantity_costs.items(): quantity_expense["sum"].append({"id": f"{cost_type}", "cost": str(amount)}) # 将工程量级费用节点添加到字典 quantity_nodes[quantity_guid_upper] = quantity_expense # 将工程量级费用节点添加到清单级费用节点 inventory_expense["children"] = list(quantity_nodes.values()) # 将清单级费用节点添加到字典 inventory_nodes[inventory_guid_upper] = inventory_expense # 将所有清单级费用节点添加到项目级费用预览的children中 # 注意:这里不覆盖原有的children,而是合并新旧节点 for guid, node in inventory_nodes.items(): # 如果已经存在于existing_inventory_nodes中,则已经处理过 if guid not in existing_inventory_nodes: expense_node["children"].append(node) print(f"成功添加GUID为 {division_node.get('GUID', '')} 的清单工程费用预览数据") return True except Exception as e: print(f"处理清单工程费用数据失败: {str(e)}") import traceback traceback.print_exc() return False def _find_node_by_guid(self, node, target_guid): """在节点及其子节点中查找指定GUID的节点""" # 标准化GUID格式 target_guid = target_guid.strip("{}").upper() if isinstance(node, dict): # 检查当前节点 node_guid = self.get_node_guid(node) if node_guid == target_guid: return node # 检查子节点 if "children" in node and isinstance(node["children"], list): for child in node["children"]: result = self._find_node_by_guid(child, target_guid) if result: return result elif isinstance(node, list): # 检查列表中的每个项 for item in node: result = self._find_node_by_guid(item, target_guid) if result: return result return None def add_labor_material_machine_expense_data(self, project_guid, lmm_data): """添加人材机节点费用数据到rcj数组""" try: # 1. 查找项目划分节点 division_node = self.find_project_division_node(project_guid) if not division_node: print(f"未找到GUID为 {project_guid} 的项目划分节点") return False # 2. 查找费用预览节点 expense_node = self.find_expense_preview_node(project_guid) if not expense_node: print(f"未找到GUID为 {project_guid} 的费用预览节点") return False # 3. 确保节点有标准格式 self.ensure_standard_format(expense_node) # 4. 确保rcj数组存在 if "rcj" not in expense_node: expense_node["rcj"] = [] # 4. 处理人材机数据 # 添加人工节点 if "人工节点" in lmm_data: for item in lmm_data["人工节点"]: expense_node["rcj"].append( { "type": "人工", "编码": item.get("编码", ""), "名称": item.get("名称", ""), "单位": item.get("单位", ""), "预算价不含税": item.get("预算价不含税", ""), "市场价不含税": item.get("市场价不含税", ""), "预算价合价": item.get("预算价合价", ""), "市场价合价": item.get("市场价合价", ""), "价差": item.get("价差", ""), "数量": item.get("数量", ""), } ) # 添加材料节点 if "材料节点" in lmm_data: for item in lmm_data["材料节点"]: expense_node["rcj"].append( { "type": "材料", "供货方": item.get("供货方", ""), "编码": item.get("编码", ""), "名称": item.get("名称", ""), "单位": item.get("单位", ""), "预算价不含税": item.get("预算价不含税", ""), "市场价不含税": item.get("市场价不含税", ""), "预算价合价": item.get("预算价合价", ""), "市场价合价": item.get("市场价合价", ""), "价差": item.get("价差", ""), "数量": item.get("数量", ""), } ) # 添加机械节点 if "机械节点" in lmm_data: for item in lmm_data["机械节点"]: expense_node["rcj"].append( { "type": "机械", "编码": item.get("编码", ""), "名称": item.get("名称", ""), "单位": item.get("单位", ""), "预算价不含税": item.get("预算价不含税", ""), "市场价不含税": item.get("市场价不含税", ""), "预算价合价": item.get("预算价合价", ""), "市场价合价": item.get("市场价合价", ""), "价差": item.get("价差", ""), "数量": item.get("数量", ""), } ) print(f"成功添加GUID为 {project_guid} 的人材机节点费用预览数据到rcj数组") return True except Exception as e: print(f"添加人材机节点费用预览数据失败: {str(e)}") return False def batch_process_expense_data(self, data_list): """ 批量处理多个项目的费用预览数据 :param data_list: 包含项目GUID和费用数据的列表 :return: 是否全部成功处理 """ all_success = True for data in data_list: project_guid = data.get("projectGuid") calculation_results = data.get("calculationResults") labor_material_machine_data = data.get("laborMaterialMachineData") if calculation_results: success1 = self.add_quantity_node_expense_data(project_guid, calculation_results) all_success &= success1 if labor_material_machine_data: success2 = self.add_labor_material_machine_expense_data(project_guid, labor_material_machine_data) all_success &= success2 return all_success def get_node_guid(self, node): """获取节点的GUID,无论是大写还是小写""" if "GUID" in node: return node["GUID"].strip("{}").upper() elif "guid" in node: return node["guid"].strip("{}").upper() elif "id" in node and node["id"].startswith("{") and node["id"].endswith("}"): # 如果id是GUID格式 return node["id"].strip("{}").upper() return None def set_node_guid(self, node, guid_value): """设置节点的GUID,统一使用大写GUID,并保留原有的guid和id(如果它们是GUID格式)""" # 确保guid_value是标准格式(带花括号的大写GUID) if not guid_value.startswith("{"): guid_value = "{" + guid_value if not guid_value.endswith("}"): guid_value = guid_value + "}" guid_value = guid_value.upper() # 设置GUID属性(大写) node["GUID"] = guid_value # 如果已有小写guid属性,保持不变 # 如果已有id属性且是GUID格式,保持不变 # 这样可以保留原有的属性,避免数据丢失 return node def batch_process_from_folder(self, folder_path, original_division=""): """ 批量处理指定文件夹下的工程量和人材机JSON文件 :param folder_path: 包含JSON文件的文件夹路径 :param original_division: 原始文件中的division字段 :return: 成功处理的数量 """ # 先列出所有项目划分中的GUID print("\n=== 列出所有项目划分中的GUID ===") all_guids = self.list_all_division_guids() print("=== 列出结束 ===\n") # 首先确保所有费用预览节点都有标准格式 print("\n=== 确保所有费用预览节点都有标准格式 ===") standardized_count = 0 if all_guids: for guid, name, path in all_guids: try: # 查找费用预览节点 expense_node = self.find_expense_preview_node(guid) if expense_node: self.ensure_standard_format(expense_node) standardized_count += 1 except Exception as e: print(f"标准化节点 {guid} 时出错: {str(e)}") continue print(f"已标准化 {standardized_count} 个费用预览节点") print("=== 标准化结束 ===\n") success_count = 0 files = os.listdir(folder_path) guid_map = {} # 第一步:遍历所有文件,按 GUID 分组 for filename in files: if not filename.endswith(".json"): continue # 检查文件名格式 parts = filename.split("_") if len(parts) < 2: continue # 尝试从文件名中提取GUID guid = None is_calculation_file = False is_rcj_file = False is_inventory_project = False # 新增变量,用于标记是否为清单工程文件 # 检查文件名是否包含GUID格式 import re guid_match = re.search( r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", filename, re.IGNORECASE ) if guid_match: # 新格式:项目名称_GUID_文件类型.json guid = guid_match.group(0) if "calculation_results" in filename: is_calculation_file = True elif "rcj" in filename: is_rcj_file = True elif "清单工程" in filename: # 新增检查,如果文件名包含"清单工程",则标记为清单工程 is_inventory_project = True is_calculation_file = True # 清单工程也作为计算结果处理 else: # 旧格式:假设GUID是文件名中的第二个字段 if len(parts) >= 3: guid = parts[1] # 文件名中的第二个字段为 GUID guid = guid.strip("{}") # 去掉可能的花括号 if "调差_预算工程" in filename: is_calculation_file = True elif "调差_rcj" in filename: is_rcj_file = True elif "清单工程" in filename: # 新增检查,如果文件名包含"清单工程",则标记为清单工程 is_inventory_project = True is_calculation_file = True # 清单工程也作为计算结果处理 if not guid: print(f"无法从文件名 {filename} 提取GUID,跳过") continue # print(f"从文件名 {filename} 提取的GUID: {guid}") if guid not in guid_map: guid_map[guid] = { "calc": None, "rcj": None, "division": original_division, "is_inventory": is_inventory_project, } if is_calculation_file: guid_map[guid]["calc"] = os.path.join(folder_path, filename) guid_map[guid]["is_inventory"] = is_inventory_project elif is_rcj_file: guid_map[guid]["rcj"] = os.path.join(folder_path, filename) # 第二步:逐个 GUID 加载数据并调用处理函数 for guid, paths in guid_map.items(): try: # 尝试通过GUID查找项目划分节点 division_node = self.find_project_division_node(guid) # 如果找不到,尝试通过文件名前缀(项目名称)查找 if not division_node: file_path = paths["calc"] or paths["rcj"] if file_path: filename = os.path.basename(file_path) project_name = filename.split("_")[0] # 假设文件名第一部分是项目名称 print(f"通过GUID未找到节点,尝试通过名称 '{project_name}' 查找") division_node = self.find_division_node_by_name(project_name) if not division_node: print(f"无法找到对应的项目划分节点,跳过处理 GUID: {guid}") continue # 使用找到的节点的GUID actual_guid = division_node.get("GUID", "") # print( # f"找到项目划分节点,GUID: {actual_guid}, 名称: {division_node.get('项目名称', division_node.get('name', '未命名'))}" # ) # 查找费用预览节点,如果不存在则创建 expense_node = self.find_expense_preview_node(actual_guid) if not expense_node: print(f"未找到GUID为 {actual_guid} 的费用预览节点,将在处理时创建") calc_data = None rcj_data = None # 处理计算结果文件 if paths["calc"]: try: with open(paths["calc"], "r", encoding="utf-8") as f: file_data = json.load(f) # 使用原始文件中的division字段 if original_division: paths["division"] = original_division print(f"使用原始文件中的division字段: {original_division}") # 判断是否为清单工程 is_inventory = paths["is_inventory"] if is_inventory: # 清单工程,保留原始结构 calc_data = file_data print("检测到清单工程,保留原始数据结构") else: # 预算工程,检查数据格式 has_guid_key = False for key in file_data.keys(): if re.search( r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", key, re.IGNORECASE, ): has_guid_key = True break if has_guid_key: # 如果预算工程中有GUID键,也保留原始结构 calc_data = file_data print("检测到预算工程中包含GUID键,保留原始数据结构") else: # 旧格式:直接使用 calc_data = file_data except Exception as e: print(f"读取工程量文件失败 {paths['calc']}: {e}") continue # 处理人材机文件 if paths["rcj"]: try: with open(paths["rcj"], "r", encoding="utf-8") as f: file_data = json.load(f) # 使用原始文件中的division字段 if original_division: paths["division"] = original_division print(f"使用原始文件中的division字段: {original_division}") # 判断是否为清单工程 is_inventory = paths["is_inventory"] if is_inventory: # 清单工程,保留原始结构 rcj_data = file_data print("检测到清单工程,保留原始数据结构") else: # 预算工程,检查数据格式 has_guid_key = False for key in file_data.keys(): if re.search( r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", key, re.IGNORECASE, ): has_guid_key = True break if has_guid_key: # 如果预算工程中有GUID键,也保留原始结构 rcj_data = file_data print("检测到预算工程中包含GUID键,保留原始数据结构") else: # 旧格式:直接使用 rcj_data = file_data except Exception as e: print(f"读取人材机文件失败 {paths['rcj']}: {e}") continue # 调用处理方法 success = True if calc_data: # 如果有division字段,添加到calc_data中 if paths["division"]: calc_data["division"] = paths["division"] success &= self.add_quantity_node_expense_data(guid, calc_data) if rcj_data: # 如果有division字段,添加到rcj_data中 if paths["division"]: rcj_data["division"] = paths["division"] success &= self.add_labor_material_machine_expense_data(guid, rcj_data) if success: success_count += 1 print(f"✅ 成功处理 GUID: {guid}") else: print(f"❌ 处理 GUID: {guid} 时发生错误") except Exception as e: print(f"处理 GUID: {guid} 时发生异常: {str(e)}") continue return success_count def export_to_json(self, pretty=True): """ 导出为JSON字符串 :param pretty: 是否格式化输出 :return: JSON字符串 """ return json.dumps(self.project_data, ensure_ascii=False, indent=2 if pretty else 0) def list_all_division_guids(self): """列出所有项目划分中的GUID""" if "projectData" not in self.project_data or "projectDivision" not in self.project_data["projectData"]: print("警告: project_data 为空或不包含 projectData.projectDivision") return guids = [] def collect_guids(node, path=""): """递归收集所有GUID""" if isinstance(node, list): for i, item in enumerate(node): collect_guids(item, f"{path}[{i}]") elif isinstance(node, dict): guid = self.get_node_guid(node) if guid: name = node.get("项目名称", node.get("name", "未命名")) guid_with_braces = "{" + guid + "}" guids.append((guid_with_braces, name, path)) # 递归检查所有子节点,包括children和其他字典值 for key, value in node.items(): if isinstance(value, (dict, list)): collect_guids(value, f"{path}.{key}") # 从projectDivision开始收集 collect_guids(self.project_data["projectData"]["projectDivision"], "projectData.projectDivision") # print(f"项目划分中共有 {len(guids)} 个GUID:") # for guid, name, path in guids: # print(f"GUID: {guid}, 名称: {name}, 路径: {path}") return guids def find_division_node_by_name(self, name): """通过名称查找项目划分节点""" if "projectData" not in self.project_data or "projectDivision" not in self.project_data["projectData"]: print("警告: project_data 为空或不包含 projectData.projectDivision") return None def search_by_name(node): if isinstance(node, list): for item in node: result = search_by_name(item) if result: return result elif isinstance(node, dict): node_name = node.get("项目名称", node.get("name", "")) if node_name == name: return node if "children" in node: result = search_by_name(node["children"]) if result: return result return None for category in self.project_data["projectData"]["projectDivision"].values(): result = search_by_name(category) if result: return result return None def ensure_standard_format(self, expense_node): """确保费用预览节点有标准格式(sum, children, rcj)""" # 确保sum存在 if "sum" not in expense_node: expense_node["sum"] = [] # 如果children中有费用项(直接费用项,不是子节点),将其移至sum并清空children if "children" in expense_node: has_direct_cost_items = False for child in expense_node["children"]: if "cost" in child and "id" in child and "GUID" not in child: # 这是直接费用项,应该移到sum中 has_direct_cost_items = True # 检查是否已经存在相同id的项 exists = False for item in expense_node["sum"]: if item.get("id") == child["id"]: exists = True break if not exists: # 只保留id和cost两个属性 expense_node["sum"].append({"id": child["id"], "cost": child["cost"]}) # 如果children中只有直接费用项,清空children if has_direct_cost_items and all("GUID" not in child for child in expense_node["children"]): expense_node["children"] = [] else: expense_node["children"] = [] # 确保rcj存在 if "rcj" not in expense_node: expense_node["rcj"] = [] # 确保sum中的项只有id和cost两个属性 for i, item in enumerate(expense_node["sum"]): if "id" in item and "cost" in item: expense_node["sum"][i] = {"id": item["id"], "cost": item["cost"]} return expense_node def write_BCLresult_into_json(original_json_path, bcl_result_folder, output_json_path): """ 将BCL计算结果写入到原始JSON文件中 :param original_json_path: 原始JSON文件路径 :param bcl_result_folder: BCL计算结果文件夹路径 :param output_json_path: 输出JSON文件路径(合并后的JSON) :return: 是否成功处理 """ try: # 1. 加载原始项目数据 print(f"加载原始项目数据: {original_json_path}") with open(original_json_path, "r", encoding="utf-8") as f: project_data = json.load(f) # 获取原始文件中的division字段 original_division = project_data.get("division", "") print(f"从原始文件中获取到division字段: {original_division}") # 2. 初始化处理器 processor = ProjectExpenseProcessor() processor.initialize_project_data(project_data) # 3. 检查BCL计算结果文件夹是否存在 if not os.path.exists(bcl_result_folder): print(f"错误: BCL计算结果文件夹不存在: {bcl_result_folder}") return False print(f"使用BCL计算结果文件夹: {bcl_result_folder}") # 4. 批量处理 count = processor.batch_process_from_folder(bcl_result_folder, original_division) print(f"共成功处理了 {count} 个项目节点。") # 5. 导出更新后的数据 print(f"保存更新后的数据: {output_json_path}") os.makedirs(os.path.dirname(output_json_path), exist_ok=True) # 确保输出目录存在 with open(output_json_path, "w", encoding="utf-8") as f: f.write(processor.export_to_json(pretty=True)) print(f"✅ 数据已保存至 {output_json_path}") return True except Exception as e: print(f"❌ 处理失败: {str(e)}") import traceback traceback.print_exc() return False def batch_write_BCLresult_into_json(original_folder, bcl_result_folder, output_folder): """ 批量处理文件夹中的所有JSON文件,将BCL计算结果写入到原始JSON文件中 :param original_folder: 原始JSON文件夹路径 :param bcl_result_folder: BCL计算结果文件夹路径 :param output_folder: 输出文件夹路径(合并后的JSON) :return: 处理成功的文件数量 """ # 确保输出文件夹存在 os.makedirs(output_folder, exist_ok=True) # 查找所有JSON文件 json_files = [] for file in os.listdir(original_folder): if file.lower().endswith(".json"): json_files.append(os.path.join(original_folder, file)) if not json_files: print(f"警告: 在目录 {original_folder} 中没有找到JSON文件") return 0 # 处理每个JSON文件 success_count = 0 for original_file in json_files: # 构建输出文件路径 rel_path = os.path.relpath(original_file, original_folder) output_file = os.path.join(output_folder, rel_path) # 获取文件名(不含扩展名),用于查找对应的BCL计算结果文件夹 base_filename = os.path.splitext(os.path.basename(original_file))[0] # 构建BCL计算结果文件夹路径(假设与原始文件同名) file_bcl_result_folder = os.path.join(bcl_result_folder, base_filename) # 如果不存在同名文件夹,使用BCL计算结果文件夹本身 if not os.path.exists(file_bcl_result_folder): file_bcl_result_folder = bcl_result_folder print(f"\n处理文件: {original_file}") print(f"BCL计算结果文件夹: {file_bcl_result_folder}") print(f"输出文件: {output_file}") # 处理文件 if write_BCLresult_into_json(original_file, file_bcl_result_folder, output_file): success_count += 1 return success_count if __name__ == "__main__": # 使用硬编码的文件路径,不需要命令行参数 original_file = "project2json/outputs/GPRB1MJL/merged/架线检修国网.json" # 原始JSON文件 bcl_result_folder = "project2json/outputs/GPRB1MJL/bclresults/架线检修国网" # BCL计算结果文件夹 output_file = "project2json/outputs/GPRB1MJL/final/架线检修国网.json" # 输出文件(合并后的JSON) print(f"原始JSON文件: {original_file}") print(f"BCL计算结果文件夹: {bcl_result_folder}") print(f"输出文件: {output_file}") success = write_BCLresult_into_json(original_file, bcl_result_folder, output_file) if success: print(f"\n处理成功: 数据已保存至 {output_file}") else: print("\n处理失败")