修改费用计算代码

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
+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)