1075 lines
43 KiB
Python
1075 lines
43 KiB
Python
import json
|
||
import os
|
||
import math
|
||
from typing import Dict, List, Any, Tuple
|
||
from copy import deepcopy
|
||
|
||
from item_acquisition import get_quantity_nodes, get_classified_resource_nodes, load_project_data
|
||
|
||
# 修改后的代码:先将字典转换为对象
|
||
from bcl_utils import create_node_from_type, create_material_or_equipment_from_node
|
||
|
||
from bcl_utils import (
|
||
ZjMaterialOrEquipmentBCLContext,
|
||
ZjProjectBCLContext,
|
||
BCLVariant,
|
||
ZjBillBCLContext,
|
||
init_bcl_calculator,
|
||
create_project_contexts,
|
||
calculator,
|
||
)
|
||
|
||
from bcl_utils import BCLDataSourceItem, BCLDataSourceContext
|
||
from item_acquisition import get_quantity_nodes, get_classified_resource_nodes
|
||
|
||
# 人材机节点定义合并条件常量
|
||
LABOR_MERGE_CONDITIONS = [
|
||
"编码",
|
||
"名称",
|
||
"单位",
|
||
"预算价含税",
|
||
"预算价不含税",
|
||
"市场价不含税",
|
||
"市场价含税",
|
||
"调差类型",
|
||
"专业属性",
|
||
"供货方",
|
||
"物料类材料",
|
||
]
|
||
|
||
MATERIAL_MERGE_CONDITIONS = [
|
||
"类型",
|
||
"编码",
|
||
"名称",
|
||
"单位",
|
||
"预算价不含税",
|
||
"预算价含税",
|
||
"市场价不含税",
|
||
"市场价含税",
|
||
"调差类型",
|
||
"专业属性",
|
||
"供货方",
|
||
"集中配送",
|
||
"卸车",
|
||
"保管",
|
||
"物料类材料",
|
||
]
|
||
|
||
MACHINE_MERGE_CONDITIONS = [
|
||
"编码",
|
||
"名称",
|
||
"单位",
|
||
"预算价不含税",
|
||
"预算价含税",
|
||
"市场价不含税",
|
||
"市场价含税",
|
||
"调差类型",
|
||
"专业属性",
|
||
"供货方",
|
||
"物料类材料",
|
||
]
|
||
|
||
|
||
def process_DXdata(json_data):
|
||
"""
|
||
处理 projectData 中的线路特征段数据,计算每条 bpBillZhXsTable 记录的加权值,
|
||
并返回一个字典列表,每个字典包含 '特征段' 和对应的 ItemName: 计算结果。
|
||
|
||
:param json_data: 解析后的 JSON 数据(字典格式)
|
||
:return: 字典列表,例如 [{"特征段": "1", "工地运输...": "21.8"}, ...]
|
||
"""
|
||
result_list = []
|
||
|
||
# 遍历每个线路特征段
|
||
for segment in json_data.get("projectData", {}).get("线路特征段", []):
|
||
seg_name = segment.get("Name", "")
|
||
# 提取特征段中的数字,例如 "特征段1" -> "1"
|
||
seg_number = "".join(filter(str.isdigit, seg_name))
|
||
if not seg_number:
|
||
seg_number = "0" # 默认值
|
||
|
||
# 获取 bpBillZhXsTable 列表
|
||
table_list = segment.get("bpBillZhXsTable", [])
|
||
for item in table_list:
|
||
try:
|
||
# 提取字段并转换为浮点数
|
||
KnapScale = float(item.get("KnapScale", 0))
|
||
Knap = float(item.get("Knap", 0))
|
||
HillScale = float(item.get("HillScale", 0))
|
||
Hill = float(item.get("Hill", 0))
|
||
EdelweissScale = float(item.get("EdelweissScale", 0))
|
||
edelweiss = float(item.get("edelweiss", 0))
|
||
MountainScale = float(item.get("MountainScale", 0))
|
||
Mountain = float(item.get("Mountain", 0))
|
||
SloughScale = float(item.get("SloughScale", 0))
|
||
Slough = float(item.get("Slough", 0))
|
||
RiverScale = float(item.get("RiverScale", 0))
|
||
River = float(item.get("River", 0))
|
||
DesertScale = float(item.get("DesertScale", 0))
|
||
Desert = float(item.get("Desert", 0))
|
||
|
||
# 执行计算
|
||
total = (
|
||
(KnapScale * Knap)
|
||
+ (HillScale * Hill)
|
||
+ (EdelweissScale * edelweiss)
|
||
+ (MountainScale * Mountain)
|
||
+ (SloughScale * Slough)
|
||
+ (RiverScale * River)
|
||
+ (DesertScale * Desert)
|
||
) * 0.01
|
||
|
||
# 保留一位小数,格式化为字符串
|
||
calculated_value = f"{total:.1f}"
|
||
|
||
# 获取 ItemName
|
||
item_name = item.get("ItemName", "未知项目")
|
||
|
||
# 构建结果字典
|
||
item_dict = {"特征段": seg_number, item_name: calculated_value}
|
||
|
||
# 添加到结果列表
|
||
result_list.append(item_dict)
|
||
|
||
except (ValueError, TypeError) as e:
|
||
print(f"数据转换错误,跳过该项: {e}")
|
||
continue
|
||
|
||
return result_list
|
||
|
||
|
||
# 在 resource_fee_calculator.py 文件中修改 calc_rcj_count 函数中的相关代码
|
||
def calc_rcj_count(
|
||
rcj_nodes: List[Tuple[Dict[str, Any], str]], project_children: List[Dict[str, Any]], json_file_path: str = None
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
计算人材机节点的数量,考虑父级消耗量
|
||
|
||
Args:
|
||
rcj_nodes: 人材机节点列表,每个元素是(节点, 父级ID)元组
|
||
project_children: 项目划分级别下的所有工程量节点
|
||
json_file_path: JSON文件路径,用于获取工程信息
|
||
|
||
Returns:
|
||
List[Dict[str, Any]]: 计算数量后的人材机节点列表
|
||
"""
|
||
result_nodes = []
|
||
|
||
# 检查rcj_nodes是否为空
|
||
if not rcj_nodes:
|
||
print("没有找到人材机节点")
|
||
return result_nodes
|
||
|
||
# 检查project_children是否为None
|
||
if project_children is None:
|
||
print("没有找到工程量节点,使用默认数量")
|
||
# 使用默认数量处理所有人材机节点
|
||
for node, parent_id in rcj_nodes:
|
||
node_copy = deepcopy(node)
|
||
node_copy["计算数量"] = node_copy.get("数量", "1.0")
|
||
# 保存父级ID
|
||
node_copy["parent_id"] = parent_id
|
||
# 由于没有找到父级节点,使用默认名称
|
||
node_copy["parent_name"] = "未知工程量节点"
|
||
result_nodes.append(node_copy)
|
||
return result_nodes
|
||
|
||
# 创建父级ID到工程量节点的映射
|
||
parent_nodes = {}
|
||
for node in project_children:
|
||
# 如果节点没有id,使用节点的名称作为id
|
||
node_id = node.get("id")
|
||
if not node_id:
|
||
# 使用节点名称或其他唯一标识符作为ID
|
||
node_id = node.get("项目名称", node.get("name", ""))
|
||
# 如果还是没有,使用内存地址作为唯一标识符
|
||
if not node_id:
|
||
node_id = f"node_{id(node)}"
|
||
# 将生成的ID添加到节点中
|
||
node["id"] = node_id
|
||
print(f"为工程量节点 '{node.get('项目名称', node.get('name', '未命名'))}' 分配ID: {node_id}")
|
||
|
||
parent_nodes[node_id] = node
|
||
node_name = node.get("项目名称", node.get("name", "未命名"))
|
||
print(f"工程量节点: ID={node_id}, 名称={node_name}")
|
||
|
||
print(f"找到 {len(parent_nodes)} 个父级工程量节点")
|
||
|
||
# 如果只有一个工程量节点,为所有没有父级ID的人材机节点分配这个工程量节点作为父级
|
||
default_parent_node = None
|
||
default_parent_id = None
|
||
if len(parent_nodes) == 1:
|
||
default_parent_id = list(parent_nodes.keys())[0]
|
||
default_parent_node = list(parent_nodes.values())[0]
|
||
print(
|
||
f"只有一个工程量节点,将用作默认父级: ID={default_parent_id}, 名称={default_parent_node.get('项目名称', default_parent_node.get('name', '未命名'))}"
|
||
)
|
||
|
||
# 添加调试信息,打印人材机节点的父级ID
|
||
for i, (node, parent_id) in enumerate(rcj_nodes):
|
||
node_name = node.get("名称", "未命名")
|
||
|
||
# 如果人材机节点没有父级ID且有默认父级,分配默认父级
|
||
if parent_id is None and default_parent_id is not None:
|
||
rcj_nodes[i] = (node, default_parent_id)
|
||
parent_id = default_parent_id
|
||
print(f"人材机节点: 名称={node_name}, 分配默认父级ID={parent_id}")
|
||
else:
|
||
print(f"人材机节点: 名称={node_name}, 父级ID={parent_id}")
|
||
|
||
# 初始化BCL计算器
|
||
if not init_bcl_calculator():
|
||
print("初始化BCL计算器失败,使用默认数量")
|
||
# 使用默认数量处理所有人材机节点
|
||
for node, parent_id in rcj_nodes:
|
||
node_copy = deepcopy(node)
|
||
node_copy["计算数量"] = node_copy.get("数量", "1.0")
|
||
# 保存父级ID
|
||
node_copy["parent_id"] = parent_id
|
||
# 由于没有找到父级节点,使用默认名称
|
||
node_copy["parent_name"] = "未知工程量节点"
|
||
result_nodes.append(node_copy)
|
||
return result_nodes
|
||
|
||
# 创建工程信息上下文(顶层上下文)
|
||
project_context = create_project_contexts(json_file_path=json_file_path)
|
||
|
||
# 遍历所有节点,计算数量
|
||
for node, parent_id in rcj_nodes:
|
||
node_copy = deepcopy(node)
|
||
|
||
# 保存父级ID
|
||
node_copy["parent_id"] = parent_id
|
||
|
||
# 获取父级工程量节点
|
||
parent_node = parent_nodes.get(parent_id)
|
||
if not parent_node:
|
||
print(f"未找到ID为 {parent_id} 的父级工程量节点,使用默认数量")
|
||
# 使用默认数量
|
||
node_copy["计算数量"] = node_copy.get("数量", "1.0")
|
||
# 由于没有找到父级节点,使用默认名称
|
||
node_copy["parent_name"] = "未知工程量节点"
|
||
result_nodes.append(node_copy)
|
||
continue
|
||
|
||
# 保存父级节点名称
|
||
parent_name = parent_node.get("项目名称", parent_node.get("name", "未命名工程量"))
|
||
node_copy["parent_name"] = parent_name
|
||
|
||
# 确保父级节点有数量字段
|
||
if "数量" not in parent_node or not parent_node["数量"]:
|
||
print(f"父级节点 {parent_id} 没有数量字段,使用默认数量")
|
||
parent_node["数量"] = "1.0"
|
||
|
||
# 打印父级节点信息
|
||
print(f"父级节点 {parent_id} ({parent_name}) 的数量: {parent_node.get('数量', '1.0')}")
|
||
|
||
# 原始代码
|
||
# ration_context = ZjBillBCLContext(prefix="定额", valueDict=parent_node, prevContext=project_context)
|
||
# context = ZjMaterialOrEquipmentBCLContext(node_data=node, parent_node=parent_node, prevContext=ration_context)
|
||
|
||
# 将父级节点转换为对象
|
||
parent_obj = create_node_from_type(parent_node)
|
||
|
||
# 将人材机节点转换为对象
|
||
node_obj = create_material_or_equipment_from_node(node)
|
||
|
||
# 使用对象创建数据源项
|
||
QuantityItem = BCLDataSourceItem(parent_obj)
|
||
moeItem = BCLDataSourceItem(node_obj)
|
||
context = BCLDataSourceContext([QuantityItem, moeItem], project_context)
|
||
|
||
# 根据节点类型选择不同的计算表达式
|
||
node_type = node.get("类型", "")
|
||
if node_type == "人工":
|
||
calc_expr = "_材机合并人工数量"
|
||
elif node_type == "材料":
|
||
calc_expr = "_材机合并材料数量"
|
||
elif node_type == "机械":
|
||
calc_expr = "_材机合并机械数量"
|
||
else:
|
||
# 未知类型,使用默认数量
|
||
print(f"未知节点类型 {node_type},使用默认数量")
|
||
node_copy["计算数量"] = node_copy.get("数量", "1.0")
|
||
result_nodes.append(node_copy)
|
||
continue
|
||
|
||
try:
|
||
# 使用BCL计算器计算数量
|
||
result = calculator.calculate(calc_expr, context)
|
||
|
||
# 打印调试信息
|
||
print(f"计算 {node.get('名称', '未知节点')} 的数量,表达式: {calc_expr}, 结果: {result}")
|
||
|
||
# 处理计算结果
|
||
if hasattr(result, "value"):
|
||
calculated_quantity = result.value
|
||
elif isinstance(result, (int, float)):
|
||
calculated_quantity = result
|
||
else:
|
||
# 尝试转换为浮点数
|
||
try:
|
||
calculated_quantity = float(result)
|
||
except (ValueError, TypeError):
|
||
print(f"无法将计算结果转换为浮点数: {result}")
|
||
calculated_quantity = float(node_copy.get("数量", "1.0") or "1.0")
|
||
|
||
# 如果计算结果为0,尝试使用原始数量
|
||
if calculated_quantity == 0:
|
||
orig_quantity = float(node_copy.get("数量", "0.0") or "0.0")
|
||
if orig_quantity > 0:
|
||
print(f"计算结果为0")
|
||
calculated_quantity = 0
|
||
|
||
# 设置计算数量
|
||
node_copy["计算数量"] = str(calculated_quantity)
|
||
except Exception as e:
|
||
print(f"计算节点 '{node.get('名称', '未知节点')}' 的数量时出错: {e}")
|
||
# 使用默认数量
|
||
node_copy["计算数量"] = node_copy.get("数量", "1.0")
|
||
|
||
result_nodes.append(node_copy)
|
||
|
||
return result_nodes
|
||
|
||
|
||
def generate_node_key(node: Dict[str, Any], conditions: List[str]) -> str:
|
||
"""
|
||
根据合并条件生成节点的唯一键,不再使用parent_id作为条件
|
||
"""
|
||
key_parts = []
|
||
|
||
# 添加合并条件
|
||
for condition in conditions:
|
||
# 排除与父节点相关的字段
|
||
if condition in ["parent_id", "parent_name", "id", "合并来源", "合并数量", "计算数量", "取整数量"]:
|
||
print(f" 排除字段: {condition}")
|
||
continue
|
||
|
||
value = node.get(condition, "")
|
||
# 确保值是字符串并进行标准化处理
|
||
if value is None:
|
||
value = ""
|
||
print(f" 字段 {condition} 的值为None,设置为空字符串")
|
||
elif isinstance(value, (int, float)):
|
||
# 对数值进行标准化处理,保留固定小数位
|
||
value = f"{float(value):.4f}"
|
||
print(f" 字段 {condition} 的值是数值,标准化为: {value}")
|
||
elif isinstance(value, str):
|
||
# 对字符串进行标准化处理,去除空格
|
||
original = value
|
||
value = value.strip()
|
||
if original != value:
|
||
print(f" 字段 {condition} 的值是字符串,去除空格: '{original}' -> '{value}'")
|
||
else:
|
||
# 其他类型转换为字符串
|
||
original = value
|
||
value = str(value).strip()
|
||
print(f" 字段 {condition} 的值是其他类型,转换为字符串: {type(original)} -> '{value}'")
|
||
|
||
key_parts.append(f"{condition}:{value}")
|
||
|
||
result = "|".join(key_parts)
|
||
return result
|
||
|
||
|
||
# 测试代码**************************
|
||
def debug_node_differences(node1, node2, conditions):
|
||
"""比较两个节点在合并条件上的差异"""
|
||
print(f"\n比较节点: {node1.get('名称', '未知')} 和 {node2.get('名称', '未知')}")
|
||
for condition in conditions:
|
||
val1 = node1.get(condition, "")
|
||
val2 = node2.get(condition, "")
|
||
if val1 != val2:
|
||
print(f" 条件 '{condition}' 不同: '{val1}' != '{val2}'")
|
||
# 进一步分析差异类型
|
||
if isinstance(val1, str) and isinstance(val2, str):
|
||
if val1.strip() == val2.strip():
|
||
print(f" 差异仅在空格")
|
||
elif val1.lower() == val2.lower():
|
||
print(f" 差异仅在大小写")
|
||
|
||
|
||
# **************************
|
||
|
||
|
||
def merge_similar_nodes(nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
"""
|
||
合并相似的节点
|
||
"""
|
||
print(f"\n进入merge_similar_nodes函数,节点数量: {len(nodes)}")
|
||
|
||
if not nodes:
|
||
return []
|
||
|
||
# 按节点类型分组
|
||
nodes_by_type = {}
|
||
for node in nodes:
|
||
node_type = node.get("类型", "")
|
||
if node_type not in nodes_by_type:
|
||
nodes_by_type[node_type] = []
|
||
nodes_by_type[node_type].append(node)
|
||
|
||
print(f"节点类型分组: {list(nodes_by_type.keys())}")
|
||
|
||
merged_nodes = []
|
||
|
||
# 处理每种类型的节点
|
||
for node_type, type_nodes in nodes_by_type.items():
|
||
print(f"\n处理节点类型: {node_type}, 数量: {len(type_nodes)}")
|
||
|
||
# 选择合并条件
|
||
if node_type == "人工" or node_type == "2":
|
||
conditions = LABOR_MERGE_CONDITIONS
|
||
elif node_type == "材料" or node_type == "3":
|
||
conditions = MATERIAL_MERGE_CONDITIONS
|
||
elif node_type == "机械" or node_type == "4":
|
||
conditions = MACHINE_MERGE_CONDITIONS
|
||
else:
|
||
# 未知类型,不合并
|
||
print(f"未知节点类型: {node_type}, 不合并")
|
||
merged_nodes.extend(type_nodes)
|
||
continue
|
||
|
||
print(f"使用合并条件: {conditions}")
|
||
|
||
# 检查节点是否有所有合并条件字段
|
||
for i, node in enumerate(type_nodes):
|
||
print(f"\n节点 {i+1}: {node.get('名称')}")
|
||
missing_fields = []
|
||
for condition in conditions:
|
||
if condition not in node:
|
||
missing_fields.append(condition)
|
||
if missing_fields:
|
||
print(f" 缺少字段: {missing_fields}")
|
||
|
||
# 打印节点的所有字段
|
||
print(" 节点字段:")
|
||
for key, value in node.items():
|
||
print(f" {key}: {value}")
|
||
|
||
# 使用字典存储合并后的节点,键是根据合并条件生成的唯一键
|
||
merged_dict = {}
|
||
|
||
# 检查是否有parent_id或parent_name字段
|
||
has_parent_fields = False
|
||
for node in type_nodes:
|
||
if "parent_id" in node or "parent_name" in node:
|
||
has_parent_fields = True
|
||
print(
|
||
f"发现parent字段: parent_id={node.get('parent_id', '无')}, parent_name={node.get('parent_name', '无')}"
|
||
)
|
||
break
|
||
|
||
if has_parent_fields:
|
||
print("警告: 节点包含parent字段,这可能导致合并失败。考虑从合并键中排除这些字段。")
|
||
|
||
for i, node in enumerate(type_nodes):
|
||
print(f"\n处理节点 {i+1}: {node.get('名称')}")
|
||
|
||
# 生成唯一键前,打印节点的合并条件字段
|
||
print(" 合并条件字段值:")
|
||
for condition in conditions:
|
||
value = node.get(condition, "")
|
||
print(f" {condition}: '{value}' (类型: {type(value)})")
|
||
|
||
key = generate_node_key(node, conditions)
|
||
print(f" 生成的唯一键: {key}")
|
||
|
||
# 检查键是否在字典中
|
||
if key in merged_dict:
|
||
print(f" 键已存在,合并节点")
|
||
# 合并节点
|
||
merged_node = merged_dict[key]
|
||
|
||
# 打印合并前的数量
|
||
merged_quantity = float(merged_node.get("取整数量", "0.0") or "0.0")
|
||
node_quantity = float(node.get("取整数量", "0.0") or "0.0")
|
||
print(f" 合并数量: {merged_quantity} + {node_quantity} = {merged_quantity + node_quantity}")
|
||
|
||
# 合并数量
|
||
merged_node["取整数量"] = str(merged_quantity + node_quantity)
|
||
|
||
# 合并计算数量
|
||
merged_calc_quantity = float(merged_node.get("计算数量", "0.0") or "0.0")
|
||
node_calc_quantity = float(node.get("计算数量", "0.0") or "0.0")
|
||
merged_node["计算数量"] = str(merged_calc_quantity + node_calc_quantity)
|
||
|
||
# 更新原始数量
|
||
merged_orig_quantity = float(merged_node.get("数量", "0.0") or "0.0")
|
||
node_orig_quantity = float(node.get("数量", "0.0") or "0.0")
|
||
merged_node["数量"] = str(merged_orig_quantity + node_orig_quantity)
|
||
|
||
# 添加合并信息
|
||
if "合并来源" not in merged_node:
|
||
# 使用唯一标识符,如果没有id字段则使用节点本身的索引
|
||
merged_node_id = merged_node.get("id", f"node_{id(merged_node)}")
|
||
merged_node["合并来源"] = [merged_node_id]
|
||
|
||
# 添加当前节点ID到合并来源
|
||
node_id = node.get("id", f"node_{id(node)}")
|
||
merged_node["合并来源"].append(node_id)
|
||
|
||
# 更新合并数量计数
|
||
merged_node["合并数量"] = str(len(merged_node["合并来源"]))
|
||
else:
|
||
print(f" 键不存在,创建新节点")
|
||
# 创建新节点
|
||
merged_dict[key] = deepcopy(node)
|
||
|
||
# 添加合并信息
|
||
node_id = node.get("id", f"node_{id(node)}")
|
||
merged_dict[key]["合并来源"] = [node_id]
|
||
merged_dict[key]["合并数量"] = "1"
|
||
|
||
# 检查合并结果
|
||
print(f"\n合并后节点数量: {len(merged_dict)}")
|
||
for key, node in merged_dict.items():
|
||
print(f" 键: {key}")
|
||
print(f" 节点: {node.get('名称')}, 取整数量: {node.get('取整数量')}")
|
||
if "合并来源" in node and len(node["合并来源"]) > 1:
|
||
print(f" 合并来源数量: {len(node['合并来源'])}")
|
||
|
||
# 将合并后的节点添加到结果列表
|
||
merged_nodes.extend(merged_dict.values())
|
||
|
||
return merged_nodes
|
||
|
||
|
||
def cat_rcj_count(rcj_nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
"""
|
||
处理人材机节点数量的变化并合并相似节点
|
||
"""
|
||
print(f"进入cat_rcj_count函数,节点数量: {len(rcj_nodes)}")
|
||
for node in rcj_nodes:
|
||
print(f"节点: {node.get('名称')}, 计算数量: {node.get('计算数量')}")
|
||
|
||
# 使用计算数量,不进行向上取整
|
||
processed_nodes = []
|
||
|
||
for node in rcj_nodes:
|
||
node_copy = deepcopy(node)
|
||
|
||
# 获取计算数量,直接使用,不再向上取整
|
||
calc_quantity = float(node_copy.get("计算数量", "0.0") or "0.0")
|
||
node_copy["取整数量"] = str(calc_quantity)
|
||
|
||
processed_nodes.append(node_copy)
|
||
|
||
print(f"处理后节点数量: {len(processed_nodes)}")
|
||
|
||
# 合并相似节点
|
||
merged_nodes = merge_similar_nodes(processed_nodes)
|
||
|
||
print(f"合并后节点数量: {len(merged_nodes)}")
|
||
for node in merged_nodes:
|
||
print(f"合并后节点: {node.get('名称')}, 取整数量: {node.get('取整数量')}")
|
||
|
||
return merged_nodes
|
||
|
||
|
||
def calc_rcj_fee(rcj_nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
"""
|
||
计算人材机节点的合价
|
||
|
||
Args:
|
||
rcj_nodes: 人材机节点列表
|
||
|
||
Returns:
|
||
List[Dict[str, Any]]: 计算合价后的人材机节点列表
|
||
"""
|
||
result_nodes = []
|
||
|
||
for node in rcj_nodes:
|
||
node_copy = deepcopy(node)
|
||
|
||
# 获取取整数量
|
||
quantity = float(node_copy.get("取整数量", "0.0") or "0.0")
|
||
|
||
# 获取预算价不含税和市场价不含税
|
||
budget_price = float(node_copy.get("预算价不含税", "0.0") or "0.0")
|
||
market_price = float(node_copy.get("市场价不含税", "0.0") or "0.0")
|
||
|
||
# 计算预算价合价和市场价合价
|
||
budget_total = quantity * budget_price
|
||
market_total = quantity * market_price
|
||
|
||
# 计算价差
|
||
price_diff = market_total - budget_total
|
||
|
||
# 设置合价信息
|
||
node_copy["预算价合价"] = str(budget_total)
|
||
node_copy["市场价合价"] = str(market_total)
|
||
node_copy["价差"] = str(price_diff)
|
||
|
||
result_nodes.append(node_copy)
|
||
|
||
return result_nodes
|
||
|
||
|
||
def format_rcj_output(rcj_nodes: List[Dict[str, Any]], node_type: str) -> List[Dict[str, Any]]:
|
||
"""
|
||
格式化人材机节点输出,只保留需要的字段,不再保留父级节点信息
|
||
|
||
Args:
|
||
rcj_nodes: 人材机节点列表
|
||
node_type: 节点类型("人工"、"材料"、"机械")
|
||
|
||
Returns:
|
||
List[Dict[str, Any]]: 格式化后的节点列表
|
||
"""
|
||
formatted_nodes = []
|
||
|
||
for node in rcj_nodes:
|
||
formatted_node = {}
|
||
|
||
# 根据节点类型保留特定字段
|
||
if node_type == "人工":
|
||
# 人工节点保存:编码,名称,单位,数量,预算价不含税,市场价不含税,预算价合价,市场价合价,价差
|
||
fields = [
|
||
"编码",
|
||
"名称",
|
||
"单位",
|
||
"取整数量",
|
||
"预算价不含税",
|
||
"市场价不含税",
|
||
"预算价合价",
|
||
"市场价合价",
|
||
"价差",
|
||
]
|
||
for field in fields:
|
||
formatted_node[field] = node.get(field, "")
|
||
elif node_type == "材料":
|
||
# 材料节点保存:供货方,编码,名称,单位,数量,预算价不含税,市场价不含税,预算价合价,市场价合价,价差
|
||
fields = [
|
||
"供货方",
|
||
"编码",
|
||
"名称",
|
||
"单位",
|
||
"取整数量",
|
||
"预算价不含税",
|
||
"市场价不含税",
|
||
"预算价合价",
|
||
"市场价合价",
|
||
"价差",
|
||
]
|
||
for field in fields:
|
||
formatted_node[field] = node.get(field, "")
|
||
elif node_type == "机械":
|
||
# 机械节点保存:编码,名称,单位,数量,预算价不含税,市场价不含税,预算价合价,市场价合价,价差
|
||
fields = [
|
||
"编码",
|
||
"名称",
|
||
"单位",
|
||
"取整数量",
|
||
"预算价不含税",
|
||
"市场价不含税",
|
||
"预算价合价",
|
||
"市场价合价",
|
||
"价差",
|
||
]
|
||
for field in fields:
|
||
formatted_node[field] = node.get(field, "")
|
||
|
||
# 重命名取整数量为数量
|
||
if "取整数量" in formatted_node:
|
||
formatted_node["数量"] = formatted_node.pop("取整数量")
|
||
|
||
formatted_nodes.append(formatted_node)
|
||
|
||
return formatted_nodes
|
||
|
||
|
||
def calculate_rcj_fees(
|
||
json_file_path: str,
|
||
project_name: str,
|
||
project_guid: str = None,
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
计算项目划分节点下所有人材机节点的合价
|
||
|
||
Args:
|
||
json_file_path: JSON文件路径
|
||
project_name: 项目名称
|
||
project_guid: 项目GUID,用于区分同名项目
|
||
|
||
Returns:
|
||
Dict[str, Any]: 计算结果,包含人工节点、材料节点和机械节点
|
||
"""
|
||
# 加载项目数据
|
||
data, project_data, cost_setting, project_division, target_node = load_project_data(
|
||
json_file_path, project_name, project_guid
|
||
)
|
||
|
||
# 如果没有找到项目划分节点
|
||
if not target_node:
|
||
print(f"警告: 未找到项目 '{project_name}' 的项目划分节点")
|
||
return {}
|
||
|
||
# 获取项目子节点
|
||
project_children = target_node.get("children", [])
|
||
if not project_children:
|
||
print(f"警告: 项目 '{project_name}' (GUID: {project_guid}) 没有工程量节点,跳过计算")
|
||
return {}
|
||
|
||
# 直接从工程量节点中提取人材机节点,保持父子关系
|
||
labor_nodes = [] # 人工节点 (节点, 父级节点) 元组列表
|
||
material_nodes = [] # 材料节点 (节点, 父级节点) 元组列表
|
||
machine_nodes = [] # 机械节点 (节点, 父级节点) 元组列表
|
||
|
||
# 递归处理工程量节点,提取人材机节点
|
||
def process_nodes(nodes):
|
||
for node in nodes:
|
||
# 如果是定额节点,处理其材机列表和children
|
||
if node.get("类型") == "定额" or node.get("类型") == "0":
|
||
print(f"找到定额节点: {node.get('项目名称', node.get('name', '未命名'))}")
|
||
|
||
# 处理材机列表
|
||
if "材机列表" in node and node["材机列表"]:
|
||
print(
|
||
f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 有材机列表,数量: {len(node['材机列表'])}"
|
||
)
|
||
for rcj_node in node["材机列表"]:
|
||
node_type = rcj_node.get("类型")
|
||
# 同时支持数字类型和字符串类型
|
||
if node_type in ["人工", "2"]:
|
||
labor_nodes.append((rcj_node, node))
|
||
print(
|
||
f"从材机列表找到人工节点: {rcj_node.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
|
||
)
|
||
elif node_type in ["材料", "3"]:
|
||
material_nodes.append((rcj_node, node))
|
||
print(
|
||
f"从材机列表找到材料节点: {rcj_node.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
|
||
)
|
||
elif node_type in ["机械", "4"]:
|
||
machine_nodes.append((rcj_node, node))
|
||
print(
|
||
f"从材机列表找到机械节点: {rcj_node.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
|
||
)
|
||
else:
|
||
print(f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 没有材机列表")
|
||
|
||
# 处理children中的人材机节点
|
||
if "children" in node and node["children"]:
|
||
print(
|
||
f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 有子节点,数量: {len(node['children'])}"
|
||
)
|
||
for child in node["children"]:
|
||
child_type = child.get("类型")
|
||
if child_type in ["人工", "2"]:
|
||
labor_nodes.append((child, node))
|
||
print(
|
||
f"从children找到人工节点: {child.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
|
||
)
|
||
elif child_type in ["材料", "3"]:
|
||
material_nodes.append((child, node))
|
||
print(
|
||
f"从children找到材料节点: {child.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
|
||
)
|
||
elif child_type in ["机械", "4"]:
|
||
machine_nodes.append((child, node))
|
||
print(
|
||
f"从children找到机械节点: {child.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
|
||
)
|
||
else:
|
||
print(f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 没有子节点")
|
||
|
||
# 递归处理子节点
|
||
if "children" in node and node["children"]:
|
||
process_nodes(node["children"])
|
||
|
||
# 处理所有工程量节点
|
||
process_nodes(project_children)
|
||
|
||
print(f"找到 {len(labor_nodes)} 个人工节点, {len(material_nodes)} 个材料节点, {len(machine_nodes)} 个机械节点")
|
||
|
||
# 如果仍然没有找到人材机节点,返回空结果
|
||
if not labor_nodes and not material_nodes and not machine_nodes:
|
||
print(f"项目 '{project_name}' (GUID: {project_guid}) 没有人材机数据,不保存结果")
|
||
return {}
|
||
|
||
# 修改calc_rcj_count函数调用,直接传递父级节点对象而不是ID
|
||
def calc_rcj_count_with_parent(rcj_nodes):
|
||
"""
|
||
计算人材机节点的数量,考虑父级消耗量
|
||
|
||
Args:
|
||
rcj_nodes: 人材机节点列表,每个元素是(节点, 父级节点)元组
|
||
|
||
Returns:
|
||
List[Dict[str, Any]]: 计算数量后的人材机节点列表
|
||
"""
|
||
result_nodes = []
|
||
|
||
# 检查rcj_nodes是否为空
|
||
if not rcj_nodes:
|
||
print("没有找到人材机节点")
|
||
return result_nodes
|
||
|
||
# # 初始化BCL计算器
|
||
# if not init_bcl_calculator():
|
||
# print("初始化BCL计算器失败,使用默认数量")
|
||
# # 使用默认数量处理所有人材机节点
|
||
# for node, parent_node in rcj_nodes:
|
||
# node_copy = deepcopy(node)
|
||
# node_copy["计算数量"] = node_copy.get("数量", "1.0")
|
||
# # 保存父级信息
|
||
# node_copy["parent_id"] = parent_node.get("id", "")
|
||
# node_copy["parent_name"] = parent_node.get("项目名称", parent_node.get("name", "未命名工程量"))
|
||
# result_nodes.append(node_copy)
|
||
# return result_nodes
|
||
|
||
# 创建工程信息上下文(顶层上下文)
|
||
project_context = create_project_contexts(json_file_path=json_file_path)
|
||
|
||
with open(json_file_path, "r", encoding="utf-8") as file:
|
||
data = json.load(file)
|
||
|
||
processed_data = process_DXdata(data)
|
||
|
||
DXITEM = [BCLDataSourceItem(item) for item in processed_data]
|
||
|
||
# 遍历所有节点,计算数量
|
||
for node, parent_node in rcj_nodes:
|
||
node_copy = deepcopy(node)
|
||
|
||
# 保存父级信息
|
||
parent_id = parent_node.get("id", "")
|
||
parent_name = parent_node.get("项目名称", parent_node.get("name", "未命名工程量"))
|
||
node_copy["parent_id"] = parent_id
|
||
node_copy["parent_name"] = parent_name
|
||
|
||
# 确保父级节点有数量字段
|
||
if "数量" not in parent_node or not parent_node["数量"]:
|
||
print(f"父级节点 {parent_name} 没有数量字段,使用默认数量")
|
||
parent_node["数量"] = "1.0"
|
||
|
||
# 打印父级节点信息
|
||
print(f"父级节点 {parent_name} 的数量: {parent_node.get('数量', '1.0')}")
|
||
|
||
# 确保父级节点的类型字段正确设置
|
||
if "类型" in parent_node:
|
||
parent_type = parent_node["类型"]
|
||
print(f"父级节点 {parent_name} 的类型: {parent_type}")
|
||
else:
|
||
# 如果父级节点没有类型字段,默认设置为"定额"
|
||
parent_node["类型"] = "定额"
|
||
print(f"父级节点 {parent_name} 没有类型字段,设置为默认类型: 定额")
|
||
|
||
# 处理 parent_node 的 "特征段" 字段
|
||
import re
|
||
|
||
segment_value = parent_node.get("特征段", "")
|
||
if isinstance(segment_value, str):
|
||
match = re.search(r"\d+", segment_value) # 提取第一个连续数字
|
||
if match:
|
||
parent_node["特征段"] = match.group() # 例如 "特征1" -> "1"
|
||
else:
|
||
parent_node["特征段"] = "" # 无法提取数字则清空或设为默认值
|
||
|
||
# 将父级节点转换为对象
|
||
parent_obj = create_node_from_type(parent_node)
|
||
|
||
# 确保父级对象的type属性正确设置
|
||
if hasattr(parent_obj, "type") and not parent_obj.type:
|
||
parent_obj.type = parent_node.get("类型", "定额")
|
||
print(f"设置父级对象 {parent_name} 的type属性为: {parent_obj.type}")
|
||
|
||
# 将人材机节点转换为对象
|
||
node_obj = create_material_or_equipment_from_node(node)
|
||
|
||
dxitem_context = BCLDataSourceContext(DXITEM, project_context)
|
||
dxitem_context.variables["@特征段地形系数"] = BCLVariant(DXITEM)
|
||
|
||
# 使用对象创建数据源项
|
||
QuantityItem = BCLDataSourceItem(parent_obj, None, "定额")
|
||
moeItem = BCLDataSourceItem(node_obj, QuantityItem)
|
||
|
||
if hasattr(parent_obj, "type"):
|
||
print(f"父级对象type属性值: {parent_obj.type}")
|
||
|
||
# 创建数据源上下文
|
||
context = BCLDataSourceContext([moeItem], dxitem_context)
|
||
|
||
# 根据节点类型选择不同的计算表达式
|
||
node_type = node.get("类型", "")
|
||
if node_type in ["人工", "2"]:
|
||
calc_expr = "_材机合并人工数量"
|
||
elif node_type in ["材料", "3"]:
|
||
calc_expr = "_材机合并材料数量"
|
||
elif node_type in ["机械", "4"]:
|
||
calc_expr = "_材机合并机械数量"
|
||
else:
|
||
# 未知类型,使用默认数量
|
||
print(f"未知节点类型 {node_type},使用默认数量")
|
||
node_copy["计算数量"] = node_copy.get("数量", "1.0")
|
||
result_nodes.append(node_copy)
|
||
continue
|
||
|
||
try:
|
||
# 使用BCL计算器计算数量
|
||
result = calculator.calculate(calc_expr, context)
|
||
|
||
# 打印调试信息
|
||
print(f"计算 {node.get('名称', '未知节点')} 的数量,表达式: {calc_expr}, 结果: {result}")
|
||
|
||
# 处理计算结果
|
||
if hasattr(result, "value"):
|
||
calculated_quantity = result.value
|
||
elif isinstance(result, (int, float)):
|
||
calculated_quantity = result
|
||
else:
|
||
# 尝试转换为浮点数
|
||
try:
|
||
calculated_quantity = float(result)
|
||
except (ValueError, TypeError):
|
||
print(f"无法将计算结果转换为浮点数: {result}")
|
||
calculated_quantity = float(node_copy.get("数量", "1.0") or "1.0")
|
||
|
||
# 如果计算结果为0,使用原始数量
|
||
if calculated_quantity == 0:
|
||
orig_quantity = float(node_copy.get("数量", "0.0") or "0.0")
|
||
if orig_quantity > 0:
|
||
print(f"计算结果为0,使用原始数量: {orig_quantity}")
|
||
calculated_quantity = orig_quantity
|
||
|
||
# 设置计算数量
|
||
node_copy["计算数量"] = str(calculated_quantity)
|
||
except Exception as e:
|
||
print(f"计算节点 '{node.get('名称', '未知节点')}' 的数量时出错: {e}")
|
||
# 使用默认数量
|
||
node_copy["计算数量"] = node_copy.get("数量", "1.0")
|
||
|
||
result_nodes.append(node_copy)
|
||
|
||
return result_nodes
|
||
|
||
# 计算人工节点的数量和合价
|
||
labor_with_count = calc_rcj_count_with_parent(labor_nodes)
|
||
print(f"calc_rcj_count_with_parent 返回节点数量: {len(labor_with_count)}")
|
||
labor_with_cat = cat_rcj_count(labor_with_count)
|
||
print(f"cat_rcj_count 返回节点数量: {len(labor_with_cat)}")
|
||
labor_with_fee = calc_rcj_fee(labor_with_cat)
|
||
print(f"calc_rcj_fee 返回节点数量: {len(labor_with_fee)}")
|
||
labor_formatted = format_rcj_output(labor_with_fee, "人工")
|
||
print(f"format_rcj_output 返回节点数量: {len(labor_formatted)}")
|
||
|
||
# 计算材料节点的数量和合价
|
||
material_with_count = calc_rcj_count_with_parent(material_nodes)
|
||
material_with_cat = cat_rcj_count(material_with_count)
|
||
material_with_fee = calc_rcj_fee(material_with_cat)
|
||
material_formatted = format_rcj_output(material_with_fee, "材料")
|
||
|
||
# 计算机械节点的数量和合价
|
||
machine_with_count = calc_rcj_count_with_parent(machine_nodes)
|
||
machine_with_cat = cat_rcj_count(machine_with_count)
|
||
machine_with_fee = calc_rcj_fee(machine_with_cat)
|
||
machine_formatted = format_rcj_output(machine_with_fee, "机械")
|
||
|
||
# 直接返回合并后的结果,不再按工程量节点分组
|
||
result = {"人工节点": labor_formatted, "材料节点": material_formatted, "机械节点": machine_formatted}
|
||
|
||
# 计算合价只用于调试显示
|
||
labor_fee = sum(float(node.get("预算价合价", "0.0") or "0.0") for node in labor_formatted)
|
||
material_fee = sum(float(node.get("预算价合价", "0.0") or "0.0") for node in material_formatted)
|
||
machine_fee = sum(float(node.get("预算价合价", "0.0") or "0.0") for node in machine_formatted)
|
||
print(f"项目 '{project_name}': 人工合价={labor_fee}, 材料合价={material_fee}, 机械合价={machine_fee}")
|
||
|
||
return result
|
||
|
||
|
||
def calculate_resource_fees(
|
||
json_file_path: str,
|
||
project_name: str,
|
||
project_guid: str = None,
|
||
calculation_strategy=None,
|
||
) -> str:
|
||
"""
|
||
计算人材机合价
|
||
|
||
Args:
|
||
json_file_path: JSON文件路径
|
||
project_name: 项目名称
|
||
project_guid: 项目GUID,用于区分同名项目
|
||
calculation_strategy: 计算策略,如果为None则使用默认策略
|
||
|
||
Returns:
|
||
str: 输出文件路径,如果没有工程量节点则返回None
|
||
"""
|
||
# 如果没有提供计算策略,使用默认策略
|
||
if calculation_strategy is None:
|
||
from calculation_strategy import DefaultCalculationStrategy
|
||
|
||
calculation_strategy = DefaultCalculationStrategy()
|
||
|
||
# 获取项目划分节点的GUID
|
||
_, _, _, _, target_node = load_project_data(json_file_path, project_name, project_guid)
|
||
# 如果传入了GUID但与节点的GUID不一致,优先使用传入的GUID
|
||
node_guid = target_node.get("GUID") or target_node.get("guid", "") if target_node else ""
|
||
project_guid = project_guid if project_guid else node_guid
|
||
|
||
# 将文件名中的非法字符替换为下划线
|
||
safe_project_name = (
|
||
project_name.replace("/", "_")
|
||
.replace("\\", "_")
|
||
.replace(":", "_")
|
||
.replace("*", "_")
|
||
.replace("?", "_")
|
||
.replace('"', "_")
|
||
.replace("<", "_")
|
||
.replace(">", "_")
|
||
.replace("|", "_")
|
||
)
|
||
|
||
# 将GUID中的非法字符替换为下划线
|
||
safe_project_guid = ""
|
||
if project_guid:
|
||
safe_project_guid = (
|
||
project_guid.replace("/", "_")
|
||
.replace("\\", "_")
|
||
.replace(":", "_")
|
||
.replace("*", "_")
|
||
.replace("?", "_")
|
||
.replace('"', "_")
|
||
.replace("<", "_")
|
||
.replace(">", "_")
|
||
.replace("|", "_")
|
||
.replace("{", "")
|
||
.replace("}", "")
|
||
)
|
||
# 添加下划线作为分隔符
|
||
safe_project_guid = f"_{safe_project_guid}"
|
||
|
||
# 设置输出目录和文件名
|
||
output_dir = "计算结果"
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
rcj_output_file = os.path.join(output_dir, f"{safe_project_name}{safe_project_guid}_rcj_fees.json")
|
||
|
||
# 计算人材机节点的合价,传递project_guid参数
|
||
# 这里使用 calculation_strategy 的 calculate_rcj_fees 方法
|
||
if hasattr(calculation_strategy, "calculate_rcj_fees"):
|
||
rcj_results = calculation_strategy.calculate_rcj_fees(json_file_path, project_name, project_guid)
|
||
else:
|
||
# 如果计算策略没有实现 calculate_rcj_fees 方法,使用原始函数
|
||
rcj_results = calculate_rcj_fees(json_file_path, project_name, project_guid)
|
||
|
||
# 检查是否有人材机数据
|
||
has_data = False
|
||
for node_type in ["人工节点", "材料节点", "机械节点"]:
|
||
if rcj_results.get(node_type) and len(rcj_results[node_type]) > 0:
|
||
has_data = True
|
||
break
|
||
|
||
if not has_data:
|
||
print(f"项目 '{project_name}' (GUID: {project_guid}) 没有人材机数据,不保存结果")
|
||
return None
|
||
|
||
# 保存人材机合价计算结果到JSON文件
|
||
with open(rcj_output_file, "w", encoding="utf-8") as f:
|
||
json.dump(rcj_results, f, ensure_ascii=False, indent=2)
|
||
|
||
print(f"\n人材机合价计算结果已保存到 {rcj_output_file}")
|
||
|
||
return rcj_output_file
|