修改费用计算代码

This commit is contained in:
chentianrui
2025-08-22 18:13:09 +08:00
parent 8d595b339c
commit be848c3e78
20 changed files with 2569 additions and 360 deletions
+414 -88
View File
@@ -74,6 +74,22 @@ def map_quantity_node_types(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": "设备"}
@@ -86,9 +102,9 @@ def map_quantity_node_types(node):
node_copy["费用类型"] = "取费"
# 如果已经是"取费"则保持不变
# 递归处理子节点
if "children" in node_copy and node_copy["children"]:
node_copy["children"] = [map_quantity_node_types(child) for child in node_copy["children"]]
# 不在此处递归 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
@@ -101,15 +117,15 @@ def map_resource_node_types(node):
# 复制节点以避免修改原始数据
node_copy = deepcopy(node)
# 映射类型字段
if "类型" in node_copy:
type_mapping = {
"2": "人工",
"3": "材料",
"4": "机械",
}
if node_copy["类型"] in type_mapping:
node_copy["类型"] = type_mapping[node_copy["类型"]]
# 统一标准化类型字段:同时规范化 '类型' 与 '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"]:
@@ -132,50 +148,72 @@ def extract_resource_nodes(node, parent_id=None):
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")
current_id = node.get("id") or node.get("GUID") or node.get("guid")
resource_nodes = []
# 检查是否是人材机节点(支持字符串和数字类型)
node_type = node.get("类型")
is_resource_node = False
# 检查是否是人材机节点(支持字符串和数字类型、双字段
node_type = node.get("类型") or node.get("type")
is_resource_node = _is_resource_type(node_type)
# 同时支持数字类型和字符串类型
if node_type in ["人工", "材料", "机械", "2", "3", "4"]:
is_resource_node = True
# 合并处理材机列表和children字段 # 处理逻辑:
# - 若为资源节点且存在children:不返回父节点;其children继承父类型后继续递归提取
# - 若为资源节点且无children:返回该节点
# - 若非资源节点:常规递归处理children/材机列表
# 复制节点但不包含children和材机列表
node_copy = {k: v for k, v in node.items() if k not in ["children", "材机列表"]}
# 添加父级ID
if parent_id:
node_copy["parent_id"] = parent_id
# 添加到资源列表
resource_nodes.append(node_copy)
# 合并处理材机列表和children字段 - 只处理一种来源的子节点,优先处理材机列表
# 优先处理“材机列表
child_nodes = []
if "材机列表" in node and node["材机列表"]:
child_nodes = node["材机列表"]
elif "children" in node and node["children"] and not is_resource_node:
# 只有在不是资源节点时才处理children
elif "children" in node and node["children"]:
child_nodes = node["children"]
# 处理子节点
for child in child_nodes:
child_copy = deepcopy(child)
child_copy["parent_id"] = current_id
# 递归处理
sub_resources = extract_resource_nodes(child_copy, current_id)
if sub_resources:
resource_nodes.extend(sub_resources)
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):
def process_project_children(children, software_type=None):
"""处理项目子节点,分离工程量节点和人材机节点,支持嵌套的工程量节点"""
if not children:
return None, None
@@ -183,15 +221,135 @@ def process_project_children(children):
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)
# 提取人材机节点 - extract_resource_nodes会递归提取所有层级的人材机节点
resources = extract_resource_nodes(child)
# 在“技改/主网”下,对定额节点做嵌套检测(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]
@@ -204,24 +362,146 @@ def process_project_children(children):
# 清理子节点中的人材机节点
clean_resource_nodes_from_quantity(quantity_node)
# 递归处理剩余的工程量节点
if "children" in quantity_node and quantity_node["children"]:
sub_quantity_nodes, sub_resource_nodes = process_project_children(quantity_node["children"])
# 🔥 核心修改:仅当当前节点是"定额"时,才递归处理其children并提取嵌套的工程量节点
current_node_type = quantity_node.get("类型") or quantity_node.get("type")
is_quota_node = current_node_type in ("定额", "0")
# 更新quantity_node的children
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) # 使用pop安全删除
quantity_node.pop("children", None)
# 合并子节点中提取的资源节点
# 合并资源节点
if sub_resource_nodes:
resource_nodes.extend(sub_resource_nodes)
# else:
# 非定额节点:不递归提取子节点作为工程量,不处理 sub_quantity_nodes
# 添加到工程量节点列表
quantity_nodes.append(quantity_node)
# 对于定额节点:将“材机列表”里符合条件的项(主材/设备或配件类型为主材/配件)也作为工程量节点并入
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
return (quantity_nodes if quantity_nodes else None, resource_nodes if resource_nodes else None)
# 🔥 最后:将当前节点本身加入 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):
@@ -249,12 +529,16 @@ def clean_resource_nodes_from_quantity(node):
del node["children"]
def load_project_data(json_file_path, project_name, project_guid=None):
def load_project_data(json_file_path, project_name, project_guid=None, json_data=None):
"""加载JSON数据并获取目标项目节点"""
try:
# 读取JSON文件
with open(json_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 支持透传已加载的 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", {})
@@ -280,12 +564,12 @@ def load_project_data(json_file_path, project_name, project_guid=None):
return None, None, None, None, None
def get_cost_table_children(json_file_path, project_name, project_guid=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_file_path, project_name, project_guid, json_data=json_data
)
if not target_node:
@@ -307,7 +591,7 @@ def get_cost_table_children(json_file_path, project_name, project_guid=None):
return None
def get_quantity_nodes(json_file_path, project_name, engineering_type, project_guid=None):
def get_quantity_nodes(json_file_path, project_name, engineering_type, project_guid=None, software_type=None):
"""
获取工程量节点
@@ -394,7 +678,7 @@ def get_quantity_nodes(json_file_path, project_name, engineering_type, project_g
if bill_children:
# 处理清单节点的子节点,获取工程量节点
bill_quantity_nodes, _ = process_project_children(bill_children)
bill_quantity_nodes, _ = process_project_children(bill_children, software_type)
if bill_quantity_nodes:
print(f"清单节点 '{bill_name}' 下找到 {len(bill_quantity_nodes)} 个工程量节点")
@@ -495,7 +779,7 @@ def get_quantity_nodes(json_file_path, project_name, engineering_type, project_g
return quantity_nodes
else:
# 预算工程 - 直接获取工程量节点
quantity_nodes, _ = process_project_children(project_children)
quantity_nodes, _ = process_project_children(project_children, software_type)
# 如果没有工程量节点,返回空列表而不是None
if not quantity_nodes:
@@ -754,35 +1038,77 @@ def get_bill_node_by_id(bill_id: str) -> Dict[str, Any]:
return {}
# # 测试代码
# if __name__ == "__main__":
# json_file_path = os.path.join("技改预算", "架线.json")
# project_name = "基础工程材料工地运输"
# adjustment_type = "拆除"
# # 测试参数(使用新合并的JSON文件)
# json_file_path = "project2json/outputs/merged/变电检修国网-副本.json"
# project_name = "1.12新增卸车保管"
# project_guid = "{0952D46D-E5C7-4412-88FF-93517EF2633C}"
# # 获取并输出取费表子节点
# cost_children = get_cost_table_children(json_file_path, project_name)
# print("\n取费表子节点:")
# print(json.dumps(cost_children, ensure_ascii=False, indent=2) if cost_children else None)
# 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 "<无名称>"
# # 获取并输出工程量节点
# quantity_nodes = get_quantity_nodes(json_file_path, project_name, adjustment_type)
# print("\n工程量节点:")
# print(json.dumps(quantity_nodes, ensure_ascii=False, indent=2) if quantity_nodes else None)
# def _node_type(n: dict):
# return n.get("类型") or n.get("type") or "<无类型>"
# # 获取并输出分类后的人材机节点
# labor_nodes, material_nodes, machine_nodes = get_classified_resource_nodes(
# json_file_path, project_name, adjustment_type
# )
# def _node_id(n: dict):
# return n.get("GUID") or n.get("guid") or n.get("id") or n.get("parent_id") or "<无ID>"
# print("\n人工节点列表:")
# for node, parent_id in labor_nodes:
# print(f"节点名称: {node.get('名称')}, 父级ID: {parent_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}")
# print("\n材料节点列表:")
# for node, parent_id in material_nodes:
# print(f"节点名称: {node.get('名称')}, 父级ID: {parent_id}")
# def _check_resource_parents_with_children(nodes):
# """统计是否存在类型为人/材/机且仍带children的父节点"""
# print("\n机械节点列表:")
# for node, parent_id in machine_nodes:
# print(f"节点名称: {node.get('名称')}, 父级ID: {parent_id}")
# 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("测试完成!")