""" 第三步:将bcl计算结果补充到json文件中 """ import json import os from copy import deepcopy import uuid class ProjectExpenseProcessor: def __init__(self): self.project_data = None def initialize_project_data(self, project_data): """初始化项目数据,深拷贝避免修改原始数据""" self.project_data = deepcopy(project_data) # 添加调试信息 print(f"项目数据结构: {list(self.project_data.keys())}") # 检查projectData是否存在 if "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)}") for group_name, group in category.items(): print(f" 组: {group_name}, 类型: {type(group)}") for i, item in enumerate(group): if "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: for category_name, category in self.project_data.get("projectDivision", {}).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: guid = node["GUID"] print(f"项目划分GUID: {guid} 在路径: {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 "projectData" not in self.project_data or "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 = node.get("GUID", "").strip("{}").upper() 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(): for group_name, group in category.items(): 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 # print(f"在顶层搜索中未找到GUID: {target_guid}") return None # 检查当前节点 current_guid = node.get("GUID", "").strip("{}").upper() 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() + "}" 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. 清空children准备添加工程量节点 expense_node["children"] = [] # 5. 处理每个工程量计算结果 for node_name, costs in calculation_results.items(): # 5.1 查找项目划分中对应的工程量节点 quantity_node = None if "children" in division_node: for child in division_node["children"]: if child.get("项目名称") == node_name or child.get("name") == node_name: quantity_node = child break if not quantity_node: print(f"未找到名称为 {node_name} 的工程量节点") continue # 5.2 如果工程量节点没有GUID,生成一个 if "GUID" not in quantity_node: quantity_node["GUID"] = self.generate_guid() quantity_guid = quantity_node["GUID"] # 5.3 创建工程量费用预览节点(使用标准格式) quantity_expense = {"GUID": quantity_guid, "sum": [], "children": [], "rcj": []} # 5.4 添加费用项到sum for cost_type, amount in costs.items(): quantity_expense["sum"].append({"id": f"{cost_type}", "cost": str(amount)}) # 5.5 将工程量费用预览节点添加到项目划分费用预览的children中 expense_node["children"].append(quantity_expense) print(f"成功添加GUID为 {project_guid} 的工程量节点费用预览数据") return True except Exception as e: print(f"添加工程量节点费用预览数据失败: {str(e)}") return False 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 batch_process_from_folder(self, folder_path): """ 批量处理指定文件夹下的工程量和人材机JSON文件 :param folder_path: 包含JSON文件的文件夹路径 :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: # 查找费用预览节点 expense_node = self.find_expense_preview_node(guid) if expense_node: self.ensure_standard_format(expense_node) standardized_count += 1 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) < 3: continue # 尝试从文件名中提取GUID guid = parts[1] # 文件名中的第二个字段为 GUID # 确保GUID格式正确 guid = guid.strip("{}") # 去掉可能的花括号 # print(f"从文件名 {filename} 提取的GUID: {guid}") if guid not in guid_map: guid_map[guid] = {"calc": None, "rcj": None} if "调差_预算工程" in filename: guid_map[guid]["calc"] = os.path.join(folder_path, filename) elif "调差_rcj" in filename: guid_map[guid]["rcj"] = os.path.join(folder_path, filename) # 第二步:逐个 GUID 加载数据并调用处理函数 for guid, paths in guid_map.items(): # 尝试通过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: calc_data = json.load(f) except Exception as e: print(f"读取工程量文件失败 {paths['calc']}: {e}") continue if paths["rcj"]: try: with open(paths["rcj"], "r", encoding="utf-8") as f: rcj_data = json.load(f) except Exception as e: print(f"读取人材机文件失败 {paths['rcj']}: {e}") continue # 调用处理方法 success = True if calc_data: success &= self.add_quantity_node_expense_data(guid, calc_data) if rcj_data: 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} 时发生错误") return success_count def get_processed_project_data(self): """ 获取处理后的项目数据 :return: 处理后的项目数据 """ return self.project_data 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): if "GUID" in node: guid = node["GUID"] name = node.get("项目名称", node.get("name", "未命名")) guids.append((guid, 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) # 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) 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_folder = "project2json/outputs" # 原始JSON文件夹 bcl_result_folder = "outputs-2" # BCL计算结果文件夹 output_folder = "final_outputs" # 输出文件夹(合并后的JSON) print(f"原始JSON文件夹: {original_folder}") print(f"BCL计算结果文件夹: {bcl_result_folder}") print(f"输出文件夹: {output_folder}") count = batch_write_BCLresult_into_json(original_folder, bcl_result_folder, output_folder) print(f"\n批量处理完成: 共成功处理了 {count} 个文件")