修改费用计算代码

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
View File
+2 -1
View File
@@ -1,3 +1,4 @@
{
"Codegeex.RepoIndex": true
"Codegeex.RepoIndex": true,
"python.languageServer": "None"
}
-4
View File
@@ -1,4 +0,0 @@
[neo4j]
uri = bolt://10.1.6.34:7687
user = neo4j
password = password
+3 -1
View File
@@ -33,7 +33,9 @@ def convert_json_to_readable(input_file, output_file=None):
if __name__ == "__main__":
# 指定输入文件路径
input_file = r"project2json/outputs/json/变电技改国网.json"
input_file = (
r"E:/文件/LLM_model/RAG/code/Engineering_data_KG-1/equipment_dataset/数据工程/技改/预算/通信线路检修国网.json"
)
# 调用转换函数
convert_json_to_readable(input_file)
+2 -2
View File
@@ -170,10 +170,10 @@ def save_comparison_to_txt(comparison, output_txt_path):
def main():
# ================== 配置路径 ==================
# 存放所有 calculation_results.json 的文件夹
calc_results_folder = "project2json/outputs/bclresults/变电技改国网"
calc_results_folder = "project2json/outputs/bclresults/变电检修国网"
# 主 project_data.json 路径(参考数据源)
project_data_json_path = "project2json/outputs/json/变电技改国网.json"
project_data_json_path = "project2json/outputs/json/变电检修国网.json"
# 输出对比结果的文件夹
output_folder = "project2json/outputs/comparison_results"
+88 -3
View File
@@ -1,16 +1,19 @@
from __future__ import annotations
import logging
from memory_profiler import profile
import xml.etree.ElementTree as ET
from typing import Dict, Callable, Any, Optional
import os
from enum import Enum, auto
from equipment_calculation.expressioncalculator import ExpressionCalculator
import copy
import re
from xml.dom import minidom
# 配置logging
logging.basicConfig(
level=logging.DEBUG,
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.FileHandler("bcl_calculator.log"), logging.StreamHandler()],
)
@@ -744,6 +747,86 @@ def strfind_func(funcname: str, claculate, context: BCLContext, *args):
return BCLVariant(position)
def _is_in_range(value, range_expression):
"""判断值是否在范围表达式中
Args:
value (str): 要检查的值
range_expression (str): 范围表达式,支持以下格式:
- 单个值: "JYT19-71"
- 范围: "JYT17-1~189"
- 范围(后半省略前缀且带前导零): "C09032001~09032315"
- 多个范围用逗号分隔: "JYT17-1~189,JYT18-1~100"
Returns:
bool: 如果值在范围中返回True,否则返回False
"""
# 统一转为字符串
if value is None or range_expression is None:
return False
value = str(value).strip()
range_expression = str(range_expression).strip()
# 拆分为(前缀, 尾部数字)——前缀为末尾数字前的全部,数字为末尾一段数字
def split_prefix_num(s: str) -> tuple[str, Optional[int]]:
s = str(s)
m = re.match(r"^(.*?)(\d+)$", s)
if not m:
return s, None
prefix, num_str = m.group(1), m.group(2)
try:
return prefix, int(num_str)
except ValueError:
return prefix, None
value_prefix, value_num = split_prefix_num(value)
# 处理多个范围表达式(用逗号分隔)
for seg in range_expression.split(","):
seg = seg.strip()
if not seg:
continue
# 范围:start~end
if "~" in seg:
try:
start, end = [x.strip() for x in seg.split("~", 1)]
s_pref, s_num = split_prefix_num(start)
e_pref, e_num = split_prefix_num(end)
# 允许 end 省略前缀(如 JYT17-1~189 或 C09032001~09032315),
# 即只要 end 全为数字,则继承 start 的前缀,并按数值比较(忽略前导零差异)
if e_pref == "" and end.isdigit():
e_pref = s_pref
try:
e_num = int(end)
except ValueError:
e_num = None
# 要求前缀完全一致
if s_pref != e_pref:
continue
# 值必须与范围前缀完全一致
if value_prefix != s_pref:
continue
# 三者数字齐备方可比较
if value_num is None or s_num is None or e_num is None:
continue
if s_num <= value_num <= e_num:
return True
except Exception:
continue
else:
# 单值:要求完全相等
if value == seg:
return True
return False
def in_func(funcname: str, claculate, context: BCLContext, *args):
# 参数数量校验
if len(args) != 2:
@@ -780,8 +863,8 @@ def in_func(funcname: str, claculate, context: BCLContext, *args):
else param2.value
)
# 判断value1是否在value2中
result = value1 in value2
# 使用自定义的范围判断函数
result = _is_in_range(value1, value2)
# 返回布尔类型的BCLVariant对象
return BCLVariant(result)
@@ -1173,6 +1256,8 @@ class BCLCalculator:
return False
name = expr.get("name")
if name in self.expressions:
logging.warning(f"BCLExpression名称重复: {name},后来的定义将覆盖之前的。")
self.expressions[name] = expr
return True
+171 -15
View File
@@ -1,4 +1,5 @@
import json
from memory_profiler import profile
from typing import Dict, List, Any, Optional, Tuple
from equipment_calculation.bcl_calculator import (
BCLCalculator,
@@ -17,6 +18,48 @@ import logging
# 全局变量
calculator = BCLCalculator()
# 项目类型到需加载XML文件名的映射(按软件分类预留)。
# 仅在 calculation_type == "工程量" 时生效;人材机仍按目录全部加载。
# 注意:文件名需与目录中文件名完全匹配。
PROJECT_TYPE_XML_MAP: Dict[str, Dict[str, List[str]]] = {
# 主网软件分类
"主网": {
# 示例:变电需要读取以下文件(用户后续可补充/修改)
# 其他项目类型占位
# "输电": [],
# "线路": [],
},
# 配网软件分类
"配网": {
# 预留:用户后续填充
# "配网项目类型示例": ["xxx.xml"],
},
# 技改软件分类
"技改": {
"变电": [
"变量计算配置.xml",
"变量计算配置(变电).xml",
"定额基本信息费用计算.xml",
"工程量统计配置.xml",
"材机分析配置.xml",
],
"线路": [
"变量计算配置.xml",
"变量计算配置(线路).xml",
"定额基本信息费用计算.xml",
"工程量统计配置.xml",
"材机分析配置.xml",
],
"配网": [
"变量计算配置.xml",
"变量计算配置(配网).xml",
"定额基本信息费用计算.xml",
"工程量统计配置.xml",
"材机分析配置.xml",
],
},
}
def load_json_data(json_file_path: str, json_path: str = None) -> Dict[str, Any]:
"""
@@ -52,6 +95,9 @@ def load_json_data(json_file_path: str, json_path: str = None) -> Dict[str, Any]
return {}
import logging
def create_node_from_type(node: dict[str, any]):
"""
根据项目划分子节点类型动态创建对应类型的对象
@@ -60,23 +106,44 @@ def create_node_from_type(node: dict[str, any]):
node: 项目划分子节点数据
Returns:
对应类型的对象:Ration、Material或Equipment
对应类型的对象:Ration、Material或Equipment;不支持的类型返回None
"""
# 获取节点类型
node_type = node.get("类型", "")
type_code = node.get("type", "")
# 获取配件类型的附加判断字段(仅当是配件时使用)
peijian_type = node.get("配件类型", "")
# 判断是否为“定额”类型node_type 或 type_code 是 "定额" 或 "0"
# 判断是否为“定额”类型
if node_type in ["定额", "0"] or type_code in ["定额", "0"]:
return create_ration_from_node(node)
# 判断是否为“主材”类型
elif node_type in ["主材", "1"] or type_code in ["主材", "1"]:
return create_material_from_node(node)
elif node_type in ["设备", "配件"] or type_code in ["设备", "配件"]:
# 判断是否为“设备”类型
elif node_type in ["设备", "5"] or type_code in ["设备", "5"]:
return create_equipment_from_node(node)
else:
logging.warning(f"未知节点类型: 类型={node_type}, type={type_code},默认创建Ration对象")
# 判断是否为“一笔性费用”类型
elif node_type in ["一笔性费用", ""] or type_code in ["一笔性费用", ""]:
return create_ration_from_node(node)
# 特殊处理:“配件”需要根据“配件类型”进一步判断
elif node_type == "配件" or type_code == "配件":
if peijian_type == "主材":
return create_material_from_node(node)
elif peijian_type == "配件":
return create_equipment_from_node(node)
else:
logging.warning(f"配件类型未识别: 配件类型={peijian_type}, 节点类型={node_type}, type={type_code},跳过")
return None
else:
logging.warning(f"未知节点类型: 类型={node_type}, type={type_code},跳过")
return None
def create_list_from_node(node: dict[str, any]) -> bill_node:
"""
@@ -164,6 +231,7 @@ def create_ration_from_node(node: dict[str, any]) -> Ration:
ration.所属定额库 = node.get("所属定额库")
ration.专业属性 = node.get("专业属性")
ration.地形费计算方式 = node.get("地形费计算方式")
ration.专业类型 = node.get("调差类型")
# 遍历节点的所有属性,确保所有可能的字段都被设置
for key, value in node.items():
@@ -172,9 +240,32 @@ def create_ration_from_node(node: dict[str, any]) -> Ration:
setattr(ration, key, str(value) if value != "" else "")
ration.乙供材料费不含税 = node.get("材料费")
ration.children = node.get("children")
return ration
def _derive_moe_type(node: dict[str, any]) -> str:
"""
基于节点字段推导人/材/机对象在BCL中的 type,不修改原始节点:
- 若 类型/type 为数值代码,映射到中文
- 若 类型/type 为“配件”,根据 配件类型:主材->主材;配件->设备
- 其他返回原值
"""
t = node.get("类型") or node.get("type")
pj = node.get("配件类型")
if t in ("1", "主材"):
return "主材"
if t in ("5", "设备"):
return "设备"
if t == "配件":
if pj == "主材":
return "主材"
if pj == "配件":
return "设备"
return t or ""
def create_material_from_node(node: dict[str, any]) -> Material:
"""
创建主材对象
@@ -190,7 +281,8 @@ def create_material_from_node(node: dict[str, any]) -> Material:
# 设置主材相关属性
material.id = node.get("id")
material.name = node.get("项目名称")
material.type = node.get("类型", "主材")
# 对象层的 type 使用派生类型,不覆盖原始字典
material.type = _derive_moe_type(node) or "主材"
material.供货方 = node.get("供货方")
material.关联父级量 = node.get("关联父级量")
material.制造长度 = node.get("制造长度")
@@ -236,6 +328,7 @@ def create_material_from_node(node: dict[str, any]) -> Material:
setattr(material, key, str(value) if value != "" else "")
material.预算价不含税 = material.单价不含税
material.children = node.get("children")
return material
@@ -255,7 +348,8 @@ def create_equipment_from_node(node: dict[str, any]) -> Equipment:
# 设置设备相关属性
equipment.id = node.get("id")
equipment.name = node.get("项目名称")
equipment.type = node.get("类型", "设备")
# 对象层的 type 使用派生类型,不覆盖原始字典
equipment.type = _derive_moe_type(node) or "设备"
equipment.供货方 = node.get("供货方")
equipment.关联父级量 = node.get("关联父级量")
equipment.制造长度 = node.get("制造长度")
@@ -296,11 +390,12 @@ def create_equipment_from_node(node: dict[str, any]) -> Equipment:
if hasattr(equipment, key) and not key.startswith("_"):
if value is not None:
setattr(equipment, key, str(value) if value != "" else "")
equipment.children = node.get("children")
return equipment
def init_bcl_calculator(software_category, engineering_type, calculation_type):
def init_bcl_calculator(software_category, engineering_type, calculation_type, project_type: Optional[str] = None):
"""
初始化BCL计算器
@@ -308,6 +403,7 @@ def init_bcl_calculator(software_category, engineering_type, calculation_type):
software_category: 软件类别(主网/配网/技改)
engineering_type: 工程类型(预算/清单)
calculation_type: 计算类型(工程量/人材机)
project_type: 项目类型(如 变电/线路 等);仅用于工程量时筛选要加载的XML文件
Returns:
bool: 是否成功初始化
@@ -352,7 +448,46 @@ def init_bcl_calculator(software_category, engineering_type, calculation_type):
config_path = default_path
# 加载脚本
result = calculator.load_scripts_dir(config_path)
# 对于工程量,若提供了project_type且存在映射,则仅加载映射中指定的XML
result = True
use_filtered_files = False
try:
import os
if calculation_type == "工程量" and project_type:
target_list = PROJECT_TYPE_XML_MAP.get(software_category, {}).get(project_type) or []
if target_list:
use_filtered_files = True
print(
f"根据项目类型筛选加载XML: 软件={software_category}, 项目类型={project_type}, 目标文件={target_list}"
)
all_ok = True
missing_files = []
for filename in target_list:
xml_path = os.path.join(config_path, filename)
if not os.path.exists(xml_path):
missing_files.append(filename)
all_ok = False
continue
if not calculator.load_script(xml_path):
print(f"加载脚本失败: {xml_path}, 错误: {calculator.get_last_error()}")
all_ok = False
else:
print(f"成功加载脚本: {xml_path}")
if missing_files:
print(f"警告: 以下映射指定的文件在目录中未找到: {missing_files}")
result = all_ok
else:
print(f"未找到映射项,按目录全部加载: 软件={software_category}, 项目类型={project_type}")
except Exception as _e:
# 映射/筛选流程异常时,退回目录加载
print(f"按项目类型筛选加载发生异常,退回目录加载: {_e}")
use_filtered_files = False
if not use_filtered_files:
result = calculator.load_scripts_dir(config_path)
if False == result:
print(f"加载脚本错误: {calculator.get_last_error()}")
# 尝试使用默认配置
@@ -404,12 +539,12 @@ def create_material_or_equipment_from_node(node: dict[str, any]) -> MaterialOrEq
"""
me = MaterialOrEquipment()
# 设置基本属性
# 设置基本属性(type 使用派生值,不改写原始字典)
me.id = node.get("id")
me.编码 = node.get("编码")
me.名称 = node.get("名称")
me.单位 = node.get("单位")
me.type = node.get("类型")
me.type = _derive_moe_type(node)
me.供货方 = node.get("供货方", "")
me.预算价不含税 = node.get("预算价不含税")
me.市场价不含税 = node.get("市场价不含税")
@@ -426,12 +561,13 @@ def create_material_or_equipment_from_node(node: dict[str, any]) -> MaterialOrEq
me.商品砼 = node.get("商品砼", "")
me.数量 = node.get("数量")
me.是否未计价 = node.get("是否未计价")
# 遍历节点的所有属性,确保所有可能的字段都被设置
for key, value in node.items():
if hasattr(me, key) and not key.startswith("_"):
setattr(me, key, str(value))
me.children = node.get("children")
return me
@@ -550,7 +686,11 @@ class PrefixConfig:
self.field_mappings = field_mappings or {}
def create_project_contexts(json_file_path: str, prefix_configs: List[PrefixConfig] = None) -> BCLContext:
def create_project_contexts(
json_file_path: str,
prefix_configs: List[PrefixConfig] = None,
json_data: Optional[Dict[str, Any]] = None,
) -> BCLContext:
"""
从JSON文件创建多个前缀上下文,并将它们链接在一起
@@ -577,10 +717,26 @@ def create_project_contexts(json_file_path: str, prefix_configs: List[PrefixConf
# 创建根上下文
root_context = None
# 简单路径解析器:从已有 json_data 中按路径提取
def _get_from_data(data_src: Dict[str, Any], path: Optional[str]):
if not path:
return data_src or {}
parts = path.split(".")
cur = data_src or {}
for p in parts:
if isinstance(cur, dict) and p in cur:
cur = cur.get(p, {})
else:
return {}
return cur
# 为每个前缀创建上下文并链接
for config in prefix_configs:
# 加载数据
data = load_json_data(json_file_path, config.json_path)
# 加载数据:优先使用传入的 json_data,否则从文件按路径读取
if json_data is not None:
data = _get_from_data(json_data, config.json_path)
else:
data = load_json_data(json_file_path, config.json_path)
# 创建上下文
context = ZjProjectBCLContext(prefix=config.prefix, prevContext=root_context)
+22 -4
View File
@@ -1,4 +1,5 @@
from abc import ABC, abstractmethod
from memory_profiler import profile
from typing import Dict, List, Any, Optional, Set, Tuple
@@ -117,6 +118,7 @@ class CalculationStrategy(ABC):
cost_table: Dict[str, Any],
json_file_path: Optional[str] = None,
engineering_type: Optional[str] = None,
json_data: Optional[Dict[str, Any]] = None,
) -> Dict[str, float]:
"""
计算所有费用
@@ -138,6 +140,7 @@ class CalculationStrategy(ABC):
rcj_nodes: List[Tuple[Dict[str, Any], str]],
project_children: List[Dict[str, Any]],
json_file_path: Optional[str] = None,
json_data: Optional[Dict[str, Any]] = None,
) -> List[Dict[str, Any]]:
"""
计算人材机节点的数量,考虑父级消耗量
@@ -322,12 +325,20 @@ class DefaultCalculationStrategy(CalculationStrategy):
cost_table: Dict[str, Any],
json_file_path: Optional[str] = None,
engineering_type: Optional[str] = None,
json_data: Optional[Dict[str, Any]] = None,
) -> Dict[str, float]:
"""计算所有费用"""
from equipment_calculation.quantity_fee_calculator import calculate_all_fees as original_calculate_all_fees
# 传递自身作为计算策略
return original_calculate_all_fees(project_node, cost_table, json_file_path, engineering_type, self)
return original_calculate_all_fees(
project_node,
cost_table,
json_file_path,
engineering_type,
self,
json_data=json_data,
)
def calculate_rcj_count(
self,
@@ -338,7 +349,7 @@ class DefaultCalculationStrategy(CalculationStrategy):
"""计算人材机节点的数量"""
from equipment_calculation.resource_fee_calculator import calc_rcj_count as original_calc_rcj_count
return original_calc_rcj_count(rcj_nodes, project_children, json_file_path)
return original_calc_rcj_count(rcj_nodes, project_children, json_file_path, json_data=json_data)
def cat_rcj_count(self, rcj_nodes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""汇总人材机节点的数量"""
@@ -359,7 +370,14 @@ class DefaultCalculationStrategy(CalculationStrategy):
return original_format_rcj_output(rcj_nodes, node_type)
# 修复 calculate_rcj_fees 未定义的错误
def calculate_rcj_fees(self, json_file_path, project_name, project_guid=None):
def calculate_rcj_fees(
self,
json_file_path,
project_name,
project_guid=None,
json_data: Optional[Dict[str, Any]] = None,
engineering_type: Optional[str] = None,
):
"""
计算项目划分节点下所有人材机节点的合价
@@ -375,7 +393,7 @@ class DefaultCalculationStrategy(CalculationStrategy):
from equipment_calculation.resource_fee_calculator import calculate_rcj_fees as original_calculate_rcj_fees
# 调用原始函数
return original_calculate_rcj_fees(json_file_path, project_name, project_guid)
return original_calculate_rcj_fees(json_file_path, project_name, project_guid, json_data=json_data)
def post_process_quantity_fees(self, output_file: str, project_name: str) -> None:
"""对工程量取费表进行后处理"""
+87 -13
View File
@@ -1,5 +1,6 @@
import os
from abc import ABC, abstractmethod
from memory_profiler import profile
from typing import Dict, List, Any, Optional, Tuple
from equipment_calculation.software_types import SoftwareType
from equipment_calculation.find_project_nodes import get_project_divisions_list
@@ -7,6 +8,8 @@ from equipment_calculation.quantity_fee_calculator import calculate_quantity_fee
from equipment_calculation.resource_fee_calculator import calculate_resource_fees as base_calculate_resource_fees
from equipment_calculation.calculation_strategy import CalculationStrategy, DefaultCalculationStrategy
import json
import psutil
from datetime import datetime
class CalculatorBase(ABC):
@@ -57,10 +60,45 @@ class CalculatorBase(ABC):
if hasattr(self.calculation_strategy, "set_output_dir"):
self.calculation_strategy.set_output_dir(output_dir)
def _append_log(self, text: str, filename: str = "performance_memory_log.txt") -> str:
"""将文本即时追加写入输出目录下的日志文件,并返回日志路径。"""
try:
out_dir = self.get_output_dir()
os.makedirs(out_dir, exist_ok=True)
log_path = os.path.join(out_dir, filename)
with open(log_path, "a", encoding="utf-8") as f:
f.write(text + "\n")
f.flush()
return log_path
except Exception as e:
# 即使写日志失败,也不要影响主流程
print(f"写入性能日志失败: {e}")
return ""
def _print_memory_usage(self, prefix: str = "") -> None:
"""记录当前进程的内存占用情况(MB)到输出目录日志文件。"""
try:
process = psutil.Process(os.getpid())
mem_info = process.memory_info()
rss = mem_info.rss / (1024**2) # 物理内存(MB
vms = mem_info.vms / (1024**2) # 虚拟内存(MB
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
header = f"[{ts}]" + (f" [{prefix}]" if prefix else "")
lines = [
f"{header} 当前进程占用的物理内存: {rss:.2f} MB",
f"{header} 当前进程占用的虚拟内存: {vms:.2f} MB",
]
for line in lines:
self._append_log(line)
except Exception as e:
# 日志失败不影响主流程
print(f"记录内存信息失败: {e}")
def calculate_quantity_fee_tables(
self,
json_file_path: str,
project_name: str = None,
project_type: Optional[str] = None,
) -> None:
"""
计算工程量取费表
@@ -76,12 +114,30 @@ class CalculatorBase(ABC):
if callable(preprocess_func): # 检查返回值是否可调用
preprocess_func(json_file_path) # 调用预处理函数
# 外层只读取一次 JSON
with open(json_file_path, "r", encoding="utf-8") as _f:
_json_data = json.load(_f)
# 外层只初始化一次 BCL 计算器
from equipment_calculation.bcl_utils import init_bcl_calculator
init_bcl_calculator(
software_category=self.software_type.category.value,
engineering_type=self.software_type.engineering_type.value,
calculation_type="工程量",
project_type=project_type,
)
if project_name:
# 处理单个项目划分
print(f"处理单个项目划分: {project_name}")
output_file = self._calculate_quantity_fees(json_file_path, project_name, engineering_type)
output_file = self._calculate_quantity_fees(
json_file_path, project_name, engineering_type, project_guid=None, json_data=_json_data
)
if output_file:
print(f"已完成 {project_name} 的工程量取费表计算,结果保存在 {output_file}")
# 性能打印:每个项目划分完成后打印内存占用
self._print_memory_usage(prefix=f"工程量-{project_name}")
else:
# 处理所有项目划分
project_divisions = get_project_divisions_list(json_file_path)
@@ -90,9 +146,13 @@ class CalculatorBase(ABC):
for i, (proj_name, proj_guid) in enumerate(project_divisions, 1):
# 使用命令行参数中指定的调差类型,而不是从JSON文件中获取的调差类型
print(f"处理 {i}/{len(project_divisions)}: {proj_name} (GUID: {proj_guid})")
output_file = self._calculate_quantity_fees(json_file_path, proj_name, engineering_type, proj_guid)
output_file = self._calculate_quantity_fees(
json_file_path, proj_name, engineering_type, proj_guid, json_data=_json_data
)
if output_file:
print(f"已完成 {proj_name} (GUID: {proj_guid}) 的工程量取费表计算")
# 性能打印:每个项目划分完成后打印内存占用
self._print_memory_usage(prefix=f"工程量-{proj_name}")
print(f"所有项目划分节点的工程量取费表计算完成,结果保存在 {self.get_output_dir()} 目录")
@@ -110,14 +170,30 @@ class CalculatorBase(ABC):
# 在计算前应用软件特定的规则
self.apply_resource_fee_rules()
# 外层只读取一次 JSON
with open(json_file_path, "r", encoding="utf-8") as _f:
_json_data = json.load(_f)
# 外层只初始化一次 BCL 计算器(人材机)
from equipment_calculation.bcl_utils import init_bcl_calculator
init_bcl_calculator(
software_category=self.software_type.category.value,
engineering_type=self.software_type.engineering_type.value,
calculation_type="人材机",
)
if project_name:
# 处理单个项目划分
output_file = self._calculate_resource_fees(
json_file_path,
project_name,
json_data=_json_data,
)
if output_file:
print(f"已完成 {project_name} 的人材机合价计算,结果保存在 {output_file}")
# 性能打印:每个项目划分完成后打印内存占用
self._print_memory_usage(prefix=f"人材机-{project_name}")
else:
# 处理所有项目划分
project_divisions = get_project_divisions_list(json_file_path)
@@ -125,9 +201,11 @@ class CalculatorBase(ABC):
for i, (proj_name, proj_guid) in enumerate(project_divisions, 1):
print(f"处理 {i}/{len(project_divisions)}: {proj_name} (GUID: {proj_guid})")
output_file = self._calculate_resource_fees(json_file_path, proj_name, proj_guid)
output_file = self._calculate_resource_fees(json_file_path, proj_name, proj_guid, json_data=_json_data)
if output_file:
print(f"已完成 {proj_name} (GUID: {proj_guid}) 的人材机合价计算")
# 性能打印:每个项目划分完成后打印内存占用
self._print_memory_usage(prefix=f"人材机-{proj_name}")
print(f"所有项目划分节点的人材机合价计算完成,结果保存在 {self.get_output_dir()} 目录")
@@ -137,6 +215,7 @@ class CalculatorBase(ABC):
project_name: str,
engineering_type: str,
project_guid: str = None,
json_data: dict | None = None,
) -> str:
"""
计算工程量取费表,并应用软件特定的后处理规则
@@ -153,17 +232,8 @@ class CalculatorBase(ABC):
# 确保输出目录存在
os.makedirs(self.get_output_dir(), exist_ok=True)
# 初始化BCL计算器,传递软件类型和计算类型
from equipment_calculation.bcl_utils import init_bcl_calculator
init_bcl_calculator(
software_category=self.software_type.category.value,
engineering_type=self.software_type.engineering_type.value,
calculation_type="工程量",
)
# 打印计算策略信息
print(f"使用计算策略: {self.calculation_strategy.__class__.__name__}")
# print(f"使用计算策略: {self.calculation_strategy.__class__.__name__}")
# 调用基础计算函数,传入计算策略和项目GUID
output_file = base_calculate_quantity_fees(
@@ -172,6 +242,8 @@ class CalculatorBase(ABC):
engineering_type,
project_guid=project_guid,
calculation_strategy=self.calculation_strategy,
software_type=self.software_type.category.value,
json_data=json_data,
)
# 应用软件特定的后处理规则
@@ -185,6 +257,7 @@ class CalculatorBase(ABC):
json_file_path: str,
project_name: str,
project_guid: str = None,
json_data: dict | None = None,
) -> str:
"""
计算人材机合价,并应用软件特定的后处理规则
@@ -216,6 +289,7 @@ class CalculatorBase(ABC):
project_name,
project_guid=project_guid,
calculation_strategy=self.calculation_strategy,
json_data=json_data,
)
# 应用软件特定的后处理规则
@@ -125,7 +125,7 @@ class ExpressionCalculator:
# 处理一元运算符
if token_type == "OPERATOR" and value in ("+", "-"):
self.current_token += 1
node = self.parse_factor()
node = self._parse_factor()
return ("UNARYOP", value, node)
# 处理变量
+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("测试完成!")
+56 -80
View File
@@ -1,5 +1,6 @@
import os
import json
from memory_profiler import profile
from typing import Dict, List, Any, Optional, Tuple
from equipment_calculation.software_types import (
get_software_type,
@@ -29,101 +30,65 @@ CATEGORY_MAPPING = {
}
def parse_json_content(json_file_path: str) -> Tuple[Optional[str], Optional[str]]:
# 项目类型名称映射字典,将各种变体映射到标准类型(预算/清单)
PROJECT_TYPE_MAPPING = {
# 预算类变体
"概预算工程": "预算",
}
####应该是加在json后没释放
def parse_json_content(json_file_path: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
"""
从JSON文件内容中解析软件类别和工程类型
从JSON文件内容中解析
- 软件类别: 来自 basicData["软件类别"](若无则尝试 basicData["软件名称"] 作为兜底)
- 项目类型: 来自 basicData["项目类型"](期望为 "预算""清单"
- 工程类型: 来自 projectData.projectInfo["工程类型"]
:param json_file_path: JSON文件路径
:return: (category, engineering_type) 元组,如果解析失败返回 (None, None)
:return: (category, project_type, engineering_type) 元组,解析失败对应位置返回 None
"""
try:
with open(json_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 定义阶段类型映射表
budget_types = ["概预算", "定额", "定额计价", "概算", "概预算工程"]
list_types = ["清单", "结算", "招标控制价", "招投标工程", "清单计价"]
# 从division字段获取软件名称和阶段类型
if "division" in data:
division = data["division"]
print(f"找到division字段: {division}")
# 使用-分割division字段
parts = division.split("-")
if len(parts) == 2:
category = parts[0].strip()
stage_type = parts[1].strip()
elif len(parts) == 3:
category = parts[0].strip()
stage_type = parts[2].strip()
# 提取 basicData
basic_data = data.get("basicData", {}) if isinstance(data, dict) else {}
# 软件类别(优先 软件类别,其次 软件名称)
category = basic_data.get("软件名称") or basic_data.get("软件名称")
engineering_type = basic_data.get("项目类型")
# 规范化项目类型为 预算/清单
if engineering_type:
mapped_pt = PROJECT_TYPE_MAPPING.get(engineering_type)
if mapped_pt:
engineering_type = mapped_pt
else:
print(f"警告: division字段 '{division}' 格式不正确,无法分割")
# 可以选择返回默认值或抛出异常,这里以返回默认值为例
category = "主网"
print(f"警告: basicData.项目类型 '{engineering_type}' 不是有效值,将使用默认值 '清单'")
engineering_type = "清单"
print(f"使用默认值: 软件名称={category}, 阶段类型={engineering_type}")
return category, engineering_type
# 使用映射字典规范化软件类别
# 规范化软件类别
if category:
if category in CATEGORY_MAPPING:
category = CATEGORY_MAPPING[category]
else:
print(f"警告: division中的软件名称 '{category}' 不是有效值,将使用默认值 '主网'")
print(f"警告: basicData中的软件名称/名称 '{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 = "清单"
# 提取工程类型:projectData.projectInfo.工程类型
project_info = data.get("projectData", {}).get("projectInfo", {}) if isinstance(data, dict) else {}
project_type = project_info.get("工程类型")
print(f"从division解析: 软件名称={category}, 阶段类型={engineering_type} (原始值: {stage_type})")
return category, engineering_type
# 打印解析结果(便于调试)
print(f"解析完成: 软件类别={category}, 项目类型={engineering_type}, 工程类型={project_type}")
else:
print(f"警告: JSON文件中未找到division字段,尝试从basicData中解析")
# 作为备选,尝试从basicData中获取
if "basicData" in data:
basic_data = data["basicData"]
category = basic_data.get("软件名称")
engineering_type = basic_data.get("阶段类型")
# 验证解析结果
if category and engineering_type:
# 使用映射字典规范化软件类别
if category in CATEGORY_MAPPING:
category = CATEGORY_MAPPING[category]
else:
print(f"警告: basicData中的软件名称 '{category}' 不是有效值,将使用默认值 '主网'")
category = "主网"
# 确保engineering_type是有效值
if engineering_type not in ["预算", "清单"]:
print(f"警告: basicData中的阶段类型 '{engineering_type}' 不是有效值,将使用默认值 '清单'")
engineering_type = "清单"
print(f"从basicData解析: 软件名称={category}, 阶段类型={engineering_type}")
return category, engineering_type
else:
print(f"警告: basicData中未找到软件名称或阶段类型")
else:
print(f"警告: JSON文件中未找到basicData部分")
return None, None
return category, engineering_type, project_type
except Exception as e:
print(f"解析JSON文件内容时出错: {str(e)}")
return None, None
return None, None, None
def process_json_file(
json_file_path: str, output_dir: str, calculate_type: str = "all", project_name: str = None
) -> bool:
def process_json_file(json_file_path: str, output_dir: str, calculate_type: str, project_name: str = None) -> bool:
"""
处理单个JSON文件
@@ -137,20 +102,27 @@ def process_json_file(
bool: 处理是否成功
"""
try:
# 从JSON文件内容中解析软件类别和工程类型
category, engineering_type = parse_json_content(json_file_path)
# 从JSON文件内容中解析软件类别、项目类型(预算/清单) 和 工程类型(如 变电/线路/配网)
# parse_json_content 返回顺序为: (category, project_type, engineering_type)
category, project_type, engineering_type = parse_json_content(json_file_path)
# 如果解析失败,使用默认值
if category is None:
category = "主网"
print(f"无法从文件中解析软件类别,使用默认值: {category}")
if engineering_type is None:
engineering_type = "预算"
print(f"无法从文件中解析工程类型,使用默认值: {engineering_type}")
# 项目类型(预算/清单) 兜底
if project_type is None:
project_type = "预算"
print(f"无法从文件中解析项目类型(预算/清单),使用默认值: {project_type}")
# 可选:记录工程类型
if engineering_type:
print(f"工程类型: {engineering_type}")
# 获取软件类型
software_type = get_software_type(category, engineering_type)
# 这里的第二个参数应为项目类型(预算/清单)
software_type = get_software_type(category, project_type)
print(f"使用软件类型: {software_type.name}")
# 获取计算器
@@ -190,6 +162,8 @@ def process_json_file(
calculator.calculate_quantity_fee_tables(
json_file_path=json_file_path,
project_name=project_name,
# 这里的 project_type 传递给计算器用于BCL筛选,应为 工程类型,例如"变电/线路/配网"
project_type=engineering_type,
)
if calculate_type in ["all", "resource"]:
@@ -217,6 +191,8 @@ def process_json_file(
custom_calculator.calculate_quantity_fee_tables(
json_file_path=json_file_path,
project_name=project_name,
# 这里的 project_type 传递给计算器用于BCL筛选,应为 工程类型,例如"变电/线路/配网"
project_type=engineering_type,
)
if calculate_type in ["all", "resource"]:
@@ -236,7 +212,7 @@ def process_json_file(
return False
def process_directory(input_dir: str, output_dir: str, calculate_type: str = "all") -> None:
def process_directory(input_dir: str, output_dir: str, calculate_type: str = "quantity") -> None:
"""
批量处理目录中的JSON文件
@@ -269,7 +245,7 @@ def process_directory(input_dir: str, output_dir: str, calculate_type: str = "al
print(f"处理完成,成功: {success_count}/{len(json_files)}")
def bcl_calculate(input_dir: str, output_dir: str, calculate_type: str = "all") -> None:
def bcl_calculate(input_dir: str, output_dir: str, calculate_type: str = "quantity") -> None:
"""
主函数,处理指定目录中的所有JSON文件
File diff suppressed because it is too large Load Diff
+3
View File
@@ -24,6 +24,7 @@ class ProjectQuantity:
self.cost_set = None # xsd:CostSet
self.设备类型 = None # xsd:string
self.供货方 = None # xsd:string
self.children = None
# (定额)
self.综合地形类型 = None # xsd:string
@@ -32,6 +33,8 @@ class ProjectQuantity:
self.机械费不含税 = None # xsd:string
self.地形费计算方式 = None # xsd:string
self.专业属性 = None # xsd:string
self.专业类型 = None # xsd:string
self.浇捣方式 = None # xsd:string
# (主材)
self.拆分 = None # xsd:string
+154 -27
View File
@@ -1,8 +1,10 @@
import json
import os
import sys
from memory_profiler import profile
from typing import Dict, List, Any, Optional, Tuple, Set
from equipment_calculation.expressioncalculator import ExpressionCalculator
from equipment_calculation.bcl_utils import (
ZjQuantityBCLContext,
ZjProjectBCLContext,
@@ -15,6 +17,7 @@ from equipment_calculation.bcl_utils import (
BCLDataSourceContext,
create_list_from_node,
create_node_from_type,
create_material_or_equipment_from_node,
)
from equipment_calculation.item_acquisition import (
get_cost_table_children,
@@ -23,6 +26,7 @@ from equipment_calculation.item_acquisition import (
find_cost_table,
get_bill_node_by_id,
load_project_data,
get_classified_resource_nodes,
)
# 缓存已计算过的费用
@@ -223,7 +227,7 @@ def calculate_external_variable(var_name: str, context: ZjQuantityBCLContext, ca
try:
# 如果有计算策略,使用计算策略的方法
if calculation_strategy:
print(f"使用计算策略 {calculation_strategy.__class__.__name__} 计算表外变量: {var_name}")
# print(f"使用计算策略 {calculation_strategy.__class__.__name__} 计算表外变量: {var_name}")
# 直接调用计算策略的方法,而不是递归调用自己
return calculation_strategy.calculate_external_variable(var_name, context)
@@ -447,17 +451,21 @@ def calculate_all_fees(
json_file_path: str = None,
engineering_type: str = None,
calculation_strategy=None,
json_data: dict | None = None,
) -> Dict[str, float]:
"""
计算项目节点的所有费用,确保清单数量正确传递
"""
results = {}
# 1. 工程信息上下文
project_context = create_project_contexts(json_file_path=json_file_path)
# 1. 工程信息上下文(优先使用传入的 json_data
project_context = create_project_contexts(json_file_path=json_file_path, json_data=json_data)
with open(json_file_path, "r", encoding="utf-8") as file:
data = json.load(file)
if json_data is not None:
data = json_data
else:
with open(json_file_path, "r", encoding="utf-8") as file:
data = json.load(file)
processed_data = process_DXdata(data)
@@ -497,8 +505,37 @@ def calculate_all_fees(
billItem.bill_quantity = bill_obj.quantity
print(f"设置清单数据源项数量: {billItem.bill_quantity}")
# 如果无法创建工程量对象,则跳过并返回当前已计算结果,避免未定义的 QuantityItem
if not project_obj:
print(
f"警告: 无法从项目节点创建工程量对象,跳过该节点: "
f"{project_node.get('项目名称', project_node.get('name', '未知节点'))}"
)
return results
QuantityItem = BCLDataSourceItem(project_obj, billItem)
childItems = []
if project_obj.children:
for moe in project_obj.children:
childItems.append(BCLDataSourceItem(moe, QuantityItem))
QuantityItem.set_childs(childItems)
bill_context = BCLDataSourceContext([billItem], dxitem_context)
# # 构建人材机数据源项(若存在)并接入上下文
# rcj_nodes_for_parent = project_node.get("_rcj_nodes", [])
# moe_items = []
# if rcj_nodes_for_parent:
# try:
# for rcj_node in rcj_nodes_for_parent:
# node_obj = create_material_or_equipment_from_node(rcj_node)
# moe_items.append(BCLDataSourceItem(node_obj, QuantityItem))
# except Exception as e:
# print(f"构建人材机数据源项出错: {e}")
# items_chain = [QuantityItem] + moe_items if moe_items else [QuantityItem]
context = BCLDataSourceContext([QuantityItem], bill_context)
# 如果计算策略存在,设置清单数量
@@ -513,7 +550,35 @@ def calculate_all_fees(
dxitem_context = BCLDataSourceContext(DXITEM, project_context)
dxitem_context.variables["@特征段地形系数"] = BCLVariant(DXITEM)
# 如果无法创建工程量对象,则跳过并返回当前已计算结果,避免未定义的 QuantityItem
if not project_obj:
print(
f"警告: 无法从项目节点创建工程量对象,跳过该节点: "
f"{project_node.get('项目名称', project_node.get('name', '未知节点'))}"
)
return results
QuantityItem = BCLDataSourceItem(project_obj)
childItems = []
if project_obj.children:
for moe in project_obj.children:
childItems.append(BCLDataSourceItem(moe, QuantityItem))
QuantityItem.set_childs(childItems)
# # 构建人材机数据源项(若存在)并接入上下文
# rcj_nodes_for_parent = project_node.get("_rcj_nodes", [])
# moe_items = []
# if rcj_nodes_for_parent:
# try:
# for rcj_node in rcj_nodes_for_parent:
# node_obj = create_material_or_equipment_from_node(rcj_node)
# moe_items.append(BCLDataSourceItem(node_obj, QuantityItem))
# except Exception as e:
# print(f"构建人材机数据源项出错: {e}")
# items_chain = [QuantityItem] + moe_items if moe_items else [QuantityItem]
context = BCLDataSourceContext([QuantityItem], dxitem_context)
# 查找包含取费基数的节点
@@ -696,6 +761,8 @@ def calculate_quantity_fees(
engineering_type: str = None,
project_guid: str = None,
calculation_strategy=None,
software_type: str = None,
json_data: dict | None = None,
) -> str:
"""
计算工程量取费表,不使用全局变量,并按清单节点组织结果
@@ -769,7 +836,9 @@ def calculate_quantity_fees(
if engineering_type == "预算工程":
# 预算工程 - 获取项目划分节点的取费表,传递project_guid参数
cost_table_children = get_cost_table_children(json_file_path, project_name, project_guid)
project_children = get_quantity_nodes(json_file_path, project_name, engineering_type, project_guid)
project_children = get_quantity_nodes(
json_file_path, project_name, engineering_type, project_guid, software_type
)
if not cost_table_children or not project_children:
print(f"未找到项目 '{project_name}' (GUID: {project_guid}) 的取费表或项目划分节点")
@@ -777,7 +846,9 @@ def calculate_quantity_fees(
elif engineering_type == "清单工程":
# 清单工程 - 获取清单节点的取费表,传递project_guid参数
project_children = get_quantity_nodes(json_file_path, project_name, engineering_type, project_guid)
project_children = get_quantity_nodes(
json_file_path, project_name, engineering_type, project_guid, software_type
)
if not project_children:
print(f"未找到项目 '{project_name}' (GUID: {project_guid}) 的项目划分节点")
return None
@@ -785,6 +856,16 @@ def calculate_quantity_fees(
# 为每个工程量节点找到对应的清单节点取费表
cost_tables = {}
# 准备一次性 JSON 数据(清单工程需要从 projectData 中获取 costSetting 与 bill
if json_data is not None:
_data_all = json_data
else:
with open(json_file_path, "r", encoding="utf-8") as f:
_data_all = json.load(f)
_project_data_all = _data_all.get("projectData", {})
_cost_setting_all = _project_data_all.get("costSetting", {})
_bill_data_all = _project_data_all.get("bill", {})
for project_node in project_children:
node_name = project_node.get("项目名称", project_node.get("name", "未知节点"))
@@ -813,15 +894,8 @@ def calculate_quantity_fees(
if bill_id not in cost_tables:
print(f"查找清单节点 '{bill_name}' 的取费表: {fee_table_name}")
# 查找取费表
with open(json_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
project_data = data.get("projectData", {})
cost_setting = project_data.get("costSetting", {})
# 查找取费表
cost_table = find_cost_table(cost_setting, fee_table_name)
# 查找取费表:使用已加载的数据
cost_table = find_cost_table(_cost_setting_all, fee_table_name)
if not cost_table:
print(f"未找到取费表 '{fee_table_name}'")
@@ -835,12 +909,8 @@ def calculate_quantity_fees(
cost_tables[bill_id] = cost_table_children
print(f"成功获取清单节点 '{bill_name}' 的取费表")
# 在读取JSON文件后,遍历项目中的清单节点,获取并保存清单数量
with open(json_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
project_data = data.get("projectData", {})
bill_data = project_data.get("bill", {})
# 遍历项目中的清单节点,获取并保存清单数量(使用已加载的数据)
bill_data = _bill_data_all
# 递归函数获取所有清单节点
def get_bill_nodes(node):
@@ -885,6 +955,55 @@ def calculate_quantity_fees(
print(f"不支持的工程类型: {engineering_type}")
return None
# 预取并分派人材机节点到各工程量节点上,供后续上下文使用
try:
labor_nodes, material_nodes, machine_nodes = get_classified_resource_nodes(
json_file_path, project_name, project_guid
)
# 合并三类节点,形成父ID到节点列表的映射
rcj_all = []
if labor_nodes:
rcj_all.extend([n for n, _ in labor_nodes])
if material_nodes:
rcj_all.extend([n for n, _ in material_nodes])
if machine_nodes:
rcj_all.extend([n for n, _ in machine_nodes])
rcj_by_parent: Dict[str, List[Dict[str, Any]]] = {}
# 注意:get_classified_resource_nodes 返回的是 (node, parent_id) 元组列表
def add_to_map(pairs):
if not pairs:
return
for node, parent_id in pairs:
if parent_id is None:
continue
key = str(parent_id)
rcj_by_parent.setdefault(key, []).append(node)
add_to_map(labor_nodes)
add_to_map(material_nodes)
add_to_map(machine_nodes)
# 将对应的人材机列表写入每个工程量节点
if project_children:
for node in project_children:
pid_candidates = [
node.get("GUID"),
node.get("guid"),
node.get("id"),
]
rcj_list = []
for pid in pid_candidates:
if pid and str(pid) in rcj_by_parent:
rcj_list = rcj_by_parent.get(str(pid), [])
break
if rcj_list:
node["_rcj_nodes"] = rcj_list
except Exception as e:
print(f"预取人材机节点失败: {e}")
# 在获取 project_children 后添加预处理代码
if project_children and calculation_strategy and hasattr(calculation_strategy, "preprocess_quantity_fee_node"):
print(f"对项目节点进行预处理...")
@@ -946,7 +1065,11 @@ def calculate_quantity_fees(
if engineering_type == "预算工程":
# 预算工程 - 使用项目划分节点的取费表
node_results = calculation_strategy.calculate_all_fees(
project_node, {"children": cost_table_children}, json_file_path, engineering_type
project_node,
{"children": cost_table_children},
json_file_path,
engineering_type,
json_data=json_data,
)
# 直接使用节点键作为结果键
@@ -958,7 +1081,11 @@ def calculate_quantity_fees(
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[bill_id]}, json_file_path, engineering_type
project_node,
{"children": cost_tables[bill_id]},
json_file_path,
engineering_type,
json_data=json_data,
)
# 获取清单信息
@@ -987,9 +1114,9 @@ def calculate_quantity_fees(
continue
# 输出计算结果
print(f"费用计算结果:")
for fee_name, fee_value in node_results.items():
print(f" {fee_name}: {fee_value}")
# print(f"费用计算结果:")
# for fee_name, fee_value in node_results.items():
# print(f" {fee_name}: {fee_value}")
# 保存计算结果到JSON文件
with open(output_file, "w", encoding="utf-8") as f:
@@ -3,6 +3,7 @@ import os
import math
from typing import Dict, List, Any, Tuple
from copy import deepcopy
from memory_profiler import profile
from equipment_calculation.item_acquisition import get_quantity_nodes, get_classified_resource_nodes, load_project_data
@@ -140,7 +141,10 @@ def process_DXdata(json_data):
# 在 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
rcj_nodes: List[Tuple[Dict[str, Any], str]],
project_children: List[Dict[str, Any]],
json_file_path: str = None,
json_data: dict | None = None,
) -> List[Dict[str, Any]]:
"""
计算人材机节点的数量,考虑父级消耗量
@@ -231,8 +235,8 @@ def calc_rcj_count(
result_nodes.append(node_copy)
return result_nodes
# 创建工程信息上下文(顶层上下文)
project_context = create_project_contexts(json_file_path=json_file_path)
# 创建工程信息上下文(顶层上下文,优先使用 json_data
project_context = create_project_contexts(json_file_path=json_file_path, json_data=json_data)
# 遍历所有节点,计算数量
for node, parent_id in rcj_nodes:
@@ -684,6 +688,7 @@ def calculate_rcj_fees(
json_file_path: str,
project_name: str,
project_guid: str = None,
json_data: dict | None = None,
) -> Dict[str, Any]:
"""
计算项目划分节点下所有人材机节点的合价
@@ -698,7 +703,7 @@ def calculate_rcj_fees(
"""
# 加载项目数据
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
)
# 如果没有找到项目划分节点
@@ -717,7 +722,56 @@ def calculate_rcj_fees(
material_nodes = [] # 材料节点 (节点, 父级节点) 元组列表
machine_nodes = [] # 机械节点 (节点, 父级节点) 元组列表
# 递归处理工程量节点,提取人材机节点
# 工具:类型判断与资源拆分上提
def _is_resource_type(t):
return t in ("人工", "材料", "机械", "2", "3", "4")
def _normalize_type(t):
return {"2": "人工", "3": "材料", "4": "机械"}.get(t, t)
def _flatten_resource_splits(n, inherited_type=None, qty_factor: float = 1.0):
"""
将可能带 children 的人材机节点递归拆平:
- 若是人材机且有children:不计入父,children 继承本节点类型继续拆分;
- 若是人材机且无children:作为叶子返回;
- 若非人材机:递归其children以发现资源叶子。
返回叶子资源节点列表(dict)。
"""
results = []
if not isinstance(n, dict):
return results
n_type = _normalize_type(n.get("类型") or n.get("type"))
children = n.get("children") or []
if _is_resource_type(n_type):
# 当前节点是人材机
current_type = _normalize_type(inherited_type or n_type)
# 当前资源节点自身数量,默认1
try:
self_qty = float((n.get("数量") or "1").strip())
except Exception:
self_qty = 1.0
new_factor = qty_factor * self_qty
if children:
for ch in children:
results.extend(_flatten_resource_splits(ch, current_type, new_factor))
else:
leaf = deepcopy(n)
leaf["类型"] = current_type
# 叶子数量 = 父链乘积 * 自身(若叶子也有数量则已计入 new_factor)
try:
# 如果叶子本身还有数量字段,已包含在 new_factor 中,这里直接覆盖为乘积
leaf["数量"] = str(new_factor)
except Exception:
leaf["数量"] = str(new_factor)
results.append(leaf)
else:
# 非资源,继续向下查找
for ch in children:
results.extend(_flatten_resource_splits(ch, inherited_type, qty_factor))
return results
# 递归处理工程量节点,提取人材机节点(对children内的人材机执行拆分上提)
def process_nodes(nodes):
for node in nodes:
# 如果是定额节点,处理其材机列表和children
@@ -756,22 +810,25 @@ def calculate_rcj_fees(
f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 有子节点,数量: {len(node['children'])}"
)
for child in node["children"]:
child_type = child.get("类型", child.get("type"))
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', '未命名'))}"
)
# 将children中的资源节点拆分为叶子资源,排除中间父级
flattened = _flatten_resource_splits(child)
for leaf in flattened:
leaf_type = _normalize_type(leaf.get("类型") or leaf.get("type"))
if leaf_type == "人工":
labor_nodes.append((leaf, node))
print(
f"从children(拆分后)找到人工节点: {leaf.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
)
elif leaf_type == "材料":
material_nodes.append((leaf, node))
print(
f"从children(拆分后)找到材料节点: {leaf.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
)
elif leaf_type == "机械":
machine_nodes.append((leaf, node))
print(
f"从children(拆分后)找到机械节点: {leaf.get('名称', '未命名')}, 父节点: {node.get('项目名称', node.get('name', '未命名'))}"
)
else:
print(f"定额节点 '{node.get('项目名称', node.get('name', '未命名'))}' 没有子节点")
@@ -820,11 +877,14 @@ def calculate_rcj_fees(
# result_nodes.append(node_copy)
# return result_nodes
# 创建工程信息上下文(顶层上下文)
project_context = create_project_contexts(json_file_path=json_file_path)
# 创建工程信息上下文(顶层上下文,优先使用 json_data
project_context = create_project_contexts(json_file_path=json_file_path, json_data=json_data)
with open(json_file_path, "r", encoding="utf-8") as file:
data = json.load(file)
if json_data is not None:
data = json_data
else:
with open(json_file_path, "r", encoding="utf-8") as file:
data = json.load(file)
processed_data = process_DXdata(data)
@@ -984,6 +1044,7 @@ def calculate_resource_fees(
project_name: str,
project_guid: str = None,
calculation_strategy=None,
json_data: dict | None = None,
) -> str:
"""
计算人材机合价
@@ -1004,7 +1065,7 @@ def calculate_resource_fees(
calculation_strategy = DefaultCalculationStrategy()
# 获取项目划分节点的GUID
_, _, _, _, target_node = load_project_data(json_file_path, project_name, project_guid)
_, _, _, _, target_node = load_project_data(json_file_path, project_name, project_guid, json_data=json_data)
# 如果传入了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
@@ -1055,10 +1116,16 @@ def calculate_resource_fees(
# 计算人材机节点的合价,传递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)
try:
rcj_results = calculation_strategy.calculate_rcj_fees(
json_file_path, project_name, project_guid, json_data=json_data
)
except TypeError:
# 向后兼容:旧策略不支持 json_data 形参
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)
rcj_results = calculate_rcj_fees(json_file_path, project_name, project_guid, json_data=json_data)
# 检查是否有人材机数据
has_data = False
+19 -17
View File
@@ -1,6 +1,7 @@
import re
import codecs
def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats_path="warning_statistics.txt"):
"""
从日志文件中提取 WARNING 和 ERROR 及其 Traceback 堆栈信息,保存到新文件
@@ -8,15 +9,15 @@ def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats
同时统计WARNING信息并输出到单独文件
"""
# 正则匹配日志行开头(时间戳格式)
log_pattern = re.compile(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})')
log_pattern = re.compile(r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})")
# 尝试多种编码格式读取文件
encodings = ['utf-8', 'gbk', 'gb2312', 'ascii']
encodings = ["utf-8", "gbk", "gb2312", "ascii"]
lines = []
for encoding in encodings:
try:
with open(input_log_path, 'r', encoding=encoding) as f:
with open(input_log_path, "r", encoding=encoding) as f:
lines = f.readlines()
print(f"✅ 成功使用 {encoding} 编码读取文件")
break
@@ -26,10 +27,10 @@ def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats
# 如果所有编码都失败,则使用二进制模式读取并尝试解码
if not lines:
try:
with open(input_log_path, 'rb') as f:
with open(input_log_path, "rb") as f:
content = f.read()
# 尝试解码,忽略错误
lines = content.decode('utf-8', errors='ignore').splitlines(True)
lines = content.decode("utf-8", errors="ignore").splitlines(True)
print("⚠️ 使用二进制模式读取文件,可能有字符丢失")
except Exception as e:
print(f"❌ 无法读取文件: {e}")
@@ -46,10 +47,10 @@ def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats
if is_new_log:
# 判断是否为 WARNING 或 ERROR
if ' - WARNING - ' in line or ' - ERROR - ' in line:
if " - WARNING - " in line or " - ERROR - " in line:
error_lines.append(line.rstrip())
# 如果是 ERROR,捕获后续的 Traceback 信息
if ' - ERROR - ' in line:
if " - ERROR - " in line:
i += 1
# 继续读取后续行,直到遇到下一个时间戳行或文件结束
while i < len(lines):
@@ -65,7 +66,7 @@ def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats
error_lines.append(next_line.rstrip())
i += 1
# 如果是DEBUG/INFO行,检查是否包含Traceback
elif ' - DEBUG - ' in line and i + 1 < len(lines) and 'Traceback' in lines[i + 1]:
elif " - DEBUG - " in line and i + 1 < len(lines) and "Traceback" in lines[i + 1]:
# 这是一个包含Traceback的DEBUG信息,也提取
error_lines.append(line.rstrip())
i += 1
@@ -92,35 +93,36 @@ def extract_errors_and_warnings(input_log_path, output_error_path, warning_stats
i += 1
# 写入输出文件
with open(output_error_path, 'w', encoding='utf-8') as f:
with open(output_error_path, "w", encoding="utf-8") as f:
for err_line in error_lines:
f.write(err_line + '\n')
f.write(err_line + "\n")
# 统计WARNING信息
warning_dict = {}
for line in error_lines:
if ' - WARNING - ' in line:
if " - WARNING - " in line:
# 提取WARNING后的内容作为键
warning_content = line.split(' - WARNING - ', 1)[1]
warning_content = line.split(" - WARNING - ", 1)[1]
if warning_content in warning_dict:
warning_dict[warning_content] += 1
else:
warning_dict[warning_content] = 1
# 写入统计结果到文件
with open(warning_stats_path, 'w', encoding='utf-8') as f:
with open(warning_stats_path, "w", encoding="utf-8") as f:
f.write("WARNING统计结果:\n")
f.write(f"共找到 {len(warning_dict)} 种不同的WARNING信息\n\n")
for warning_content, count in warning_dict.items():
f.write(f"{count}次: {warning_content}\n")
f.write(f"{warning_content}\n")
print(f"✅ 提取完成!共找到 {len(error_lines)} 行错误/警告信息。")
print(f"📁 已保存到: {output_error_path}")
print(f"📊 WARNING统计已保存到: {warning_stats_path}")
# ============ 使用示例 ============
if __name__ == "__main__":
input_file = "bcl_calculator.log" # 替换为你的日志文件路径
output_file = "error_report.txt" # 输出的错误报告文件
warning_stats_file = "warning_statistics.txt" # WARNING统计结果文件
input_file = "bcl_calculator.log" # 替换为你的日志文件路径
output_file = "error_report.txt" # 输出的错误报告文件
warning_stats_file = "warning_statistics.txt" # WARNING统计结果文件
extract_errors_and_warnings(input_file, output_file, warning_stats_file)
+62
View File
@@ -57,6 +57,37 @@ def transform_expense_preview(input_file, output_file):
print(f"原始expensePreview中的顶级分类: {list(original_expense_preview.keys())}")
print(f"projectDivision中的顶级分类: {list(project_division.keys())}")
# 先清理 projectDivision:递归删除任意带有 "删除": "1" 或 1 的节点
def _filter_deleted_nodes(obj):
# 若当前对象本身标记了删除,则直接丢弃
if isinstance(obj, dict):
flag = obj.get("删除")
if flag == "1" or flag == 1:
return None
new_obj = {}
for k, v in obj.items():
filtered = _filter_deleted_nodes(v)
if filtered is not None:
new_obj[k] = filtered
return new_obj
elif isinstance(obj, list):
new_list = []
for item in obj:
filtered = _filter_deleted_nodes(item)
if filtered is not None:
new_list.append(filtered)
return new_list
else:
return obj
cleaned_project_division = _filter_deleted_nodes(project_division) or {}
if cleaned_project_division != project_division:
print("已根据 '删除' 标记清理 projectDivision 中的节点")
project_division = cleaned_project_division
# 回写清理后的结构,确保后续流程与落盘一致
if "projectData" in data:
data["projectData"]["projectDivision"] = project_division
# 创建新的expensePreview结构
new_expense_preview = {}
@@ -478,6 +509,37 @@ def transform_json_types(input_file_path, output_file_path=None):
with open(input_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# 在主网流程中,同样先清理 projectDivision:递归删除任意带有 "删除": "1" 或 1 的节点
def _filter_deleted_nodes(obj):
if isinstance(obj, dict):
flag = obj.get("删除")
if flag == "1" or flag == 1:
return None
new_obj = {}
for k, v in obj.items():
filtered = _filter_deleted_nodes(v)
if filtered is not None:
new_obj[k] = filtered
return new_obj
elif isinstance(obj, list):
new_list = []
for item in obj:
filtered = _filter_deleted_nodes(item)
if filtered is not None:
new_list.append(filtered)
return new_list
else:
return obj
try:
pd = data.get("projectData", {}).get("projectDivision", {})
cleaned_pd = _filter_deleted_nodes(pd) or {}
if cleaned_pd != pd and "projectData" in data:
data["projectData"]["projectDivision"] = cleaned_pd
print("[主网] 已根据 '删除' 标记清理 projectDivision 中的节点")
except Exception:
pass
# 递归处理函数
def traverse(obj):
if isinstance(obj, dict):