增加了知识图谱导出excel

This commit is contained in:
chentianrui
2025-08-18 15:14:37 +08:00
parent ce2986fbe2
commit 3fd0b2af0c
610 changed files with 6062 additions and 4932473 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ import copy
# 配置logging
logging.basicConfig(
level=logging.INFO,
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.FileHandler("bcl_calculator.log"), logging.StreamHandler()],
)
@@ -10,6 +10,16 @@ class CalculationStrategy(ABC):
每种软件类型可以提供自己的实现,以修改计算过程中的任何规则。
"""
@abstractmethod
def get_output_dir(self) -> str:
"""
获取输出目录路径
Returns:
str: 输出目录路径
"""
pass
@abstractmethod
def calculate_fee_base(
self,
@@ -243,6 +253,17 @@ class DefaultCalculationStrategy(CalculationStrategy):
# 缓存已计算过的费用
self.calculated_fees = {}
# 默认输出目录
self.output_dir = "计算结果"
def get_output_dir(self) -> str:
"""获取输出目录路径"""
return self.output_dir
def set_output_dir(self, output_dir: str) -> None:
"""设置输出目录路径"""
self.output_dir = output_dir
def calculate_fee_base(
self,
fee_base: str,
+16 -3
View File
@@ -23,6 +23,8 @@ class CalculatorBase(ABC):
self.config_dir = software_type.config_dir
# 使用默认计算策略
self.calculation_strategy = self.create_calculation_strategy()
# 默认输出目录
self.output_dir = os.path.join("计算结果", self.software_type.name)
def create_calculation_strategy(self) -> CalculationStrategy:
"""
@@ -41,13 +43,24 @@ class CalculatorBase(ABC):
def get_output_dir(self) -> str:
"""获取输出目录路径"""
base_dir = "计算结果"
return os.path.join(base_dir, self.software_type.name)
return self.output_dir
def set_output_dir(self, output_dir: str) -> None:
"""
设置输出目录路径
Args:
output_dir: 输出目录路径
"""
self.output_dir = output_dir
# 如果计算策略支持设置输出目录,也设置计算策略的输出目录
if hasattr(self.calculation_strategy, "set_output_dir"):
self.calculation_strategy.set_output_dir(output_dir)
def calculate_quantity_fee_tables(
self,
json_file_path: str,
project_name: str,
project_name: str = None,
) -> None:
"""
计算工程量取费表
+15 -6
View File
@@ -356,7 +356,8 @@ def get_quantity_nodes(json_file_path, project_name, engineering_type, project_g
)
node_name = node.get("清单名称", node.get("项目名称", "未命名"))
node_id = node.get("id", node.get("GUID", "未知ID"))
# 强制使用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:
@@ -386,7 +387,8 @@ def get_quantity_nodes(json_file_path, project_name, engineering_type, project_g
# 处理每个清单节点
for bill_node in bill_nodes:
bill_id = bill_node.get("id") or bill_node.get("GUID")
# 强制使用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", [])
@@ -400,7 +402,9 @@ def get_quantity_nodes(json_file_path, project_name, engineering_type, project_g
# 为每个工程量节点设置清单节点信息
for node in bill_quantity_nodes:
# 存储关键信息而不是整个对象
node["bill_id"] = bill_id
# 强制使用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以保持兼容性
@@ -448,7 +452,9 @@ def get_quantity_nodes(json_file_path, project_name, engineering_type, project_g
# 在项目中查找父节点
def find_node_by_id(node_list, node_id):
for n in node_list:
if n.get("id") == node_id or n.get("GUID") == node_id:
# 强制使用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)
@@ -471,10 +477,13 @@ def get_quantity_nodes(json_file_path, project_name, engineering_type, project_g
)
if is_bill_node:
bill_id = parent_node.get("id") or parent_node.get("GUID")
# 强制使用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("项目名称", "未命名清单"))
node["bill_id"] = bill_id
# 强制使用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以保持兼容性
+174 -202
View File
@@ -1,10 +1,4 @@
"""
第二步:计算bcl结果
"""
import os
import argparse
import re
import json
from typing import Dict, List, Any, Optional, Tuple
from equipment_calculation.software_types import (
@@ -16,78 +10,23 @@ from equipment_calculation.software_types import (
from equipment_calculation.software_calculators import get_calculator
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="工程量取费和人材机合价计算程序")
# 软件类型参数
parser.add_argument(
"--category", choices=["主网", "配网", "技改"], default="主网", help="软件类别(主网/配网/技改)"
)
parser.add_argument("--engineering-type", choices=["预算", "清单"], default="清单", help="工程类型(预算/清单)")
# 计算类型参数
parser.add_argument(
"--calculate",
choices=["all", "quantity", "resource"],
default="quantity",
help="计算类型(all: 全部, quantity: 工程量取费, resource: 人材机合价)",
)
# 项目参数
parser.add_argument("--project", help="项目名称,如果不指定则处理所有项目")
parser.add_argument("--adjustment-type", default="调差", help="调差类型,默认为'调差'")
# 输入文件参数
parser.add_argument(
"--input-file",
default="测试案例/主网清单变电.json",
help="输入JSON文件路径,如果不指定则使用默认路径",
)
# 添加输入和输出文件夹参数
parser.add_argument("--input-folder", help="输入文件夹路径,包含要处理的JSON文件")
parser.add_argument("--output-folder", default="计算结果", help="输出文件夹路径,用于保存计算结果")
return parser.parse_args()
def parse_filename(filename: str) -> Tuple[str, str]:
"""
从文件名中解析软件类别和工程类型(备用方法)
:param filename: JSON文件名,例如"主网预算变电.json"
:return: (category, engineering_type) 元组,例如 ("主网", "预算")
"""
# 移除扩展名
basename = os.path.splitext(filename)[0]
# 查找类别(主网/配网/技改)
category = None
for cat in ["主网", "配网", "技改"]:
if cat in basename:
category = cat
break
# 查找工程类型(预算/清单)
engineering_type = None
for eng_type in ["预算", "清单"]:
if eng_type in basename:
engineering_type = eng_type
break
# 如果未找到,使用默认值
if not category:
print(f"警告: 无法从文件名 '{filename}' 中解析软件类别,使用默认值 '主网'")
category = "主网"
if not engineering_type:
print(f"警告: 无法从文件名 '{filename}' 中解析工程类型,使用默认值 '清单'")
engineering_type = "清单"
return category, engineering_type
# 软件类别名称映射字典,将各种变体映射到标准类别
CATEGORY_MAPPING = {
# 主网及其变体
"主网": "主网",
"主网工程": "主网",
"主网项目": "主网",
# 配网及其变体
"配网": "配网",
"配网造价": "配网",
"配网清单": "配网",
# 技改及其变体
"技改": "技改",
"技改工程": "技改",
"技改项目": "技改",
"技改造价": "技改",
"技改清单": "技改",
}
def parse_json_content(json_file_path: str) -> Tuple[Optional[str], Optional[str]]:
@@ -102,8 +41,8 @@ def parse_json_content(json_file_path: str) -> Tuple[Optional[str], Optional[str
data = json.load(f)
# 定义阶段类型映射表
budget_types = ["概预算", "定额", "", ""]
list_types = ["清单", "结算", "招标控制价", "招投标工程"]
budget_types = ["概预算", "定额", "定额计价", "", "预算工程"]
list_types = ["清单", "结算", "招标控制价", "招投标工程", "清单计价"]
# 从division字段获取软件名称和阶段类型
if "division" in data:
@@ -112,28 +51,39 @@ def parse_json_content(json_file_path: str) -> Tuple[Optional[str], Optional[str
# 使用-分割division字段
parts = division.split("-")
if len(parts) >= 2:
if len(parts) == 2:
category = parts[0].strip()
stage_type = parts[1].strip()
# 验证软件类别
if category not in ["主网", "配网", "技改"]:
print(f"警告: division中的软件名称 '{category}' 不是有效值,将使用默认值 '主网'")
category = "主网"
# 映射阶段类型
if any(budget_type in stage_type for budget_type in budget_types):
engineering_type = "预算"
elif any(list_type in stage_type for list_type in list_types):
engineering_type = "清单"
else:
print(f"警告: division中的阶段类型 '{stage_type}' 无法映射到预算或清单,将使用默认值 '清单'")
engineering_type = "清单"
print(f"从division解析: 软件名称={category}, 阶段类型={engineering_type} (原始值: {stage_type})")
return category, engineering_type
elif len(parts) == 3:
category = parts[0].strip()
stage_type = parts[2].strip()
else:
print(f"警告: division字段 '{division}' 格式不正确,无法分割")
# 可以选择返回默认值或抛出异常,这里以返回默认值为例
category = "主网"
engineering_type = "清单"
print(f"使用默认值: 软件名称={category}, 阶段类型={engineering_type}")
return category, engineering_type
# 使用映射字典规范化软件类别
if category in CATEGORY_MAPPING:
category = CATEGORY_MAPPING[category]
else:
print(f"警告: division中的软件名称 '{category}' 不是有效值,将使用默认值 '主网'")
category = "主网"
# 映射阶段类型
if any(budget_type in stage_type for budget_type in budget_types):
engineering_type = "预算"
elif any(list_type in stage_type for list_type in list_types):
engineering_type = "清单"
else:
print(f"警告: division中的阶段类型 '{stage_type}' 无法映射到预算或清单,将使用默认值 '清单'")
engineering_type = "清单"
print(f"从division解析: 软件名称={category}, 阶段类型={engineering_type} (原始值: {stage_type})")
return category, engineering_type
else:
print(f"警告: JSON文件中未找到division字段,尝试从basicData中解析")
@@ -145,8 +95,10 @@ def parse_json_content(json_file_path: str) -> Tuple[Optional[str], Optional[str
# 验证解析结果
if category and engineering_type:
# 确保category是有效值
if category not in ["主网", "配网", "技改"]:
# 使用映射字典规范化软件类别
if category in CATEGORY_MAPPING:
category = CATEGORY_MAPPING[category]
else:
print(f"警告: basicData中的软件名称 '{category}' 不是有效值,将使用默认值 '主网'")
category = "主网"
@@ -170,146 +122,166 @@ def parse_json_content(json_file_path: str) -> Tuple[Optional[str], Optional[str
def process_json_file(
input_file: str,
base_output_dir: str = "计算结果",
category: Optional[str] = None,
engineering_type: Optional[str] = None,
project: Optional[str] = None,
adjustment_type: str = "调差",
json_file_path: str, output_dir: str, calculate_type: str = "all", project_name: str = None
) -> bool:
"""
处理单个JSON文件
:param input_file: 输入JSON文件路径
:param base_output_dir: 基础输出目录路径,实际结果会保存在此目录下的子文件夹中
:param category: 软件类别,如果为None则从JSON内容中解析
:param engineering_type: 工程类型,如果为None则从JSON内容中解析
:param project: 项目名称,如果为None则处理所有项目
:param adjustment_type: 调差类型
:return: 处理是否成功
Args:
json_file_path: JSON文件路径
output_dir: 输出目录
calculate_type: 计算类型(all: 全部, quantity: 工程量取费, resource: 人材机合价)
project_name: 项目名称,如果不指定则处理所有项目
Returns:
bool: 处理是否成功
"""
try:
# 获取文件名(不含扩展名)作为输出子文件夹的名称
filename = os.path.basename(input_file)
file_basename = os.path.splitext(filename)[0]
output_dir = os.path.join(base_output_dir, file_basename)
# 从JSON文件内容中解析软件类别和工程类型
category, engineering_type = parse_json_content(json_file_path)
# 创建输出子文件夹
os.makedirs(output_dir, exist_ok=True)
print(f"创建输出目录: {output_dir}")
# 如果解析失败,使用默认值
if category is None:
category = "主网"
print(f"无法从文件中解析软件类别,使用默认值: {category}")
# 如果未指定category或engineering_type,从JSON内容中解析
if category is None or engineering_type is None:
# 首先尝试从JSON内容中解析
json_category, json_engineering_type = parse_json_content(input_file)
if category is None:
if json_category:
category = json_category
else:
# 如果从JSON内容中解析失败,尝试从文件名中解析(作为备用)
print("从JSON内容解析软件名称失败,尝试从文件名解析...")
parsed_category, _ = parse_filename(filename)
category = parsed_category
if engineering_type is None:
if json_engineering_type:
engineering_type = json_engineering_type
else:
# 如果从JSON内容中解析失败,尝试从文件名中解析(作为备用)
print("从JSON内容解析阶段类型失败,尝试从文件名解析...")
_, parsed_engineering_type = parse_filename(filename)
engineering_type = parsed_engineering_type
print(f"处理文件: {input_file}")
print(f" 软件类别: {category}")
print(f" 工程类型: {engineering_type}")
if engineering_type is None:
engineering_type = "预算"
print(f"无法从文件中解析工程类型,使用默认值: {engineering_type}")
# 获取软件类型
software_type = get_software_type(category, engineering_type)
print(f" 使用软件类型: {software_type.name}")
print(f"使用软件类型: {software_type.name}")
# 获取计算器
calculator = get_calculator(software_type)
if not calculator:
print(f" 错误: 未找到软件类型 {software_type.name} 的计算器")
print(f"错误: 未找到软件类型 {software_type.name} 的计算器")
return False
# 设置输出目录
calculator.set_output_dir(output_dir)
# 获取文件名(不含扩展名),并替换空格和特殊字符
base_filename = os.path.basename(json_file_path).replace(".json", "")
# 创建安全的目录名(替换空格和特殊字符)
safe_dirname = base_filename.replace(" ", "_").replace("\\", "_").replace("/", "_")
# 执行计算
print(" 开始计算工程量取费表...")
calculator.calculate_quantity_fee_tables(json_file_path=input_file, project_name=project)
# 设置自定义输出目录
custom_output_dir = os.path.join(output_dir, safe_dirname)
print(" 开始计算人材机合价...")
calculator.calculate_resource_fee_tables(json_file_path=input_file, project_name=project)
# 确保输出目录存在
try:
os.makedirs(custom_output_dir, exist_ok=True)
print(f"创建输出目录: {custom_output_dir}")
except Exception as e:
print(f"创建输出目录失败: {str(e)}")
# 尝试使用更安全的目录名
safe_dirname = f"project_{hash(base_filename) % 10000}"
custom_output_dir = os.path.join(output_dir, safe_dirname)
os.makedirs(custom_output_dir, exist_ok=True)
print(f"使用备用输出目录: {custom_output_dir}")
print(f" 处理完成: {input_file}")
print(f" 结果保存在: {output_dir}")
# 检查计算器是否有set_output_dir方法,如果有则直接设置输出目录
if hasattr(calculator, "set_output_dir"):
calculator.set_output_dir(custom_output_dir)
print(f"已设置计算器输出目录: {custom_output_dir}")
# 根据计算类型执行计算
if calculate_type in ["all", "quantity"]:
print("开始计算工程量取费表...")
calculator.calculate_quantity_fee_tables(
json_file_path=json_file_path,
project_name=project_name,
)
if calculate_type in ["all", "resource"]:
print("开始计算人材机合价...")
calculator.calculate_resource_fee_tables(json_file_path=json_file_path, project_name=project_name)
else:
# 创建一个新的计算器,复制原始计算器的属性
class CustomOutputCalculator:
def __init__(self, original_calculator, custom_dir):
self.original_calculator = original_calculator
self.custom_dir = custom_dir
def get_output_dir(self):
return self.custom_dir
def __getattr__(self, name):
return getattr(self.original_calculator, name)
# 创建自定义输出目录的计算器
custom_calculator = CustomOutputCalculator(calculator, custom_output_dir)
# 根据计算类型执行计算
if calculate_type in ["all", "quantity"]:
print("开始计算工程量取费表...")
custom_calculator.calculate_quantity_fee_tables(
json_file_path=json_file_path,
project_name=project_name,
)
if calculate_type in ["all", "resource"]:
print("开始计算人材机合价...")
custom_calculator.calculate_resource_fee_tables(
json_file_path=json_file_path, project_name=project_name
)
print(f"文件 {json_file_path} 处理完成")
return True
except Exception as e:
print(f" 处理文件 {input_file} 时出错: {str(e)}")
print(f"处理文件 {json_file_path} 时出错: {str(e)}")
import traceback
traceback.print_exc()
return False
def process_BCL_calculate(input_folder: str, output_folder: str) -> List[Tuple[str, bool]]:
def process_directory(input_dir: str, output_dir: str, calculate_type: str = "all") -> None:
"""
处理指定文件夹中的所有JSON文件
批量处理目录中的JSON文件
:param input_folder: 输入文件夹路径,包含要处理的JSON文件
:param output_folder: 输出文件夹路径,用于保存计算结果
:return: 处理结果列表,格式为 [(文件路径, 是否成功), ...]
Args:
input_dir: 输入目录路径
output_dir: 输出目录路径
calculate_type: 计算类型(all: 全部, quantity: 工程量取费, resource: 人材机合价)
"""
# 确保基础输出目录存在
os.makedirs(output_folder, exist_ok=True)
# 确保输出目录存在
os.makedirs(output_dir, exist_ok=True)
# 查找所有JSON文件
json_files = []
for file in os.listdir(input_folder):
if file.lower().endswith(".json"):
json_files.append(os.path.join(input_folder, file))
# 获取目录中的所有JSON文件
json_files = [f for f in os.listdir(input_dir) if f.lower().endswith(".json")]
if not json_files:
print(f"警告: 在目录 {input_folder}没有找到JSON文件")
return []
print(f"警告: 在目录 {input_dir}找到JSON文件")
return
print(f"找到 {len(json_files)} 个JSON文件,开始处理...")
# 处理每个JSON文件
results = []
for input_file in json_files:
success = process_json_file(
input_file=input_file,
base_output_dir=output_folder, # 传递基础输出目录
category=None, # 从JSON内容中解析
engineering_type=None, # 从JSON内容中解析
project=None, # 处理所有项目
adjustment_type="调差",
)
success_count = 0
for i, json_file in enumerate(json_files, 1):
json_file_path = os.path.join(input_dir, json_file)
print(f"处理文件 {i}/{len(json_files)}: {json_file}")
results.append((input_file, success))
if process_json_file(json_file_path, output_dir, calculate_type):
success_count += 1
return results
print(f"处理完成,成功: {success_count}/{len(json_files)}")
def main():
"""程序入口"""
def bcl_calculate(input_dir: str, output_dir: str, calculate_type: str = "all") -> None:
"""
主函数,处理指定目录中的所有JSON文件
input_folder = "project2json/outputs/json"
output_folder = "project2json/outputs/bcl_results"
Args:
input_dir: 输入目录路径
output_dir: 输出目录路径
calculate_type: 计算类型(all: 全部, quantity: 工程量取费, resource: 人材机合价)
"""
print(f"开始处理目录: {input_dir}")
print(f"输出目录: {output_dir}")
print(f"计算类型: {calculate_type}")
results = process_BCL_calculate(input_folder, output_folder)
process_directory(input_dir, output_dir, calculate_type)
# 显示处理结果
success_count = sum(1 for _, success in results if success)
print(f"\n处理完成: 成功 {success_count}/{len(results)} 个文件")
if success_count < len(results):
print("处理失败的文件:")
for file_path, success in results:
if not success:
print(f" - {os.path.basename(file_path)}")
if __name__ == "__main__":
main()
print("所有文件处理完成")
File diff suppressed because it is too large Load Diff
+136 -104
View File
@@ -28,11 +28,12 @@ from equipment_calculation.item_acquisition import (
# 缓存已计算过的费用
calculated_fees = {}
# 添加一个全局变量和一个缓存字典来存储清单数量
_BILL_QUANTITY = 1.0
bill_quantity_cache = {}
# # 添加一个全局变量和一个缓存字典来存储清单数量
# _BILL_QUANTITY = 1.0
# bill_quantity_cache = {}
# 主网处理地形系数
def process_DXdata(json_data):
"""
处理 projectData 中的线路特征段数据,计算每条 bpBillZhXsTable 记录的加权值,
@@ -102,40 +103,39 @@ def process_DXdata(json_data):
# 在create_list_from_node函数后添加一个包装函数
def create_list_from_node_with_quantity(node, quantity=None):
"""创建清单对象并设置数量"""
def create_list_from_node_with_bill_quantity(node, quantity=None):
"""创建清单对象并设置数量,不使用全局变量"""
from equipment_calculation.bcl_utils import create_list_from_node
bill_obj = create_list_from_node(node)
# 设置清单数量
bill_quantity = quantity if quantity is not None else node.get("数量")
if quantity is not None:
bill_quantity = quantity
elif "数量" in node:
try:
bill_quantity = float(node["数量"])
except (ValueError, TypeError):
bill_quantity = 1.0
else:
bill_quantity = 1.0
# 设置到对象属性
bill_obj.quantity = bill_quantity
# 保存到全局变量和缓存
global _BILL_QUANTITY
_BILL_QUANTITY = bill_quantity
# 如果节点有ID,保存到缓存
bill_id = node.get("id") or node.get("GUID")
if bill_id:
bill_quantity_cache[bill_id] = bill_quantity
# 设置环境变量作为后备方案
os.environ["BILL_QUANTITY"] = str(bill_quantity)
if hasattr(bill_obj, "quantity"):
bill_obj.quantity = bill_quantity
else:
# 如果对象没有quantity属性,尝试设置到其他可能的属性
try:
setattr(bill_obj, "quantity", bill_quantity)
except:
pass
print(f"设置清单对象数量: {bill_quantity}")
print(f"全局变量_BILL_QUANTITY: {_BILL_QUANTITY}")
if bill_id:
print(f"缓存清单{bill_id}的数量: {bill_quantity_cache[bill_id]}")
return bill_obj
# 递归查找取费表中包含"取费基数"的节点
def find_fee_base_nodes(node: Dict[str, Any], result: List = None) -> List[Dict[str, Any]]:
def find_fee_base_nodes(node: Dict[str, Any], result: List[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
"""
递归查找取费表中包含"取费基数"的节点或有子节点的费用项
@@ -449,21 +449,11 @@ def calculate_all_fees(
calculation_strategy=None,
) -> Dict[str, float]:
"""
计算项目节点的所有费用
Args:
project_node: 工程量节点
cost_table: 取费表
json_file_path: JSON文件路径,用于获取工程信息
engineering_type: 工程类型,如"清单工程""预算工程"
calculation_strategy: 计算策略,如果为None则使用默认策略
Returns:
Dict[str, float]: 费用计算结果
计算项目节点的所有费用,确保清单数量正确传递
"""
results = {}
# 1. 工程信息上下文 ,在这里暂时先将参数项目划分名称固定
# 1. 工程信息上下文
project_context = create_project_contexts(json_file_path=json_file_path)
with open(json_file_path, "r", encoding="utf-8") as file:
@@ -478,9 +468,9 @@ def calculate_all_fees(
# 获取清单节点
bill_id = project_node.get("bill_id")
bill_name = project_node.get("bill_name")
bill_node = project_node.get("bill_node")
if not bill_node:
bill_node = {
bill_node_data = project_node.get("bill_node")
if not bill_node_data:
bill_node_data = {
"id": bill_id,
"GUID": bill_id,
"清单名称": bill_name,
@@ -491,16 +481,9 @@ def calculate_all_fees(
}
# 使用包装函数创建清单对象
# bill_obj = create_list_from_node_with_quantity(bill_node)
bill_obj = create_list_from_node(bill_node)
# 如果清单节点有数量,确保它被保存到bill_obj
if "数量" in bill_node:
bill_obj.quantity = float(bill_node["数量"])
print(f"设置清单对象数量: {bill_obj.quantity}")
bill_obj = create_list_from_node_with_bill_quantity(bill_node_data)
# 创建工程量对象
from equipment_calculation.bcl_utils import create_node_from_type
project_obj = create_node_from_type(project_node)
dxitem_context = BCLDataSourceContext(DXITEM, project_context)
@@ -508,17 +491,23 @@ def calculate_all_fees(
# 递归数据源链式上下文
billItem = BCLDataSourceItem(bill_obj)
# 设置一个显式的数量属性
# billItem.bill_quantity = float(bill_node.get("数量", 1.0))
# print(f"设置清单数据源项数量: {billItem.bill_quantity}")
# 将清单数量直接设置到billItem对象上
if hasattr(bill_obj, "quantity"):
billItem.bill_quantity = bill_obj.quantity
print(f"设置清单数据源项数量: {billItem.bill_quantity}")
QuantityItem = BCLDataSourceItem(project_obj, billItem)
bill_context = BCLDataSourceContext([billItem], dxitem_context)
context = BCLDataSourceContext([QuantityItem], bill_context)
# 如果计算策略存在,设置清单数量
if calculation_strategy and hasattr(calculation_strategy, "set_bill_quantity"):
if hasattr(bill_obj, "quantity"):
calculation_strategy.set_bill_quantity(bill_obj.quantity)
print(f"设置计算策略的清单数量: {bill_obj.quantity}")
else:
# 非清单工程 - 工程信息 -> 工程量节点
from equipment_calculation.bcl_utils import create_node_from_type
project_obj = create_node_from_type(project_node)
dxitem_context = BCLDataSourceContext(DXITEM, project_context)
@@ -527,16 +516,10 @@ def calculate_all_fees(
QuantityItem = BCLDataSourceItem(project_obj)
context = BCLDataSourceContext([QuantityItem], dxitem_context)
# 打印项目节点信息,用于调试
node_name = project_node.get("项目名称", project_node.get("name", "未知节点"))
print(f"\n处理项目节点: {node_name}")
# 查找包含取费基数的节点
fee_base_nodes = find_fee_base_nodes(cost_table)
# 计算每个费用项
# 先计算基本费用项,再计算复合费用项
# 这里简单按照节点在列表中的顺序计算,可能需要更复杂的依赖解析
for node in fee_base_nodes:
fee_name = node.get("费用名称", node.get("name", "未知费用"))
fee_code = node.get("代码", "")
@@ -715,17 +698,7 @@ def calculate_quantity_fees(
calculation_strategy=None,
) -> str:
"""
计算工程量取费表
Args:
json_file_path: JSON文件路径
project_name: 项目名称
engineering_type: 工程类型
project_guid: 项目GUID,用于区分同名项目
calculation_strategy: 计算策略,如果为None则使用默认策略
Returns:
str: 输出文件路径
计算工程量取费表,不使用全局变量,并按清单节点组织结果
"""
# 如果没有提供计算策略,使用默认策略
if calculation_strategy is None:
@@ -734,7 +707,13 @@ def calculate_quantity_fees(
calculation_strategy = DefaultCalculationStrategy()
# 设置输出目录和文件名
output_dir = "计算结果"
# 检查calculation_strategy是否有get_output_dir方法
if hasattr(calculation_strategy, "get_output_dir"):
output_dir = calculation_strategy.get_output_dir()
else:
# 兼容旧代码,使用默认目录
output_dir = "计算结果"
os.makedirs(output_dir, exist_ok=True)
# 获取项目划分节点的GUID
@@ -781,8 +760,10 @@ def calculate_quantity_fees(
f"{safe_project_name}{safe_project_guid}_{engineering_type}_calculation_results.json",
)
# 在处理清单工程时,保存清单数量供后续使用
# 创建一个字典来存储清单数量
bill_quantities = {}
# 创建一个字典来存储清单信息
bill_info = {}
# 根据工程类型获取取费表和项目节点
if engineering_type == "预算工程":
@@ -809,6 +790,7 @@ def calculate_quantity_fees(
# 获取清单节点信息
bill_id = project_node.get("bill_id")
bill_name = project_node.get("bill_name", "未命名清单")
fee_table_name = project_node.get("取费表名称")
if not bill_id:
@@ -819,9 +801,17 @@ def calculate_quantity_fees(
print(f"工程量节点 '{node_name}' 没有取费表名称")
continue
# 保存清单信息
if bill_id not in bill_info:
bill_info[bill_id] = {
"name": bill_name,
"fee_table_name": fee_table_name,
"guid": bill_id.replace("{", "").replace("}", "") if bill_id else "",
}
# 使用清单节点的取费表名称直接查找取费表
if bill_id not in cost_tables:
print(f"查找清单节点 '{project_node.get('bill_name', '未命名清单')}' 的取费表: {fee_table_name}")
print(f"查找清单节点 '{bill_name}' 的取费表: {fee_table_name}")
# 查找取费表
with open(json_file_path, "r", encoding="utf-8") as f:
@@ -843,7 +833,7 @@ def calculate_quantity_fees(
continue
cost_tables[bill_id] = cost_table_children
print(f"成功获取清单节点 '{project_node.get('bill_name', '未命名清单')}' 的取费表")
print(f"成功获取清单节点 '{bill_name}' 的取费表")
# 在读取JSON文件后,遍历项目中的清单节点,获取并保存清单数量
with open(json_file_path, "r", encoding="utf-8") as f:
@@ -853,10 +843,7 @@ def calculate_quantity_fees(
bill_data = project_data.get("bill", {})
# 递归函数获取所有清单节点
def get_bill_nodes(node, results=None):
if results is None:
results = []
def get_bill_nodes(node):
if isinstance(node, dict):
# 检查是否是清单节点
is_bill = (
@@ -864,39 +851,36 @@ def calculate_quantity_fees(
)
if is_bill and ("id" in node or "GUID" in node):
bill_id = node.get("id") or node.get("GUID")
# 强制使用GUID而不是ID
bill_id = node.get("GUID") or node.get("guid") or node.get("id")
quantity = 1.0 # 默认值
if "数量" in node:
try:
quantity = float(node["数量"])
except (TypeError, ValueError):
pass
results.append((bill_id, quantity))
# 保存到字典
bill_quantities[bill_id] = quantity
# 直接设置到全局变量和缓存
global _BILL_QUANTITY
_BILL_QUANTITY = quantity
bill_quantity_cache[bill_id] = quantity
# 补充清单信息
if bill_id not in bill_info:
bill_info[bill_id] = {
"name": node.get("清单名称", "未命名清单"),
"fee_table_name": node.get("取费表名称", ""),
"guid": bill_id.replace("{", "").replace("}", "") if bill_id else "",
}
# 设置环境变量
os.environ["BILL_QUANTITY"] = str(quantity)
print(f"设置清单 {bill_id} 的数量为全局变量: {quantity}")
print(f"保存清单 {bill_id} 的数量: {quantity}")
# 递归处理子节点
if "children" in node and isinstance(node["children"], list):
for child in node["children"]:
get_bill_nodes(child, results)
return results
get_bill_nodes(child)
# 获取和设置清单数量
get_bill_nodes(bill_data)
# 如果有计算策略,设置清单数量
if calculation_strategy and hasattr(calculation_strategy, "set_bill_quantity"):
calculation_strategy.set_bill_quantity(_BILL_QUANTITY)
else:
print(f"不支持的工程类型: {engineering_type}")
return None
@@ -919,41 +903,89 @@ def calculate_quantity_fees(
# 初始化缓存
calculated_fees.clear() # 使用全局缓存,而不是计算策略中的缓存
# 计算每个项目节点的费用
all_results = {}
# 创建一个新的结果结构,按清单节点组织
structured_results = {}
# 辅助函数:处理ID,移除花括号并保留完整ID
def process_id(id_str):
if not id_str:
return ""
# 移除花括号
return id_str.replace("{", "").replace("}", "")
# 计算每个项目节点的费用
for project_node in project_children:
# 为每个节点清空缓存,确保计算独立
calculated_fees.clear()
node_name = project_node.get("项目名称", project_node.get("name", "未知节点"))
# 强制使用GUID而不是ID
node_id = project_node.get("GUID") or project_node.get("guid") or project_node.get("id", "")
# 添加清单数量信息
# 处理节点ID
processed_node_id = process_id(node_id)
# 创建唯一的节点键
node_key = node_name
if processed_node_id:
node_key = f"{node_name}_{processed_node_id}"
# 添加清单数量信息到项目节点
if engineering_type == "清单工程":
bill_id = project_node.get("bill_id")
# 强制使用GUID而不是ID
bill_id = project_node.get("bill_guid") or project_node.get("bill_id")
if bill_id in bill_quantities:
project_node["bill_quantity"] = bill_quantities[bill_id]
print(f"为工程量节点 '{node_name}' 设置清单数量: {bill_quantities[bill_id]}")
# 如果计算策略支持,设置清单数量
if calculation_strategy and hasattr(calculation_strategy, "set_bill_quantity"):
calculation_strategy.set_bill_quantity(bill_quantities[bill_id])
# 根据工程类型获取对应的取费表
if engineering_type == "预算工程":
# 预算工程 - 使用项目划分节点的取费表
node_results = calculation_strategy.calculate_all_fees(
project_node, {"children": cost_table_children}, json_file_path, engineering_type
)
# 直接使用节点键作为结果键
structured_results[node_key] = node_results
else: # 清单工程
# 清单工程 - 使用清单节点的取费表
parent_id = project_node.get("parent_id")
if parent_id and parent_id in cost_tables:
# 强制使用GUID而不是ID
bill_id = project_node.get("bill_guid") or project_node.get("bill_id")
if bill_id and bill_id in cost_tables:
node_results = calculation_strategy.calculate_all_fees(
project_node, {"children": cost_tables[parent_id]}, json_file_path, engineering_type
project_node, {"children": cost_tables[bill_id]}, json_file_path, engineering_type
)
# 获取清单信息
bill_name = bill_info.get(bill_id, {}).get("name", "未命名清单")
# 处理清单ID - 强制使用GUID
processed_bill_id = process_id(bill_id)
# 创建清单的唯一键 - 强制使用GUID
bill_key = bill_name
if processed_bill_id:
# 获取清单的guid而不是id
bill_guid = bill_info.get(bill_id, {}).get("guid", processed_bill_id)
if not bill_guid:
bill_guid = processed_bill_id
bill_key = f"{bill_name}_{bill_guid}"
# 如果清单节点不存在于结果中,创建它
if bill_key not in structured_results:
structured_results[bill_key] = {}
# 将工程量节点的计算结果添加到对应的清单节点下
structured_results[bill_key][node_key] = node_results
else:
print(f"无法获取工程量节点 '{node_name}' 的取费表")
continue
all_results[node_name] = node_results
# 输出计算结果
print(f"费用计算结果:")
for fee_name, fee_value in node_results.items():
@@ -961,7 +993,7 @@ def calculate_quantity_fees(
# 保存计算结果到JSON文件
with open(output_file, "w", encoding="utf-8") as f:
json.dump(all_results, f, ensure_ascii=False, indent=2)
json.dump(structured_results, f, ensure_ascii=False, indent=2)
print(f"\n计算结果已保存到 {output_file}")
@@ -280,12 +280,12 @@ def calc_rcj_count(
context = BCLDataSourceContext([QuantityItem, moeItem], project_context)
# 根据节点类型选择不同的计算表达式
node_type = node.get("类型", "")
if node_type == "人工":
node_type = node.get("类型", node.get("type", ""))
if node_type == "人工" or node_type == "2":
calc_expr = "_材机合并人工数量"
elif node_type == "材料":
elif node_type == "材料" or node_type == "3":
calc_expr = "_材机合并材料数量"
elif node_type == "机械":
elif node_type == "机械" or node_type == "4":
calc_expr = "_材机合并机械数量"
else:
# 未知类型,使用默认数量
@@ -730,7 +730,7 @@ def calculate_rcj_fees(
f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 有材机列表,数量: {len(node['材机列表'])}"
)
for rcj_node in node["材机列表"]:
node_type = rcj_node.get("类型")
node_type = rcj_node.get("类型", rcj_node.get("type"))
# 同时支持数字类型和字符串类型
if node_type in ["人工", "2"]:
labor_nodes.append((rcj_node, node))
@@ -756,7 +756,7 @@ def calculate_rcj_fees(
f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 有子节点,数量: {len(node['children'])}"
)
for child in node["children"]:
child_type = child.get("类型")
child_type = child.get("类型", child.get("type"))
if child_type in ["人工", "2"]:
labor_nodes.append((child, node))
print(
@@ -893,7 +893,7 @@ def calculate_rcj_fees(
context = BCLDataSourceContext([moeItem], dxitem_context)
# 根据节点类型选择不同的计算表达式
node_type = node.get("类型", "")
node_type = node.get("类型", node.get("type", ""))
if node_type in ["人工", "2"]:
calc_expr = "_材机合并人工数量"
elif node_type in ["材料", "3"]:
@@ -1042,7 +1042,13 @@ def calculate_resource_fees(
safe_project_guid = f"_{safe_project_guid}"
# 设置输出目录和文件名
output_dir = "计算结果"
# 检查calculation_strategy是否有get_output_dir方法
if hasattr(calculation_strategy, "get_output_dir"):
output_dir = calculation_strategy.get_output_dir()
else:
# 兼容旧代码,使用默认目录
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")
+73 -131
View File
@@ -15,9 +15,9 @@ from typing import Any, Dict, Optional, Set
import sys
import re
# 添加一个全局变量和一个缓存字典来存储清单数量
_BILL_QUANTITY = 1.0
bill_quantity_cache = {}
# 移除全局变量声明
# _BILL_QUANTITY = 1.0
# bill_quantity_cache = {}
# 为每种软件类型定义特定的计算策略
@@ -90,46 +90,42 @@ class MainGridBillCalculationStrategy(DefaultCalculationStrategy):
result = calculator.calculate(var_name, context)
# 获取清单数量 - 首先从全局变量获取
global _BILL_QUANTITY # 在模块级别定义一个全局变量
parent_quantity = _BILL_QUANTITY if "_BILL_QUANTITY" in globals() else 1.0
# 获取清单数量 - 首先尝试从上下文中获取
parent_quantity = 1.0 # 默认值
print(f"\n===== 获取清单数量 =====")
print(f"从全局变量获取清单数量: {parent_quantity}")
# 如果全局变量没有设置正确的值,尝试从bill_quantity_cache获取
if parent_quantity == 1.0 and hasattr(context, "__dict__"):
module = sys.modules.get("quantity_fee_calculator")
if module and hasattr(module, "bill_quantity_cache"):
bill_cache = getattr(module, "bill_quantity_cache")
bill_id = None
# 1. 首先尝试从计算策略中获取
if hasattr(self, "bill_quantity"):
parent_quantity = self.bill_quantity
print(f"从计算策略获取清单数量: {parent_quantity}")
# 尝试从上下文中获取bill_id
if hasattr(context, "bill_id"):
bill_id = context.bill_id
elif hasattr(context, "datasource") and context.datasource:
for item in context.datasource:
if hasattr(item, "object") and hasattr(item.object, "bill_id"):
bill_id = item.object.bill_id
# 2. 如果计算策略中没有,尝试从上下文中获取
if parent_quantity == 1.0 and context:
# 尝试从上下文的数据源中获取
if hasattr(context, "datasource") and context.datasource:
for item in context.datasource:
# 检查是否有bill_quantity属性
if hasattr(item, "bill_quantity"):
parent_quantity = item.bill_quantity
print(f"从数据源项获取清单数量: {parent_quantity}")
break
# 检查是否有object属性,且object有quantity属性
if hasattr(item, "object") and hasattr(item.object, "quantity"):
parent_quantity = item.object.quantity
print(f"从数据源项对象获取清单数量: {parent_quantity}")
break
# 尝试从上下文的父级上下文中获取
if parent_quantity == 1.0 and hasattr(context, "prevContext"):
prev_context = context.prevContext
if prev_context and hasattr(prev_context, "datasource") and prev_context.datasource:
for item in prev_context.datasource:
if hasattr(item, "object") and hasattr(item.object, "quantity"):
parent_quantity = item.object.quantity
print(f"从父级上下文获取清单数量: {parent_quantity}")
break
# 如果找到bill_id并且在缓存中,使用缓存的数量
if bill_id and bill_id in bill_cache:
parent_quantity = bill_cache[bill_id]
print(f"从bill_quantity_cache获取清单{bill_id}的数量: {parent_quantity}")
# 设置一个临时环境变量作为最后的手段
if parent_quantity == 1.0:
import os
env_quantity = os.environ.get("BILL_QUANTITY")
if env_quantity:
try:
parent_quantity = float(env_quantity)
print(f"从环境变量获取清单数量: {parent_quantity}")
except:
pass
print(f"===== 获取清单数量结束 =====\n")
# 将结果乘以父级清单的数量
@@ -260,20 +256,18 @@ class TechnicalRenovationBudgetCalculationStrategy(DefaultCalculationStrategy):
class TechnicalRenovationBillCalculationStrategy(DefaultCalculationStrategy):
"""技改清单计算策略"""
# def calculate_fee_base(
# self, fee_base: str, cost_table: dict, project_node: dict, context, in_calculation=None
# ) -> float:
# """计算取费基数,可以重写以添加技改清单特定的计算规则"""
# # 示例:如果取费基数是特定值,使用特定的计算规则
# if fee_base == "人工费+材料费+机械费":
# print(f"应用技改清单特定的取费基数计算规则: {fee_base}")
# # 这里可以添加特定的计算逻辑
def __init__(self):
super().__init__()
self.bill_quantity = 1.0 # 默认值
# # 暂时仍然使用默认实现
# return super().calculate_fee_base(fee_base, cost_table, project_node, context, in_calculation)
def set_bill_quantity(self, quantity):
"""设置清单数量"""
try:
self.bill_quantity = float(quantity)
print(f"设置计算策略的清单数量: {self.bill_quantity}")
except (TypeError, ValueError):
print(f"无法将 {quantity} 转换为浮点数")
# # 对于其他取费基数,使用默认实现
# return super().calculate_fee_base(fee_base, cost_table, project_node, context, in_calculation)
def calculate_external_variable(self, var_name: str, context: Any) -> float:
"""计算表外变量,并乘以父级清单的数量"""
# 使用默认实现计算表外变量
@@ -281,46 +275,42 @@ class TechnicalRenovationBillCalculationStrategy(DefaultCalculationStrategy):
result = calculator.calculate(var_name, context)
# 获取清单数量 - 首先从全局变量获取
global _BILL_QUANTITY # 在模块级别定义一个全局变量
parent_quantity = _BILL_QUANTITY if "_BILL_QUANTITY" in globals() else 1.0
# 获取清单数量 - 首先尝试从上下文中获取
parent_quantity = 1.0 # 默认值
print(f"\n===== 获取清单数量 =====")
print(f"从全局变量获取清单数量: {parent_quantity}")
# 如果全局变量没有设置正确的值,尝试从bill_quantity_cache获取
if parent_quantity == 1.0 and hasattr(context, "__dict__"):
module = sys.modules.get("quantity_fee_calculator")
if module and hasattr(module, "bill_quantity_cache"):
bill_cache = getattr(module, "bill_quantity_cache")
bill_id = None
# 1. 首先尝试从计算策略中获取
if hasattr(self, "bill_quantity"):
parent_quantity = self.bill_quantity
print(f"从计算策略获取清单数量: {parent_quantity}")
# 尝试从上下文中获取bill_id
if hasattr(context, "bill_id"):
bill_id = context.bill_id
elif hasattr(context, "datasource") and context.datasource:
for item in context.datasource:
if hasattr(item, "object") and hasattr(item.object, "bill_id"):
bill_id = item.object.bill_id
# 2. 如果计算策略中没有,尝试从上下文中获取
if parent_quantity == 1.0 and context:
# 尝试从上下文的数据源中获取
if hasattr(context, "datasource") and context.datasource:
for item in context.datasource:
# 检查是否有bill_quantity属性
if hasattr(item, "bill_quantity"):
parent_quantity = item.bill_quantity
print(f"从数据源项获取清单数量: {parent_quantity}")
break
# 检查是否有object属性,且object有quantity属性
if hasattr(item, "object") and hasattr(item.object, "quantity"):
parent_quantity = item.object.quantity
print(f"从数据源项对象获取清单数量: {parent_quantity}")
break
# 尝试从上下文的父级上下文中获取
if parent_quantity == 1.0 and hasattr(context, "prevContext"):
prev_context = context.prevContext
if prev_context and hasattr(prev_context, "datasource") and prev_context.datasource:
for item in prev_context.datasource:
if hasattr(item, "object") and hasattr(item.object, "quantity"):
parent_quantity = item.object.quantity
print(f"从父级上下文获取清单数量: {parent_quantity}")
break
# 如果找到bill_id并且在缓存中,使用缓存的数量
if bill_id and bill_id in bill_cache:
parent_quantity = bill_cache[bill_id]
print(f"从bill_quantity_cache获取清单{bill_id}的数量: {parent_quantity}")
# 设置一个临时环境变量作为最后的手段
if parent_quantity == 1.0:
import os
env_quantity = os.environ.get("BILL_QUANTITY")
if env_quantity:
try:
parent_quantity = float(env_quantity)
print(f"从环境变量获取清单数量: {parent_quantity}")
except:
pass
print(f"===== 获取清单数量结束 =====\n")
# 将结果乘以父级清单的数量
@@ -401,14 +391,6 @@ class MainGridBudgetCalculator(CalculatorBase):
"""创建主网预算计算策略"""
return MainGridBudgetCalculationStrategy()
def set_output_dir(self, output_dir):
"""
设置计算结果的输出目录
:param output_dir: 输出目录的路径
"""
self.output_dir = output_dir
def apply_quantity_fee_rules(self) -> None:
"""应用主网预算特定的工程量取费规则"""
print(f"应用{self.software_type.name}特定的工程量取费规则 - 对节点进行预处理")
@@ -475,14 +457,6 @@ class MainGridBillCalculator(CalculatorBase):
"""创建主网清单计算策略"""
return MainGridBillCalculationStrategy()
def set_output_dir(self, output_dir):
"""
设置计算结果的输出目录
:param output_dir: 输出目录的路径
"""
self.output_dir = output_dir
def apply_quantity_fee_rules(self) -> None:
"""应用主网清单特定的工程量取费规则"""
print(f"应用{self.software_type.name}特定的工程量取费规则 - 对节点进行预处理")
@@ -547,14 +521,6 @@ class DistributionBudgetCalculator(CalculatorBase):
def __init__(self):
super().__init__(DISTRIBUTION_BUDGET)
def set_output_dir(self, output_dir):
"""
设置计算结果的输出目录
:param output_dir: 输出目录的路径
"""
self.output_dir = output_dir
def create_calculation_strategy(self) -> CalculationStrategy:
"""创建配网预算计算策略"""
return DistributionBudgetCalculationStrategy()
@@ -586,14 +552,6 @@ class DistributionBillCalculator(CalculatorBase):
"""创建配网清单计算策略"""
return DistributionBillCalculationStrategy()
def set_output_dir(self, output_dir):
"""
设置计算结果的输出目录
:param output_dir: 输出目录的路径
"""
self.output_dir = output_dir
def apply_quantity_fee_rules(self) -> None:
"""应用配网清单特定的工程量取费规则"""
print(f"应用{self.software_type.name}特定的工程量取费规则")
@@ -621,14 +579,6 @@ class TechnicalRenovationBudgetCalculator(CalculatorBase):
"""创建技改预算计算策略"""
return TechnicalRenovationBudgetCalculationStrategy()
def set_output_dir(self, output_dir):
"""
设置计算结果的输出目录
:param output_dir: 输出目录的路径
"""
self.output_dir = output_dir
def apply_quantity_fee_rules(self) -> None:
"""应用技改预算特定的工程量取费规则"""
print(f"应用{self.software_type.name}特定的工程量取费规则")
@@ -656,14 +606,6 @@ class TechnicalRenovationBillCalculator(CalculatorBase):
"""创建技改清单计算策略"""
return TechnicalRenovationBillCalculationStrategy()
def set_output_dir(self, output_dir):
"""
设置计算结果的输出目录
:param output_dir: 输出目录的路径
"""
self.output_dir = output_dir
def apply_quantity_fee_rules(self) -> None:
"""应用技改清单特定的工程量取费规则"""
print(f"应用{self.software_type.name}特定的工程量取费规则")
@@ -1,718 +0,0 @@
import json
import os
import math
from typing import Dict, List, Any, Tuple
from copy import deepcopy
from equipment_calculation.item_acquisition import get_quantity_nodes, get_classified_resource_nodes, load_project_data
from bcl_utils import (
ZjMaterialOrEquipmentBCLContext,
ZjProjectBCLContext,
ZjBillBCLContext,
init_bcl_calculator,
calculator,
)
from item_acquisition import get_quantity_nodes, get_classified_resource_nodes
# 人材机节点定义合并条件常量
LABOR_MERGE_CONDITIONS = [
"编码",
"名称",
"单位",
"预算价含税",
"预算价不含税",
"市场价不含税",
"市场价含税",
"调差类型",
"专业属性",
"供货方",
"物料类材料",
]
MATERIAL_MERGE_CONDITIONS = [
"类型",
"编码",
"名称",
"单位",
"预算价不含税",
"预算价含税",
"市场价不含税",
"市场价含税",
"调差类型",
"专业属性",
"供货方",
"集中配送",
"卸车",
"保管",
"物料类材料",
]
MACHINE_MERGE_CONDITIONS = [
"编码",
"名称",
"单位",
"预算价不含税",
"预算价含税",
"市场价不含税",
"市场价含税",
"调差类型",
"专业属性",
"供货方",
"物料类材料",
]
from bcl_utils import (
ZjMaterialOrEquipmentBCLContext,
ZjProjectBCLContext,
ZjBillBCLContext,
init_bcl_calculator,
calculator,
)
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:
if "id" in node:
parent_nodes[node["id"]] = node
print(f"找到 {len(parent_nodes)} 个父级工程量节点")
# 初始化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 = ZjProjectBCLContext(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)
# 根据节点类型选择不同的计算表达式
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,使用原始数量: {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
def generate_node_key(node: Dict[str, Any], conditions: List[str]) -> str:
"""
根据合并条件生成节点的唯一键,增加parent_id作为条件
Args:
node: 节点
conditions: 合并条件列表
Returns:
str: 节点的唯一键
"""
key_parts = []
# 添加parent_id作为合并条件的第一个元素
parent_id = node.get("parent_id", "")
key_parts.append(f"parent_id:{parent_id}")
# 添加其他合并条件
for condition in conditions:
value = node.get(condition, "")
# 确保值是字符串
if value is None:
value = ""
elif not isinstance(value, str):
value = str(value)
key_parts.append(f"{condition}:{value}")
return "|".join(key_parts)
def merge_similar_nodes(nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
合并相似的节点
Args:
nodes: 节点列表
Returns:
List[Dict[str, Any]]: 合并后的节点列表
"""
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)
merged_nodes = []
# 处理每种类型的节点
for node_type, type_nodes in nodes_by_type.items():
# 选择合并条件
if node_type == "人工":
conditions = LABOR_MERGE_CONDITIONS
elif node_type == "材料":
conditions = MATERIAL_MERGE_CONDITIONS
elif node_type == "机械":
conditions = MACHINE_MERGE_CONDITIONS
else:
# 未知类型,不合并
merged_nodes.extend(type_nodes)
continue
# 使用字典存储合并后的节点,键是根据合并条件生成的唯一键
merged_dict = {}
for node in type_nodes:
key = generate_node_key(node, conditions)
if key in merged_dict:
# 合并节点
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")
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:
# 创建新节点
merged_dict[key] = deepcopy(node)
# 添加合并信息
node_id = node.get("id", f"node_{id(node)}")
merged_dict[key]["合并来源"] = [node_id]
merged_dict[key]["合并数量"] = "1"
# 将合并后的节点添加到结果列表
merged_nodes.extend(merged_dict.values())
return merged_nodes
def cat_rcj_count(rcj_nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
处理人材机节点数量的变化并合并相似节点
Args:
rcj_nodes: 人材机节点列表
Returns:
List[Dict[str, Any]]: 处理数量变化后的人材机节点列表
"""
# 使用计算数量,不进行向上取整
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)
# 合并相似节点
merged_nodes = merge_similar_nodes(processed_nodes)
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 = {}
# 保留父级工程量节点信息
formatted_node["parent_id"] = node.get("parent_id", "")
formatted_node["parent_name"] = node.get("parent_name", "未知工程量节点")
# 根据节点类型保留特定字段
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,
adjustment_type: str = "拆除",
engineering_type: str = "预算工程",
project_guid: str = None,
) -> Dict[str, Any]:
"""
计算项目划分节点下所有人材机节点的合价
Args:
json_file_path: JSON文件路径
project_name: 项目名称
adjustment_type: 调差类型,默认为"拆除"
engineering_type: 工程类型,默认为"预算工程"
project_guid: 项目GUID,用于区分同名项目
Returns:
Dict[str, Any]: 计算结果,按工程量节点分组
"""
# 获取工程量节点,传递project_guid参数
project_children = get_quantity_nodes(json_file_path, project_name, adjustment_type, engineering_type, project_guid)
# 如果没有找到项目划分节点
if project_children is None:
print(f"警告: 未找到项目 '{project_name}' 的项目划分节点")
return {}
# 如果找到项目划分节点但没有工程量节点
if len(project_children) == 0:
print(f"警告: 项目 '{project_name}' (GUID: {project_guid}) 没有工程量节点,跳过计算")
return {}
# 获取分类后的人材机节点,传递project_guid参数
labor_nodes, material_nodes, machine_nodes = get_classified_resource_nodes(
json_file_path, project_name, adjustment_type, project_guid
)
# 打印调试信息
print(f"找到 {len(labor_nodes)} 个人工节点, {len(material_nodes)} 个材料节点, {len(machine_nodes)} 个机械节点")
# 计算人工节点的数量和合价
labor_with_count = calc_rcj_count(labor_nodes, project_children, json_file_path)
labor_with_cat = cat_rcj_count(labor_with_count)
labor_with_fee = calc_rcj_fee(labor_with_cat)
labor_formatted = format_rcj_output(labor_with_fee, "人工")
# 计算材料节点的数量和合价
material_with_count = calc_rcj_count(material_nodes, project_children, json_file_path)
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(machine_nodes, project_children, json_file_path)
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, "机械")
# 创建工程量节点ID到名称的映射
node_id_to_name = {}
for node in project_children:
if "id" in node:
node_name = node.get("项目名称", node.get("name", f"工程量节点_{node['id']}"))
node_id_to_name[node["id"]] = node_name
# 按工程量节点分组结果
result_by_parent = {}
# 处理人工节点
for node in labor_formatted:
parent_id = node.pop("parent_id", "")
parent_name = node.pop("parent_name", "未知工程量节点")
# 使用父级节点名称作为键
parent_key = node_id_to_name.get(parent_id, parent_name)
if parent_key not in result_by_parent:
result_by_parent[parent_key] = {"人工节点": [], "材料节点": [], "机械节点": []}
result_by_parent[parent_key]["人工节点"].append(node)
# 处理材料节点
for node in material_formatted:
parent_id = node.pop("parent_id", "")
parent_name = node.pop("parent_name", "未知工程量节点")
# 使用父级节点名称作为键
parent_key = node_id_to_name.get(parent_id, parent_name)
if parent_key not in result_by_parent:
result_by_parent[parent_key] = {"人工节点": [], "材料节点": [], "机械节点": []}
result_by_parent[parent_key]["材料节点"].append(node)
# 处理机械节点
for node in machine_formatted:
parent_id = node.pop("parent_id", "")
parent_name = node.pop("parent_name", "未知工程量节点")
# 使用父级节点名称作为键
parent_key = node_id_to_name.get(parent_id, parent_name)
if parent_key not in result_by_parent:
result_by_parent[parent_key] = {"人工节点": [], "材料节点": [], "机械节点": []}
result_by_parent[parent_key]["机械节点"].append(node)
# 计算合价只用于调试显示
for parent_key, parent_data in result_by_parent.items():
labor_fee = sum(float(node.get("预算价合价", "0.0") or "0.0") for node in parent_data["人工节点"])
material_fee = sum(float(node.get("预算价合价", "0.0") or "0.0") for node in parent_data["材料节点"])
machine_fee = sum(float(node.get("预算价合价", "0.0") or "0.0") for node in parent_data["机械节点"])
print(f"工程量节点 '{parent_key}': 人工合价={labor_fee}, 材料合价={material_fee}, 机械合价={machine_fee}")
return result_by_parent
def calculate_resource_fees(
json_file_path: str,
project_name: str,
adjustment_type: str,
engineering_type: str,
project_guid: str = None,
calculation_strategy=None,
) -> str:
"""
计算人材机合价
Args:
json_file_path: JSON文件路径
project_name: 项目名称
adjustment_type: 调差类型
engineering_type: 工程类型
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}_{adjustment_type}_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, adjustment_type, engineering_type, project_guid
)
else:
# 如果计算策略没有实现 calculate_rcj_fees 方法,使用原始函数
rcj_results = calculate_rcj_fees(json_file_path, project_name, adjustment_type, engineering_type, project_guid)
# 检查是否有人材机数据 - 适应新的按工程量节点分组的结构
has_data = False
# 如果是旧结构(直接包含人工节点、材料节点、机械节点)
if any(key in rcj_results for key in ["人工节点", "材料节点", "机械节点"]):
for node_type in ["人工节点", "材料节点", "机械节点"]:
if rcj_results.get(node_type) and len(rcj_results[node_type]) > 0:
has_data = True
break
# 如果是新结构(按工程量节点分组)
else:
for parent_name, parent_data in rcj_results.items():
for node_type in ["人工节点", "材料节点", "机械节点"]:
if parent_data.get(node_type) and len(parent_data[node_type]) > 0:
has_data = True
break
if has_data:
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