Files
KG_generation/equipment_calculation/item_acquisition.py
T
2025-08-22 18:13:09 +08:00

1115 lines
47 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
import os
from copy import deepcopy
from typing import Dict, Any, Optional, Set
# 添加全局缓存
_bill_node_cache = {}
def find_project_division_node(node, project_name, project_guid=None, result=None):
"""递归查找指定项目名称的项目划分节点,可选匹配GUID"""
if result is not None and result:
return result
if isinstance(node, dict):
# 检查是否是项目划分节点
is_project_division = node.get("type") == "项目划分"
# 检查项目名称是否匹配
name_matches = node.get("项目名称") == project_name
# 检查GUID是否匹配(如果提供了GUID)
guid_matches = True
if project_guid:
node_guid = node.get("GUID") or node.get("guid")
guid_matches = node_guid == project_guid
# 如果是项目划分节点,且名称和GUID都匹配(或没有提供GUID)
if is_project_division and name_matches and guid_matches:
return [node]
# 递归查找子节点
for key, value in node.items():
if isinstance(value, (dict, list)):
result = find_project_division_node(value, project_name, project_guid, result)
if result:
return result
elif isinstance(node, list):
for item in node:
if isinstance(item, (dict, list)):
result = find_project_division_node(item, project_name, project_guid, result)
if result:
return result
return result or []
def find_cost_table(cost_setting, table_name):
"""在costSetting中查找指定名称的取费表"""
if isinstance(cost_setting, dict):
if cost_setting.get("name") == table_name:
return cost_setting
for key, value in cost_setting.items():
if isinstance(value, (dict, list)):
result = find_cost_table(value, table_name)
if result:
return result
elif isinstance(cost_setting, list):
for item in cost_setting:
if isinstance(item, (dict, list)):
result = find_cost_table(item, table_name)
if result:
return result
return None
def map_quantity_node_types(node):
"""映射工程量节点的类型和费用类型"""
if not isinstance(node, dict):
return node
# 复制节点以避免修改原始数据
node_copy = deepcopy(node)
# 先根据“配件判定规则”标准化类型:
# 若 节点的 type/类型 == "配件" 且 配型 == "配件",则检查 配件类型:
# - 配件类型 == "主材" -> 作为 主材 节点
# - 配件类型 == "配件" -> 作为 设备 节点
raw_type = node_copy.get("类型") if node_copy.get("类型") is not None else node_copy.get("type")
raw_peixing = node_copy.get("配型")
# 放宽条件:当 类型/type 为“配件”,且(配型为“配件”或缺省),并且存在“配件类型”时,按配件类型映射
if str(raw_type) == "配件" and (raw_peixing in (None, "", "配件")):
pj_type = node_copy.get("配件类型")
if pj_type == "主材":
node_copy["类型"] = "主材"
node_copy["type"] = "主材"
elif pj_type == "配件":
node_copy["类型"] = "设备"
node_copy["type"] = "设备"
# 映射类型字段
if "类型" in node_copy:
type_mapping = {"0": "定额", "1": "主材", "5": "设备"}
if node_copy["类型"] in type_mapping:
node_copy["类型"] = type_mapping[node_copy["类型"]]
# 映射费用类型字段
if "费用类型" in node_copy:
if node_copy["费用类型"] == "0":
node_copy["费用类型"] = "取费"
# 如果已经是"取费"则保持不变
# 不在此处递归 children,避免对子节点进行类型标准化,交由各层独立处理
# if isinstance(node_copy.get("children"), list):
# node_copy["children"] = [map_quantity_node_types(child) for child in node_copy["children"]]
return node_copy
def map_resource_node_types(node):
"""映射人材机节点的类型"""
if not isinstance(node, dict):
return node
# 复制节点以避免修改原始数据
node_copy = deepcopy(node)
# 统一标准化类型字段:同时规范化 '类型' 与 'type',输出为中文(人工/材料/机械)
type_mapping = {"2": "人工", "3": "材料", "4": "机械"}
# 取原始值(可能存在任一字段)
raw_t = node_copy.get("类型") if node_copy.get("类型") is not None else node_copy.get("type")
if raw_t is not None:
# 若为代码,转换为中文;若已为中文则保持
normalized = type_mapping.get(str(raw_t), raw_t)
node_copy["类型"] = normalized
node_copy["type"] = normalized
# 递归处理子节点
if "children" in node_copy and node_copy["children"]:
node_copy["children"] = [map_resource_node_types(child) for child in node_copy["children"]]
return node_copy
def extract_resource_nodes(node, parent_id=None):
"""
提取人材机节点并保持父子结构
Args:
node: 节点
parent_id: 父级节点ID
Returns:
list: 提取的人材机节点列表,每个节点包含parent_id字段
"""
if not isinstance(node, dict):
return [] # 返回空列表而不是None,便于后续处理
# 类型工具
def _is_resource_type(t):
return t in ("人工", "材料", "机械", "2", "3", "4")
str2code = {"人工": "2", "材料": "3", "机械": "4"}
code2str = {v: k for k, v in str2code.items()}
# 获取当前节点ID
current_id = node.get("id") or node.get("GUID") or node.get("guid")
resource_nodes = []
# 检查是否是人材机节点(支持字符串和数字类型、双字段)
node_type = node.get("类型") or node.get("type")
is_resource_node = _is_resource_type(node_type)
# 合并处理材机列表和children字段 # 处理逻辑:
# - 若为资源节点且存在children:不返回父节点;其children继承父类型后继续递归提取
# - 若为资源节点且无children:返回该节点
# - 若非资源节点:常规递归处理children/材机列表
# 优先处理“材机列表”
child_nodes = []
if "材机列表" in node and node["材机列表"]:
child_nodes = node["材机列表"]
elif "children" in node and node["children"]:
child_nodes = node["children"]
if is_resource_node:
if child_nodes:
# 父为资源且有子:上提子项,子项继承父类型(同时设置中英文字段),并递归
parent_code = str2code.get(node_type, node_type)
parent_str = code2str.get(node_type, node_type)
for child in child_nodes:
child_copy = deepcopy(child)
if isinstance(child_copy, dict):
child_copy["parent_id"] = current_id
child_copy["type"] = parent_code
child_copy["类型"] = parent_str
sub_resources = extract_resource_nodes(child_copy, current_id)
if sub_resources:
resource_nodes.extend(sub_resources)
else:
# 基本类型直接忽略
continue
else:
# 父为资源且无子:直接计入
node_copy = deepcopy(node)
node_copy["parent_id"] = parent_id
# 统一双字段
node_copy["type"] = str2code.get(node_type, node_type)
node_copy["类型"] = code2str.get(node_type, node_type)
resource_nodes.append(node_copy)
else:
# 非资源节点:递归其子节点
for child in child_nodes:
child_copy = deepcopy(child)
if isinstance(child_copy, dict):
child_copy["parent_id"] = current_id
sub_resources = extract_resource_nodes(child_copy, current_id)
if sub_resources:
resource_nodes.extend(sub_resources)
return resource_nodes
def process_project_children(children, software_type=None):
"""处理项目子节点,分离工程量节点和人材机节点,支持嵌套的工程量节点"""
if not children:
return None, None
quantity_nodes = []
resource_nodes = []
# 内部工具:在人材机存在拆分时展开其子节点,删除中间父级人材机节点
def _flatten_resource_splits_in_place(node: dict):
"""
递归+迭代地展开任意深度的人材机拆分:
- 如果某个子节点是人材机(人工/材料/机械/2/3/4)且其自身有children
* 将其children上提为当前层的children
* 被上提的每个子节点的 类型/type 设为与父节点相同;
* 移除该父级人材机节点本身;
- 对非被移除的子节点递归处理;
通过 while 循环保证多层嵌套被完全展开。
"""
if not isinstance(node, dict):
return
if "children" not in node or not node["children"]:
return
def _is_resource_type(t):
return t in ("人工", "材料", "机械", "2", "3", "4")
# 中英文类型映射
str2code = {"人工": "2", "材料": "3", "机械": "4"}
code2str = {v: k for k, v in str2code.items()}
changed = True
while changed:
changed = False
new_children = []
for ch in node["children"]:
if isinstance(ch, dict):
t = ch.get("类型") or ch.get("type")
# 命中“人材机且存在children”,则将其子节点上提并继承类型
if _is_resource_type(t) and ch.get("children"):
parent_code = str2code.get(t, t) # 若 t 已是代码则保持
parent_str = code2str.get(t, t) # 若 t 已是中文则保持
for gc in ch.get("children", []) or []:
if isinstance(gc, dict):
gc["type"] = parent_code
gc["类型"] = parent_str
new_children.append(gc)
else:
new_children.append(gc)
# 丢弃父级人材机节点
changed = True
continue
else:
# 对未被移除的子节点继续递归展开
_flatten_resource_splits_in_place(ch)
new_children.append(ch)
else:
new_children.append(ch)
node["children"] = new_children
for child in children:
# 先复制两份:一份用于工程量树(quantity_node),一份用于资源提取(resource_node_src)
quantity_node = deepcopy(child)
resource_node_src = deepcopy(child)
# 额外保留一份原始工程量节点用于最终输出,保持原样不作修改
original_quantity_node = deepcopy(child)
# 在两份结构中都先执行“人材机拆分展开”,以便:
# 1) 工程量树不保留中间的人材机父节点
# 2) 资源提取时直接提取到展开后的子资源,且不包含中间父节点
_flatten_resource_splits_in_place(quantity_node)
_flatten_resource_splits_in_place(resource_node_src)
# 应用工程量节点类型映射
quantity_node = map_quantity_node_types(quantity_node)
# 在“技改/主网”下,对定额节点做嵌套检测(children 或 材机列表 中包含 定额/主材/设备 或 配件类型为 主材/配件)
if software_type in ("技改", "主网"):
def _type_is_quota_material_equipment(t):
# 支持代码与中文类型名
return t in ("定额", "主材", "设备", "0", "1", "5")
def _collect_nested_matches(node):
matches = []
# 检查 children
for ch in node.get("children", []) or []:
if isinstance(ch, dict):
t = ch.get("类型") or ch.get("type")
pj_t = ch.get("配件类型")
if _type_is_quota_material_equipment(t) or pj_t in ("主材", "配件"):
matches.append(
{
"来源": "children",
"名称": ch.get("项目名称") or ch.get("清单名称") or ch.get("name"),
"类型": t,
"配件类型": pj_t,
"id": ch.get("GUID") or ch.get("guid") or ch.get("id"),
}
)
# 递归继续检查更深层的嵌套
sub = _collect_nested_matches(ch)
if sub:
matches.extend(sub)
# 检查 材机列表(如存在)
for it in node.get("材机列表", []) or []:
if isinstance(it, dict):
t = it.get("类型") or it.get("type")
pj_t = it.get("配件类型")
if _type_is_quota_material_equipment(t) or pj_t in ("主材", "配件"):
matches.append(
{
"来源": "材机列表",
"名称": it.get("项目名称")
or it.get("清单名称")
or it.get("name")
or it.get("材料名称"),
"类型": t,
"配件类型": pj_t,
"id": it.get("GUID") or it.get("guid") or it.get("id"),
}
)
return matches
node_type = quantity_node.get("类型") or quantity_node.get("type")
if node_type in ("定额", "0"):
nested_matches = _collect_nested_matches(quantity_node)
if nested_matches:
quantity_node["嵌套检测"] = {
"存在嵌套": True,
"命中数量": len(nested_matches),
"命中项": nested_matches,
}
# 提取人材机节点 - 基于已展开的 resource_node_src 提取
resources = extract_resource_nodes(resource_node_src)
if resources:
# 应用人材机节点类型映射
resources = [map_resource_node_types(resource) for resource in resources]
resource_nodes.extend(resources)
# 从工程量节点中移除材机列表
if "材机列表" in quantity_node:
del quantity_node["材机列表"]
# 清理子节点中的人材机节点
clean_resource_nodes_from_quantity(quantity_node)
# 🔥 核心修改:仅当当前节点是"定额"时,才递归处理其children并提取嵌套的工程量节点
current_node_type = quantity_node.get("类型") or quantity_node.get("type")
is_quota_node = current_node_type in ("定额", "0")
sub_quantity_nodes = None
sub_resource_nodes = None
if is_quota_node and "children" in quantity_node and quantity_node["children"]:
# 处理定额的人材机节点
# 关键:递归输入改为原始副本的 children,避免上一层标准化对子层“原始类型字段”的影响
sub_quantity_nodes, sub_resource_nodes = process_project_children(
(original_quantity_node.get("children") or []), software_type
)
# 更新当前节点的 children(保留结构)
if sub_quantity_nodes:
quantity_node["children"] = sub_quantity_nodes
# 关键:将子层的工程量节点(如主材/设备)直接并入当前层输出,
# 并确保类型为标准化后的中文值(主材/设备),避免后续 BCL 过滤不到
# 不修改原始子节点类型,仅做深拷贝并并入
normalized_children = [deepcopy(n) for n in sub_quantity_nodes]
quantity_nodes.extend(normalized_children)
# 调试:打印并入的子工程量节点类型
try:
for ch in normalized_children:
nm = (
ch.get("项目名称")
or ch.get("清单名称")
or ch.get("name")
or ch.get("材料名称")
or "<未命名>"
)
tp = ch.get("类型") or ch.get("type")
# print(f"[DEBUG] 并入子工程量节点: 名称={nm} | 类型={tp}")
except Exception:
pass
else:
quantity_node.pop("children", None)
# 合并资源节点
if sub_resource_nodes:
resource_nodes.extend(sub_resource_nodes)
# else:
# 非定额节点:不递归提取子节点作为工程量,不处理 sub_quantity_nodes
# 对于定额节点:将“材机列表”里符合条件的项(主材/设备或配件类型为主材/配件)也作为工程量节点并入
if is_quota_node:
mj_items = (quantity_node.get("材机列表") or []) if isinstance(quantity_node, dict) else []
for it in mj_items:
if not isinstance(it, dict):
continue
t = it.get("类型") or it.get("type")
pj_t = it.get("配件类型")
# 类型代码转中文
if t in ("1", "5"):
t = {"1": "主材", "5": "设备"}[t]
new_type = None
if t in ("主材", "设备"):
new_type = t
elif pj_t == "主材":
new_type = "主材"
elif pj_t == "配件":
new_type = "设备"
if new_type:
# 不改写原始字段,直接将条目拷贝加入,类型由下游对象构造派生
new_node = deepcopy(it)
quantity_nodes.append(new_node)
try:
nm = (
new_node.get("项目名称")
or new_node.get("清单名称")
or new_node.get("name")
or new_node.get("材料名称")
or "<未命名>"
)
# print(f"[DEBUG] 从材机列表并入工程量节点: 名称={nm} | 类型={t} | 派生类型={new_type}")
except Exception:
pass
# 🔥 最后:将当前节点本身加入 quantity_nodes
# 为满足“工程量节点输出保持原样”,此处使用 original_quantity_node 代替处理后的 quantity_node
# 规则:
# - 若当前节点为“主材/设备(1/5)”,直接计入工程量节点;
# - 若为“定额(0)”,沿用既有两个条件之一成立时计入;
def _children_have_no_children(n: dict) -> bool:
ch = (n.get("children") or []) if isinstance(n, dict) else []
# 若无 children,则视作“没有更深层 children”
if not ch:
return True
for c in ch:
if isinstance(c, dict) and (c.get("children") or []):
return False
return True
def _mj_has_hit(n: dict) -> bool:
items = (n.get("材机列表") or []) if isinstance(n, dict) else []
for it in items:
if not isinstance(it, dict):
continue
t = it.get("类型") or it.get("type")
pj_t = it.get("配件类型")
if t in ("定额", "主材", "设备", "0", "1", "5") or pj_t in ("主材", "配件"):
return True
return False
# 主材/设备直接计入(以标准化类型入列,避免后续 BCL 过滤不到)
if current_node_type in ("主材", "设备", "1", "5"):
to_append = deepcopy(original_quantity_node)
quantity_nodes.append(to_append)
try:
nm = (
to_append.get("项目名称")
or to_append.get("清单名称")
or to_append.get("name")
or to_append.get("材料名称")
or "<未命名>"
)
tp = to_append.get("类型") or to_append.get("type")
# print(f"[DEBUG] 入列工程量节点(主材/设备): 名称={nm} | 类型={tp}")
except Exception:
pass
# 定额按条件计入
elif is_quota_node:
if _children_have_no_children(quantity_node) or _mj_has_hit(quantity_node):
quantity_nodes.append(original_quantity_node)
try:
nm = (
original_quantity_node.get("项目名称")
or original_quantity_node.get("清单名称")
or original_quantity_node.get("name")
or original_quantity_node.get("材料名称")
or "<未命名>"
)
tp = original_quantity_node.get("类型") or original_quantity_node.get("type")
# print(f"[DEBUG] 入列工程量节点(定额): 名称={nm} | 类型={tp}")
except Exception:
pass
# 始终返回列表,避免上层出现 None 导致判定困难
return (quantity_nodes or [], resource_nodes or [])
def clean_resource_nodes_from_quantity(node):
"""递归清理工程量节点中的人材机节点"""
if not isinstance(node, dict):
return
# 清理材机列表
if "材机列表" in node:
del node["材机列表"]
# 清理子节点中的人材机节点
if "children" in node and node["children"]:
# 过滤掉人材机类型的子节点
node["children"] = [
c for c in node["children"] if not (isinstance(c, dict) and c.get("类型") in ["人工", "材料", "机械"])
]
# 递归清理剩余子节点
for child in node["children"]:
clean_resource_nodes_from_quantity(child)
# 如果没有子节点了,删除children属性
if not node["children"]:
del node["children"]
def load_project_data(json_file_path, project_name, project_guid=None, json_data=None):
"""加载JSON数据并获取目标项目节点"""
try:
# 支持透传已加载的 JSON 数据,避免重复读取
if json_data is not None:
data = json_data
else:
# 读取JSON文件
with open(json_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 获取projectData中的costSetting和projectDivision
project_data = data.get("projectData", {})
cost_setting = project_data.get("costSetting", {})
project_division = project_data.get("projectDivision", {})
# 查找指定项目名称和GUID的节点
target_nodes = find_project_division_node(project_division, project_name, project_guid)
if not target_nodes:
if project_guid:
print(f"未找到项目名称为 '{project_name}' 且GUID为 '{project_guid}' 的节点")
else:
print(f"未找到项目名称为 '{project_name}' 的节点")
return None, None, None, None, None
# 获取找到的节点
target_node = target_nodes[0]
return data, project_data, cost_setting, project_division, target_node
except Exception as e:
print(f"加载项目数据时出错: {e}")
return None, None, None, None, None
def get_cost_table_children(json_file_path, project_name, project_guid=None, json_data=None):
"""获取取费表子节点"""
try:
# 加载项目数据,传递project_guid参数
data, project_data, cost_setting, project_division, target_node = load_project_data(
json_file_path, project_name, project_guid, json_data=json_data
)
if not target_node:
return None
# 获取取费表名称
fee_table_name = target_node.get("取费表")
if not fee_table_name:
print(f"目标项目划分节点中没有'取费表'字段")
return None
# 查找对应的取费表
cost_table = find_cost_table(cost_setting, fee_table_name)
cost_table_children = cost_table.get("children", None) if cost_table else None
return cost_table_children
except Exception as e:
print(f"获取取费表子节点时出错: {e}")
return None
def get_quantity_nodes(json_file_path, project_name, engineering_type, project_guid=None, software_type=None):
"""
获取工程量节点
Args:
json_file_path: JSON文件路径
project_name: 项目名称
engineering_type: 工程类型
project_guid: 项目GUID,用于区分同名项目
Returns:
list: 工程量节点列表,如果没有找到项目划分节点则返回None,如果找到项目划分节点但没有工程量节点则返回空列表
"""
try:
# 加载项目数据,传递project_guid参数
data, project_data, cost_setting, project_division, target_node = load_project_data(
json_file_path, project_name, project_guid
)
if not target_node:
return None # 没有找到项目划分节点
# 处理项目子节点
project_children = target_node.get("children", None)
if not project_children:
print(f"项目 '{project_name}' (GUID: {project_guid}) 没有子节点(工程量节点)")
return [] # 找到项目划分节点,但没有子节点(工程量节点)
# 如果是清单工程,需要先找到清单节点,然后获取其下的工程量节点
if engineering_type == "清单工程":
quantity_nodes = []
print(f"开始处理清单工程,项目名称: {project_name}")
# 递归查找真正的清单节点
def find_true_bill_nodes(node_list, parent_path=""):
result = []
for node in node_list:
# 检查是否是清单节点
is_bill_node = (
node.get("类型") == "8"
or node.get("类型") == "清单"
or node.get("type") == "8"
or node.get("type") == "清单"
or "清单名称" in node
or "清单全码" in node
)
node_name = node.get("清单名称", node.get("项目名称", "未命名"))
# 强制使用GUID而不是ID
node_id = node.get("GUID") or node.get("guid") or node.get("id", "未知ID")
current_path = f"{parent_path}/{node_name}" if parent_path else node_name
if is_bill_node:
print(f"找到清单节点: ID={node_id}, 名称={node_name}, 路径={current_path}")
# 检查清单节点是否有取费表名称
has_fee_table = "取费表名称" in node or "取费表" in node
if has_fee_table:
fee_table_name = node.get("取费表名称", node.get("取费表", "未知"))
print(f"清单节点有取费表: {fee_table_name}")
# 添加到缓存
if node_id != "未知ID":
_bill_node_cache[node_id] = node
print(f"缓存清单节点: ID={node_id}")
result.append(node)
else:
print(f"警告:清单节点没有取费表名称: {node_name}")
# 递归处理子节点
if "children" in node and node["children"]:
result.extend(find_true_bill_nodes(node["children"], current_path))
return result
# 查找所有真正的清单节点
bill_nodes = find_true_bill_nodes(project_children)
print(f"找到 {len(bill_nodes)} 个有效清单节点")
# 处理每个清单节点
for bill_node in bill_nodes:
# 强制使用GUID而不是ID
bill_id = bill_node.get("GUID") or bill_node.get("guid") or bill_node.get("id")
bill_name = bill_node.get("清单名称", bill_node.get("项目名称", "未命名清单"))
bill_children = bill_node.get("children", [])
if bill_children:
# 处理清单节点的子节点,获取工程量节点
bill_quantity_nodes, _ = process_project_children(bill_children, software_type)
if bill_quantity_nodes:
print(f"清单节点 '{bill_name}' 下找到 {len(bill_quantity_nodes)} 个工程量节点")
# 为每个工程量节点设置清单节点信息
for node in bill_quantity_nodes:
# 存储关键信息而不是整个对象
# 强制使用GUID而不是ID
node["bill_guid"] = bill_id # 新增GUID字段
node["bill_id"] = bill_id # 保持兼容性
node["bill_name"] = bill_name
node["取费表名称"] = bill_node.get("取费表名称", bill_node.get("取费表", ""))
# 设置parent_id以保持兼容性
node["parent_id"] = bill_id
print(f"设置工程量节点 '{node.get('项目名称', '未命名')}' 的清单节点信息")
# 添加这一行,将完整的清单节点添加到工程量节点中
node["bill_node"] = bill_node
quantity_nodes.extend(bill_quantity_nodes)
else:
print(f"清单节点 '{bill_name}' 下未找到工程量节点")
else:
print(f"清单节点 '{bill_name}' 没有子节点")
# 如果没有找到任何工程量节点,尝试直接获取定额节点
if not quantity_nodes:
print("未找到清单节点下的工程量节点,尝试直接获取定额节点...")
def find_quota_nodes(node_list):
result = []
for node in node_list:
# 检查是否是定额节点
is_quota_node = node.get("类型") == "定额" or node.get("type") == "定额"
if is_quota_node:
result.append(node)
# 递归处理子节点
if "children" in node and node["children"]:
result.extend(find_quota_nodes(node["children"]))
return result
# 查找所有定额节点
quota_nodes = find_quota_nodes(project_children)
if quota_nodes:
print(f"找到 {len(quota_nodes)} 个定额节点")
# 检查这些定额节点是否有清单节点信息
for node in quota_nodes:
parent_node = None
parent_id = node.get("parent_id")
# 在项目中查找父节点
def find_node_by_id(node_list, node_id):
for n in node_list:
# 强制使用GUID而不是ID进行匹配
node_guid = n.get("GUID") or n.get("guid") or n.get("id")
if node_guid == node_id:
return n
if "children" in n and n["children"]:
found = find_node_by_id(n["children"], node_id)
if found:
return found
return None
if parent_id:
parent_node = find_node_by_id(project_children, parent_id)
# 如果找到父节点且是清单节点,使用其信息
if parent_node:
is_bill_node = (
parent_node.get("类型") == "8"
or parent_node.get("类型") == "清单"
or parent_node.get("type") == "8"
or parent_node.get("type") == "清单"
or "清单名称" in parent_node
or "清单全码" in parent_node
)
if is_bill_node:
# 强制使用GUID而不是ID
bill_id = parent_node.get("GUID") or parent_node.get("guid") or parent_node.get("id")
bill_name = parent_node.get("清单名称", parent_node.get("项目名称", "未命名清单"))
# 强制使用GUID而不是ID
node["bill_guid"] = bill_id # 新增GUID字段
node["bill_id"] = bill_id # 保持兼容性
node["bill_name"] = bill_name
node["取费表名称"] = parent_node.get("取费表名称", parent_node.get("取费表", ""))
# 设置parent_id以保持兼容性
node["parent_id"] = bill_id
print(f"为定额节点 '{node.get('项目名称', '未命名')}' 设置清单节点信息")
quantity_nodes.extend(quota_nodes)
return quantity_nodes
else:
# 预算工程 - 直接获取工程量节点
quantity_nodes, _ = process_project_children(project_children, software_type)
# 如果没有工程量节点,返回空列表而不是None
if not quantity_nodes:
print(f"项目 '{project_name}' (GUID: {project_guid}) 没有工程量节点")
return []
return quantity_nodes
except Exception as e:
print(f"获取工程量节点时出错: {e}")
import traceback
traceback.print_exc() # 打印详细错误堆栈
return None
def get_resource_nodes(json_file_path, project_name, project_guid=None):
"""
获取人材机节点
Args:
json_file_path: JSON文件路径
project_name: 项目名称
project_guid: 项目GUID,用于区分同名项目
Returns:
list: 人材机节点列表
"""
try:
# 加载项目数据,传递project_guid参数
data, project_data, cost_setting, project_division, target_node = load_project_data(
json_file_path, project_name, project_guid
)
if not target_node:
return None
# 获取项目子节点
project_children = target_node.get("children", None)
if not project_children:
print(f"项目 '{project_name}' (GUID: {project_guid}) 没有子节点")
return []
# 提取人材机节点
resource_nodes = []
for child in project_children:
# 提取人材机节点并保持父子结构
nodes = extract_resource_nodes(child)
if nodes:
resource_nodes.extend(nodes)
# 映射节点类型
resource_nodes = [map_resource_node_types(node) for node in resource_nodes]
return resource_nodes
except Exception as e:
print(f"获取人材机节点时出错: {e}")
return None
def get_classified_resource_nodes(json_file_path, project_name, project_guid=None):
"""
获取分类后的人材机节点
Args:
json_file_path: JSON文件路径
project_name: 项目名称
project_guid: 项目GUID,用于区分同名项目
Returns:
tuple: (人工节点列表, 材料节点列表, 机械节点列表),每个列表包含(节点, 父级ID)元组
"""
# 获取所有人材机节点,传递project_guid参数
resource_nodes = get_resource_nodes(json_file_path, project_name, project_guid)
if not resource_nodes:
return [], [], []
# 分类存储
labor_nodes = [] # 人工节点
material_nodes = [] # 材料节点
machine_nodes = [] # 机械节点
# 递归函数,用于处理节点及其子节点
def process_node(node):
node_type = node.get("类型")
parent_id = node.get("parent_id")
# 同时支持数字类型和字符串类型
if node_type in ["人工", "2"]:
node_type = "人工" # 统一转换为字符串类型
labor_nodes.append((node, parent_id))
elif node_type in ["材料", "3"]:
node_type = "材料" # 统一转换为字符串类型
material_nodes.append((node, parent_id))
elif node_type in ["机械", "4"]:
node_type = "机械" # 统一转换为字符串类型
machine_nodes.append((node, parent_id))
# 更新类型字段
node["类型"] = node_type
# 处理子节点
if "children" in node and node["children"]:
for child in node["children"]:
# 确保子节点有父级ID
if "parent_id" not in child:
child["parent_id"] = node.get("id")
process_node(child)
# 处理所有节点
for node in resource_nodes:
process_node(node)
return labor_nodes, material_nodes, machine_nodes
def find_bill_node_by_id(node, bill_id, result=None):
"""递归查找指定ID的清单节点"""
if result is not None and result:
return result
if isinstance(node, dict):
# 检查当前节点是否是目标清单节点
node_id_matches = node.get("id") == bill_id or node.get("GUID") == bill_id
if node_id_matches:
# 检查是否是清单节点
is_bill_node = (
node.get("类型") == "8"
or node.get("类型") == "清单"
or node.get("type") == "8"
or node.get("type") == "清单"
or "清单名称" in node
or "清单全码" in node
)
if is_bill_node:
print(f"找到清单节点 ID={bill_id}, 类型={node.get('类型', node.get('type', '未知'))}")
return [node]
else:
print(
f"找到ID匹配但不是清单节点的节点: ID={bill_id}, 类型={node.get('类型', node.get('type', '未知'))}"
)
# 不返回非清单节点
# 递归查找子节点
if "children" in node and isinstance(node["children"], list):
for child in node["children"]:
result = find_bill_node_by_id(child, bill_id, result)
if result:
return result
# 检查其他字段
for key, value in node.items():
if key != "children" and isinstance(value, (dict, list)):
result = find_bill_node_by_id(value, bill_id, result)
if result:
return result
elif isinstance(node, list):
for item in node:
if isinstance(item, (dict, list)):
result = find_bill_node_by_id(item, bill_id, result)
if result:
return result
return result or []
def get_bill_cost_table(json_file_path, bill_id):
"""获取清单节点的取费表子节点"""
try:
# 读取JSON文件
with open(json_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 获取projectData中的costSetting和projectDivision
project_data = data.get("projectData", {})
cost_setting = project_data.get("costSetting", {})
project_division = project_data.get("projectDivision", {})
print(f"正在查找清单节点ID: {bill_id}")
# 查找指定ID的清单节点
bill_nodes = find_bill_node_by_id(project_division, bill_id)
if not bill_nodes:
print(f"未找到ID为 '{bill_id}' 的清单节点")
return None
# 获取找到的节点
bill_node = bill_nodes[0]
# 获取取费表名称或ID - 尝试多种可能的字段名
fee_table_name = bill_node.get("取费表名称") or bill_node.get("取费表") or bill_node.get("费率表")
if not fee_table_name:
print(f"清单节点中没有'取费表名称'字段")
# 打印节点信息以便调试
print(f"清单节点字段: {', '.join(bill_node.keys())}")
# 尝试从父级节点获取取费表名称
if "parent_id" in bill_node:
parent_nodes = find_bill_node_by_id(project_division, bill_node["parent_id"])
if parent_nodes:
parent_node = parent_nodes[0]
fee_table_name = (
parent_node.get("取费表名称") or parent_node.get("取费表") or parent_node.get("费率表")
)
if fee_table_name:
print(f"从父级节点获取到取费表名称: {fee_table_name}")
if not fee_table_name:
return None
print(f"找到取费表名称: {fee_table_name}")
# 使用相同的函数查找取费表 - 与预算工程保持一致
cost_table = find_cost_table(cost_setting, fee_table_name)
if not cost_table:
print(f"未找到取费表 '{fee_table_name}'")
return None
cost_table_children = cost_table.get("children", None)
return cost_table_children
except Exception as e:
print(f"获取清单节点取费表时出错: {e}")
import traceback
traceback.print_exc() # 打印详细错误堆栈
return None
def get_bill_node_by_id(bill_id: str) -> Dict[str, Any]:
"""
根据ID从缓存中获取清单节点
Args:
bill_id: 清单节点ID
Returns:
Dict[str, Any]: 清单节点数据
"""
# 清理ID格式
clean_bill_id = str(bill_id).strip("{}").upper()
# 在缓存中查找
for cached_id, node in _bill_node_cache.items():
if str(cached_id).strip("{}").upper() == clean_bill_id:
print(f"从缓存中获取清单节点: {node.get('清单名称', '未命名')}")
return node
print(f"在缓存中未找到ID为 {bill_id} 的清单节点")
return {}
# if __name__ == "__main__":
# # 测试参数(使用新合并的JSON文件)
# json_file_path = "project2json/outputs/merged/变电检修国网-副本.json"
# project_name = "1.12新增卸车保管"
# project_guid = "{0952D46D-E5C7-4412-88FF-93517EF2633C}"
# def _node_name(n: dict):
# # 尽可能稳健地取名称字段
# for k in ("名称", "name", "材料名称", "定额名称", "项目名称", "title"):
# if isinstance(n, dict) and n.get(k):
# return n.get(k)
# return n.get("newGuid") or n.get("GUID") or n.get("id") or "<无名称>"
# def _node_type(n: dict):
# return n.get("类型") or n.get("type") or "<无类型>"
# def _node_id(n: dict):
# return n.get("GUID") or n.get("guid") or n.get("id") or n.get("parent_id") or "<无ID>"
# def _print_nodes(nodes, title):
# print(f"{title} 共 {len(nodes) if nodes else 0} 个:")
# if not nodes:
# return
# for i, item in enumerate(nodes, 1):
# n = item[0] if isinstance(item, tuple) and item and isinstance(item[0], dict) else item
# try:
# print(f" {i:02d}. 名称={_node_name(n)} | 类型={_node_type(n)} | ID={_node_id(n)}")
# except Exception:
# print(f" {i:02d}. {item}")
# def _check_resource_parents_with_children(nodes):
# """统计是否存在类型为人/材/机且仍带children的父节点"""
# def _is_res(t):
# return t in ("人工", "材料", "机械", "2", "3", "4")
# bad = []
# for item in nodes or []:
# n = item[0] if isinstance(item, tuple) and item and isinstance(item[0], dict) else item
# if isinstance(n, dict) and _is_res(n.get("类型") or n.get("type")) and n.get("children"):
# bad.append(n)
# if bad:
# print("[警告] 发现仍存在带children的人材机父节点,这些父节点应被跳过,子节点应上提:")
# _print_nodes(bad, "异常父级人材机节点")
# else:
# print("[校验] 未发现带children的人材机父节点,扁平化正常。")
# print(f"测试项目: {project_name}")
# print(f"文件路径: {json_file_path}")
# print(f"项目GUID: {project_guid}")
# print("-" * 50)
# # 获取工程量节点
# print("获取工程量节点...")
# quantity_nodes = get_quantity_nodes(json_file_path, project_name, "预算工程", project_guid, software_type="技改")
# _print_nodes(quantity_nodes or [], "工程量节点")
# # 获取人材机节点(应为:父为人材机且有children时,父不计,子上提并继承父类型)
# print("获取人材机节点...")
# resource_nodes = get_resource_nodes(json_file_path, project_name, project_guid)
# _print_nodes(resource_nodes or [], "人材机节点")
# _check_resource_parents_with_children(resource_nodes or [])
# # 获取分类后的人材机节点
# print("获取分类后的人材机节点...")
# labor_nodes, material_nodes, machinery_nodes = get_classified_resource_nodes(
# json_file_path, project_name, project_guid
# )
# _print_nodes(labor_nodes or [], "人工节点")
# _print_nodes(material_nodes or [], "材料节点")
# _print_nodes(machinery_nodes or [], "机械节点")
# print("-" * 50)
# print("测试完成!")