修改费用计算代码
This commit is contained in:
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"Codegeex.RepoIndex": true
|
||||
"Codegeex.RepoIndex": true,
|
||||
"python.languageServer": "None"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
[neo4j]
|
||||
uri = bolt://10.1.6.34:7687
|
||||
user = neo4j
|
||||
password = password
|
||||
+3
-1
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
"""对工程量取费表进行后处理"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
# 处理变量
|
||||
|
||||
@@ -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("测试完成!")
|
||||
|
||||
@@ -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文件
|
||||
|
||||
|
||||
+1385
-71
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+24
-22
@@ -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,28 +9,28 @@ 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
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
|
||||
# 如果所有编码都失败,则使用二进制模式读取并尝试解码
|
||||
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统计结果文件
|
||||
extract_errors_and_warnings(input_file, output_file, warning_stats_file)
|
||||
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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user