Upload 70 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +37 -36
- app.py +48 -5
- assets/images/logo.png +3 -0
- config.py +229 -229
- config_manager.py +37 -37
- data/exports/boq_20250403_133827.xlsx +0 -0
- data/exports/boq_20250403_135418.xlsx +0 -0
- data/exports/boq_20250403_135819.xlsx +0 -0
- data/exports/boq_20250403_140953.xlsx +0 -0
- data/exports/boq_20250403_143659.xlsx +0 -0
- database/db_connector.py +14 -1
- database/models.py +78 -81
- docs/architecture.md +136 -136
- docs/missing_modules_analysis.md +88 -88
- docs/pricing_module_design.md +714 -714
- docs/pricing_module_requirements.md +155 -155
- fixed_pricing_app.py +544 -544
- fixed_pricing_app_complete.py +0 -0
- modules/ai_assistant/ai_app.py +0 -0
- modules/ai_assistant/assistant.py +0 -0
- modules/ai_assistant/contract_analyzer.py +0 -0
- modules/ai_assistant/data_integration.py +577 -577
- modules/ai_assistant/document_analyzer.py +507 -507
- modules/data_analysis/data_analysis_app.py +502 -502
- modules/document_analysis/analyzer.py +281 -281
- modules/document_analysis/document_app.py +152 -144
- modules/document_comparison/document_comparison_app.py +1003 -1003
- modules/maps/maps_app.py +456 -456
- modules/notifications/notifications_app.py +707 -707
- modules/pricing/price_analyzer.py +0 -0
- modules/pricing/pricing_app.py +127 -112
- modules/pricing/pricing_engine.py +430 -430
- modules/project_management/project_management_app.py +666 -666
- modules/reports/reports_app.py +404 -404
- modules/resources/resources_app.py +89 -60
- modules/risk_analysis/risk_analyzer.py +180 -733
- modules/scheduling/schedule_app.py +249 -0
- modules/translation/translation_app.py +936 -936
- pricing_system/docs/user_guide.md +235 -235
- pricing_system/integrated_app.py +995 -314
- pricing_system/integration_framework.py +49 -2
- pricing_system/modules/analysis/market_analysis.py +114 -0
- pricing_system/modules/analysis/smart_price_analysis.py +358 -313
- pricing_system/modules/catalogs/equipment_catalog.py +0 -0
- pricing_system/modules/catalogs/labor_catalog.py +0 -0
- pricing_system/modules/catalogs/materials_catalog.py +0 -0
- pricing_system/modules/catalogs/subcontractors_catalog.py +0 -0
- pricing_system/modules/indirect_support/overheads.py +0 -0
- pricing_system/modules/pricing_strategies/__init__.py +10 -0
- pricing_system/modules/pricing_strategies/balanced_pricing.py +92 -0
.gitattributes
CHANGED
@@ -1,36 +1,37 @@
|
|
1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
-
images/logo.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
images/logo.png filter=lfs diff=lfs merge=lfs -text
|
37 |
+
assets/images/logo.png filter=lfs diff=lfs merge=lfs -text
|
app.py
CHANGED
@@ -9,6 +9,7 @@ import matplotlib.pyplot as plt
|
|
9 |
import seaborn as sns
|
10 |
import plotly.express as px
|
11 |
import plotly.graph_objects as go
|
|
|
12 |
|
13 |
# إعداد المسارات
|
14 |
sys.path.append(str(Path(__file__).parent.parent))
|
@@ -29,7 +30,8 @@ from modules.document_comparison.document_comparison_app import DocumentComparis
|
|
29 |
from modules.translation.translation_app import TranslationApp
|
30 |
from modules.ai_assistant.ai_app import AIAssistantApp
|
31 |
from modules.data_analysis.data_analysis_app import DataAnalysisApp
|
32 |
-
from pricing_system.
|
|
|
33 |
from styling.enhanced_ui import UIEnhancer
|
34 |
|
35 |
# تهيئة مدير التكوين
|
@@ -46,7 +48,7 @@ config_manager.set_page_config_if_needed(
|
|
46 |
'Report a bug': "https://www.example.com/bug",
|
47 |
'About': "### نظام تحليل المناقصات\nالإصدار 2.0.0"
|
48 |
}
|
49 |
-
)
|
50 |
|
51 |
# تطبيق التنسيق العام
|
52 |
ui_enhancer = UIEnhancer(page_title="نظام تحليل المناقصات", page_icon="📊")
|
@@ -116,9 +118,47 @@ elif selected == "تحليل المستندات":
|
|
116 |
document_app.run()
|
117 |
|
118 |
elif selected == "نظام التسعير":
|
|
|
|
|
|
|
|
|
119 |
integrated_pricing = IntegratedApp()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
integrated_pricing.run()
|
121 |
|
|
|
122 |
elif selected == "الموارد والتكاليف":
|
123 |
resources_app = ResourcesApp()
|
124 |
resources_app.run()
|
@@ -155,6 +195,11 @@ elif selected == "تحليل البيانات":
|
|
155 |
data_analysis_app = DataAnalysisApp()
|
156 |
data_analysis_app.run()
|
157 |
|
|
|
|
|
|
|
|
|
|
|
158 |
elif selected == "الإعدادات":
|
159 |
ui_enhancer.create_header("الإعدادات", "إعدادات النظام والحساب")
|
160 |
st.markdown("### إعدادات النظام")
|
@@ -179,6 +224,4 @@ elif selected == "الإعدادات":
|
|
179 |
with tabs[3]:
|
180 |
st.text_input("مفتاح OpenAI API", type="password")
|
181 |
st.text_input("مفتاح Google Maps API", type="password")
|
182 |
-
st.button("حفظ مفاتيح API")
|
183 |
-
|
184 |
-
|
|
|
9 |
import seaborn as sns
|
10 |
import plotly.express as px
|
11 |
import plotly.graph_objects as go
|
12 |
+
from datetime import datetime
|
13 |
|
14 |
# إعداد المسارات
|
15 |
sys.path.append(str(Path(__file__).parent.parent))
|
|
|
30 |
from modules.translation.translation_app import TranslationApp
|
31 |
from modules.ai_assistant.ai_app import AIAssistantApp
|
32 |
from modules.data_analysis.data_analysis_app import DataAnalysisApp
|
33 |
+
from pricing_system.modules.pricing_strategies.pricing_strategies import PricingStrategies #added import
|
34 |
+
from pricing_system.integrated_app import IntegratedApp #added import
|
35 |
from styling.enhanced_ui import UIEnhancer
|
36 |
|
37 |
# تهيئة مدير التكوين
|
|
|
48 |
'Report a bug': "https://www.example.com/bug",
|
49 |
'About': "### نظام تحليل المناقصات\nالإصدار 2.0.0"
|
50 |
}
|
51 |
+
)
|
52 |
|
53 |
# تطبيق التنسيق العام
|
54 |
ui_enhancer = UIEnhancer(page_title="نظام تحليل المناقصات", page_icon="📊")
|
|
|
118 |
document_app.run()
|
119 |
|
120 |
elif selected == "نظام التسعير":
|
121 |
+
import streamlit as st
|
122 |
+
from pricing_system.integrated_app import IntegratedApp
|
123 |
+
|
124 |
+
# تهيئة النظام المتكامل
|
125 |
integrated_pricing = IntegratedApp()
|
126 |
+
|
127 |
+
# إعداد التكوين مرة واحدة في بداية التطبيق
|
128 |
+
config_manager.set_page_config_if_needed(
|
129 |
+
page_title="نظام التسعير المتكامل",
|
130 |
+
page_icon="💰",
|
131 |
+
layout="wide",
|
132 |
+
initial_sidebar_state="expanded"
|
133 |
+
)
|
134 |
+
|
135 |
+
# عرض الشعار وعنوان النظام
|
136 |
+
st.markdown("""
|
137 |
+
<style>
|
138 |
+
.title-container {
|
139 |
+
display: flex;
|
140 |
+
align-items: center;
|
141 |
+
padding: 1rem;
|
142 |
+
background-color: #f0f2f6;
|
143 |
+
border-radius: 0.5rem;
|
144 |
+
margin-bottom: 2rem;
|
145 |
+
}
|
146 |
+
.main-title {
|
147 |
+
color: #1f77b4;
|
148 |
+
font-size: 1.8rem;
|
149 |
+
margin: 0;
|
150 |
+
padding: 0;
|
151 |
+
}
|
152 |
+
</style>
|
153 |
+
<div class="title-container">
|
154 |
+
<h1 class="main-title">نظام التسعير المتكامل</h1>
|
155 |
+
</div>
|
156 |
+
""", unsafe_allow_html=True)
|
157 |
+
|
158 |
+
# تشغيل النظام المتكامل
|
159 |
integrated_pricing.run()
|
160 |
|
161 |
+
|
162 |
elif selected == "الموارد والتكاليف":
|
163 |
resources_app = ResourcesApp()
|
164 |
resources_app.run()
|
|
|
195 |
data_analysis_app = DataAnalysisApp()
|
196 |
data_analysis_app.run()
|
197 |
|
198 |
+
elif selected == "الجدول الزمني":
|
199 |
+
from modules.scheduling.schedule_app import ScheduleApp
|
200 |
+
schedule_app = ScheduleApp()
|
201 |
+
schedule_app.run()
|
202 |
+
|
203 |
elif selected == "الإعدادات":
|
204 |
ui_enhancer.create_header("الإعدادات", "إعدادات النظام والحساب")
|
205 |
st.markdown("### إعدادات النظام")
|
|
|
224 |
with tabs[3]:
|
225 |
st.text_input("مفتاح OpenAI API", type="password")
|
226 |
st.text_input("مفتاح Google Maps API", type="password")
|
227 |
+
st.button("حفظ مفاتيح API")
|
|
|
|
assets/images/logo.png
CHANGED
![]() |
![]() |
Git LFS Details
|
config.py
CHANGED
@@ -1,229 +1,229 @@
|
|
1 |
-
"""
|
2 |
-
ملف الإعدادات لنظام إدارة المناقصات
|
3 |
-
"""
|
4 |
-
|
5 |
-
import os
|
6 |
-
import json
|
7 |
-
from pathlib import Path
|
8 |
-
|
9 |
-
class AppConfig:
|
10 |
-
"""فئة إعدادات التطبيق"""
|
11 |
-
|
12 |
-
def __init__(self):
|
13 |
-
"""تهيئة الإعدادات"""
|
14 |
-
# المسارات الأساسية
|
15 |
-
self.app_dir = os.path.dirname(os.path.abspath(__file__))
|
16 |
-
self.assets_dir = os.path.join(self.app_dir, "assets")
|
17 |
-
self.data_dir = os.path.join(self.app_dir, "data")
|
18 |
-
|
19 |
-
# إنشاء المجلدات إذا لم تكن موجودة
|
20 |
-
Path(self.assets_dir).mkdir(parents=True, exist_ok=True)
|
21 |
-
Path(self.data_dir).mkdir(parents=True, exist_ok=True)
|
22 |
-
|
23 |
-
# مسارات الأصول
|
24 |
-
self.icons_dir = os.path.join(self.assets_dir, "icons")
|
25 |
-
self.images_dir = os.path.join(self.assets_dir, "images")
|
26 |
-
self.fonts_dir = os.path.join(self.assets_dir, "fonts")
|
27 |
-
|
28 |
-
# إنشاء مجلدات الأصول إذا لم تكن موجودة
|
29 |
-
Path(self.icons_dir).mkdir(parents=True, exist_ok=True)
|
30 |
-
Path(self.images_dir).mkdir(parents=True, exist_ok=True)
|
31 |
-
Path(self.fonts_dir).mkdir(parents=True, exist_ok=True)
|
32 |
-
|
33 |
-
# مسارات البيانات
|
34 |
-
self.database_file = os.path.join(self.data_dir, "database.db")
|
35 |
-
self.settings_file = os.path.join(self.data_dir, "settings.json")
|
36 |
-
self.charts_dir = os.path.join(self.data_dir, "charts")
|
37 |
-
|
38 |
-
# إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
|
39 |
-
Path(self.charts_dir).mkdir(parents=True, exist_ok=True)
|
40 |
-
|
41 |
-
# تحميل الإعدادات
|
42 |
-
self.settings = self._load_settings()
|
43 |
-
|
44 |
-
def _load_settings(self):
|
45 |
-
"""تحميل الإعدادات من ملف JSON"""
|
46 |
-
default_settings = {
|
47 |
-
"app": {
|
48 |
-
"name": "نظام إدارة المناقصات",
|
49 |
-
"version": "1.0.0",
|
50 |
-
"language": "ar",
|
51 |
-
"theme": "light",
|
52 |
-
"font": "Cairo",
|
53 |
-
"font_size": 12
|
54 |
-
},
|
55 |
-
"database": {
|
56 |
-
"type": "sqlite",
|
57 |
-
"path": self.database_file
|
58 |
-
},
|
59 |
-
"ui": {
|
60 |
-
"window_width": 1200,
|
61 |
-
"window_height": 800,
|
62 |
-
"sidebar_width": 250
|
63 |
-
},
|
64 |
-
"notifications": {
|
65 |
-
"enabled": True,
|
66 |
-
"email_enabled": True,
|
67 |
-
"email_server": "smtp.example.com",
|
68 |
-
"email_port": 587,
|
69 |
-
"email_username": "",
|
70 |
-
"email_password": ""
|
71 |
-
},
|
72 |
-
"reports": {
|
73 |
-
"default_format": "pdf",
|
74 |
-
"default_path": os.path.join(self.data_dir, "reports")
|
75 |
-
},
|
76 |
-
"backup": {
|
77 |
-
"auto_backup": True,
|
78 |
-
"backup_frequency": "weekly",
|
79 |
-
"backup_path": os.path.join(self.data_dir, "backups"),
|
80 |
-
"max_backups": 10
|
81 |
-
}
|
82 |
-
}
|
83 |
-
|
84 |
-
# إذا كان ملف الإعدادات موجودًا، قم بتحميله
|
85 |
-
if os.path.exists(self.settings_file):
|
86 |
-
try:
|
87 |
-
with open(self.settings_file, "r", encoding="utf-8") as f:
|
88 |
-
settings = json.load(f)
|
89 |
-
|
90 |
-
# دمج الإعدادات المحملة مع الإعدادات الافتراضية
|
91 |
-
self._merge_settings(default_settings, settings)
|
92 |
-
return default_settings
|
93 |
-
except Exception as e:
|
94 |
-
print(f"خطأ في تحميل الإعدادات: {str(e)}")
|
95 |
-
return default_settings
|
96 |
-
else:
|
97 |
-
# إنشاء ملف الإعدادات الافتراضية
|
98 |
-
self._save_settings(default_settings)
|
99 |
-
return default_settings
|
100 |
-
|
101 |
-
def _merge_settings(self, default_settings, loaded_settings):
|
102 |
-
"""دمج الإعدادات المحملة مع الإعدادات الافتراضية"""
|
103 |
-
for key, value in loaded_settings.items():
|
104 |
-
if key in default_settings:
|
105 |
-
if isinstance(value, dict) and isinstance(default_settings[key], dict):
|
106 |
-
self._merge_settings(default_settings[key], value)
|
107 |
-
else:
|
108 |
-
default_settings[key] = value
|
109 |
-
|
110 |
-
def _save_settings(self, settings=None):
|
111 |
-
"""حفظ الإعدادات إلى ملف JSON"""
|
112 |
-
if settings is None:
|
113 |
-
settings = self.settings
|
114 |
-
|
115 |
-
try:
|
116 |
-
with open(self.settings_file, "w", encoding="utf-8") as f:
|
117 |
-
json.dump(settings, f, ensure_ascii=False, indent=4)
|
118 |
-
return True
|
119 |
-
except Exception as e:
|
120 |
-
print(f"خطأ في حفظ الإعدادات: {str(e)}")
|
121 |
-
return False
|
122 |
-
|
123 |
-
def get_setting(self, section, key, default=None):
|
124 |
-
"""الحصول على
|
125 |
-
try:
|
126 |
-
return self.settings[section][key]
|
127 |
-
except KeyError:
|
128 |
-
return default
|
129 |
-
|
130 |
-
def set_setting(self, section, key, value):
|
131 |
-
"""تعيين قيمة إعداد معين"""
|
132 |
-
if section not in self.settings:
|
133 |
-
self.settings[section] = {}
|
134 |
-
|
135 |
-
self.settings[section][key] = value
|
136 |
-
self._save_settings()
|
137 |
-
|
138 |
-
def get_app_name(self):
|
139 |
-
"""الحصول على اسم التطبيق"""
|
140 |
-
return self.get_setting("app", "name", "نظام إدارة المناقصات")
|
141 |
-
|
142 |
-
def get_app_version(self):
|
143 |
-
"""الحصول على إصدار التطبيق"""
|
144 |
-
return self.get_setting("app", "version", "1.0.0")
|
145 |
-
|
146 |
-
def get_language(self):
|
147 |
-
"""الحصول على لغة التطبيق"""
|
148 |
-
return self.get_setting("app", "language", "ar")
|
149 |
-
|
150 |
-
def set_language(self, language):
|
151 |
-
"""تعيين لغة التطبيق"""
|
152 |
-
self.set_setting("app", "language", language)
|
153 |
-
|
154 |
-
def get_theme(self):
|
155 |
-
"""الحصول على نمط التطبيق"""
|
156 |
-
return self.get_setting("app", "theme", "light")
|
157 |
-
|
158 |
-
def set_theme(self, theme):
|
159 |
-
"""تعيين نمط التطبيق"""
|
160 |
-
self.set_setting("app", "theme", theme)
|
161 |
-
|
162 |
-
def get_font(self):
|
163 |
-
"""الحصول على خط التطبيق"""
|
164 |
-
return self.get_setting("app", "font", "Cairo")
|
165 |
-
|
166 |
-
def set_font(self, font):
|
167 |
-
"""تعيين خط التطبيق"""
|
168 |
-
self.set_setting("app", "font", font)
|
169 |
-
|
170 |
-
def get_font_size(self):
|
171 |
-
"""الحصول على حجم خط التطبيق"""
|
172 |
-
return self.get_setting("app", "font_size", 12)
|
173 |
-
|
174 |
-
def set_font_size(self, font_size):
|
175 |
-
"""تعيين حجم خط التطبيق"""
|
176 |
-
self.set_setting("app", "font_size", font_size)
|
177 |
-
|
178 |
-
def get_window_size(self):
|
179 |
-
"""الحصول على حجم نافذة التطبيق"""
|
180 |
-
width = self.get_setting("ui", "window_width", 1200)
|
181 |
-
height = self.get_setting("ui", "window_height", 800)
|
182 |
-
return (width, height)
|
183 |
-
|
184 |
-
def set_window_size(self, width, height):
|
185 |
-
"""تعيين حجم نافذة التطبيق"""
|
186 |
-
self.set_setting("ui", "window_width", width)
|
187 |
-
self.set_setting("ui", "window_height", height)
|
188 |
-
|
189 |
-
def get_sidebar_width(self):
|
190 |
-
"""الحصول على عرض الشريط الجانبي"""
|
191 |
-
return self.get_setting("ui", "sidebar_width", 250)
|
192 |
-
|
193 |
-
def set_sidebar_width(self, width):
|
194 |
-
"""تعيين عرض الشريط الجانبي"""
|
195 |
-
self.set_setting("ui", "sidebar_width", width)
|
196 |
-
|
197 |
-
def get_database_config(self):
|
198 |
-
"""الحصول على إعدادات قاعدة البيانات"""
|
199 |
-
return self.settings.get("database", {
|
200 |
-
"type": "sqlite",
|
201 |
-
"path": self.database_file
|
202 |
-
})
|
203 |
-
|
204 |
-
def get_notifications_config(self):
|
205 |
-
"""الحصول على إعدادات الإشعارات"""
|
206 |
-
return self.settings.get("notifications", {
|
207 |
-
"enabled": True,
|
208 |
-
"email_enabled": True,
|
209 |
-
"email_server": "smtp.example.com",
|
210 |
-
"email_port": 587,
|
211 |
-
"email_username": "",
|
212 |
-
"email_password": ""
|
213 |
-
})
|
214 |
-
|
215 |
-
def get_reports_config(self):
|
216 |
-
"""الحصول على إعدادات التقارير"""
|
217 |
-
return self.settings.get("reports", {
|
218 |
-
"default_format": "pdf",
|
219 |
-
"default_path": os.path.join(self.data_dir, "reports")
|
220 |
-
})
|
221 |
-
|
222 |
-
def get_backup_config(self):
|
223 |
-
"""الحصول على إعدادات النسخ الاحتياطي"""
|
224 |
-
return self.settings.get("backup", {
|
225 |
-
"auto_backup": True,
|
226 |
-
"backup_frequency": "weekly",
|
227 |
-
"backup_path": os.path.join(self.data_dir, "backups"),
|
228 |
-
"max_backups": 10
|
229 |
-
})
|
|
|
1 |
+
"""
|
2 |
+
ملف الإعدادات لنظام إدارة المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import json
|
7 |
+
from pathlib import Path
|
8 |
+
|
9 |
+
class AppConfig:
|
10 |
+
"""فئة إعدادات التطبيق"""
|
11 |
+
|
12 |
+
def __init__(self):
|
13 |
+
"""تهيئة الإعدادات"""
|
14 |
+
# المسارات الأساسية
|
15 |
+
self.app_dir = os.path.dirname(os.path.abspath(__file__))
|
16 |
+
self.assets_dir = os.path.join(self.app_dir, "assets")
|
17 |
+
self.data_dir = os.path.join(self.app_dir, "data")
|
18 |
+
|
19 |
+
# إنشاء المجلدات إذا لم تكن موجودة
|
20 |
+
Path(self.assets_dir).mkdir(parents=True, exist_ok=True)
|
21 |
+
Path(self.data_dir).mkdir(parents=True, exist_ok=True)
|
22 |
+
|
23 |
+
# مسارات الأصول
|
24 |
+
self.icons_dir = os.path.join(self.assets_dir, "icons")
|
25 |
+
self.images_dir = os.path.join(self.assets_dir, "images")
|
26 |
+
self.fonts_dir = os.path.join(self.assets_dir, "fonts")
|
27 |
+
|
28 |
+
# إنشاء مجلدات الأصول إذا لم تكن موجودة
|
29 |
+
Path(self.icons_dir).mkdir(parents=True, exist_ok=True)
|
30 |
+
Path(self.images_dir).mkdir(parents=True, exist_ok=True)
|
31 |
+
Path(self.fonts_dir).mkdir(parents=True, exist_ok=True)
|
32 |
+
|
33 |
+
# مسارات البيانات
|
34 |
+
self.database_file = os.path.join(self.data_dir, "database.db")
|
35 |
+
self.settings_file = os.path.join(self.data_dir, "settings.json")
|
36 |
+
self.charts_dir = os.path.join(self.data_dir, "charts")
|
37 |
+
|
38 |
+
# إنشاء مجلد الرسوم البيانية إذا لم يكن موجودًا
|
39 |
+
Path(self.charts_dir).mkdir(parents=True, exist_ok=True)
|
40 |
+
|
41 |
+
# تحميل الإعدادات
|
42 |
+
self.settings = self._load_settings()
|
43 |
+
|
44 |
+
def _load_settings(self):
|
45 |
+
"""تحميل الإعدادات من ملف JSON"""
|
46 |
+
default_settings = {
|
47 |
+
"app": {
|
48 |
+
"name": "نظام إدارة المناقصات",
|
49 |
+
"version": "1.0.0",
|
50 |
+
"language": "ar",
|
51 |
+
"theme": "light",
|
52 |
+
"font": "Cairo",
|
53 |
+
"font_size": 12
|
54 |
+
},
|
55 |
+
"database": {
|
56 |
+
"type": "sqlite",
|
57 |
+
"path": self.database_file
|
58 |
+
},
|
59 |
+
"ui": {
|
60 |
+
"window_width": 1200,
|
61 |
+
"window_height": 800,
|
62 |
+
"sidebar_width": 250
|
63 |
+
},
|
64 |
+
"notifications": {
|
65 |
+
"enabled": True,
|
66 |
+
"email_enabled": True,
|
67 |
+
"email_server": "smtp.example.com",
|
68 |
+
"email_port": 587,
|
69 |
+
"email_username": "",
|
70 |
+
"email_password": ""
|
71 |
+
},
|
72 |
+
"reports": {
|
73 |
+
"default_format": "pdf",
|
74 |
+
"default_path": os.path.join(self.data_dir, "reports")
|
75 |
+
},
|
76 |
+
"backup": {
|
77 |
+
"auto_backup": True,
|
78 |
+
"backup_frequency": "weekly",
|
79 |
+
"backup_path": os.path.join(self.data_dir, "backups"),
|
80 |
+
"max_backups": 10
|
81 |
+
}
|
82 |
+
}
|
83 |
+
|
84 |
+
# إذا كان ملف الإعدادات موجودًا، قم بتحميله
|
85 |
+
if os.path.exists(self.settings_file):
|
86 |
+
try:
|
87 |
+
with open(self.settings_file, "r", encoding="utf-8") as f:
|
88 |
+
settings = json.load(f)
|
89 |
+
|
90 |
+
# دمج الإعدادات المحملة مع الإعدادات الافتراضية
|
91 |
+
self._merge_settings(default_settings, settings)
|
92 |
+
return default_settings
|
93 |
+
except Exception as e:
|
94 |
+
print(f"خطأ في تحميل الإعدادات: {str(e)}")
|
95 |
+
return default_settings
|
96 |
+
else:
|
97 |
+
# إنشاء ملف الإعدادات الافتراضية
|
98 |
+
self._save_settings(default_settings)
|
99 |
+
return default_settings
|
100 |
+
|
101 |
+
def _merge_settings(self, default_settings, loaded_settings):
|
102 |
+
"""دمج الإعدادات المحملة مع الإعدادات الافتراضية"""
|
103 |
+
for key, value in loaded_settings.items():
|
104 |
+
if key in default_settings:
|
105 |
+
if isinstance(value, dict) and isinstance(default_settings[key], dict):
|
106 |
+
self._merge_settings(default_settings[key], value)
|
107 |
+
else:
|
108 |
+
default_settings[key] = value
|
109 |
+
|
110 |
+
def _save_settings(self, settings=None):
|
111 |
+
"""حفظ الإعدادات إلى ملف JSON"""
|
112 |
+
if settings is None:
|
113 |
+
settings = self.settings
|
114 |
+
|
115 |
+
try:
|
116 |
+
with open(self.settings_file, "w", encoding="utf-8") as f:
|
117 |
+
json.dump(settings, f, ensure_ascii=False, indent=4)
|
118 |
+
return True
|
119 |
+
except Exception as e:
|
120 |
+
print(f"خطأ في حفظ الإعدادات: {str(e)}")
|
121 |
+
return False
|
122 |
+
|
123 |
+
def get_setting(self, section, key, default=None):
|
124 |
+
"""الحصول على قيمة إعداد معين"""
|
125 |
+
try:
|
126 |
+
return self.settings[section][key]
|
127 |
+
except KeyError:
|
128 |
+
return default
|
129 |
+
|
130 |
+
def set_setting(self, section, key, value):
|
131 |
+
"""تعيين قيمة إعداد معين"""
|
132 |
+
if section not in self.settings:
|
133 |
+
self.settings[section] = {}
|
134 |
+
|
135 |
+
self.settings[section][key] = value
|
136 |
+
self._save_settings()
|
137 |
+
|
138 |
+
def get_app_name(self):
|
139 |
+
"""الحصول على اسم التطبيق"""
|
140 |
+
return self.get_setting("app", "name", "نظام إدارة المناقصات")
|
141 |
+
|
142 |
+
def get_app_version(self):
|
143 |
+
"""الحصول على إصدار التطبيق"""
|
144 |
+
return self.get_setting("app", "version", "1.0.0")
|
145 |
+
|
146 |
+
def get_language(self):
|
147 |
+
"""الحصول على لغة التطبيق"""
|
148 |
+
return self.get_setting("app", "language", "ar")
|
149 |
+
|
150 |
+
def set_language(self, language):
|
151 |
+
"""تعيين لغة التطبيق"""
|
152 |
+
self.set_setting("app", "language", language)
|
153 |
+
|
154 |
+
def get_theme(self):
|
155 |
+
"""الحصول على نمط التطبيق"""
|
156 |
+
return self.get_setting("app", "theme", "light")
|
157 |
+
|
158 |
+
def set_theme(self, theme):
|
159 |
+
"""تعيين نمط التطبيق"""
|
160 |
+
self.set_setting("app", "theme", theme)
|
161 |
+
|
162 |
+
def get_font(self):
|
163 |
+
"""الحصول على خط التطبيق"""
|
164 |
+
return self.get_setting("app", "font", "Cairo")
|
165 |
+
|
166 |
+
def set_font(self, font):
|
167 |
+
"""تعيين خط التطبيق"""
|
168 |
+
self.set_setting("app", "font", font)
|
169 |
+
|
170 |
+
def get_font_size(self):
|
171 |
+
"""الحصول على حجم خط التطبيق"""
|
172 |
+
return self.get_setting("app", "font_size", 12)
|
173 |
+
|
174 |
+
def set_font_size(self, font_size):
|
175 |
+
"""تعيين حجم خط التطبيق"""
|
176 |
+
self.set_setting("app", "font_size", font_size)
|
177 |
+
|
178 |
+
def get_window_size(self):
|
179 |
+
"""الحصول على حجم نافذة التطبيق"""
|
180 |
+
width = self.get_setting("ui", "window_width", 1200)
|
181 |
+
height = self.get_setting("ui", "window_height", 800)
|
182 |
+
return (width, height)
|
183 |
+
|
184 |
+
def set_window_size(self, width, height):
|
185 |
+
"""تعيين حجم نافذة التطبيق"""
|
186 |
+
self.set_setting("ui", "window_width", width)
|
187 |
+
self.set_setting("ui", "window_height", height)
|
188 |
+
|
189 |
+
def get_sidebar_width(self):
|
190 |
+
"""الحصول على عرض الشريط الجانبي"""
|
191 |
+
return self.get_setting("ui", "sidebar_width", 250)
|
192 |
+
|
193 |
+
def set_sidebar_width(self, width):
|
194 |
+
"""تعيين عرض الشريط الجانبي"""
|
195 |
+
self.set_setting("ui", "sidebar_width", width)
|
196 |
+
|
197 |
+
def get_database_config(self):
|
198 |
+
"""الحصول على إعدادات قاعدة البيانات"""
|
199 |
+
return self.settings.get("database", {
|
200 |
+
"type": "sqlite",
|
201 |
+
"path": self.database_file
|
202 |
+
})
|
203 |
+
|
204 |
+
def get_notifications_config(self):
|
205 |
+
"""الحصول على إعدادات الإشعارات"""
|
206 |
+
return self.settings.get("notifications", {
|
207 |
+
"enabled": True,
|
208 |
+
"email_enabled": True,
|
209 |
+
"email_server": "smtp.example.com",
|
210 |
+
"email_port": 587,
|
211 |
+
"email_username": "",
|
212 |
+
"email_password": ""
|
213 |
+
})
|
214 |
+
|
215 |
+
def get_reports_config(self):
|
216 |
+
"""الحصول على إعدادات التقارير"""
|
217 |
+
return self.settings.get("reports", {
|
218 |
+
"default_format": "pdf",
|
219 |
+
"default_path": os.path.join(self.data_dir, "reports")
|
220 |
+
})
|
221 |
+
|
222 |
+
def get_backup_config(self):
|
223 |
+
"""الحصول على إعدادات النسخ الاحتياطي"""
|
224 |
+
return self.settings.get("backup", {
|
225 |
+
"auto_backup": True,
|
226 |
+
"backup_frequency": "weekly",
|
227 |
+
"backup_path": os.path.join(self.data_dir, "backups"),
|
228 |
+
"max_backups": 10
|
229 |
+
})
|
config_manager.py
CHANGED
@@ -1,37 +1,37 @@
|
|
1 |
-
"""
|
2 |
-
مدير تكوين تطبيق Streamlit
|
3 |
-
يستخدم لمنع استدعاء set_page_config() أكثر من مرة في التطبيق
|
4 |
-
"""
|
5 |
-
|
6 |
-
class ConfigManager:
|
7 |
-
"""مدير تكوين التطبيق لمنع استدعاء set_page_config() أكثر من مرة"""
|
8 |
-
|
9 |
-
_instance = None
|
10 |
-
_page_config_set = False
|
11 |
-
|
12 |
-
def __new__(cls):
|
13 |
-
if cls._instance is None:
|
14 |
-
cls._instance = super(ConfigManager, cls).__new__(cls)
|
15 |
-
return cls._instance
|
16 |
-
|
17 |
-
def set_page_config_if_needed(self, **kwargs):
|
18 |
-
"""
|
19 |
-
تعيين تكوين الصفحة إذا لم يتم تعيينه بالفعل
|
20 |
-
|
21 |
-
المعلمات:
|
22 |
-
**kwargs: معلمات لدالة st.set_page_config()
|
23 |
-
|
24 |
-
العوائد:
|
25 |
-
bool: True إذا تم تعيين التكوين، False إذا كان التكوين معينًا بالفعل
|
26 |
-
"""
|
27 |
-
import streamlit as st
|
28 |
-
|
29 |
-
if not ConfigManager._page_config_set:
|
30 |
-
st.set_page_config(**kwargs)
|
31 |
-
ConfigManager._page_config_set = True
|
32 |
-
return True
|
33 |
-
return False
|
34 |
-
|
35 |
-
def is_page_config_set(self):
|
36 |
-
"""التحقق مما إذا كان تكوين الصفحة قد تم تعيينه بالفعل"""
|
37 |
-
return ConfigManager._page_config_set
|
|
|
1 |
+
"""
|
2 |
+
مدير تكوين تطبيق Streamlit
|
3 |
+
يستخدم لمنع استدعاء set_page_config() أكثر من مرة في التطبيق
|
4 |
+
"""
|
5 |
+
|
6 |
+
class ConfigManager:
|
7 |
+
"""مدير تكوين التطبيق لمنع استدعاء set_page_config() أكثر من مرة"""
|
8 |
+
|
9 |
+
_instance = None
|
10 |
+
_page_config_set = False
|
11 |
+
|
12 |
+
def __new__(cls):
|
13 |
+
if cls._instance is None:
|
14 |
+
cls._instance = super(ConfigManager, cls).__new__(cls)
|
15 |
+
return cls._instance
|
16 |
+
|
17 |
+
def set_page_config_if_needed(self, **kwargs):
|
18 |
+
"""
|
19 |
+
تعيين تكوين الصفحة إذا لم يتم تعيينه بالفعل
|
20 |
+
|
21 |
+
المعلمات:
|
22 |
+
**kwargs: معلمات لدالة st.set_page_config()
|
23 |
+
|
24 |
+
العوائد:
|
25 |
+
bool: True إذا تم تعيين التكوين، False إذا كان التكوين معينًا بالفعل
|
26 |
+
"""
|
27 |
+
import streamlit as st
|
28 |
+
|
29 |
+
if not ConfigManager._page_config_set:
|
30 |
+
st.set_page_config(**kwargs)
|
31 |
+
ConfigManager._page_config_set = True
|
32 |
+
return True
|
33 |
+
return False
|
34 |
+
|
35 |
+
def is_page_config_set(self):
|
36 |
+
"""التحقق مما إذا كان تكوين الصفحة قد تم تعيينه بالفعل"""
|
37 |
+
return ConfigManager._page_config_set
|
data/exports/boq_20250403_133827.xlsx
ADDED
Binary file (5.14 kB). View file
|
|
data/exports/boq_20250403_135418.xlsx
ADDED
Binary file (5.17 kB). View file
|
|
data/exports/boq_20250403_135819.xlsx
ADDED
Binary file (5.18 kB). View file
|
|
data/exports/boq_20250403_140953.xlsx
ADDED
Binary file (5.26 kB). View file
|
|
data/exports/boq_20250403_143659.xlsx
ADDED
Binary file (5.28 kB). View file
|
|
database/db_connector.py
CHANGED
@@ -45,6 +45,19 @@ class DatabaseConnector:
|
|
45 |
|
46 |
def _create_tables(self):
|
47 |
"""إنشاء جداول قاعدة البيانات"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
# جدول المستخدمين
|
49 |
self.cursor.execute('''
|
50 |
CREATE TABLE IF NOT EXISTS users (
|
@@ -320,4 +333,4 @@ class DatabaseConnector:
|
|
320 |
"""إغلاق الاتصال"""
|
321 |
if self.connection:
|
322 |
self.connection.close()
|
323 |
-
logger.info("تم إغلاق الاتصال بقاعدة البيانات")
|
|
|
45 |
|
46 |
def _create_tables(self):
|
47 |
"""إنشاء جداول قاعدة البيانات"""
|
48 |
+
# جدول المشاريع المحفوظة
|
49 |
+
self.cursor.execute('''
|
50 |
+
CREATE TABLE IF NOT EXISTS saved_projects (
|
51 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
52 |
+
project_name TEXT NOT NULL,
|
53 |
+
project_code TEXT,
|
54 |
+
project_description TEXT,
|
55 |
+
boq_data TEXT NOT NULL,
|
56 |
+
total_cost REAL NOT NULL,
|
57 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
58 |
+
)
|
59 |
+
''')
|
60 |
+
|
61 |
# جدول المستخدمين
|
62 |
self.cursor.execute('''
|
63 |
CREATE TABLE IF NOT EXISTS users (
|
|
|
333 |
"""إغلاق الاتصال"""
|
334 |
if self.connection:
|
335 |
self.connection.close()
|
336 |
+
logger.info("تم إغلاق الاتصال بقاعدة البيانات")
|
database/models.py
CHANGED
@@ -378,88 +378,84 @@ class Document:
|
|
378 |
return False
|
379 |
|
380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
381 |
class PricingItem:
|
382 |
"""نموذج بند التسعير"""
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
self.
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
'unit_price': self.unit_price,
|
454 |
-
'total_price': self.total_price,
|
455 |
-
'created_by': self.created_by
|
456 |
-
}
|
457 |
-
|
458 |
-
self.id = db.insert('pricing_items', data)
|
459 |
-
return self.id
|
460 |
-
except Exception as e:
|
461 |
-
logger.error(f"خطأ في حفظ بند التسعير: {str(e)}")
|
462 |
-
return None
|
463 |
|
464 |
|
465 |
class Risk:
|
@@ -546,6 +542,7 @@ class Risk:
|
|
546 |
return None
|
547 |
|
548 |
|
|
|
549 |
class Report:
|
550 |
"""نموذج التقرير"""
|
551 |
|
@@ -623,4 +620,4 @@ class Report:
|
|
623 |
return self.id
|
624 |
except Exception as e:
|
625 |
logger.error(f"خطأ في حفظ التقرير: {str(e)}")
|
626 |
-
return None
|
|
|
378 |
return False
|
379 |
|
380 |
|
381 |
+
"""
|
382 |
+
نماذج قاعدة البيانات للتسعير
|
383 |
+
"""
|
384 |
+
import sqlite3
|
385 |
+
from datetime import datetime
|
386 |
+
|
387 |
class PricingItem:
|
388 |
"""نموذج بند التسعير"""
|
389 |
+
def __init__(self, db):
|
390 |
+
self.db = db
|
391 |
+
|
392 |
+
def create_table(self):
|
393 |
+
"""إنشاء جدول بنود التسعير"""
|
394 |
+
self.db.execute("""
|
395 |
+
CREATE TABLE IF NOT EXISTS pricing_items (
|
396 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
397 |
+
project_id INTEGER,
|
398 |
+
code TEXT NOT NULL,
|
399 |
+
description TEXT NOT NULL,
|
400 |
+
unit TEXT NOT NULL,
|
401 |
+
quantity REAL NOT NULL,
|
402 |
+
unit_price REAL NOT NULL,
|
403 |
+
total_price REAL NOT NULL,
|
404 |
+
category TEXT,
|
405 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
406 |
+
FOREIGN KEY (project_id) REFERENCES projects (id)
|
407 |
+
)
|
408 |
+
""")
|
409 |
+
|
410 |
+
def add_item(self, project_id, item_data):
|
411 |
+
"""إضافة بند جديد"""
|
412 |
+
sql = """
|
413 |
+
INSERT INTO pricing_items (
|
414 |
+
project_id, code, description, unit,
|
415 |
+
quantity, unit_price, total_price, category
|
416 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
417 |
+
"""
|
418 |
+
values = (
|
419 |
+
project_id,
|
420 |
+
item_data['code'],
|
421 |
+
item_data['description'],
|
422 |
+
item_data['unit'],
|
423 |
+
item_data['quantity'],
|
424 |
+
item_data['unit_price'],
|
425 |
+
item_data['total_price'],
|
426 |
+
item_data.get('category')
|
427 |
+
)
|
428 |
+
return self.db.execute(sql, values)
|
429 |
+
|
430 |
+
def get_project_items(self, project_id):
|
431 |
+
"""جلب بنود المشروع"""
|
432 |
+
sql = "SELECT * FROM pricing_items WHERE project_id = ?"
|
433 |
+
return self.db.fetch_all(sql, (project_id,))
|
434 |
+
|
435 |
+
def update_item(self, item_id, item_data):
|
436 |
+
"""تحديث بند"""
|
437 |
+
sql = """
|
438 |
+
UPDATE pricing_items
|
439 |
+
SET code=?, description=?, unit=?, quantity=?,
|
440 |
+
unit_price=?, total_price=?, category=?
|
441 |
+
WHERE id=?
|
442 |
+
"""
|
443 |
+
values = (
|
444 |
+
item_data['code'],
|
445 |
+
item_data['description'],
|
446 |
+
item_data['unit'],
|
447 |
+
item_data['quantity'],
|
448 |
+
item_data['unit_price'],
|
449 |
+
item_data['total_price'],
|
450 |
+
item_data.get('category'),
|
451 |
+
item_id
|
452 |
+
)
|
453 |
+
return self.db.execute(sql, values)
|
454 |
+
|
455 |
+
def delete_item(self, item_id):
|
456 |
+
"""حذف بند"""
|
457 |
+
sql = "DELETE FROM pricing_items WHERE id = ?"
|
458 |
+
return self.db.execute(sql, (item_id,))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
459 |
|
460 |
|
461 |
class Risk:
|
|
|
542 |
return None
|
543 |
|
544 |
|
545 |
+
|
546 |
class Report:
|
547 |
"""نموذج التقرير"""
|
548 |
|
|
|
620 |
return self.id
|
621 |
except Exception as e:
|
622 |
logger.error(f"خطأ في حفظ التقرير: {str(e)}")
|
623 |
+
return None
|
docs/architecture.md
CHANGED
@@ -1,136 +1,136 @@
|
|
1 |
-
# هيكلية النظام المحسنة لنظام إدارة المناقصات
|
2 |
-
|
3 |
-
## نظرة عامة
|
4 |
-
هذا المستند يوضح هيكلية النظام المحسنة لنظام إدارة المناقصات، والذي يتضمن الوحدات التالية:
|
5 |
-
- وحدة التسعير المتكاملة
|
6 |
-
- وحدة الذكاء الاصطناعي
|
7 |
-
- وحدة تحليل البيانات
|
8 |
-
- وحدة الموارد
|
9 |
-
|
10 |
-
## هيكلية المجلدات
|
11 |
-
|
12 |
-
```
|
13 |
-
tender_system/
|
14 |
-
├── app.py # نقطة الدخول الرئيسية للتطبيق
|
15 |
-
├── config.py # إعدادات التطبيق
|
16 |
-
├── requirements.txt # متطلبات المكتبات
|
17 |
-
├── README.md # توثيق النظام
|
18 |
-
├── assets/ # الأصول الثابتة
|
19 |
-
│ ├── images/ # الصور
|
20 |
-
│ ├── icons/ # الأيقونات
|
21 |
-
│ └── fonts/ # الخطوط
|
22 |
-
├── data/ # البيانات
|
23 |
-
│ ├── templates/ # قوالب البيانات
|
24 |
-
│ └── charts/ # بيانات الرسوم البيانية
|
25 |
-
├── database/ # قاعدة البيانات
|
26 |
-
│ ├── db_connector.py # موصل قاعدة البيانات
|
27 |
-
│ └── models.py # نماذج البيانات
|
28 |
-
├── modules/ # وحدات النظام
|
29 |
-
│ ├── pricing/ # وحدة التسعير
|
30 |
-
│ │ ├── pricing_app.py # تطبيق التسعير
|
31 |
-
│ │ └── services/ # خدمات التسعير
|
32 |
-
│ │ ├── standard_pricing.py
|
33 |
-
│ │ ├── unbalanced_pricing.py
|
34 |
-
│ │ ├── local_content_calculator.py
|
35 |
-
│ │ ├── price_prediction.py
|
36 |
-
│ │ ├── construction_cost_calculator.py
|
37 |
-
│ │ └── construction_templates.py
|
38 |
-
│ ├── ai_assistant/ # وحدة الذكاء الاصطناعي
|
39 |
-
│ │ ├── ai_app.py # تطبيق الذكاء الاصطناعي
|
40 |
-
│ │ └── services/ # خدمات الذكاء الاصطناعي
|
41 |
-
│ │ ├── openai_service.py
|
42 |
-
│ │ ├── anthropic_service.py
|
43 |
-
│ │ ├── local_llm_service.py
|
44 |
-
│ │ └── prompt_templates.py
|
45 |
-
│ ├── document_analysis/ # وحدة تحليل المستندات
|
46 |
-
│ │ ├── document_app.py # تطبيق تحليل المستندات
|
47 |
-
│ │ └── services/ # خدمات تحليل المستندات
|
48 |
-
│ │ ├── text_extractor.py
|
49 |
-
│ │ ├── item_extractor.py
|
50 |
-
│ │ └── document_parser.py
|
51 |
-
│ ├── resources/ # وحدة الموارد
|
52 |
-
│ │ ├── resources_app.py # تطبيق الموارد
|
53 |
-
│ │ └── services/ # خدمات الموارد
|
54 |
-
│ │ ├── material_manager.py
|
55 |
-
│ │ ├── labor_manager.py
|
56 |
-
│ │ ├── equipment_manager.py
|
57 |
-
│ │ └── subcontractor_manager.py
|
58 |
-
│ ├── project_management/ # وحدة إدارة المشاريع
|
59 |
-
│ │ └── project_app.py # تطبيق إدارة المشاريع
|
60 |
-
│ └── reports/ # وحدة التقارير
|
61 |
-
│ └── reports_app.py # تطبيق التقارير
|
62 |
-
├── styling/ # التنسيق
|
63 |
-
│ ├── theme.py # سمات التطبيق
|
64 |
-
│ ├── icons.py # أيقونات التطبيق
|
65 |
-
│ └── charts.py # تنسيق الرسوم البيانية
|
66 |
-
├── utils/ # أدوات مساعدة
|
67 |
-
│ ├── excel_handler.py # معالج ملفات Excel
|
68 |
-
│ ├── pdf_handler.py # معالج ملفات PDF
|
69 |
-
│ ├── helpers.py # دوال مساعدة
|
70 |
-
│ └── auth.py # المصادقة
|
71 |
-
└── tests/ # اختبارات
|
72 |
-
├── test_pricing.py # اختبارات وحدة التسعير
|
73 |
-
├── test_ai.py # اختبارات وحدة الذكاء الاصطناعي
|
74 |
-
├── test_document.py # اختبارات وحدة تحليل المستندات
|
75 |
-
└── test_resources.py # اختبارات وحدة الموارد
|
76 |
-
```
|
77 |
-
|
78 |
-
## تفاصيل الوحدات
|
79 |
-
|
80 |
-
### 1. وحدة التسعير المتكاملة
|
81 |
-
- **الوظائف الرئيسية**:
|
82 |
-
- إنشاء تسعير جديد
|
83 |
-
- تحليل سعر البند
|
84 |
-
- نموذج التسعير الشامل
|
85 |
-
- التسعير غير المتزن
|
86 |
-
- المحتوى المحلي
|
87 |
-
- حاسبة تكاليف البناء
|
88 |
-
- الأدوات المساعدة
|
89 |
-
|
90 |
-
### 2. وحدة الذكاء الاصطناعي
|
91 |
-
- **الوظائف
|
92 |
-
- تحليل المستندات باستخدام الذكاء الاصطناعي
|
93 |
-
- توليد توصيات ذكية للتسعير
|
94 |
-
- تحليل المخاطر باستخدام الذكاء الاصطناعي
|
95 |
-
- مساعد المحادثة الذكي
|
96 |
-
- تلخيص المستندات
|
97 |
-
- استخراج المعلومات الرئيسية
|
98 |
-
|
99 |
-
### 3. وحدة تحليل البيانات
|
100 |
-
- **الوظائف الرئيسية**:
|
101 |
-
- استخراج النصوص من المستندات
|
102 |
-
- استخراج الجداول والبنود
|
103 |
-
- تحليل المستندات
|
104 |
-
- تحويل المستندات إلى بيانات منظمة
|
105 |
-
- تحليل الصور والمخططات
|
106 |
-
|
107 |
-
### 4. وحدة الموارد
|
108 |
-
- **الوظائف الرئيسية**:
|
109 |
-
- إدارة المواد
|
110 |
-
- إدارة العمالة
|
111 |
-
- إدارة المعدات
|
112 |
-
- إدارة المقاولين من الباطن
|
113 |
-
- تحليل تكاليف الموارد
|
114 |
-
- تخطيط الموارد
|
115 |
-
|
116 |
-
## واجهة المستخدم
|
117 |
-
- تستخدم إطار عمل Streamlit لبناء واجهة مستخدم تفاعلية
|
118 |
-
- تدعم اللغة العربية بشكل كامل
|
119 |
-
- تتضمن تبويبات لكل وحدة من وحدات النظام
|
120 |
-
- تدعم الوضعين الفاتح والداكن
|
121 |
-
- تتضمن رسومات بيانية تفاعلية باستخدام Plotly
|
122 |
-
|
123 |
-
## تكامل الوحدات
|
124 |
-
- تتكامل وحدة التسعير مع وحدة الموارد لاستخدام بيانات الأسعار
|
125 |
-
- تتكامل وحدة تحليل البيانات مع وحدة التسعير لاستخراج بنود المناقصة
|
126 |
-
- تتكامل وحدة الذكاء الاصطناعي مع جميع الوحدات لتقديم توصيات ذكية
|
127 |
-
- تتكامل جميع الوحدات مع قاعدة البيانات المركزية
|
128 |
-
|
129 |
-
## التقنيات المستخدمة
|
130 |
-
- **لغة البرمجة**: Python
|
131 |
-
- **إطار عمل واجهة المستخدم**: Streamlit
|
132 |
-
- **معالجة البيانات**: Pandas, NumPy
|
133 |
-
- **الرسوم البيانية**: Plotly, Matplotlib
|
134 |
-
- **الذكاء الاصطناعي**: OpenAI API, Anthropic API, Transformers
|
135 |
-
- **معالجة المستندات**: PyPDF2, python-docx, pdf2image
|
136 |
-
- **قاعدة البيانات**: SQLAlchemy
|
|
|
1 |
+
# هيكلية النظام المحسنة لنظام إدارة المناقصات
|
2 |
+
|
3 |
+
## نظرة عامة
|
4 |
+
هذا المستند يوضح هيكلية النظام المحسنة لنظام إدارة المناقصات، والذي يتضمن الوحدات التالية:
|
5 |
+
- وحدة التسعير المتكاملة
|
6 |
+
- وحدة الذكاء الاصطناعي
|
7 |
+
- وحدة تحليل البيانات
|
8 |
+
- وحدة الموارد
|
9 |
+
|
10 |
+
## هيكلية المجلدات
|
11 |
+
|
12 |
+
```
|
13 |
+
tender_system/
|
14 |
+
├── app.py # نقطة الدخول الرئيسية للتطبيق
|
15 |
+
├── config.py # إعدادات التطبيق
|
16 |
+
├── requirements.txt # متطلبات المكتبات
|
17 |
+
├── README.md # توثيق النظام
|
18 |
+
├── assets/ # الأصول الثابتة
|
19 |
+
│ ├── images/ # الصور
|
20 |
+
│ ├── icons/ # الأيقونات
|
21 |
+
│ └── fonts/ # الخطوط
|
22 |
+
├── data/ # البيانات
|
23 |
+
│ ├── templates/ # قوالب البيانات
|
24 |
+
│ └── charts/ # بيانات الرسوم البيانية
|
25 |
+
├── database/ # قاعدة البيانات
|
26 |
+
│ ├── db_connector.py # موصل قاعدة البيانات
|
27 |
+
│ └── models.py # نماذج البيانات
|
28 |
+
├── modules/ # وحدات النظام
|
29 |
+
│ ├── pricing/ # وحدة التسعير
|
30 |
+
│ │ ├── pricing_app.py # تطبيق التسعير
|
31 |
+
│ │ └── services/ # خدمات التسعير
|
32 |
+
│ │ ├── standard_pricing.py
|
33 |
+
│ │ ├── unbalanced_pricing.py
|
34 |
+
│ │ ├── local_content_calculator.py
|
35 |
+
│ │ ├── price_prediction.py
|
36 |
+
│ │ ├── construction_cost_calculator.py
|
37 |
+
│ │ └── construction_templates.py
|
38 |
+
│ ├── ai_assistant/ # وحدة الذكاء الاصطناعي
|
39 |
+
│ │ ├── ai_app.py # تطبيق الذكاء الاصطناعي
|
40 |
+
│ │ └── services/ # خدمات الذكاء الاصطناعي
|
41 |
+
│ │ ├── openai_service.py
|
42 |
+
│ │ ├── anthropic_service.py
|
43 |
+
│ │ ├── local_llm_service.py
|
44 |
+
│ │ └── prompt_templates.py
|
45 |
+
│ ├── document_analysis/ # وحدة تحليل المستندات
|
46 |
+
│ │ ├── document_app.py # تطبيق تحليل المستندات
|
47 |
+
│ │ └── services/ # خدمات تحليل المستندات
|
48 |
+
│ │ ├── text_extractor.py
|
49 |
+
│ │ ├── item_extractor.py
|
50 |
+
│ │ └── document_parser.py
|
51 |
+
│ ├── resources/ # وحدة الموارد
|
52 |
+
│ │ ├── resources_app.py # تطبيق الموارد
|
53 |
+
│ │ └── services/ # خدمات الموارد
|
54 |
+
│ │ ├── material_manager.py
|
55 |
+
│ │ ├── labor_manager.py
|
56 |
+
│ │ ├── equipment_manager.py
|
57 |
+
│ │ └── subcontractor_manager.py
|
58 |
+
│ ├── project_management/ # وحدة إدارة المشاريع
|
59 |
+
│ │ └── project_app.py # تطبيق إدارة المشاريع
|
60 |
+
│ └── reports/ # وحدة التقارير
|
61 |
+
│ └── reports_app.py # تطبيق التقارير
|
62 |
+
├── styling/ # التنسيق
|
63 |
+
│ ├── theme.py # سمات التطبيق
|
64 |
+
│ ├── icons.py # أيقونات التطبيق
|
65 |
+
│ └── charts.py # تنسيق الرسوم البيانية
|
66 |
+
├── utils/ # أدوات مساعدة
|
67 |
+
│ ├── excel_handler.py # معالج ملفات Excel
|
68 |
+
│ ├── pdf_handler.py # معالج ملفات PDF
|
69 |
+
│ ├── helpers.py # دوال مساعدة
|
70 |
+
│ └── auth.py # المصادقة
|
71 |
+
└── tests/ # اختبارات
|
72 |
+
├── test_pricing.py # اختبارات وحدة التسعير
|
73 |
+
├── test_ai.py # اختبارات وحدة الذكاء الاصطناعي
|
74 |
+
├── test_document.py # اختبارات وحدة تحليل المستندات
|
75 |
+
└── test_resources.py # اختبارات وحدة الموارد
|
76 |
+
```
|
77 |
+
|
78 |
+
## تفاصيل الوحدات
|
79 |
+
|
80 |
+
### 1. وحدة التسعير المتكاملة
|
81 |
+
- **الوظائف الرئيسية**:
|
82 |
+
- إنشاء تسعير جديد
|
83 |
+
- تحليل سعر البند
|
84 |
+
- نموذج التسعير الشامل
|
85 |
+
- التسعير غير المتزن
|
86 |
+
- المحتوى المحلي
|
87 |
+
- حاسبة تكاليف البناء
|
88 |
+
- الأدوات المساعدة
|
89 |
+
|
90 |
+
### 2. وحدة الذكاء الاصطناعي
|
91 |
+
- **الوظائف الرئيسية**:
|
92 |
+
- تحليل المستندات باستخدام الذكاء الاصطناعي
|
93 |
+
- توليد توصيات ذكية للتسعير
|
94 |
+
- تحليل المخاطر باستخدام الذكاء الاصطناعي
|
95 |
+
- مساعد المحادثة الذكي
|
96 |
+
- تلخيص المستندات
|
97 |
+
- استخراج المعلومات الرئيسية
|
98 |
+
|
99 |
+
### 3. وحدة تحليل البيانات
|
100 |
+
- **الوظائف الرئيسية**:
|
101 |
+
- استخراج النصوص من المستندات
|
102 |
+
- استخراج الجداول والبنود
|
103 |
+
- تحليل المستندات
|
104 |
+
- تحويل المستندات إلى بيانات منظمة
|
105 |
+
- تحليل الصور والمخططات
|
106 |
+
|
107 |
+
### 4. وحدة الموارد
|
108 |
+
- **الوظائف الرئيسية**:
|
109 |
+
- إدارة المواد
|
110 |
+
- إدارة العمالة
|
111 |
+
- إدارة المعدات
|
112 |
+
- إدارة المقاولين من الباطن
|
113 |
+
- تحليل تكاليف الموارد
|
114 |
+
- تخطيط الموارد
|
115 |
+
|
116 |
+
## واجهة المستخدم
|
117 |
+
- تستخدم إطار عمل Streamlit لبناء واجهة مستخدم تفاعلية
|
118 |
+
- تدعم اللغة العربية بشكل كامل
|
119 |
+
- تتضمن تبويبات لكل وحدة من وحدات النظام
|
120 |
+
- تدعم الوضعين الفاتح والداكن
|
121 |
+
- تتضمن رسومات بيانية تفاعلية باستخدام Plotly
|
122 |
+
|
123 |
+
## تكامل الوحدات
|
124 |
+
- تتكامل وحدة التسعير مع وحدة الموارد لاستخدام بيانات الأسعار
|
125 |
+
- تتكامل وحدة تحليل البيانات مع وحدة التسعير لاستخراج بنود المناقصة
|
126 |
+
- تتكامل وحدة الذكاء الاصطناعي مع جميع الوحدات لتقديم توصيات ذكية
|
127 |
+
- تتكامل جميع الوحدات مع قاعدة البيانات المركزية
|
128 |
+
|
129 |
+
## التقنيات المستخدمة
|
130 |
+
- **لغة البرمجة**: Python
|
131 |
+
- **إطار عمل واجهة المستخدم**: Streamlit
|
132 |
+
- **معالجة البيانات**: Pandas, NumPy
|
133 |
+
- **الرسوم البيانية**: Plotly, Matplotlib
|
134 |
+
- **الذكاء الاصطناعي**: OpenAI API, Anthropic API, Transformers
|
135 |
+
- **معالجة المستندات**: PyPDF2, python-docx, pdf2image
|
136 |
+
- **قاعدة البيانات**: SQLAlchemy
|
docs/missing_modules_analysis.md
CHANGED
@@ -1,88 +1,88 @@
|
|
1 |
-
# تحليل الوحدات الناقصة والميزات المطلوبة
|
2 |
-
|
3 |
-
## الوحدات الناقصة في النظام الحالي
|
4 |
-
|
5 |
-
بناءً على تحليل دليل المستخدم والمتطلبات المقدمة، تم تحديد الوحدات والميزات التالية التي يجب إضافتها أو تحسينها في النظام:
|
6 |
-
|
7 |
-
### 1. وحدة الخرائط والمواقع
|
8 |
-
- خرائط تفاعلية لمواقع المشاريع
|
9 |
-
- عرض المشاريع على الخريطة بناءً على الموقع الجغرافي
|
10 |
-
- إمكانية تحديد المسافات والمناطق
|
11 |
-
|
12 |
-
### 2. وحدة الإشعارات الذكية
|
13 |
-
- نظام إشعارات متكامل للتنبيهات المهمة
|
14 |
-
- إشعارات للمواعيد النهائية والمهام
|
15 |
-
- إشعارات لتغييرات حالة المناقصات والمشاريع
|
16 |
-
|
17 |
-
### 3. وحدة مقارنة المستندات
|
18 |
-
- أدوات متطورة لمقارنة المستندات
|
19 |
-
- تحديد الاختلافات بين إصدارات المستندات
|
20 |
-
- تحليل التغييرات في الشروط والبنود
|
21 |
-
|
22 |
-
### 4. وحدة الترجمة
|
23 |
-
- ترجمة المستندات والنصوص بين اللغات
|
24 |
-
- دعم خاص للترجمة بين العربية والإنجليزية
|
25 |
-
- ترجمة المصطلحات الفنية والقانونية
|
26 |
-
|
27 |
-
### 5. تحسين وحدة الذكاء الاصطناعي
|
28 |
-
- تكامل مع نماذج الذكاء الاصطناعي المتقدمة
|
29 |
-
- العمل في بيئة هجينة (محلية وسحابية)
|
30 |
-
- طلب مفاتيح API من قسم الأمان
|
31 |
-
|
32 |
-
### 6. تحسين وحدة التسعير المتكامل
|
33 |
-
- تطوير نظام تسعير شامل ومتكامل
|
34 |
-
- تحسين واجهة المستخدم وسهولة الاستخدام
|
35 |
-
- إضافة ميزات تحليل الأسعار المتقدمة
|
36 |
-
|
37 |
-
### 7. تحسين وحدة إدارة المشاريع
|
38 |
-
- تطوير وحدة متكاملة لإدارة المشاريع المرساة
|
39 |
-
- متابعة تنفيذ المشاريع ومراحلها
|
40 |
-
- إدارة الموارد والجداول الزمنية للمشاريع
|
41 |
-
|
42 |
-
## تحسينات الواجهة المرئية المطلوبة
|
43 |
-
|
44 |
-
### 1. تحسين التصميم العام
|
45 |
-
- تطوير واجهة مستخدم أكثر احترافية
|
46 |
-
- تحسين الألوان والتباين
|
47 |
-
- تنسيق متناسق بين جميع الوحدات
|
48 |
-
|
49 |
-
### 2. تحسين القوائم والتنقل
|
50 |
-
- تطوير شريط قوائم رئيسي متكامل
|
51 |
-
- تحسين تجربة التنقل بين الوحدات
|
52 |
-
- إضافة اختصارات للوظائف الأكثر استخداماً
|
53 |
-
|
54 |
-
### 3. تحسين عرض الشعار والهوية
|
55 |
-
- تحسين عرض شعار الشركة
|
56 |
-
- تطبيق هوية بصرية موحدة في جميع أجزاء النظام
|
57 |
-
- إضافة خيارات تخصيص الواجهة
|
58 |
-
|
59 |
-
### 4. تحسين لوحات المعلومات
|
60 |
-
- تطوير لوحات معلومات تفاعلية وجذابة
|
61 |
-
- تحسين عرض المؤشرات والإحصائيات
|
62 |
-
- إضافة رسوم بيانية متقدمة
|
63 |
-
|
64 |
-
## متطلبات التكامل والاتساق
|
65 |
-
|
66 |
-
### 1. تكامل الوحدات
|
67 |
-
- ضمان التكامل السلس بين جميع وحدات النظام
|
68 |
-
- مشاركة البيانات بين الوحدات المختلفة
|
69 |
-
- واجهة مستخدم موحدة عبر جميع الوحدات
|
70 |
-
|
71 |
-
### 2. اتساق البيانات
|
72 |
-
- ضمان اتساق البيانات بين جميع أجزاء النظام
|
73 |
-
- تزامن البيانات بين الوحدات المختلفة
|
74 |
-
- منع تكرار البيانات وتضاربها
|
75 |
-
|
76 |
-
### 3. أداء النظام
|
77 |
-
- تحسين سرعة استجابة النظام
|
78 |
-
- تحسين كفاءة استخدام الموارد
|
79 |
-
- تحسين تجربة المستخدم العامة
|
80 |
-
|
81 |
-
## الاعتماديات والمتطلبات الفنية
|
82 |
-
|
83 |
-
يجب تحديث ملف المتطلبات ليشمل جميع المكتبات اللازمة للوحدات الجديدة والمحسنة، بما في ذلك:
|
84 |
-
- مكتبات الخرائط والتحليل المكاني
|
85 |
-
- مكتبات الترجمة ومعالجة اللغات
|
86 |
-
- مكتبات الذكاء الاصطناعي والتعلم الآلي
|
87 |
-
- مكتبات الرسوم البيانية والتصور
|
88 |
-
- مكتبات واجهة المستخدم المتقدمة
|
|
|
1 |
+
# تحليل الوحدات الناقصة والميزات المطلوبة
|
2 |
+
|
3 |
+
## الوحدات الناقصة في النظام الحالي
|
4 |
+
|
5 |
+
بناءً على تحليل دليل المستخدم والمتطلبات المقدمة، تم تحديد الوحدات والميزات التالية التي يجب إضافتها أو تحسينها في النظام:
|
6 |
+
|
7 |
+
### 1. وحدة الخرائط والمواقع
|
8 |
+
- خرائط تفاعلية لمواقع المشاريع
|
9 |
+
- عرض المشاريع على الخريطة بناءً على الموقع الجغرافي
|
10 |
+
- إمكانية تحديد المسافات والمناطق
|
11 |
+
|
12 |
+
### 2. وحدة الإشعارات الذكية
|
13 |
+
- نظام إشعارات متكامل للتنبيهات المهمة
|
14 |
+
- إشعارات للمواعيد النهائية والمهام
|
15 |
+
- إشعارات لتغييرات حالة المناقصات والمشاريع
|
16 |
+
|
17 |
+
### 3. وحدة مقارنة المستندات
|
18 |
+
- أدوات متطورة لمقارنة المستندات
|
19 |
+
- تحديد الاختلافات بين إصدارات المستندات
|
20 |
+
- تحليل التغييرات في الشروط والبنود
|
21 |
+
|
22 |
+
### 4. وحدة الترجمة
|
23 |
+
- ترجمة المستندات والنصوص بين اللغات
|
24 |
+
- دعم خاص للترجمة بين العربية والإنجليزية
|
25 |
+
- ترجمة المصطلحات الفنية والقانونية
|
26 |
+
|
27 |
+
### 5. تحسين وحدة الذكاء الاصطناعي
|
28 |
+
- تكامل مع نماذج الذكاء الاصطناعي المتقدمة
|
29 |
+
- العمل في بيئة هجينة (محلية وسحابية)
|
30 |
+
- طلب مفاتيح API من قسم الأمان
|
31 |
+
|
32 |
+
### 6. تحسين وحدة التسعير المتكامل
|
33 |
+
- تطوير نظام تسعير شامل ومتكامل
|
34 |
+
- تحسين واجهة المستخدم وسهولة الاستخدام
|
35 |
+
- إضافة ميزات تحليل الأسعار المتقدمة
|
36 |
+
|
37 |
+
### 7. تحسين وحدة إدارة المشاريع
|
38 |
+
- تطوير وحدة متكاملة لإدارة المشاريع المرساة
|
39 |
+
- متابعة تنفيذ المشاريع ومراحلها
|
40 |
+
- إدارة الموارد والجداول الزمنية للمشاريع
|
41 |
+
|
42 |
+
## تحسينات الواجهة المرئية المطلوبة
|
43 |
+
|
44 |
+
### 1. تحسين التصميم العام
|
45 |
+
- تطوير واجهة مستخدم أكثر احترافية
|
46 |
+
- تحسين الألوان والتباين
|
47 |
+
- تنسيق متناسق بين جميع الوحدات
|
48 |
+
|
49 |
+
### 2. تحسين القوائم والتنقل
|
50 |
+
- تطوير شريط قوائم رئيسي متكامل
|
51 |
+
- تحسين تجربة التنقل بين الوحدات
|
52 |
+
- إضافة اختصارات للوظائف الأكثر استخداماً
|
53 |
+
|
54 |
+
### 3. تحسين عرض الشعار والهوية
|
55 |
+
- تحسين عرض شعار الشركة
|
56 |
+
- تطبيق هوية بصرية موحدة في جميع أجزاء النظام
|
57 |
+
- إضافة خيارات تخصيص الواجهة
|
58 |
+
|
59 |
+
### 4. تحسين لوحات المعلومات
|
60 |
+
- تطوير لوحات معلومات تفاعلية وجذابة
|
61 |
+
- تحسين عرض المؤشرات والإحصائيات
|
62 |
+
- إضافة رسوم بيانية متقدمة
|
63 |
+
|
64 |
+
## متطلبات التكامل والاتساق
|
65 |
+
|
66 |
+
### 1. تكامل الوحدات
|
67 |
+
- ضمان التكامل السلس بين جميع وحدات النظام
|
68 |
+
- مشاركة البيانات بين الوحدات المختلفة
|
69 |
+
- واجهة مستخدم موحدة عبر جميع الوحدات
|
70 |
+
|
71 |
+
### 2. اتساق البيانات
|
72 |
+
- ضمان اتساق البيانات بين جميع أجزاء النظام
|
73 |
+
- تزامن البيانات بين الوحدات المختلفة
|
74 |
+
- منع تكرار البيانات وتضاربها
|
75 |
+
|
76 |
+
### 3. أداء النظام
|
77 |
+
- تحسين سرعة استجابة النظام
|
78 |
+
- تحسين كفاءة استخدام الموارد
|
79 |
+
- تحسين تجربة المستخدم العامة
|
80 |
+
|
81 |
+
## الاعتماديات والمتطلبات الفنية
|
82 |
+
|
83 |
+
يجب تحديث ملف المتطلبات ليشمل جميع المكتبات اللازمة للوحدات الجديدة والمحسنة، بما في ذلك:
|
84 |
+
- مكتبات الخرائط والتحليل المكاني
|
85 |
+
- مكتبات الترجمة ومعالجة اللغات
|
86 |
+
- مكتبات الذكاء الاصطناعي والتعلم الآلي
|
87 |
+
- مكتبات الرسوم البيانية والتصور
|
88 |
+
- مكتبات واجهة المستخدم المتقدمة
|
docs/pricing_module_design.md
CHANGED
@@ -1,714 +1,714 @@
|
|
1 |
-
# تصميم وحدة التسعير المتكاملة وتحليل الأسعار
|
2 |
-
|
3 |
-
## هيكل قاعدة البيانات
|
4 |
-
|
5 |
-
### جدول فئات البنود (pricing_categories)
|
6 |
-
```sql
|
7 |
-
CREATE TABLE pricing_categories (
|
8 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
9 |
-
name TEXT NOT NULL,
|
10 |
-
description TEXT,
|
11 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
12 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
13 |
-
);
|
14 |
-
```
|
15 |
-
|
16 |
-
### جدول وحدات القياس (measurement_units)
|
17 |
-
```sql
|
18 |
-
CREATE TABLE measurement_units (
|
19 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
20 |
-
name TEXT NOT NULL,
|
21 |
-
symbol TEXT NOT NULL,
|
22 |
-
description TEXT,
|
23 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
24 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
25 |
-
);
|
26 |
-
```
|
27 |
-
|
28 |
-
### جدول بنود التسعير الأساسية (pricing_items_base)
|
29 |
-
```sql
|
30 |
-
CREATE TABLE pricing_items_base (
|
31 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
32 |
-
code TEXT NOT NULL,
|
33 |
-
name TEXT NOT NULL,
|
34 |
-
description TEXT,
|
35 |
-
category_id INTEGER,
|
36 |
-
unit_id INTEGER,
|
37 |
-
base_price REAL NOT NULL,
|
38 |
-
last_updated_date TEXT,
|
39 |
-
price_source TEXT,
|
40 |
-
notes TEXT,
|
41 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
42 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
43 |
-
FOREIGN KEY (category_id) REFERENCES pricing_categories (id),
|
44 |
-
FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
|
45 |
-
);
|
46 |
-
```
|
47 |
-
|
48 |
-
### جدول تاريخ أسعار البنود (pricing_items_history)
|
49 |
-
```sql
|
50 |
-
CREATE TABLE pricing_items_history (
|
51 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
52 |
-
base_item_id INTEGER,
|
53 |
-
price REAL NOT NULL,
|
54 |
-
price_date TEXT NOT NULL,
|
55 |
-
price_source TEXT,
|
56 |
-
notes TEXT,
|
57 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
58 |
-
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id)
|
59 |
-
);
|
60 |
-
```
|
61 |
-
|
62 |
-
### جدول بنود التسعير للمشاريع (project_pricing_items)
|
63 |
-
```sql
|
64 |
-
CREATE TABLE project_pricing_items (
|
65 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
66 |
-
project_id INTEGER,
|
67 |
-
base_item_id INTEGER,
|
68 |
-
item_number TEXT NOT NULL,
|
69 |
-
description TEXT NOT NULL,
|
70 |
-
unit_id INTEGER,
|
71 |
-
quantity REAL NOT NULL,
|
72 |
-
unit_price REAL NOT NULL,
|
73 |
-
total_price REAL NOT NULL,
|
74 |
-
direct_cost REAL,
|
75 |
-
indirect_cost REAL,
|
76 |
-
profit_margin REAL,
|
77 |
-
risk_factor REAL,
|
78 |
-
notes TEXT,
|
79 |
-
created_by INTEGER,
|
80 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
81 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
82 |
-
FOREIGN KEY (project_id) REFERENCES projects (id),
|
83 |
-
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id),
|
84 |
-
FOREIGN KEY (unit_id) REFERENCES measurement_units (id),
|
85 |
-
FOREIGN KEY (created_by) REFERENCES users (id)
|
86 |
-
);
|
87 |
-
```
|
88 |
-
|
89 |
-
### جدول مكونات بنود التسعير (pricing_item_components)
|
90 |
-
```sql
|
91 |
-
CREATE TABLE pricing_item_components (
|
92 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
93 |
-
pricing_item_id INTEGER,
|
94 |
-
component_type TEXT NOT NULL, -- 'material', 'labor', 'equipment', 'subcontractor', 'other'
|
95 |
-
component_name TEXT NOT NULL,
|
96 |
-
unit_id INTEGER,
|
97 |
-
quantity REAL NOT NULL,
|
98 |
-
unit_price REAL NOT NULL,
|
99 |
-
total_price REAL NOT NULL,
|
100 |
-
notes TEXT,
|
101 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
102 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
103 |
-
FOREIGN KEY (pricing_item_id) REFERENCES project_pricing_items (id),
|
104 |
-
FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
|
105 |
-
);
|
106 |
-
```
|
107 |
-
|
108 |
-
### جدول عوامل التعديل (adjustment_factors)
|
109 |
-
```sql
|
110 |
-
CREATE TABLE adjustment_factors (
|
111 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
112 |
-
name TEXT NOT NULL,
|
113 |
-
description TEXT,
|
114 |
-
factor_type TEXT NOT NULL, -- 'inflation', 'location', 'risk', 'market', 'other'
|
115 |
-
value REAL NOT NULL,
|
116 |
-
start_date TEXT,
|
117 |
-
end_date TEXT,
|
118 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
119 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
120 |
-
);
|
121 |
-
```
|
122 |
-
|
123 |
-
### جدول نماذج التسعير (pricing_templates)
|
124 |
-
```sql
|
125 |
-
CREATE TABLE pricing_templates (
|
126 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
127 |
-
name TEXT NOT NULL,
|
128 |
-
description TEXT,
|
129 |
-
created_by INTEGER,
|
130 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
131 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
132 |
-
FOREIGN KEY (created_by) REFERENCES users (id)
|
133 |
-
);
|
134 |
-
```
|
135 |
-
|
136 |
-
### جدول بنود نماذج التسعير (pricing_template_items)
|
137 |
-
```sql
|
138 |
-
CREATE TABLE pricing_template_items (
|
139 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
140 |
-
template_id INTEGER,
|
141 |
-
base_item_id INTEGER,
|
142 |
-
item_number TEXT NOT NULL,
|
143 |
-
description TEXT NOT NULL,
|
144 |
-
unit_id INTEGER,
|
145 |
-
unit_price REAL NOT NULL,
|
146 |
-
notes TEXT,
|
147 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
148 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
149 |
-
FOREIGN KEY (template_id) REFERENCES pricing_templates (id),
|
150 |
-
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id),
|
151 |
-
FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
|
152 |
-
);
|
153 |
-
```
|
154 |
-
|
155 |
-
### جدول تنبؤات الأسعار (price_forecasts)
|
156 |
-
```sql
|
157 |
-
CREATE TABLE price_forecasts (
|
158 |
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
159 |
-
base_item_id INTEGER,
|
160 |
-
forecast_date TEXT NOT NULL,
|
161 |
-
forecast_price REAL NOT NULL,
|
162 |
-
forecast_model TEXT,
|
163 |
-
confidence_level REAL,
|
164 |
-
scenario TEXT, -- 'optimistic', 'baseline', 'pessimistic'
|
165 |
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
166 |
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
167 |
-
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id)
|
168 |
-
);
|
169 |
-
```
|
170 |
-
|
171 |
-
## هيكل الكلاسات
|
172 |
-
|
173 |
-
### 1. مدير التسعير (PricingManager)
|
174 |
-
```python
|
175 |
-
class PricingManager:
|
176 |
-
"""فئة مدير التسعير الرئيسية"""
|
177 |
-
|
178 |
-
def __init__(self, db_connector, config):
|
179 |
-
"""تهيئة مدير التسعير"""
|
180 |
-
self.db = db_connector
|
181 |
-
self.config = config
|
182 |
-
self.item_manager = PricingItemManager(db_connector)
|
183 |
-
self.cost_calculator = CostCalculator(db_connector)
|
184 |
-
self.price_analyzer = PriceAnalyzer(db_connector)
|
185 |
-
self.price_forecaster = PriceForecaster(db_connector)
|
186 |
-
self.report_generator = PricingReportGenerator(db_connector)
|
187 |
-
|
188 |
-
def initialize_database(self):
|
189 |
-
"""تهيئة قاعدة البيانات للتسعير"""
|
190 |
-
# إنشاء الجداول إذا لم تكن موجودة
|
191 |
-
pass
|
192 |
-
|
193 |
-
def load_default_data(self):
|
194 |
-
"""تحميل البيانات الافتراضية"""
|
195 |
-
# تحميل الفئات ووحدات القياس الافتراضية
|
196 |
-
pass
|
197 |
-
|
198 |
-
def get_project_pricing_summary(self, project_id):
|
199 |
-
"""الحصول على ملخص التسعير للمشروع"""
|
200 |
-
pass
|
201 |
-
|
202 |
-
def import_pricing_data(self, file_path, import_type):
|
203 |
-
"""استيراد بيانات التسعير من ملف خارجي"""
|
204 |
-
pass
|
205 |
-
|
206 |
-
def export_pricing_data(self, project_id, export_type, file_path):
|
207 |
-
"""تصدير بيانات التسعير إلى ملف خارجي"""
|
208 |
-
pass
|
209 |
-
```
|
210 |
-
|
211 |
-
### 2. مدير بنود التسعير (PricingItemManager)
|
212 |
-
```python
|
213 |
-
class PricingItemManager:
|
214 |
-
"""فئة إدارة بنود التسعير"""
|
215 |
-
|
216 |
-
def __init__(self, db_connector):
|
217 |
-
"""تهيئة مدير بنود التسعير"""
|
218 |
-
self.db = db_connector
|
219 |
-
|
220 |
-
def get_all_base_items(self, filters=None):
|
221 |
-
"""الحصول على جميع البنود الأساسية"""
|
222 |
-
pass
|
223 |
-
|
224 |
-
def get_base_item_by_id(self, item_id):
|
225 |
-
"""الحصول على بند أساسي بواسطة المعرف"""
|
226 |
-
pass
|
227 |
-
|
228 |
-
def add_base_item(self, item_data):
|
229 |
-
"""إضافة بند أساسي جديد"""
|
230 |
-
pass
|
231 |
-
|
232 |
-
def update_base_item(self, item_id, item_data):
|
233 |
-
"""تحديث بند أساسي"""
|
234 |
-
pass
|
235 |
-
|
236 |
-
def delete_base_item(self, item_id):
|
237 |
-
"""حذف بند أساسي"""
|
238 |
-
pass
|
239 |
-
|
240 |
-
def get_project_items(self, project_id):
|
241 |
-
"""الحصول على بنود المشروع"""
|
242 |
-
pass
|
243 |
-
|
244 |
-
def add_project_item(self, project_id, item_data):
|
245 |
-
"""إضافة بند للمشروع"""
|
246 |
-
pass
|
247 |
-
|
248 |
-
def update_project_item(self, item_id, item_data):
|
249 |
-
"""تحديث بند المشروع"""
|
250 |
-
pass
|
251 |
-
|
252 |
-
def delete_project_item(self, item_id):
|
253 |
-
"""حذف بند المشروع"""
|
254 |
-
pass
|
255 |
-
|
256 |
-
def get_item_components(self, item_id):
|
257 |
-
"""الحصول على مكونات البند"""
|
258 |
-
pass
|
259 |
-
|
260 |
-
def add_item_component(self, item_id, component_data):
|
261 |
-
"""إضافة مكون للبند"""
|
262 |
-
pass
|
263 |
-
|
264 |
-
def update_item_component(self, component_id, component_data):
|
265 |
-
"""تحديث مكون البند"""
|
266 |
-
pass
|
267 |
-
|
268 |
-
def delete_item_component(self, component_id):
|
269 |
-
"""حذف مكون البند"""
|
270 |
-
pass
|
271 |
-
|
272 |
-
def get_categories(self):
|
273 |
-
"""الحصول على فئات البنود"""
|
274 |
-
pass
|
275 |
-
|
276 |
-
def get_measurement_units(self):
|
277 |
-
"""الحصول على وحدات القياس"""
|
278 |
-
pass
|
279 |
-
|
280 |
-
def get_templates(self):
|
281 |
-
"""الحصول على نماذج التسعير"""
|
282 |
-
pass
|
283 |
-
|
284 |
-
def get_template_items(self, template_id):
|
285 |
-
"""الحصول على بنود النموذج"""
|
286 |
-
pass
|
287 |
-
|
288 |
-
def apply_template_to_project(self, project_id, template_id):
|
289 |
-
"""تطبيق نموذج على مشروع"""
|
290 |
-
pass
|
291 |
-
```
|
292 |
-
|
293 |
-
### 3. حاسبة التكاليف (CostCalculator)
|
294 |
-
```python
|
295 |
-
class CostCalculator:
|
296 |
-
"""فئة حساب التكاليف"""
|
297 |
-
|
298 |
-
def __init__(self, db_connector):
|
299 |
-
"""تهيئة حاسبة التكاليف"""
|
300 |
-
self.db = db_connector
|
301 |
-
|
302 |
-
def calculate_direct_costs(self, project_id):
|
303 |
-
"""حساب التكاليف المباشرة"""
|
304 |
-
pass
|
305 |
-
|
306 |
-
def calculate_indirect_costs(self, project_id, indirect_cost_percentage):
|
307 |
-
"""حساب التكاليف غير المباشرة"""
|
308 |
-
pass
|
309 |
-
|
310 |
-
def calculate_profit_margin(self, project_id, profit_percentage):
|
311 |
-
"""حساب هامش الربح"""
|
312 |
-
pass
|
313 |
-
|
314 |
-
def calculate_risk_contingency(self, project_id, risk_factors):
|
315 |
-
"""حساب احتياطي المخاطر"""
|
316 |
-
pass
|
317 |
-
|
318 |
-
def calculate_taxes_and_fees(self, project_id, tax_rates):
|
319 |
-
"""حساب الضرائب والرسوم"""
|
320 |
-
pass
|
321 |
-
|
322 |
-
def calculate_total_cost(self, project_id):
|
323 |
-
"""حساب التكلفة الإجمالية"""
|
324 |
-
pass
|
325 |
-
|
326 |
-
def calculate_unit_rates(self, project_id):
|
327 |
-
"""حساب معدلات الوحدات"""
|
328 |
-
pass
|
329 |
-
|
330 |
-
def apply_adjustment_factors(self, project_id, factors):
|
331 |
-
"""تطبيق عوامل التعديل"""
|
332 |
-
pass
|
333 |
-
|
334 |
-
def calculate_cost_breakdown(self, project_id):
|
335 |
-
"""حساب تفصيل التكاليف"""
|
336 |
-
pass
|
337 |
-
```
|
338 |
-
|
339 |
-
### 4. محلل الأسعار (PriceAnalyzer)
|
340 |
-
```python
|
341 |
-
class PriceAnalyzer:
|
342 |
-
"""فئة تحليل الأسعار"""
|
343 |
-
|
344 |
-
def __init__(self, db_connector):
|
345 |
-
"""تهيئة محلل الأسعار"""
|
346 |
-
self.db = db_connector
|
347 |
-
|
348 |
-
def get_price_history(self, item_id):
|
349 |
-
"""الحصول على تاريخ الأسعار"""
|
350 |
-
pass
|
351 |
-
|
352 |
-
def analyze_price_trends(self, item_id, start_date, end_date):
|
353 |
-
"""تحليل اتجاهات الأسعار"""
|
354 |
-
pass
|
355 |
-
|
356 |
-
def compare_prices(self, items, date=None):
|
357 |
-
"""مقارنة الأسعار"""
|
358 |
-
pass
|
359 |
-
|
360 |
-
def calculate_price_volatility(self, item_id, period):
|
361 |
-
"""حساب تقلب الأسعار"""
|
362 |
-
pass
|
363 |
-
|
364 |
-
def perform_sensitivity_analysis(self, project_id, variable_items, ranges):
|
365 |
-
"""إجراء تحليل الحساسية"""
|
366 |
-
pass
|
367 |
-
|
368 |
-
def analyze_price_correlations(self, items):
|
369 |
-
"""تحليل ارتباطات الأسعار"""
|
370 |
-
pass
|
371 |
-
|
372 |
-
def compare_with_market_prices(self, items):
|
373 |
-
"""مقارنة مع أسعار السوق"""
|
374 |
-
pass
|
375 |
-
|
376 |
-
def analyze_cost_drivers(self, project_id):
|
377 |
-
"""تحليل محركات التكلفة"""
|
378 |
-
pass
|
379 |
-
|
380 |
-
def generate_price_analysis_charts(self, analysis_type, params):
|
381 |
-
"""إنشاء رسوم بيانية لتحليل الأسعار"""
|
382 |
-
pass
|
383 |
-
```
|
384 |
-
|
385 |
-
### 5. متنبئ الأسعار (PriceForecaster)
|
386 |
-
```python
|
387 |
-
class PriceForecaster:
|
388 |
-
"""فئة التنبؤ بالأسعار"""
|
389 |
-
|
390 |
-
def __init__(self, db_connector):
|
391 |
-
"""
|
392 |
-
self.db = db_connector
|
393 |
-
|
394 |
-
def forecast_price(self, item_id, forecast_date, model_type='arima'):
|
395 |
-
"""التنبؤ بالسعر"""
|
396 |
-
pass
|
397 |
-
|
398 |
-
def generate_price_scenarios(self, item_id, forecast_date):
|
399 |
-
"""إنشاء سيناريوهات الأسعار"""
|
400 |
-
pass
|
401 |
-
|
402 |
-
def calculate_inflation_impact(self, project_id, inflation_rate, duration):
|
403 |
-
"""حساب تأثير التضخم"""
|
404 |
-
pass
|
405 |
-
|
406 |
-
def forecast_project_costs(self, project_id, forecast_date):
|
407 |
-
"""التنبؤ بتكاليف المشروع"""
|
408 |
-
pass
|
409 |
-
|
410 |
-
def evaluate_forecast_accuracy(self, item_id):
|
411 |
-
"""تقييم دقة التنبؤ"""
|
412 |
-
pass
|
413 |
-
|
414 |
-
def generate_forecast_charts(self, item_id, forecast_date):
|
415 |
-
"""إنشاء رسوم بيانية للتنبؤ"""
|
416 |
-
pass
|
417 |
-
```
|
418 |
-
|
419 |
-
### 6. مولد تقارير التسعير (PricingReportGenerator)
|
420 |
-
```python
|
421 |
-
class PricingReportGenerator:
|
422 |
-
"""فئة إنشاء تقارير التسعير"""
|
423 |
-
|
424 |
-
def __init__(self, db_connector):
|
425 |
-
"""تهيئة مولد تقارير التسعير"""
|
426 |
-
self.db = db_connector
|
427 |
-
|
428 |
-
def generate_cost_summary_report(self, project_id):
|
429 |
-
"""إنشاء تقرير ملخص التكاليف"""
|
430 |
-
pass
|
431 |
-
|
432 |
-
def generate_detailed_items_report(self, project_id):
|
433 |
-
"""إنشاء تقرير تفصيلي للبنود"""
|
434 |
-
pass
|
435 |
-
|
436 |
-
def generate_price_comparison_report(self, items, parameters):
|
437 |
-
"""إنشاء تقرير مقارنة الأسعار"""
|
438 |
-
pass
|
439 |
-
|
440 |
-
def generate_sensitivity_analysis_report(self, project_id, parameters):
|
441 |
-
"""إنشاء تقرير تحليل الحساسية"""
|
442 |
-
pass
|
443 |
-
|
444 |
-
def generate_price_forecast_report(self, items, forecast_date):
|
445 |
-
"""إنشاء تقرير التنبؤ بالأسعار"""
|
446 |
-
pass
|
447 |
-
|
448 |
-
def generate_price_risk_report(self, project_id):
|
449 |
-
"""إنشاء تقرير مخاطر الأسعار"""
|
450 |
-
pass
|
451 |
-
|
452 |
-
def export_report_to_pdf(self, report_data, file_path):
|
453 |
-
"""تصدير التقرير إلى PDF"""
|
454 |
-
pass
|
455 |
-
|
456 |
-
def export_report_to_excel(self, report_data, file_path):
|
457 |
-
"""تصدير التقرير إلى Excel"""
|
458 |
-
pass
|
459 |
-
```
|
460 |
-
|
461 |
-
## تصميم واجهة المستخدم
|
462 |
-
|
463 |
-
### 1. الشاشة الرئيسية لوحدة التسعير
|
464 |
-
|
465 |
-
```
|
466 |
-
+--------------------------------------------------+
|
467 |
-
| وحدة التسعير |
|
468 |
-
+--------------------------------------------------+
|
469 |
-
| |
|
470 |
-
| +----------------+ +----------------------+ |
|
471 |
-
| | المناقصات | | إحصائيات التسعير | |
|
472 |
-
| | الحالية | | | |
|
473 |
-
| | | | | |
|
474 |
-
| | | | | |
|
475 |
-
| | | | | |
|
476 |
-
| +----------------+ +----------------------+ |
|
477 |
-
| |
|
478 |
-
| +----------------+ +----------------------+ |
|
479 |
-
| | الوصول | | آخر التحديثات | |
|
480 |
-
| | السريع | | | |
|
481 |
-
| | | | | |
|
482 |
-
| | | | | |
|
483 |
-
| | | | | |
|
484 |
-
| +----------------+ +----------------------+ |
|
485 |
-
| |
|
486 |
-
+--------------------------------------------------+
|
487 |
-
```
|
488 |
-
|
489 |
-
### 2. شاشة إدارة بنود التسعير
|
490 |
-
|
491 |
-
```
|
492 |
-
+--------------------------------------------------+
|
493 |
-
| إدارة بنود التسعير |
|
494 |
-
+--------------------------------------------------+
|
495 |
-
| بحث: [ ] [تصفية▼] [تصدير] |
|
496 |
-
+--------------------------------------------------+
|
497 |
-
| # | الكود | الوصف | الوحدة | الكمية | السعر | المجموع |
|
498 |
-
+--------------------------------------------------+
|
499 |
-
| 1 | | | | | | |
|
500 |
-
| 2 | | | | | | |
|
501 |
-
| 3 | | | | | | |
|
502 |
-
| 4 | | | | | | |
|
503 |
-
| 5 | | | | | | |
|
504 |
-
+--------------------------------------------------+
|
505 |
-
| [إضافة بند] [حذف المحدد] [استيراد من Excel] |
|
506 |
-
+--------------------------------------------------+
|
507 |
-
| المجموع الكلي: |
|
508 |
-
+--------------------------------------------------+
|
509 |
-
```
|
510 |
-
|
511 |
-
### 3. شاشة تفاصيل البند
|
512 |
-
|
513 |
-
```
|
514 |
-
+--------------------------------------------------+
|
515 |
-
| تفاصيل البند |
|
516 |
-
+--------------------------------------------------+
|
517 |
-
| الكود: [ ] الوصف: [ ] |
|
518 |
-
| الفئة: [ ▼] الوحدة: [ ▼] |
|
519 |
-
+--------------------------------------------------+
|
520 |
-
| مكونات البند: |
|
521 |
-
+--------------------------------------------------+
|
522 |
-
| النوع | الوصف | الوحدة | الكمية | السعر | المجموع |
|
523 |
-
+--------------------------------------------------+
|
524 |
-
| مواد | | | | | |
|
525 |
-
| عمالة | | | | | |
|
526 |
-
| معدات | | | | | |
|
527 |
-
| أخرى | | | | | |
|
528 |
-
+--------------------------------------------------+
|
529 |
-
| [إضافة مكون] [حذف المحدد] |
|
530 |
-
+--------------------------------------------------+
|
531 |
-
| التكلفة المباشرة: |
|
532 |
-
| التكلفة غير المباشرة: |
|
533 |
-
| هامش الربح: |
|
534 |
-
| احتياطي المخاطر: |
|
535 |
-
| السعر النهائي: |
|
536 |
-
+--------------------------------------------------+
|
537 |
-
| [حفظ] [إلغاء] |
|
538 |
-
+--------------------------------------------------+
|
539 |
-
```
|
540 |
-
|
541 |
-
### 4. شاشة تحليل الأسعار
|
542 |
-
|
543 |
-
```
|
544 |
-
+--------------------------------------------------+
|
545 |
-
| تحليل الأسعار |
|
546 |
-
+--------------------------------------------------+
|
547 |
-
| [اختيار البند▼] [الفترة الزمنية▼] [تحليل] |
|
548 |
-
+--------------------------------------------------+
|
549 |
-
| |
|
550 |
-
| |
|
551 |
-
| |
|
552 |
-
| (رسم بياني للأسعار) |
|
553 |
-
| |
|
554 |
-
| |
|
555 |
-
| |
|
556 |
-
+--------------------------------------------------+
|
557 |
-
| إحصائيات: |
|
558 |
-
| - متوسط السعر: |
|
559 |
-
| - أعلى سعر: |
|
560 |
-
| - أدنى سعر: |
|
561 |
-
| - معدل التغير: |
|
562 |
-
| - التقلب: |
|
563 |
-
+--------------------------------------------------+
|
564 |
-
| [مقارنة مع بنود أخرى] [تصدير التحليل] |
|
565 |
-
+--------------------------------------------------+
|
566 |
-
```
|
567 |
-
|
568 |
-
### 5.
|
569 |
-
|
570 |
-
```
|
571 |
-
+--------------------------------------------------+
|
572 |
-
| التنبؤ بالأسعار |
|
573 |
-
+--------------------------------------------------+
|
574 |
-
| [اختيار البند▼] [تاريخ التنبؤ] [نموذج التنبؤ▼] |
|
575 |
-
+--------------------------------------------------+
|
576 |
-
| |
|
577 |
-
| |
|
578 |
-
| |
|
579 |
-
| (رسم بياني للتنبؤ بالأسعار) |
|
580 |
-
| |
|
581 |
-
| |
|
582 |
-
| |
|
583 |
-
+--------------------------------------------------+
|
584 |
-
| السيناريوهات: |
|
585 |
-
| - متفائل: |
|
586 |
-
| - متوسط: |
|
587 |
-
| - متشائم: |
|
588 |
-
+--------------------------------------------------+
|
589 |
-
| عوامل التأثير: |
|
590 |
-
| - التضخم: |
|
591 |
-
| - تغيرات السوق: |
|
592 |
-
| - العوامل الموسمية: |
|
593 |
-
+--------------------------------------------------+
|
594 |
-
| [تطبيق على المشروع] [تصدير التنبؤ] |
|
595 |
-
+--------------------------------------------------+
|
596 |
-
```
|
597 |
-
|
598 |
-
### 6. شاشة تحليل الحساسية
|
599 |
-
|
600 |
-
```
|
601 |
-
+--------------------------------------------------+
|
602 |
-
| تحليل الحساسية |
|
603 |
-
+--------------------------------------------------+
|
604 |
-
| المشروع: [ ▼] |
|
605 |
-
+--------------------------------------------------+
|
606 |
-
| المتغيرات: |
|
607 |
-
| [✓] أسعار المواد الخام (±20%) |
|
608 |
-
| [✓] تكلفة العمالة (±15%) |
|
609 |
-
| [✓] تكلفة المعدات (±10%) |
|
610 |
-
| [ ] المصاريف
|
611 |
-
+--------------------------------------------------+
|
612 |
-
| |
|
613 |
-
| |
|
614 |
-
| (رسم بياني لتحليل الحساسية) |
|
615 |
-
| |
|
616 |
-
| |
|
617 |
-
| |
|
618 |
-
+--------------------------------------------------+
|
619 |
-
| النتائج: |
|
620 |
-
| - أكثر العوامل تأثيراً: |
|
621 |
-
| - نطاق التغير المتوقع: |
|
622 |
-
| - توصيات: |
|
623 |
-
+--------------------------------------------------+
|
624 |
-
| [تحديث التحليل] [تصدير النتائج] |
|
625 |
-
+--------------------------------------------------+
|
626 |
-
```
|
627 |
-
|
628 |
-
### 7. شاشة التقارير
|
629 |
-
|
630 |
-
```
|
631 |
-
+--------------------------------------------------+
|
632 |
-
| التقارير |
|
633 |
-
+--------------------------------------------------+
|
634 |
-
| [نوع التقرير▼] [المشروع▼] [إنشاء تقرير] |
|
635 |
-
+--------------------------------------------------+
|
636 |
-
| التقارير المتاحة: |
|
637 |
-
| |
|
638 |
-
| ○ ملخص التكاليف |
|
639 |
-
| ○ تفصيل البنود |
|
640 |
-
| ○ مقارنة الأسعار |
|
641 |
-
| ○ تحليل الحساسية |
|
642 |
-
| ○ التنبؤ بالأسعار |
|
643 |
-
| ○ مخاطر الأسعار |
|
644 |
-
| |
|
645 |
-
+--------------------------------------------------+
|
646 |
-
| خيارات التقرير: |
|
647 |
-
| |
|
648 |
-
| [✓] تضمين الرسوم البيانية |
|
649 |
-
| [✓] تضمين التوصيات |
|
650 |
-
| [ ] تضمين البيانات التفصيلية |
|
651 |
-
| |
|
652 |
-
+--------------------------------------------------+
|
653 |
-
| [PDF] [Excel] [طباعة] |
|
654 |
-
+--------------------------------------------------+
|
655 |
-
```
|
656 |
-
|
657 |
-
## تكامل النظام
|
658 |
-
|
659 |
-
### 1. تكامل مع وحدة تحليل المستندات
|
660 |
-
- استخراج بنود التسعير من وثائق المناقصة
|
661 |
-
- تحديد الكميات والمواصفات من المستندات
|
662 |
-
- مقارنة البنود المستخرجة مع قاعدة البيانات
|
663 |
-
|
664 |
-
### 2. تكامل مع وحدة تحليل المخاطر
|
665 |
-
- تحديد المخاطر المرتبطة بالتسعير
|
666 |
-
- تقييم تأثير المخاطر على التكاليف
|
667 |
-
- تحديد احتياطي المخاطر المناسب
|
668 |
-
|
669 |
-
### 3. تكامل مع وحدة إدارة المشاريع
|
670 |
-
- متابعة التكاليف الفعلية مقابل المخططة
|
671 |
-
- تحديث التنبؤات بناءً على بيانات المشروع الفعلية
|
672 |
-
- تحليل انحرافات التكاليف
|
673 |
-
|
674 |
-
### 4. تكامل مع وحدة التقارير
|
675 |
-
- إنشاء تقارير متكاملة تشمل بيانات التسعير
|
676 |
-
- دمج تحليلات التسعير في تقارير المشروع
|
677 |
-
- توفير لوحات معلومات متكاملة
|
678 |
-
|
679 |
-
## خطة التنفيذ التفصيلية
|
680 |
-
|
681 |
-
### المرحلة 1: إعداد البنية التحتية (3 أيام)
|
682 |
-
- تصميم وإنشاء جداول قاعدة البيانات
|
683 |
-
- إعداد هيكل الملفات والمجلدات
|
684 |
-
- تهيئة البيئة التطويرية
|
685 |
-
|
686 |
-
### المرحلة 2: تنفيذ الوظائف الأساسية (5 أيام)
|
687 |
-
- تنفيذ فئة مدير التسعير
|
688 |
-
- تنفيذ فئة مدير بنود التسعير
|
689 |
-
- تنفيذ فئة حاسبة التكاليف
|
690 |
-
- إنشاء واجهات المستخدم الأساسية
|
691 |
-
|
692 |
-
### المرحلة 3: تنفيذ وظائف التحليل (7 أيام)
|
693 |
-
- تنفيذ فئة محلل الأسعار
|
694 |
-
- تنفيذ فئة متنبئ الأسعار
|
695 |
-
- إنشاء الرسوم البيانية والتحليلات
|
696 |
-
- تنفيذ واجهات المستخدم للتحليل
|
697 |
-
|
698 |
-
### المرحلة 4: تنفيذ التقارير والتكامل (5 أيام)
|
699 |
-
- تنفيذ فئة مولد تقارير التسعير
|
700 |
-
- تكامل مع الوحدات الأخرى
|
701 |
-
- إنشاء واجهات المستخدم للتقارير
|
702 |
-
- اختبار التكامل
|
703 |
-
|
704 |
-
### المرحلة 5: الاختبار والتحسين (3 أيام)
|
705 |
-
- اختبار جميع الوظائف
|
706 |
-
- تحسين الأداء
|
707 |
-
- إصلاح الأخطاء
|
708 |
-
- تحسين واجهة المستخدم
|
709 |
-
|
710 |
-
### المرحلة 6: التوثيق والتسليم (2 أيام)
|
711 |
-
- إعداد وثائق المستخدم
|
712 |
-
- إعداد وثائق المطور
|
713 |
-
- تجهيز النسخة النهائية
|
714 |
-
- تسليم النظام
|
|
|
1 |
+
# تصميم وحدة التسعير المتكاملة وتحليل الأسعار
|
2 |
+
|
3 |
+
## هيكل قاعدة البيانات
|
4 |
+
|
5 |
+
### جدول فئات البنود (pricing_categories)
|
6 |
+
```sql
|
7 |
+
CREATE TABLE pricing_categories (
|
8 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
9 |
+
name TEXT NOT NULL,
|
10 |
+
description TEXT,
|
11 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
12 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
13 |
+
);
|
14 |
+
```
|
15 |
+
|
16 |
+
### جدول وحدات القياس (measurement_units)
|
17 |
+
```sql
|
18 |
+
CREATE TABLE measurement_units (
|
19 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
20 |
+
name TEXT NOT NULL,
|
21 |
+
symbol TEXT NOT NULL,
|
22 |
+
description TEXT,
|
23 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
24 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
25 |
+
);
|
26 |
+
```
|
27 |
+
|
28 |
+
### جدول بنود التسعير الأساسية (pricing_items_base)
|
29 |
+
```sql
|
30 |
+
CREATE TABLE pricing_items_base (
|
31 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
32 |
+
code TEXT NOT NULL,
|
33 |
+
name TEXT NOT NULL,
|
34 |
+
description TEXT,
|
35 |
+
category_id INTEGER,
|
36 |
+
unit_id INTEGER,
|
37 |
+
base_price REAL NOT NULL,
|
38 |
+
last_updated_date TEXT,
|
39 |
+
price_source TEXT,
|
40 |
+
notes TEXT,
|
41 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
42 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
43 |
+
FOREIGN KEY (category_id) REFERENCES pricing_categories (id),
|
44 |
+
FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
|
45 |
+
);
|
46 |
+
```
|
47 |
+
|
48 |
+
### جدول تاريخ أسعار البنود (pricing_items_history)
|
49 |
+
```sql
|
50 |
+
CREATE TABLE pricing_items_history (
|
51 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
52 |
+
base_item_id INTEGER,
|
53 |
+
price REAL NOT NULL,
|
54 |
+
price_date TEXT NOT NULL,
|
55 |
+
price_source TEXT,
|
56 |
+
notes TEXT,
|
57 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
58 |
+
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id)
|
59 |
+
);
|
60 |
+
```
|
61 |
+
|
62 |
+
### جدول بنود التسعير للمشاريع (project_pricing_items)
|
63 |
+
```sql
|
64 |
+
CREATE TABLE project_pricing_items (
|
65 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
66 |
+
project_id INTEGER,
|
67 |
+
base_item_id INTEGER,
|
68 |
+
item_number TEXT NOT NULL,
|
69 |
+
description TEXT NOT NULL,
|
70 |
+
unit_id INTEGER,
|
71 |
+
quantity REAL NOT NULL,
|
72 |
+
unit_price REAL NOT NULL,
|
73 |
+
total_price REAL NOT NULL,
|
74 |
+
direct_cost REAL,
|
75 |
+
indirect_cost REAL,
|
76 |
+
profit_margin REAL,
|
77 |
+
risk_factor REAL,
|
78 |
+
notes TEXT,
|
79 |
+
created_by INTEGER,
|
80 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
81 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
82 |
+
FOREIGN KEY (project_id) REFERENCES projects (id),
|
83 |
+
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id),
|
84 |
+
FOREIGN KEY (unit_id) REFERENCES measurement_units (id),
|
85 |
+
FOREIGN KEY (created_by) REFERENCES users (id)
|
86 |
+
);
|
87 |
+
```
|
88 |
+
|
89 |
+
### جدول مكونات بنود التسعير (pricing_item_components)
|
90 |
+
```sql
|
91 |
+
CREATE TABLE pricing_item_components (
|
92 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
93 |
+
pricing_item_id INTEGER,
|
94 |
+
component_type TEXT NOT NULL, -- 'material', 'labor', 'equipment', 'subcontractor', 'other'
|
95 |
+
component_name TEXT NOT NULL,
|
96 |
+
unit_id INTEGER,
|
97 |
+
quantity REAL NOT NULL,
|
98 |
+
unit_price REAL NOT NULL,
|
99 |
+
total_price REAL NOT NULL,
|
100 |
+
notes TEXT,
|
101 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
102 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
103 |
+
FOREIGN KEY (pricing_item_id) REFERENCES project_pricing_items (id),
|
104 |
+
FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
|
105 |
+
);
|
106 |
+
```
|
107 |
+
|
108 |
+
### جدول عوامل التعديل (adjustment_factors)
|
109 |
+
```sql
|
110 |
+
CREATE TABLE adjustment_factors (
|
111 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
112 |
+
name TEXT NOT NULL,
|
113 |
+
description TEXT,
|
114 |
+
factor_type TEXT NOT NULL, -- 'inflation', 'location', 'risk', 'market', 'other'
|
115 |
+
value REAL NOT NULL,
|
116 |
+
start_date TEXT,
|
117 |
+
end_date TEXT,
|
118 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
119 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
120 |
+
);
|
121 |
+
```
|
122 |
+
|
123 |
+
### جدول نماذج التسعير (pricing_templates)
|
124 |
+
```sql
|
125 |
+
CREATE TABLE pricing_templates (
|
126 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
127 |
+
name TEXT NOT NULL,
|
128 |
+
description TEXT,
|
129 |
+
created_by INTEGER,
|
130 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
131 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
132 |
+
FOREIGN KEY (created_by) REFERENCES users (id)
|
133 |
+
);
|
134 |
+
```
|
135 |
+
|
136 |
+
### جدول بنود نماذج التسعير (pricing_template_items)
|
137 |
+
```sql
|
138 |
+
CREATE TABLE pricing_template_items (
|
139 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
140 |
+
template_id INTEGER,
|
141 |
+
base_item_id INTEGER,
|
142 |
+
item_number TEXT NOT NULL,
|
143 |
+
description TEXT NOT NULL,
|
144 |
+
unit_id INTEGER,
|
145 |
+
unit_price REAL NOT NULL,
|
146 |
+
notes TEXT,
|
147 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
148 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
149 |
+
FOREIGN KEY (template_id) REFERENCES pricing_templates (id),
|
150 |
+
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id),
|
151 |
+
FOREIGN KEY (unit_id) REFERENCES measurement_units (id)
|
152 |
+
);
|
153 |
+
```
|
154 |
+
|
155 |
+
### جدول تنبؤات الأسعار (price_forecasts)
|
156 |
+
```sql
|
157 |
+
CREATE TABLE price_forecasts (
|
158 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
159 |
+
base_item_id INTEGER,
|
160 |
+
forecast_date TEXT NOT NULL,
|
161 |
+
forecast_price REAL NOT NULL,
|
162 |
+
forecast_model TEXT,
|
163 |
+
confidence_level REAL,
|
164 |
+
scenario TEXT, -- 'optimistic', 'baseline', 'pessimistic'
|
165 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
166 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
167 |
+
FOREIGN KEY (base_item_id) REFERENCES pricing_items_base (id)
|
168 |
+
);
|
169 |
+
```
|
170 |
+
|
171 |
+
## هيكل الكلاسات
|
172 |
+
|
173 |
+
### 1. مدير التسعير (PricingManager)
|
174 |
+
```python
|
175 |
+
class PricingManager:
|
176 |
+
"""فئة مدير التسعير الرئيسية"""
|
177 |
+
|
178 |
+
def __init__(self, db_connector, config):
|
179 |
+
"""تهيئة مدير التسعير"""
|
180 |
+
self.db = db_connector
|
181 |
+
self.config = config
|
182 |
+
self.item_manager = PricingItemManager(db_connector)
|
183 |
+
self.cost_calculator = CostCalculator(db_connector)
|
184 |
+
self.price_analyzer = PriceAnalyzer(db_connector)
|
185 |
+
self.price_forecaster = PriceForecaster(db_connector)
|
186 |
+
self.report_generator = PricingReportGenerator(db_connector)
|
187 |
+
|
188 |
+
def initialize_database(self):
|
189 |
+
"""تهيئة قاعدة البيانات للتسعير"""
|
190 |
+
# إنشاء الجداول إذا لم تكن موجودة
|
191 |
+
pass
|
192 |
+
|
193 |
+
def load_default_data(self):
|
194 |
+
"""تحميل البيانات الافتراضية"""
|
195 |
+
# تحميل الفئات ووحدات القياس الافتراضية
|
196 |
+
pass
|
197 |
+
|
198 |
+
def get_project_pricing_summary(self, project_id):
|
199 |
+
"""الحصول على ملخص التسعير للمشروع"""
|
200 |
+
pass
|
201 |
+
|
202 |
+
def import_pricing_data(self, file_path, import_type):
|
203 |
+
"""استيراد بيانات التسعير من ملف خارجي"""
|
204 |
+
pass
|
205 |
+
|
206 |
+
def export_pricing_data(self, project_id, export_type, file_path):
|
207 |
+
"""تصدير بيانات التسعير إلى ملف خارجي"""
|
208 |
+
pass
|
209 |
+
```
|
210 |
+
|
211 |
+
### 2. مدير بنود التسعير (PricingItemManager)
|
212 |
+
```python
|
213 |
+
class PricingItemManager:
|
214 |
+
"""فئة إدارة بنود التسعير"""
|
215 |
+
|
216 |
+
def __init__(self, db_connector):
|
217 |
+
"""تهيئة مدير بنود التسعير"""
|
218 |
+
self.db = db_connector
|
219 |
+
|
220 |
+
def get_all_base_items(self, filters=None):
|
221 |
+
"""الحصول على جميع البنود الأساسية"""
|
222 |
+
pass
|
223 |
+
|
224 |
+
def get_base_item_by_id(self, item_id):
|
225 |
+
"""الحصول على بند أساسي بواسطة المعرف"""
|
226 |
+
pass
|
227 |
+
|
228 |
+
def add_base_item(self, item_data):
|
229 |
+
"""إضافة بند أساسي جديد"""
|
230 |
+
pass
|
231 |
+
|
232 |
+
def update_base_item(self, item_id, item_data):
|
233 |
+
"""تحديث بند أساسي"""
|
234 |
+
pass
|
235 |
+
|
236 |
+
def delete_base_item(self, item_id):
|
237 |
+
"""حذف بند أساسي"""
|
238 |
+
pass
|
239 |
+
|
240 |
+
def get_project_items(self, project_id):
|
241 |
+
"""الحصول على بنود المشروع"""
|
242 |
+
pass
|
243 |
+
|
244 |
+
def add_project_item(self, project_id, item_data):
|
245 |
+
"""إضافة بند للمشروع"""
|
246 |
+
pass
|
247 |
+
|
248 |
+
def update_project_item(self, item_id, item_data):
|
249 |
+
"""تحديث بند المشروع"""
|
250 |
+
pass
|
251 |
+
|
252 |
+
def delete_project_item(self, item_id):
|
253 |
+
"""حذف بند المشروع"""
|
254 |
+
pass
|
255 |
+
|
256 |
+
def get_item_components(self, item_id):
|
257 |
+
"""الحصول على مكونات البند"""
|
258 |
+
pass
|
259 |
+
|
260 |
+
def add_item_component(self, item_id, component_data):
|
261 |
+
"""إضافة مكون للبند"""
|
262 |
+
pass
|
263 |
+
|
264 |
+
def update_item_component(self, component_id, component_data):
|
265 |
+
"""تحديث مكون البند"""
|
266 |
+
pass
|
267 |
+
|
268 |
+
def delete_item_component(self, component_id):
|
269 |
+
"""حذف مكون البند"""
|
270 |
+
pass
|
271 |
+
|
272 |
+
def get_categories(self):
|
273 |
+
"""الحصول على فئات البنود"""
|
274 |
+
pass
|
275 |
+
|
276 |
+
def get_measurement_units(self):
|
277 |
+
"""الحصول على وحدات القياس"""
|
278 |
+
pass
|
279 |
+
|
280 |
+
def get_templates(self):
|
281 |
+
"""الحصول على نماذج التسعير"""
|
282 |
+
pass
|
283 |
+
|
284 |
+
def get_template_items(self, template_id):
|
285 |
+
"""الحصول على بنود النموذج"""
|
286 |
+
pass
|
287 |
+
|
288 |
+
def apply_template_to_project(self, project_id, template_id):
|
289 |
+
"""تطبيق نموذج على مشروع"""
|
290 |
+
pass
|
291 |
+
```
|
292 |
+
|
293 |
+
### 3. حاسبة التكاليف (CostCalculator)
|
294 |
+
```python
|
295 |
+
class CostCalculator:
|
296 |
+
"""فئة حساب التكاليف"""
|
297 |
+
|
298 |
+
def __init__(self, db_connector):
|
299 |
+
"""تهيئة حاسبة التكاليف"""
|
300 |
+
self.db = db_connector
|
301 |
+
|
302 |
+
def calculate_direct_costs(self, project_id):
|
303 |
+
"""حساب التكاليف المباشرة"""
|
304 |
+
pass
|
305 |
+
|
306 |
+
def calculate_indirect_costs(self, project_id, indirect_cost_percentage):
|
307 |
+
"""حساب التكاليف غير المباشرة"""
|
308 |
+
pass
|
309 |
+
|
310 |
+
def calculate_profit_margin(self, project_id, profit_percentage):
|
311 |
+
"""حساب هامش الربح"""
|
312 |
+
pass
|
313 |
+
|
314 |
+
def calculate_risk_contingency(self, project_id, risk_factors):
|
315 |
+
"""حساب احتياطي المخاطر"""
|
316 |
+
pass
|
317 |
+
|
318 |
+
def calculate_taxes_and_fees(self, project_id, tax_rates):
|
319 |
+
"""حساب الضرائب والرسوم"""
|
320 |
+
pass
|
321 |
+
|
322 |
+
def calculate_total_cost(self, project_id):
|
323 |
+
"""حساب التكلفة الإجمالية"""
|
324 |
+
pass
|
325 |
+
|
326 |
+
def calculate_unit_rates(self, project_id):
|
327 |
+
"""حساب معدلات الوحدات"""
|
328 |
+
pass
|
329 |
+
|
330 |
+
def apply_adjustment_factors(self, project_id, factors):
|
331 |
+
"""تطبيق عوامل التعديل"""
|
332 |
+
pass
|
333 |
+
|
334 |
+
def calculate_cost_breakdown(self, project_id):
|
335 |
+
"""حساب تفصيل التكاليف"""
|
336 |
+
pass
|
337 |
+
```
|
338 |
+
|
339 |
+
### 4. محلل الأسعار (PriceAnalyzer)
|
340 |
+
```python
|
341 |
+
class PriceAnalyzer:
|
342 |
+
"""فئة تحليل الأسعار"""
|
343 |
+
|
344 |
+
def __init__(self, db_connector):
|
345 |
+
"""تهيئة محلل الأسعار"""
|
346 |
+
self.db = db_connector
|
347 |
+
|
348 |
+
def get_price_history(self, item_id):
|
349 |
+
"""الحصول على تاريخ الأسعار"""
|
350 |
+
pass
|
351 |
+
|
352 |
+
def analyze_price_trends(self, item_id, start_date, end_date):
|
353 |
+
"""تحليل اتجاهات الأسعار"""
|
354 |
+
pass
|
355 |
+
|
356 |
+
def compare_prices(self, items, date=None):
|
357 |
+
"""مقارنة الأسعار"""
|
358 |
+
pass
|
359 |
+
|
360 |
+
def calculate_price_volatility(self, item_id, period):
|
361 |
+
"""حساب تقلب الأسعار"""
|
362 |
+
pass
|
363 |
+
|
364 |
+
def perform_sensitivity_analysis(self, project_id, variable_items, ranges):
|
365 |
+
"""إجراء تحليل الحساسية"""
|
366 |
+
pass
|
367 |
+
|
368 |
+
def analyze_price_correlations(self, items):
|
369 |
+
"""تحليل ارتباطات الأسعار"""
|
370 |
+
pass
|
371 |
+
|
372 |
+
def compare_with_market_prices(self, items):
|
373 |
+
"""مقارنة مع أسعار السوق"""
|
374 |
+
pass
|
375 |
+
|
376 |
+
def analyze_cost_drivers(self, project_id):
|
377 |
+
"""تحليل محركات التكلفة"""
|
378 |
+
pass
|
379 |
+
|
380 |
+
def generate_price_analysis_charts(self, analysis_type, params):
|
381 |
+
"""إنشاء رسوم بيانية لتحليل الأسعار"""
|
382 |
+
pass
|
383 |
+
```
|
384 |
+
|
385 |
+
### 5. متنبئ الأسعار (PriceForecaster)
|
386 |
+
```python
|
387 |
+
class PriceForecaster:
|
388 |
+
"""فئة التنبؤ بالأسعار"""
|
389 |
+
|
390 |
+
def __init__(self, db_connector):
|
391 |
+
"""تهيئ�� متنبئ الأسعار"""
|
392 |
+
self.db = db_connector
|
393 |
+
|
394 |
+
def forecast_price(self, item_id, forecast_date, model_type='arima'):
|
395 |
+
"""التنبؤ بالسعر"""
|
396 |
+
pass
|
397 |
+
|
398 |
+
def generate_price_scenarios(self, item_id, forecast_date):
|
399 |
+
"""إنشاء سيناريوهات الأسعار"""
|
400 |
+
pass
|
401 |
+
|
402 |
+
def calculate_inflation_impact(self, project_id, inflation_rate, duration):
|
403 |
+
"""حساب تأثير التضخم"""
|
404 |
+
pass
|
405 |
+
|
406 |
+
def forecast_project_costs(self, project_id, forecast_date):
|
407 |
+
"""التنبؤ بتكاليف المشروع"""
|
408 |
+
pass
|
409 |
+
|
410 |
+
def evaluate_forecast_accuracy(self, item_id):
|
411 |
+
"""تقييم دقة التنبؤ"""
|
412 |
+
pass
|
413 |
+
|
414 |
+
def generate_forecast_charts(self, item_id, forecast_date):
|
415 |
+
"""إنشاء رسوم بيانية للتنبؤ"""
|
416 |
+
pass
|
417 |
+
```
|
418 |
+
|
419 |
+
### 6. مولد تقارير التسعير (PricingReportGenerator)
|
420 |
+
```python
|
421 |
+
class PricingReportGenerator:
|
422 |
+
"""فئة إنشاء تقارير التسعير"""
|
423 |
+
|
424 |
+
def __init__(self, db_connector):
|
425 |
+
"""تهيئة مولد تقارير التسعير"""
|
426 |
+
self.db = db_connector
|
427 |
+
|
428 |
+
def generate_cost_summary_report(self, project_id):
|
429 |
+
"""إنشاء تقرير ملخص التكاليف"""
|
430 |
+
pass
|
431 |
+
|
432 |
+
def generate_detailed_items_report(self, project_id):
|
433 |
+
"""إنشاء تقرير تفصيلي للبنود"""
|
434 |
+
pass
|
435 |
+
|
436 |
+
def generate_price_comparison_report(self, items, parameters):
|
437 |
+
"""إنشاء تقرير مقارنة الأسعار"""
|
438 |
+
pass
|
439 |
+
|
440 |
+
def generate_sensitivity_analysis_report(self, project_id, parameters):
|
441 |
+
"""إنشاء تقرير تحليل الحساسية"""
|
442 |
+
pass
|
443 |
+
|
444 |
+
def generate_price_forecast_report(self, items, forecast_date):
|
445 |
+
"""إنشاء تقرير التنبؤ بالأسعار"""
|
446 |
+
pass
|
447 |
+
|
448 |
+
def generate_price_risk_report(self, project_id):
|
449 |
+
"""إنشاء تقرير مخاطر الأسعار"""
|
450 |
+
pass
|
451 |
+
|
452 |
+
def export_report_to_pdf(self, report_data, file_path):
|
453 |
+
"""تصدير التقرير إلى PDF"""
|
454 |
+
pass
|
455 |
+
|
456 |
+
def export_report_to_excel(self, report_data, file_path):
|
457 |
+
"""تصدير التقرير إلى Excel"""
|
458 |
+
pass
|
459 |
+
```
|
460 |
+
|
461 |
+
## تصميم واجهة المستخدم
|
462 |
+
|
463 |
+
### 1. الشاشة الرئيسية لوحدة التسعير
|
464 |
+
|
465 |
+
```
|
466 |
+
+--------------------------------------------------+
|
467 |
+
| وحدة التسعير |
|
468 |
+
+--------------------------------------------------+
|
469 |
+
| |
|
470 |
+
| +----------------+ +----------------------+ |
|
471 |
+
| | المناقصات | | إحصائيات التسعير | |
|
472 |
+
| | الحالية | | | |
|
473 |
+
| | | | | |
|
474 |
+
| | | | | |
|
475 |
+
| | | | | |
|
476 |
+
| +----------------+ +----------------------+ |
|
477 |
+
| |
|
478 |
+
| +----------------+ +----------------------+ |
|
479 |
+
| | الوصول | | آخر التحديثات | |
|
480 |
+
| | السريع | | | |
|
481 |
+
| | | | | |
|
482 |
+
| | | | | |
|
483 |
+
| | | | | |
|
484 |
+
| +----------------+ +----------------------+ |
|
485 |
+
| |
|
486 |
+
+--------------------------------------------------+
|
487 |
+
```
|
488 |
+
|
489 |
+
### 2. شاشة إدارة بنود التسعير
|
490 |
+
|
491 |
+
```
|
492 |
+
+--------------------------------------------------+
|
493 |
+
| إدارة بنود التسعير |
|
494 |
+
+--------------------------------------------------+
|
495 |
+
| بحث: [ ] [تصفية▼] [تصدير] |
|
496 |
+
+--------------------------------------------------+
|
497 |
+
| # | الكود | الوصف | الوحدة | الكمية | السعر | المجموع |
|
498 |
+
+--------------------------------------------------+
|
499 |
+
| 1 | | | | | | |
|
500 |
+
| 2 | | | | | | |
|
501 |
+
| 3 | | | | | | |
|
502 |
+
| 4 | | | | | | |
|
503 |
+
| 5 | | | | | | |
|
504 |
+
+--------------------------------------------------+
|
505 |
+
| [إضافة بند] [حذف المحدد] [استيراد من Excel] |
|
506 |
+
+--------------------------------------------------+
|
507 |
+
| المجموع الكلي: |
|
508 |
+
+--------------------------------------------------+
|
509 |
+
```
|
510 |
+
|
511 |
+
### 3. شاشة تفاصيل البند
|
512 |
+
|
513 |
+
```
|
514 |
+
+--------------------------------------------------+
|
515 |
+
| تفاصيل البند |
|
516 |
+
+--------------------------------------------------+
|
517 |
+
| الكود: [ ] الوصف: [ ] |
|
518 |
+
| الفئة: [ ▼] الوحدة: [ ▼] |
|
519 |
+
+--------------------------------------------------+
|
520 |
+
| مكونات البند: |
|
521 |
+
+--------------------------------------------------+
|
522 |
+
| النوع | الوصف | الوحدة | الكمية | السعر | المجموع |
|
523 |
+
+--------------------------------------------------+
|
524 |
+
| مواد | | | | | |
|
525 |
+
| عمالة | | | | | |
|
526 |
+
| معدات | | | | | |
|
527 |
+
| أخرى | | | | | |
|
528 |
+
+--------------------------------------------------+
|
529 |
+
| [إضافة مكون] [حذف المحدد] |
|
530 |
+
+--------------------------------------------------+
|
531 |
+
| التكلفة المباشرة: |
|
532 |
+
| التكلفة غير المباشرة: |
|
533 |
+
| هامش الربح: |
|
534 |
+
| احتياطي المخاطر: |
|
535 |
+
| السعر النهائي: |
|
536 |
+
+--------------------------------------------------+
|
537 |
+
| [حفظ] [إلغاء] |
|
538 |
+
+--------------------------------------------------+
|
539 |
+
```
|
540 |
+
|
541 |
+
### 4. شاشة تحليل الأسعار
|
542 |
+
|
543 |
+
```
|
544 |
+
+--------------------------------------------------+
|
545 |
+
| تحليل الأسعار |
|
546 |
+
+--------------------------------------------------+
|
547 |
+
| [اختيار البند▼] [الفترة الزمنية▼] [تحليل] |
|
548 |
+
+--------------------------------------------------+
|
549 |
+
| |
|
550 |
+
| |
|
551 |
+
| |
|
552 |
+
| (رسم بياني للأسعار) |
|
553 |
+
| |
|
554 |
+
| |
|
555 |
+
| |
|
556 |
+
+--------------------------------------------------+
|
557 |
+
| إحصائيات: |
|
558 |
+
| - متوسط السعر: |
|
559 |
+
| - أعلى سعر: |
|
560 |
+
| - أدنى سعر: |
|
561 |
+
| - معدل التغير: |
|
562 |
+
| - التقلب: |
|
563 |
+
+--------------------------------------------------+
|
564 |
+
| [مقارنة مع بنود أخرى] [تصدير التحليل] |
|
565 |
+
+--------------------------------------------------+
|
566 |
+
```
|
567 |
+
|
568 |
+
### 5. شاشة التنبؤ بالأسعار
|
569 |
+
|
570 |
+
```
|
571 |
+
+--------------------------------------------------+
|
572 |
+
| التنبؤ بالأسعار |
|
573 |
+
+--------------------------------------------------+
|
574 |
+
| [اختيار البند▼] [تاريخ التنبؤ] [نموذج التنبؤ▼] |
|
575 |
+
+--------------------------------------------------+
|
576 |
+
| |
|
577 |
+
| |
|
578 |
+
| |
|
579 |
+
| (رسم بياني للتنبؤ بالأسعار) |
|
580 |
+
| |
|
581 |
+
| |
|
582 |
+
| |
|
583 |
+
+--------------------------------------------------+
|
584 |
+
| السيناريوهات: |
|
585 |
+
| - متفائل: |
|
586 |
+
| - متوسط: |
|
587 |
+
| - متشائم: |
|
588 |
+
+--------------------------------------------------+
|
589 |
+
| عوامل التأثير: |
|
590 |
+
| - التضخم: |
|
591 |
+
| - تغيرات السوق: |
|
592 |
+
| - العوامل الموسمية: |
|
593 |
+
+--------------------------------------------------+
|
594 |
+
| [تطبيق على المشروع] [تصدير التنبؤ] |
|
595 |
+
+--------------------------------------------------+
|
596 |
+
```
|
597 |
+
|
598 |
+
### 6. شاشة تحليل الحساسية
|
599 |
+
|
600 |
+
```
|
601 |
+
+--------------------------------------------------+
|
602 |
+
| تحليل الحساسية |
|
603 |
+
+--------------------------------------------------+
|
604 |
+
| المشروع: [ ▼] |
|
605 |
+
+--------------------------------------------------+
|
606 |
+
| المتغيرات: |
|
607 |
+
| [✓] أسعار المواد الخام (±20%) |
|
608 |
+
| [✓] تكلفة العمالة (±15%) |
|
609 |
+
| [✓] تكلفة المعدات (±10%) |
|
610 |
+
| [ ] المصاريف العا��ة (±5%) |
|
611 |
+
+--------------------------------------------------+
|
612 |
+
| |
|
613 |
+
| |
|
614 |
+
| (رسم بياني لتحليل الحساسية) |
|
615 |
+
| |
|
616 |
+
| |
|
617 |
+
| |
|
618 |
+
+--------------------------------------------------+
|
619 |
+
| النتائج: |
|
620 |
+
| - أكثر العوامل تأثيراً: |
|
621 |
+
| - نطاق التغير المتوقع: |
|
622 |
+
| - توصيات: |
|
623 |
+
+--------------------------------------------------+
|
624 |
+
| [تحديث التحليل] [تصدير النتائج] |
|
625 |
+
+--------------------------------------------------+
|
626 |
+
```
|
627 |
+
|
628 |
+
### 7. شاشة التقارير
|
629 |
+
|
630 |
+
```
|
631 |
+
+--------------------------------------------------+
|
632 |
+
| التقارير |
|
633 |
+
+--------------------------------------------------+
|
634 |
+
| [نوع التقرير▼] [المشروع▼] [إنشاء تقرير] |
|
635 |
+
+--------------------------------------------------+
|
636 |
+
| التقارير المتاحة: |
|
637 |
+
| |
|
638 |
+
| ○ ملخص التكاليف |
|
639 |
+
| ○ تفصيل البنود |
|
640 |
+
| ○ مقارنة الأسعار |
|
641 |
+
| ○ تحليل الحساسية |
|
642 |
+
| ○ التنبؤ بالأسعار |
|
643 |
+
| ○ مخاطر الأسعار |
|
644 |
+
| |
|
645 |
+
+--------------------------------------------------+
|
646 |
+
| خيارات التقرير: |
|
647 |
+
| |
|
648 |
+
| [✓] تضمين الرسوم البيانية |
|
649 |
+
| [✓] تضمين التوصيات |
|
650 |
+
| [ ] تضمين البيانات التفصيلية |
|
651 |
+
| |
|
652 |
+
+--------------------------------------------------+
|
653 |
+
| [PDF] [Excel] [طباعة] |
|
654 |
+
+--------------------------------------------------+
|
655 |
+
```
|
656 |
+
|
657 |
+
## تكامل النظام
|
658 |
+
|
659 |
+
### 1. تكامل مع وحدة تحليل المستندات
|
660 |
+
- استخراج بنود التسعير من وثائق المناقصة
|
661 |
+
- تحديد الكميات والمواصفات من المستندات
|
662 |
+
- مقارنة البنود المستخرجة مع قاعدة البيانات
|
663 |
+
|
664 |
+
### 2. تكامل مع وحدة تحليل المخاطر
|
665 |
+
- تحديد المخاطر المرتبطة بالتسعير
|
666 |
+
- تقييم تأثير المخاطر على التكاليف
|
667 |
+
- تحديد احتياطي المخاطر المناسب
|
668 |
+
|
669 |
+
### 3. تكامل مع وحدة إدارة المشاريع
|
670 |
+
- متابعة التكاليف الفعلية مقابل المخططة
|
671 |
+
- تحديث التنبؤات بناءً على بيانات المشروع الفعلية
|
672 |
+
- تحليل انحرافات التكاليف
|
673 |
+
|
674 |
+
### 4. تكامل مع وحدة التقارير
|
675 |
+
- إنشاء تقارير متكاملة تشمل بيانات التسعير
|
676 |
+
- دمج تحليلات التسعير في تقارير المشروع
|
677 |
+
- توفير لوحات معلومات متكاملة
|
678 |
+
|
679 |
+
## خطة التنفيذ التفصيلية
|
680 |
+
|
681 |
+
### المرحلة 1: إعداد البنية التحتية (3 أيام)
|
682 |
+
- تصميم وإنشاء جداول قاعدة البيانات
|
683 |
+
- إعداد هيكل الملفات والمجلدات
|
684 |
+
- تهيئة البيئة التطويرية
|
685 |
+
|
686 |
+
### المرحلة 2: تنفيذ الوظائف الأساسية (5 أيام)
|
687 |
+
- تنفيذ فئة مدير التسعير
|
688 |
+
- تنفيذ فئة مدير بنود التسعير
|
689 |
+
- تنفيذ فئة حاسبة التكاليف
|
690 |
+
- إنشاء واجهات المستخدم الأساسية
|
691 |
+
|
692 |
+
### المرحلة 3: تنفيذ وظائف التحليل (7 أيام)
|
693 |
+
- تنفيذ فئة محلل الأسعار
|
694 |
+
- تنفيذ فئة متنبئ الأسعار
|
695 |
+
- إنشاء الرسوم البيانية والتحليلات
|
696 |
+
- تنفيذ واجهات المستخدم للتحليل
|
697 |
+
|
698 |
+
### المرحلة 4: تنفيذ التقارير والتكامل (5 أيام)
|
699 |
+
- تنفيذ فئة مولد تقارير التسعير
|
700 |
+
- تكامل مع الوحدات الأخرى
|
701 |
+
- إنشاء واجهات المستخدم للتقارير
|
702 |
+
- اختبار التكامل
|
703 |
+
|
704 |
+
### المرحلة 5: الاختبار والتحسين (3 أيام)
|
705 |
+
- اختبار جميع الوظائف
|
706 |
+
- تحسين الأداء
|
707 |
+
- إصلاح الأخطاء
|
708 |
+
- تحسين واجهة المستخدم
|
709 |
+
|
710 |
+
### المرحلة 6: التوثيق والتسليم (2 أيام)
|
711 |
+
- إعداد وثائق المستخدم
|
712 |
+
- إعداد وثائق المطور
|
713 |
+
- تجهيز النسخة النهائية
|
714 |
+
- تسليم النظام
|
docs/pricing_module_requirements.md
CHANGED
@@ -1,155 +1,155 @@
|
|
1 |
-
# متطلبات وحدة التسعير المتكاملة وتحليل الأسعار
|
2 |
-
|
3 |
-
## الهدف
|
4 |
-
تطوير وحدة متكاملة للتسعير وتحليل الأسعار في نظام إدارة المناقصات لتمكين المستخدمين من إدارة عمليات التسعير بشكل أكثر دقة وفعالية، وتوفير أدوات تحليلية متقدمة لاتخاذ قرارات أفضل.
|
5 |
-
|
6 |
-
## المتطلبات الوظيفية
|
7 |
-
|
8 |
-
### 1. إدارة بنود التسعير
|
9 |
-
- إنشاء وتحرير وحذف بنود التسعير
|
10 |
-
- تصنيف البنود حسب الفئات (مواد، عمالة، معدات، مصاريف عامة)
|
11 |
-
- دعم الوحدات المختلفة (متر مربع، متر مكعب، عدد، طن، إلخ)
|
12 |
-
- إمكانية استيراد بنود التسعير من ملفات Excel
|
13 |
-
- إمكانية تصدير بنود التسعير إلى ملفات Excel
|
14 |
-
|
15 |
-
### 2. حساب التكاليف
|
16 |
-
- حساب تكلفة المواد المباشرة
|
17 |
-
- حساب تكلفة العمالة المباشرة
|
18 |
-
- حساب تكلفة المعدات
|
19 |
-
- حساب المصاريف العامة والإدارية
|
20 |
-
- حساب هامش الربح
|
21 |
-
- حساب الضرائب والرسوم
|
22 |
-
|
23 |
-
### 3. تحليل الأسعار
|
24 |
-
- مقارنة الأسعار التاريخية للبنود
|
25 |
-
- تحليل تغيرات الأسعار عبر الزمن
|
26 |
-
- تحليل حساسية الأسعار للمتغيرات المختلفة
|
27 |
-
- تحليل المخاطر المرتبطة بتغيرات الأسعار
|
28 |
-
- مقارنة الأسعار مع أسعار السوق
|
29 |
-
- تحليل تأثير تغير أسعار المواد الخام على التكلفة الإجمالية
|
30 |
-
|
31 |
-
### 4. التنبؤ بالأسعار
|
32 |
-
- التنبؤ بتغيرات الأسعار المستقبلية
|
33 |
-
- حساب معدلات التضخم المتوقعة
|
34 |
-
- تقدير تأثير العوامل الاقتصادية على الأسعار
|
35 |
-
- إنشاء سيناريوهات مختلفة للأسعار (متفائل، متوسط، متشائم)
|
36 |
-
|
37 |
-
### 5. تقارير التسعير
|
38 |
-
- تقرير ملخص التكاليف
|
39 |
-
- تقرير تفصيلي للبنود
|
40 |
-
- تقرير مقارنة الأسعار
|
41 |
-
- تقرير تحليل الحساسية
|
42 |
-
- تقرير التنبؤ بالأسعار
|
43 |
-
- تقرير
|
44 |
-
|
45 |
-
### 6. لوحة معلومات التسعير
|
46 |
-
- عرض مؤشرات الأداء الرئيسية للتسعير
|
47 |
-
- عرض الرسوم البيانية لتحليل الأسعار
|
48 |
-
- عرض تنبيهات لتغيرات الأسعار الكبيرة
|
49 |
-
- عرض مقارنات مع المناقصات السابقة
|
50 |
-
|
51 |
-
## المتطلبات غير الوظيفية
|
52 |
-
|
53 |
-
### 1. الأداء
|
54 |
-
- سرعة استجابة عالية عند التعامل مع كميات كبيرة من البيانات
|
55 |
-
- قدرة على معالجة آلاف البنود في المناقصة الواحدة
|
56 |
-
|
57 |
-
### 2. قابلية الاستخدام
|
58 |
-
- واجهة مستخدم بديهية وسهلة الاستخدام
|
59 |
-
- إمكانية تخصيص العرض حسب احتياجات المستخدم
|
60 |
-
- توفير أدوات مساعدة وشروحات للمستخدمين
|
61 |
-
|
62 |
-
### 3. التكامل
|
63 |
-
- تكامل مع وحدة تحليل المستندات لاستخراج بنود التسعير من وثائق المناقصة
|
64 |
-
- تكامل مع وحدة تحليل المخاطر لتقييم مخاطر التسعير
|
65 |
-
- تكامل مع وحدة إدارة المشاريع لمتابعة التكاليف الفعلية مقابل المخططة
|
66 |
-
|
67 |
-
### 4. الأمان
|
68 |
-
- تحديد صلاحيات الوصول لبيانات التسعير
|
69 |
-
- تسجيل جميع التغييرات على بيانات التسعير
|
70 |
-
- حماية البيانات الحساسة المتعلقة بالتسعير
|
71 |
-
|
72 |
-
## التقنيات المقترحة
|
73 |
-
|
74 |
-
### 1. تخزين البيانات
|
75 |
-
- استخدام قاعدة بيانات SQLite لتخزين بيانات التسعير
|
76 |
-
- تصميم جداول مناسبة لتخزين البنود والتكاليف والأسعار التاريخية
|
77 |
-
|
78 |
-
### 2. تحليل البيانات
|
79 |
-
- استخدام مكتبة Pandas لمعالجة وتحليل بيانات الأسعار
|
80 |
-
- استخدام مكتبة NumPy للعمليات الحسابية المتقدمة
|
81 |
-
- استخدام مكتبة SciPy للتحليل الإحصائي
|
82 |
-
|
83 |
-
### 3. التنبؤ بالأسعار
|
84 |
-
- استخدام مكتبة Statsmodels للنماذج الإحصائية
|
85 |
-
- استخدام مكتبة Prophet للتنبؤ بالسلاسل الزمنية
|
86 |
-
- استخدام مكتبة scikit-learn لنماذج التعلم الآلي
|
87 |
-
|
88 |
-
### 4. العرض المرئي
|
89 |
-
- استخدام مكتبة Matplotlib لإنشاء الرسوم البيانية الأساسية
|
90 |
-
- استخدام مكتبة Seaborn للرسوم البيانية الإحصائية المتقدمة
|
91 |
-
- استخدام مكتبة Plotly للرسوم البيانية التفاعلية
|
92 |
-
|
93 |
-
## الواجهة
|
94 |
-
|
95 |
-
### 1. الشاشة الرئيسية لوحدة التسعير
|
96 |
-
- قائمة بالمناقصات الحالية
|
97 |
-
- ملخص لإحصائيات التسعير
|
98 |
-
- الوصول السريع للوظائف الشائعة
|
99 |
-
|
100 |
-
### 2. شاشة إدارة بنود التسعير
|
101 |
-
- جدول لعرض وتحرير البنود
|
102 |
-
- أدوات للتصفية والبحث
|
103 |
-
- أزرار للإضافة والحذف والاستيراد والتصدير
|
104 |
-
|
105 |
-
### 3. شاشة حساب التكاليف
|
106 |
-
- نموذج لإدخال معلومات التكاليف
|
107 |
-
- عرض ملخص للتكاليف حسب الفئات
|
108 |
-
- حاسبة تفاعلية للتكاليف
|
109 |
-
|
110 |
-
### 4. شاشة تحليل الأسعار
|
111 |
-
- رسوم بيانية لتحليل الأسعار
|
112 |
-
- أدوات للمقارنة والتحليل
|
113 |
-
- خيارات لتخصيص التحليل
|
114 |
-
|
115 |
-
### 5. شاشة التنبؤ بالأسعار
|
116 |
-
- نماذج للتنبؤ بالأسعار
|
117 |
-
- عرض السيناريوهات المختلفة
|
118 |
-
- تحليل الحساسية للمتغيرات
|
119 |
-
|
120 |
-
### 6. شاشة تقارير التسعير
|
121 |
-
- قائمة بالتقارير المتاحة
|
122 |
-
- خيارات لتخصيص التقارير
|
123 |
-
- أدوات لتصدير التقارير
|
124 |
-
|
125 |
-
## خطة التنفيذ
|
126 |
-
|
127 |
-
### المرحلة 1: تصميم قاعدة البيانات وهيكل الوحدة
|
128 |
-
- تصميم جداول قاعدة البيانات
|
129 |
-
- تصميم هيكل الكلاسات والوظائف
|
130 |
-
- تصميم واجهة المستخدم
|
131 |
-
|
132 |
-
### المرحلة 2: تنفيذ إدارة بنود التسعير وحساب التكاليف
|
133 |
-
- تنفيذ وظائف إدارة البنود
|
134 |
-
- تنفيذ وظائف حساب التكاليف
|
135 |
-
- تنفيذ واجهة المستخدم لهذه الوظائف
|
136 |
-
|
137 |
-
### المرحلة 3: تنفيذ تحليل الأسعار والتنبؤ
|
138 |
-
- تنفيذ وظائف تحليل الأسعار
|
139 |
-
- تنفيذ وظائف التنبؤ بالأسعار
|
140 |
-
- تنفيذ واجهة المستخدم لهذه الوظائف
|
141 |
-
|
142 |
-
### المرحلة 4: تنفيذ التقارير ولوحة المعلومات
|
143 |
-
- تنفيذ وظائف إنشاء التقارير
|
144 |
-
- تنفيذ لوحة معلومات التسعير
|
145 |
-
- تنفيذ واجهة المستخدم لهذه الوظائف
|
146 |
-
|
147 |
-
### المرحلة 5:
|
148 |
-
- تكامل الوحدة مع النظام الحالي
|
149 |
-
- اختبار الوظائف والأداء
|
150 |
-
- تصحيح الأخطاء وتحسين الأداء
|
151 |
-
|
152 |
-
### المرحلة 6: التوثيق والتسليم
|
153 |
-
- توثيق الوحدة وكيفية استخدامها
|
154 |
-
- إعداد أمثلة ودروس تعليمية
|
155 |
-
- تسليم الوحدة للمستخدم النهائي
|
|
|
1 |
+
# متطلبات وحدة التسعير المتكاملة وتحليل الأسعار
|
2 |
+
|
3 |
+
## الهدف
|
4 |
+
تطوير وحدة متكاملة للتسعير وتحليل الأسعار في نظام إدارة المناقصات لتمكين المستخدمين من إدارة عمليات التسعير بشكل أكثر دقة وفعالية، وتوفير أدوات تحليلية متقدمة لاتخاذ قرارات أفضل.
|
5 |
+
|
6 |
+
## المتطلبات الوظيفية
|
7 |
+
|
8 |
+
### 1. إدارة بنود التسعير
|
9 |
+
- إنشاء وتحرير وحذف بنود التسعير
|
10 |
+
- تصنيف البنود حسب الفئات (مواد، عمالة، معدات، مصاريف عامة)
|
11 |
+
- دعم الوحدات المختلفة (متر مربع، متر مكعب، عدد، طن، إلخ)
|
12 |
+
- إمكانية استيراد بنود التسعير من ملفات Excel
|
13 |
+
- إمكانية تصدير بنود التسعير إلى ملفات Excel
|
14 |
+
|
15 |
+
### 2. حساب التكاليف
|
16 |
+
- حساب تكلفة المواد المباشرة
|
17 |
+
- حساب تكلفة العمالة المباشرة
|
18 |
+
- حساب تكلفة المعدات
|
19 |
+
- حساب المصاريف العامة والإدارية
|
20 |
+
- حساب هامش الربح
|
21 |
+
- حساب الضرائب والرسوم
|
22 |
+
|
23 |
+
### 3. تحليل الأسعار
|
24 |
+
- مقارنة الأسعار التاريخية للبنود
|
25 |
+
- تحليل تغيرات الأسعار عبر الزمن
|
26 |
+
- تحليل حساسية الأسعار للمتغيرات المختلفة
|
27 |
+
- تحليل المخاطر المرتبطة بتغيرات الأسعار
|
28 |
+
- مقارنة الأسعار مع أسعار السوق
|
29 |
+
- تحليل تأثير تغير أسعار المواد الخام على التكلفة الإجمالية
|
30 |
+
|
31 |
+
### 4. التنبؤ بالأسعار
|
32 |
+
- التنبؤ بتغيرات الأسعار المستقبلية
|
33 |
+
- حساب معدلات التضخم المتوقعة
|
34 |
+
- تقدير تأثير العوامل الاقتصادية على الأسعار
|
35 |
+
- إنشاء سيناريوهات مختلفة للأسعار (متفائل، متوسط، متشائم)
|
36 |
+
|
37 |
+
### 5. تقارير التسعير
|
38 |
+
- تقرير ملخص التكاليف
|
39 |
+
- تقرير تفصيلي للبنود
|
40 |
+
- تقرير مقارنة الأسعار
|
41 |
+
- تقرير تحليل الحساسية
|
42 |
+
- تقرير التنبؤ بالأسعار
|
43 |
+
- تقرير المخاط�� المرتبطة بالأسعار
|
44 |
+
|
45 |
+
### 6. لوحة معلومات التسعير
|
46 |
+
- عرض مؤشرات الأداء الرئيسية للتسعير
|
47 |
+
- عرض الرسوم البيانية لتحليل الأسعار
|
48 |
+
- عرض تنبيهات لتغيرات الأسعار الكبيرة
|
49 |
+
- عرض مقارنات مع المناقصات السابقة
|
50 |
+
|
51 |
+
## المتطلبات غير الوظيفية
|
52 |
+
|
53 |
+
### 1. الأداء
|
54 |
+
- سرعة استجابة عالية عند التعامل مع كميات كبيرة من البيانات
|
55 |
+
- قدرة على معالجة آلاف البنود في المناقصة الواحدة
|
56 |
+
|
57 |
+
### 2. قابلية الاستخدام
|
58 |
+
- واجهة مستخدم بديهية وسهلة الاستخدام
|
59 |
+
- إمكانية تخصيص العرض حسب احتياجات المستخدم
|
60 |
+
- توفير أدوات مساعدة وشروحات للمستخدمين
|
61 |
+
|
62 |
+
### 3. التكامل
|
63 |
+
- تكامل مع وحدة تحليل المستندات لاستخراج بنود التسعير من وثائق المناقصة
|
64 |
+
- تكامل مع وحدة تحليل المخاطر لتقييم مخاطر التسعير
|
65 |
+
- تكامل مع وحدة إدارة المشاريع لمتابعة التكاليف الفعلية مقابل المخططة
|
66 |
+
|
67 |
+
### 4. الأمان
|
68 |
+
- تحديد صلاحيات الوصول لبيانات التسعير
|
69 |
+
- تسجيل جميع التغييرات على بيانات التسعير
|
70 |
+
- حماية البيانات الحساسة المتعلقة بالتسعير
|
71 |
+
|
72 |
+
## التقنيات المقترحة
|
73 |
+
|
74 |
+
### 1. تخزين البيانات
|
75 |
+
- استخدام قاعدة بيانات SQLite لتخزين بيانات التسعير
|
76 |
+
- تصميم جداول مناسبة لتخزين البنود والتكاليف والأسعار التاريخية
|
77 |
+
|
78 |
+
### 2. تحليل البيانات
|
79 |
+
- استخدام مكتبة Pandas لمعالجة وتحليل بيانات الأسعار
|
80 |
+
- استخدام مكتبة NumPy للعمليات الحسابية المتقدمة
|
81 |
+
- استخدام مكتبة SciPy للتحليل الإحصائي
|
82 |
+
|
83 |
+
### 3. التنبؤ بالأسعار
|
84 |
+
- استخدام مكتبة Statsmodels للنماذج الإحصائية
|
85 |
+
- استخدام مكتبة Prophet للتنبؤ بالسلاسل الزمنية
|
86 |
+
- استخدام مكتبة scikit-learn لنماذج التعلم الآلي
|
87 |
+
|
88 |
+
### 4. العرض المرئي
|
89 |
+
- استخدام مكتبة Matplotlib لإنشاء الرسوم البيانية الأساسية
|
90 |
+
- استخدام مكتبة Seaborn للرسوم البيانية الإحصائية المتقدمة
|
91 |
+
- استخدام مكتبة Plotly للرسوم البيانية التفاعلية
|
92 |
+
|
93 |
+
## الواجهة المقترحة
|
94 |
+
|
95 |
+
### 1. الشاشة الرئيسية لوحدة التسعير
|
96 |
+
- قائمة بالمناقصات الحالية
|
97 |
+
- ملخص لإحصائيات التسعير
|
98 |
+
- الوصول السريع للوظائف الشائعة
|
99 |
+
|
100 |
+
### 2. شاشة إدارة بنود التسعير
|
101 |
+
- جدول لعرض وتحرير البنود
|
102 |
+
- أدوات للتصفية والبحث
|
103 |
+
- أزرار للإضافة والحذف والاستيراد والتصدير
|
104 |
+
|
105 |
+
### 3. شاشة حساب التكاليف
|
106 |
+
- نموذج لإدخال معلومات التكاليف
|
107 |
+
- عرض ملخص للتكاليف حسب الفئات
|
108 |
+
- حاسبة تفاعلية للتكاليف
|
109 |
+
|
110 |
+
### 4. شاشة تحليل الأسعار
|
111 |
+
- رسوم بيانية لتحليل الأسعار
|
112 |
+
- أدوات للمقارنة والتحليل
|
113 |
+
- خيارات لتخصيص التحليل
|
114 |
+
|
115 |
+
### 5. شاشة التنبؤ بالأسعار
|
116 |
+
- نماذج للتنبؤ بالأسعار
|
117 |
+
- عرض السيناريوهات المختلفة
|
118 |
+
- تحليل الحساسية للمتغيرات
|
119 |
+
|
120 |
+
### 6. شاشة تقارير التسعير
|
121 |
+
- قائمة بالتقارير المتاحة
|
122 |
+
- خيارات لتخصيص التقارير
|
123 |
+
- أدوات لتصدير التقارير
|
124 |
+
|
125 |
+
## خطة التنفيذ
|
126 |
+
|
127 |
+
### المرحلة 1: تصميم قاعدة البيانات وهيكل الوحدة
|
128 |
+
- تصميم جداول قاعدة البيانات
|
129 |
+
- تصميم هيكل الكلاسات والوظائف
|
130 |
+
- تصميم واجهة المستخدم
|
131 |
+
|
132 |
+
### المرحلة 2: تنفيذ إدارة بنود التسعير وحساب التكاليف
|
133 |
+
- تنفيذ وظائف إدارة البنود
|
134 |
+
- تنفيذ وظائف حساب التكاليف
|
135 |
+
- تنفيذ واجهة المستخدم لهذه الوظائف
|
136 |
+
|
137 |
+
### المرحلة 3: تنفيذ تحليل الأسعار والتنبؤ
|
138 |
+
- تنفيذ وظائف تحليل الأسعار
|
139 |
+
- تنفيذ وظائف التنبؤ بالأسعار
|
140 |
+
- تنفيذ واجهة المستخدم لهذه الوظائف
|
141 |
+
|
142 |
+
### المرحلة 4: تنفيذ التقارير ولوحة المعلومات
|
143 |
+
- تنفيذ وظائف إنشاء التقارير
|
144 |
+
- تنفيذ لوحة معلومات التسعير
|
145 |
+
- تنفيذ واجهة المستخدم لهذه الوظائف
|
146 |
+
|
147 |
+
### المرحلة 5: ا��تكامل والاختبار
|
148 |
+
- تكامل الوحدة مع النظام الحالي
|
149 |
+
- اختبار الوظائف والأداء
|
150 |
+
- تصحيح الأخطاء وتحسين الأداء
|
151 |
+
|
152 |
+
### المرحلة 6: التوثيق والتسليم
|
153 |
+
- توثيق الوحدة وكيفية استخدامها
|
154 |
+
- إعداد أمثلة ودروس تعليمية
|
155 |
+
- تسليم الوحدة للمستخدم النهائي
|
fixed_pricing_app.py
CHANGED
@@ -1,544 +1,544 @@
|
|
1 |
-
"""
|
2 |
-
وحدة التسعير - التطبيق الرئيسي
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
import pandas as pd
|
7 |
-
import numpy as np
|
8 |
-
import matplotlib.pyplot as plt
|
9 |
-
import plotly.express as px
|
10 |
-
import plotly.graph_objects as go
|
11 |
-
from datetime import datetime
|
12 |
-
import time
|
13 |
-
import io
|
14 |
-
import os
|
15 |
-
import json
|
16 |
-
import base64
|
17 |
-
from pathlib import Path
|
18 |
-
|
19 |
-
class PricingApp:
|
20 |
-
"""وحدة التسعير"""
|
21 |
-
|
22 |
-
def __init__(self):
|
23 |
-
"""تهيئة وحدة التسعير"""
|
24 |
-
|
25 |
-
# تهيئة حالة الجلسة
|
26 |
-
if 'bill_of_quantities' not in st.session_state:
|
27 |
-
st.session_state.bill_of_quantities = [
|
28 |
-
{
|
29 |
-
'id': 1,
|
30 |
-
'code': 'A-001',
|
31 |
-
'description': 'أعمال الحفر والردم',
|
32 |
-
'unit': 'م3',
|
33 |
-
'quantity': 1500,
|
34 |
-
'unit_price': 45,
|
35 |
-
'total_price': 67500,
|
36 |
-
'category': 'أعمال ترابية'
|
37 |
-
},
|
38 |
-
{
|
39 |
-
'id': 2,
|
40 |
-
'code': 'A-002',
|
41 |
-
'description': 'توريد وصب خرسانة عادية',
|
42 |
-
'unit': 'م3',
|
43 |
-
'quantity': 250,
|
44 |
-
'unit_price': 350,
|
45 |
-
'total_price': 87500,
|
46 |
-
'category': 'أعمال خرسانية'
|
47 |
-
},
|
48 |
-
{
|
49 |
-
'id': 3,
|
50 |
-
'code': 'A-003',
|
51 |
-
'description': 'توريد وصب خرسانة مسلحة للأساسات',
|
52 |
-
'unit': 'م3',
|
53 |
-
'quantity': 180,
|
54 |
-
'unit_price': 450,
|
55 |
-
'total_price': 81000,
|
56 |
-
'category': 'أعمال خرسانية'
|
57 |
-
},
|
58 |
-
{
|
59 |
-
'id': 4,
|
60 |
-
'code': 'A-004',
|
61 |
-
'description': 'توريد وصب خرسانة مسلحة للأعمدة',
|
62 |
-
'unit': 'م3',
|
63 |
-
'quantity': 120,
|
64 |
-
'unit_price': 500,
|
65 |
-
'total_price': 60000,
|
66 |
-
'category': 'أعمال خرسانية'
|
67 |
-
},
|
68 |
-
{
|
69 |
-
'id': 5,
|
70 |
-
'code': 'A-005',
|
71 |
-
'description': 'توريد وتركيب حديد تسليح',
|
72 |
-
'unit': 'طن',
|
73 |
-
'quantity': 45,
|
74 |
-
'unit_price': 3000,
|
75 |
-
'total_price': 135000,
|
76 |
-
'category': 'أعمال حديد'
|
77 |
-
},
|
78 |
-
{
|
79 |
-
'id': 6,
|
80 |
-
'code': 'A-006',
|
81 |
-
'description': 'توريد وبناء طابوق',
|
82 |
-
'unit': 'م2',
|
83 |
-
'quantity': 1200,
|
84 |
-
'unit_price': 45,
|
85 |
-
'total_price': 54000,
|
86 |
-
'category': 'أعمال بناء'
|
87 |
-
},
|
88 |
-
{
|
89 |
-
'id': 7,
|
90 |
-
'code': 'A-007',
|
91 |
-
'description': 'أعمال اللياسة والتشطيبات',
|
92 |
-
'unit': 'م2',
|
93 |
-
'quantity': 2400,
|
94 |
-
'unit_price': 35,
|
95 |
-
'total_price': 84000,
|
96 |
-
'category': 'أعمال تشطيبات'
|
97 |
-
},
|
98 |
-
{
|
99 |
-
'id': 8,
|
100 |
-
'code': 'A-008',
|
101 |
-
'description': 'أعمال الدهانات',
|
102 |
-
'unit': 'م2',
|
103 |
-
'quantity': 2400,
|
104 |
-
'unit_price': 25,
|
105 |
-
'total_price': 60000,
|
106 |
-
'category': 'أعمال تشطيبات'
|
107 |
-
},
|
108 |
-
{
|
109 |
-
'id': 9,
|
110 |
-
'code': 'A-009',
|
111 |
-
'description': 'توريد وتركيب أبواب خشبية',
|
112 |
-
'unit': 'عدد',
|
113 |
-
'quantity': 24,
|
114 |
-
'unit_price': 750,
|
115 |
-
'total_price': 18000,
|
116 |
-
'category': 'أعمال نجارة'
|
117 |
-
},
|
118 |
-
{
|
119 |
-
'id': 10,
|
120 |
-
'code': 'A-010',
|
121 |
-
'description': 'توريد وتركيب نوافذ ألمنيوم',
|
122 |
-
'unit': 'م2',
|
123 |
-
'quantity': 120,
|
124 |
-
'unit_price': 350,
|
125 |
-
'total_price': 42000,
|
126 |
-
'category': 'أعمال ألمنيوم'
|
127 |
-
}
|
128 |
-
]
|
129 |
-
|
130 |
-
if 'cost_analysis' not in st.session_state:
|
131 |
-
st.session_state.cost_analysis = [
|
132 |
-
{
|
133 |
-
'id': 1,
|
134 |
-
'category': 'تكاليف مباشرة',
|
135 |
-
'subcategory': 'مواد',
|
136 |
-
'description': 'خرسانة',
|
137 |
-
'amount': 120000,
|
138 |
-
'percentage': 17.9
|
139 |
-
},
|
140 |
-
{
|
141 |
-
'id': 2,
|
142 |
-
'category': 'تكاليف مباشرة',
|
143 |
-
'subcategory': 'مواد',
|
144 |
-
'description': 'حديد تسليح',
|
145 |
-
'amount': 135000,
|
146 |
-
'percentage': 20.1
|
147 |
-
},
|
148 |
-
{
|
149 |
-
'id': 3,
|
150 |
-
'category': 'تكاليف مباشرة',
|
151 |
-
'subcategory': 'مواد',
|
152 |
-
'description': 'طابوق',
|
153 |
-
'amount': 54000,
|
154 |
-
'percentage': 8.1
|
155 |
-
},
|
156 |
-
{
|
157 |
-
'id': 4,
|
158 |
-
'category': 'تكاليف مباشرة',
|
159 |
-
'subcategory': 'عمالة',
|
160 |
-
'description': 'عمالة تنفيذ',
|
161 |
-
'amount': 120000,
|
162 |
-
'percentage': 17.9
|
163 |
-
},
|
164 |
-
{
|
165 |
-
'id': 5,
|
166 |
-
'category': 'تكاليف مباشرة',
|
167 |
-
'subcategory': 'معدات',
|
168 |
-
'description': 'معدات إنشائية',
|
169 |
-
'amount': 85000,
|
170 |
-
'percentage': 12.7
|
171 |
-
},
|
172 |
-
{
|
173 |
-
'id': 6,
|
174 |
-
'category': 'تكاليف غير مباشرة',
|
175 |
-
'subcategory': 'إدارة',
|
176 |
-
'description': 'إدارة المشروع',
|
177 |
-
'amount': 45000,
|
178 |
-
'percentage': 6.7
|
179 |
-
},
|
180 |
-
{
|
181 |
-
'id': 7,
|
182 |
-
'category': 'تكاليف غير مباشرة',
|
183 |
-
'subcategory': 'إدارة',
|
184 |
-
'description': 'إشراف هندسي',
|
185 |
-
'amount': 35000,
|
186 |
-
'percentage': 5.2
|
187 |
-
},
|
188 |
-
{
|
189 |
-
'id': 8,
|
190 |
-
'category': 'تكاليف غير مباشرة',
|
191 |
-
'subcategory': 'عامة',
|
192 |
-
'description': 'تأمينات وضمانات',
|
193 |
-
'amount': 25000,
|
194 |
-
'percentage': 3.7
|
195 |
-
},
|
196 |
-
{
|
197 |
-
'id': 9,
|
198 |
-
'category': 'تكاليف غير مباشرة',
|
199 |
-
'subcategory': 'عامة',
|
200 |
-
'description': 'مصاريف إدارية',
|
201 |
-
'amount': 30000,
|
202 |
-
'percentage': 4.5
|
203 |
-
},
|
204 |
-
{
|
205 |
-
'id': 10,
|
206 |
-
'category': 'أرباح',
|
207 |
-
'subcategory': 'أرباح',
|
208 |
-
'description': 'هامش الربح',
|
209 |
-
'amount': 55000,
|
210 |
-
'percentage': 8.2
|
211 |
-
}
|
212 |
-
]
|
213 |
-
|
214 |
-
if 'price_scenarios' not in st.session_state:
|
215 |
-
st.session_state.price_scenarios = [
|
216 |
-
{
|
217 |
-
'id': 1,
|
218 |
-
'name': 'السيناريو الأساسي',
|
219 |
-
'description': 'التسعير الأساسي مع هامش ربح 8%',
|
220 |
-
'total_cost': 615000,
|
221 |
-
'profit_margin': 8.2,
|
222 |
-
'total_price': 670000,
|
223 |
-
'is_active': True
|
224 |
-
},
|
225 |
-
{
|
226 |
-
'id': 2,
|
227 |
-
'name': 'سيناريو تنافسي',
|
228 |
-
'description': 'تخفيض هامش الربح للمنافسة',
|
229 |
-
'total_cost': 615000,
|
230 |
-
'profit_margin': 5.0,
|
231 |
-
'total_price': 650000,
|
232 |
-
'is_active': False
|
233 |
-
},
|
234 |
-
{
|
235 |
-
'id': 3,
|
236 |
-
'name': 'سيناريو مرتفع',
|
237 |
-
'description': 'زيادة هامش الربح للمشاريع ذات المخاطر العالية',
|
238 |
-
'total_cost': 615000,
|
239 |
-
'profit_margin': 12.0,
|
240 |
-
'total_price': 700000,
|
241 |
-
'is_active': False
|
242 |
-
}
|
243 |
-
]
|
244 |
-
|
245 |
-
def run(self):
|
246 |
-
"""تشغيل وحدة التسعير"""
|
247 |
-
# استدعاء دالة العرض
|
248 |
-
self.render()
|
249 |
-
|
250 |
-
def render(self):
|
251 |
-
"""عرض واجهة وحدة التسعير"""
|
252 |
-
|
253 |
-
st.markdown("<h1 class='module-title'>وحدة التسعير</h1>", unsafe_allow_html=True)
|
254 |
-
|
255 |
-
tabs = st.tabs([
|
256 |
-
"لوحة التحكم",
|
257 |
-
"جدول الكميات",
|
258 |
-
"تحليل التكاليف",
|
259 |
-
"سيناريوهات التسعير",
|
260 |
-
"المقارنة التنافسية",
|
261 |
-
"التقارير"
|
262 |
-
])
|
263 |
-
|
264 |
-
with tabs[0]:
|
265 |
-
self._render_dashboard_tab()
|
266 |
-
|
267 |
-
with tabs[1]:
|
268 |
-
self._render_bill_of_quantities_tab()
|
269 |
-
|
270 |
-
with tabs[2]:
|
271 |
-
self._render_cost_analysis_tab()
|
272 |
-
|
273 |
-
with tabs[3]:
|
274 |
-
self._render_pricing_scenarios_tab()
|
275 |
-
|
276 |
-
with tabs[4]:
|
277 |
-
self._render_competitive_analysis_tab()
|
278 |
-
|
279 |
-
with tabs[5]:
|
280 |
-
self._render_reports_tab()
|
281 |
-
|
282 |
-
def _render_dashboard_tab(self):
|
283 |
-
"""عرض تبويب لوحة التحكم"""
|
284 |
-
|
285 |
-
st.markdown("### لوحة تحكم التسعير")
|
286 |
-
|
287 |
-
# عرض ملخص التسعير
|
288 |
-
col1, col2, col3, col4 = st.columns(4)
|
289 |
-
|
290 |
-
# حساب إجمالي التكاليف
|
291 |
-
total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
|
292 |
-
total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
|
293 |
-
total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
|
294 |
-
total_cost = total_direct_cost + total_indirect_cost
|
295 |
-
total_price = total_cost + total_profit
|
296 |
-
|
297 |
-
with col1:
|
298 |
-
st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.0f} ريال")
|
299 |
-
|
300 |
-
with col2:
|
301 |
-
st.metric("إجمالي التكاليف غير المباشرة", f"{total_indirect_cost:,.0f} ريال")
|
302 |
-
|
303 |
-
with col3:
|
304 |
-
st.metric("إجمالي التكاليف", f"{total_cost:,.0f} ريال")
|
305 |
-
|
306 |
-
with col4:
|
307 |
-
st.metric("السعر الإجمالي", f"{total_price:,.0f} ريال")
|
308 |
-
|
309 |
-
# عرض توزيع التكاليف
|
310 |
-
st.markdown("### توزيع التكاليف")
|
311 |
-
|
312 |
-
# تجميع البيانات حسب الفئة
|
313 |
-
cost_categories = {}
|
314 |
-
|
315 |
-
for item in st.session_state.cost_analysis:
|
316 |
-
category = item['category']
|
317 |
-
if category in cost_categories:
|
318 |
-
cost_categories[category] += item['amount']
|
319 |
-
else:
|
320 |
-
cost_categories[category] = item['amount']
|
321 |
-
|
322 |
-
# إنشاء DataFrame للرسم البياني
|
323 |
-
cost_df = pd.DataFrame({
|
324 |
-
'الفئة': list(cost_categories.keys()),
|
325 |
-
'المبلغ': list(cost_categories.values())
|
326 |
-
})
|
327 |
-
|
328 |
-
# إنشاء رسم بياني دائري
|
329 |
-
fig = px.pie(
|
330 |
-
cost_df,
|
331 |
-
values='المبلغ',
|
332 |
-
names='الفئة',
|
333 |
-
title='توزيع التكاليف حسب الفئة',
|
334 |
-
color_discrete_sequence=px.colors.qualitative.Set3
|
335 |
-
)
|
336 |
-
|
337 |
-
st.plotly_chart(fig, use_container_width=True)
|
338 |
-
|
339 |
-
# عرض توزيع التكاليف المباشرة
|
340 |
-
st.markdown("### توزيع التكاليف المباشرة")
|
341 |
-
|
342 |
-
# تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
|
343 |
-
direct_cost_subcategories = {}
|
344 |
-
|
345 |
-
for item in st.session_state.cost_analysis:
|
346 |
-
if item['category'] == 'تكاليف مباشرة':
|
347 |
-
subcategory = item['subcategory']
|
348 |
-
if subcategory in direct_cost_subcategories:
|
349 |
-
direct_cost_subcategories[subcategory] += item['amount']
|
350 |
-
else:
|
351 |
-
direct_cost_subcategories[subcategory] = item['amount']
|
352 |
-
|
353 |
-
# إنشاء DataFrame للرسم البياني
|
354 |
-
direct_cost_df = pd.DataFrame({
|
355 |
-
'الفئة الفرعية': list(direct_cost_subcategories.keys()),
|
356 |
-
'المبلغ': list(direct_cost_subcategories.values())
|
357 |
-
})
|
358 |
-
|
359 |
-
# إنشاء رسم بياني دائري
|
360 |
-
fig = px.pie(
|
361 |
-
direct_cost_df,
|
362 |
-
values='المبلغ',
|
363 |
-
names='الفئة الفرعية',
|
364 |
-
title='توزيع التكاليف المباشرة',
|
365 |
-
color_discrete_sequence=px.colors.qualitative.Pastel
|
366 |
-
)
|
367 |
-
|
368 |
-
st.plotly_chart(fig, use_container_width=True)
|
369 |
-
|
370 |
-
# عرض توزيع التكاليف غير المباشرة
|
371 |
-
st.markdown("### توزيع التكاليف غير المباشرة")
|
372 |
-
|
373 |
-
# تجميع البيانات حسب الفئة الفرعية للتكاليف غير المباشرة
|
374 |
-
indirect_cost_subcategories = {}
|
375 |
-
|
376 |
-
for item in st.session_state.cost_analysis:
|
377 |
-
if item['category'] == 'تكاليف غير مباشرة':
|
378 |
-
subcategory = item['subcategory']
|
379 |
-
if subcategory in indirect_cost_subcategories:
|
380 |
-
indirect_cost_subcategories[subcategory] += item['amount']
|
381 |
-
else:
|
382 |
-
indirect_cost_subcategories[subcategory] = item['amount']
|
383 |
-
|
384 |
-
# إنشاء DataFrame للرسم البياني
|
385 |
-
indirect_cost_df = pd.DataFrame({
|
386 |
-
'الفئة الفرعية': list(indirect_cost_subcategories.keys()),
|
387 |
-
'المبلغ': list(indirect_cost_subcategories.values())
|
388 |
-
})
|
389 |
-
|
390 |
-
# إنشاء رسم بياني دائري
|
391 |
-
fig = px.pie(
|
392 |
-
indirect_cost_df,
|
393 |
-
values='المبلغ',
|
394 |
-
names='الفئة الفرعية',
|
395 |
-
title='توزيع التكاليف غير المباشرة',
|
396 |
-
color_discrete_sequence=px.colors.qualitative.Pastel1
|
397 |
-
)
|
398 |
-
|
399 |
-
st.plotly_chart(fig, use_container_width=True)
|
400 |
-
|
401 |
-
def _render_bill_of_quantities_tab(self):
|
402 |
-
"""عرض تبويب جدول الكميات"""
|
403 |
-
|
404 |
-
st.markdown("### جدول الكميات")
|
405 |
-
|
406 |
-
# إنشاء DataFrame من بيانات جدول الكميات
|
407 |
-
boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
|
408 |
-
|
409 |
-
# عرض جدول الكميات
|
410 |
-
st.dataframe(
|
411 |
-
boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
|
412 |
-
column_config={
|
413 |
-
'code': 'الكود',
|
414 |
-
'description': 'الوصف',
|
415 |
-
'unit': 'الوحدة',
|
416 |
-
'quantity': 'الكمية',
|
417 |
-
'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
|
418 |
-
'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
|
419 |
-
'category': 'الفئة'
|
420 |
-
},
|
421 |
-
hide_index=True,
|
422 |
-
use_container_width=True
|
423 |
-
)
|
424 |
-
|
425 |
-
# إضافة بند جديد
|
426 |
-
st.markdown("### إضافة بند جديد")
|
427 |
-
|
428 |
-
col1, col2 = st.columns(2)
|
429 |
-
|
430 |
-
with col1:
|
431 |
-
new_code = st.text_input("الكود", key="new_boq_code")
|
432 |
-
new_description = st.text_input("الوصف", key="new_boq_description")
|
433 |
-
new_unit = st.selectbox("الوحدة", ["م3", "م2", "طن", "عدد", "متر طولي"], key="new_boq_unit")
|
434 |
-
|
435 |
-
with col2:
|
436 |
-
new_quantity = st.number_input("الكمية", min_value=0.0, step=1.0, key="new_boq_quantity")
|
437 |
-
new_unit_price = st.number_input("سعر الوحدة", min_value=0.0, step=10.0, key="new_boq_unit_price")
|
438 |
-
new_category = st.selectbox(
|
439 |
-
"الفئة",
|
440 |
-
[
|
441 |
-
"أعمال ترابية",
|
442 |
-
"أعمال خرسانية",
|
443 |
-
"أعمال حديد",
|
444 |
-
"أعمال بناء",
|
445 |
-
"أعمال تشطيبات",
|
446 |
-
"أعمال نجارة",
|
447 |
-
"أعمال ألمنيوم",
|
448 |
-
"أعمال كهربائية",
|
449 |
-
"أعمال ميكانيكية",
|
450 |
-
"أعمال صحية"
|
451 |
-
],
|
452 |
-
key="new_boq_category"
|
453 |
-
)
|
454 |
-
|
455 |
-
if st.button("إضافة البند", key="add_boq_item"):
|
456 |
-
if new_code and new_description and new_quantity > 0 and new_unit_price > 0:
|
457 |
-
# حساب السعر الإجمالي
|
458 |
-
new_total_price = new_quantity * new_unit_price
|
459 |
-
|
460 |
-
# إضافة بند جديد
|
461 |
-
new_id = max([item['id'] for item in st.session_state.bill_of_quantities]) + 1
|
462 |
-
|
463 |
-
st.session_state.bill_of_quantities.append({
|
464 |
-
'id': new_id,
|
465 |
-
'code': new_code,
|
466 |
-
'description': new_description,
|
467 |
-
'unit': new_unit,
|
468 |
-
'quantity': new_quantity,
|
469 |
-
'unit_price': new_unit_price,
|
470 |
-
'total_price': new_total_price,
|
471 |
-
'category': new_category
|
472 |
-
})
|
473 |
-
|
474 |
-
st.success(f"تمت إضافة البند بنجاح: {new_description}")
|
475 |
-
|
476 |
-
# تحديث الصفحة لعرض البند الجديد
|
477 |
-
st.rerun()
|
478 |
-
else:
|
479 |
-
st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
|
480 |
-
|
481 |
-
# عرض ملخص جدول الكميات (إزالة التكرار)
|
482 |
-
st.markdown("### ملخص جدول الكميات")
|
483 |
-
|
484 |
-
# تجميع البيانات حسب الفئة
|
485 |
-
category_totals = {}
|
486 |
-
for item in st.session_state.bill_of_quantities:
|
487 |
-
category = item['category']
|
488 |
-
if category in category_totals:
|
489 |
-
category_totals[category] += item['total_price']
|
490 |
-
else:
|
491 |
-
category_totals[category] = item['total_price']
|
492 |
-
|
493 |
-
# إنشاء DataFrame للرسم البياني
|
494 |
-
category_df = pd.DataFrame({
|
495 |
-
'الفئة': list(category_totals.keys()),
|
496 |
-
'المبلغ': list(category_totals.values())
|
497 |
-
})
|
498 |
-
|
499 |
-
# ترتيب البيانات تنازليًا حسب المبلغ
|
500 |
-
category_df = category_df.sort_values('المبلغ', ascending=False)
|
501 |
-
|
502 |
-
# إنشاء رسم بياني شريطي
|
503 |
-
fig = px.bar(
|
504 |
-
category_df,
|
505 |
-
x='الفئة',
|
506 |
-
y='المبلغ',
|
507 |
-
title='إجمالي تكلفة البنود حسب الفئة',
|
508 |
-
color='الفئة',
|
509 |
-
text_auto=True
|
510 |
-
)
|
511 |
-
|
512 |
-
st.plotly_chart(fig, use_container_width=True)
|
513 |
-
|
514 |
-
# حساب إجمالي جدول الكميات
|
515 |
-
total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
|
516 |
-
|
517 |
-
# يجب إضافة باقي الدوال المفقودة هنا
|
518 |
-
def _render_cost_analysis_tab(self):
|
519 |
-
"""عرض تبويب تحليل التكاليف"""
|
520 |
-
# تنفيذ هذه الدالة حسب متطلبات التطبيق
|
521 |
-
st.markdown("### تحليل التكاليف")
|
522 |
-
# محتوى مؤقت
|
523 |
-
st.info("تبويب تحليل التكاليف قيد التطوير")
|
524 |
-
|
525 |
-
def _render_pricing_scenarios_tab(self):
|
526 |
-
"""عرض تبويب سيناريوهات التسعير"""
|
527 |
-
# تنفيذ هذه الدالة حسب متطلبات التطبيق
|
528 |
-
st.markdown("### سيناريوهات التسعير")
|
529 |
-
# محتوى مؤقت
|
530 |
-
st.info("تبويب سيناريوهات التسعير قيد التطوير")
|
531 |
-
|
532 |
-
def _render_competitive_analysis_tab(self):
|
533 |
-
"""عرض تبويب المقارنة التنافسية"""
|
534 |
-
# تنفيذ هذه الدالة حسب متطلبات التطبيق
|
535 |
-
st.markdown("### المقارنة التنافسية")
|
536 |
-
# محتوى مؤقت
|
537 |
-
st.info("تبويب المقارنة التنافسية قيد التطوير")
|
538 |
-
|
539 |
-
def _render_reports_tab(self):
|
540 |
-
"""عرض تبويب التقارير"""
|
541 |
-
# تنفيذ هذه الدالة حسب متطلبات التطبيق
|
542 |
-
st.markdown("### التقارير")
|
543 |
-
# محتوى مؤقت
|
544 |
-
st.info("تبويب التقارير قيد التطوير")
|
|
|
1 |
+
"""
|
2 |
+
وحدة التسعير - التطبيق الرئيسي
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import matplotlib.pyplot as plt
|
9 |
+
import plotly.express as px
|
10 |
+
import plotly.graph_objects as go
|
11 |
+
from datetime import datetime
|
12 |
+
import time
|
13 |
+
import io
|
14 |
+
import os
|
15 |
+
import json
|
16 |
+
import base64
|
17 |
+
from pathlib import Path
|
18 |
+
|
19 |
+
class PricingApp:
|
20 |
+
"""وحدة التسعير"""
|
21 |
+
|
22 |
+
def __init__(self):
|
23 |
+
"""تهيئة وحدة التسعير"""
|
24 |
+
|
25 |
+
# تهيئة حالة الجلسة
|
26 |
+
if 'bill_of_quantities' not in st.session_state:
|
27 |
+
st.session_state.bill_of_quantities = [
|
28 |
+
{
|
29 |
+
'id': 1,
|
30 |
+
'code': 'A-001',
|
31 |
+
'description': 'أعمال الحفر والردم',
|
32 |
+
'unit': 'م3',
|
33 |
+
'quantity': 1500,
|
34 |
+
'unit_price': 45,
|
35 |
+
'total_price': 67500,
|
36 |
+
'category': 'أعمال ترابية'
|
37 |
+
},
|
38 |
+
{
|
39 |
+
'id': 2,
|
40 |
+
'code': 'A-002',
|
41 |
+
'description': 'توريد وصب خرسانة عادية',
|
42 |
+
'unit': 'م3',
|
43 |
+
'quantity': 250,
|
44 |
+
'unit_price': 350,
|
45 |
+
'total_price': 87500,
|
46 |
+
'category': 'أعمال خرسانية'
|
47 |
+
},
|
48 |
+
{
|
49 |
+
'id': 3,
|
50 |
+
'code': 'A-003',
|
51 |
+
'description': 'توريد وصب خرسانة مسلحة للأساسات',
|
52 |
+
'unit': 'م3',
|
53 |
+
'quantity': 180,
|
54 |
+
'unit_price': 450,
|
55 |
+
'total_price': 81000,
|
56 |
+
'category': 'أعمال خرسانية'
|
57 |
+
},
|
58 |
+
{
|
59 |
+
'id': 4,
|
60 |
+
'code': 'A-004',
|
61 |
+
'description': 'توريد وصب خرسانة مسلحة للأعمدة',
|
62 |
+
'unit': 'م3',
|
63 |
+
'quantity': 120,
|
64 |
+
'unit_price': 500,
|
65 |
+
'total_price': 60000,
|
66 |
+
'category': 'أعمال خرسانية'
|
67 |
+
},
|
68 |
+
{
|
69 |
+
'id': 5,
|
70 |
+
'code': 'A-005',
|
71 |
+
'description': 'توريد وتركيب حديد تسليح',
|
72 |
+
'unit': 'طن',
|
73 |
+
'quantity': 45,
|
74 |
+
'unit_price': 3000,
|
75 |
+
'total_price': 135000,
|
76 |
+
'category': 'أعمال حديد'
|
77 |
+
},
|
78 |
+
{
|
79 |
+
'id': 6,
|
80 |
+
'code': 'A-006',
|
81 |
+
'description': 'توريد وبناء طابوق',
|
82 |
+
'unit': 'م2',
|
83 |
+
'quantity': 1200,
|
84 |
+
'unit_price': 45,
|
85 |
+
'total_price': 54000,
|
86 |
+
'category': 'أعمال بناء'
|
87 |
+
},
|
88 |
+
{
|
89 |
+
'id': 7,
|
90 |
+
'code': 'A-007',
|
91 |
+
'description': 'أعمال اللياسة والتشطيبات',
|
92 |
+
'unit': 'م2',
|
93 |
+
'quantity': 2400,
|
94 |
+
'unit_price': 35,
|
95 |
+
'total_price': 84000,
|
96 |
+
'category': 'أعمال تشطيبات'
|
97 |
+
},
|
98 |
+
{
|
99 |
+
'id': 8,
|
100 |
+
'code': 'A-008',
|
101 |
+
'description': 'أعمال الدهانات',
|
102 |
+
'unit': 'م2',
|
103 |
+
'quantity': 2400,
|
104 |
+
'unit_price': 25,
|
105 |
+
'total_price': 60000,
|
106 |
+
'category': 'أعمال تشطيبات'
|
107 |
+
},
|
108 |
+
{
|
109 |
+
'id': 9,
|
110 |
+
'code': 'A-009',
|
111 |
+
'description': 'توريد وتركيب أبواب خشبية',
|
112 |
+
'unit': 'عدد',
|
113 |
+
'quantity': 24,
|
114 |
+
'unit_price': 750,
|
115 |
+
'total_price': 18000,
|
116 |
+
'category': 'أعمال نجارة'
|
117 |
+
},
|
118 |
+
{
|
119 |
+
'id': 10,
|
120 |
+
'code': 'A-010',
|
121 |
+
'description': 'توريد وتركيب نوافذ ألمنيوم',
|
122 |
+
'unit': 'م2',
|
123 |
+
'quantity': 120,
|
124 |
+
'unit_price': 350,
|
125 |
+
'total_price': 42000,
|
126 |
+
'category': 'أعمال ألمنيوم'
|
127 |
+
}
|
128 |
+
]
|
129 |
+
|
130 |
+
if 'cost_analysis' not in st.session_state:
|
131 |
+
st.session_state.cost_analysis = [
|
132 |
+
{
|
133 |
+
'id': 1,
|
134 |
+
'category': 'تكاليف مباشرة',
|
135 |
+
'subcategory': 'مواد',
|
136 |
+
'description': 'خرسانة',
|
137 |
+
'amount': 120000,
|
138 |
+
'percentage': 17.9
|
139 |
+
},
|
140 |
+
{
|
141 |
+
'id': 2,
|
142 |
+
'category': 'تكاليف مباشرة',
|
143 |
+
'subcategory': 'مواد',
|
144 |
+
'description': 'حديد تسليح',
|
145 |
+
'amount': 135000,
|
146 |
+
'percentage': 20.1
|
147 |
+
},
|
148 |
+
{
|
149 |
+
'id': 3,
|
150 |
+
'category': 'تكاليف مباشرة',
|
151 |
+
'subcategory': 'مواد',
|
152 |
+
'description': 'طابوق',
|
153 |
+
'amount': 54000,
|
154 |
+
'percentage': 8.1
|
155 |
+
},
|
156 |
+
{
|
157 |
+
'id': 4,
|
158 |
+
'category': 'تكاليف مباشرة',
|
159 |
+
'subcategory': 'عمالة',
|
160 |
+
'description': 'عمالة تنفيذ',
|
161 |
+
'amount': 120000,
|
162 |
+
'percentage': 17.9
|
163 |
+
},
|
164 |
+
{
|
165 |
+
'id': 5,
|
166 |
+
'category': 'تكاليف مباشرة',
|
167 |
+
'subcategory': 'معدات',
|
168 |
+
'description': 'معدات إنشائية',
|
169 |
+
'amount': 85000,
|
170 |
+
'percentage': 12.7
|
171 |
+
},
|
172 |
+
{
|
173 |
+
'id': 6,
|
174 |
+
'category': 'تكاليف غير مباشرة',
|
175 |
+
'subcategory': 'إدارة',
|
176 |
+
'description': 'إدارة المشروع',
|
177 |
+
'amount': 45000,
|
178 |
+
'percentage': 6.7
|
179 |
+
},
|
180 |
+
{
|
181 |
+
'id': 7,
|
182 |
+
'category': 'تكاليف غير مباشرة',
|
183 |
+
'subcategory': 'إدارة',
|
184 |
+
'description': 'إشراف هندسي',
|
185 |
+
'amount': 35000,
|
186 |
+
'percentage': 5.2
|
187 |
+
},
|
188 |
+
{
|
189 |
+
'id': 8,
|
190 |
+
'category': 'تكاليف غير مباشرة',
|
191 |
+
'subcategory': 'عامة',
|
192 |
+
'description': 'تأمينات وضمانات',
|
193 |
+
'amount': 25000,
|
194 |
+
'percentage': 3.7
|
195 |
+
},
|
196 |
+
{
|
197 |
+
'id': 9,
|
198 |
+
'category': 'تكاليف غير مباشرة',
|
199 |
+
'subcategory': 'عامة',
|
200 |
+
'description': 'مصاريف إدارية',
|
201 |
+
'amount': 30000,
|
202 |
+
'percentage': 4.5
|
203 |
+
},
|
204 |
+
{
|
205 |
+
'id': 10,
|
206 |
+
'category': 'أرباح',
|
207 |
+
'subcategory': 'أرباح',
|
208 |
+
'description': 'هامش الربح',
|
209 |
+
'amount': 55000,
|
210 |
+
'percentage': 8.2
|
211 |
+
}
|
212 |
+
]
|
213 |
+
|
214 |
+
if 'price_scenarios' not in st.session_state:
|
215 |
+
st.session_state.price_scenarios = [
|
216 |
+
{
|
217 |
+
'id': 1,
|
218 |
+
'name': 'السيناريو الأساسي',
|
219 |
+
'description': 'التسعير الأساسي مع هامش ربح 8%',
|
220 |
+
'total_cost': 615000,
|
221 |
+
'profit_margin': 8.2,
|
222 |
+
'total_price': 670000,
|
223 |
+
'is_active': True
|
224 |
+
},
|
225 |
+
{
|
226 |
+
'id': 2,
|
227 |
+
'name': 'سيناريو تنافسي',
|
228 |
+
'description': 'تخفيض هامش الربح للمنافسة',
|
229 |
+
'total_cost': 615000,
|
230 |
+
'profit_margin': 5.0,
|
231 |
+
'total_price': 650000,
|
232 |
+
'is_active': False
|
233 |
+
},
|
234 |
+
{
|
235 |
+
'id': 3,
|
236 |
+
'name': 'سيناريو مرتفع',
|
237 |
+
'description': 'زيادة هامش الربح للمشاريع ذات المخاطر العالية',
|
238 |
+
'total_cost': 615000,
|
239 |
+
'profit_margin': 12.0,
|
240 |
+
'total_price': 700000,
|
241 |
+
'is_active': False
|
242 |
+
}
|
243 |
+
]
|
244 |
+
|
245 |
+
def run(self):
|
246 |
+
"""تشغيل وحدة التسعير"""
|
247 |
+
# استدعاء دالة العرض
|
248 |
+
self.render()
|
249 |
+
|
250 |
+
def render(self):
|
251 |
+
"""عرض واجهة وحدة التسعير"""
|
252 |
+
|
253 |
+
st.markdown("<h1 class='module-title'>وحدة التسعير</h1>", unsafe_allow_html=True)
|
254 |
+
|
255 |
+
tabs = st.tabs([
|
256 |
+
"لوحة التحكم",
|
257 |
+
"جدول الكميات",
|
258 |
+
"تحليل التكاليف",
|
259 |
+
"سيناريوهات التسعير",
|
260 |
+
"المقارنة التنافسية",
|
261 |
+
"التقارير"
|
262 |
+
])
|
263 |
+
|
264 |
+
with tabs[0]:
|
265 |
+
self._render_dashboard_tab()
|
266 |
+
|
267 |
+
with tabs[1]:
|
268 |
+
self._render_bill_of_quantities_tab()
|
269 |
+
|
270 |
+
with tabs[2]:
|
271 |
+
self._render_cost_analysis_tab()
|
272 |
+
|
273 |
+
with tabs[3]:
|
274 |
+
self._render_pricing_scenarios_tab()
|
275 |
+
|
276 |
+
with tabs[4]:
|
277 |
+
self._render_competitive_analysis_tab()
|
278 |
+
|
279 |
+
with tabs[5]:
|
280 |
+
self._render_reports_tab()
|
281 |
+
|
282 |
+
def _render_dashboard_tab(self):
|
283 |
+
"""عرض تبويب لوحة التحكم"""
|
284 |
+
|
285 |
+
st.markdown("### لوحة تحكم التسعير")
|
286 |
+
|
287 |
+
# عرض ملخص التسعير
|
288 |
+
col1, col2, col3, col4 = st.columns(4)
|
289 |
+
|
290 |
+
# حساب إجمالي التكاليف
|
291 |
+
total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
|
292 |
+
total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
|
293 |
+
total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
|
294 |
+
total_cost = total_direct_cost + total_indirect_cost
|
295 |
+
total_price = total_cost + total_profit
|
296 |
+
|
297 |
+
with col1:
|
298 |
+
st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.0f} ريال")
|
299 |
+
|
300 |
+
with col2:
|
301 |
+
st.metric("إجمالي التكاليف غير المباشرة", f"{total_indirect_cost:,.0f} ريال")
|
302 |
+
|
303 |
+
with col3:
|
304 |
+
st.metric("إجمالي التكاليف", f"{total_cost:,.0f} ريال")
|
305 |
+
|
306 |
+
with col4:
|
307 |
+
st.metric("السعر الإجمالي", f"{total_price:,.0f} ريال")
|
308 |
+
|
309 |
+
# عرض توزيع التكاليف
|
310 |
+
st.markdown("### توزيع التكاليف")
|
311 |
+
|
312 |
+
# تجميع البيانات حسب الفئة
|
313 |
+
cost_categories = {}
|
314 |
+
|
315 |
+
for item in st.session_state.cost_analysis:
|
316 |
+
category = item['category']
|
317 |
+
if category in cost_categories:
|
318 |
+
cost_categories[category] += item['amount']
|
319 |
+
else:
|
320 |
+
cost_categories[category] = item['amount']
|
321 |
+
|
322 |
+
# إنشاء DataFrame للرسم البياني
|
323 |
+
cost_df = pd.DataFrame({
|
324 |
+
'الفئة': list(cost_categories.keys()),
|
325 |
+
'المبلغ': list(cost_categories.values())
|
326 |
+
})
|
327 |
+
|
328 |
+
# إنشاء رسم بياني دائري
|
329 |
+
fig = px.pie(
|
330 |
+
cost_df,
|
331 |
+
values='المبلغ',
|
332 |
+
names='الفئة',
|
333 |
+
title='توزيع التكاليف حسب الفئة',
|
334 |
+
color_discrete_sequence=px.colors.qualitative.Set3
|
335 |
+
)
|
336 |
+
|
337 |
+
st.plotly_chart(fig, use_container_width=True)
|
338 |
+
|
339 |
+
# عرض توزيع التكاليف المباشرة
|
340 |
+
st.markdown("### توزيع التكاليف المباشرة")
|
341 |
+
|
342 |
+
# تجميع البيانات حسب الفئة الفرعية للتكاليف المباشرة
|
343 |
+
direct_cost_subcategories = {}
|
344 |
+
|
345 |
+
for item in st.session_state.cost_analysis:
|
346 |
+
if item['category'] == 'تكاليف مباشرة':
|
347 |
+
subcategory = item['subcategory']
|
348 |
+
if subcategory in direct_cost_subcategories:
|
349 |
+
direct_cost_subcategories[subcategory] += item['amount']
|
350 |
+
else:
|
351 |
+
direct_cost_subcategories[subcategory] = item['amount']
|
352 |
+
|
353 |
+
# إنشاء DataFrame للرسم البياني
|
354 |
+
direct_cost_df = pd.DataFrame({
|
355 |
+
'الفئة الفرعية': list(direct_cost_subcategories.keys()),
|
356 |
+
'المبلغ': list(direct_cost_subcategories.values())
|
357 |
+
})
|
358 |
+
|
359 |
+
# إنشاء رسم بياني دائري
|
360 |
+
fig = px.pie(
|
361 |
+
direct_cost_df,
|
362 |
+
values='المبلغ',
|
363 |
+
names='الفئة الفرعية',
|
364 |
+
title='توزيع التكاليف المباشرة',
|
365 |
+
color_discrete_sequence=px.colors.qualitative.Pastel
|
366 |
+
)
|
367 |
+
|
368 |
+
st.plotly_chart(fig, use_container_width=True)
|
369 |
+
|
370 |
+
# عرض توزيع التكاليف غير المباشرة
|
371 |
+
st.markdown("### توزيع التكاليف غير المباشرة")
|
372 |
+
|
373 |
+
# تجميع البيانات حسب الفئة الفرعية للتكاليف غير المباشرة
|
374 |
+
indirect_cost_subcategories = {}
|
375 |
+
|
376 |
+
for item in st.session_state.cost_analysis:
|
377 |
+
if item['category'] == 'تكاليف غير مباشرة':
|
378 |
+
subcategory = item['subcategory']
|
379 |
+
if subcategory in indirect_cost_subcategories:
|
380 |
+
indirect_cost_subcategories[subcategory] += item['amount']
|
381 |
+
else:
|
382 |
+
indirect_cost_subcategories[subcategory] = item['amount']
|
383 |
+
|
384 |
+
# إنشاء DataFrame للرسم البياني
|
385 |
+
indirect_cost_df = pd.DataFrame({
|
386 |
+
'الفئة الفرعية': list(indirect_cost_subcategories.keys()),
|
387 |
+
'المبلغ': list(indirect_cost_subcategories.values())
|
388 |
+
})
|
389 |
+
|
390 |
+
# إنشاء رسم بياني دائري
|
391 |
+
fig = px.pie(
|
392 |
+
indirect_cost_df,
|
393 |
+
values='المبلغ',
|
394 |
+
names='الفئة الفرعية',
|
395 |
+
title='توزيع التكاليف غير المباشرة',
|
396 |
+
color_discrete_sequence=px.colors.qualitative.Pastel1
|
397 |
+
)
|
398 |
+
|
399 |
+
st.plotly_chart(fig, use_container_width=True)
|
400 |
+
|
401 |
+
def _render_bill_of_quantities_tab(self):
|
402 |
+
"""عرض تبويب جدول الكميات"""
|
403 |
+
|
404 |
+
st.markdown("### جدول الكميات")
|
405 |
+
|
406 |
+
# إنشاء DataFrame من بيانات جدول الكميات
|
407 |
+
boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
|
408 |
+
|
409 |
+
# عرض جدول الكميات
|
410 |
+
st.dataframe(
|
411 |
+
boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
|
412 |
+
column_config={
|
413 |
+
'code': 'الكود',
|
414 |
+
'description': 'الوصف',
|
415 |
+
'unit': 'الوحدة',
|
416 |
+
'quantity': 'الكمية',
|
417 |
+
'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
|
418 |
+
'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
|
419 |
+
'category': 'الفئة'
|
420 |
+
},
|
421 |
+
hide_index=True,
|
422 |
+
use_container_width=True
|
423 |
+
)
|
424 |
+
|
425 |
+
# إضافة بند جديد
|
426 |
+
st.markdown("### إضافة بند جديد")
|
427 |
+
|
428 |
+
col1, col2 = st.columns(2)
|
429 |
+
|
430 |
+
with col1:
|
431 |
+
new_code = st.text_input("الكود", key="new_boq_code")
|
432 |
+
new_description = st.text_input("الوصف", key="new_boq_description")
|
433 |
+
new_unit = st.selectbox("الوحدة", ["م3", "م2", "طن", "عدد", "متر طولي"], key="new_boq_unit")
|
434 |
+
|
435 |
+
with col2:
|
436 |
+
new_quantity = st.number_input("الكمية", min_value=0.0, step=1.0, key="new_boq_quantity")
|
437 |
+
new_unit_price = st.number_input("سعر الوحدة", min_value=0.0, step=10.0, key="new_boq_unit_price")
|
438 |
+
new_category = st.selectbox(
|
439 |
+
"الفئة",
|
440 |
+
[
|
441 |
+
"أعمال ترابية",
|
442 |
+
"أعمال خرسانية",
|
443 |
+
"أعمال حديد",
|
444 |
+
"أعمال بناء",
|
445 |
+
"أعمال تشطيبات",
|
446 |
+
"أعمال نجارة",
|
447 |
+
"أعمال ألمنيوم",
|
448 |
+
"أعمال كهربائية",
|
449 |
+
"أعمال ميكانيكية",
|
450 |
+
"أعمال صحية"
|
451 |
+
],
|
452 |
+
key="new_boq_category"
|
453 |
+
)
|
454 |
+
|
455 |
+
if st.button("إضافة البند", key="add_boq_item"):
|
456 |
+
if new_code and new_description and new_quantity > 0 and new_unit_price > 0:
|
457 |
+
# حساب السعر الإجمالي
|
458 |
+
new_total_price = new_quantity * new_unit_price
|
459 |
+
|
460 |
+
# إضافة بند جديد
|
461 |
+
new_id = max([item['id'] for item in st.session_state.bill_of_quantities]) + 1
|
462 |
+
|
463 |
+
st.session_state.bill_of_quantities.append({
|
464 |
+
'id': new_id,
|
465 |
+
'code': new_code,
|
466 |
+
'description': new_description,
|
467 |
+
'unit': new_unit,
|
468 |
+
'quantity': new_quantity,
|
469 |
+
'unit_price': new_unit_price,
|
470 |
+
'total_price': new_total_price,
|
471 |
+
'category': new_category
|
472 |
+
})
|
473 |
+
|
474 |
+
st.success(f"تمت إضافة البند بنجاح: {new_description}")
|
475 |
+
|
476 |
+
# تحديث الصفحة لعرض البند الجديد
|
477 |
+
st.rerun()
|
478 |
+
else:
|
479 |
+
st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
|
480 |
+
|
481 |
+
# عرض ملخص جدول الكميات (إزالة التكرار)
|
482 |
+
st.markdown("### ملخص جدول الكميات")
|
483 |
+
|
484 |
+
# تجميع البيانات حسب الفئة
|
485 |
+
category_totals = {}
|
486 |
+
for item in st.session_state.bill_of_quantities:
|
487 |
+
category = item['category']
|
488 |
+
if category in category_totals:
|
489 |
+
category_totals[category] += item['total_price']
|
490 |
+
else:
|
491 |
+
category_totals[category] = item['total_price']
|
492 |
+
|
493 |
+
# إنشاء DataFrame للرسم البياني
|
494 |
+
category_df = pd.DataFrame({
|
495 |
+
'الفئة': list(category_totals.keys()),
|
496 |
+
'المبلغ': list(category_totals.values())
|
497 |
+
})
|
498 |
+
|
499 |
+
# ترتيب البيانات تنازليًا حسب المبلغ
|
500 |
+
category_df = category_df.sort_values('المبلغ', ascending=False)
|
501 |
+
|
502 |
+
# إنشاء رسم بياني شريطي
|
503 |
+
fig = px.bar(
|
504 |
+
category_df,
|
505 |
+
x='الفئة',
|
506 |
+
y='المبلغ',
|
507 |
+
title='إجمالي تكلفة البنود حسب الفئة',
|
508 |
+
color='الفئة',
|
509 |
+
text_auto=True
|
510 |
+
)
|
511 |
+
|
512 |
+
st.plotly_chart(fig, use_container_width=True)
|
513 |
+
|
514 |
+
# حساب إجمالي جدول الكميات
|
515 |
+
total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
|
516 |
+
|
517 |
+
# يجب إضافة باقي الدوال المفقودة هنا
|
518 |
+
def _render_cost_analysis_tab(self):
|
519 |
+
"""عرض تبويب تحليل التكاليف"""
|
520 |
+
# تنفيذ هذه الدالة حسب متطلبات التطبيق
|
521 |
+
st.markdown("### تحليل التكاليف")
|
522 |
+
# محتوى مؤقت
|
523 |
+
st.info("تبويب تحليل التكاليف قيد التطوير")
|
524 |
+
|
525 |
+
def _render_pricing_scenarios_tab(self):
|
526 |
+
"""عرض تبويب سيناريوهات التسعير"""
|
527 |
+
# تنفيذ هذه الدالة حسب متطلبات التطبيق
|
528 |
+
st.markdown("### سيناريوهات التسعير")
|
529 |
+
# محتوى مؤقت
|
530 |
+
st.info("تبويب سيناريوهات التسعير قيد التطوير")
|
531 |
+
|
532 |
+
def _render_competitive_analysis_tab(self):
|
533 |
+
"""عرض تبويب المقارنة التنافسية"""
|
534 |
+
# تنفيذ هذه الدالة حسب متطلبات التطبيق
|
535 |
+
st.markdown("### المقارنة التنافسية")
|
536 |
+
# محتوى مؤقت
|
537 |
+
st.info("تبويب المقارنة التنافسية قيد التطوير")
|
538 |
+
|
539 |
+
def _render_reports_tab(self):
|
540 |
+
"""عرض تبويب التقارير"""
|
541 |
+
# تنفيذ هذه الدالة حسب متطلبات التطبيق
|
542 |
+
st.markdown("### التقارير")
|
543 |
+
# محتوى مؤقت
|
544 |
+
st.info("تبويب التقارير قيد التطوير")
|
fixed_pricing_app_complete.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
modules/ai_assistant/ai_app.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
modules/ai_assistant/assistant.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
modules/ai_assistant/contract_analyzer.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
modules/ai_assistant/data_integration.py
CHANGED
@@ -1,577 +1,577 @@
|
|
1 |
-
"""
|
2 |
-
وحدة تكامل البيانات مع الذكاء الاصطناعي
|
3 |
-
|
4 |
-
هذا الملف يحتوي على الفئات والدوال اللازمة لتكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي.
|
5 |
-
"""
|
6 |
-
|
7 |
-
import pandas as pd
|
8 |
-
import numpy as np
|
9 |
-
import matplotlib.pyplot as plt
|
10 |
-
import plotly.express as px
|
11 |
-
import plotly.graph_objects as go
|
12 |
-
from datetime import datetime
|
13 |
-
import json
|
14 |
-
import os
|
15 |
-
import sys
|
16 |
-
from pathlib import Path
|
17 |
-
|
18 |
-
# إضافة المسار للوصول إلى وحدة تحليل البيانات
|
19 |
-
current_dir = os.path.dirname(os.path.abspath(__file__))
|
20 |
-
parent_dir = os.path.dirname(os.path.dirname(current_dir))
|
21 |
-
if parent_dir not in sys.path:
|
22 |
-
sys.path.append(parent_dir)
|
23 |
-
|
24 |
-
# محاولة استيراد وحدة تحليل البيانات
|
25 |
-
try:
|
26 |
-
from modules.data_analysis.data_analysis_app import DataAnalysisApp
|
27 |
-
except ImportError:
|
28 |
-
# تعريف فئة بديلة في حالة فشل الاستيراد
|
29 |
-
class DataAnalysisApp:
|
30 |
-
def __init__(self):
|
31 |
-
pass
|
32 |
-
|
33 |
-
def run(self):
|
34 |
-
pass
|
35 |
-
|
36 |
-
class DataAIIntegration:
|
37 |
-
"""فئة تكامل البيانات مع الذكاء الاصطناعي"""
|
38 |
-
|
39 |
-
def __init__(self):
|
40 |
-
"""تهيئة فئة تكامل البيانات مع الذكاء الاصطناعي"""
|
41 |
-
self.data_analysis_app = DataAnalysisApp()
|
42 |
-
|
43 |
-
def analyze_tender_data(self, tender_data):
|
44 |
-
"""
|
45 |
-
تحليل بيانات المناقصة باستخدام الذكاء الاصطناعي
|
46 |
-
|
47 |
-
المعلمات:
|
48 |
-
tender_data (dict): بيانات المناقصة
|
49 |
-
|
50 |
-
العوائد:
|
51 |
-
dict: نتائج التحليل
|
52 |
-
"""
|
53 |
-
# تحويل البيانات إلى DataFrame
|
54 |
-
if isinstance(tender_data, dict):
|
55 |
-
df = pd.DataFrame([tender_data])
|
56 |
-
elif isinstance(tender_data, list):
|
57 |
-
df = pd.DataFrame(tender_data)
|
58 |
-
else:
|
59 |
-
df = tender_data
|
60 |
-
|
61 |
-
# تحليل البيانات
|
62 |
-
results = {
|
63 |
-
'summary': self._generate_summary(df),
|
64 |
-
'recommendations': self._generate_recommendations(df),
|
65 |
-
'risk_analysis': self._analyze_risks(df),
|
66 |
-
'cost_analysis': self._analyze_costs(df),
|
67 |
-
'competitive_analysis': self._analyze_competition(df)
|
68 |
-
}
|
69 |
-
|
70 |
-
return results
|
71 |
-
|
72 |
-
def analyze_historical_data(self, project_type=None, location=None, time_period=None):
|
73 |
-
"""
|
74 |
-
تحليل البيانات التاريخية للمناقصات
|
75 |
-
|
76 |
-
المعلمات:
|
77 |
-
project_type (str): نوع المشروع (اختياري)
|
78 |
-
location (str): الموقع (اختياري)
|
79 |
-
time_period (str): الفترة الزمنية (اختياري)
|
80 |
-
|
81 |
-
العوائد:
|
82 |
-
dict: نتائج التحليل
|
83 |
-
"""
|
84 |
-
# الحصول على البيانات التاريخية (محاكاة)
|
85 |
-
historical_data = self._get_historical_data()
|
86 |
-
|
87 |
-
# تطبيق التصفية إذا تم تحديدها
|
88 |
-
filtered_data = historical_data.copy()
|
89 |
-
|
90 |
-
if project_type:
|
91 |
-
filtered_data = filtered_data[filtered_data['نوع المشروع'] == project_type]
|
92 |
-
|
93 |
-
if location:
|
94 |
-
filtered_data = filtered_data[filtered_data['الموقع'] == location]
|
95 |
-
|
96 |
-
if time_period:
|
97 |
-
# تنفيذ تصفية الفترة الزمنية (محاكاة)
|
98 |
-
pass
|
99 |
-
|
100 |
-
# تحليل البيانات
|
101 |
-
results = {
|
102 |
-
'win_rate': self._calculate_win_rate(filtered_data),
|
103 |
-
'avg_profit_margin': self._calculate_avg_profit_margin(filtered_data),
|
104 |
-
'price_trends': self._analyze_price_trends(filtered_data),
|
105 |
-
'success_factors': self._identify_success_factors(filtered_data),
|
106 |
-
'visualizations': self._generate_visualizations(filtered_data)
|
107 |
-
}
|
108 |
-
|
109 |
-
return results
|
110 |
-
|
111 |
-
def predict_tender_success(self, tender_data):
|
112 |
-
"""
|
113 |
-
التنبؤ بفرص نجاح المناقصة
|
114 |
-
|
115 |
-
المعلمات:
|
116 |
-
tender_data (dict): بيانات المناقصة
|
117 |
-
|
118 |
-
العوائد:
|
119 |
-
dict: نتائج التنبؤ
|
120 |
-
"""
|
121 |
-
# تحويل البيانات إلى DataFrame
|
122 |
-
if isinstance(tender_data, dict):
|
123 |
-
df = pd.DataFrame([tender_data])
|
124 |
-
elif isinstance(tender_data, list):
|
125 |
-
df = pd.DataFrame(tender_data)
|
126 |
-
else:
|
127 |
-
df = tender_data
|
128 |
-
|
129 |
-
# تنفيذ التنبؤ (محاكاة)
|
130 |
-
success_probability = np.random.uniform(0, 100)
|
131 |
-
|
132 |
-
# تحديد العوامل المؤثرة (محاكاة)
|
133 |
-
factors = [
|
134 |
-
{'name': 'السعر التنافسي', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
|
135 |
-
{'name': 'الخبرة السابقة', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
|
136 |
-
{'name': 'الجودة الفنية', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
|
137 |
-
{'name': 'المدة الزمنية', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
|
138 |
-
{'name': 'المنافسة', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'}
|
139 |
-
]
|
140 |
-
|
141 |
-
# ترتيب العوامل حسب التأثير
|
142 |
-
factors = sorted(factors, key=lambda x: x['impact'], reverse=True)
|
143 |
-
|
144 |
-
# إعداد النتائج
|
145 |
-
results = {
|
146 |
-
'success_probability': success_probability,
|
147 |
-
'confidence': np.random.uniform(70, 95),
|
148 |
-
'factors': factors,
|
149 |
-
'recommendations': self._generate_success_recommendations(factors)
|
150 |
-
}
|
151 |
-
|
152 |
-
return results
|
153 |
-
|
154 |
-
def optimize_pricing(self, tender_data, competitors_data=None):
|
155 |
-
"""
|
156 |
-
تحسين التسعير للمناقصة
|
157 |
-
|
158 |
-
المعلمات:
|
159 |
-
tender_data (dict): بيانات المناقصة
|
160 |
-
competitors_data (list): بيانات المنافسين (اختياري)
|
161 |
-
|
162 |
-
العوائد:
|
163 |
-
dict: نتائج التحسين
|
164 |
-
"""
|
165 |
-
# تحويل البيانات إلى DataFrame
|
166 |
-
if isinstance(tender_data, dict):
|
167 |
-
df = pd.DataFrame([tender_data])
|
168 |
-
elif isinstance(tender_data, list):
|
169 |
-
df = pd.DataFrame(tender_data)
|
170 |
-
else:
|
171 |
-
df = tender_data
|
172 |
-
|
173 |
-
# تحليل بيانات المنافسين إذا كانت متوفرة
|
174 |
-
if competitors_data:
|
175 |
-
competitors_df = pd.DataFrame(competitors_data)
|
176 |
-
else:
|
177 |
-
# استخدام بيانات افتراضية للمنافسين
|
178 |
-
competitors_df = self._get_competitors_data()
|
179 |
-
|
180 |
-
# تنفيذ تحسين التسعير (محاكاة)
|
181 |
-
base_price = float(df['الميزانية التقديرية'].iloc[0]) if 'الميزانية التقديرية' in df.columns else 10000000
|
182 |
-
|
183 |
-
# حساب نطاق السعر المقترح
|
184 |
-
min_price = base_price * 0.85
|
185 |
-
optimal_price = base_price * 0.92
|
186 |
-
max_price = base_price * 0.98
|
187 |
-
|
188 |
-
# تحليل حساسية السعر
|
189 |
-
price_sensitivity = []
|
190 |
-
for price_factor in np.linspace(0.8, 1.1, 7):
|
191 |
-
price = base_price * price_factor
|
192 |
-
win_probability = max(0, min(100, 100 - (price_factor - 0.9) * 200))
|
193 |
-
profit = price - (base_price * 0.75)
|
194 |
-
expected_value = win_probability / 100 * profit
|
195 |
-
|
196 |
-
price_sensitivity.append({
|
197 |
-
'price_factor': price_factor,
|
198 |
-
'price': price,
|
199 |
-
'win_probability': win_probability,
|
200 |
-
'profit': profit,
|
201 |
-
'expected_value': expected_value
|
202 |
-
})
|
203 |
-
|
204 |
-
# إعداد النتائج
|
205 |
-
results = {
|
206 |
-
'min_price': min_price,
|
207 |
-
'optimal_price': optimal_price,
|
208 |
-
'max_price': max_price,
|
209 |
-
'price_sensitivity': price_sensitivity,
|
210 |
-
'market_position': self._analyze_market_position(optimal_price, competitors_df),
|
211 |
-
'recommendations': self._generate_pricing_recommendations(optimal_price, price_sensitivity)
|
212 |
-
}
|
213 |
-
|
214 |
-
return results
|
215 |
-
|
216 |
-
def analyze_dwg_files(self, file_path):
|
217 |
-
"""
|
218 |
-
تحليل ملفات DWG باستخدام الذكاء الاصطناعي
|
219 |
-
|
220 |
-
المعلمات:
|
221 |
-
file_path (str): مسار ملف DWG
|
222 |
-
|
223 |
-
العوائد:
|
224 |
-
dict: نتائج التحليل
|
225 |
-
"""
|
226 |
-
# محاكاة تحليل ملف DWG
|
227 |
-
results = {
|
228 |
-
'file_name': os.path.basename(file_path),
|
229 |
-
'file_size': f"{np.random.randint(1, 10)} MB",
|
230 |
-
'elements_count': np.random.randint(100, 1000),
|
231 |
-
'layers_count': np.random.randint(5, 20),
|
232 |
-
'dimensions': {
|
233 |
-
'width': f"{np.random.randint(10, 100)} م",
|
234 |
-
'height': f"{np.random.randint(10, 100)} م",
|
235 |
-
'area': f"{np.random.randint(100, 10000)} م²"
|
236 |
-
},
|
237 |
-
'elements': {
|
238 |
-
'walls': np.random.randint(10, 100),
|
239 |
-
'doors': np.random.randint(5, 50),
|
240 |
-
'windows': np.random.randint(5, 50),
|
241 |
-
'columns': np.random.randint(5, 50),
|
242 |
-
'stairs': np.random.randint(1, 10)
|
243 |
-
},
|
244 |
-
'materials': [
|
245 |
-
{'name': 'خرسانة', 'volume': f"{np.random.randint(10, 1000)} م³"},
|
246 |
-
{'name': 'حديد', 'weight': f"{np.random.randint(1, 100)} طن"},
|
247 |
-
{'name': 'طابوق', 'count': f"{np.random.randint(1000, 10000)} قطعة"},
|
248 |
-
{'name': 'زجاج', 'area': f"{np.random.randint(10, 1000)} م²"},
|
249 |
-
{'name': 'خشب', 'volume': f"{np.random.randint(1, 50)} م³"}
|
250 |
-
],
|
251 |
-
'cost_estimate': {
|
252 |
-
'materials': np.random.randint(100000, 1000000),
|
253 |
-
'labor': np.random.randint(50000, 500000),
|
254 |
-
'equipment': np.random.randint(10000, 100000),
|
255 |
-
'total': np.random.randint(200000, 2000000)
|
256 |
-
},
|
257 |
-
'recommendations': [
|
258 |
-
'يمكن تقليل تكلفة المواد باستخدام بدائل أقل تكلفة',
|
259 |
-
'يمكن تحسين كفاءة استخدام المساحة',
|
260 |
-
'يمكن تقليل عدد الأعمدة لتوفير التكلفة',
|
261 |
-
'يمكن تحسين تصميم السلالم لزيادة السلامة',
|
262 |
-
'يمكن تحسين توزيع النوافذ لزيادة الإضاءة الطبيعية'
|
263 |
-
]
|
264 |
-
}
|
265 |
-
|
266 |
-
return results
|
267 |
-
|
268 |
-
def integrate_with_ai_assistant(self, ai_assistant):
|
269 |
-
"""
|
270 |
-
تكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي
|
271 |
-
|
272 |
-
المعلمات:
|
273 |
-
ai_assistant: كائن وحدة الذكاء الاصطناعي
|
274 |
-
|
275 |
-
العوائد:
|
276 |
-
bool: نجاح التكامل
|
277 |
-
"""
|
278 |
-
try:
|
279 |
-
# إضافة وظائف تحليل البيانات إلى وحدة الذكاء الاصطناعي
|
280 |
-
ai_assistant.data_integration = self
|
281 |
-
|
282 |
-
# إضافة دوال التحليل إلى وحدة الذكاء الاصطناعي
|
283 |
-
ai_assistant.analyze_tender_data = self.analyze_tender_data
|
284 |
-
ai_assistant.analyze_historical_data = self.analyze_historical_data
|
285 |
-
ai_assistant.predict_tender_success = self.predict_tender_success
|
286 |
-
ai_assistant.optimize_pricing = self.optimize_pricing
|
287 |
-
ai_assistant.analyze_dwg_files = self.analyze_dwg_files
|
288 |
-
|
289 |
-
return True
|
290 |
-
except Exception as e:
|
291 |
-
print(f"خطأ في تكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي: {str(e)}")
|
292 |
-
return False
|
293 |
-
|
294 |
-
# دوال مساعدة داخلية
|
295 |
-
|
296 |
-
def _get_historical_data(self):
|
297 |
-
"""الحصول على البيانات التاريخية"""
|
298 |
-
# محاكاة البيانات التاريخية
|
299 |
-
np.random.seed(42)
|
300 |
-
|
301 |
-
n_tenders = 50
|
302 |
-
tender_ids = [f"T-{2021 + i//20}-{i%20 + 1:03d}" for i in range(n_tenders)]
|
303 |
-
tender_types = np.random.choice(["مبنى إداري", "مبنى سكني", "مدرسة", "مستشفى", "طرق", "جسور", "بنية تحتية"], n_tenders)
|
304 |
-
tender_locations = np.random.choice(["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك"], n_tenders)
|
305 |
-
tender_areas = np.random.randint(1000, 10000, n_tenders)
|
306 |
-
tender_durations = np.random.randint(6, 36, n_tenders)
|
307 |
-
tender_budgets = np.random.randint(1000000, 50000000, n_tenders)
|
308 |
-
tender_costs = np.array([budget * np.random.uniform(0.8, 1.1) for budget in tender_budgets])
|
309 |
-
tender_profits = tender_budgets - tender_costs
|
310 |
-
tender_profit_margins = tender_profits / tender_budgets * 100
|
311 |
-
tender_statuses = np.random.choice(["فائز", "خاسر", "قيد التنفيذ", "منجز"], n_tenders)
|
312 |
-
tender_dates = [f"202{1 + i//20}-{np.random.randint(1, 13):02d}-{np.random.randint(1, 29):02d}" for i in range(n_tenders)]
|
313 |
-
|
314 |
-
# إنشاء DataFrame للمناقصات السابقة
|
315 |
-
tenders_data = {
|
316 |
-
"رقم المناقصة": tender_ids,
|
317 |
-
"نوع المشروع": tender_types,
|
318 |
-
"الموقع": tender_locations,
|
319 |
-
"المساحة (م2)": tender_areas,
|
320 |
-
"المدة (شهر)": tender_durations,
|
321 |
-
"الميزانية (ريال)": tender_budgets,
|
322 |
-
"التكلفة (ريال)": tender_costs,
|
323 |
-
"الربح (ريال)": tender_profits,
|
324 |
-
"هامش الربح (%)": tender_profit_margins,
|
325 |
-
"الحالة": tender_statuses,
|
326 |
-
"تاريخ التقديم": tender_dates
|
327 |
-
}
|
328 |
-
|
329 |
-
return pd.DataFrame(tenders_data)
|
330 |
-
|
331 |
-
def _get_competitors_data(self):
|
332 |
-
"""الحصول على بيانات المنافسين"""
|
333 |
-
# محاكاة بيانات المنافسين
|
334 |
-
n_competitors = 10
|
335 |
-
competitor_ids = [f"C-{i+1:02d}" for i in range(n_competitors)]
|
336 |
-
competitor_names = [
|
337 |
-
"شركة الإنشاءات المتطورة", "شركة البناء الحديث", "شركة التطوير العمراني", "شركة الإعمار الدولية",
|
338 |
-
"شركة البنية التحتية المتكاملة", "شركة المقاولات العامة", "شركة التشييد والبناء", "شركة الهندسة والإنشاءات",
|
339 |
-
"شركة المشاريع الكبرى", "شركة التطوير العقاري"
|
340 |
-
]
|
341 |
-
competitor_specialties = np.random.choice(["مباني", "طرق", "جسور", "بنية تحتية", "متعددة"], n_competitors)
|
342 |
-
competitor_sizes = np.random.choice(["صغيرة", "متوسطة", "كبيرة"], n_competitors)
|
343 |
-
competitor_market_shares = np.random.uniform(1, 15, n_competitors)
|
344 |
-
competitor_win_rates = np.random.uniform(10, 60, n_competitors)
|
345 |
-
competitor_avg_margins = np.random.uniform(5, 20, n_competitors)
|
346 |
-
|
347 |
-
# إنشاء DataFrame للمنافسين
|
348 |
-
competitors_data = {
|
349 |
-
"رمز المنافس": competitor_ids,
|
350 |
-
"اسم المنافس": competitor_names,
|
351 |
-
"التخصص": competitor_specialties,
|
352 |
-
"الحجم": competitor_sizes,
|
353 |
-
"حصة السوق (%)": competitor_market_shares,
|
354 |
-
"معدل الفوز (%)": competitor_win_rates,
|
355 |
-
"متوسط هامش الربح (%)": competitor_avg_margins
|
356 |
-
}
|
357 |
-
|
358 |
-
return pd.DataFrame(competitors_data)
|
359 |
-
|
360 |
-
def _generate_summary(self, df):
|
361 |
-
"""توليد ملخص للبيانات"""
|
362 |
-
# محاكاة توليد ملخص
|
363 |
-
return "تحليل البيانات يشير إلى أن هذه المناقصة تتعلق بمشروع إنشائي متوسط الحجم. تتضمن المناقصة متطلبات فنية متوسطة المستوى وشروط تعاقدية معيارية. بناءً على البيانات التاريخية، هناك فرصة جيدة للفوز بهذه المناقصة إذا تم تقديم عرض تنافسي مع التركيز على الجوانب الفنية والجودة."
|
364 |
-
|
365 |
-
def _generate_recommendations(self, df):
|
366 |
-
"""توليد توصيات بناءً على البيانات"""
|
367 |
-
# محاكاة توليد توصيات
|
368 |
-
return [
|
369 |
-
"تقديم عرض سعر تنافسي يقل بنسبة 5-10% عن الميزانية التقديرية",
|
370 |
-
"التركيز على الخبرات السابقة في مشاريع مماثلة",
|
371 |
-
"تقديم حلول مبتكرة لتقليل مدة التنفيذ",
|
372 |
-
"تعزيز الجوانب الفنية في العرض",
|
373 |
-
"تقديم خطة تنفيذ مفصلة مع جدول زمني واضح"
|
374 |
-
]
|
375 |
-
|
376 |
-
def _analyze_risks(self, df):
|
377 |
-
"""تحليل المخاطر"""
|
378 |
-
# محاكاة تحليل المخاطر
|
379 |
-
return [
|
380 |
-
{"risk": "ارتفاع أسعار المواد", "probability": "متوسطة", "impact": "عالي", "mitigation": "تثبيت أسعار المواد الرئيسية مع الموردين"},
|
381 |
-
{"risk": "تأخر التنفيذ", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع خطة تنفيذ مفصلة مع هوامش زمنية"},
|
382 |
-
{"risk": "نقص العمالة الماهرة", "probability": "منخفضة", "impact": "متوسط", "mitigation": "التعاقد المسبق مع مقاولي الباطن"},
|
383 |
-
{"risk": "تغيير نطاق العمل", "probability": "متوسطة", "impact": "عالي", "mitigation": "توثيق نطاق العمل بدقة وتحديد إجراءات التغيير"},
|
384 |
-
{"risk": "مشاكل في التربة", "probability": "منخفضة", "impact": "عالي", "mitigation": "إجراء فحوصات شاملة للتربة قبل البدء"}
|
385 |
-
]
|
386 |
-
|
387 |
-
def _analyze_costs(self, df):
|
388 |
-
"""تحليل التكاليف"""
|
389 |
-
# محاكاة تحليل التكاليف
|
390 |
-
total_budget = float(df['الميزانية التقديرية'].iloc[0]) if 'الميزانية التقديرية' in df.columns else 10000000
|
391 |
-
|
392 |
-
# توزيع التكاليف
|
393 |
-
materials_cost = total_budget * 0.6
|
394 |
-
labor_cost = total_budget * 0.25
|
395 |
-
equipment_cost = total_budget * 0.1
|
396 |
-
overhead_cost = total_budget * 0.05
|
397 |
-
|
398 |
-
return {
|
399 |
-
"total_budget": total_budget,
|
400 |
-
"cost_breakdown": [
|
401 |
-
{"category": "المواد", "amount": materials_cost, "percentage": 60},
|
402 |
-
{"category": "العمالة", "amount": labor_cost, "percentage": 25},
|
403 |
-
{"category": "المعدات", "amount": equipment_cost, "percentage": 10},
|
404 |
-
{"category": "المصاريف العامة", "amount": overhead_cost, "percentage": 5}
|
405 |
-
],
|
406 |
-
"cost_saving_opportunities": [
|
407 |
-
{"item": "استخدام مواد بديلة", "potential_saving": total_budget * 0.05},
|
408 |
-
{"item": "تحسين إنتاجية العمالة", "potential_saving": total_budget * 0.03},
|
409 |
-
{"item": "تأجير المعدات بدلاً من شرائها", "potential_saving": total_budget * 0.02}
|
410 |
-
]
|
411 |
-
}
|
412 |
-
|
413 |
-
def _analyze_competition(self, df):
|
414 |
-
"""تحليل المنافسة"""
|
415 |
-
# محاكاة تحليل المنافسة
|
416 |
-
return {
|
417 |
-
"expected_competitors": [
|
418 |
-
{"name": "شركة الإنشاءات المتطورة", "strength": "خبرة طويلة في مشاريع مماثلة", "weakness": "أسعار مرتفعة", "win_probability": 30},
|
419 |
-
{"name": "شركة البناء الحديث", "strength": "أسعار تنافسية", "weakness": "خبرة محدودة", "win_probability": 25},
|
420 |
-
{"name": "شركة التطوير العمراني", "strength": "جودة عالية", "weakness": "بطء في التنفيذ", "win_probability": 20}
|
421 |
-
],
|
422 |
-
"competitive_advantages": [
|
423 |
-
"خبرة في مشاريع مماثلة",
|
424 |
-
"فريق فني متميز",
|
425 |
-
"علاقات جيدة مع الموردين",
|
426 |
-
"تقنيات حديثة في
|
427 |
-
],
|
428 |
-
"competitive_disadvantages": [
|
429 |
-
"محدودية الموارد المالية",
|
430 |
-
"قلة الخبرة في بعض الجوانب الفنية"
|
431 |
-
]
|
432 |
-
}
|
433 |
-
|
434 |
-
def _calculate_win_rate(self, df):
|
435 |
-
"""حساب معدل الفوز"""
|
436 |
-
# محاكاة حساب معدل الفوز
|
437 |
-
if 'الحالة' in df.columns:
|
438 |
-
total_tenders = len(df)
|
439 |
-
won_tenders = len(df[df['الحالة'] == 'فائز'])
|
440 |
-
win_rate = won_tenders / total_tenders * 100 if total_tenders > 0 else 0
|
441 |
-
else:
|
442 |
-
win_rate = 35 # قيمة افتراضية
|
443 |
-
|
444 |
-
return {
|
445 |
-
"overall_win_rate": win_rate,
|
446 |
-
"win_rate_by_type": [
|
447 |
-
{"type": "مبنى إداري", "win_rate": 40},
|
448 |
-
{"type": "مبنى سكني", "win_rate": 35},
|
449 |
-
{"type": "مدرسة", "win_rate": 45},
|
450 |
-
{"type": "مستشفى", "win_rate": 30},
|
451 |
-
{"type": "طرق", "win_rate": 25},
|
452 |
-
{"type": "جسور", "win_rate": 20},
|
453 |
-
{"type": "بنية تحتية", "win_rate": 30}
|
454 |
-
],
|
455 |
-
"win_rate_by_location": [
|
456 |
-
{"location": "الرياض", "win_rate": 40},
|
457 |
-
{"location": "جدة", "win_rate": 35},
|
458 |
-
{"location": "الدمام", "win_rate": 30},
|
459 |
-
{"location": "مكة", "win_rate": 25},
|
460 |
-
{"location": "المدينة", "win_rate": 30},
|
461 |
-
{"location": "أبها", "win_rate": 35},
|
462 |
-
{"location": "تبوك", "win_rate": 40}
|
463 |
-
]
|
464 |
-
}
|
465 |
-
|
466 |
-
def _calculate_avg_profit_margin(self, df):
|
467 |
-
"""حساب متوسط هامش الربح"""
|
468 |
-
# محاكاة حساب متوسط هامش الربح
|
469 |
-
if 'هامش الربح (%)' in df.columns:
|
470 |
-
avg_profit_margin = df['هامش الربح (%)'].mean()
|
471 |
-
else:
|
472 |
-
avg_profit_margin = 15 # قيمة افتراضية
|
473 |
-
|
474 |
-
return {
|
475 |
-
"overall_avg_profit_margin": avg_profit_margin,
|
476 |
-
"profit_margin_by_type": [
|
477 |
-
{"type": "مبنى إداري", "profit_margin": 18},
|
478 |
-
{"type": "مبنى سكني", "profit_margin": 15},
|
479 |
-
{"type": "مدرسة", "profit_margin": 20},
|
480 |
-
{"type": "مستشفى", "profit_margin": 12},
|
481 |
-
{"type": "طرق", "profit_margin": 10},
|
482 |
-
{"type": "جسور", "profit_margin": 8},
|
483 |
-
{"type": "بنية تحتية", "profit_margin": 14}
|
484 |
-
],
|
485 |
-
"profit_margin_by_location": [
|
486 |
-
{"location": "الرياض", "profit_margin": 16},
|
487 |
-
{"location": "جدة", "profit_margin": 14},
|
488 |
-
{"location": "الدمام", "profit_margin": 15},
|
489 |
-
{"location": "مكة", "profit_margin": 12},
|
490 |
-
{"location": "المدينة", "profit_margin": 13},
|
491 |
-
{"location": "أبها", "profit_margin": 18},
|
492 |
-
{"location": "تبوك", "profit_margin": 17}
|
493 |
-
]
|
494 |
-
}
|
495 |
-
|
496 |
-
def _analyze_price_trends(self, df):
|
497 |
-
"""تحليل اتجاهات الأسعار"""
|
498 |
-
# محاكاة تحليل اتجاهات الأسعار
|
499 |
-
return {
|
500 |
-
"price_trends_by_year": [
|
501 |
-
{"year": 2021, "avg_price_per_sqm": 3500},
|
502 |
-
{"year": 2022, "avg_price_per_sqm": 3800},
|
503 |
-
{"year": 2023, "avg_price_per_sqm": 4200},
|
504 |
-
{"year": 2024, "avg_price_per_sqm": 4500}
|
505 |
-
],
|
506 |
-
"price_trends_by_material": [
|
507 |
-
{"material": "خرسانة", "price_change": 15},
|
508 |
-
{"material": "حديد", "price_change": 20},
|
509 |
-
{"material": "أسمنت", "price_change": 10},
|
510 |
-
{"material": "طابوق", "price_change": 5},
|
511 |
-
{"material": "ألمنيوم", "price_change": 25}
|
512 |
-
],
|
513 |
-
"price_forecast": [
|
514 |
-
{"year": 2025, "forecasted_price_change": 8},
|
515 |
-
{"year": 2026, "forecasted_price_change": 5},
|
516 |
-
{"year": 2027, "forecasted_price_change": 3}
|
517 |
-
]
|
518 |
-
}
|
519 |
-
|
520 |
-
def _identify_success_factors(self, df):
|
521 |
-
"""تحديد عوامل النجاح"""
|
522 |
-
# محاكاة تحديد عوامل النجاح
|
523 |
-
return [
|
524 |
-
{"factor": "السعر التنافسي", "importance": 0.8, "description": "تقديم أسعار أقل من المنافسين بنسبة 5-10%"},
|
525 |
-
{"factor": "الجودة الفنية", "importance": 0.7, "description": "تقديم حلول فنية متميزة ومبتكرة"},
|
526 |
-
{"factor": "الخبرة السابقة", "importance": 0.6, "description": "إظهار خبرة سابقة في مشاريع مماثلة"},
|
527 |
-
{"factor": "مدة التنفيذ", "importance": 0.5, "description": "تقديم جدول زمني أقصر من المطلوب"},
|
528 |
-
{"factor": "السمعة", "importance": 0.4, "description": "سمعة جيدة في السوق وعلاقات قوية مع العملاء"}
|
529 |
-
]
|
530 |
-
|
531 |
-
def _generate_visualizations(self, df):
|
532 |
-
"""توليد الرسوم البيانية"""
|
533 |
-
# محاكاة توليد الرسوم البيانية
|
534 |
-
return {
|
535 |
-
"visualization_types": [
|
536 |
-
"توزيع المناقصات حسب النوع",
|
537 |
-
"توزيع المناقصات حسب الموقع",
|
538 |
-
"معدل الفوز حسب النوع",
|
539 |
-
"معدل الفوز حسب الموقع",
|
540 |
-
"متوسط هامش الربح حسب النوع",
|
541 |
-
"متوسط هامش الربح حسب الموقع",
|
542 |
-
"اتجاهات الأسعار عبر الزمن"
|
543 |
-
]
|
544 |
-
}
|
545 |
-
|
546 |
-
def _generate_success_recommendations(self, factors):
|
547 |
-
"""توليد توصيات لزيادة فرص النجاح"""
|
548 |
-
# محاكاة توليد توصيات
|
549 |
-
return [
|
550 |
-
"تخفيض السعر بنسبة 5-10% لزيادة التنافسية",
|
551 |
-
"تعزيز الجوانب الفنية في العرض",
|
552 |
-
"إبراز الخبرات السابقة في مشاريع مماثلة",
|
553 |
-
"تقديم جدول زمني أقصر من المطلوب",
|
554 |
-
"تقديم ضمانات إضافية للجودة"
|
555 |
-
]
|
556 |
-
|
557 |
-
def _analyze_market_position(self, price, competitors_df):
|
558 |
-
"""تحليل الموقف التنافسي في السوق"""
|
559 |
-
# محاكاة تحليل الموقف التنافسي
|
560 |
-
return {
|
561 |
-
"market_position": "متوسط",
|
562 |
-
"price_percentile": 45,
|
563 |
-
"competitors_below": 3,
|
564 |
-
"competitors_above": 7,
|
565 |
-
"price_competitiveness": "عالية"
|
566 |
-
}
|
567 |
-
|
568 |
-
def _generate_pricing_recommendations(self, optimal_price, price_sensitivity):
|
569 |
-
"""توليد توصيات التسعير"""
|
570 |
-
# محاكاة توليد توصيات التسعير
|
571 |
-
return [
|
572 |
-
f"السعر الأمثل: {optimal_price:,.0f} ريال",
|
573 |
-
"تقديم خصم إضافي للعميل المتكرر",
|
574 |
-
"تقديم خيارات دفع مرنة",
|
575 |
-
"تضمين خدمات إضافية لتعزيز القيمة",
|
576 |
-
"تقديم ضمانات إضافية لتبرير السعر"
|
577 |
-
]
|
|
|
1 |
+
"""
|
2 |
+
وحدة تكامل البيانات مع الذكاء الاصطناعي
|
3 |
+
|
4 |
+
هذا الملف يحتوي على الفئات والدوال اللازمة لتكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي.
|
5 |
+
"""
|
6 |
+
|
7 |
+
import pandas as pd
|
8 |
+
import numpy as np
|
9 |
+
import matplotlib.pyplot as plt
|
10 |
+
import plotly.express as px
|
11 |
+
import plotly.graph_objects as go
|
12 |
+
from datetime import datetime
|
13 |
+
import json
|
14 |
+
import os
|
15 |
+
import sys
|
16 |
+
from pathlib import Path
|
17 |
+
|
18 |
+
# إضافة المسار للوصول إلى وحدة تحليل البيانات
|
19 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
20 |
+
parent_dir = os.path.dirname(os.path.dirname(current_dir))
|
21 |
+
if parent_dir not in sys.path:
|
22 |
+
sys.path.append(parent_dir)
|
23 |
+
|
24 |
+
# محاولة استيراد وحدة تحليل البيانات
|
25 |
+
try:
|
26 |
+
from modules.data_analysis.data_analysis_app import DataAnalysisApp
|
27 |
+
except ImportError:
|
28 |
+
# تعريف فئة بديلة في حالة فشل الاستيراد
|
29 |
+
class DataAnalysisApp:
|
30 |
+
def __init__(self):
|
31 |
+
pass
|
32 |
+
|
33 |
+
def run(self):
|
34 |
+
pass
|
35 |
+
|
36 |
+
class DataAIIntegration:
|
37 |
+
"""فئة تكامل البيانات مع الذكاء الاصطناعي"""
|
38 |
+
|
39 |
+
def __init__(self):
|
40 |
+
"""تهيئة فئة تكامل البيانات مع الذكاء الاصطناعي"""
|
41 |
+
self.data_analysis_app = DataAnalysisApp()
|
42 |
+
|
43 |
+
def analyze_tender_data(self, tender_data):
|
44 |
+
"""
|
45 |
+
تحليل بيانات المناقصة باستخدام الذكاء الاصطناعي
|
46 |
+
|
47 |
+
المعلمات:
|
48 |
+
tender_data (dict): بيانات المناقصة
|
49 |
+
|
50 |
+
العوائد:
|
51 |
+
dict: نتائج التحليل
|
52 |
+
"""
|
53 |
+
# تحويل البيانات إلى DataFrame
|
54 |
+
if isinstance(tender_data, dict):
|
55 |
+
df = pd.DataFrame([tender_data])
|
56 |
+
elif isinstance(tender_data, list):
|
57 |
+
df = pd.DataFrame(tender_data)
|
58 |
+
else:
|
59 |
+
df = tender_data
|
60 |
+
|
61 |
+
# تحليل البيانات
|
62 |
+
results = {
|
63 |
+
'summary': self._generate_summary(df),
|
64 |
+
'recommendations': self._generate_recommendations(df),
|
65 |
+
'risk_analysis': self._analyze_risks(df),
|
66 |
+
'cost_analysis': self._analyze_costs(df),
|
67 |
+
'competitive_analysis': self._analyze_competition(df)
|
68 |
+
}
|
69 |
+
|
70 |
+
return results
|
71 |
+
|
72 |
+
def analyze_historical_data(self, project_type=None, location=None, time_period=None):
|
73 |
+
"""
|
74 |
+
تحليل البيانات التاريخية للمناقصات
|
75 |
+
|
76 |
+
المعلمات:
|
77 |
+
project_type (str): نوع المشروع (اختياري)
|
78 |
+
location (str): الموقع (اختياري)
|
79 |
+
time_period (str): الفترة الزمنية (اختياري)
|
80 |
+
|
81 |
+
العوائد:
|
82 |
+
dict: نتائج التحليل
|
83 |
+
"""
|
84 |
+
# الحصول على البيانات التاريخية (محاكاة)
|
85 |
+
historical_data = self._get_historical_data()
|
86 |
+
|
87 |
+
# تطبيق التصفية إذا تم تحديدها
|
88 |
+
filtered_data = historical_data.copy()
|
89 |
+
|
90 |
+
if project_type:
|
91 |
+
filtered_data = filtered_data[filtered_data['نوع المشروع'] == project_type]
|
92 |
+
|
93 |
+
if location:
|
94 |
+
filtered_data = filtered_data[filtered_data['الموقع'] == location]
|
95 |
+
|
96 |
+
if time_period:
|
97 |
+
# تنفيذ تصفية الفترة الزمنية (محاكاة)
|
98 |
+
pass
|
99 |
+
|
100 |
+
# تحليل البيانات
|
101 |
+
results = {
|
102 |
+
'win_rate': self._calculate_win_rate(filtered_data),
|
103 |
+
'avg_profit_margin': self._calculate_avg_profit_margin(filtered_data),
|
104 |
+
'price_trends': self._analyze_price_trends(filtered_data),
|
105 |
+
'success_factors': self._identify_success_factors(filtered_data),
|
106 |
+
'visualizations': self._generate_visualizations(filtered_data)
|
107 |
+
}
|
108 |
+
|
109 |
+
return results
|
110 |
+
|
111 |
+
def predict_tender_success(self, tender_data):
|
112 |
+
"""
|
113 |
+
التنبؤ بفرص نجاح المناقصة
|
114 |
+
|
115 |
+
المعلمات:
|
116 |
+
tender_data (dict): بيانات المناقصة
|
117 |
+
|
118 |
+
العوائد:
|
119 |
+
dict: نتائج التنبؤ
|
120 |
+
"""
|
121 |
+
# تحويل البيانات إلى DataFrame
|
122 |
+
if isinstance(tender_data, dict):
|
123 |
+
df = pd.DataFrame([tender_data])
|
124 |
+
elif isinstance(tender_data, list):
|
125 |
+
df = pd.DataFrame(tender_data)
|
126 |
+
else:
|
127 |
+
df = tender_data
|
128 |
+
|
129 |
+
# تنفيذ التنبؤ (محاكاة)
|
130 |
+
success_probability = np.random.uniform(0, 100)
|
131 |
+
|
132 |
+
# تحديد العوامل المؤثرة (محاكاة)
|
133 |
+
factors = [
|
134 |
+
{'name': 'السعر التنافسي', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
|
135 |
+
{'name': 'الخبرة السابقة', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
|
136 |
+
{'name': 'الجودة الفنية', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
|
137 |
+
{'name': 'المدة الزمنية', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'},
|
138 |
+
{'name': 'المنافسة', 'impact': np.random.uniform(0, 1), 'direction': 'إيجابي' if np.random.random() > 0.5 else 'سلبي'}
|
139 |
+
]
|
140 |
+
|
141 |
+
# ترتيب العوامل حسب التأثير
|
142 |
+
factors = sorted(factors, key=lambda x: x['impact'], reverse=True)
|
143 |
+
|
144 |
+
# إعداد النتائج
|
145 |
+
results = {
|
146 |
+
'success_probability': success_probability,
|
147 |
+
'confidence': np.random.uniform(70, 95),
|
148 |
+
'factors': factors,
|
149 |
+
'recommendations': self._generate_success_recommendations(factors)
|
150 |
+
}
|
151 |
+
|
152 |
+
return results
|
153 |
+
|
154 |
+
def optimize_pricing(self, tender_data, competitors_data=None):
|
155 |
+
"""
|
156 |
+
تحسين التسعير للمناقصة
|
157 |
+
|
158 |
+
المعلمات:
|
159 |
+
tender_data (dict): بيانات المناقصة
|
160 |
+
competitors_data (list): بيانات المنافسين (اختياري)
|
161 |
+
|
162 |
+
العوائد:
|
163 |
+
dict: نتائج التحسين
|
164 |
+
"""
|
165 |
+
# تحويل البيانات إلى DataFrame
|
166 |
+
if isinstance(tender_data, dict):
|
167 |
+
df = pd.DataFrame([tender_data])
|
168 |
+
elif isinstance(tender_data, list):
|
169 |
+
df = pd.DataFrame(tender_data)
|
170 |
+
else:
|
171 |
+
df = tender_data
|
172 |
+
|
173 |
+
# تحليل بيانات المنافسين إذا كانت متوفرة
|
174 |
+
if competitors_data:
|
175 |
+
competitors_df = pd.DataFrame(competitors_data)
|
176 |
+
else:
|
177 |
+
# استخدام بيانات افتراضية للمنافسين
|
178 |
+
competitors_df = self._get_competitors_data()
|
179 |
+
|
180 |
+
# تنفيذ تحسين التسعير (محاكاة)
|
181 |
+
base_price = float(df['الميزانية التقديرية'].iloc[0]) if 'الميزانية التقديرية' in df.columns else 10000000
|
182 |
+
|
183 |
+
# حساب نطاق السعر المقترح
|
184 |
+
min_price = base_price * 0.85
|
185 |
+
optimal_price = base_price * 0.92
|
186 |
+
max_price = base_price * 0.98
|
187 |
+
|
188 |
+
# تحليل حساسية السعر
|
189 |
+
price_sensitivity = []
|
190 |
+
for price_factor in np.linspace(0.8, 1.1, 7):
|
191 |
+
price = base_price * price_factor
|
192 |
+
win_probability = max(0, min(100, 100 - (price_factor - 0.9) * 200))
|
193 |
+
profit = price - (base_price * 0.75)
|
194 |
+
expected_value = win_probability / 100 * profit
|
195 |
+
|
196 |
+
price_sensitivity.append({
|
197 |
+
'price_factor': price_factor,
|
198 |
+
'price': price,
|
199 |
+
'win_probability': win_probability,
|
200 |
+
'profit': profit,
|
201 |
+
'expected_value': expected_value
|
202 |
+
})
|
203 |
+
|
204 |
+
# إعداد النتائج
|
205 |
+
results = {
|
206 |
+
'min_price': min_price,
|
207 |
+
'optimal_price': optimal_price,
|
208 |
+
'max_price': max_price,
|
209 |
+
'price_sensitivity': price_sensitivity,
|
210 |
+
'market_position': self._analyze_market_position(optimal_price, competitors_df),
|
211 |
+
'recommendations': self._generate_pricing_recommendations(optimal_price, price_sensitivity)
|
212 |
+
}
|
213 |
+
|
214 |
+
return results
|
215 |
+
|
216 |
+
def analyze_dwg_files(self, file_path):
|
217 |
+
"""
|
218 |
+
تحليل ملفات DWG باستخدام الذكاء الاصطناعي
|
219 |
+
|
220 |
+
المعلمات:
|
221 |
+
file_path (str): مسار ملف DWG
|
222 |
+
|
223 |
+
العوائد:
|
224 |
+
dict: نتائج التحليل
|
225 |
+
"""
|
226 |
+
# محاكاة تحليل ملف DWG
|
227 |
+
results = {
|
228 |
+
'file_name': os.path.basename(file_path),
|
229 |
+
'file_size': f"{np.random.randint(1, 10)} MB",
|
230 |
+
'elements_count': np.random.randint(100, 1000),
|
231 |
+
'layers_count': np.random.randint(5, 20),
|
232 |
+
'dimensions': {
|
233 |
+
'width': f"{np.random.randint(10, 100)} م",
|
234 |
+
'height': f"{np.random.randint(10, 100)} م",
|
235 |
+
'area': f"{np.random.randint(100, 10000)} م²"
|
236 |
+
},
|
237 |
+
'elements': {
|
238 |
+
'walls': np.random.randint(10, 100),
|
239 |
+
'doors': np.random.randint(5, 50),
|
240 |
+
'windows': np.random.randint(5, 50),
|
241 |
+
'columns': np.random.randint(5, 50),
|
242 |
+
'stairs': np.random.randint(1, 10)
|
243 |
+
},
|
244 |
+
'materials': [
|
245 |
+
{'name': 'خرسانة', 'volume': f"{np.random.randint(10, 1000)} م³"},
|
246 |
+
{'name': 'حديد', 'weight': f"{np.random.randint(1, 100)} طن"},
|
247 |
+
{'name': 'طابوق', 'count': f"{np.random.randint(1000, 10000)} قطعة"},
|
248 |
+
{'name': 'زجاج', 'area': f"{np.random.randint(10, 1000)} م²"},
|
249 |
+
{'name': 'خشب', 'volume': f"{np.random.randint(1, 50)} م³"}
|
250 |
+
],
|
251 |
+
'cost_estimate': {
|
252 |
+
'materials': np.random.randint(100000, 1000000),
|
253 |
+
'labor': np.random.randint(50000, 500000),
|
254 |
+
'equipment': np.random.randint(10000, 100000),
|
255 |
+
'total': np.random.randint(200000, 2000000)
|
256 |
+
},
|
257 |
+
'recommendations': [
|
258 |
+
'يمكن تقليل تكلفة المواد باستخدام بدائل أقل تكلفة',
|
259 |
+
'يمكن تحسين كفاءة استخدام المساحة',
|
260 |
+
'يمكن تقليل عدد الأعمدة لتوفير التكلفة',
|
261 |
+
'يمكن تحسين تصميم السلالم لزيادة السلامة',
|
262 |
+
'يمكن تحسين توزيع النوافذ لزيادة الإضاءة الطبيعية'
|
263 |
+
]
|
264 |
+
}
|
265 |
+
|
266 |
+
return results
|
267 |
+
|
268 |
+
def integrate_with_ai_assistant(self, ai_assistant):
|
269 |
+
"""
|
270 |
+
تكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي
|
271 |
+
|
272 |
+
المعلمات:
|
273 |
+
ai_assistant: كائن وحدة الذكاء الاصطناعي
|
274 |
+
|
275 |
+
العوائد:
|
276 |
+
bool: نجاح التكامل
|
277 |
+
"""
|
278 |
+
try:
|
279 |
+
# إضافة وظائف تحليل البيانات إلى وحدة الذكاء الاصطناعي
|
280 |
+
ai_assistant.data_integration = self
|
281 |
+
|
282 |
+
# إضافة دوال التحليل إلى وحدة الذكاء الاصطناعي
|
283 |
+
ai_assistant.analyze_tender_data = self.analyze_tender_data
|
284 |
+
ai_assistant.analyze_historical_data = self.analyze_historical_data
|
285 |
+
ai_assistant.predict_tender_success = self.predict_tender_success
|
286 |
+
ai_assistant.optimize_pricing = self.optimize_pricing
|
287 |
+
ai_assistant.analyze_dwg_files = self.analyze_dwg_files
|
288 |
+
|
289 |
+
return True
|
290 |
+
except Exception as e:
|
291 |
+
print(f"خطأ في تكامل وحدة تحليل البيانات مع وحدة الذكاء الاصطناعي: {str(e)}")
|
292 |
+
return False
|
293 |
+
|
294 |
+
# دوال مساعدة داخلية
|
295 |
+
|
296 |
+
def _get_historical_data(self):
|
297 |
+
"""الحصول على البيانات التاريخية"""
|
298 |
+
# محاكاة البيانات التاريخية
|
299 |
+
np.random.seed(42)
|
300 |
+
|
301 |
+
n_tenders = 50
|
302 |
+
tender_ids = [f"T-{2021 + i//20}-{i%20 + 1:03d}" for i in range(n_tenders)]
|
303 |
+
tender_types = np.random.choice(["مبنى إداري", "مبنى سكني", "مدرسة", "مستشفى", "طرق", "جسور", "بنية تحتية"], n_tenders)
|
304 |
+
tender_locations = np.random.choice(["الرياض", "جدة", "الدمام", "مكة", "المدينة", "أبها", "تبوك"], n_tenders)
|
305 |
+
tender_areas = np.random.randint(1000, 10000, n_tenders)
|
306 |
+
tender_durations = np.random.randint(6, 36, n_tenders)
|
307 |
+
tender_budgets = np.random.randint(1000000, 50000000, n_tenders)
|
308 |
+
tender_costs = np.array([budget * np.random.uniform(0.8, 1.1) for budget in tender_budgets])
|
309 |
+
tender_profits = tender_budgets - tender_costs
|
310 |
+
tender_profit_margins = tender_profits / tender_budgets * 100
|
311 |
+
tender_statuses = np.random.choice(["فائز", "خاسر", "قيد التنفيذ", "منجز"], n_tenders)
|
312 |
+
tender_dates = [f"202{1 + i//20}-{np.random.randint(1, 13):02d}-{np.random.randint(1, 29):02d}" for i in range(n_tenders)]
|
313 |
+
|
314 |
+
# إنشاء DataFrame للمناقصات السابقة
|
315 |
+
tenders_data = {
|
316 |
+
"رقم المناقصة": tender_ids,
|
317 |
+
"نوع المشروع": tender_types,
|
318 |
+
"الموقع": tender_locations,
|
319 |
+
"المساحة (م2)": tender_areas,
|
320 |
+
"المدة (شهر)": tender_durations,
|
321 |
+
"الميزانية (ريال)": tender_budgets,
|
322 |
+
"التكلفة (ريال)": tender_costs,
|
323 |
+
"الربح (ريال)": tender_profits,
|
324 |
+
"هامش الربح (%)": tender_profit_margins,
|
325 |
+
"الحالة": tender_statuses,
|
326 |
+
"تاريخ التقديم": tender_dates
|
327 |
+
}
|
328 |
+
|
329 |
+
return pd.DataFrame(tenders_data)
|
330 |
+
|
331 |
+
def _get_competitors_data(self):
|
332 |
+
"""الحصول على بيانات المنافسين"""
|
333 |
+
# محاكاة بيانات المنافسين
|
334 |
+
n_competitors = 10
|
335 |
+
competitor_ids = [f"C-{i+1:02d}" for i in range(n_competitors)]
|
336 |
+
competitor_names = [
|
337 |
+
"شركة الإنشاءات المتطورة", "شركة البناء الحديث", "شركة التطوير العمراني", "شركة الإعمار الدولية",
|
338 |
+
"شركة البنية التحتية المتكاملة", "شركة المقاولات العامة", "شركة التشييد والبناء", "شركة الهندسة والإنشاءات",
|
339 |
+
"شركة المشاريع الكبرى", "شركة التطوير العقاري"
|
340 |
+
]
|
341 |
+
competitor_specialties = np.random.choice(["مباني", "طرق", "جسور", "بنية تحتية", "متعددة"], n_competitors)
|
342 |
+
competitor_sizes = np.random.choice(["صغيرة", "متوسطة", "كبيرة"], n_competitors)
|
343 |
+
competitor_market_shares = np.random.uniform(1, 15, n_competitors)
|
344 |
+
competitor_win_rates = np.random.uniform(10, 60, n_competitors)
|
345 |
+
competitor_avg_margins = np.random.uniform(5, 20, n_competitors)
|
346 |
+
|
347 |
+
# إنشاء DataFrame للمنافسين
|
348 |
+
competitors_data = {
|
349 |
+
"رمز المنافس": competitor_ids,
|
350 |
+
"اسم المنافس": competitor_names,
|
351 |
+
"التخصص": competitor_specialties,
|
352 |
+
"الحجم": competitor_sizes,
|
353 |
+
"حصة السوق (%)": competitor_market_shares,
|
354 |
+
"معدل الفوز (%)": competitor_win_rates,
|
355 |
+
"متوسط هامش الربح (%)": competitor_avg_margins
|
356 |
+
}
|
357 |
+
|
358 |
+
return pd.DataFrame(competitors_data)
|
359 |
+
|
360 |
+
def _generate_summary(self, df):
|
361 |
+
"""توليد ملخص للبيانات"""
|
362 |
+
# محاكاة توليد ملخص
|
363 |
+
return "تحليل البيانات يشير إلى أن هذه المناقصة تتعلق بمشروع إنشائي متوسط الحجم. تتضمن المناقصة متطلبات فنية متوسطة المستوى وشروط تعاقدية معيارية. بناءً على البيانات التاريخية، هناك فرصة جيدة للفوز بهذه المناقصة إذا تم تقديم عرض تنافسي مع التركيز على الجوانب الفنية والجودة."
|
364 |
+
|
365 |
+
def _generate_recommendations(self, df):
|
366 |
+
"""توليد توصيات بناءً على البيانات"""
|
367 |
+
# محاكاة توليد توصيات
|
368 |
+
return [
|
369 |
+
"تقديم عرض سعر تنافسي يقل بنسبة 5-10% عن الميزانية التقديرية",
|
370 |
+
"التركيز على الخبرات السابقة في مشاريع مماثلة",
|
371 |
+
"تقديم حلول مبتكرة لتقليل مدة التنفيذ",
|
372 |
+
"تعزيز الجوانب الفنية في العرض",
|
373 |
+
"تقديم خطة تنفيذ مفصلة مع جدول زمني واضح"
|
374 |
+
]
|
375 |
+
|
376 |
+
def _analyze_risks(self, df):
|
377 |
+
"""تحليل المخاطر"""
|
378 |
+
# محاكاة تحليل المخاطر
|
379 |
+
return [
|
380 |
+
{"risk": "ارتفاع أسعار المواد", "probability": "متوسطة", "impact": "عالي", "mitigation": "تثبيت أسعار المواد الرئيسية مع الموردين"},
|
381 |
+
{"risk": "تأخر التنفيذ", "probability": "متوسطة", "impact": "عالي", "mitigation": "وضع خطة تنفيذ مفصلة مع هوامش زمنية"},
|
382 |
+
{"risk": "نقص العمالة الماهرة", "probability": "منخفضة", "impact": "متوسط", "mitigation": "التعاقد المسبق مع مقاولي الباطن"},
|
383 |
+
{"risk": "تغيير نطاق العمل", "probability": "متوسطة", "impact": "عالي", "mitigation": "توثيق نطاق العمل بدقة وتحديد إجراءات التغيير"},
|
384 |
+
{"risk": "مشاكل في التربة", "probability": "منخفضة", "impact": "عالي", "mitigation": "إجراء فحوصات شاملة للتربة قبل البدء"}
|
385 |
+
]
|
386 |
+
|
387 |
+
def _analyze_costs(self, df):
|
388 |
+
"""تحليل التكاليف"""
|
389 |
+
# محاكاة تحليل التكاليف
|
390 |
+
total_budget = float(df['الميزانية التقديرية'].iloc[0]) if 'الميزانية التقديرية' in df.columns else 10000000
|
391 |
+
|
392 |
+
# توزيع التكاليف
|
393 |
+
materials_cost = total_budget * 0.6
|
394 |
+
labor_cost = total_budget * 0.25
|
395 |
+
equipment_cost = total_budget * 0.1
|
396 |
+
overhead_cost = total_budget * 0.05
|
397 |
+
|
398 |
+
return {
|
399 |
+
"total_budget": total_budget,
|
400 |
+
"cost_breakdown": [
|
401 |
+
{"category": "المواد", "amount": materials_cost, "percentage": 60},
|
402 |
+
{"category": "العمالة", "amount": labor_cost, "percentage": 25},
|
403 |
+
{"category": "المعدات", "amount": equipment_cost, "percentage": 10},
|
404 |
+
{"category": "المصاريف العامة", "amount": overhead_cost, "percentage": 5}
|
405 |
+
],
|
406 |
+
"cost_saving_opportunities": [
|
407 |
+
{"item": "استخدام مواد بديلة", "potential_saving": total_budget * 0.05},
|
408 |
+
{"item": "تحسين إنتاجية العمالة", "potential_saving": total_budget * 0.03},
|
409 |
+
{"item": "تأجير المعدات بدلاً من شرائها", "potential_saving": total_budget * 0.02}
|
410 |
+
]
|
411 |
+
}
|
412 |
+
|
413 |
+
def _analyze_competition(self, df):
|
414 |
+
"""تحليل المنافسة"""
|
415 |
+
# محاكاة تحليل المنافسة
|
416 |
+
return {
|
417 |
+
"expected_competitors": [
|
418 |
+
{"name": "شركة الإنشاءات المتطورة", "strength": "خبرة طويلة في مشاريع مماثلة", "weakness": "أسعار مرتفعة", "win_probability": 30},
|
419 |
+
{"name": "شركة البناء الحديث", "strength": "أسعار تنافسية", "weakness": "خبرة محدودة", "win_probability": 25},
|
420 |
+
{"name": "شركة التطوير العمراني", "strength": "جودة عالية", "weakness": "بطء في التنفيذ", "win_probability": 20}
|
421 |
+
],
|
422 |
+
"competitive_advantages": [
|
423 |
+
"خبرة في مشاريع مماثلة",
|
424 |
+
"فريق فني متميز",
|
425 |
+
"علاقات جيدة مع الموردين",
|
426 |
+
"تقنيات حديثة في التنفي��"
|
427 |
+
],
|
428 |
+
"competitive_disadvantages": [
|
429 |
+
"محدودية الموارد المالية",
|
430 |
+
"قلة الخبرة في بعض الجوانب الفنية"
|
431 |
+
]
|
432 |
+
}
|
433 |
+
|
434 |
+
def _calculate_win_rate(self, df):
|
435 |
+
"""حساب معدل الفوز"""
|
436 |
+
# محاكاة حساب معدل الفوز
|
437 |
+
if 'الحالة' in df.columns:
|
438 |
+
total_tenders = len(df)
|
439 |
+
won_tenders = len(df[df['الحالة'] == 'فائز'])
|
440 |
+
win_rate = won_tenders / total_tenders * 100 if total_tenders > 0 else 0
|
441 |
+
else:
|
442 |
+
win_rate = 35 # قيمة افتراضية
|
443 |
+
|
444 |
+
return {
|
445 |
+
"overall_win_rate": win_rate,
|
446 |
+
"win_rate_by_type": [
|
447 |
+
{"type": "مبنى إداري", "win_rate": 40},
|
448 |
+
{"type": "مبنى سكني", "win_rate": 35},
|
449 |
+
{"type": "مدرسة", "win_rate": 45},
|
450 |
+
{"type": "مستشفى", "win_rate": 30},
|
451 |
+
{"type": "طرق", "win_rate": 25},
|
452 |
+
{"type": "جسور", "win_rate": 20},
|
453 |
+
{"type": "بنية تحتية", "win_rate": 30}
|
454 |
+
],
|
455 |
+
"win_rate_by_location": [
|
456 |
+
{"location": "الرياض", "win_rate": 40},
|
457 |
+
{"location": "جدة", "win_rate": 35},
|
458 |
+
{"location": "الدمام", "win_rate": 30},
|
459 |
+
{"location": "مكة", "win_rate": 25},
|
460 |
+
{"location": "المدينة", "win_rate": 30},
|
461 |
+
{"location": "أبها", "win_rate": 35},
|
462 |
+
{"location": "تبوك", "win_rate": 40}
|
463 |
+
]
|
464 |
+
}
|
465 |
+
|
466 |
+
def _calculate_avg_profit_margin(self, df):
|
467 |
+
"""حساب متوسط هامش الربح"""
|
468 |
+
# محاكاة حساب متوسط هامش الربح
|
469 |
+
if 'هامش الربح (%)' in df.columns:
|
470 |
+
avg_profit_margin = df['هامش الربح (%)'].mean()
|
471 |
+
else:
|
472 |
+
avg_profit_margin = 15 # قيمة افتراضية
|
473 |
+
|
474 |
+
return {
|
475 |
+
"overall_avg_profit_margin": avg_profit_margin,
|
476 |
+
"profit_margin_by_type": [
|
477 |
+
{"type": "مبنى إداري", "profit_margin": 18},
|
478 |
+
{"type": "مبنى سكني", "profit_margin": 15},
|
479 |
+
{"type": "مدرسة", "profit_margin": 20},
|
480 |
+
{"type": "مستشفى", "profit_margin": 12},
|
481 |
+
{"type": "طرق", "profit_margin": 10},
|
482 |
+
{"type": "جسور", "profit_margin": 8},
|
483 |
+
{"type": "بنية تحتية", "profit_margin": 14}
|
484 |
+
],
|
485 |
+
"profit_margin_by_location": [
|
486 |
+
{"location": "الرياض", "profit_margin": 16},
|
487 |
+
{"location": "جدة", "profit_margin": 14},
|
488 |
+
{"location": "الدمام", "profit_margin": 15},
|
489 |
+
{"location": "مكة", "profit_margin": 12},
|
490 |
+
{"location": "المدينة", "profit_margin": 13},
|
491 |
+
{"location": "أبها", "profit_margin": 18},
|
492 |
+
{"location": "تبوك", "profit_margin": 17}
|
493 |
+
]
|
494 |
+
}
|
495 |
+
|
496 |
+
def _analyze_price_trends(self, df):
|
497 |
+
"""تحليل اتجاهات الأسعار"""
|
498 |
+
# محاكاة تحليل اتجاهات الأسعار
|
499 |
+
return {
|
500 |
+
"price_trends_by_year": [
|
501 |
+
{"year": 2021, "avg_price_per_sqm": 3500},
|
502 |
+
{"year": 2022, "avg_price_per_sqm": 3800},
|
503 |
+
{"year": 2023, "avg_price_per_sqm": 4200},
|
504 |
+
{"year": 2024, "avg_price_per_sqm": 4500}
|
505 |
+
],
|
506 |
+
"price_trends_by_material": [
|
507 |
+
{"material": "خرسانة", "price_change": 15},
|
508 |
+
{"material": "حديد", "price_change": 20},
|
509 |
+
{"material": "أسمنت", "price_change": 10},
|
510 |
+
{"material": "طابوق", "price_change": 5},
|
511 |
+
{"material": "ألمنيوم", "price_change": 25}
|
512 |
+
],
|
513 |
+
"price_forecast": [
|
514 |
+
{"year": 2025, "forecasted_price_change": 8},
|
515 |
+
{"year": 2026, "forecasted_price_change": 5},
|
516 |
+
{"year": 2027, "forecasted_price_change": 3}
|
517 |
+
]
|
518 |
+
}
|
519 |
+
|
520 |
+
def _identify_success_factors(self, df):
|
521 |
+
"""تحديد عوامل النجاح"""
|
522 |
+
# محاكاة تحديد عوامل النجاح
|
523 |
+
return [
|
524 |
+
{"factor": "السعر التنافسي", "importance": 0.8, "description": "تقديم أسعار أقل من المنافسين بنسبة 5-10%"},
|
525 |
+
{"factor": "الجودة الفنية", "importance": 0.7, "description": "تقديم حلول فنية متميزة ومبتكرة"},
|
526 |
+
{"factor": "الخبرة السابقة", "importance": 0.6, "description": "إظهار خبرة سابقة في مشاريع مماثلة"},
|
527 |
+
{"factor": "مدة التنفيذ", "importance": 0.5, "description": "تقديم جدول زمني أقصر من المطلوب"},
|
528 |
+
{"factor": "السمعة", "importance": 0.4, "description": "سمعة جيدة في السوق وعلاقات قوية مع العملاء"}
|
529 |
+
]
|
530 |
+
|
531 |
+
def _generate_visualizations(self, df):
|
532 |
+
"""توليد الرسوم البيانية"""
|
533 |
+
# محاكاة توليد الرسوم البيانية
|
534 |
+
return {
|
535 |
+
"visualization_types": [
|
536 |
+
"توزيع المناقصات حسب النوع",
|
537 |
+
"توزيع المناقصات حسب الموقع",
|
538 |
+
"معدل الفوز حسب النوع",
|
539 |
+
"معدل الفوز حسب الموقع",
|
540 |
+
"متوسط هامش الربح حسب النوع",
|
541 |
+
"متوسط هامش الربح حسب الموقع",
|
542 |
+
"اتجاهات الأسعار عبر الزمن"
|
543 |
+
]
|
544 |
+
}
|
545 |
+
|
546 |
+
def _generate_success_recommendations(self, factors):
|
547 |
+
"""توليد توصيات لزيادة فرص النجاح"""
|
548 |
+
# محاكاة توليد توصيات
|
549 |
+
return [
|
550 |
+
"تخفيض السعر بنسبة 5-10% لزيادة التنافسية",
|
551 |
+
"تعزيز الجوانب الفنية في العرض",
|
552 |
+
"إبراز الخبرات السابقة في مشاريع مماثلة",
|
553 |
+
"تقديم جدول زمني أقصر من المطلوب",
|
554 |
+
"تقديم ضمانات إضافية للجودة"
|
555 |
+
]
|
556 |
+
|
557 |
+
def _analyze_market_position(self, price, competitors_df):
|
558 |
+
"""تحليل الموقف التنافسي في السوق"""
|
559 |
+
# محاكاة تحليل الموقف التنافسي
|
560 |
+
return {
|
561 |
+
"market_position": "متوسط",
|
562 |
+
"price_percentile": 45,
|
563 |
+
"competitors_below": 3,
|
564 |
+
"competitors_above": 7,
|
565 |
+
"price_competitiveness": "عالية"
|
566 |
+
}
|
567 |
+
|
568 |
+
def _generate_pricing_recommendations(self, optimal_price, price_sensitivity):
|
569 |
+
"""توليد توصيات التسعير"""
|
570 |
+
# محاكاة توليد توصيات التسعير
|
571 |
+
return [
|
572 |
+
f"السعر الأمثل: {optimal_price:,.0f} ريال",
|
573 |
+
"تقديم خصم إضافي للعميل المتكرر",
|
574 |
+
"تقديم خيارات دفع مرنة",
|
575 |
+
"تضمين خدمات إضافية لتعزيز القيمة",
|
576 |
+
"تقديم ضمانات إضافية لتبرير السعر"
|
577 |
+
]
|
modules/ai_assistant/document_analyzer.py
CHANGED
@@ -1,507 +1,507 @@
|
|
1 |
-
# -*- coding: utf-8 -*-
|
2 |
-
"""
|
3 |
-
وحدة تحليل المستندات المتقدمة
|
4 |
-
|
5 |
-
هذا الملف يحتوي على الفئات المسؤولة عن تحليل المستندات بشكل احترافي
|
6 |
-
باستخدام تقنيات الذكاء الاصطناعي المتقدمة.
|
7 |
-
"""
|
8 |
-
|
9 |
-
# استيراد المكتبات القياسية
|
10 |
-
import os
|
11 |
-
import sys
|
12 |
-
import logging
|
13 |
-
import base64
|
14 |
-
import json
|
15 |
-
import time
|
16 |
-
from io import BytesIO
|
17 |
-
from pathlib import Path
|
18 |
-
from urllib.parse import urlparse
|
19 |
-
from tempfile import NamedTemporaryFile
|
20 |
-
|
21 |
-
# استيراد مكتبة Streamlit
|
22 |
-
import streamlit as st
|
23 |
-
|
24 |
-
# استيراد المكتبات الإضافية
|
25 |
-
import requests
|
26 |
-
from PIL import Image
|
27 |
-
import pandas as pd
|
28 |
-
import numpy as np
|
29 |
-
|
30 |
-
# تكوين التسجيل
|
31 |
-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
32 |
-
logger = logging.getLogger(__name__)
|
33 |
-
|
34 |
-
try:
|
35 |
-
# استيراد مكتبة pdf2image للتعامل مع ملفات PDF
|
36 |
-
from pdf2image import convert_from_path
|
37 |
-
pdf_conversion_available = True
|
38 |
-
except ImportError:
|
39 |
-
pdf_conversion_available = False
|
40 |
-
logger.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
|
41 |
-
|
42 |
-
|
43 |
-
class TextExtractor:
|
44 |
-
"""فئة استخراج النصوص من المستندات"""
|
45 |
-
|
46 |
-
def __init__(self, config=None):
|
47 |
-
"""تهيئة مستخرج النصوص"""
|
48 |
-
self.config = config or {}
|
49 |
-
|
50 |
-
def extract_from_pdf(self, file_path):
|
51 |
-
"""استخراج النص من ملف PDF"""
|
52 |
-
try:
|
53 |
-
# محاكاة استخراج النص من PDF
|
54 |
-
#
|
55 |
-
return f"تم استخراج النص من ملف PDF: {file_path}"
|
56 |
-
except Exception as e:
|
57 |
-
logger.error(f"خطأ في استخراج النص من PDF: {str(e)}")
|
58 |
-
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
59 |
-
|
60 |
-
def extract_from_docx(self, file_path):
|
61 |
-
"""استخراج النص من ملف DOCX"""
|
62 |
-
try:
|
63 |
-
# محاكاة استخراج النص من DOCX
|
64 |
-
# في التطبيق الحقيقي، يمكن استخدام مكتبة python-docx
|
65 |
-
return f"تم استخراج النص من ملف DOCX: {file_path}"
|
66 |
-
except Exception as e:
|
67 |
-
logger.error(f"خطأ في استخراج النص من DOCX: {str(e)}")
|
68 |
-
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
69 |
-
|
70 |
-
def extract_from_image(self, file_path):
|
71 |
-
"""استخراج النص من صورة باستخدام OCR"""
|
72 |
-
try:
|
73 |
-
# محاكاة استخراج النص من صورة
|
74 |
-
# في التطبيق الحقيقي، يمكن استخدام مكتبة pytesseract
|
75 |
-
return f"تم استخراج النص من صورة: {file_path}"
|
76 |
-
except Exception as e:
|
77 |
-
logger.error(f"خطأ في استخراج النص من صورة: {str(e)}")
|
78 |
-
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
79 |
-
|
80 |
-
def extract(self, file_path):
|
81 |
-
"""استخراج النص من ملف بناءً على نوعه"""
|
82 |
-
_, ext = os.path.splitext(file_path)
|
83 |
-
ext = ext.lower()
|
84 |
-
|
85 |
-
if ext == '.pdf':
|
86 |
-
return self.extract_from_pdf(file_path)
|
87 |
-
elif ext in ('.doc', '.docx'):
|
88 |
-
return self.extract_from_docx(file_path)
|
89 |
-
elif ext in ('.jpg', '.jpeg', '.png'):
|
90 |
-
return self.extract_from_image(file_path)
|
91 |
-
else:
|
92 |
-
return "نوع ملف غير مدعوم"
|
93 |
-
|
94 |
-
|
95 |
-
class ItemExtractor:
|
96 |
-
"""فئة استخراج العناصر من المستندات"""
|
97 |
-
|
98 |
-
def __init__(self, config=None):
|
99 |
-
"""تهيئة مستخرج العناصر"""
|
100 |
-
self.config = config or {}
|
101 |
-
|
102 |
-
def extract_tables(self, document):
|
103 |
-
"""استخراج الجداول من المستند"""
|
104 |
-
try:
|
105 |
-
# محاكاة استخراج الجداول
|
106 |
-
# في التطبيق الحقيقي، يمكن استخدام مكتبات مثل camelot-py أو tabula-py
|
107 |
-
return [
|
108 |
-
{
|
109 |
-
"عنوان": "جدول البنود والكميات",
|
110 |
-
"بيانات": [
|
111 |
-
{"البند": "أعمال الحفر", "الكمية": 1000, "الوحدة": "م³", "السعر": 50, "الإجمالي": 50000},
|
112 |
-
{"البند": "أعمال الخرسانة", "الكمية": 500, "الوحدة": "م³", "السعر": 300, "الإجمالي": 150000},
|
113 |
-
{"البند": "أعمال التشطيبات", "الكمية": 2000, "الوحدة": "م²", "السعر": 100, "الإجمالي": 200000}
|
114 |
-
]
|
115 |
-
},
|
116 |
-
{
|
117 |
-
"عنوان": "جدول الجدول الزمني",
|
118 |
-
"بيانات": [
|
119 |
-
{"المرحلة": "التصميم", "المدة": "30 يوم", "تاريخ البدء": "2025-04-01", "تاريخ الانتهاء": "2025-04-30"},
|
120 |
-
{"المرحلة": "الإنشاء", "المدة": "180 يوم", "تاريخ البدء": "2025-05-01", "تاريخ الانتهاء": "2025-10-31"},
|
121 |
-
{"المرحلة": "التسليم", "المدة": "30 يوم", "تاريخ البدء": "2025-11-01", "تاريخ الانتهاء": "2025-11-30"}
|
122 |
-
]
|
123 |
-
}
|
124 |
-
]
|
125 |
-
except Exception as e:
|
126 |
-
logger.error(f"خطأ في استخراج الجداول: {str(e)}")
|
127 |
-
return []
|
128 |
-
|
129 |
-
def extract_items(self, file_path):
|
130 |
-
"""استخراج البنود من المستند"""
|
131 |
-
try:
|
132 |
-
# محاكاة استخراج البنود
|
133 |
-
return [
|
134 |
-
{"بند": "أعمال الحفر والردم", "قيمة": 250000, "نسبة": "10%"},
|
135 |
-
{"بند": "أعمال الخرسانة المسلحة", "قيمة": 750000, "نسبة": "30%"},
|
136 |
-
{"بند": "أعمال التشطيبات", "قيمة": 500000, "نسبة": "20%"},
|
137 |
-
{"بند": "أعمال الكهرباء", "قيمة": 350000, "نسبة": "14%"},
|
138 |
-
{"بند": "أعمال السباكة", "قيمة": 300000, "نسبة": "12%"},
|
139 |
-
{"بند": "أعمال التكييف", "قيمة": 350000, "نسبة": "14%"}
|
140 |
-
]
|
141 |
-
except Exception as e:
|
142 |
-
logger.error(f"خطأ في استخراج البنود: {str(e)}")
|
143 |
-
return []
|
144 |
-
|
145 |
-
def extract(self, file_path):
|
146 |
-
"""استخراج جميع العناصر من المستند"""
|
147 |
-
return {
|
148 |
-
"بنود": self.extract_items(file_path),
|
149 |
-
"جداول": self.extract_tables(file_path)
|
150 |
-
}
|
151 |
-
|
152 |
-
|
153 |
-
class DocumentParser:
|
154 |
-
"""فئة تحليل المستندات"""
|
155 |
-
|
156 |
-
def __init__(self, config=None):
|
157 |
-
"""تهيئة محلل المستندات"""
|
158 |
-
self.config = config or {}
|
159 |
-
self.text_extractor = TextExtractor(config)
|
160 |
-
self.item_extractor = ItemExtractor(config)
|
161 |
-
|
162 |
-
def parse_contract(self, file_path):
|
163 |
-
"""تحليل مستند عقد"""
|
164 |
-
try:
|
165 |
-
# محاكاة تحليل عقد
|
166 |
-
return {
|
167 |
-
"نوع المستند": "عقد",
|
168 |
-
"معلومات العقد": {
|
169 |
-
"رقم العقد": "CT-2025-001",
|
170 |
-
"تاريخ العقد": "2025-03-15",
|
171 |
-
"قيمة العقد": "2,500,000 ريال",
|
172 |
-
"مدة العقد": "12 شهر",
|
173 |
-
"تاريخ البدء": "2025-04-01",
|
174 |
-
"تاريخ الانتهاء": "2026-03-31"
|
175 |
-
},
|
176 |
-
"أطراف العقد": {
|
177 |
-
"الطرف الأول": "وزارة الإسكان",
|
178 |
-
"الطرف الثاني": "شركة الإنشاءات المتطورة"
|
179 |
-
},
|
180 |
-
"بنود العقد": [
|
181 |
-
"يلتزم الطرف الثاني بتنفيذ المشروع وفقاً للمواصفات والشروط المرفقة",
|
182 |
-
"مدة تنفيذ المشروع 12 شهراً من تاريخ استلام الموقع",
|
183 |
-
"قيمة العقد 2,500,000 ريال شاملة جميع الضرائب والرسوم",
|
184 |
-
"يتم الدفع على دفعات شهرية حسب نسبة الإنجاز",
|
185 |
-
"غرامة التأخير 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%"
|
186 |
-
],
|
187 |
-
"المرفقات": [
|
188 |
-
"جدول الكميات",
|
189 |
-
"المواصفات الفنية",
|
190 |
-
"الجدول الزمني",
|
191 |
-
"الضمانات والتأمينات"
|
192 |
-
],
|
193 |
-
"درجة الثقة": "95%"
|
194 |
-
}
|
195 |
-
except Exception as e:
|
196 |
-
logger.error(f"خطأ في تحليل العقد: {str(e)}")
|
197 |
-
return {"error": f"حدث خطأ أثناء تحليل العقد: {str(e)}"}
|
198 |
-
|
199 |
-
def parse_tender(self, file_path):
|
200 |
-
"""تحليل مستند مناقصة"""
|
201 |
-
try:
|
202 |
-
# محاكاة تحليل مناقصة
|
203 |
-
return {
|
204 |
-
"نوع المستند": "مناقصة",
|
205 |
-
"معلومات المناقصة": {
|
206 |
-
"رقم المناقصة": "T-2025-002",
|
207 |
-
"اسم المشروع": "إنشاء مبنى إداري",
|
208 |
-
"الجهة المالكة": "وزارة المالية",
|
209 |
-
"تاريخ الطرح": "2025-03-01",
|
210 |
-
"تاريخ الإقفال": "2025-04-15",
|
211 |
-
"القيمة التقديرية": "3,000,000 ريال"
|
212 |
-
},
|
213 |
-
"شروط المناقصة": [
|
214 |
-
"تصنيف المقاول: الدرجة الأولى في مجال المباني",
|
215 |
-
"خبرة سابقة: 5 مشاريع مماثلة خلال الـ 10 سنوات الماضية",
|
216 |
-
"الضمان الابتدائي: 2% من قيمة العطاء",
|
217 |
-
"الضمان النهائي: 5% من قيمة العقد",
|
218 |
-
"مدة تنفيذ المشروع: 18 شهراً"
|
219 |
-
],
|
220 |
-
"المستندات المطلوبة": [
|
221 |
-
"شهادة التصنيف",
|
222 |
-
"السجل التجاري",
|
223 |
-
"شهادة الزكاة والدخل",
|
224 |
-
"شهادة التأمينات الاجتماعية",
|
225 |
-
"قائمة المشاريع المماثلة"
|
226 |
-
],
|
227 |
-
"معايير التقييم": [
|
228 |
-
{"المعيار": "السعر", "الوزن": "50%"},
|
229 |
-
{"المعيار": "الخبرة الفنية", "الوزن": "25%"},
|
230 |
-
{"المعيار": "الجدول الزمني", "الوزن": "15%"},
|
231 |
-
{"المعيار": "فريق العمل", "الوزن": "10%"}
|
232 |
-
],
|
233 |
-
"درجة الثقة": "92%"
|
234 |
-
}
|
235 |
-
except Exception as e:
|
236 |
-
logger.error(f"خطأ في تحليل المناقصة: {str(e)}")
|
237 |
-
return {"error": f"حدث خطأ أثناء تحليل المناقصة: {str(e)}"}
|
238 |
-
|
239 |
-
def parse_specifications(self, file_path):
|
240 |
-
"""تحليل كراسة الشروط والمواصفات"""
|
241 |
-
try:
|
242 |
-
# محاكاة تحليل كراسة الشروط والمواصفات
|
243 |
-
return {
|
244 |
-
"نوع المستند": "كراسة شروط ومواصفات",
|
245 |
-
"معلومات المشروع": {
|
246 |
-
"اسم المشروع": "إنشاء مبنى إداري",
|
247 |
-
"الموقع": "الرياض - حي العليا",
|
248 |
-
"المساحة": "5000 متر مربع",
|
249 |
-
"عدد الطوابق": "5 طوابق"
|
250 |
-
},
|
251 |
-
"المواصفات الفنية": {
|
252 |
-
"الهيكل الإنشائي": "خرسانة مسلحة",
|
253 |
-
"الواجهات": "زجاج عاكس وحجر طبيعي",
|
254 |
-
"التشطيبات الداخلية": "رخام للأرضيات، جبس للأسقف، دهانات عالية الجودة للجدران",
|
255 |
-
"أنظمة الكهرباء": "نظام إنارة LED موفر للطاقة، نظام تحكم ذكي",
|
256 |
-
"أنظمة التكييف": "نظام تكييف مركزي مع تحكم منفصل لكل منطقة",
|
257 |
-
"أنظمة السلامة": "نظام إنذار وإطفاء حريق آلي، كاميرات مراقبة، نظام تحكم في الدخول"
|
258 |
-
},
|
259 |
-
"الشروط العامة": [
|
260 |
-
"الالتزام بكود البناء السعودي",
|
261 |
-
"الالتزام بمتطلبات الدفاع المدني",
|
262 |
-
"الالتزام بمتطلبات الاستدامة وكفاءة الطاقة",
|
263 |
-
"تقديم مخططات تنفيذية معتمدة قبل البدء في التنفيذ",
|
264 |
-
"تقديم عينات للمواد للاعتماد قبل التوريد"
|
265 |
-
],
|
266 |
-
"المرفقات": [
|
267 |
-
"المخططات المعمارية",
|
268 |
-
"المخططات الإنشائية",
|
269 |
-
"مخططات الكهرباء",
|
270 |
-
"مخططات التكييف",
|
271 |
-
"مخططات السباكة",
|
272 |
-
"جدول الكميات"
|
273 |
-
],
|
274 |
-
"درجة الثقة": "90%"
|
275 |
-
}
|
276 |
-
except Exception as e:
|
277 |
-
logger.error(f"خطأ في تحليل كراسة الشروط والمواصفات: {str(e)}")
|
278 |
-
return {"error": f"حدث خطأ أثناء تحليل كراسة الشروط والمواصفات: {str(e)}"}
|
279 |
-
|
280 |
-
def parse_dwg(self, file_path):
|
281 |
-
"""تحليل ملف DWG"""
|
282 |
-
try:
|
283 |
-
# محاكاة تحليل ملف DWG
|
284 |
-
return {
|
285 |
-
"نوع المستند": "ملف DWG",
|
286 |
-
"معلومات الملف": {
|
287 |
-
"اسم الملف": os.path.basename(file_path),
|
288 |
-
"حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
|
289 |
-
"تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
|
290 |
-
},
|
291 |
-
"محتويات الملف": {
|
292 |
-
"عدد الطبقات": 15,
|
293 |
-
"عدد الكائنات": 1250,
|
294 |
-
"أبعاد الرسم": "50م × 30م"
|
295 |
-
},
|
296 |
-
"تحليل المساحات": {
|
297 |
-
"المساحة الإجمالية": "4,500 م²",
|
298 |
-
"مساحة البناء": "3,200 م²",
|
299 |
-
"مساحة الخدمات": "800 م²",
|
300 |
-
"مساحة الممرات": "500 م²"
|
301 |
-
},
|
302 |
-
"تحليل العناصر": {
|
303 |
-
"عدد الغرف": 25,
|
304 |
-
"عدد الأبواب": 40,
|
305 |
-
"عدد النوافذ": 30,
|
306 |
-
"عدد الأعمدة": 20
|
307 |
-
},
|
308 |
-
"ملاحظات": [
|
309 |
-
"تصميم يتوافق مع متطلبات كود البناء السعودي",
|
310 |
-
"توزيع جيد للمساحات",
|
311 |
-
"تصميم يراعي متطلبات ذوي الاحتياجات الخاصة",
|
312 |
-
"تصميم يراعي متطلبات السلامة والإخلاء"
|
313 |
-
],
|
314 |
-
"درجة الثقة": "85%"
|
315 |
-
}
|
316 |
-
except Exception as e:
|
317 |
-
logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
|
318 |
-
return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
|
319 |
-
|
320 |
-
def parse(self, file_path):
|
321 |
-
"""تحليل المستند بناءً على نوعه"""
|
322 |
-
try:
|
323 |
-
_, ext = os.path.splitext(file_path)
|
324 |
-
ext = ext.lower()
|
325 |
-
|
326 |
-
# تحديد نوع المستند بناءً على محتواه (محاكاة)
|
327 |
-
file_name = os.path.basename(file_path).lower()
|
328 |
-
|
329 |
-
if ext == '.dwg':
|
330 |
-
return self.parse_dwg(file_path)
|
331 |
-
elif 'contract' in file_name or 'عقد' in file_name:
|
332 |
-
return self.parse_contract(file_path)
|
333 |
-
elif 'tender' in file_name or 'مناقصة' in file_name:
|
334 |
-
return self.parse_tender(file_path)
|
335 |
-
elif 'spec' in file_name or 'شروط' in file_name or 'مواصفات' in file_name:
|
336 |
-
return self.parse_specifications(file_path)
|
337 |
-
else:
|
338 |
-
# تحليل عام للمستند
|
339 |
-
return {
|
340 |
-
"نوع المستند": "مستند عام",
|
341 |
-
"معلومات الملف": {
|
342 |
-
"اسم الملف": os.path.basename(file_path),
|
343 |
-
"حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
|
344 |
-
"تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
|
345 |
-
},
|
346 |
-
"محتوى المستند": {
|
347 |
-
"نص": self.text_extractor.extract(file_path),
|
348 |
-
"عناصر": self.item_extractor.extract(file_path)
|
349 |
-
},
|
350 |
-
"درجة الثقة": "75%"
|
351 |
-
}
|
352 |
-
except Exception as e:
|
353 |
-
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
354 |
-
return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
|
355 |
-
|
356 |
-
|
357 |
-
class AIDocumentAnalyzer:
|
358 |
-
"""فئة تحليل المستندات باستخدام الذكاء الاصطناعي"""
|
359 |
-
|
360 |
-
def __init__(self):
|
361 |
-
"""تهيئة محلل المستندات الذكي"""
|
362 |
-
self.document_parser = DocumentParser()
|
363 |
-
self.api_keys = {}
|
364 |
-
|
365 |
-
def set_api_key(self, provider, key):
|
366 |
-
"""تعيين مفتاح API لمزود خدمة الذكاء الاصطناعي"""
|
367 |
-
self.api_keys[provider] = key
|
368 |
-
|
369 |
-
def get_api_key(self, provider):
|
370 |
-
"""الحصول على مفتاح API لمزود خدمة الذكاء الاصطناعي"""
|
371 |
-
return self.api_keys.get(provider)
|
372 |
-
|
373 |
-
def analyze_document(self, file_path, provider="local"):
|
374 |
-
"""تحليل المستند باستخدام الذكاء الاصطناعي"""
|
375 |
-
try:
|
376 |
-
# تحليل محلي للمستند
|
377 |
-
local_analysis = self.document_parser.parse(file_path)
|
378 |
-
|
379 |
-
if provider == "local":
|
380 |
-
return local_analysis
|
381 |
-
|
382 |
-
# تحليل باستخدام خدمات الذكاء الاصطناعي السحابية
|
383 |
-
if provider == "openai":
|
384 |
-
# محاكاة تحليل باستخدام OpenAI
|
385 |
-
enhanced_analysis = self._enhance_with_openai(local_analysis)
|
386 |
-
return enhanced_analysis
|
387 |
-
elif provider == "claude":
|
388 |
-
# محاكاة تحليل باستخدام Claude
|
389 |
-
enhanced_analysis = self._enhance_with_claude(local_analysis)
|
390 |
-
return enhanced_analysis
|
391 |
-
else:
|
392 |
-
return local_analysis
|
393 |
-
except Exception as e:
|
394 |
-
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
395 |
-
return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
|
396 |
-
|
397 |
-
def _enhance_with_openai(self, analysis):
|
398 |
-
"""تحسين التحليل باستخدام OpenAI"""
|
399 |
-
# محاكاة تحسين التحليل باستخدام OpenAI
|
400 |
-
analysis["مصدر التحليل"] = "OpenAI"
|
401 |
-
analysis["درجة الثقة"] = "98%"
|
402 |
-
|
403 |
-
# إضافة تحليل المخاطر
|
404 |
-
if "تحليل المخاطر" not in analysis:
|
405 |
-
analysis["تحليل المخاطر"] = [
|
406 |
-
{"المخاطرة": "تأخر التوريدات", "الاحتمالية": "متوسطة", "التأثير": "عالي", "استراتيجية التخفيف": "وضع خطة توريدات بديلة"},
|
407 |
-
{"المخاطرة": "زيادة أسعار المواد", "الاحتمالية": "عالية", "التأثير": "عالي", "استراتيجية التخفيف": "تثبيت أسعار المواد الرئيسية مع الموردين"},
|
408 |
-
{"المخاطرة": "نقص العمالة الماهرة", "الاحتمالية": "متوسطة", "التأثير": "متوسط", "استراتيجية التخفيف": "التعاقد المسبق مع مقاولي الباطن"},
|
409 |
-
{"المخاطرة": "تغيير نطاق العمل", "الاحتمالية": "منخفضة", "التأثير": "عالي", "استراتيجية التخفيف": "توثيق نطاق العمل بدقة وإدارة التغيير"}
|
410 |
-
]
|
411 |
-
|
412 |
-
# إضافة توصيات
|
413 |
-
if "التوصيات" not in analysis:
|
414 |
-
analysis["التوصيات"] = [
|
415 |
-
"مراجعة بنود العقد بدقة قبل التوقيع",
|
416 |
-
"التأكد من وضوح نطاق العمل وعدم وجود غموض",
|
417 |
-
"التحقق من توافق المواصفات الفنية مع المعايير المحلية",
|
418 |
-
"وضع خطة إدارة مخاطر شاملة للمشروع",
|
419 |
-
"تخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع"
|
420 |
-
]
|
421 |
-
|
422 |
-
return analysis
|
423 |
-
|
424 |
-
def _enhance_with_claude(self, analysis):
|
425 |
-
"""تحسين التحليل باستخدام Claude"""
|
426 |
-
# محاكاة تحسين التحليل باستخدام Claude
|
427 |
-
analysis["مصدر التحليل"] = "Claude"
|
428 |
-
analysis["درجة الثقة"] = "97%"
|
429 |
-
|
430 |
-
# إضافة تحليل الفرص
|
431 |
-
if "تحليل الفرص" not in analysis:
|
432 |
-
analysis["تحليل الفرص"] = [
|
433 |
-
{"الفرصة": "تحسين التصميم", "الفائدة": "تقليل التكلفة بنسبة 5-10%", "المتطلبات": "مراجعة هندسية شاملة"},
|
434 |
-
{"الفرصة": "استخدام مواد بديلة", "الفائدة": "تقليل وقت التنفيذ", "المتطلبات": "اعتماد المواصفات الجديدة"},
|
435 |
-
{"الفرصة": "زيادة المحتوى المحلي", "الفائدة": "تحسين التصنيف في برنامج القيمة المضافة", "المتطلبات": "تحديد الموردين المحليين"},
|
436 |
-
{"الفرصة": "تطبيق تقنيات البناء الحديثة", "الفائدة": "تحسين الجودة وتقليل الهدر", "المتطلبات": "تدريب فريق العمل"}
|
437 |
-
]
|
438 |
-
|
439 |
-
# إضافة ملخص تنفيذي
|
440 |
-
if "الملخص التنفيذي" not in analysis:
|
441 |
-
analysis["الملخص التنفيذي"] = """
|
442 |
-
يتضمن هذا المستند تفاصيل مشروع إنشاء مبنى إداري بمساحة إجمالية 5000 متر مربع.
|
443 |
-
المشروع يتكون من 5 طوابق ويتضمن مواصفات فنية عالية الجودة.
|
444 |
-
تقدر تكلفة المشروع بحوالي 3 مليون ريال ومدة التنفيذ 18 شهراً.
|
445 |
-
يتميز المشروع بتصميم يراعي متطلبات الاستدامة وكفاءة الطاقة.
|
446 |
-
تم تحديد عدة مخاطر محتملة للمشروع مع استراتيجيات التخفيف المناسبة.
|
447 |
-
كما تم تحديد عدة فرص لتحسين المشروع من حيث التكلفة والجودة ووقت التنفيذ.
|
448 |
-
"""
|
449 |
-
|
450 |
-
return analysis
|
451 |
-
|
452 |
-
def analyze_dwg(self, file_path, provider="local"):
|
453 |
-
"""تحليل ملف DWG باستخدام الذكاء الاصطناعي"""
|
454 |
-
try:
|
455 |
-
# تحليل محلي لملف DWG
|
456 |
-
local_analysis = self.document_parser.parse_dwg(file_path)
|
457 |
-
|
458 |
-
if provider == "local":
|
459 |
-
return local_analysis
|
460 |
-
|
461 |
-
# تحسين التحليل باستخدام خدمات الذكاء الاصطناعي
|
462 |
-
if provider == "openai" or provider == "claude":
|
463 |
-
# إضافة تحليل متقدم
|
464 |
-
local_analysis["تحليل متقدم"] = {
|
465 |
-
"تقييم التصميم": "جيد جداً",
|
466 |
-
"كفاءة استخدام المساحة": "90%",
|
467 |
-
"توافق مع المعايير": "متوافق مع كود البناء السعودي",
|
468 |
-
"اقتراحات التحسين": [
|
469 |
-
"تحسين توزيع المساحات لزيادة كفاءة استخدام المساحة",
|
470 |
-
"تحسين تصميم الممرات لتسهيل الحركة",
|
471 |
-
"إضافة عناصر تصميم مستدامة لتقليل استهلاك الطاقة",
|
472 |
-
"تحسين تصميم الواجهات لزيادة الإضاءة الطبيعية"
|
473 |
-
]
|
474 |
-
}
|
475 |
-
|
476 |
-
# إضافة تقدير التكلفة
|
477 |
-
local_analysis["تقدير التكلفة"] = {
|
478 |
-
"التكلفة الإجمالية التقديرية": "3,200,000 ريال",
|
479 |
-
"تكلفة المتر المربع": "800 ريال",
|
480 |
-
"توزيع التكلفة": [
|
481 |
-
{"البند": "الهيكل الإنشائي", "النسبة": "35%", "
|
482 |
-
{"البند": "التشطيبات", "النسبة": "25%", "القيمة": "800,000 ريال"},
|
483 |
-
{"البند": "الأنظمة الكهربائية", "النسبة": "15%", "القيمة": "480,000 ريال"},
|
484 |
-
{"البند": "الأنظمة الميكانيكية", "النسبة": "15%", "القيمة": "480,000 ريال"},
|
485 |
-
{"البند": "الأعمال الخارجية", "النسبة": "10%", "القيمة": "320,000 ريال"}
|
486 |
-
]
|
487 |
-
}
|
488 |
-
|
489 |
-
# إضافة الجدول الزمني
|
490 |
-
local_analysis["الجدول الزمني التقديري"] = {
|
491 |
-
"المدة الإجمالية": "18 شهر",
|
492 |
-
"المراحل": [
|
493 |
-
{"المرحلة": "أعمال الحفر والأساسات", "المدة": "3 أشهر", "النسبة": "15%"},
|
494 |
-
{"المرحلة": "الهيكل الإنشائي", "المدة": "6 أشهر", "النسبة": "35%"},
|
495 |
-
{"المرحلة": "التشطيبات الداخلية", "المدة": "5 أشهر", "النسبة": "25%"},
|
496 |
-
{"المرحلة": "الأنظمة الكهربائية والميكانيكية", "المدة": "3 أشهر", "النسبة": "15%"},
|
497 |
-
{"المرحلة": "الأعمال الخارجية والتسليم", "المدة": "1 شهر", "النسبة": "10%"}
|
498 |
-
]
|
499 |
-
}
|
500 |
-
|
501 |
-
local_analysis["مصدر التحليل"] = provider.capitalize()
|
502 |
-
local_analysis["درجة الثقة"] = "95%"
|
503 |
-
|
504 |
-
return local_analysis
|
505 |
-
except Exception as e:
|
506 |
-
logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
|
507 |
-
return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
"""
|
3 |
+
وحدة تحليل المستندات المتقدمة
|
4 |
+
|
5 |
+
هذا الملف يحتوي على الفئات المسؤولة عن تحليل المستندات بشكل احترافي
|
6 |
+
باستخدام تقنيات الذكاء الاصطناعي المتقدمة.
|
7 |
+
"""
|
8 |
+
|
9 |
+
# استيراد المكتبات القياسية
|
10 |
+
import os
|
11 |
+
import sys
|
12 |
+
import logging
|
13 |
+
import base64
|
14 |
+
import json
|
15 |
+
import time
|
16 |
+
from io import BytesIO
|
17 |
+
from pathlib import Path
|
18 |
+
from urllib.parse import urlparse
|
19 |
+
from tempfile import NamedTemporaryFile
|
20 |
+
|
21 |
+
# استيراد مكتبة Streamlit
|
22 |
+
import streamlit as st
|
23 |
+
|
24 |
+
# استيراد المكتبات الإضافية
|
25 |
+
import requests
|
26 |
+
from PIL import Image
|
27 |
+
import pandas as pd
|
28 |
+
import numpy as np
|
29 |
+
|
30 |
+
# تكوين التسجيل
|
31 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
32 |
+
logger = logging.getLogger(__name__)
|
33 |
+
|
34 |
+
try:
|
35 |
+
# استيراد مكتبة pdf2image للتعامل مع ملفات PDF
|
36 |
+
from pdf2image import convert_from_path
|
37 |
+
pdf_conversion_available = True
|
38 |
+
except ImportError:
|
39 |
+
pdf_conversion_available = False
|
40 |
+
logger.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
|
41 |
+
|
42 |
+
|
43 |
+
class TextExtractor:
|
44 |
+
"""فئة استخراج النصوص من المستندات"""
|
45 |
+
|
46 |
+
def __init__(self, config=None):
|
47 |
+
"""تهيئة مستخرج النصوص"""
|
48 |
+
self.config = config or {}
|
49 |
+
|
50 |
+
def extract_from_pdf(self, file_path):
|
51 |
+
"""استخراج النص من ملف PDF"""
|
52 |
+
try:
|
53 |
+
# محاكاة استخراج النص من PDF
|
54 |
+
# ��ي التطبيق الحقيقي، يمكن استخدام مكتبات مثل PyPDF2 أو pdfplumber
|
55 |
+
return f"تم استخراج النص من ملف PDF: {file_path}"
|
56 |
+
except Exception as e:
|
57 |
+
logger.error(f"خطأ في استخراج النص من PDF: {str(e)}")
|
58 |
+
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
59 |
+
|
60 |
+
def extract_from_docx(self, file_path):
|
61 |
+
"""استخراج النص من ملف DOCX"""
|
62 |
+
try:
|
63 |
+
# محاكاة استخراج النص من DOCX
|
64 |
+
# في التطبيق الحقيقي، يمكن استخدام مكتبة python-docx
|
65 |
+
return f"تم استخراج النص من ملف DOCX: {file_path}"
|
66 |
+
except Exception as e:
|
67 |
+
logger.error(f"خطأ في استخراج النص من DOCX: {str(e)}")
|
68 |
+
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
69 |
+
|
70 |
+
def extract_from_image(self, file_path):
|
71 |
+
"""استخراج النص من صورة باستخدام OCR"""
|
72 |
+
try:
|
73 |
+
# محاكاة استخراج النص من صورة
|
74 |
+
# في التطبيق الحقيقي، يمكن استخدام مكتبة pytesseract
|
75 |
+
return f"تم استخراج النص من صورة: {file_path}"
|
76 |
+
except Exception as e:
|
77 |
+
logger.error(f"خطأ في استخراج النص من صورة: {str(e)}")
|
78 |
+
return f"حدث خطأ أثناء استخراج النص: {str(e)}"
|
79 |
+
|
80 |
+
def extract(self, file_path):
|
81 |
+
"""استخراج النص من ملف بناءً على نوعه"""
|
82 |
+
_, ext = os.path.splitext(file_path)
|
83 |
+
ext = ext.lower()
|
84 |
+
|
85 |
+
if ext == '.pdf':
|
86 |
+
return self.extract_from_pdf(file_path)
|
87 |
+
elif ext in ('.doc', '.docx'):
|
88 |
+
return self.extract_from_docx(file_path)
|
89 |
+
elif ext in ('.jpg', '.jpeg', '.png'):
|
90 |
+
return self.extract_from_image(file_path)
|
91 |
+
else:
|
92 |
+
return "نوع ملف غير مدعوم"
|
93 |
+
|
94 |
+
|
95 |
+
class ItemExtractor:
|
96 |
+
"""فئة استخراج العناصر من المستندات"""
|
97 |
+
|
98 |
+
def __init__(self, config=None):
|
99 |
+
"""تهيئة مستخرج العناصر"""
|
100 |
+
self.config = config or {}
|
101 |
+
|
102 |
+
def extract_tables(self, document):
|
103 |
+
"""استخراج الجداول من المستند"""
|
104 |
+
try:
|
105 |
+
# محاكاة استخراج الجداول
|
106 |
+
# في التطبيق الحقيقي، يمكن استخدام مكتبات مثل camelot-py أو tabula-py
|
107 |
+
return [
|
108 |
+
{
|
109 |
+
"عنوان": "جدول البنود والكميات",
|
110 |
+
"بيانات": [
|
111 |
+
{"البند": "أعمال الحفر", "الكمية": 1000, "الوحدة": "م³", "السعر": 50, "الإجمالي": 50000},
|
112 |
+
{"البند": "أعمال الخرسانة", "الكمية": 500, "الوحدة": "م³", "السعر": 300, "الإجمالي": 150000},
|
113 |
+
{"البند": "أعمال التشطيبات", "الكمية": 2000, "الوحدة": "م²", "السعر": 100, "الإجمالي": 200000}
|
114 |
+
]
|
115 |
+
},
|
116 |
+
{
|
117 |
+
"عنوان": "جدول الجدول الزمني",
|
118 |
+
"بيانات": [
|
119 |
+
{"المرحلة": "التصميم", "المدة": "30 يوم", "تاريخ البدء": "2025-04-01", "تاريخ الانتهاء": "2025-04-30"},
|
120 |
+
{"المرحلة": "الإنشاء", "المدة": "180 يوم", "تاريخ البدء": "2025-05-01", "تاريخ الانتهاء": "2025-10-31"},
|
121 |
+
{"المرحلة": "التسليم", "المدة": "30 يوم", "تاريخ البدء": "2025-11-01", "تاريخ الانتهاء": "2025-11-30"}
|
122 |
+
]
|
123 |
+
}
|
124 |
+
]
|
125 |
+
except Exception as e:
|
126 |
+
logger.error(f"خطأ في استخراج الجداول: {str(e)}")
|
127 |
+
return []
|
128 |
+
|
129 |
+
def extract_items(self, file_path):
|
130 |
+
"""استخراج البنود من المستند"""
|
131 |
+
try:
|
132 |
+
# محاكاة استخراج البنود
|
133 |
+
return [
|
134 |
+
{"بند": "أعمال الحفر والردم", "قيمة": 250000, "نسبة": "10%"},
|
135 |
+
{"بند": "أعمال الخرسانة المسلحة", "قيمة": 750000, "نسبة": "30%"},
|
136 |
+
{"بند": "أعمال التشطيبات", "قيمة": 500000, "نسبة": "20%"},
|
137 |
+
{"بند": "أعمال الكهرباء", "قيمة": 350000, "نسبة": "14%"},
|
138 |
+
{"بند": "أعمال السباكة", "قيمة": 300000, "نسبة": "12%"},
|
139 |
+
{"بند": "أعمال التكييف", "قيمة": 350000, "نسبة": "14%"}
|
140 |
+
]
|
141 |
+
except Exception as e:
|
142 |
+
logger.error(f"خطأ في استخراج البنود: {str(e)}")
|
143 |
+
return []
|
144 |
+
|
145 |
+
def extract(self, file_path):
|
146 |
+
"""استخراج جميع العناصر من المستند"""
|
147 |
+
return {
|
148 |
+
"بنود": self.extract_items(file_path),
|
149 |
+
"جداول": self.extract_tables(file_path)
|
150 |
+
}
|
151 |
+
|
152 |
+
|
153 |
+
class DocumentParser:
|
154 |
+
"""فئة تحليل المستندات"""
|
155 |
+
|
156 |
+
def __init__(self, config=None):
|
157 |
+
"""تهيئة محلل المستندات"""
|
158 |
+
self.config = config or {}
|
159 |
+
self.text_extractor = TextExtractor(config)
|
160 |
+
self.item_extractor = ItemExtractor(config)
|
161 |
+
|
162 |
+
def parse_contract(self, file_path):
|
163 |
+
"""تحليل مستند عقد"""
|
164 |
+
try:
|
165 |
+
# محاكاة تحليل عقد
|
166 |
+
return {
|
167 |
+
"نوع المستند": "عقد",
|
168 |
+
"معلومات العقد": {
|
169 |
+
"رقم العقد": "CT-2025-001",
|
170 |
+
"تاريخ العقد": "2025-03-15",
|
171 |
+
"قيمة العقد": "2,500,000 ريال",
|
172 |
+
"مدة العقد": "12 شهر",
|
173 |
+
"تاريخ البدء": "2025-04-01",
|
174 |
+
"تاريخ الانتهاء": "2026-03-31"
|
175 |
+
},
|
176 |
+
"أطراف العقد": {
|
177 |
+
"الطرف الأول": "وزارة الإسكان",
|
178 |
+
"الطرف الثاني": "شركة الإنشاءات المتطورة"
|
179 |
+
},
|
180 |
+
"بنود العقد": [
|
181 |
+
"يلتزم الطرف الثاني بتنفيذ المشروع وفقاً للمواصفات والشروط المرفقة",
|
182 |
+
"مدة تنفيذ المشروع 12 شهراً من تاريخ استلام الموقع",
|
183 |
+
"قيمة العقد 2,500,000 ريال شاملة جميع الضرائب والرسوم",
|
184 |
+
"يتم الدفع على دفعات شهرية حسب نسبة الإنجاز",
|
185 |
+
"غرامة التأخير 1% من قيمة العقد عن كل أسبوع تأخير بحد أقصى 10%"
|
186 |
+
],
|
187 |
+
"المرفقات": [
|
188 |
+
"جدول الكميات",
|
189 |
+
"المواصفات الفنية",
|
190 |
+
"الجدول الزمني",
|
191 |
+
"الضمانات والتأمينات"
|
192 |
+
],
|
193 |
+
"درجة الثقة": "95%"
|
194 |
+
}
|
195 |
+
except Exception as e:
|
196 |
+
logger.error(f"خطأ في تحليل العقد: {str(e)}")
|
197 |
+
return {"error": f"حدث خطأ أثناء تحليل العقد: {str(e)}"}
|
198 |
+
|
199 |
+
def parse_tender(self, file_path):
|
200 |
+
"""تحليل مستند مناقصة"""
|
201 |
+
try:
|
202 |
+
# محاكاة تحليل مناقصة
|
203 |
+
return {
|
204 |
+
"نوع المستند": "مناقصة",
|
205 |
+
"معلومات المناقصة": {
|
206 |
+
"رقم المناقصة": "T-2025-002",
|
207 |
+
"اسم المشروع": "إنشاء مبنى إداري",
|
208 |
+
"الجهة المالكة": "وزارة المالية",
|
209 |
+
"تاريخ الطرح": "2025-03-01",
|
210 |
+
"تاريخ الإقفال": "2025-04-15",
|
211 |
+
"القيمة التقديرية": "3,000,000 ريال"
|
212 |
+
},
|
213 |
+
"شروط المناقصة": [
|
214 |
+
"تصنيف المقاول: الدرجة الأولى في مجال المباني",
|
215 |
+
"خبرة سابقة: 5 مشاريع مماثلة خلال الـ 10 سنوات الماضية",
|
216 |
+
"الضمان الابتدائي: 2% من قيمة العطاء",
|
217 |
+
"الضمان النهائي: 5% من قيمة العقد",
|
218 |
+
"مدة تنفيذ المشروع: 18 شهراً"
|
219 |
+
],
|
220 |
+
"المستندات المطلوبة": [
|
221 |
+
"شهادة التصنيف",
|
222 |
+
"السجل التجاري",
|
223 |
+
"شهادة الزكاة والدخل",
|
224 |
+
"شهادة التأمينات الاجتماعية",
|
225 |
+
"قائمة المشاريع المماثلة"
|
226 |
+
],
|
227 |
+
"معايير التقييم": [
|
228 |
+
{"المعيار": "السعر", "الوزن": "50%"},
|
229 |
+
{"المعيار": "الخبرة الفنية", "الوزن": "25%"},
|
230 |
+
{"المعيار": "الجدول الزمني", "الوزن": "15%"},
|
231 |
+
{"المعيار": "فريق العمل", "الوزن": "10%"}
|
232 |
+
],
|
233 |
+
"درجة الثقة": "92%"
|
234 |
+
}
|
235 |
+
except Exception as e:
|
236 |
+
logger.error(f"خطأ في تحليل المناقصة: {str(e)}")
|
237 |
+
return {"error": f"حدث خطأ أثناء تحليل المناقصة: {str(e)}"}
|
238 |
+
|
239 |
+
def parse_specifications(self, file_path):
|
240 |
+
"""تحليل كراسة الشروط والمواصفات"""
|
241 |
+
try:
|
242 |
+
# محاكاة تحليل كراسة الشروط والمواصفات
|
243 |
+
return {
|
244 |
+
"نوع المستند": "كراسة شروط ومواصفات",
|
245 |
+
"معلومات المشروع": {
|
246 |
+
"اسم المشروع": "إنشاء مبنى إداري",
|
247 |
+
"الموقع": "الرياض - حي العليا",
|
248 |
+
"المساحة": "5000 متر مربع",
|
249 |
+
"عدد الطوابق": "5 طوابق"
|
250 |
+
},
|
251 |
+
"المواصفات الفنية": {
|
252 |
+
"الهيكل الإنشائي": "خرسانة مسلحة",
|
253 |
+
"الواجهات": "زجاج عاكس وحجر طبيعي",
|
254 |
+
"التشطيبات الداخلية": "رخام للأرضيات، جبس للأسقف، دهانات عالية الجودة للجدران",
|
255 |
+
"أنظمة الكهرباء": "نظام إنارة LED موفر للطاقة، نظام تحكم ذكي",
|
256 |
+
"أنظمة التكييف": "نظام تكييف مركزي مع تحكم منفصل لكل منطقة",
|
257 |
+
"أنظمة السلامة": "نظام إنذار وإطفاء حريق آلي، كاميرات مراقبة، نظام تحكم في الدخول"
|
258 |
+
},
|
259 |
+
"الشروط العامة": [
|
260 |
+
"الالتزام بكود البناء السعودي",
|
261 |
+
"الالتزام بمتطلبات الدفاع المدني",
|
262 |
+
"الالتزام بمتطلبات الاستدامة وكفاءة الطاقة",
|
263 |
+
"تقديم مخططات تنفيذية معتمدة قبل البدء في التنفيذ",
|
264 |
+
"تقديم عينات للمواد للاعتماد قبل التوريد"
|
265 |
+
],
|
266 |
+
"المرفقات": [
|
267 |
+
"المخططات المعمارية",
|
268 |
+
"المخططات الإنشائية",
|
269 |
+
"مخططات الكهرباء",
|
270 |
+
"مخططات التكييف",
|
271 |
+
"مخططات السباكة",
|
272 |
+
"جدول الكميات"
|
273 |
+
],
|
274 |
+
"درجة الثقة": "90%"
|
275 |
+
}
|
276 |
+
except Exception as e:
|
277 |
+
logger.error(f"خطأ في تحليل كراسة الشروط والمواصفات: {str(e)}")
|
278 |
+
return {"error": f"حدث خطأ أثناء تحليل كراسة الشروط والمواصفات: {str(e)}"}
|
279 |
+
|
280 |
+
def parse_dwg(self, file_path):
|
281 |
+
"""تحليل ملف DWG"""
|
282 |
+
try:
|
283 |
+
# محاكاة تحليل ملف DWG
|
284 |
+
return {
|
285 |
+
"نوع المستند": "ملف DWG",
|
286 |
+
"معلومات الملف": {
|
287 |
+
"اسم الملف": os.path.basename(file_path),
|
288 |
+
"حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
|
289 |
+
"تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
|
290 |
+
},
|
291 |
+
"محتويات الملف": {
|
292 |
+
"عدد الطبقات": 15,
|
293 |
+
"عدد الكائنات": 1250,
|
294 |
+
"أبعاد الرسم": "50م × 30م"
|
295 |
+
},
|
296 |
+
"تحليل المساحات": {
|
297 |
+
"المساحة الإجمالية": "4,500 م²",
|
298 |
+
"مساحة البناء": "3,200 م²",
|
299 |
+
"مساحة الخدمات": "800 م²",
|
300 |
+
"مساحة الممرات": "500 م²"
|
301 |
+
},
|
302 |
+
"تحليل العناصر": {
|
303 |
+
"عدد الغرف": 25,
|
304 |
+
"عدد الأبواب": 40,
|
305 |
+
"عدد النوافذ": 30,
|
306 |
+
"عدد الأعمدة": 20
|
307 |
+
},
|
308 |
+
"ملاحظات": [
|
309 |
+
"تصميم يتوافق مع متطلبات كود البناء السعودي",
|
310 |
+
"توزيع جيد للمساحات",
|
311 |
+
"تصميم يراعي متطلبات ذوي الاحتياجات الخاصة",
|
312 |
+
"تصميم يراعي متطلبات السلامة والإخلاء"
|
313 |
+
],
|
314 |
+
"درجة الثقة": "85%"
|
315 |
+
}
|
316 |
+
except Exception as e:
|
317 |
+
logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
|
318 |
+
return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
|
319 |
+
|
320 |
+
def parse(self, file_path):
|
321 |
+
"""تحليل المستند بناءً على نوعه"""
|
322 |
+
try:
|
323 |
+
_, ext = os.path.splitext(file_path)
|
324 |
+
ext = ext.lower()
|
325 |
+
|
326 |
+
# تحديد نوع المستند بناءً على محتواه (محاكاة)
|
327 |
+
file_name = os.path.basename(file_path).lower()
|
328 |
+
|
329 |
+
if ext == '.dwg':
|
330 |
+
return self.parse_dwg(file_path)
|
331 |
+
elif 'contract' in file_name or 'عقد' in file_name:
|
332 |
+
return self.parse_contract(file_path)
|
333 |
+
elif 'tender' in file_name or 'مناقصة' in file_name:
|
334 |
+
return self.parse_tender(file_path)
|
335 |
+
elif 'spec' in file_name or 'شروط' in file_name or 'مواصفات' in file_name:
|
336 |
+
return self.parse_specifications(file_path)
|
337 |
+
else:
|
338 |
+
# تحليل عام للمستند
|
339 |
+
return {
|
340 |
+
"نوع المستند": "مستند عام",
|
341 |
+
"معلومات الملف": {
|
342 |
+
"اسم الملف": os.path.basename(file_path),
|
343 |
+
"حجم الملف": f"{os.path.getsize(file_path) / (1024*1024):.2f} ميجابايت",
|
344 |
+
"تاريخ التعديل": time.ctime(os.path.getmtime(file_path))
|
345 |
+
},
|
346 |
+
"محتوى المستند": {
|
347 |
+
"نص": self.text_extractor.extract(file_path),
|
348 |
+
"عناصر": self.item_extractor.extract(file_path)
|
349 |
+
},
|
350 |
+
"درجة الثقة": "75%"
|
351 |
+
}
|
352 |
+
except Exception as e:
|
353 |
+
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
354 |
+
return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
|
355 |
+
|
356 |
+
|
357 |
+
class AIDocumentAnalyzer:
|
358 |
+
"""فئة تحليل المستندات باستخدام الذكاء الاصطناعي"""
|
359 |
+
|
360 |
+
def __init__(self):
|
361 |
+
"""تهيئة محلل المستندات الذكي"""
|
362 |
+
self.document_parser = DocumentParser()
|
363 |
+
self.api_keys = {}
|
364 |
+
|
365 |
+
def set_api_key(self, provider, key):
|
366 |
+
"""تعيين مفتاح API لمزود خدمة الذكاء الاصطناعي"""
|
367 |
+
self.api_keys[provider] = key
|
368 |
+
|
369 |
+
def get_api_key(self, provider):
|
370 |
+
"""الحصول على مفتاح API لمزود خدمة الذكاء الاصطناعي"""
|
371 |
+
return self.api_keys.get(provider)
|
372 |
+
|
373 |
+
def analyze_document(self, file_path, provider="local"):
|
374 |
+
"""تحليل المستند باستخدام الذكاء الاصطناعي"""
|
375 |
+
try:
|
376 |
+
# تحليل محلي للمستند
|
377 |
+
local_analysis = self.document_parser.parse(file_path)
|
378 |
+
|
379 |
+
if provider == "local":
|
380 |
+
return local_analysis
|
381 |
+
|
382 |
+
# تحليل باستخدام خدمات الذكاء الاصطناعي السحابية
|
383 |
+
if provider == "openai":
|
384 |
+
# محاكاة تحليل باستخدام OpenAI
|
385 |
+
enhanced_analysis = self._enhance_with_openai(local_analysis)
|
386 |
+
return enhanced_analysis
|
387 |
+
elif provider == "claude":
|
388 |
+
# محاكاة تحليل باستخدام Claude
|
389 |
+
enhanced_analysis = self._enhance_with_claude(local_analysis)
|
390 |
+
return enhanced_analysis
|
391 |
+
else:
|
392 |
+
return local_analysis
|
393 |
+
except Exception as e:
|
394 |
+
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
395 |
+
return {"error": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
|
396 |
+
|
397 |
+
def _enhance_with_openai(self, analysis):
|
398 |
+
"""تحسين التحليل باستخدام OpenAI"""
|
399 |
+
# محاكاة تحسين التحليل باستخدام OpenAI
|
400 |
+
analysis["مصدر التحليل"] = "OpenAI"
|
401 |
+
analysis["درجة الثقة"] = "98%"
|
402 |
+
|
403 |
+
# إضافة تحليل المخاطر
|
404 |
+
if "تحليل المخاطر" not in analysis:
|
405 |
+
analysis["تحليل المخاطر"] = [
|
406 |
+
{"المخاطرة": "تأخر التوريدات", "الاحتمالية": "متوسطة", "التأثير": "عالي", "استراتيجية التخفيف": "وضع خطة توريدات بديلة"},
|
407 |
+
{"المخاطرة": "زيادة أسعار المواد", "الاحتمالية": "عالية", "التأثير": "عالي", "استراتيجية التخفيف": "تثبيت أسعار المواد الرئيسية مع الموردين"},
|
408 |
+
{"المخاطرة": "نقص العمالة الماهرة", "الاحتمالية": "متوسطة", "التأثير": "متوسط", "استراتيجية التخفيف": "التعاقد المسبق مع مقاولي الباطن"},
|
409 |
+
{"المخاطرة": "تغيير نطاق العمل", "الاحتمالية": "منخفضة", "التأثير": "عالي", "استراتيجية التخفيف": "توثيق نطاق العمل بدقة وإدارة التغيير"}
|
410 |
+
]
|
411 |
+
|
412 |
+
# إضافة توصيات
|
413 |
+
if "التوصيات" not in analysis:
|
414 |
+
analysis["التوصيات"] = [
|
415 |
+
"مراجعة بنود العقد بدقة قبل التوقيع",
|
416 |
+
"التأكد من وضوح نطاق العمل وعدم وجود غموض",
|
417 |
+
"التحقق من توافق المواصفات الفنية مع المعايير المحلية",
|
418 |
+
"وضع خطة إدارة مخاطر شاملة للمشروع",
|
419 |
+
"تخصيص احتياطي للطوارئ بنسبة 10-15% من قيمة المشروع"
|
420 |
+
]
|
421 |
+
|
422 |
+
return analysis
|
423 |
+
|
424 |
+
def _enhance_with_claude(self, analysis):
|
425 |
+
"""تحسين التحليل باستخدام Claude"""
|
426 |
+
# محاكاة تحسين التحليل باستخدام Claude
|
427 |
+
analysis["مصدر التحليل"] = "Claude"
|
428 |
+
analysis["درجة الثقة"] = "97%"
|
429 |
+
|
430 |
+
# إضافة تحليل الفرص
|
431 |
+
if "تحليل الفرص" not in analysis:
|
432 |
+
analysis["تحليل الفرص"] = [
|
433 |
+
{"الفرصة": "تحسين التصميم", "الفائدة": "تقليل التكلفة بنسبة 5-10%", "المتطلبات": "مراجعة هندسية شاملة"},
|
434 |
+
{"الفرصة": "استخدام مواد بديلة", "الفائدة": "تقليل وقت التنفيذ", "المتطلبات": "اعتماد المواصفات الجديدة"},
|
435 |
+
{"الفرصة": "زيادة المحتوى المحلي", "الفائدة": "تحسين التصنيف في برنامج القيمة المضافة", "المتطلبات": "تحديد الموردين المحليين"},
|
436 |
+
{"الفرصة": "تطبيق تقنيات البناء الحديثة", "الفائدة": "تحسين الجودة وتقليل الهدر", "المتطلبات": "تدريب فريق العمل"}
|
437 |
+
]
|
438 |
+
|
439 |
+
# إضافة ملخص تنفيذي
|
440 |
+
if "الملخص التنفيذي" not in analysis:
|
441 |
+
analysis["الملخص التنفيذي"] = """
|
442 |
+
يتضمن هذا المستند تفاصيل مشروع إنشاء مبنى إداري بمساحة إجمالية 5000 متر مربع.
|
443 |
+
المشروع يتكون من 5 طوابق ويتضمن مواصفات فنية عالية الجودة.
|
444 |
+
تقدر تكلفة المشروع بحوالي 3 مليون ريال ومدة التنفيذ 18 شهراً.
|
445 |
+
يتميز المشروع بتصميم يراعي متطلبات الاستدامة وكفاءة الطاقة.
|
446 |
+
تم تحديد عدة مخاطر محتملة للمشروع مع استراتيجيات التخفيف المناسبة.
|
447 |
+
كما تم تحديد عدة فرص لتحسين المشروع من حيث التكلفة والجودة ووقت التنفيذ.
|
448 |
+
"""
|
449 |
+
|
450 |
+
return analysis
|
451 |
+
|
452 |
+
def analyze_dwg(self, file_path, provider="local"):
|
453 |
+
"""تحليل ملف DWG باستخدام الذكاء الاصطناعي"""
|
454 |
+
try:
|
455 |
+
# تحليل محلي لملف DWG
|
456 |
+
local_analysis = self.document_parser.parse_dwg(file_path)
|
457 |
+
|
458 |
+
if provider == "local":
|
459 |
+
return local_analysis
|
460 |
+
|
461 |
+
# تحسين التحليل باستخدام خدمات الذكاء الاصطناعي
|
462 |
+
if provider == "openai" or provider == "claude":
|
463 |
+
# إضافة تحليل متقدم
|
464 |
+
local_analysis["تحليل متقدم"] = {
|
465 |
+
"تقييم التصميم": "جيد جداً",
|
466 |
+
"كفاءة استخدام المساحة": "90%",
|
467 |
+
"توافق مع المعايير": "متوافق مع كود البناء السعودي",
|
468 |
+
"اقتراحات التحسين": [
|
469 |
+
"تحسين توزيع المساحات لزيادة كفاءة استخدام المساحة",
|
470 |
+
"تحسين تصميم الممرات لتسهيل الحركة",
|
471 |
+
"إضافة عناصر تصميم مستدامة لتقليل استهلاك الطاقة",
|
472 |
+
"تحسين تصميم الواجهات لزيادة الإضاءة الطبيعية"
|
473 |
+
]
|
474 |
+
}
|
475 |
+
|
476 |
+
# إضافة تقدير التكلفة
|
477 |
+
local_analysis["تقدير التكلفة"] = {
|
478 |
+
"التكلفة الإجمالية التقديرية": "3,200,000 ريال",
|
479 |
+
"تكلفة المتر المربع": "800 ريال",
|
480 |
+
"توزيع التكلفة": [
|
481 |
+
{"البند": "الهيكل الإنشائي", "النسبة": "35%", "القي��ة": "1,120,000 ريال"},
|
482 |
+
{"البند": "التشطيبات", "النسبة": "25%", "القيمة": "800,000 ريال"},
|
483 |
+
{"البند": "الأنظمة الكهربائية", "النسبة": "15%", "القيمة": "480,000 ريال"},
|
484 |
+
{"البند": "الأنظمة الميكانيكية", "النسبة": "15%", "القيمة": "480,000 ريال"},
|
485 |
+
{"البند": "الأعمال الخارجية", "النسبة": "10%", "القيمة": "320,000 ريال"}
|
486 |
+
]
|
487 |
+
}
|
488 |
+
|
489 |
+
# إضافة الجدول الزمني
|
490 |
+
local_analysis["الجدول الزمني التقديري"] = {
|
491 |
+
"المدة الإجمالية": "18 شهر",
|
492 |
+
"المراحل": [
|
493 |
+
{"المرحلة": "أعمال الحفر والأساسات", "المدة": "3 أشهر", "النسبة": "15%"},
|
494 |
+
{"المرحلة": "الهيكل الإنشائي", "المدة": "6 أشهر", "النسبة": "35%"},
|
495 |
+
{"المرحلة": "التشطيبات الداخلية", "المدة": "5 أشهر", "النسبة": "25%"},
|
496 |
+
{"المرحلة": "الأنظمة الكهربائية والميكانيكية", "المدة": "3 أشهر", "النسبة": "15%"},
|
497 |
+
{"المرحلة": "الأعمال الخارجية والتسليم", "المدة": "1 شهر", "النسبة": "10%"}
|
498 |
+
]
|
499 |
+
}
|
500 |
+
|
501 |
+
local_analysis["مصدر التحليل"] = provider.capitalize()
|
502 |
+
local_analysis["درجة الثقة"] = "95%"
|
503 |
+
|
504 |
+
return local_analysis
|
505 |
+
except Exception as e:
|
506 |
+
logger.error(f"خطأ في تحليل ملف DWG: {str(e)}")
|
507 |
+
return {"error": f"حدث خطأ أثناء تحليل ملف DWG: {str(e)}"}
|
modules/data_analysis/data_analysis_app.py
CHANGED
@@ -1,502 +1,502 @@
|
|
1 |
-
import streamlit as st
|
2 |
-
import pandas as pd
|
3 |
-
import numpy as np
|
4 |
-
import matplotlib.pyplot as plt
|
5 |
-
import plotly.express as px
|
6 |
-
import plotly.graph_objects as go
|
7 |
-
import seaborn as sns
|
8 |
-
from datetime import datetime
|
9 |
-
import os
|
10 |
-
import sys
|
11 |
-
from pathlib import Path
|
12 |
-
|
13 |
-
# إضافة المسار للوصول إلى الوحدات الأخرى
|
14 |
-
current_dir = os.path.dirname(os.path.abspath(__file__))
|
15 |
-
parent_dir = os.path.dirname(os.path.dirname(current_dir))
|
16 |
-
if parent_dir not in sys.path:
|
17 |
-
sys.path.append(parent_dir)
|
18 |
-
|
19 |
-
class DataAnalysisApp:
|
20 |
-
"""تطبيق تحليل البيانات"""
|
21 |
-
|
22 |
-
def __init__(self):
|
23 |
-
"""تهيئة تطبيق تحليل البيانات"""
|
24 |
-
self.data = None
|
25 |
-
self.file_path = None
|
26 |
-
|
27 |
-
def run(self):
|
28 |
-
"""تشغيل تطبيق تحليل البيانات"""
|
29 |
-
# استيراد مدير التكوين
|
30 |
-
from config_manager import ConfigManager
|
31 |
-
|
32 |
-
# محاولة تعيين تكوين الصفحة (سيتم تجاهلها إذا كان التكوين معينًا بالفعل)
|
33 |
-
config_manager = ConfigManager()
|
34 |
-
config_manager.set_page_config_if_needed(
|
35 |
-
page_title="تحليل البيانات",
|
36 |
-
page_icon="📊",
|
37 |
-
layout="wide"
|
38 |
-
)
|
39 |
-
|
40 |
-
# عرض عنوان التطبيق
|
41 |
-
st.title("تحليل البيانات")
|
42 |
-
st.write("استخدم هذه الأداة لتحليل بيانات المناقصات والمشاريع")
|
43 |
-
|
44 |
-
# إنشاء علامات تبويب للتطبيق
|
45 |
-
tabs = st.tabs(["تحميل البيانات", "استكشاف البيانات", "تحليل متقدم", "التصور المرئي", "التقارير"])
|
46 |
-
|
47 |
-
with tabs[0]:
|
48 |
-
self._load_data_tab()
|
49 |
-
|
50 |
-
with tabs[1]:
|
51 |
-
self._explore_data_tab()
|
52 |
-
|
53 |
-
with tabs[2]:
|
54 |
-
self._advanced_analysis_tab()
|
55 |
-
|
56 |
-
with tabs[3]:
|
57 |
-
self._visualization_tab()
|
58 |
-
|
59 |
-
with tabs[4]:
|
60 |
-
self._reports_tab()
|
61 |
-
|
62 |
-
def _load_data_tab(self):
|
63 |
-
"""علامة تبويب تحميل البيانات"""
|
64 |
-
st.header("تحميل البيانات")
|
65 |
-
|
66 |
-
# خيارات تحميل البيانات
|
67 |
-
data_source = st.radio(
|
68 |
-
"اختر مصدر البيانات:",
|
69 |
-
["تحميل ملف", "استيراد من قاعدة البيانات", "استخدام بيانات نموذجية"]
|
70 |
-
)
|
71 |
-
|
72 |
-
if data_source == "تحميل ملف":
|
73 |
-
uploaded_file = st.file_uploader("اختر ملف CSV أو Excel", type=["csv", "xlsx", "xls"])
|
74 |
-
|
75 |
-
if uploaded_file is not None:
|
76 |
-
try:
|
77 |
-
if uploaded_file.name.endswith('.csv'):
|
78 |
-
self.data = pd.read_csv(uploaded_file)
|
79 |
-
else:
|
80 |
-
self.data = pd.read_excel(uploaded_file)
|
81 |
-
|
82 |
-
st.success(f"تم تحميل الملف بنجاح! عدد الصفوف: {self.data.shape[0]}, عدد الأعمدة: {self.data.shape[1]}")
|
83 |
-
st.write("معاينة البيانات:")
|
84 |
-
st.dataframe(self.data.head())
|
85 |
-
except Exception as e:
|
86 |
-
st.error(f"حدث خطأ أثناء تحميل الملف: {str(e)}")
|
87 |
-
|
88 |
-
elif data_source == "استيراد من قاعدة البيانات":
|
89 |
-
st.info("هذه الميزة قيد التطوير")
|
90 |
-
|
91 |
-
# محاكاة الاتصال بقاعدة البيانات
|
92 |
-
if st.button("اتصال بقاعدة البيانات"):
|
93 |
-
with st.spinner("جاري الاتصال بقاعدة البيانات..."):
|
94 |
-
# محاكاة تأخير الاتصال
|
95 |
-
import time
|
96 |
-
time.sleep(2)
|
97 |
-
|
98 |
-
# إنشاء بيانات نموذجية
|
99 |
-
self.data = self._create_sample_data()
|
100 |
-
|
101 |
-
st.success("تم الاتصال بقاعدة البيانات بنجاح!")
|
102 |
-
st.write("معاينة البيانات:")
|
103 |
-
st.dataframe(self.data.head())
|
104 |
-
|
105 |
-
elif data_source == "استخدام بيانات نموذجية":
|
106 |
-
if st.button("تحميل بيانات نموذجية"):
|
107 |
-
self.data = self._create_sample_data()
|
108 |
-
st.success("تم تحميل البيانات النموذجية بنجاح!")
|
109 |
-
st.write("معاينة البيانات:")
|
110 |
-
st.dataframe(self.data.head())
|
111 |
-
|
112 |
-
def _explore_data_tab(self):
|
113 |
-
"""علامة تبويب استكشاف البيانات"""
|
114 |
-
st.header("استكشاف البيانات")
|
115 |
-
|
116 |
-
if self.data is None:
|
117 |
-
st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
|
118 |
-
return
|
119 |
-
|
120 |
-
# عرض معلومات عامة عن البيانات
|
121 |
-
st.subheader("معلومات عامة")
|
122 |
-
col1, col2 = st.columns(2)
|
123 |
-
|
124 |
-
with col1:
|
125 |
-
st.write(f"عدد الصفوف: {self.data.shape[0]}")
|
126 |
-
st.write(f"عدد الأعمدة: {self.data.shape[1]}")
|
127 |
-
st.write(f"القيم المفقودة: {self.data.isna().sum().sum()}")
|
128 |
-
|
129 |
-
with col2:
|
130 |
-
st.write(f"أنواع البيانات:")
|
131 |
-
st.write(self.data.dtypes)
|
132 |
-
|
133 |
-
# عرض إحصاءات وصفية
|
134 |
-
st.subheader("إحصاءات وصفية")
|
135 |
-
st.dataframe(self.data.describe())
|
136 |
-
|
137 |
-
# عرض معلومات عن الأعمدة
|
138 |
-
st.subheader("معلومات الأعمدة")
|
139 |
-
|
140 |
-
selected_column = st.selectbox("اختر عمودًا لتحليله:", self.data.columns)
|
141 |
-
|
142 |
-
if selected_column:
|
143 |
-
col1, col2 = st.columns(2)
|
144 |
-
|
145 |
-
with col1:
|
146 |
-
st.write(f"نوع البيانات: {self.data[selected_column].dtype}")
|
147 |
-
st.write(f"القيم الفريدة: {self.data[selected_column].nunique()}")
|
148 |
-
st.write(f"القيم المفقودة: {self.data[selected_column].isna().sum()}")
|
149 |
-
|
150 |
-
with col2:
|
151 |
-
if pd.api.types.is_numeric_dtype(self.data[selected_column]):
|
152 |
-
st.write(f"الحد الأدنى: {self.data[selected_column].min()}")
|
153 |
-
st.write(f"الحد الأقصى: {self.data[selected_column].max()}")
|
154 |
-
st.write(f"المتوسط: {self.data[selected_column].mean()}")
|
155 |
-
st.write(f"الوسيط: {self.data[selected_column].median()}")
|
156 |
-
else:
|
157 |
-
st.write("القيم الأكثر تكرارًا:")
|
158 |
-
st.write(self.data[selected_column].value_counts().head())
|
159 |
-
|
160 |
-
# عرض رسم بياني للعمود المحدد
|
161 |
-
st.subheader(f"رسم بياني لـ {selected_column}")
|
162 |
-
|
163 |
-
if pd.api.types.is_numeric_dtype(self.data[selected_column]):
|
164 |
-
fig = px.histogram(self.data, x=selected_column, title=f"توزيع {selected_column}")
|
165 |
-
st.plotly_chart(fig, use_container_width=True)
|
166 |
-
else:
|
167 |
-
# الكود المعدل لحل مشكلة الرسم البياني
|
168 |
-
value_counts_df = self.data[selected_column].value_counts().reset_index()
|
169 |
-
value_counts_df.columns = ['القيمة', 'العدد'] # تسمية الأعمدة بأسماء واضحة
|
170 |
-
fig = px.bar(value_counts_df, x='القيمة', y='العدد', title=f"توزيع {selected_column}")
|
171 |
-
fig.update_layout(xaxis_title="القيمة", yaxis_title="العدد")
|
172 |
-
st.plotly_chart(fig, use_container_width=True)
|
173 |
-
|
174 |
-
def _advanced_analysis_tab(self):
|
175 |
-
"""علامة تبويب التحليل المتقدم"""
|
176 |
-
st.header("تحليل متقدم")
|
177 |
-
|
178 |
-
if self.data is None:
|
179 |
-
st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
|
180 |
-
return
|
181 |
-
|
182 |
-
# أنواع التحليل المتقدم
|
183 |
-
analysis_type = st.selectbox(
|
184 |
-
"اختر نوع التحليل:",
|
185 |
-
["تحليل الارتباط", "تحليل الاتجاهات", "تحليل المجموعات", "تحليل التباين"]
|
186 |
-
)
|
187 |
-
|
188 |
-
if analysis_type == "تحليل الارتباط":
|
189 |
-
st.subheader("تحليل الارتباط")
|
190 |
-
|
191 |
-
# اختيار الأعمدة الرقمية فقط
|
192 |
-
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
193 |
-
|
194 |
-
if len(numeric_columns) < 2:
|
195 |
-
st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإجراء تحليل الارتباط")
|
196 |
-
return
|
197 |
-
|
198 |
-
# حساب مصفوفة الارتباط
|
199 |
-
correlation_matrix = self.data[numeric_columns].corr()
|
200 |
-
|
201 |
-
# عرض مصفوفة الارتباط
|
202 |
-
st.write("مصفوفة الارتباط:")
|
203 |
-
st.dataframe(correlation_matrix)
|
204 |
-
|
205 |
-
# رسم خريطة حرارية للارتباط
|
206 |
-
st.write("خريطة حرارية للارتباط:")
|
207 |
-
fig = px.imshow(correlation_matrix, text_auto=True, aspect="auto",
|
208 |
-
title="خريطة حرارية لمصفوفة الارتباط")
|
209 |
-
st.plotly_chart(fig, use_container_width=True)
|
210 |
-
|
211 |
-
# تحليل الارتباط بين عمودين محددين
|
212 |
-
st.subheader("تحليل الارتباط بين عمودين محددين")
|
213 |
-
|
214 |
-
col1 = st.selectbox("اختر
|
215 |
-
col2 = st.selectbox("اختر العمود الثاني:", numeric_columns, key="corr_col2")
|
216 |
-
|
217 |
-
if col1 != col2:
|
218 |
-
# حساب معامل الارتباط
|
219 |
-
correlation = self.data[col1].corr(self.data[col2])
|
220 |
-
|
221 |
-
st.write(f"معامل الارتباط بين {col1} و {col2}: {correlation:.4f}")
|
222 |
-
|
223 |
-
# رسم مخطط التشتت
|
224 |
-
fig = px.scatter(self.data, x=col1, y=col2, title=f"مخطط التشتت: {col1} مقابل {col2}")
|
225 |
-
fig.update_layout(xaxis_title=col1, yaxis_title=col2)
|
226 |
-
st.plotly_chart(fig, use_container_width=True)
|
227 |
-
else:
|
228 |
-
st.warning("الرجاء اختيار عمودين مختلفين")
|
229 |
-
|
230 |
-
elif analysis_type == "تحليل الاتجاهات":
|
231 |
-
st.subheader("تحليل الاتجاهات")
|
232 |
-
st.info("هذه الميزة قيد التطوير")
|
233 |
-
|
234 |
-
elif analysis_type == "تحليل المجموعات":
|
235 |
-
st.subheader("تحليل المجموعات")
|
236 |
-
st.info("هذه الميزة قيد التطوير")
|
237 |
-
|
238 |
-
elif analysis_type == "تحليل التباين":
|
239 |
-
st.subheader("تحليل التباين")
|
240 |
-
st.info("هذه الميزة قيد التطوير")
|
241 |
-
|
242 |
-
def _visualization_tab(self):
|
243 |
-
"""علامة تبويب التصور المرئي"""
|
244 |
-
st.header("التصور المرئي")
|
245 |
-
|
246 |
-
if self.data is None:
|
247 |
-
st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
|
248 |
-
return
|
249 |
-
|
250 |
-
# أنواع الرسوم البيانية
|
251 |
-
chart_type = st.selectbox(
|
252 |
-
"اختر نوع الرسم البياني:",
|
253 |
-
["مخطط شريطي", "مخطط خطي", "مخطط دائري", "مخطط تشتت", "مخطط صندوقي", "مخطط حراري"]
|
254 |
-
)
|
255 |
-
|
256 |
-
# اختيار الأعمدة حسب نوع الرسم البياني
|
257 |
-
if chart_type == "مخطط شريطي":
|
258 |
-
st.subheader("مخطط شريطي")
|
259 |
-
|
260 |
-
x_column = st.selectbox("اختر عمود المحور الأفقي (x):", self.data.columns, key="bar_x")
|
261 |
-
y_column = st.selectbox("اختر عمود المحور الرأسي (y):",
|
262 |
-
self.data.select_dtypes(include=['number']).columns.tolist(),
|
263 |
-
key="bar_y")
|
264 |
-
|
265 |
-
# خيارات إضافية
|
266 |
-
color_column = st.selectbox("اختر عمود اللون (اختياري):",
|
267 |
-
["لا يوجد"] + self.data.columns.tolist(),
|
268 |
-
key="bar_color")
|
269 |
-
|
270 |
-
# إنشاء الرسم البياني
|
271 |
-
if color_column == "لا يوجد":
|
272 |
-
fig = px.bar(self.data, x=x_column, y=y_column, title=f"{y_column} حسب {x_column}")
|
273 |
-
else:
|
274 |
-
fig = px.bar(self.data, x=x_column, y=y_column, color=color_column,
|
275 |
-
title=f"{y_column} حسب {x_column} (مصنف حسب {color_column})")
|
276 |
-
|
277 |
-
fig.update_layout(xaxis_title=x_column, yaxis_title=y_column)
|
278 |
-
st.plotly_chart(fig, use_container_width=True)
|
279 |
-
|
280 |
-
elif chart_type == "مخطط خطي":
|
281 |
-
st.subheader("مخطط خطي")
|
282 |
-
|
283 |
-
x_column = st.selectbox("اختر عمود المحور الأفقي (x):", self.data.columns, key="line_x")
|
284 |
-
y_columns = st.multiselect("اختر أعمدة المحور الرأسي (y):",
|
285 |
-
self.data.select_dtypes(include=['number']).columns.tolist(),
|
286 |
-
key="line_y")
|
287 |
-
|
288 |
-
if y_columns:
|
289 |
-
# إنشاء الرسم البياني
|
290 |
-
fig = go.Figure()
|
291 |
-
|
292 |
-
for y_column in y_columns:
|
293 |
-
fig.add_trace(go.Scatter(x=self.data[x_column], y=self.data[y_column],
|
294 |
-
mode='lines+markers', name=y_column))
|
295 |
-
|
296 |
-
fig.update_layout(title=f"مخطط خطي", xaxis_title=x_column, yaxis_title="القيمة")
|
297 |
-
st.plotly_chart(fig, use_container_width=True)
|
298 |
-
else:
|
299 |
-
st.warning("الرجاء اختيار عمود واحد على الأقل للمحور الرأسي")
|
300 |
-
|
301 |
-
elif chart_type == "مخطط دائري":
|
302 |
-
st.subheader("مخطط دائري")
|
303 |
-
|
304 |
-
column = st.selectbox("اختر العمود:", self.data.columns, key="pie_column")
|
305 |
-
|
306 |
-
# إنشاء الرسم البياني
|
307 |
-
# تعديل لحل مشكلة مماثلة في مخطط دائري
|
308 |
-
value_counts_df = self.data[column].value_counts().reset_index()
|
309 |
-
value_counts_df.columns = ['القيمة', 'العدد']
|
310 |
-
fig = px.pie(value_counts_df, names='القيمة', values='العدد', title=f"توزيع {column}")
|
311 |
-
st.plotly_chart(fig, use_container_width=True)
|
312 |
-
|
313 |
-
elif chart_type == "مخطط تشتت":
|
314 |
-
st.subheader("مخطط تشتت")
|
315 |
-
|
316 |
-
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
317 |
-
|
318 |
-
if len(numeric_columns) < 2:
|
319 |
-
st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإنشاء مخطط تشتت")
|
320 |
-
return
|
321 |
-
|
322 |
-
x_column = st.selectbox("اختر عمود المحور الأفقي (x):", numeric_columns, key="scatter_x")
|
323 |
-
y_column = st.selectbox("اختر عمود المحور الرأسي (y):", numeric_columns, key="scatter_y")
|
324 |
-
|
325 |
-
# خيارات إضافية
|
326 |
-
color_column = st.selectbox("اختر عمود اللون (اختياري):",
|
327 |
-
["لا يوجد"] + self.data.columns.tolist(),
|
328 |
-
key="scatter_color")
|
329 |
-
|
330 |
-
size_column = st.selectbox("اختر عمود الحجم (اختياري):",
|
331 |
-
["لا يوجد"] + numeric_columns,
|
332 |
-
key="scatter_size")
|
333 |
-
|
334 |
-
# إنشاء الرسم البياني
|
335 |
-
if color_column == "لا يوجد" and size_column == "لا يوجد":
|
336 |
-
fig = px.scatter(self.data, x=x_column, y=y_column,
|
337 |
-
title=f"{y_column} مقابل {x_column}")
|
338 |
-
elif color_column != "لا يوجد" and size_column == "لا يوجد":
|
339 |
-
fig = px.scatter(self.data, x=x_column, y=y_column, color=color_column,
|
340 |
-
title=f"{y_column} مقابل {x_column} (مصنف حسب {color_column})")
|
341 |
-
elif color_column == "لا يوجد" and size_column != "لا يوجد":
|
342 |
-
fig = px.scatter(self.data, x=x_column, y=y_column, size=size_column,
|
343 |
-
title=f"{y_column} مقابل {x_column} (الحجم حسب {size_column})")
|
344 |
-
else:
|
345 |
-
fig = px.scatter(self.data, x=x_column, y=y_column, color=color_column, size=size_column,
|
346 |
-
title=f"{y_column} مقابل {x_column} (مصنف حسب {color_column}, الحجم حسب {size_column})")
|
347 |
-
|
348 |
-
fig.update_layout(xaxis_title=x_column, yaxis_title=y_column)
|
349 |
-
st.plotly_chart(fig, use_container_width=True)
|
350 |
-
|
351 |
-
elif chart_type == "مخطط صندوقي":
|
352 |
-
st.subheader("مخطط صندوقي")
|
353 |
-
|
354 |
-
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
355 |
-
|
356 |
-
if not numeric_columns:
|
357 |
-
st.warning("يجب أن يكون هناك عمود رقمي واحد على الأقل لإنشاء مخطط صندوقي")
|
358 |
-
return
|
359 |
-
|
360 |
-
y_column = st.selectbox("اختر عمود القيمة:", numeric_columns, key="box_y")
|
361 |
-
|
362 |
-
# خيارات إضافية
|
363 |
-
x_column = st.selectbox("اختر عمود التصنيف (اختياري):",
|
364 |
-
["لا يوجد"] + self.data.columns.tolist(),
|
365 |
-
key="box_x")
|
366 |
-
|
367 |
-
# إنشاء الرسم البياني
|
368 |
-
if x_column == "لا يوجد":
|
369 |
-
fig = px.box(self.data, y=y_column, title=f"مخطط صندوقي لـ {y_column}")
|
370 |
-
else:
|
371 |
-
fig = px.box(self.data, x=x_column, y=y_column,
|
372 |
-
title=f"مخطط صندوقي لـ {y_column} حسب {x_column}")
|
373 |
-
|
374 |
-
st.plotly_chart(fig, use_container_width=True)
|
375 |
-
|
376 |
-
elif chart_type == "مخطط حراري":
|
377 |
-
st.subheader("مخطط حراري")
|
378 |
-
|
379 |
-
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
380 |
-
|
381 |
-
if len(numeric_columns) < 2:
|
382 |
-
st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإنشاء مخطط حراري")
|
383 |
-
return
|
384 |
-
|
385 |
-
# اختيار الأعمدة للمخطط الحراري
|
386 |
-
selected_columns = st.multiselect("اختر الأعمدة للمخطط الحراري:",
|
387 |
-
numeric_columns,
|
388 |
-
default=numeric_columns[:5] if len(numeric_columns) > 5 else numeric_columns)
|
389 |
-
|
390 |
-
if selected_columns:
|
391 |
-
# حساب مصفوفة الارتباط
|
392 |
-
correlation_matrix = self.data[selected_columns].corr()
|
393 |
-
|
394 |
-
# إنشاء الرسم البياني
|
395 |
-
fig = px.imshow(correlation_matrix, text_auto=True, aspect="auto",
|
396 |
-
title="مخطط حراري لمصفوفة الارتباط")
|
397 |
-
st.plotly_chart(fig, use_container_width=True)
|
398 |
-
else:
|
399 |
-
st.warning("الرجاء اختيار عمود واحد على الأقل")
|
400 |
-
|
401 |
-
def _reports_tab(self):
|
402 |
-
"""علامة تبويب التقارير"""
|
403 |
-
st.header("التقارير")
|
404 |
-
|
405 |
-
if self.data is None:
|
406 |
-
st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
|
407 |
-
return
|
408 |
-
|
409 |
-
st.subheader("إنشاء تقرير")
|
410 |
-
|
411 |
-
# خيارات التقرير
|
412 |
-
report_type = st.selectbox(
|
413 |
-
"اختر نوع التقرير:",
|
414 |
-
["تقرير ملخص", "تقرير تحليلي", "تقرير مقارنة"]
|
415 |
-
)
|
416 |
-
|
417 |
-
if report_type == "تقرير ملخص":
|
418 |
-
st.write("محتوى التقرير:")
|
419 |
-
|
420 |
-
# إنشاء ملخص للبيانات
|
421 |
-
st.write("### ملخص البيانات")
|
422 |
-
st.write(f"عدد الصفوف: {self.data.shape[0]}")
|
423 |
-
st.write(f"عدد الأعمدة: {self.data.shape[1]}")
|
424 |
-
|
425 |
-
# إحصاءات وصفية
|
426 |
-
st.write("### إحصاءات وصفية")
|
427 |
-
st.dataframe(self.data.describe())
|
428 |
-
|
429 |
-
# معلومات عن القيم المفقودة
|
430 |
-
st.write("### القيم المفقودة")
|
431 |
-
missing_data = pd.DataFrame({
|
432 |
-
'العمود': self.data.columns,
|
433 |
-
'عدد القيم المفقودة': self.data.isna().sum().values,
|
434 |
-
'نسبة القيم المفقودة (%)': (self.data.isna().sum().values / len(self.data) * 100).round(2)
|
435 |
-
})
|
436 |
-
st.dataframe(missing_data)
|
437 |
-
|
438 |
-
# توزيع البيانات الرقمية
|
439 |
-
st.write("### توزيع البيانات الرقمية")
|
440 |
-
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
441 |
-
|
442 |
-
if numeric_columns:
|
443 |
-
for i in range(0, len(numeric_columns), 2):
|
444 |
-
cols = st.columns(2)
|
445 |
-
for j in range(2):
|
446 |
-
if i + j < len(numeric_columns):
|
447 |
-
col = numeric_columns[i + j]
|
448 |
-
with cols[j]:
|
449 |
-
fig = px.histogram(self.data, x=col, title=f"توزيع {col}")
|
450 |
-
st.plotly_chart(fig, use_container_width=True)
|
451 |
-
|
452 |
-
# خيارات تصدير التقرير
|
453 |
-
st.subheader("تصدير التقرير")
|
454 |
-
export_format = st.radio("اختر صيغة التصدير:", ["PDF", "Excel", "HTML"])
|
455 |
-
|
456 |
-
if st.button("تصدير التقرير"):
|
457 |
-
st.success(f"تم تصدير التقرير بصيغة {export_format} بنجاح!")
|
458 |
-
|
459 |
-
elif report_type == "تقرير تحليلي":
|
460 |
-
st.info("هذه الميزة قيد التطوير")
|
461 |
-
|
462 |
-
elif report_type == "تقرير مقارنة":
|
463 |
-
st.info("هذه الميزة قيد التطوير")
|
464 |
-
|
465 |
-
def _create_sample_data(self):
|
466 |
-
"""إنشاء بيانات نموذجية للمناقصات"""
|
467 |
-
# إنشاء تواريخ عشوائية
|
468 |
-
start_date = datetime(2023, 1, 1)
|
469 |
-
end_date = datetime(2025, 3, 31)
|
470 |
-
days = (end_date - start_date).days
|
471 |
-
|
472 |
-
# إنشاء بيانات نموذجية
|
473 |
-
data = {
|
474 |
-
'رقم المناقصة': [f'T-{i:04d}' for i in range(1, 101)],
|
475 |
-
'اسم المشروع': [f'مشروع {i}' for i in range(1, 101)],
|
476 |
-
'نوع المشروع': np.random.choice(['بناء', 'صيانة', 'تطوير', 'توريد', 'خدمات'], 100),
|
477 |
-
'الموقع': np.random.choice(['الرياض', 'جدة', 'الدمام', 'مكة', 'المدينة', 'تبوك', 'أبها'], 100),
|
478 |
-
'تاريخ الإعلان': [start_date + pd.Timedelta(days=np.random.randint(0, days)) for _ in range(100)],
|
479 |
-
'تاريخ الإغلاق': [start_date + pd.Timedelta(days=np.random.randint(30, days)) for _ in range(100)],
|
480 |
-
'الميزانية التقديرية': np.random.uniform(1000000, 50000000, 100),
|
481 |
-
'عدد المتقدمين': np.random.randint(1, 20, 100),
|
482 |
-
'سعر العرض': np.random.uniform(900000, 55000000, 100),
|
483 |
-
'نسبة الفوز (%)': np.random.uniform(0, 100, 100),
|
484 |
-
'مدة التنفيذ (أشهر)': np.random.randint(3, 36, 100),
|
485 |
-
'عدد العمال': np.random.randint(10, 500, 100),
|
486 |
-
'تكلفة المواد': np.random.uniform(500000, 30000000, 100),
|
487 |
-
'تكلفة
|
488 |
-
'تكلفة المعدات': np.random.uniform(100000, 10000000, 100),
|
489 |
-
'هامش الربح (%)': np.random.uniform(5, 25, 100),
|
490 |
-
'درجة المخاطرة': np.random.choice(['منخفضة', 'متوسطة', 'عالية'], 100),
|
491 |
-
'حالة المناقصة': np.random.choice(['جارية', 'مغلقة', 'ملغاة', 'فائزة', 'خاسرة'], 100)
|
492 |
-
}
|
493 |
-
|
494 |
-
# إنشاء DataFrame
|
495 |
-
df = pd.DataFrame(data)
|
496 |
-
|
497 |
-
# إضافة بعض العلاقات المنطقية
|
498 |
-
df['إجمالي التكلفة'] = df['تكلفة المواد'] + df['تكلفة العمالة'] + df['تكلفة المعدات']
|
499 |
-
df['الربح المتوقع'] = df['سعر العرض'] - df['إجمالي التكلفة']
|
500 |
-
df['نسبة التكلفة من العرض (%)'] = (df['إجمالي التكلفة'] / df['سعر العرض'] * 100).round(2)
|
501 |
-
|
502 |
-
return df
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import numpy as np
|
4 |
+
import matplotlib.pyplot as plt
|
5 |
+
import plotly.express as px
|
6 |
+
import plotly.graph_objects as go
|
7 |
+
import seaborn as sns
|
8 |
+
from datetime import datetime
|
9 |
+
import os
|
10 |
+
import sys
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# إضافة المسار للوصول إلى الوحدات الأخرى
|
14 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
15 |
+
parent_dir = os.path.dirname(os.path.dirname(current_dir))
|
16 |
+
if parent_dir not in sys.path:
|
17 |
+
sys.path.append(parent_dir)
|
18 |
+
|
19 |
+
class DataAnalysisApp:
|
20 |
+
"""تطبيق تحليل البيانات"""
|
21 |
+
|
22 |
+
def __init__(self):
|
23 |
+
"""تهيئة تطبيق تحليل البيانات"""
|
24 |
+
self.data = None
|
25 |
+
self.file_path = None
|
26 |
+
|
27 |
+
def run(self):
|
28 |
+
"""تشغيل تطبيق تحليل البيانات"""
|
29 |
+
# استيراد مدير التكوين
|
30 |
+
from config_manager import ConfigManager
|
31 |
+
|
32 |
+
# محاولة تعيين تكوين الصفحة (سيتم تجاهلها إذا كان التكوين معينًا بالفعل)
|
33 |
+
config_manager = ConfigManager()
|
34 |
+
config_manager.set_page_config_if_needed(
|
35 |
+
page_title="تحليل البيانات",
|
36 |
+
page_icon="📊",
|
37 |
+
layout="wide"
|
38 |
+
)
|
39 |
+
|
40 |
+
# عرض عنوان التطبيق
|
41 |
+
st.title("تحليل البيانات")
|
42 |
+
st.write("استخدم هذه الأداة لتحليل بيانات المناقصات والمشاريع")
|
43 |
+
|
44 |
+
# إنشاء علامات تبويب للتطبيق
|
45 |
+
tabs = st.tabs(["تحميل البيانات", "استكشاف البيانات", "تحليل متقدم", "التصور المرئي", "التقارير"])
|
46 |
+
|
47 |
+
with tabs[0]:
|
48 |
+
self._load_data_tab()
|
49 |
+
|
50 |
+
with tabs[1]:
|
51 |
+
self._explore_data_tab()
|
52 |
+
|
53 |
+
with tabs[2]:
|
54 |
+
self._advanced_analysis_tab()
|
55 |
+
|
56 |
+
with tabs[3]:
|
57 |
+
self._visualization_tab()
|
58 |
+
|
59 |
+
with tabs[4]:
|
60 |
+
self._reports_tab()
|
61 |
+
|
62 |
+
def _load_data_tab(self):
|
63 |
+
"""علامة تبويب تحميل البيانات"""
|
64 |
+
st.header("تحميل البيانات")
|
65 |
+
|
66 |
+
# خيارات تحميل البيانات
|
67 |
+
data_source = st.radio(
|
68 |
+
"اختر مصدر البيانات:",
|
69 |
+
["تحميل ملف", "استيراد من قاعدة البيانات", "استخدام بيانات نموذجية"]
|
70 |
+
)
|
71 |
+
|
72 |
+
if data_source == "تحميل ملف":
|
73 |
+
uploaded_file = st.file_uploader("اختر ملف CSV أو Excel", type=["csv", "xlsx", "xls"])
|
74 |
+
|
75 |
+
if uploaded_file is not None:
|
76 |
+
try:
|
77 |
+
if uploaded_file.name.endswith('.csv'):
|
78 |
+
self.data = pd.read_csv(uploaded_file)
|
79 |
+
else:
|
80 |
+
self.data = pd.read_excel(uploaded_file)
|
81 |
+
|
82 |
+
st.success(f"تم تحميل الملف بنجاح! عدد الصفوف: {self.data.shape[0]}, عدد الأعمدة: {self.data.shape[1]}")
|
83 |
+
st.write("معاينة البيانات:")
|
84 |
+
st.dataframe(self.data.head())
|
85 |
+
except Exception as e:
|
86 |
+
st.error(f"حدث خطأ أثناء تحميل الملف: {str(e)}")
|
87 |
+
|
88 |
+
elif data_source == "استيراد من قاعدة البيانات":
|
89 |
+
st.info("هذه الميزة قيد التطوير")
|
90 |
+
|
91 |
+
# محاكاة الاتصال بقاعدة البيانات
|
92 |
+
if st.button("اتصال بقاعدة البيانات"):
|
93 |
+
with st.spinner("جاري الاتصال بقاعدة البيانات..."):
|
94 |
+
# محاكاة تأخير الاتصال
|
95 |
+
import time
|
96 |
+
time.sleep(2)
|
97 |
+
|
98 |
+
# إنشاء بيانات نموذجية
|
99 |
+
self.data = self._create_sample_data()
|
100 |
+
|
101 |
+
st.success("تم الاتصال بقاعدة البيانات بنجاح!")
|
102 |
+
st.write("معاينة البيانات:")
|
103 |
+
st.dataframe(self.data.head())
|
104 |
+
|
105 |
+
elif data_source == "استخدام بيانات نموذجية":
|
106 |
+
if st.button("تحميل بيانات نموذجية"):
|
107 |
+
self.data = self._create_sample_data()
|
108 |
+
st.success("تم تحميل البيانات النموذجية بنجاح!")
|
109 |
+
st.write("معاينة البيانات:")
|
110 |
+
st.dataframe(self.data.head())
|
111 |
+
|
112 |
+
def _explore_data_tab(self):
|
113 |
+
"""علامة تبويب استكشاف البيانات"""
|
114 |
+
st.header("استكشاف البيانات")
|
115 |
+
|
116 |
+
if self.data is None:
|
117 |
+
st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
|
118 |
+
return
|
119 |
+
|
120 |
+
# عرض معلومات عامة عن البيانات
|
121 |
+
st.subheader("معلومات عامة")
|
122 |
+
col1, col2 = st.columns(2)
|
123 |
+
|
124 |
+
with col1:
|
125 |
+
st.write(f"عدد الصفوف: {self.data.shape[0]}")
|
126 |
+
st.write(f"عدد الأعمدة: {self.data.shape[1]}")
|
127 |
+
st.write(f"القيم المفقودة: {self.data.isna().sum().sum()}")
|
128 |
+
|
129 |
+
with col2:
|
130 |
+
st.write(f"أنواع البيانات:")
|
131 |
+
st.write(self.data.dtypes)
|
132 |
+
|
133 |
+
# عرض إحصاءات وصفية
|
134 |
+
st.subheader("إحصاءات وصفية")
|
135 |
+
st.dataframe(self.data.describe())
|
136 |
+
|
137 |
+
# عرض معلومات عن الأعمدة
|
138 |
+
st.subheader("معلومات الأعمدة")
|
139 |
+
|
140 |
+
selected_column = st.selectbox("اختر عمودًا لتحليله:", self.data.columns)
|
141 |
+
|
142 |
+
if selected_column:
|
143 |
+
col1, col2 = st.columns(2)
|
144 |
+
|
145 |
+
with col1:
|
146 |
+
st.write(f"نوع البيانات: {self.data[selected_column].dtype}")
|
147 |
+
st.write(f"القيم الفريدة: {self.data[selected_column].nunique()}")
|
148 |
+
st.write(f"القيم المفقودة: {self.data[selected_column].isna().sum()}")
|
149 |
+
|
150 |
+
with col2:
|
151 |
+
if pd.api.types.is_numeric_dtype(self.data[selected_column]):
|
152 |
+
st.write(f"الحد الأدنى: {self.data[selected_column].min()}")
|
153 |
+
st.write(f"الحد الأقصى: {self.data[selected_column].max()}")
|
154 |
+
st.write(f"المتوسط: {self.data[selected_column].mean()}")
|
155 |
+
st.write(f"الوسيط: {self.data[selected_column].median()}")
|
156 |
+
else:
|
157 |
+
st.write("القيم الأكثر تكرارًا:")
|
158 |
+
st.write(self.data[selected_column].value_counts().head())
|
159 |
+
|
160 |
+
# عرض رسم بياني للعمود المحدد
|
161 |
+
st.subheader(f"رسم بياني لـ {selected_column}")
|
162 |
+
|
163 |
+
if pd.api.types.is_numeric_dtype(self.data[selected_column]):
|
164 |
+
fig = px.histogram(self.data, x=selected_column, title=f"توزيع {selected_column}")
|
165 |
+
st.plotly_chart(fig, use_container_width=True)
|
166 |
+
else:
|
167 |
+
# الكود المعدل لحل مشكلة الرسم البياني
|
168 |
+
value_counts_df = self.data[selected_column].value_counts().reset_index()
|
169 |
+
value_counts_df.columns = ['القيمة', 'العدد'] # تسمية الأعمدة بأسماء واضحة
|
170 |
+
fig = px.bar(value_counts_df, x='القيمة', y='العدد', title=f"توزيع {selected_column}")
|
171 |
+
fig.update_layout(xaxis_title="القيمة", yaxis_title="العدد")
|
172 |
+
st.plotly_chart(fig, use_container_width=True)
|
173 |
+
|
174 |
+
def _advanced_analysis_tab(self):
|
175 |
+
"""علامة تبويب التحليل المتقدم"""
|
176 |
+
st.header("تحليل متقدم")
|
177 |
+
|
178 |
+
if self.data is None:
|
179 |
+
st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
|
180 |
+
return
|
181 |
+
|
182 |
+
# أنواع التحليل المتقدم
|
183 |
+
analysis_type = st.selectbox(
|
184 |
+
"اختر نوع التحليل:",
|
185 |
+
["تحليل الارتباط", "تحليل الاتجاهات", "تحليل المجموعات", "تحليل التباين"]
|
186 |
+
)
|
187 |
+
|
188 |
+
if analysis_type == "تحليل الارتباط":
|
189 |
+
st.subheader("تحليل الارتباط")
|
190 |
+
|
191 |
+
# اختيار الأعمدة الرقمية فقط
|
192 |
+
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
193 |
+
|
194 |
+
if len(numeric_columns) < 2:
|
195 |
+
st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإجراء تحليل الارتباط")
|
196 |
+
return
|
197 |
+
|
198 |
+
# حساب مصفوفة الارتباط
|
199 |
+
correlation_matrix = self.data[numeric_columns].corr()
|
200 |
+
|
201 |
+
# عرض مصفوفة الارتباط
|
202 |
+
st.write("مصفوفة الارتباط:")
|
203 |
+
st.dataframe(correlation_matrix)
|
204 |
+
|
205 |
+
# رسم خريطة حرارية للارتباط
|
206 |
+
st.write("خريطة حرارية للارتباط:")
|
207 |
+
fig = px.imshow(correlation_matrix, text_auto=True, aspect="auto",
|
208 |
+
title="خريطة حرارية لمصفوفة الارتباط")
|
209 |
+
st.plotly_chart(fig, use_container_width=True)
|
210 |
+
|
211 |
+
# تحليل الارتباط بين عمودين محددين
|
212 |
+
st.subheader("تحليل الارتباط بين عمودين محددين")
|
213 |
+
|
214 |
+
col1 = st.selectbox("اختر العمود الأول:", numeric_columns, key="corr_col1")
|
215 |
+
col2 = st.selectbox("اختر العمود الثاني:", numeric_columns, key="corr_col2")
|
216 |
+
|
217 |
+
if col1 != col2:
|
218 |
+
# حساب معامل الارتباط
|
219 |
+
correlation = self.data[col1].corr(self.data[col2])
|
220 |
+
|
221 |
+
st.write(f"معامل الارتباط بين {col1} و {col2}: {correlation:.4f}")
|
222 |
+
|
223 |
+
# رسم مخطط التشتت
|
224 |
+
fig = px.scatter(self.data, x=col1, y=col2, title=f"مخطط التشتت: {col1} مقابل {col2}")
|
225 |
+
fig.update_layout(xaxis_title=col1, yaxis_title=col2)
|
226 |
+
st.plotly_chart(fig, use_container_width=True)
|
227 |
+
else:
|
228 |
+
st.warning("الرجاء اختيار عمودين مختلفين")
|
229 |
+
|
230 |
+
elif analysis_type == "تحليل الاتجاهات":
|
231 |
+
st.subheader("تحليل الاتجاهات")
|
232 |
+
st.info("هذه الميزة قيد التطوير")
|
233 |
+
|
234 |
+
elif analysis_type == "تحليل المجموعات":
|
235 |
+
st.subheader("تحليل المجموعات")
|
236 |
+
st.info("هذه الميزة قيد التطوير")
|
237 |
+
|
238 |
+
elif analysis_type == "تحليل التباين":
|
239 |
+
st.subheader("تحليل التباين")
|
240 |
+
st.info("هذه الميزة قيد التطوير")
|
241 |
+
|
242 |
+
def _visualization_tab(self):
|
243 |
+
"""علامة تبويب التصور المرئي"""
|
244 |
+
st.header("التصور المرئي")
|
245 |
+
|
246 |
+
if self.data is None:
|
247 |
+
st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
|
248 |
+
return
|
249 |
+
|
250 |
+
# أنواع الرسوم البيانية
|
251 |
+
chart_type = st.selectbox(
|
252 |
+
"اختر نوع الرسم البياني:",
|
253 |
+
["مخطط شريطي", "مخطط خطي", "مخطط دائري", "مخطط تشتت", "مخطط صندوقي", "مخطط حراري"]
|
254 |
+
)
|
255 |
+
|
256 |
+
# اختيار الأعمدة حسب نوع الرسم البياني
|
257 |
+
if chart_type == "مخطط شريطي":
|
258 |
+
st.subheader("مخطط شريطي")
|
259 |
+
|
260 |
+
x_column = st.selectbox("اختر عمود المحور الأفقي (x):", self.data.columns, key="bar_x")
|
261 |
+
y_column = st.selectbox("اختر عمود المحور الرأسي (y):",
|
262 |
+
self.data.select_dtypes(include=['number']).columns.tolist(),
|
263 |
+
key="bar_y")
|
264 |
+
|
265 |
+
# خيارات إضافية
|
266 |
+
color_column = st.selectbox("اختر عمود اللون (اختياري):",
|
267 |
+
["لا يوجد"] + self.data.columns.tolist(),
|
268 |
+
key="bar_color")
|
269 |
+
|
270 |
+
# إنشاء الرسم البياني
|
271 |
+
if color_column == "لا يوجد":
|
272 |
+
fig = px.bar(self.data, x=x_column, y=y_column, title=f"{y_column} حسب {x_column}")
|
273 |
+
else:
|
274 |
+
fig = px.bar(self.data, x=x_column, y=y_column, color=color_column,
|
275 |
+
title=f"{y_column} حسب {x_column} (مصنف حسب {color_column})")
|
276 |
+
|
277 |
+
fig.update_layout(xaxis_title=x_column, yaxis_title=y_column)
|
278 |
+
st.plotly_chart(fig, use_container_width=True)
|
279 |
+
|
280 |
+
elif chart_type == "مخطط خطي":
|
281 |
+
st.subheader("مخطط خطي")
|
282 |
+
|
283 |
+
x_column = st.selectbox("اختر عمود المحور الأفقي (x):", self.data.columns, key="line_x")
|
284 |
+
y_columns = st.multiselect("اختر أعمدة المحور الرأسي (y):",
|
285 |
+
self.data.select_dtypes(include=['number']).columns.tolist(),
|
286 |
+
key="line_y")
|
287 |
+
|
288 |
+
if y_columns:
|
289 |
+
# إنشاء الرسم البياني
|
290 |
+
fig = go.Figure()
|
291 |
+
|
292 |
+
for y_column in y_columns:
|
293 |
+
fig.add_trace(go.Scatter(x=self.data[x_column], y=self.data[y_column],
|
294 |
+
mode='lines+markers', name=y_column))
|
295 |
+
|
296 |
+
fig.update_layout(title=f"مخطط خطي", xaxis_title=x_column, yaxis_title="القيمة")
|
297 |
+
st.plotly_chart(fig, use_container_width=True)
|
298 |
+
else:
|
299 |
+
st.warning("الرجاء اختيار عمود واحد على الأقل للمحور الرأسي")
|
300 |
+
|
301 |
+
elif chart_type == "مخطط دائري":
|
302 |
+
st.subheader("مخطط دائري")
|
303 |
+
|
304 |
+
column = st.selectbox("اختر العمود:", self.data.columns, key="pie_column")
|
305 |
+
|
306 |
+
# إنشاء الرسم البياني
|
307 |
+
# تعديل لحل مشكلة مماثلة في مخطط دائري
|
308 |
+
value_counts_df = self.data[column].value_counts().reset_index()
|
309 |
+
value_counts_df.columns = ['القيمة', 'العدد']
|
310 |
+
fig = px.pie(value_counts_df, names='القيمة', values='العدد', title=f"توزيع {column}")
|
311 |
+
st.plotly_chart(fig, use_container_width=True)
|
312 |
+
|
313 |
+
elif chart_type == "مخطط تشتت":
|
314 |
+
st.subheader("مخطط تشتت")
|
315 |
+
|
316 |
+
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
317 |
+
|
318 |
+
if len(numeric_columns) < 2:
|
319 |
+
st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإنشاء مخطط تشتت")
|
320 |
+
return
|
321 |
+
|
322 |
+
x_column = st.selectbox("اختر عمود المحور الأفقي (x):", numeric_columns, key="scatter_x")
|
323 |
+
y_column = st.selectbox("اختر عمود المحور الرأسي (y):", numeric_columns, key="scatter_y")
|
324 |
+
|
325 |
+
# خيارات إضافية
|
326 |
+
color_column = st.selectbox("اختر عمود اللون (اختياري):",
|
327 |
+
["لا يوجد"] + self.data.columns.tolist(),
|
328 |
+
key="scatter_color")
|
329 |
+
|
330 |
+
size_column = st.selectbox("اختر عمود الحجم (اختياري):",
|
331 |
+
["لا يوجد"] + numeric_columns,
|
332 |
+
key="scatter_size")
|
333 |
+
|
334 |
+
# إنشاء الرسم البياني
|
335 |
+
if color_column == "لا يوجد" and size_column == "لا يوجد":
|
336 |
+
fig = px.scatter(self.data, x=x_column, y=y_column,
|
337 |
+
title=f"{y_column} مقابل {x_column}")
|
338 |
+
elif color_column != "لا يوجد" and size_column == "لا يوجد":
|
339 |
+
fig = px.scatter(self.data, x=x_column, y=y_column, color=color_column,
|
340 |
+
title=f"{y_column} مقابل {x_column} (مصنف حسب {color_column})")
|
341 |
+
elif color_column == "لا يوجد" and size_column != "لا يوجد":
|
342 |
+
fig = px.scatter(self.data, x=x_column, y=y_column, size=size_column,
|
343 |
+
title=f"{y_column} مقابل {x_column} (الحجم حسب {size_column})")
|
344 |
+
else:
|
345 |
+
fig = px.scatter(self.data, x=x_column, y=y_column, color=color_column, size=size_column,
|
346 |
+
title=f"{y_column} مقابل {x_column} (مصنف حسب {color_column}, الحجم حسب {size_column})")
|
347 |
+
|
348 |
+
fig.update_layout(xaxis_title=x_column, yaxis_title=y_column)
|
349 |
+
st.plotly_chart(fig, use_container_width=True)
|
350 |
+
|
351 |
+
elif chart_type == "مخطط صندوقي":
|
352 |
+
st.subheader("مخطط صندوقي")
|
353 |
+
|
354 |
+
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
355 |
+
|
356 |
+
if not numeric_columns:
|
357 |
+
st.warning("يجب أن يكون هناك عمود رقمي واحد على الأقل لإنشاء مخطط صندوقي")
|
358 |
+
return
|
359 |
+
|
360 |
+
y_column = st.selectbox("اختر عمود القيمة:", numeric_columns, key="box_y")
|
361 |
+
|
362 |
+
# خيارات إضافية
|
363 |
+
x_column = st.selectbox("اختر عمود التصنيف (اختياري):",
|
364 |
+
["لا يوجد"] + self.data.columns.tolist(),
|
365 |
+
key="box_x")
|
366 |
+
|
367 |
+
# إنشاء الرسم البياني
|
368 |
+
if x_column == "لا يوجد":
|
369 |
+
fig = px.box(self.data, y=y_column, title=f"مخطط صندوقي لـ {y_column}")
|
370 |
+
else:
|
371 |
+
fig = px.box(self.data, x=x_column, y=y_column,
|
372 |
+
title=f"مخطط صندوقي لـ {y_column} حسب {x_column}")
|
373 |
+
|
374 |
+
st.plotly_chart(fig, use_container_width=True)
|
375 |
+
|
376 |
+
elif chart_type == "مخطط حراري":
|
377 |
+
st.subheader("مخطط حراري")
|
378 |
+
|
379 |
+
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
380 |
+
|
381 |
+
if len(numeric_columns) < 2:
|
382 |
+
st.warning("يجب أن يكون هناك عمودان رقميان على الأقل لإنشاء مخطط حراري")
|
383 |
+
return
|
384 |
+
|
385 |
+
# اختيار الأعمدة للمخطط الحراري
|
386 |
+
selected_columns = st.multiselect("اختر الأعمدة للمخطط الحراري:",
|
387 |
+
numeric_columns,
|
388 |
+
default=numeric_columns[:5] if len(numeric_columns) > 5 else numeric_columns)
|
389 |
+
|
390 |
+
if selected_columns:
|
391 |
+
# حساب مصفوفة الارتباط
|
392 |
+
correlation_matrix = self.data[selected_columns].corr()
|
393 |
+
|
394 |
+
# إنشاء الرسم البياني
|
395 |
+
fig = px.imshow(correlation_matrix, text_auto=True, aspect="auto",
|
396 |
+
title="مخطط حراري لمصفوفة الارتباط")
|
397 |
+
st.plotly_chart(fig, use_container_width=True)
|
398 |
+
else:
|
399 |
+
st.warning("الرجاء اختيار عمود واحد على الأقل")
|
400 |
+
|
401 |
+
def _reports_tab(self):
|
402 |
+
"""علامة تبويب التقارير"""
|
403 |
+
st.header("التقارير")
|
404 |
+
|
405 |
+
if self.data is None:
|
406 |
+
st.info("الرجاء تحميل البيانات أولاً من علامة تبويب 'تحميل البيانات'")
|
407 |
+
return
|
408 |
+
|
409 |
+
st.subheader("إنشاء تقرير")
|
410 |
+
|
411 |
+
# خيارات التقرير
|
412 |
+
report_type = st.selectbox(
|
413 |
+
"اختر نوع التقرير:",
|
414 |
+
["تقرير ملخص", "تقرير تحليلي", "تقرير مقارنة"]
|
415 |
+
)
|
416 |
+
|
417 |
+
if report_type == "تقرير ملخص":
|
418 |
+
st.write("محتوى التقرير:")
|
419 |
+
|
420 |
+
# إنشاء ملخص للبيانات
|
421 |
+
st.write("### ملخص البيانات")
|
422 |
+
st.write(f"عدد الصفوف: {self.data.shape[0]}")
|
423 |
+
st.write(f"عدد الأعمدة: {self.data.shape[1]}")
|
424 |
+
|
425 |
+
# إحصاءات وصفية
|
426 |
+
st.write("### إحصاءات وصفية")
|
427 |
+
st.dataframe(self.data.describe())
|
428 |
+
|
429 |
+
# معلومات عن القيم المفقودة
|
430 |
+
st.write("### القيم المفقودة")
|
431 |
+
missing_data = pd.DataFrame({
|
432 |
+
'العمود': self.data.columns,
|
433 |
+
'عدد القيم المفقودة': self.data.isna().sum().values,
|
434 |
+
'نسبة القيم المفقودة (%)': (self.data.isna().sum().values / len(self.data) * 100).round(2)
|
435 |
+
})
|
436 |
+
st.dataframe(missing_data)
|
437 |
+
|
438 |
+
# توزيع البيانات الرقمية
|
439 |
+
st.write("### توزيع البيانات الرقمية")
|
440 |
+
numeric_columns = self.data.select_dtypes(include=['number']).columns.tolist()
|
441 |
+
|
442 |
+
if numeric_columns:
|
443 |
+
for i in range(0, len(numeric_columns), 2):
|
444 |
+
cols = st.columns(2)
|
445 |
+
for j in range(2):
|
446 |
+
if i + j < len(numeric_columns):
|
447 |
+
col = numeric_columns[i + j]
|
448 |
+
with cols[j]:
|
449 |
+
fig = px.histogram(self.data, x=col, title=f"توزيع {col}")
|
450 |
+
st.plotly_chart(fig, use_container_width=True)
|
451 |
+
|
452 |
+
# خيارات تصدير التقرير
|
453 |
+
st.subheader("تصدير التقرير")
|
454 |
+
export_format = st.radio("اختر صيغة التصدير:", ["PDF", "Excel", "HTML"])
|
455 |
+
|
456 |
+
if st.button("تصدير التقرير"):
|
457 |
+
st.success(f"تم تصدير التقرير بصيغة {export_format} بنجاح!")
|
458 |
+
|
459 |
+
elif report_type == "تقرير تحليلي":
|
460 |
+
st.info("هذه الميزة قيد التطوير")
|
461 |
+
|
462 |
+
elif report_type == "تقرير مقارنة":
|
463 |
+
st.info("هذه الميزة قيد التطوير")
|
464 |
+
|
465 |
+
def _create_sample_data(self):
|
466 |
+
"""إنشاء بيانات نموذجية للمناقصات"""
|
467 |
+
# إنشاء تواريخ عشوائية
|
468 |
+
start_date = datetime(2023, 1, 1)
|
469 |
+
end_date = datetime(2025, 3, 31)
|
470 |
+
days = (end_date - start_date).days
|
471 |
+
|
472 |
+
# إنشاء بيانات نموذجية
|
473 |
+
data = {
|
474 |
+
'رقم المناقصة': [f'T-{i:04d}' for i in range(1, 101)],
|
475 |
+
'اسم المشروع': [f'مشروع {i}' for i in range(1, 101)],
|
476 |
+
'نوع المشروع': np.random.choice(['بناء', 'صيانة', 'تطوير', 'توريد', 'خدمات'], 100),
|
477 |
+
'الموقع': np.random.choice(['الرياض', 'جدة', 'الدمام', 'مكة', 'المدينة', 'تبوك', 'أبها'], 100),
|
478 |
+
'تاريخ الإعلان': [start_date + pd.Timedelta(days=np.random.randint(0, days)) for _ in range(100)],
|
479 |
+
'تاريخ الإغلاق': [start_date + pd.Timedelta(days=np.random.randint(30, days)) for _ in range(100)],
|
480 |
+
'الميزانية التقديرية': np.random.uniform(1000000, 50000000, 100),
|
481 |
+
'عدد المتقدمين': np.random.randint(1, 20, 100),
|
482 |
+
'سعر العرض': np.random.uniform(900000, 55000000, 100),
|
483 |
+
'نسبة الفوز (%)': np.random.uniform(0, 100, 100),
|
484 |
+
'مدة التنفيذ (أشهر)': np.random.randint(3, 36, 100),
|
485 |
+
'عدد العمال': np.random.randint(10, 500, 100),
|
486 |
+
'تكلفة المواد': np.random.uniform(500000, 30000000, 100),
|
487 |
+
'تكلفة العمالة': np.random.uniform(200000, 15000000, 100),
|
488 |
+
'تكلفة المعدات': np.random.uniform(100000, 10000000, 100),
|
489 |
+
'هامش الربح (%)': np.random.uniform(5, 25, 100),
|
490 |
+
'درجة المخاطرة': np.random.choice(['منخفضة', 'متوسطة', 'عالية'], 100),
|
491 |
+
'حالة المناقصة': np.random.choice(['جارية', 'مغلقة', 'ملغاة', 'فائزة', 'خاسرة'], 100)
|
492 |
+
}
|
493 |
+
|
494 |
+
# إنشاء DataFrame
|
495 |
+
df = pd.DataFrame(data)
|
496 |
+
|
497 |
+
# إضافة بعض العلاقات المنطقية
|
498 |
+
df['إجمالي التكلفة'] = df['تكلفة المواد'] + df['تكلفة العمالة'] + df['تكلفة المعدات']
|
499 |
+
df['الربح المتوقع'] = df['سعر العرض'] - df['إجمالي التكلفة']
|
500 |
+
df['نسبة التكلفة من العرض (%)'] = (df['إجمالي التكلفة'] / df['سعر العرض'] * 100).round(2)
|
501 |
+
|
502 |
+
return df
|
modules/document_analysis/analyzer.py
CHANGED
@@ -1,281 +1,281 @@
|
|
1 |
-
"""
|
2 |
-
وحدة تحليل المستندات لنظام إدارة المناقصات - Hybrid Face
|
3 |
-
"""
|
4 |
-
|
5 |
-
import os
|
6 |
-
import re
|
7 |
-
import logging
|
8 |
-
import threading
|
9 |
-
from pathlib import Path
|
10 |
-
import datetime
|
11 |
-
import json
|
12 |
-
|
13 |
-
# تهيئة السجل
|
14 |
-
logging.basicConfig(
|
15 |
-
level=logging.INFO,
|
16 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
-
)
|
18 |
-
logger = logging.getLogger('document_analysis')
|
19 |
-
|
20 |
-
class DocumentAnalyzer:
|
21 |
-
"""فئة تحليل المستندات"""
|
22 |
-
|
23 |
-
def __init__(self, config=None):
|
24 |
-
"""تهيئة محلل المستندات"""
|
25 |
-
self.config = config
|
26 |
-
self.analysis_in_progress = False
|
27 |
-
self.current_document = None
|
28 |
-
self.analysis_results = {}
|
29 |
-
|
30 |
-
# إنشاء مجلد المستندات إذا لم يكن موجوداً
|
31 |
-
if config and hasattr(config, 'DOCUMENTS_PATH'):
|
32 |
-
self.documents_path = Path(config.DOCUMENTS_PATH)
|
33 |
-
else:
|
34 |
-
self.documents_path = Path('data/documents')
|
35 |
-
|
36 |
-
if not self.documents_path.exists():
|
37 |
-
self.documents_path.mkdir(parents=True, exist_ok=True)
|
38 |
-
|
39 |
-
def analyze_document(self, document_path, document_type="tender", callback=None):
|
40 |
-
"""تحليل
|
41 |
-
if self.analysis_in_progress:
|
42 |
-
logger.warning("هناك عملية تحليل جارية بالفعل")
|
43 |
-
return False
|
44 |
-
|
45 |
-
if not os.path.exists(document_path):
|
46 |
-
logger.error(f"المستند غير موجود: {document_path}")
|
47 |
-
return False
|
48 |
-
|
49 |
-
self.analysis_in_progress = True
|
50 |
-
self.current_document = document_path
|
51 |
-
self.analysis_results = {
|
52 |
-
"document_path": document_path,
|
53 |
-
"document_type": document_type,
|
54 |
-
"analysis_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
55 |
-
"status": "جاري التحليل",
|
56 |
-
"items": [],
|
57 |
-
"entities": [],
|
58 |
-
"dates": [],
|
59 |
-
"amounts": [],
|
60 |
-
"risks": []
|
61 |
-
}
|
62 |
-
|
63 |
-
# بدء التحليل في خيط منفصل
|
64 |
-
thread = threading.Thread(
|
65 |
-
target=self._analyze_document_thread,
|
66 |
-
args=(document_path, document_type, callback)
|
67 |
-
)
|
68 |
-
thread.daemon = True
|
69 |
-
thread.start()
|
70 |
-
|
71 |
-
return True
|
72 |
-
|
73 |
-
def _analyze_document_thread(self, document_path, document_type, callback):
|
74 |
-
"""خيط تحليل المستند"""
|
75 |
-
try:
|
76 |
-
# تحديد نوع المستند
|
77 |
-
file_extension = os.path.splitext(document_path)[1].lower()
|
78 |
-
|
79 |
-
if file_extension == '.pdf':
|
80 |
-
self._analyze_pdf(document_path, document_type)
|
81 |
-
elif file_extension == '.docx':
|
82 |
-
self._analyze_docx(document_path, document_type)
|
83 |
-
elif file_extension == '.xlsx':
|
84 |
-
self._analyze_xlsx(document_path, document_type)
|
85 |
-
elif file_extension == '.txt':
|
86 |
-
self._analyze_txt(document_path, document_type)
|
87 |
-
else:
|
88 |
-
logger.error(f"نوع المستند غير مدعوم: {file_extension}")
|
89 |
-
self.analysis_results["status"] = "فشل التحليل"
|
90 |
-
self.analysis_results["error"] = "نوع المستند غير مدعوم"
|
91 |
-
|
92 |
-
# تحديث حالة التحليل
|
93 |
-
if self.analysis_results["status"] != "فشل التحليل":
|
94 |
-
self.analysis_results["status"] = "اكتمل التحليل"
|
95 |
-
self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
96 |
-
|
97 |
-
logger.info(f"اكتمل تحليل المستند: {document_path}")
|
98 |
-
|
99 |
-
except Exception as e:
|
100 |
-
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
101 |
-
self.analysis_results["status"] = "فشل التحليل"
|
102 |
-
self.analysis_results["error"] = str(e)
|
103 |
-
|
104 |
-
finally:
|
105 |
-
self.analysis_in_progress = False
|
106 |
-
|
107 |
-
# استدعاء دالة الاستجابة إذا تم توفيرها
|
108 |
-
if callback and callable(callback):
|
109 |
-
callback(self.analysis_results)
|
110 |
-
|
111 |
-
def _analyze_pdf(self, document_path, document_type):
|
112 |
-
"""تحليل مستند PDF"""
|
113 |
-
try:
|
114 |
-
# محاكاة تحليل مستند PDF
|
115 |
-
logger.info(f"تحليل مستند PDF: {document_path}")
|
116 |
-
|
117 |
-
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل PyPDF2 أو pdfplumber
|
118 |
-
# لاستخراج النص من ملف PDF وتحليله
|
119 |
-
|
120 |
-
# محاكاة استخراج البنود
|
121 |
-
self.analysis_results["items"] = [
|
122 |
-
{"id": 1, "name": "أعمال الحفر", "description": "حفر وإزالة التربة", "unit": "م³", "estimated_quantity": 1500},
|
123 |
-
{"id": 2, "name": "أعمال الخرسانة", "description": "صب خرسانة مسلحة", "unit": "م³", "estimated_quantity": 750},
|
124 |
-
{"id": 3, "name": "أعمال الأسفلت", "description": "تمهيد وفرش طبقة أسفلت", "unit": "م²", "estimated_quantity": 5000}
|
125 |
-
]
|
126 |
-
|
127 |
-
# محاكاة استخراج الكيانات
|
128 |
-
self.analysis_results["entities"] = [
|
129 |
-
{"type": "client", "name": "وزارة النقل", "mentions": 5},
|
130 |
-
{"type": "location", "name": "المنطقة الشرقية", "mentions": 3},
|
131 |
-
{"type": "contractor", "name": "شركة المقاولات المتحدة", "mentions": 2}
|
132 |
-
]
|
133 |
-
|
134 |
-
# محاكاة استخراج التواريخ
|
135 |
-
self.analysis_results["dates"] = [
|
136 |
-
{"type": "start_date", "date": "2025-05-01", "description": "تاريخ بدء المشروع"},
|
137 |
-
{"type": "end_date", "date": "2025-11-30", "description": "تاريخ انتهاء المشروع"},
|
138 |
-
{"type": "submission_date", "date": "2025-04-15", "description": "تاريخ تقديم العروض"}
|
139 |
-
]
|
140 |
-
|
141 |
-
# محاكاة استخراج المبالغ
|
142 |
-
self.analysis_results["amounts"] = [
|
143 |
-
{"type": "estimated_cost", "amount": 5000000, "currency": "SAR", "description": "التكلفة التقديرية للمشروع"},
|
144 |
-
{"type": "advance_payment", "amount": 500000, "currency": "SAR", "description": "الدفعة المقدمة (10%)"},
|
145 |
-
{"type": "performance_bond", "amount": 250000, "currency": "SAR", "description": "ضمان حسن التنفيذ (5%)"}
|
146 |
-
]
|
147 |
-
|
148 |
-
# محاكاة استخراج المخاطر
|
149 |
-
self.analysis_results["risks"] = [
|
150 |
-
{"type": "delay_risk", "description": "مخاطر التأخير في التنفيذ", "probability": "متوسط", "impact": "عالي"},
|
151 |
-
{"type": "cost_risk", "description": "مخاطر زيادة التكاليف", "probability": "عالي", "impact": "عالي"},
|
152 |
-
{"type": "quality_risk", "description": "مخاطر جودة التنفيذ", "probability": "منخفض", "impact": "متوسط"}
|
153 |
-
]
|
154 |
-
|
155 |
-
except Exception as e:
|
156 |
-
logger.error(f"خطأ في تحليل مستند PDF: {str(e)}")
|
157 |
-
raise
|
158 |
-
|
159 |
-
def _analyze_docx(self, document_path, document_type):
|
160 |
-
"""تحليل مستند Word"""
|
161 |
-
try:
|
162 |
-
# محاكاة تحليل مستند Word
|
163 |
-
logger.info(f"تحليل مستند Word: {document_path}")
|
164 |
-
|
165 |
-
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل python-docx
|
166 |
-
# لاستخراج النص من ملف Word وتحليله
|
167 |
-
|
168 |
-
# محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
|
169 |
-
# (مشابه لتحليل PDF)
|
170 |
-
self.analysis_results["items"] = [
|
171 |
-
{"id": 1, "name": "توريد معدات", "description": "توريد معدات المشروع", "unit": "مجموعة", "estimated_quantity": 10},
|
172 |
-
{"id": 2, "name": "تركيب المعدات", "description": "تركيب وتشغيل المعدات", "unit": "مجموعة", "estimated_quantity": 10},
|
173 |
-
{"id": 3, "name": "التدريب", "description": "تدريب الموظفين على استخدام المعدات", "unit": "يوم", "estimated_quantity": 20}
|
174 |
-
]
|
175 |
-
|
176 |
-
# محاكاة استخراج الكيانات والتواريخ والمبالغ والمخاطر
|
177 |
-
# (مشابه لتحليل PDF)
|
178 |
-
|
179 |
-
except Exception as e:
|
180 |
-
logger.error(f"خطأ في تحليل مستند Word: {str(e)}")
|
181 |
-
raise
|
182 |
-
|
183 |
-
def _analyze_xlsx(self, document_path, document_type):
|
184 |
-
"""تحليل مستند Excel"""
|
185 |
-
try:
|
186 |
-
# محاكاة تحليل مستند Excel
|
187 |
-
logger.info(f"تحليل مستند Excel: {document_path}")
|
188 |
-
|
189 |
-
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل pandas أو openpyxl
|
190 |
-
# لاستخراج البيانات من ملف Excel وتحليلها
|
191 |
-
|
192 |
-
# محاكاة استخراج البنود
|
193 |
-
self.analysis_results["items"] = [
|
194 |
-
{"id": 1, "name": "بند 1", "description": "وصف البند 1", "unit": "وحدة", "estimated_quantity": 100},
|
195 |
-
{"id": 2, "name": "بند 2", "description": "وصف البند 2", "unit": "وحدة", "estimated_quantity": 200},
|
196 |
-
{"id": 3, "name": "بند 3", "description": "وصف البند 3", "unit": "وحدة", "estimated_quantity": 300}
|
197 |
-
]
|
198 |
-
|
199 |
-
# محاكاة استخراج المبالغ
|
200 |
-
self.analysis_results["amounts"] = [
|
201 |
-
{"type": "item_cost", "amount": 10000, "currency": "SAR", "description": "تكلفة البند 1"},
|
202 |
-
{"type": "item_cost", "amount": 20000, "currency": "SAR", "description": "تكلفة البند 2"},
|
203 |
-
{"type": "item_cost", "amount": 30000, "currency": "SAR", "description": "تكلفة البند 3"}
|
204 |
-
]
|
205 |
-
|
206 |
-
except Exception as e:
|
207 |
-
logger.error(f"خطأ في تحليل مستند Excel: {str(e)}")
|
208 |
-
raise
|
209 |
-
|
210 |
-
def _analyze_txt(self, document_path, document_type):
|
211 |
-
"""تحليل مستند نصي"""
|
212 |
-
try:
|
213 |
-
# محاكاة تحليل مستند نصي
|
214 |
-
logger.info(f"تحليل مستند نصي: {document_path}")
|
215 |
-
|
216 |
-
# في التطبيق الفعلي، سيتم قراءة الملف النصي وتحليله
|
217 |
-
|
218 |
-
# محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
|
219 |
-
# (مشابه للتحليلات الأخرى)
|
220 |
-
|
221 |
-
except Exception as e:
|
222 |
-
logger.error(f"خطأ في تحليل مستند نصي: {str(e)}")
|
223 |
-
raise
|
224 |
-
|
225 |
-
def get_analysis_status(self):
|
226 |
-
"""الحصول على حالة التحليل الحالي"""
|
227 |
-
if not self.analysis_in_progress:
|
228 |
-
if not self.analysis_results:
|
229 |
-
return {"status": "لا يوجد تحليل جارٍ"}
|
230 |
-
else:
|
231 |
-
return {"status": self.analysis_results.get("status", "غير معروف")}
|
232 |
-
|
233 |
-
return {
|
234 |
-
"status": "جاري التحليل",
|
235 |
-
"document_path": self.current_document,
|
236 |
-
"start_time": self.analysis_results.get("analysis_start_time")
|
237 |
-
}
|
238 |
-
|
239 |
-
def get_analysis_results(self):
|
240 |
-
"""الحصول على نتائج التحليل"""
|
241 |
-
return self.analysis_results
|
242 |
-
|
243 |
-
def export_analysis_results(self, output_path=None):
|
244 |
-
"""تصدير نتائج التحليل إلى ملف JSON"""
|
245 |
-
if not self.analysis_results:
|
246 |
-
logger.warning("لا توجد نتائج تحليل للتصدير")
|
247 |
-
return None
|
248 |
-
|
249 |
-
if not output_path:
|
250 |
-
# إنشاء اسم ملف افتراضي
|
251 |
-
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
252 |
-
filename = f"analysis_results_{timestamp}.json"
|
253 |
-
output_path = os.path.join(self.documents_path, filename)
|
254 |
-
|
255 |
-
try:
|
256 |
-
with open(output_path, 'w', encoding='utf-8') as f:
|
257 |
-
json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
|
258 |
-
|
259 |
-
logger.info(f"تم تصدير نتائج التحليل إلى: {output_path}")
|
260 |
-
return output_path
|
261 |
-
|
262 |
-
except Exception as e:
|
263 |
-
logger.error(f"خطأ في تصدير نتائج التحليل: {str(e)}")
|
264 |
-
return None
|
265 |
-
|
266 |
-
def import_analysis_results(self, input_path):
|
267 |
-
"""استيراد نتائج التحليل من ملف JSON"""
|
268 |
-
if not os.path.exists(input_path):
|
269 |
-
logger.error(f"ملف نتائج التحليل غير موجود: {input_path}")
|
270 |
-
return False
|
271 |
-
|
272 |
-
try:
|
273 |
-
with open(input_path, 'r', encoding='utf-8') as f:
|
274 |
-
self.analysis_results = json.load(f)
|
275 |
-
|
276 |
-
logger.info(f"تم استيراد نتائج التحليل من: {input_path}")
|
277 |
-
return True
|
278 |
-
|
279 |
-
except Exception as e:
|
280 |
-
logger.error(f"خطأ في استيراد نتائج التحليل: {str(e)}")
|
281 |
-
return False
|
|
|
1 |
+
"""
|
2 |
+
وحدة تحليل المستندات لنظام إدارة المناقصات - Hybrid Face
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import re
|
7 |
+
import logging
|
8 |
+
import threading
|
9 |
+
from pathlib import Path
|
10 |
+
import datetime
|
11 |
+
import json
|
12 |
+
|
13 |
+
# تهيئة السجل
|
14 |
+
logging.basicConfig(
|
15 |
+
level=logging.INFO,
|
16 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
+
)
|
18 |
+
logger = logging.getLogger('document_analysis')
|
19 |
+
|
20 |
+
class DocumentAnalyzer:
|
21 |
+
"""فئة تحليل المستندات"""
|
22 |
+
|
23 |
+
def __init__(self, config=None):
|
24 |
+
"""تهيئة محلل المستندات"""
|
25 |
+
self.config = config
|
26 |
+
self.analysis_in_progress = False
|
27 |
+
self.current_document = None
|
28 |
+
self.analysis_results = {}
|
29 |
+
|
30 |
+
# إنشاء مجلد المستندات إذا لم يكن موجوداً
|
31 |
+
if config and hasattr(config, 'DOCUMENTS_PATH'):
|
32 |
+
self.documents_path = Path(config.DOCUMENTS_PATH)
|
33 |
+
else:
|
34 |
+
self.documents_path = Path('data/documents')
|
35 |
+
|
36 |
+
if not self.documents_path.exists():
|
37 |
+
self.documents_path.mkdir(parents=True, exist_ok=True)
|
38 |
+
|
39 |
+
def analyze_document(self, document_path, document_type="tender", callback=None):
|
40 |
+
"""تحليل مست��د"""
|
41 |
+
if self.analysis_in_progress:
|
42 |
+
logger.warning("هناك عملية تحليل جارية بالفعل")
|
43 |
+
return False
|
44 |
+
|
45 |
+
if not os.path.exists(document_path):
|
46 |
+
logger.error(f"المستند غير موجود: {document_path}")
|
47 |
+
return False
|
48 |
+
|
49 |
+
self.analysis_in_progress = True
|
50 |
+
self.current_document = document_path
|
51 |
+
self.analysis_results = {
|
52 |
+
"document_path": document_path,
|
53 |
+
"document_type": document_type,
|
54 |
+
"analysis_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
55 |
+
"status": "جاري التحليل",
|
56 |
+
"items": [],
|
57 |
+
"entities": [],
|
58 |
+
"dates": [],
|
59 |
+
"amounts": [],
|
60 |
+
"risks": []
|
61 |
+
}
|
62 |
+
|
63 |
+
# بدء التحليل في خيط منفصل
|
64 |
+
thread = threading.Thread(
|
65 |
+
target=self._analyze_document_thread,
|
66 |
+
args=(document_path, document_type, callback)
|
67 |
+
)
|
68 |
+
thread.daemon = True
|
69 |
+
thread.start()
|
70 |
+
|
71 |
+
return True
|
72 |
+
|
73 |
+
def _analyze_document_thread(self, document_path, document_type, callback):
|
74 |
+
"""خيط تحليل المستند"""
|
75 |
+
try:
|
76 |
+
# تحديد نوع المستند
|
77 |
+
file_extension = os.path.splitext(document_path)[1].lower()
|
78 |
+
|
79 |
+
if file_extension == '.pdf':
|
80 |
+
self._analyze_pdf(document_path, document_type)
|
81 |
+
elif file_extension == '.docx':
|
82 |
+
self._analyze_docx(document_path, document_type)
|
83 |
+
elif file_extension == '.xlsx':
|
84 |
+
self._analyze_xlsx(document_path, document_type)
|
85 |
+
elif file_extension == '.txt':
|
86 |
+
self._analyze_txt(document_path, document_type)
|
87 |
+
else:
|
88 |
+
logger.error(f"نوع المستند غير مدعوم: {file_extension}")
|
89 |
+
self.analysis_results["status"] = "فشل التحليل"
|
90 |
+
self.analysis_results["error"] = "نوع المستند غير مدعوم"
|
91 |
+
|
92 |
+
# تحديث حالة التحليل
|
93 |
+
if self.analysis_results["status"] != "فشل التحليل":
|
94 |
+
self.analysis_results["status"] = "اكتمل التحليل"
|
95 |
+
self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
96 |
+
|
97 |
+
logger.info(f"اكتمل تحليل المستند: {document_path}")
|
98 |
+
|
99 |
+
except Exception as e:
|
100 |
+
logger.error(f"خطأ في تحليل المستند: {str(e)}")
|
101 |
+
self.analysis_results["status"] = "فشل التحليل"
|
102 |
+
self.analysis_results["error"] = str(e)
|
103 |
+
|
104 |
+
finally:
|
105 |
+
self.analysis_in_progress = False
|
106 |
+
|
107 |
+
# استدعاء دالة الاستجابة إذا تم توفيرها
|
108 |
+
if callback and callable(callback):
|
109 |
+
callback(self.analysis_results)
|
110 |
+
|
111 |
+
def _analyze_pdf(self, document_path, document_type):
|
112 |
+
"""تحليل مستند PDF"""
|
113 |
+
try:
|
114 |
+
# محاكاة تحليل مستند PDF
|
115 |
+
logger.info(f"تحليل مستند PDF: {document_path}")
|
116 |
+
|
117 |
+
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل PyPDF2 أو pdfplumber
|
118 |
+
# لاستخراج النص من ملف PDF وتحليله
|
119 |
+
|
120 |
+
# محاكاة استخراج البنود
|
121 |
+
self.analysis_results["items"] = [
|
122 |
+
{"id": 1, "name": "أعمال الحفر", "description": "حفر وإزالة التربة", "unit": "م³", "estimated_quantity": 1500},
|
123 |
+
{"id": 2, "name": "أعمال الخرسانة", "description": "صب خرسانة مسلحة", "unit": "م³", "estimated_quantity": 750},
|
124 |
+
{"id": 3, "name": "أعمال الأسفلت", "description": "تمهيد وفرش طبقة أسفلت", "unit": "م²", "estimated_quantity": 5000}
|
125 |
+
]
|
126 |
+
|
127 |
+
# محاكاة استخراج الكيانات
|
128 |
+
self.analysis_results["entities"] = [
|
129 |
+
{"type": "client", "name": "وزارة النقل", "mentions": 5},
|
130 |
+
{"type": "location", "name": "المنطقة الشرقية", "mentions": 3},
|
131 |
+
{"type": "contractor", "name": "شركة المقاولات المتحدة", "mentions": 2}
|
132 |
+
]
|
133 |
+
|
134 |
+
# محاكاة استخراج التواريخ
|
135 |
+
self.analysis_results["dates"] = [
|
136 |
+
{"type": "start_date", "date": "2025-05-01", "description": "تاريخ بدء المشروع"},
|
137 |
+
{"type": "end_date", "date": "2025-11-30", "description": "تاريخ انتهاء المشروع"},
|
138 |
+
{"type": "submission_date", "date": "2025-04-15", "description": "تاريخ تقديم العروض"}
|
139 |
+
]
|
140 |
+
|
141 |
+
# محاكاة استخراج المبالغ
|
142 |
+
self.analysis_results["amounts"] = [
|
143 |
+
{"type": "estimated_cost", "amount": 5000000, "currency": "SAR", "description": "التكلفة التقديرية للمشروع"},
|
144 |
+
{"type": "advance_payment", "amount": 500000, "currency": "SAR", "description": "الدفعة المقدمة (10%)"},
|
145 |
+
{"type": "performance_bond", "amount": 250000, "currency": "SAR", "description": "ضمان حسن التنفيذ (5%)"}
|
146 |
+
]
|
147 |
+
|
148 |
+
# محاكاة استخراج المخاطر
|
149 |
+
self.analysis_results["risks"] = [
|
150 |
+
{"type": "delay_risk", "description": "مخاطر التأخير في التنفيذ", "probability": "متوسط", "impact": "عالي"},
|
151 |
+
{"type": "cost_risk", "description": "مخاطر زيادة التكاليف", "probability": "عالي", "impact": "عالي"},
|
152 |
+
{"type": "quality_risk", "description": "مخاطر جودة التنفيذ", "probability": "منخفض", "impact": "متوسط"}
|
153 |
+
]
|
154 |
+
|
155 |
+
except Exception as e:
|
156 |
+
logger.error(f"خطأ في تحليل مستند PDF: {str(e)}")
|
157 |
+
raise
|
158 |
+
|
159 |
+
def _analyze_docx(self, document_path, document_type):
|
160 |
+
"""تحليل مستند Word"""
|
161 |
+
try:
|
162 |
+
# محاكاة تحليل مستند Word
|
163 |
+
logger.info(f"تحليل مستند Word: {document_path}")
|
164 |
+
|
165 |
+
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل python-docx
|
166 |
+
# لاستخراج النص من ملف Word وتحليله
|
167 |
+
|
168 |
+
# محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
|
169 |
+
# (مشابه لتحليل PDF)
|
170 |
+
self.analysis_results["items"] = [
|
171 |
+
{"id": 1, "name": "توريد معدات", "description": "توريد معدات المشروع", "unit": "مجموعة", "estimated_quantity": 10},
|
172 |
+
{"id": 2, "name": "تركيب المعدات", "description": "تركيب وتشغيل المعدات", "unit": "مجموعة", "estimated_quantity": 10},
|
173 |
+
{"id": 3, "name": "التدريب", "description": "تدريب الموظفين على استخدام المعدات", "unit": "يوم", "estimated_quantity": 20}
|
174 |
+
]
|
175 |
+
|
176 |
+
# محاكاة استخراج الكيانات والتواريخ والمبالغ والمخاطر
|
177 |
+
# (مشابه لتحليل PDF)
|
178 |
+
|
179 |
+
except Exception as e:
|
180 |
+
logger.error(f"خطأ في تحليل مستند Word: {str(e)}")
|
181 |
+
raise
|
182 |
+
|
183 |
+
def _analyze_xlsx(self, document_path, document_type):
|
184 |
+
"""تحليل مستند Excel"""
|
185 |
+
try:
|
186 |
+
# محاكاة تحليل مستند Excel
|
187 |
+
logger.info(f"تحليل مستند Excel: {document_path}")
|
188 |
+
|
189 |
+
# في التطبيق الفعلي، سيتم استخدام مكتبة مثل pandas أو openpyxl
|
190 |
+
# لاستخراج البيانات من ملف Excel وتحليلها
|
191 |
+
|
192 |
+
# محاكاة استخراج البنود
|
193 |
+
self.analysis_results["items"] = [
|
194 |
+
{"id": 1, "name": "بند 1", "description": "وصف البند 1", "unit": "وحدة", "estimated_quantity": 100},
|
195 |
+
{"id": 2, "name": "بند 2", "description": "وصف البند 2", "unit": "وحدة", "estimated_quantity": 200},
|
196 |
+
{"id": 3, "name": "بند 3", "description": "وصف البند 3", "unit": "وحدة", "estimated_quantity": 300}
|
197 |
+
]
|
198 |
+
|
199 |
+
# محاكاة استخراج المبالغ
|
200 |
+
self.analysis_results["amounts"] = [
|
201 |
+
{"type": "item_cost", "amount": 10000, "currency": "SAR", "description": "تكلفة البند 1"},
|
202 |
+
{"type": "item_cost", "amount": 20000, "currency": "SAR", "description": "تكلفة البند 2"},
|
203 |
+
{"type": "item_cost", "amount": 30000, "currency": "SAR", "description": "تكلفة البند 3"}
|
204 |
+
]
|
205 |
+
|
206 |
+
except Exception as e:
|
207 |
+
logger.error(f"خطأ في تحليل مستند Excel: {str(e)}")
|
208 |
+
raise
|
209 |
+
|
210 |
+
def _analyze_txt(self, document_path, document_type):
|
211 |
+
"""تحليل مستند نصي"""
|
212 |
+
try:
|
213 |
+
# محاكاة تحليل مستند نصي
|
214 |
+
logger.info(f"تحليل مستند نصي: {document_path}")
|
215 |
+
|
216 |
+
# في التطبيق الفعلي، سيتم قراءة الملف النصي وتحليله
|
217 |
+
|
218 |
+
# محاكاة استخراج البنود والكيانات والتواريخ والمبالغ والمخاطر
|
219 |
+
# (مشابه للتحليلات الأخرى)
|
220 |
+
|
221 |
+
except Exception as e:
|
222 |
+
logger.error(f"خطأ في تحليل مستند نصي: {str(e)}")
|
223 |
+
raise
|
224 |
+
|
225 |
+
def get_analysis_status(self):
|
226 |
+
"""الحصول على حالة التحليل الحالي"""
|
227 |
+
if not self.analysis_in_progress:
|
228 |
+
if not self.analysis_results:
|
229 |
+
return {"status": "لا يوجد تحليل جارٍ"}
|
230 |
+
else:
|
231 |
+
return {"status": self.analysis_results.get("status", "غير معروف")}
|
232 |
+
|
233 |
+
return {
|
234 |
+
"status": "جاري التحليل",
|
235 |
+
"document_path": self.current_document,
|
236 |
+
"start_time": self.analysis_results.get("analysis_start_time")
|
237 |
+
}
|
238 |
+
|
239 |
+
def get_analysis_results(self):
|
240 |
+
"""الحصول على نتائج التحليل"""
|
241 |
+
return self.analysis_results
|
242 |
+
|
243 |
+
def export_analysis_results(self, output_path=None):
|
244 |
+
"""تصدير نتائج التحليل إلى ملف JSON"""
|
245 |
+
if not self.analysis_results:
|
246 |
+
logger.warning("لا توجد نتائج تحليل للتصدير")
|
247 |
+
return None
|
248 |
+
|
249 |
+
if not output_path:
|
250 |
+
# إنشاء اسم ملف افتراضي
|
251 |
+
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
252 |
+
filename = f"analysis_results_{timestamp}.json"
|
253 |
+
output_path = os.path.join(self.documents_path, filename)
|
254 |
+
|
255 |
+
try:
|
256 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
257 |
+
json.dump(self.analysis_results, f, ensure_ascii=False, indent=4)
|
258 |
+
|
259 |
+
logger.info(f"تم تصدير نتائج التحليل إلى: {output_path}")
|
260 |
+
return output_path
|
261 |
+
|
262 |
+
except Exception as e:
|
263 |
+
logger.error(f"خطأ في تصدير نتائج التحليل: {str(e)}")
|
264 |
+
return None
|
265 |
+
|
266 |
+
def import_analysis_results(self, input_path):
|
267 |
+
"""استيراد نتائج التحليل من ملف JSON"""
|
268 |
+
if not os.path.exists(input_path):
|
269 |
+
logger.error(f"ملف نتائج التحليل غير موجود: {input_path}")
|
270 |
+
return False
|
271 |
+
|
272 |
+
try:
|
273 |
+
with open(input_path, 'r', encoding='utf-8') as f:
|
274 |
+
self.analysis_results = json.load(f)
|
275 |
+
|
276 |
+
logger.info(f"تم استيراد نتائج التحليل من: {input_path}")
|
277 |
+
return True
|
278 |
+
|
279 |
+
except Exception as e:
|
280 |
+
logger.error(f"خطأ في استيراد نتائج التحليل: {str(e)}")
|
281 |
+
return False
|
modules/document_analysis/document_app.py
CHANGED
@@ -171,13 +171,15 @@ class DataAnalysisApp:
|
|
171 |
def run(self):
|
172 |
"""
|
173 |
تشغيل وحدة تحليل البيانات
|
174 |
-
|
175 |
هذه الدالة هي نقطة الدخول الرئيسية لوحدة تحليل البيانات.
|
176 |
تقوم بتهيئة واجهة المستخدم وعرض البيانات والتحليلات.
|
177 |
"""
|
178 |
try:
|
179 |
-
#
|
180 |
-
|
|
|
|
|
181 |
page_title="وحدة تحليل البيانات - نظام المناقصات",
|
182 |
page_icon="📊",
|
183 |
layout="wide",
|
@@ -216,7 +218,7 @@ class DataAnalysisApp:
|
|
216 |
|
217 |
# عرض الشريط الجانبي
|
218 |
with st.sidebar:
|
219 |
-
st.image("
|
220 |
st.markdown("## نظام تحليل المناقصات")
|
221 |
st.markdown("### وحدة تحليل البيانات")
|
222 |
|
@@ -287,146 +289,152 @@ class DataAnalysisApp:
|
|
287 |
st.markdown("#### مؤشرات الأداء الرئيسية")
|
288 |
|
289 |
# استخراج البيانات اللازمة للمؤشرات
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
st.
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
# حساب متوسط هامش الربح لكل سنة
|
369 |
-
profit_margin_by_year = tenders_df.groupby("السنة")["هامش الربح (%)"].mean().reset_index()
|
370 |
-
|
371 |
-
fig = px.line(
|
372 |
-
profit_margin_by_year,
|
373 |
-
x="السنة",
|
374 |
-
y="هامش الربح (%)",
|
375 |
-
title="تطور متوسط هامش الربح عبر السنوات",
|
376 |
-
markers=True
|
377 |
-
)
|
378 |
-
|
379 |
-
st.plotly_chart(fig, use_container_width=True)
|
380 |
-
|
381 |
-
# عرض توزيع المناقصات حسب الموقع
|
382 |
-
st.markdown("#### توزيع المناقصات حسب الموقع")
|
383 |
-
|
384 |
-
location_counts = tenders_df["الموقع"].value_counts().reset_index()
|
385 |
-
location_counts.columns = ["الموقع", "العدد"]
|
386 |
-
|
387 |
-
fig = px.bar(
|
388 |
-
location_counts,
|
389 |
-
x="الموقع",
|
390 |
-
y="العدد",
|
391 |
-
title="توزيع المناقصات حسب الموقع",
|
392 |
-
color="الموقع",
|
393 |
-
text_auto=True
|
394 |
-
)
|
395 |
-
|
396 |
-
st.plotly_chart(fig, use_container_width=True)
|
397 |
-
|
398 |
-
# عرض العلاقة بين الميزانية والتكلفة
|
399 |
-
st.markdown("#### العلاقة بين الميزانية والتكلفة")
|
400 |
-
|
401 |
-
fig = px.scatter(
|
402 |
-
tenders_df,
|
403 |
-
x="الميزانية (ريال)",
|
404 |
-
y="التكلفة (ريال)",
|
405 |
-
color="الحالة",
|
406 |
-
size="المساحة (م2)",
|
407 |
-
hover_name="رقم المناقصة",
|
408 |
-
hover_data=["نوع المشروع", "الموقع", "هامش الربح (%)"],
|
409 |
-
title="العلاقة بين الميزانية والتكلفة",
|
410 |
-
color_discrete_map={
|
411 |
-
"فائز": "#2ecc71",
|
412 |
-
"خاسر": "#e74c3c",
|
413 |
-
"قيد التنفيذ": "#3498db",
|
414 |
-
"منجز": "#f39c12"
|
415 |
-
}
|
416 |
-
)
|
417 |
-
|
418 |
-
# إضافة خط الميزانية = التكلفة
|
419 |
-
max_value = max(tenders_df["الميزانية (ريال)"].max(), tenders_df["التكلفة (ريال)"].max())
|
420 |
-
fig.add_trace(
|
421 |
-
go.Scatter(
|
422 |
-
x=[0, max_value],
|
423 |
-
y=[0, max_value],
|
424 |
-
mode="lines",
|
425 |
-
line=dict(color="gray", dash="dash"),
|
426 |
-
name="الميزانية = التكلفة"
|
427 |
)
|
428 |
-
|
429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
430 |
|
431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
432 |
|
|
|
|
171 |
def run(self):
|
172 |
"""
|
173 |
تشغيل وحدة تحليل البيانات
|
174 |
+
|
175 |
هذه الدالة هي نقطة الدخول الرئيسية لوحدة تحليل البيانات.
|
176 |
تقوم بتهيئة واجهة المستخدم وعرض البيانات والتحليلات.
|
177 |
"""
|
178 |
try:
|
179 |
+
# استخدام مدير التكوين لضبط إعدادات الصفحة
|
180 |
+
from config_manager import ConfigManager
|
181 |
+
config_manager = ConfigManager()
|
182 |
+
config_manager.set_page_config_if_needed(
|
183 |
page_title="وحدة تحليل البيانات - نظام المناقصات",
|
184 |
page_icon="📊",
|
185 |
layout="wide",
|
|
|
218 |
|
219 |
# عرض الشريط الجانبي
|
220 |
with st.sidebar:
|
221 |
+
st.image("assets/images/logo.png", width=200)
|
222 |
st.markdown("## نظام تحليل المناقصات")
|
223 |
st.markdown("### وحدة تحليل البيانات")
|
224 |
|
|
|
289 |
st.markdown("#### مؤشرات الأداء الرئيسية")
|
290 |
|
291 |
# استخراج البيانات اللازمة للمؤشرات
|
292 |
+
tenders_df = st.session_state.sample_data["tenders"]
|
293 |
+
|
294 |
+
# حساب المؤشرات
|
295 |
+
total_tenders = len(tenders_df)
|
296 |
+
won_tenders = len(tenders_df[tenders_df["الحالة"] == "فائز"])
|
297 |
+
win_rate = won_tenders / total_tenders * 100
|
298 |
+
avg_profit_margin = tenders_df["هامش الربح (%)"].mean()
|
299 |
+
total_profit = tenders_df["الربح (ريال)"].sum()
|
300 |
+
|
301 |
+
# عرض المؤشرات
|
302 |
+
col1, col2, col3, col4 = st.columns(4)
|
303 |
+
|
304 |
+
with col1:
|
305 |
+
st.metric("إجمالي المناقصات", f"{total_tenders}")
|
306 |
+
|
307 |
+
with col2:
|
308 |
+
st.metric("معدل الفوز", f"{win_rate:.1f}%")
|
309 |
+
|
310 |
+
with col3:
|
311 |
+
st.metric("متوسط هامش الربح", f"{avg_profit_margin:.1f}%")
|
312 |
+
|
313 |
+
with col4:
|
314 |
+
st.metric("إجمالي الربح", f"{total_profit:,.0f} ريال")
|
315 |
+
|
316 |
+
# عرض توزيع المناقصات حسب الحالة
|
317 |
+
st.markdown("#### توزيع المناقصات حسب الحالة")
|
318 |
+
|
319 |
+
status_counts = tenders_df["الحالة"].value_counts().reset_index()
|
320 |
+
status_counts.columns = ["الحالة", "العدد"]
|
321 |
+
|
322 |
+
fig = px.pie(
|
323 |
+
status_counts,
|
324 |
+
values="العدد",
|
325 |
+
names="الحالة",
|
326 |
+
title="توزيع المناقصات حسب الحالة",
|
327 |
+
color="الحالة",
|
328 |
+
color_discrete_map={
|
329 |
+
"فائز": "#2ecc71",
|
330 |
+
"خاسر": "#e74c3c",
|
331 |
+
"قيد التنفيذ": "#3498db",
|
332 |
+
"منجز": "#f39c12"
|
333 |
+
}
|
334 |
+
)
|
335 |
+
|
336 |
+
st.plotly_chart(fig, use_container_width=True)
|
337 |
+
|
338 |
+
# عرض توزيع المناقصات حسب نوع المشروع
|
339 |
+
st.markdown("#### توزيع المناقصات حسب نوع المشروع")
|
340 |
+
|
341 |
+
type_counts = tenders_df["نوع المشروع"].value_counts().reset_index()
|
342 |
+
type_counts.columns = ["نوع المشروع", "العدد"]
|
343 |
+
|
344 |
+
fig = px.bar(
|
345 |
+
type_counts,
|
346 |
+
x="نوع المشروع",
|
347 |
+
y="العدد",
|
348 |
+
title="توزيع المناقصات حسب نوع المشروع",
|
349 |
+
color="نوع المشروع",
|
350 |
+
text_auto=True
|
351 |
+
)
|
352 |
+
|
353 |
+
st.plotly_chart(fig, use_container_width=True)
|
354 |
+
|
355 |
+
# عرض تطور هامش الربح عبر الزمن
|
356 |
+
st.markdown("#### تطور هامش الربح عبر الزمن")
|
357 |
+
|
358 |
+
# إضافة عمود السنة
|
359 |
+
tenders_df["السنة"] = tenders_df["تاريخ التقديم"].str[:4]
|
360 |
+
|
361 |
+
# حساب متوسط هامش الربح لكل سنة
|
362 |
+
profit_margin_by_year = tenders_df.groupby("السنة")["هامش الربح (%)"].mean().reset_index()
|
363 |
+
|
364 |
+
fig = px.line(
|
365 |
+
profit_margin_by_year,
|
366 |
+
x="السنة",
|
367 |
+
y="هامش الربح (%)",
|
368 |
+
title="تطور متوسط هامش الربح عبر السنوات",
|
369 |
+
markers=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
370 |
)
|
371 |
+
|
372 |
+
st.plotly_chart(fig, use_container_width=True)
|
373 |
+
|
374 |
+
# عرض توزيع المناقصات حسب الموقع
|
375 |
+
st.markdown("#### توزيع المناقصات حسب الموقع")
|
376 |
+
|
377 |
+
location_counts = tenders_df["الموقع"].value_counts().reset_index()
|
378 |
+
location_counts.columns = ["الموقع", "العدد"]
|
379 |
+
|
380 |
+
fig = px.bar(
|
381 |
+
location_counts,
|
382 |
+
x="الموقع",
|
383 |
+
y="العدد",
|
384 |
+
title="توزيع المناقصات حسب الموقع",
|
385 |
+
color="الموقع",
|
386 |
+
text_auto=True
|
387 |
+
)
|
388 |
+
|
389 |
+
st.plotly_chart(fig, use_container_width=True)
|
390 |
+
|
391 |
+
# عرض العلاقة بين الميزانية والتكلفة
|
392 |
+
st.markdown("#### العلاقة بين الميزانية والتكلفة")
|
393 |
+
|
394 |
+
fig = px.scatter(
|
395 |
+
tenders_df,
|
396 |
+
x="الميزانية (ريال)",
|
397 |
+
y="التكلفة (ريال)",
|
398 |
+
color="الحالة",
|
399 |
+
size="المساحة (م2)",
|
400 |
+
hover_name="رقم المناقصة",
|
401 |
+
hover_data=["نوع المشروع", "الموقع", "هامش الربح (%)"],
|
402 |
+
title="العلاقة بين الميزانية والتكلفة",
|
403 |
+
color_discrete_map={
|
404 |
+
"فائز": "#2ecc71",
|
405 |
+
"خاسر": "#e74c3c",
|
406 |
+
"قيد التنفيذ": "#3498db",
|
407 |
+
"منجز": "#f39c12"
|
408 |
+
}
|
409 |
+
)
|
410 |
+
|
411 |
+
# إضافة خط الميزانية = التكلفة
|
412 |
+
max_value = max(tenders_df["الميزانية (ريال)"].max(), tenders_df["التكلفة (ريال)"].max())
|
413 |
+
fig.add_trace(
|
414 |
+
go.Scatter(
|
415 |
+
x=[0, max_value],
|
416 |
+
y=[0, max_value],
|
417 |
+
mode="lines",
|
418 |
+
line=dict(color="gray", dash="dash"),
|
419 |
+
name="الميزانية = التكلفة"
|
420 |
+
)
|
421 |
+
)
|
422 |
+
st.plotly_chart(fig, use_container_width=True)
|
423 |
|
424 |
+
def _render_tenders_analysis_tab(self):
|
425 |
+
"""عرض تبويب تحليل المناقصات"""
|
426 |
+
st.markdown("### تحليل المناقصات")
|
427 |
+
|
428 |
+
def _render_price_analysis_tab(self):
|
429 |
+
"""عرض تبويب تحليل الأسعار"""
|
430 |
+
st.markdown("### تحليل الأسعار")
|
431 |
+
|
432 |
+
def _render_competitors_analysis_tab(self):
|
433 |
+
"""عرض تبويب تحليل المنافسين"""
|
434 |
+
st.markdown("### تحليل المنافسين")
|
435 |
+
|
436 |
+
def _render_import_export_tab(self):
|
437 |
+
"""عرض تبويب استيراد وتصدير البيانات"""
|
438 |
+
st.markdown("### استيراد وتصدير البيانات")
|
439 |
|
440 |
+
DocumentAnalysisApp = DataAnalysisApp
|
modules/document_comparison/document_comparison_app.py
CHANGED
@@ -1,1003 +1,1003 @@
|
|
1 |
-
"""
|
2 |
-
وحدة مقارنة المستندات - نظام تحليل المناقصات
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
import pandas as pd
|
7 |
-
import numpy as np
|
8 |
-
import os
|
9 |
-
import sys
|
10 |
-
from pathlib import Path
|
11 |
-
import difflib
|
12 |
-
import re
|
13 |
-
import datetime
|
14 |
-
|
15 |
-
# إضافة مسار المشروع للنظام
|
16 |
-
sys.path.append(str(Path(__file__).parent.parent))
|
17 |
-
|
18 |
-
# استيراد محسن واجهة المستخدم
|
19 |
-
from styling.enhanced_ui import UIEnhancer
|
20 |
-
|
21 |
-
class DocumentComparisonApp:
|
22 |
-
"""تطبيق مقارنة المستندات"""
|
23 |
-
|
24 |
-
def __init__(self):
|
25 |
-
"""تهيئة تطبيق مقارنة المستندات"""
|
26 |
-
self.ui = UIEnhancer(page_title="مقارنة المستندات - نظام تحليل المناقصات", page_icon="📄")
|
27 |
-
self.ui.apply_theme_colors()
|
28 |
-
|
29 |
-
# بيانات المستندات (نموذجية)
|
30 |
-
self.documents_data = [
|
31 |
-
{
|
32 |
-
"id": "DOC001",
|
33 |
-
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
34 |
-
"type": "كراسة شروط",
|
35 |
-
"version": "1.0",
|
36 |
-
"date": "2025-01-15",
|
37 |
-
"size": 2.4,
|
38 |
-
"pages": 45,
|
39 |
-
"related_entity": "T-2025-001",
|
40 |
-
"path": "/documents/T-2025-001/specs_v1.pdf"
|
41 |
-
},
|
42 |
-
{
|
43 |
-
"id": "DOC002",
|
44 |
-
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
45 |
-
"type": "كراسة شروط",
|
46 |
-
"version": "1.1",
|
47 |
-
"date": "2025-02-10",
|
48 |
-
"size": 2.6,
|
49 |
-
"pages": 48,
|
50 |
-
"related_entity": "T-2025-001",
|
51 |
-
"path": "/documents/T-2025-001/specs_v1.1.pdf"
|
52 |
-
},
|
53 |
-
{
|
54 |
-
"id": "DOC003",
|
55 |
-
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
56 |
-
"type": "كراسة شروط",
|
57 |
-
"version": "2.0",
|
58 |
-
"date": "2025-03-05",
|
59 |
-
"size": 2.8,
|
60 |
-
"pages": 52,
|
61 |
-
"related_entity": "T-2025-001",
|
62 |
-
"path": "/documents/T-2025-001/specs_v2.0.pdf"
|
63 |
-
},
|
64 |
-
{
|
65 |
-
"id": "DOC004",
|
66 |
-
"name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
|
67 |
-
"type": "جدول كميات",
|
68 |
-
"version": "1.0",
|
69 |
-
"date": "2025-01-15",
|
70 |
-
"size": 1.2,
|
71 |
-
"pages": 20,
|
72 |
-
"related_entity": "T-2025-001",
|
73 |
-
"path": "/documents/T-2025-001/boq_v1.0.xlsx"
|
74 |
-
},
|
75 |
-
{
|
76 |
-
"id": "DOC005",
|
77 |
-
"name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
|
78 |
-
"type": "جدول كميات",
|
79 |
-
"version": "1.1",
|
80 |
-
"date": "2025-02-20",
|
81 |
-
"size": 1.3,
|
82 |
-
"pages": 22,
|
83 |
-
"related_entity": "T-2025-001",
|
84 |
-
"path": "/documents/T-2025-001/boq_v1.1.xlsx"
|
85 |
-
},
|
86 |
-
{
|
87 |
-
"id": "DOC006",
|
88 |
-
"name": "المخططات - مناقصة إنشاء مبنى إداري",
|
89 |
-
"type": "مخططات",
|
90 |
-
"version": "1.0",
|
91 |
-
"date": "2025-01-15",
|
92 |
-
"size": 15.6,
|
93 |
-
"pages": 30,
|
94 |
-
"related_entity": "T-2025-001",
|
95 |
-
"path": "/documents/T-2025-001/drawings_v1.0.pdf"
|
96 |
-
},
|
97 |
-
{
|
98 |
-
"id": "DOC007",
|
99 |
-
"name": "المخططات - مناقصة إنشاء مبنى إداري",
|
100 |
-
"type": "مخططات",
|
101 |
-
"version": "2.0",
|
102 |
-
"date": "2025-03-10",
|
103 |
-
"size": 18.2,
|
104 |
-
"pages": 35,
|
105 |
-
"related_entity": "T-2025-001",
|
106 |
-
"path": "/documents/T-2025-001/drawings_v2.0.pdf"
|
107 |
-
},
|
108 |
-
{
|
109 |
-
"id": "DOC008",
|
110 |
-
"name": "كراسة الشروط - مناقصة صيانة طرق",
|
111 |
-
"type": "كراسة شروط",
|
112 |
-
"version": "1.0",
|
113 |
-
"date": "2025-02-05",
|
114 |
-
"size": 1.8,
|
115 |
-
"pages": 32,
|
116 |
-
"related_entity": "T-2025-002",
|
117 |
-
"path": "/documents/T-2025-002/specs_v1.0.pdf"
|
118 |
-
},
|
119 |
-
{
|
120 |
-
"id": "DOC009",
|
121 |
-
"name": "كراسة الشروط - مناقصة صيانة طرق",
|
122 |
-
"type": "كراسة شروط",
|
123 |
-
"version": "1.1",
|
124 |
-
"date": "2025-03-15",
|
125 |
-
"size": 1.9,
|
126 |
-
"pages": 34,
|
127 |
-
"related_entity": "T-2025-002",
|
128 |
-
"path": "/documents/T-2025-002/specs_v1.1.pdf"
|
129 |
-
},
|
130 |
-
{
|
131 |
-
"id": "DOC010",
|
132 |
-
"name": "جدول الكميات - مناقصة
|
133 |
-
"type": "جدول كميات",
|
134 |
-
"version": "1.0",
|
135 |
-
"date": "2025-02-05",
|
136 |
-
"size": 0.9,
|
137 |
-
"pages": 15,
|
138 |
-
"related_entity": "T-2025-002",
|
139 |
-
"path": "/documents/T-2025-002/boq_v1.0.xlsx"
|
140 |
-
}
|
141 |
-
]
|
142 |
-
|
143 |
-
# بيانات نموذجية لمحتوى المستندات (للعرض فقط)
|
144 |
-
self.sample_document_content = {
|
145 |
-
"DOC001": """
|
146 |
-
# كراسة الشروط والمواصفات
|
147 |
-
## مناقصة إنشاء مبنى إداري
|
148 |
-
|
149 |
-
### 1. مقدمة
|
150 |
-
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
|
151 |
-
|
152 |
-
### 2. نطاق العمل
|
153 |
-
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع، ويشمل ذلك:
|
154 |
-
- أعمال الهيكل الإنشائي
|
155 |
-
- أعمال التشطيبات الداخلية والخارجية
|
156 |
-
- أعمال الكهرباء والميكانيكا
|
157 |
-
- أعمال تنسيق الموقع
|
158 |
-
|
159 |
-
### 3. المواصفات الفنية
|
160 |
-
#### 3.1 أعمال الخرسانة
|
161 |
-
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 30 نيوتن/مم²
|
162 |
-
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
163 |
-
|
164 |
-
#### 3.2 أعمال التشطيبات
|
165 |
-
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
166 |
-
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
167 |
-
|
168 |
-
### 4. الشروط العامة
|
169 |
-
- مدة التنفيذ: 18 شهراً من تاريخ استلام الموقع
|
170 |
-
- غرامة التأخير: 0.1% من قيمة العقد عن كل يوم تأخير
|
171 |
-
- ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
|
172 |
-
""",
|
173 |
-
|
174 |
-
"DOC002": """
|
175 |
-
# كراسة الشروط والمواصفات
|
176 |
-
## مناقصة إنشاء مبنى إداري
|
177 |
-
|
178 |
-
### 1. مقدمة
|
179 |
-
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
|
180 |
-
|
181 |
-
### 2. نطاق العمل
|
182 |
-
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5500 متر مربع، ويشمل ذلك:
|
183 |
-
- أعمال الهيكل الإنشائي
|
184 |
-
- أعمال التشطيبات الداخلية والخارجية
|
185 |
-
- أعمال الكهرباء والميكانيكا
|
186 |
-
- أعمال تنسيق الموقع
|
187 |
-
- أعمال أنظمة الأمن والسلامة
|
188 |
-
|
189 |
-
### 3. المواصفات الفنية
|
190 |
-
#### 3.1 أعمال الخرسانة
|
191 |
-
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 35 نيوتن/مم²
|
192 |
-
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
193 |
-
|
194 |
-
#### 3.2 أعمال التشطيبات
|
195 |
-
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
196 |
-
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
197 |
-
- يجب استخدام زجاج عاكس للحرارة للواجهات
|
198 |
-
|
199 |
-
### 4. الشروط العامة
|
200 |
-
- مدة التنفيذ: 16 شهراً من تاريخ استلام الموقع
|
201 |
-
- غرامة التأخير: 0.15% من قيمة العقد عن كل يوم تأخير
|
202 |
-
- ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
|
203 |
-
""",
|
204 |
-
|
205 |
-
"DOC003": """
|
206 |
-
# كراسة الشروط والمواصفات
|
207 |
-
## مناقصة إنشاء مبنى إداري
|
208 |
-
|
209 |
-
### 1. مقدمة
|
210 |
-
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض وفقاً للمواصفات المعتمدة من الهيئة السعودية للمواصفات والمقاييس.
|
211 |
-
|
212 |
-
### 2. نطاق العمل
|
213 |
-
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
|
214 |
-
- أعمال الهيكل الإنشائي
|
215 |
-
- أعمال التشطيبات الداخلية والخارجية
|
216 |
-
- أعمال الكهرباء والميكانيكا
|
217 |
-
- أعمال تنسيق الموقع
|
218 |
-
- أعمال أنظمة الأمن والسلامة
|
219 |
-
- أعمال أنظمة المباني الذكية
|
220 |
-
|
221 |
-
### 3. المواصفات الفنية
|
222 |
-
#### 3.1 أعمال الخرسانة
|
223 |
-
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
|
224 |
-
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
225 |
-
- يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
|
226 |
-
|
227 |
-
#### 3.2 أعمال التشطيبات
|
228 |
-
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
229 |
-
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
230 |
-
- يجب استخدام زجاج عاكس للحرارة للواجهات
|
231 |
-
- يجب استخدام مواد صديقة للبيئة
|
232 |
-
|
233 |
-
### 4. الشروط العامة
|
234 |
-
- مدة التنفيذ: 15 شهراً من تاريخ استلام الموقع
|
235 |
-
- غرامة التأخير: 0.2% من قيمة العقد عن كل يوم تأخير
|
236 |
-
- ضمان الأعمال: 15 سنوات للهيكل الإنشائي، 7 سنوات للأعمال الميكانيكية والكهربائية
|
237 |
-
|
238 |
-
### 5. متطلبات الاستدامة
|
239 |
-
- يجب أن يحقق المبنى متطلبات الاستدامة وفقاً لمعايير LEED
|
240 |
-
- يجب توفير أنظمة لترشيد استهلاك الطاقة والمياه
|
241 |
-
"""
|
242 |
-
}
|
243 |
-
|
244 |
-
def run(self):
|
245 |
-
"""تشغيل تطبيق مقارنة المستندات"""
|
246 |
-
# إنشاء قائمة العناصر
|
247 |
-
menu_items = [
|
248 |
-
{"name": "لوحة المعلومات", "icon": "house"},
|
249 |
-
{"name": "المناقصات والعقود", "icon": "file-text"},
|
250 |
-
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
251 |
-
{"name": "نظام التسعير", "icon": "calculator"},
|
252 |
-
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
253 |
-
{"name": "الموارد والتكاليف", "icon": "people"},
|
254 |
-
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
255 |
-
{"name": "إدارة المشاريع", "icon": "kanban"},
|
256 |
-
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
257 |
-
{"name": "الجدول الزمني", "icon": "calendar3"},
|
258 |
-
{"name": "الإشعارات", "icon": "bell"},
|
259 |
-
{"name": "مقارنة المستندات", "icon": "files"},
|
260 |
-
{"name": "المساعد الذكي", "icon": "robot"},
|
261 |
-
{"name": "التقارير", "icon": "bar-chart"},
|
262 |
-
{"name": "الإعدادات", "icon": "gear"}
|
263 |
-
]
|
264 |
-
|
265 |
-
# إنشاء الشريط الجانبي
|
266 |
-
selected = self.ui.create_sidebar(menu_items)
|
267 |
-
|
268 |
-
# إنشاء ترويسة الصفحة
|
269 |
-
self.ui.create_header("مقارنة المستندات", "أدوات متقدمة لمقارنة وتحليل المستندات")
|
270 |
-
|
271 |
-
# إنشاء علامات تبويب للوظائف المختلفة
|
272 |
-
tabs = st.tabs(["مقارنة الإصدارات", "مقارنة المستندات", "تحليل التغييرات", "سجل التغييرات"])
|
273 |
-
|
274 |
-
# علامة تبويب مقارنة الإصدارات
|
275 |
-
with tabs[0]:
|
276 |
-
self.compare_versions()
|
277 |
-
|
278 |
-
# علامة تبويب مقارنة المستندات
|
279 |
-
with tabs[1]:
|
280 |
-
self.compare_documents()
|
281 |
-
|
282 |
-
# علامة تبويب تحليل التغييرات
|
283 |
-
with tabs[2]:
|
284 |
-
self.analyze_changes()
|
285 |
-
|
286 |
-
# علامة تبويب سجل التغييرات
|
287 |
-
with tabs[3]:
|
288 |
-
self.show_change_history()
|
289 |
-
|
290 |
-
def compare_versions(self):
|
291 |
-
"""مقارنة إصدارات المستندات"""
|
292 |
-
st.markdown("### مقارنة إصدارات المستندات")
|
293 |
-
|
294 |
-
# اختيار المناقصة
|
295 |
-
tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
296 |
-
selected_tender = st.selectbox(
|
297 |
-
"اختر المناقصة",
|
298 |
-
options=tender_options
|
299 |
-
)
|
300 |
-
|
301 |
-
# فلترة المستندات حسب المناقصة
|
302 |
-
filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
|
303 |
-
|
304 |
-
# اختيار نوع المستند
|
305 |
-
doc_types = list(set([doc["type"] for doc in filtered_docs]))
|
306 |
-
selected_type = st.selectbox(
|
307 |
-
"اختر نوع المستند",
|
308 |
-
options=doc_types
|
309 |
-
)
|
310 |
-
|
311 |
-
# فلترة المستندات حسب النوع المختار
|
312 |
-
type_filtered_docs = [doc for doc in filtered_docs if doc["type"] == selected_type]
|
313 |
-
|
314 |
-
# ترتيب المستندات حسب الإصدار
|
315 |
-
type_filtered_docs = sorted(type_filtered_docs, key=lambda x: x["version"])
|
316 |
-
|
317 |
-
if len(type_filtered_docs) < 2:
|
318 |
-
st.warning("يجب توفر إصدارين على الأقل للمقارنة")
|
319 |
-
else:
|
320 |
-
# اختيار الإصدارات للمقارنة
|
321 |
-
col1, col2 = st.columns(2)
|
322 |
-
|
323 |
-
with col1:
|
324 |
-
version_options = [f"{doc['name']} (الإصدار {doc['version']})" for doc in type_filtered_docs]
|
325 |
-
selected_version1_index = st.selectbox(
|
326 |
-
"الإصدار الأول",
|
327 |
-
options=range(len(version_options)),
|
328 |
-
format_func=lambda x: version_options[x]
|
329 |
-
)
|
330 |
-
selected_doc1 = type_filtered_docs[selected_version1_index]
|
331 |
-
|
332 |
-
with col2:
|
333 |
-
remaining_indices = [i for i in range(len(type_filtered_docs)) if i != selected_version1_index]
|
334 |
-
selected_version2_index = st.selectbox(
|
335 |
-
"الإصدار الثاني",
|
336 |
-
options=remaining_indices,
|
337 |
-
format_func=lambda x: version_options[x]
|
338 |
-
)
|
339 |
-
selected_doc2 = type_filtered_docs[selected_version2_index]
|
340 |
-
|
341 |
-
# زر بدء المقارنة
|
342 |
-
if st.button("بدء المقارنة", use_container_width=True):
|
343 |
-
# عرض معلومات المستندات المختارة
|
344 |
-
st.markdown("### معلومات المستندات المختارة")
|
345 |
-
|
346 |
-
col1, col2 = st.columns(2)
|
347 |
-
|
348 |
-
with col1:
|
349 |
-
st.markdown(f"**الإصدار الأول:** {selected_doc1['version']}")
|
350 |
-
st.markdown(f"**التاريخ:** {selected_doc1['date']}")
|
351 |
-
st.markdown(f"**عدد الصفحات:** {selected_doc1['pages']}")
|
352 |
-
st.markdown(f"**الحجم:** {selected_doc1['size']} ميجابايت")
|
353 |
-
|
354 |
-
with col2:
|
355 |
-
st.markdown(f"**الإصدار الثاني:** {selected_doc2['version']}")
|
356 |
-
st.markdown(f"**التاريخ:** {selected_doc2['date']}")
|
357 |
-
st.markdown(f"**عدد الصفحات:** {selected_doc2['pages']}")
|
358 |
-
st.markdown(f"**الحجم:** {selected_doc2['size']} ميجابايت")
|
359 |
-
|
360 |
-
# الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
|
361 |
-
doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
|
362 |
-
doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
|
363 |
-
|
364 |
-
# إجراء المقارنة
|
365 |
-
self.display_comparison(doc1_content, doc2_content)
|
366 |
-
|
367 |
-
def display_comparison(self, text1, text2):
|
368 |
-
"""عرض نتائج المقارنة بين نصين"""
|
369 |
-
st.markdown("### نتائج المقارنة")
|
370 |
-
|
371 |
-
# تقسيم النصوص إلى أسطر
|
372 |
-
lines1 = text1.splitlines()
|
373 |
-
lines2 = text2.splitlines()
|
374 |
-
|
375 |
-
# إجراء المقارنة باستخدام difflib
|
376 |
-
d = difflib.Differ()
|
377 |
-
diff = list(d.compare(lines1, lines2))
|
378 |
-
|
379 |
-
# عرض ملخص التغييرات
|
380 |
-
added = len([line for line in diff if line.startswith('+ ')])
|
381 |
-
removed = len([line for line in diff if line.startswith('- ')])
|
382 |
-
changed = len([line for line in diff if line.startswith('? ')])
|
383 |
-
|
384 |
-
col1, col2, col3 = st.columns(3)
|
385 |
-
|
386 |
-
with col1:
|
387 |
-
self.ui.create_metric_card(
|
388 |
-
"الإضافات",
|
389 |
-
str(added),
|
390 |
-
None,
|
391 |
-
self.ui.COLORS['success']
|
392 |
-
)
|
393 |
-
|
394 |
-
with col2:
|
395 |
-
self.ui.create_metric_card(
|
396 |
-
"الحذف",
|
397 |
-
str(removed),
|
398 |
-
None,
|
399 |
-
self.ui.COLORS['danger']
|
400 |
-
)
|
401 |
-
|
402 |
-
with col3:
|
403 |
-
self.ui.create_metric_card(
|
404 |
-
"التغييرات",
|
405 |
-
str(changed // 2), # تقسيم على 2 لأن كل تغيير يظهر مرتين
|
406 |
-
None,
|
407 |
-
self.ui.COLORS['warning']
|
408 |
-
)
|
409 |
-
|
410 |
-
# عرض التغييرات بالتفصيل
|
411 |
-
st.markdown("### التغييرات بالتفصيل")
|
412 |
-
|
413 |
-
# إنشاء عرض HTML للتغييرات
|
414 |
-
html_diff = []
|
415 |
-
for line in diff:
|
416 |
-
if line.startswith('+ '):
|
417 |
-
html_diff.append(f'<div style="background-color: #e6ffe6; padding: 2px 5px; margin: 2px 0; border-left: 3px solid green;">{line[2:]}</div>')
|
418 |
-
elif line.startswith('- '):
|
419 |
-
html_diff.append(f'<div style="background-color: #ffe6e6; padding: 2px 5px; margin: 2px 0; border-left: 3px solid red;">{line[2:]}</div>')
|
420 |
-
elif line.startswith('? '):
|
421 |
-
# تجاهل أسطر التفاصيل
|
422 |
-
continue
|
423 |
-
else:
|
424 |
-
html_diff.append(f'<div style="padding: 2px 5px; margin: 2px 0;">{line[2:]}</div>')
|
425 |
-
|
426 |
-
# عرض التغييرات
|
427 |
-
st.markdown(''.join(html_diff), unsafe_allow_html=True)
|
428 |
-
|
429 |
-
# خيارات إضافية
|
430 |
-
st.markdown("### خيارات إضافية")
|
431 |
-
|
432 |
-
col1, col2, col3 = st.columns(3)
|
433 |
-
|
434 |
-
with col1:
|
435 |
-
if st.button("تصدير التغييرات", use_container_width=True):
|
436 |
-
st.success("تم تصدير التغييرات بنجاح")
|
437 |
-
|
438 |
-
with col2:
|
439 |
-
if st.button("إنشاء تقرير", use_container_width=True):
|
440 |
-
st.success("تم إنشاء التقرير بنجاح")
|
441 |
-
|
442 |
-
with col3:
|
443 |
-
if st.button("حفظ المقارنة", use_container_width=True):
|
444 |
-
st.success("تم حفظ المقارنة بنجاح")
|
445 |
-
|
446 |
-
def compare_documents(self):
|
447 |
-
"""مقارنة مستندات مختلفة"""
|
448 |
-
st.markdown("### مقارنة مستندات مختلفة")
|
449 |
-
|
450 |
-
# اختيار المستند الأول
|
451 |
-
col1, col2 = st.columns(2)
|
452 |
-
|
453 |
-
with col1:
|
454 |
-
tender1_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
455 |
-
selected_tender1 = st.selectbox(
|
456 |
-
"اختر المناقصة الأولى",
|
457 |
-
options=tender1_options,
|
458 |
-
key="tender1"
|
459 |
-
)
|
460 |
-
|
461 |
-
# فلترة المستندات حسب المناقصة المختارة
|
462 |
-
filtered_docs1 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender1]
|
463 |
-
|
464 |
-
# اختيار المستند
|
465 |
-
doc_options1 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs1]
|
466 |
-
selected_doc1_index = st.selectbox(
|
467 |
-
"اختر المستند الأول",
|
468 |
-
options=range(len(doc_options1)),
|
469 |
-
format_func=lambda x: doc_options1[x],
|
470 |
-
key="doc1"
|
471 |
-
)
|
472 |
-
selected_doc1 = filtered_docs1[selected_doc1_index]
|
473 |
-
|
474 |
-
with col2:
|
475 |
-
tender2_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
476 |
-
selected_tender2 = st.selectbox(
|
477 |
-
"اختر المناقصة الثانية",
|
478 |
-
options=tender2_options,
|
479 |
-
key="tender2"
|
480 |
-
)
|
481 |
-
|
482 |
-
# فلترة المستندات حسب المناقصة المختارة
|
483 |
-
filtered_docs2 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender2]
|
484 |
-
|
485 |
-
# اختيار المستند
|
486 |
-
doc_options2 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs2]
|
487 |
-
selected_doc2_index = st.selectbox(
|
488 |
-
"اختر المستند الثاني",
|
489 |
-
options=range(len(doc_options2)),
|
490 |
-
format_func=lambda x: doc_options2[x],
|
491 |
-
key="doc2"
|
492 |
-
)
|
493 |
-
selected_doc2 = filtered_docs2[selected_doc2_index]
|
494 |
-
|
495 |
-
# خيارات المقارنة
|
496 |
-
st.markdown("### خيارات المقارنة")
|
497 |
-
|
498 |
-
col1, col2, col3 = st.columns(3)
|
499 |
-
|
500 |
-
with col1:
|
501 |
-
comparison_type = st.radio(
|
502 |
-
"نوع المقارنة",
|
503 |
-
options=["مقارنة كاملة", "مقارنة الأقسام المتطابقة فقط", "مقارنة الاختلافات فقط"]
|
504 |
-
)
|
505 |
-
|
506 |
-
with col2:
|
507 |
-
ignore_options = st.multiselect(
|
508 |
-
"تجاهل",
|
509 |
-
options=["المسافات", "علامات الترقيم", "حالة الأحرف", "الأرقام"],
|
510 |
-
default=["المسافات"]
|
511 |
-
)
|
512 |
-
|
513 |
-
with col3:
|
514 |
-
similarity_threshold = st.slider(
|
515 |
-
"عتبة التشابه",
|
516 |
-
min_value=0.0,
|
517 |
-
max_value=1.0,
|
518 |
-
value=0.7,
|
519 |
-
step=0.05
|
520 |
-
)
|
521 |
-
|
522 |
-
# زر بدء المقارنة
|
523 |
-
if st.button("بدء المقارنة بين المستندات", use_container_width=True):
|
524 |
-
# عرض معلومات المستندات المختارة
|
525 |
-
st.markdown("### معلومات المستندات المختارة")
|
526 |
-
|
527 |
-
col1, col2 = st.columns(2)
|
528 |
-
|
529 |
-
with col1:
|
530 |
-
st.markdown(f"**المستند الأول:** {selected_doc1['name']}")
|
531 |
-
st.markdown(f"**الإصدار:** {selected_doc1['version']}")
|
532 |
-
st.markdown(f"**التاريخ:** {selected_doc1['date']}")
|
533 |
-
st.markdown(f"**المناقصة:** {selected_doc1['related_entity']}")
|
534 |
-
|
535 |
-
with col2:
|
536 |
-
st.markdown(f"**المستند الثاني:** {selected_doc2['name']}")
|
537 |
-
st.markdown(f"**الإصدار:** {selected_doc2['version']}")
|
538 |
-
st.markdown(f"**التاريخ:** {selected_doc2['date']}")
|
539 |
-
st.markdown(f"**المناقصة:** {selected_doc2['related_entity']}")
|
540 |
-
|
541 |
-
# الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
|
542 |
-
doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
|
543 |
-
doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
|
544 |
-
|
545 |
-
# إجراء المقارنة
|
546 |
-
self.display_document_comparison(doc1_content, doc2_content, comparison_type, ignore_options, similarity_threshold)
|
547 |
-
|
548 |
-
def display_document_comparison(self, text1, text2, comparison_type, ignore_options, similarity_threshold):
|
549 |
-
"""عرض نتائج المقارنة بين مستندين"""
|
550 |
-
st.markdown("### نتائج المقارنة بين المستندين")
|
551 |
-
|
552 |
-
# تقسيم النصوص إلى أقسام (في هذا المثال، نستخدم العناوين كفواصل للأقسام)
|
553 |
-
sections1 = self.split_into_sections(text1)
|
554 |
-
sections2 = self.split_into_sections(text2)
|
555 |
-
|
556 |
-
# حساب نسبة التشابه الإجمالية
|
557 |
-
similarity = difflib.SequenceMatcher(None, text1, text2).ratio()
|
558 |
-
|
559 |
-
# عرض نسبة التشابه
|
560 |
-
st.markdown(f"**نسبة التشابه الإجمالية:** {similarity:.2%}")
|
561 |
-
|
562 |
-
# عرض مقارنة الأقسام
|
563 |
-
st.markdown("### مقارنة الأقسام")
|
564 |
-
|
565 |
-
# إنشاء جدول لمقارنة الأقسام
|
566 |
-
section_comparisons = []
|
567 |
-
|
568 |
-
for section1_title, section1_content in sections1.items():
|
569 |
-
best_match = None
|
570 |
-
best_similarity = 0
|
571 |
-
|
572 |
-
for section2_title, section2_content in sections2.items():
|
573 |
-
# حساب نسبة التشابه بين عناوين الأقسام
|
574 |
-
title_similarity = difflib.SequenceMatcher(None, section1_title, section2_title).ratio()
|
575 |
-
|
576 |
-
# حساب نسبة التشابه بين محتوى الأقسام
|
577 |
-
content_similarity = difflib.SequenceMatcher(None, section1_content, section2_content).ratio()
|
578 |
-
|
579 |
-
# حساب متوسط نسبة التشابه
|
580 |
-
avg_similarity = (title_similarity + content_similarity) / 2
|
581 |
-
|
582 |
-
if avg_similarity > best_similarity:
|
583 |
-
best_similarity = avg_similarity
|
584 |
-
best_match = {
|
585 |
-
"title": section2_title,
|
586 |
-
"content": section2_content,
|
587 |
-
"similarity": avg_similarity
|
588 |
-
}
|
589 |
-
|
590 |
-
# إضافة المقارنة إلى القائمة
|
591 |
-
if best_match and best_similarity >= similarity_threshold:
|
592 |
-
section_comparisons.append({
|
593 |
-
"section1_title": section1_title,
|
594 |
-
"section2_title": best_match["title"],
|
595 |
-
"similarity": best_similarity
|
596 |
-
})
|
597 |
-
else:
|
598 |
-
section_comparisons.append({
|
599 |
-
"section1_title": section1_title,
|
600 |
-
"section2_title": "غير موجود",
|
601 |
-
"similarity": 0
|
602 |
-
})
|
603 |
-
|
604 |
-
# إضافة الأقسام الموجودة في المستند الثاني فقط
|
605 |
-
for section2_title, section2_content in sections2.items():
|
606 |
-
if not any(comp["section2_title"] == section2_title for comp in section_comparisons):
|
607 |
-
section_comparisons.append({
|
608 |
-
"section1_title": "غير موجود",
|
609 |
-
"section2_title": section2_title,
|
610 |
-
"similarity": 0
|
611 |
-
})
|
612 |
-
|
613 |
-
# عرض جدول المقارنة
|
614 |
-
section_df = pd.DataFrame(section_comparisons)
|
615 |
-
section_df = section_df.rename(columns={
|
616 |
-
"section1_title": "القسم في المستند الأول",
|
617 |
-
"section2_title": "القسم في المستند الثاني",
|
618 |
-
"similarity": "نسبة التشابه"
|
619 |
-
})
|
620 |
-
|
621 |
-
# تنسيق نسبة التشابه
|
622 |
-
section_df["نسبة التشابه"] = section_df["نسبة التشابه"].apply(lambda x: f"{x:.2%}")
|
623 |
-
|
624 |
-
st.dataframe(
|
625 |
-
section_df,
|
626 |
-
use_container_width=True,
|
627 |
-
hide_index=True
|
628 |
-
)
|
629 |
-
|
630 |
-
# عرض تفاصيل المقارنة
|
631 |
-
st.markdown("### تفاصيل المقارنة")
|
632 |
-
|
633 |
-
# اختيار قسم للمقارنة التفصيلية
|
634 |
-
selected_section = st.selectbox(
|
635 |
-
"اختر قسماً للمقارنة التفصيلية",
|
636 |
-
options=[comp["section1_title"] for comp in section_comparisons if comp["section1_title"] != "غير موجود"]
|
637 |
-
)
|
638 |
-
|
639 |
-
# العثور على القسم المقابل في المستند الثاني
|
640 |
-
matching_comparison = next((comp for comp in section_comparisons if comp["section1_title"] == selected_section), None)
|
641 |
-
|
642 |
-
if matching_comparison and matching_comparison["section2_title"] != "غير موجود":
|
643 |
-
# الحصول على محتوى القسمين
|
644 |
-
section1_content = sections1[selected_section]
|
645 |
-
section2_content = sections2[matching_comparison["section2_title"]]
|
646 |
-
|
647 |
-
# عرض المقارنة التفصيلية
|
648 |
-
self.display_comparison(section1_content, section2_content)
|
649 |
-
else:
|
650 |
-
st.warning("القسم المحدد غير موجود في المستند الثاني")
|
651 |
-
|
652 |
-
def split_into_sections(self, text):
|
653 |
-
"""تقسيم النص إلى أقسام باستخدام العناوين"""
|
654 |
-
sections = {}
|
655 |
-
current_section = None
|
656 |
-
current_content = []
|
657 |
-
|
658 |
-
for line in text.splitlines():
|
659 |
-
# البحث عن العناوين (الأسطر التي تبدأ بـ #)
|
660 |
-
if line.strip().startswith('#'):
|
661 |
-
# حفظ القسم السابق إذا وجد
|
662 |
-
if current_section:
|
663 |
-
sections[current_section] = '\n'.join(current_content)
|
664 |
-
|
665 |
-
# بدء قسم جديد
|
666 |
-
current_section = line.strip()
|
667 |
-
current_content = []
|
668 |
-
elif current_section:
|
669 |
-
# إضافة السطر إلى محتوى القسم الحالي
|
670 |
-
current_content.append(line)
|
671 |
-
|
672 |
-
# حفظ القسم الأخير
|
673 |
-
if current_section:
|
674 |
-
sections[current_section] = '\n'.join(current_content)
|
675 |
-
|
676 |
-
return sections
|
677 |
-
|
678 |
-
def analyze_changes(self):
|
679 |
-
"""تحليل التغييرات في المستندات"""
|
680 |
-
st.markdown("### تحليل التغييرات في المستندات")
|
681 |
-
|
682 |
-
# اختيار المناقصة
|
683 |
-
tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
684 |
-
selected_tender = st.selectbox(
|
685 |
-
"اختر المناقصة",
|
686 |
-
options=tender_options,
|
687 |
-
key="analyze_tender"
|
688 |
-
)
|
689 |
-
|
690 |
-
# فلترة المستندات حسب المناقصة المختارة
|
691 |
-
filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
|
692 |
-
|
693 |
-
# تجميع المستندات حسب النوع
|
694 |
-
doc_types = {}
|
695 |
-
for doc in filtered_docs:
|
696 |
-
if doc["type"] not in doc_types:
|
697 |
-
doc_types[doc["type"]] = []
|
698 |
-
doc_types[doc["type"]].append(doc)
|
699 |
-
|
700 |
-
# عرض تحليل التغييرات لكل نوع مستند
|
701 |
-
for doc_type, docs in doc_types.items():
|
702 |
-
if len(docs) > 1:
|
703 |
-
with st.expander(f"تحليل التغييرات في {doc_type}"):
|
704 |
-
# ترتيب المستندات حسب الإصدار
|
705 |
-
sorted_docs = sorted(docs, key=lambda x: x["version"])
|
706 |
-
|
707 |
-
# عرض معلومات الإصدارات
|
708 |
-
st.markdown(f"**عدد الإصدارات:** {len(sorted_docs)}")
|
709 |
-
st.markdown(f"**أول إصدار:** {sorted_docs[0]['version']} ({sorted_docs[0]['date']})")
|
710 |
-
st.markdown(f"**آخر إصدار:** {sorted_docs[-1]['version']} ({sorted_docs[-1]['date']})")
|
711 |
-
|
712 |
-
# حساب التغييرات بين الإصدارات
|
713 |
-
changes = []
|
714 |
-
for i in range(1, len(sorted_docs)):
|
715 |
-
prev_doc = sorted_docs[i-1]
|
716 |
-
curr_doc = sorted_docs[i]
|
717 |
-
|
718 |
-
# حساب التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
|
719 |
-
page_diff = curr_doc["pages"] - prev_doc["pages"]
|
720 |
-
size_diff = curr_doc["size"] - prev_doc["size"]
|
721 |
-
|
722 |
-
changes.append({
|
723 |
-
"from_version": prev_doc["version"],
|
724 |
-
"to_version": curr_doc["version"],
|
725 |
-
"date": curr_doc["date"],
|
726 |
-
"page_diff": page_diff,
|
727 |
-
"size_diff": size_diff
|
728 |
-
})
|
729 |
-
|
730 |
-
# عرض جدول التغييرات
|
731 |
-
changes_df = pd.DataFrame(changes)
|
732 |
-
changes_df = changes_df.rename(columns={
|
733 |
-
"from_version": "من الإصدار",
|
734 |
-
"to_version": "إلى الإصدار",
|
735 |
-
"date": "تاريخ التغيير",
|
736 |
-
"page_diff": "التغيير في عدد الصفحات",
|
737 |
-
"size_diff": "التغيير في الحجم (ميجابايت)"
|
738 |
-
})
|
739 |
-
|
740 |
-
st.dataframe(
|
741 |
-
changes_df,
|
742 |
-
use_container_width=True,
|
743 |
-
hide_index=True
|
744 |
-
)
|
745 |
-
|
746 |
-
# عرض رسم بياني للتغييرات
|
747 |
-
st.markdown("#### تطور حجم المستند عبر الإصدارات")
|
748 |
-
|
749 |
-
versions = [doc["version"] for doc in sorted_docs]
|
750 |
-
sizes = [doc["size"] for doc in sorted_docs]
|
751 |
-
|
752 |
-
chart_data = pd.DataFrame({
|
753 |
-
"الإصدار": versions,
|
754 |
-
"الحجم (ميجابايت)": sizes
|
755 |
-
})
|
756 |
-
|
757 |
-
st.line_chart(chart_data.set_index("الإصدار"))
|
758 |
-
|
759 |
-
# عرض رسم بياني لعدد الصفحات
|
760 |
-
st.markdown("#### تطور عدد الصفحات عبر الإصدارات")
|
761 |
-
|
762 |
-
pages = [doc["pages"] for doc in sorted_docs]
|
763 |
-
|
764 |
-
chart_data = pd.DataFrame({
|
765 |
-
"الإصدار": versions,
|
766 |
-
"عدد الصفحات": pages
|
767 |
-
})
|
768 |
-
|
769 |
-
st.line_chart(chart_data.set_index("الإصدار"))
|
770 |
-
|
771 |
-
# تحليل التغييرات الشاملة
|
772 |
-
st.markdown("### تحليل التغييرات الشاملة")
|
773 |
-
|
774 |
-
# حساب إجمالي التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
|
775 |
-
total_docs = len(filtered_docs)
|
776 |
-
total_versions = sum(len(docs) for docs in doc_types.values())
|
777 |
-
avg_versions = total_versions / len(doc_types) if doc_types else 0
|
778 |
-
|
779 |
-
col1, col2, col3 = st.columns(3)
|
780 |
-
|
781 |
-
with col1:
|
782 |
-
self.ui.create_metric_card(
|
783 |
-
"إجمالي المستندات",
|
784 |
-
str(total_docs),
|
785 |
-
None,
|
786 |
-
self.ui.COLORS['primary']
|
787 |
-
)
|
788 |
-
|
789 |
-
with col2:
|
790 |
-
self.ui.create_metric_card(
|
791 |
-
"إجمالي الإصدارات",
|
792 |
-
str(total_versions),
|
793 |
-
None,
|
794 |
-
self.ui.COLORS['secondary']
|
795 |
-
)
|
796 |
-
|
797 |
-
with col3:
|
798 |
-
self.ui.create_metric_card(
|
799 |
-
"متوسط الإصدارات لكل نوع",
|
800 |
-
f"{avg_versions:.1f}",
|
801 |
-
None,
|
802 |
-
self.ui.COLORS['accent']
|
803 |
-
)
|
804 |
-
|
805 |
-
# عرض توزيع التغييرات حسب النوع
|
806 |
-
st.markdown("#### توزيع الإصدارات حسب نوع المستند")
|
807 |
-
|
808 |
-
type_counts = {doc_type: len(docs) for doc_type, docs in doc_types.items()}
|
809 |
-
|
810 |
-
chart_data = pd.DataFrame({
|
811 |
-
"نوع المستند": list(type_counts.keys()),
|
812 |
-
"عدد الإصدارات": list(type_counts.values())
|
813 |
-
})
|
814 |
-
|
815 |
-
st.bar_chart(chart_data.set_index("نوع المستند"))
|
816 |
-
|
817 |
-
def show_change_history(self):
|
818 |
-
"""عرض سجل التغييرات"""
|
819 |
-
st.markdown("### سجل التغييرات")
|
820 |
-
|
821 |
-
# إنشاء بيانات نموذجية لسجل التغييرات
|
822 |
-
change_history = [
|
823 |
-
{
|
824 |
-
"id": "CH001",
|
825 |
-
"document_id": "DOC001",
|
826 |
-
"document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
827 |
-
"from_version": "1.0",
|
828 |
-
"to_version": "1.1",
|
829 |
-
"change_date": "2025-02-10",
|
830 |
-
"change_type": "تحديث",
|
831 |
-
"changed_by": "أحمد محمد",
|
832 |
-
"description": "تحديث المواصفات الفنية وشروط التنفيذ",
|
833 |
-
"sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
|
834 |
-
},
|
835 |
-
{
|
836 |
-
"id": "CH002",
|
837 |
-
"document_id": "DOC002",
|
838 |
-
"document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
839 |
-
"from_version": "1.1",
|
840 |
-
"to_version": "2.0",
|
841 |
-
"change_date": "2025-03-05",
|
842 |
-
"change_type": "تحديث رئيسي",
|
843 |
-
"changed_by": "سارة عبدالله",
|
844 |
-
"description": "إضافة متطلبات الاستدامة وتحديث المواصفات الفنية",
|
845 |
-
"sections_changed": ["المواصفات الفنية", "الشروط العامة", "متطلبات الاستدامة"]
|
846 |
-
},
|
847 |
-
{
|
848 |
-
"id": "CH003",
|
849 |
-
"document_id": "DOC004",
|
850 |
-
"document_name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
|
851 |
-
"from_version": "1.0",
|
852 |
-
"to_version": "1.1",
|
853 |
-
"change_date": "2025-02-20",
|
854 |
-
"change_type": "تحديث",
|
855 |
-
"changed_by": "خالد عمر",
|
856 |
-
"description": "تحديث الكميات وإضافة بنود جديدة",
|
857 |
-
"sections_changed": ["أعمال الهيكل الإنشائي", "أعمال التشطيبات", "أعمال الكهرباء"]
|
858 |
-
},
|
859 |
-
{
|
860 |
-
"id": "CH004",
|
861 |
-
"document_id": "DOC006",
|
862 |
-
"document_name": "المخططات - مناقصة إنشاء مبنى إداري",
|
863 |
-
"from_version": "1.0",
|
864 |
-
"to_version": "2.0",
|
865 |
-
"change_date": "2025-03-10",
|
866 |
-
"change_type": "تحديث رئيسي",
|
867 |
-
"changed_by": "محمد علي",
|
868 |
-
"description": "تحديث المخططات المعمارية والإنشائية",
|
869 |
-
"sections_changed": ["المخططات المعمارية", "المخططات الإنشائية", "مخططات الكهرباء"]
|
870 |
-
},
|
871 |
-
{
|
872 |
-
"id": "CH005",
|
873 |
-
"document_id": "DOC008",
|
874 |
-
"document_name": "كراسة الشروط - مناقصة صيانة طرق",
|
875 |
-
"from_version": "1.0",
|
876 |
-
"to_version": "1.1",
|
877 |
-
"change_date": "2025-03-15",
|
878 |
-
"change_type": "تحديث",
|
879 |
-
"changed_by": "فاطمة أحمد",
|
880 |
-
"description": "تحديث المواصفات الفنية وشروط التنفيذ",
|
881 |
-
"sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
|
882 |
-
}
|
883 |
-
]
|
884 |
-
|
885 |
-
# إنشاء فلاتر للسجل
|
886 |
-
col1, col2, col3 = st.columns(3)
|
887 |
-
|
888 |
-
with col1:
|
889 |
-
document_filter = st.selectbox(
|
890 |
-
"المستند",
|
891 |
-
options=["الكل"] + list(set([ch["document_name"] for ch in change_history]))
|
892 |
-
)
|
893 |
-
|
894 |
-
with col2:
|
895 |
-
change_type_filter = st.selectbox(
|
896 |
-
"نوع التغيير",
|
897 |
-
options=["الكل"] + list(set([ch["change_type"] for ch in change_history]))
|
898 |
-
)
|
899 |
-
|
900 |
-
with col3:
|
901 |
-
date_range = st.date_input(
|
902 |
-
"نطاق التاريخ",
|
903 |
-
value=(
|
904 |
-
datetime.datetime.strptime("2025-01-01", "%Y-%m-%d").date(),
|
905 |
-
datetime.datetime.strptime("2025-12-31", "%Y-%m-%d").date()
|
906 |
-
)
|
907 |
-
)
|
908 |
-
|
909 |
-
# تطبيق الفلاتر
|
910 |
-
filtered_history = change_history
|
911 |
-
|
912 |
-
if document_filter != "الكل":
|
913 |
-
filtered_history = [ch for ch in filtered_history if ch["document_name"] == document_filter]
|
914 |
-
|
915 |
-
if change_type_filter != "الكل":
|
916 |
-
filtered_history = [ch for ch in filtered_history if ch["change_type"] == change_type_filter]
|
917 |
-
|
918 |
-
if len(date_range) == 2:
|
919 |
-
start_date, end_date = date_range
|
920 |
-
filtered_history = [
|
921 |
-
ch for ch in filtered_history
|
922 |
-
if start_date <= datetime.datetime.strptime(ch["change_date"], "%Y-%m-%d").date() <= end_date
|
923 |
-
]
|
924 |
-
|
925 |
-
# عرض سجل التغييرات
|
926 |
-
if not filtered_history:
|
927 |
-
st.info("لا توجد تغييرات تطابق الفلاتر المحددة")
|
928 |
-
else:
|
929 |
-
# تحويل البيانات إلى DataFrame
|
930 |
-
history_df = pd.DataFrame(filtered_history)
|
931 |
-
|
932 |
-
# إعادة ترتيب الأعمدة وتغيير أسمائها
|
933 |
-
display_df = history_df[[
|
934 |
-
"id", "document_name", "from_version", "to_version", "change_date", "change_type", "changed_by", "description"
|
935 |
-
]].rename(columns={
|
936 |
-
"id": "الرقم",
|
937 |
-
"document_name": "
|
938 |
-
"from_version": "من الإصدار",
|
939 |
-
"to_version": "إلى الإصدار",
|
940 |
-
"change_date": "تاريخ التغيير",
|
941 |
-
"change_type": "نوع التغيير",
|
942 |
-
"changed_by": "بواسطة",
|
943 |
-
"description": "الوصف"
|
944 |
-
})
|
945 |
-
|
946 |
-
# عرض الجدول
|
947 |
-
st.dataframe(
|
948 |
-
display_df,
|
949 |
-
use_container_width=True,
|
950 |
-
hide_index=True
|
951 |
-
)
|
952 |
-
|
953 |
-
# عرض تفاصيل التغيير المحدد
|
954 |
-
st.markdown("### تفاصيل التغيير")
|
955 |
-
|
956 |
-
selected_change_id = st.selectbox(
|
957 |
-
"اختر تغييراً لعرض التفاصيل",
|
958 |
-
options=[ch["id"] for ch in filtered_history],
|
959 |
-
format_func=lambda x: next((f"{ch['id']} - {ch['document_name']} ({ch['from_version']} إلى {ch['to_version']})" for ch in filtered_history if ch["id"] == x), "")
|
960 |
-
)
|
961 |
-
|
962 |
-
# العثور على التغيير المحدد
|
963 |
-
selected_change = next((ch for ch in filtered_history if ch["id"] == selected_change_id), None)
|
964 |
-
|
965 |
-
if selected_change:
|
966 |
-
col1, col2 = st.columns(2)
|
967 |
-
|
968 |
-
with col1:
|
969 |
-
st.markdown(f"**المستند:** {selected_change['document_name']}")
|
970 |
-
st.markdown(f"**من الإصدار:** {selected_change['from_version']}")
|
971 |
-
st.markdown(f"**إلى الإصدار:** {selected_change['to_version']}")
|
972 |
-
st.markdown(f"**تاريخ التغيير:** {selected_change['change_date']}")
|
973 |
-
|
974 |
-
with col2:
|
975 |
-
st.markdown(f"**نوع التغيير:** {selected_change['change_type']}")
|
976 |
-
st.markdown(f"**بواسطة:** {selected_change['changed_by']}")
|
977 |
-
st.markdown(f"**الوصف:** {selected_change['description']}")
|
978 |
-
|
979 |
-
# عرض الأقسام التي تم تغييرها
|
980 |
-
st.markdown("#### الأقسام التي تم تغييرها")
|
981 |
-
|
982 |
-
for section in selected_change["sections_changed"]:
|
983 |
-
st.markdown(f"- {section}")
|
984 |
-
|
985 |
-
# أزرار الإجراءات
|
986 |
-
col1, col2, col3 = st.columns(3)
|
987 |
-
|
988 |
-
with col1:
|
989 |
-
if st.button("عرض التغييرات بالتفصيل", use_container_width=True):
|
990 |
-
st.success("تم فتح التغييرات بالتفصيل")
|
991 |
-
|
992 |
-
with col2:
|
993 |
-
if st.button("إنشاء تقرير", use_container_width=True):
|
994 |
-
st.success("تم إنشاء التقرير بنجاح")
|
995 |
-
|
996 |
-
with col3:
|
997 |
-
if st.button("تصدير التغييرات", use_container_width=True):
|
998 |
-
st.success("تم تصدير التغييرات بنجاح")
|
999 |
-
|
1000 |
-
# تشغيل التطبيق
|
1001 |
-
if __name__ == "__main__":
|
1002 |
-
doc_comparison_app = DocumentComparisonApp()
|
1003 |
-
doc_comparison_app.run()
|
|
|
1 |
+
"""
|
2 |
+
وحدة مقارنة المستندات - نظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import os
|
9 |
+
import sys
|
10 |
+
from pathlib import Path
|
11 |
+
import difflib
|
12 |
+
import re
|
13 |
+
import datetime
|
14 |
+
|
15 |
+
# إضافة مسار المشروع للنظام
|
16 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
17 |
+
|
18 |
+
# استيراد محسن واجهة المستخدم
|
19 |
+
from styling.enhanced_ui import UIEnhancer
|
20 |
+
|
21 |
+
class DocumentComparisonApp:
|
22 |
+
"""تطبيق مقارنة المستندات"""
|
23 |
+
|
24 |
+
def __init__(self):
|
25 |
+
"""تهيئة تطبيق مقارنة المستندات"""
|
26 |
+
self.ui = UIEnhancer(page_title="مقارنة المستندات - نظام تحليل المناقصات", page_icon="📄")
|
27 |
+
self.ui.apply_theme_colors()
|
28 |
+
|
29 |
+
# بيانات المستندات (نموذجية)
|
30 |
+
self.documents_data = [
|
31 |
+
{
|
32 |
+
"id": "DOC001",
|
33 |
+
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
34 |
+
"type": "كراسة شروط",
|
35 |
+
"version": "1.0",
|
36 |
+
"date": "2025-01-15",
|
37 |
+
"size": 2.4,
|
38 |
+
"pages": 45,
|
39 |
+
"related_entity": "T-2025-001",
|
40 |
+
"path": "/documents/T-2025-001/specs_v1.pdf"
|
41 |
+
},
|
42 |
+
{
|
43 |
+
"id": "DOC002",
|
44 |
+
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
45 |
+
"type": "كراسة شروط",
|
46 |
+
"version": "1.1",
|
47 |
+
"date": "2025-02-10",
|
48 |
+
"size": 2.6,
|
49 |
+
"pages": 48,
|
50 |
+
"related_entity": "T-2025-001",
|
51 |
+
"path": "/documents/T-2025-001/specs_v1.1.pdf"
|
52 |
+
},
|
53 |
+
{
|
54 |
+
"id": "DOC003",
|
55 |
+
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
56 |
+
"type": "كراسة شروط",
|
57 |
+
"version": "2.0",
|
58 |
+
"date": "2025-03-05",
|
59 |
+
"size": 2.8,
|
60 |
+
"pages": 52,
|
61 |
+
"related_entity": "T-2025-001",
|
62 |
+
"path": "/documents/T-2025-001/specs_v2.0.pdf"
|
63 |
+
},
|
64 |
+
{
|
65 |
+
"id": "DOC004",
|
66 |
+
"name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
|
67 |
+
"type": "جدول كميات",
|
68 |
+
"version": "1.0",
|
69 |
+
"date": "2025-01-15",
|
70 |
+
"size": 1.2,
|
71 |
+
"pages": 20,
|
72 |
+
"related_entity": "T-2025-001",
|
73 |
+
"path": "/documents/T-2025-001/boq_v1.0.xlsx"
|
74 |
+
},
|
75 |
+
{
|
76 |
+
"id": "DOC005",
|
77 |
+
"name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
|
78 |
+
"type": "جدول كميات",
|
79 |
+
"version": "1.1",
|
80 |
+
"date": "2025-02-20",
|
81 |
+
"size": 1.3,
|
82 |
+
"pages": 22,
|
83 |
+
"related_entity": "T-2025-001",
|
84 |
+
"path": "/documents/T-2025-001/boq_v1.1.xlsx"
|
85 |
+
},
|
86 |
+
{
|
87 |
+
"id": "DOC006",
|
88 |
+
"name": "المخططات - مناقصة إنشاء مبنى إداري",
|
89 |
+
"type": "مخططات",
|
90 |
+
"version": "1.0",
|
91 |
+
"date": "2025-01-15",
|
92 |
+
"size": 15.6,
|
93 |
+
"pages": 30,
|
94 |
+
"related_entity": "T-2025-001",
|
95 |
+
"path": "/documents/T-2025-001/drawings_v1.0.pdf"
|
96 |
+
},
|
97 |
+
{
|
98 |
+
"id": "DOC007",
|
99 |
+
"name": "المخططات - مناقصة إنشاء مبنى إداري",
|
100 |
+
"type": "مخططات",
|
101 |
+
"version": "2.0",
|
102 |
+
"date": "2025-03-10",
|
103 |
+
"size": 18.2,
|
104 |
+
"pages": 35,
|
105 |
+
"related_entity": "T-2025-001",
|
106 |
+
"path": "/documents/T-2025-001/drawings_v2.0.pdf"
|
107 |
+
},
|
108 |
+
{
|
109 |
+
"id": "DOC008",
|
110 |
+
"name": "كراسة الشروط - مناقصة صيانة طرق",
|
111 |
+
"type": "كراسة شروط",
|
112 |
+
"version": "1.0",
|
113 |
+
"date": "2025-02-05",
|
114 |
+
"size": 1.8,
|
115 |
+
"pages": 32,
|
116 |
+
"related_entity": "T-2025-002",
|
117 |
+
"path": "/documents/T-2025-002/specs_v1.0.pdf"
|
118 |
+
},
|
119 |
+
{
|
120 |
+
"id": "DOC009",
|
121 |
+
"name": "كراسة الشروط - مناقصة صيانة طرق",
|
122 |
+
"type": "كراسة شروط",
|
123 |
+
"version": "1.1",
|
124 |
+
"date": "2025-03-15",
|
125 |
+
"size": 1.9,
|
126 |
+
"pages": 34,
|
127 |
+
"related_entity": "T-2025-002",
|
128 |
+
"path": "/documents/T-2025-002/specs_v1.1.pdf"
|
129 |
+
},
|
130 |
+
{
|
131 |
+
"id": "DOC010",
|
132 |
+
"name": "جدول الكميات - مناقصة صيانة طرق",
|
133 |
+
"type": "جدول كميات",
|
134 |
+
"version": "1.0",
|
135 |
+
"date": "2025-02-05",
|
136 |
+
"size": 0.9,
|
137 |
+
"pages": 15,
|
138 |
+
"related_entity": "T-2025-002",
|
139 |
+
"path": "/documents/T-2025-002/boq_v1.0.xlsx"
|
140 |
+
}
|
141 |
+
]
|
142 |
+
|
143 |
+
# بيانات نموذجية لمحتوى المستندات (للعرض فقط)
|
144 |
+
self.sample_document_content = {
|
145 |
+
"DOC001": """
|
146 |
+
# كراسة الشروط والمواصفات
|
147 |
+
## مناقصة إنشاء مبنى إداري
|
148 |
+
|
149 |
+
### 1. مقدمة
|
150 |
+
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
|
151 |
+
|
152 |
+
### 2. نطاق العمل
|
153 |
+
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5000 متر مربع، ويشمل ذلك:
|
154 |
+
- أعمال الهيكل الإنشائي
|
155 |
+
- أعمال التشطيبات الداخلية والخارجية
|
156 |
+
- أعمال الكهرباء والميكانيكا
|
157 |
+
- أعمال تنسيق الموقع
|
158 |
+
|
159 |
+
### 3. المواصفات الفنية
|
160 |
+
#### 3.1 أعمال الخرسانة
|
161 |
+
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 30 نيوتن/مم²
|
162 |
+
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
163 |
+
|
164 |
+
#### 3.2 أعمال التشطيبات
|
165 |
+
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
166 |
+
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
167 |
+
|
168 |
+
### 4. الشروط العامة
|
169 |
+
- مدة التنفيذ: 18 شهراً من تاريخ استلام الموقع
|
170 |
+
- غرامة التأخير: 0.1% من قيمة العقد عن كل يوم تأخير
|
171 |
+
- ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
|
172 |
+
""",
|
173 |
+
|
174 |
+
"DOC002": """
|
175 |
+
# كراسة الشروط والمواصفات
|
176 |
+
## مناقصة إنشاء مبنى إداري
|
177 |
+
|
178 |
+
### 1. مقدمة
|
179 |
+
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
|
180 |
+
|
181 |
+
### 2. نطاق العمل
|
182 |
+
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 5 طوابق بمساحة إجمالية 5500 متر مربع، ويشمل ذلك:
|
183 |
+
- أعمال الهيكل الإنشائي
|
184 |
+
- أعمال التشطيبات الداخلية والخارجية
|
185 |
+
- أعمال الكهرباء والميكانيكا
|
186 |
+
- أعمال تنسيق الموقع
|
187 |
+
- أعمال أنظمة الأمن والسلامة
|
188 |
+
|
189 |
+
### 3. المواصفات الفنية
|
190 |
+
#### 3.1 أعمال الخرسانة
|
191 |
+
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 35 نيوتن/مم²
|
192 |
+
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
193 |
+
|
194 |
+
#### 3.2 أعمال التشطيبات
|
195 |
+
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
196 |
+
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
197 |
+
- يجب استخدام زجاج عاكس للحرارة للواجهات
|
198 |
+
|
199 |
+
### 4. الشروط العامة
|
200 |
+
- مدة التنفيذ: 16 شهراً من تاريخ استلام الموقع
|
201 |
+
- غرامة التأخير: 0.15% من قيمة العقد عن كل يوم تأخير
|
202 |
+
- ضمان الأعمال: 10 سنوات للهيكل الإنشائي، 5 سنوات للأعمال الميكانيكية والكهربائية
|
203 |
+
""",
|
204 |
+
|
205 |
+
"DOC003": """
|
206 |
+
# كراسة الشروط والمواصفات
|
207 |
+
## مناقصة إنشاء مبنى إداري
|
208 |
+
|
209 |
+
### 1. مقدمة
|
210 |
+
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض وفقاً للمواصفات المعتمدة من الهيئة السعودية للمواصفات والمقاييس.
|
211 |
+
|
212 |
+
### 2. نطاق العمل
|
213 |
+
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
|
214 |
+
- أعمال الهيكل الإنشائي
|
215 |
+
- أعمال التشطيبات الداخلية والخارجية
|
216 |
+
- أعمال الكهرباء والميكانيكا
|
217 |
+
- أعمال تنسيق الموقع
|
218 |
+
- أعمال أنظمة الأمن والسلامة
|
219 |
+
- أعمال أنظمة المباني الذكية
|
220 |
+
|
221 |
+
### 3. المواصفات الفنية
|
222 |
+
#### 3.1 أعمال الخرسانة
|
223 |
+
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
|
224 |
+
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
225 |
+
- يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
|
226 |
+
|
227 |
+
#### 3.2 أعمال التشطيبات
|
228 |
+
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
229 |
+
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
230 |
+
- يجب استخدام زجاج عاكس للحرارة للواجهات
|
231 |
+
- يجب استخدام مواد صديقة للبيئة
|
232 |
+
|
233 |
+
### 4. الشروط العامة
|
234 |
+
- مدة التنفيذ: 15 شهراً من تاريخ استلام الموقع
|
235 |
+
- غرامة التأخير: 0.2% من قيمة العقد عن كل يوم تأخير
|
236 |
+
- ضمان الأعمال: 15 سنوات للهيكل الإنشائي، 7 سنوات للأعمال الميكانيكية والكهربائية
|
237 |
+
|
238 |
+
### 5. متطلبات الاستدامة
|
239 |
+
- يجب أن يحقق المبنى متطلبات الاستدامة وفقاً لمعايير LEED
|
240 |
+
- يجب توفير أنظمة لترشيد استهلاك الطاقة والمياه
|
241 |
+
"""
|
242 |
+
}
|
243 |
+
|
244 |
+
def run(self):
|
245 |
+
"""تشغيل تطبيق مقارنة المستندات"""
|
246 |
+
# إنشاء قائمة العناصر
|
247 |
+
menu_items = [
|
248 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
249 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
250 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
251 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
252 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
253 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
254 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
255 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
256 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
257 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
258 |
+
{"name": "الإشعارات", "icon": "bell"},
|
259 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
260 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
261 |
+
{"name": "التقارير", "icon": "bar-chart"},
|
262 |
+
{"name": "الإعدادات", "icon": "gear"}
|
263 |
+
]
|
264 |
+
|
265 |
+
# إنشاء الشريط الجانبي
|
266 |
+
selected = self.ui.create_sidebar(menu_items)
|
267 |
+
|
268 |
+
# إنشاء ترويسة الصفحة
|
269 |
+
self.ui.create_header("مقارنة المستندات", "أدوات متقدمة لمقارنة وتحليل المستندات")
|
270 |
+
|
271 |
+
# إنشاء علامات تبويب للوظائف المختلفة
|
272 |
+
tabs = st.tabs(["مقارنة الإصدارات", "مقارنة المستندات", "تحليل التغييرات", "سجل التغييرات"])
|
273 |
+
|
274 |
+
# علامة تبويب مقارنة الإصدارات
|
275 |
+
with tabs[0]:
|
276 |
+
self.compare_versions()
|
277 |
+
|
278 |
+
# علامة تبويب مقارنة المستندات
|
279 |
+
with tabs[1]:
|
280 |
+
self.compare_documents()
|
281 |
+
|
282 |
+
# علامة تبويب تحليل التغييرات
|
283 |
+
with tabs[2]:
|
284 |
+
self.analyze_changes()
|
285 |
+
|
286 |
+
# علامة تبويب سجل التغييرات
|
287 |
+
with tabs[3]:
|
288 |
+
self.show_change_history()
|
289 |
+
|
290 |
+
def compare_versions(self):
|
291 |
+
"""مقارنة إصدارات المستندات"""
|
292 |
+
st.markdown("### مقارنة إصدارات المستندات")
|
293 |
+
|
294 |
+
# اختيار المناقصة
|
295 |
+
tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
296 |
+
selected_tender = st.selectbox(
|
297 |
+
"اختر المناقصة",
|
298 |
+
options=tender_options
|
299 |
+
)
|
300 |
+
|
301 |
+
# فلترة المستندات حسب المناقصة المختارة
|
302 |
+
filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
|
303 |
+
|
304 |
+
# اختيار نوع المستند
|
305 |
+
doc_types = list(set([doc["type"] for doc in filtered_docs]))
|
306 |
+
selected_type = st.selectbox(
|
307 |
+
"اختر نوع المستند",
|
308 |
+
options=doc_types
|
309 |
+
)
|
310 |
+
|
311 |
+
# فلترة المستندات حسب النوع المختار
|
312 |
+
type_filtered_docs = [doc for doc in filtered_docs if doc["type"] == selected_type]
|
313 |
+
|
314 |
+
# ترتيب المستندات حسب الإصدار
|
315 |
+
type_filtered_docs = sorted(type_filtered_docs, key=lambda x: x["version"])
|
316 |
+
|
317 |
+
if len(type_filtered_docs) < 2:
|
318 |
+
st.warning("يجب توفر إصدارين على الأقل للمقارنة")
|
319 |
+
else:
|
320 |
+
# اختيار الإصدارات للمقارنة
|
321 |
+
col1, col2 = st.columns(2)
|
322 |
+
|
323 |
+
with col1:
|
324 |
+
version_options = [f"{doc['name']} (الإصدار {doc['version']})" for doc in type_filtered_docs]
|
325 |
+
selected_version1_index = st.selectbox(
|
326 |
+
"الإصدار الأول",
|
327 |
+
options=range(len(version_options)),
|
328 |
+
format_func=lambda x: version_options[x]
|
329 |
+
)
|
330 |
+
selected_doc1 = type_filtered_docs[selected_version1_index]
|
331 |
+
|
332 |
+
with col2:
|
333 |
+
remaining_indices = [i for i in range(len(type_filtered_docs)) if i != selected_version1_index]
|
334 |
+
selected_version2_index = st.selectbox(
|
335 |
+
"الإصدار الثاني",
|
336 |
+
options=remaining_indices,
|
337 |
+
format_func=lambda x: version_options[x]
|
338 |
+
)
|
339 |
+
selected_doc2 = type_filtered_docs[selected_version2_index]
|
340 |
+
|
341 |
+
# زر بدء المقارنة
|
342 |
+
if st.button("بدء المقارنة", use_container_width=True):
|
343 |
+
# عرض معلومات المستندات المختارة
|
344 |
+
st.markdown("### معلومات المستندات المختارة")
|
345 |
+
|
346 |
+
col1, col2 = st.columns(2)
|
347 |
+
|
348 |
+
with col1:
|
349 |
+
st.markdown(f"**الإصدار الأول:** {selected_doc1['version']}")
|
350 |
+
st.markdown(f"**التاريخ:** {selected_doc1['date']}")
|
351 |
+
st.markdown(f"**عدد الصفحات:** {selected_doc1['pages']}")
|
352 |
+
st.markdown(f"**الحجم:** {selected_doc1['size']} ميجابايت")
|
353 |
+
|
354 |
+
with col2:
|
355 |
+
st.markdown(f"**الإصدار الثاني:** {selected_doc2['version']}")
|
356 |
+
st.markdown(f"**التاريخ:** {selected_doc2['date']}")
|
357 |
+
st.markdown(f"**عدد الصفحات:** {selected_doc2['pages']}")
|
358 |
+
st.markdown(f"**الحجم:** {selected_doc2['size']} ميجابايت")
|
359 |
+
|
360 |
+
# الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
|
361 |
+
doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
|
362 |
+
doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
|
363 |
+
|
364 |
+
# إجراء المقارنة
|
365 |
+
self.display_comparison(doc1_content, doc2_content)
|
366 |
+
|
367 |
+
def display_comparison(self, text1, text2):
|
368 |
+
"""عرض نتائج المقارنة بين نصين"""
|
369 |
+
st.markdown("### نتائج المقارنة")
|
370 |
+
|
371 |
+
# تقسيم النصوص إلى أسطر
|
372 |
+
lines1 = text1.splitlines()
|
373 |
+
lines2 = text2.splitlines()
|
374 |
+
|
375 |
+
# إجراء المقارنة باستخدام difflib
|
376 |
+
d = difflib.Differ()
|
377 |
+
diff = list(d.compare(lines1, lines2))
|
378 |
+
|
379 |
+
# عرض ملخص التغييرات
|
380 |
+
added = len([line for line in diff if line.startswith('+ ')])
|
381 |
+
removed = len([line for line in diff if line.startswith('- ')])
|
382 |
+
changed = len([line for line in diff if line.startswith('? ')])
|
383 |
+
|
384 |
+
col1, col2, col3 = st.columns(3)
|
385 |
+
|
386 |
+
with col1:
|
387 |
+
self.ui.create_metric_card(
|
388 |
+
"الإضافات",
|
389 |
+
str(added),
|
390 |
+
None,
|
391 |
+
self.ui.COLORS['success']
|
392 |
+
)
|
393 |
+
|
394 |
+
with col2:
|
395 |
+
self.ui.create_metric_card(
|
396 |
+
"الحذف",
|
397 |
+
str(removed),
|
398 |
+
None,
|
399 |
+
self.ui.COLORS['danger']
|
400 |
+
)
|
401 |
+
|
402 |
+
with col3:
|
403 |
+
self.ui.create_metric_card(
|
404 |
+
"التغييرات",
|
405 |
+
str(changed // 2), # تقسيم على 2 لأن كل تغيير يظهر مرتين
|
406 |
+
None,
|
407 |
+
self.ui.COLORS['warning']
|
408 |
+
)
|
409 |
+
|
410 |
+
# عرض التغييرات بالتفصيل
|
411 |
+
st.markdown("### التغييرات بالتفصيل")
|
412 |
+
|
413 |
+
# إنشاء عرض HTML للتغييرات
|
414 |
+
html_diff = []
|
415 |
+
for line in diff:
|
416 |
+
if line.startswith('+ '):
|
417 |
+
html_diff.append(f'<div style="background-color: #e6ffe6; padding: 2px 5px; margin: 2px 0; border-left: 3px solid green;">{line[2:]}</div>')
|
418 |
+
elif line.startswith('- '):
|
419 |
+
html_diff.append(f'<div style="background-color: #ffe6e6; padding: 2px 5px; margin: 2px 0; border-left: 3px solid red;">{line[2:]}</div>')
|
420 |
+
elif line.startswith('? '):
|
421 |
+
# تجاهل أسطر التفاصيل
|
422 |
+
continue
|
423 |
+
else:
|
424 |
+
html_diff.append(f'<div style="padding: 2px 5px; margin: 2px 0;">{line[2:]}</div>')
|
425 |
+
|
426 |
+
# عرض التغييرات
|
427 |
+
st.markdown(''.join(html_diff), unsafe_allow_html=True)
|
428 |
+
|
429 |
+
# خيارات إضافية
|
430 |
+
st.markdown("### خيارات إضافية")
|
431 |
+
|
432 |
+
col1, col2, col3 = st.columns(3)
|
433 |
+
|
434 |
+
with col1:
|
435 |
+
if st.button("تصدير التغييرات", use_container_width=True):
|
436 |
+
st.success("تم تصدير التغييرات بنجاح")
|
437 |
+
|
438 |
+
with col2:
|
439 |
+
if st.button("إنشاء تقرير", use_container_width=True):
|
440 |
+
st.success("تم إنشاء التقرير بنجاح")
|
441 |
+
|
442 |
+
with col3:
|
443 |
+
if st.button("حفظ المقارنة", use_container_width=True):
|
444 |
+
st.success("تم حفظ المقارنة بنجاح")
|
445 |
+
|
446 |
+
def compare_documents(self):
|
447 |
+
"""مقارنة مستندات مختلفة"""
|
448 |
+
st.markdown("### مقارنة مستندات مختلفة")
|
449 |
+
|
450 |
+
# اختيار المستند الأول
|
451 |
+
col1, col2 = st.columns(2)
|
452 |
+
|
453 |
+
with col1:
|
454 |
+
tender1_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
455 |
+
selected_tender1 = st.selectbox(
|
456 |
+
"اختر المناقصة الأولى",
|
457 |
+
options=tender1_options,
|
458 |
+
key="tender1"
|
459 |
+
)
|
460 |
+
|
461 |
+
# فلترة المستندات حسب المناقصة المختارة
|
462 |
+
filtered_docs1 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender1]
|
463 |
+
|
464 |
+
# اختيار المستند
|
465 |
+
doc_options1 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs1]
|
466 |
+
selected_doc1_index = st.selectbox(
|
467 |
+
"اختر المستند الأول",
|
468 |
+
options=range(len(doc_options1)),
|
469 |
+
format_func=lambda x: doc_options1[x],
|
470 |
+
key="doc1"
|
471 |
+
)
|
472 |
+
selected_doc1 = filtered_docs1[selected_doc1_index]
|
473 |
+
|
474 |
+
with col2:
|
475 |
+
tender2_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
476 |
+
selected_tender2 = st.selectbox(
|
477 |
+
"اختر المناقصة الثانية",
|
478 |
+
options=tender2_options,
|
479 |
+
key="tender2"
|
480 |
+
)
|
481 |
+
|
482 |
+
# فلترة المستندات حسب المناقصة المختارة
|
483 |
+
filtered_docs2 = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender2]
|
484 |
+
|
485 |
+
# اختيار المستند
|
486 |
+
doc_options2 = [f"{doc['name']} (الإصدار {doc['version']})" for doc in filtered_docs2]
|
487 |
+
selected_doc2_index = st.selectbox(
|
488 |
+
"اختر المستند الثاني",
|
489 |
+
options=range(len(doc_options2)),
|
490 |
+
format_func=lambda x: doc_options2[x],
|
491 |
+
key="doc2"
|
492 |
+
)
|
493 |
+
selected_doc2 = filtered_docs2[selected_doc2_index]
|
494 |
+
|
495 |
+
# خيارات المقارنة
|
496 |
+
st.markdown("### خيارات المقارنة")
|
497 |
+
|
498 |
+
col1, col2, col3 = st.columns(3)
|
499 |
+
|
500 |
+
with col1:
|
501 |
+
comparison_type = st.radio(
|
502 |
+
"نوع المقارنة",
|
503 |
+
options=["مقارنة كاملة", "مقارنة الأقسام المتطابقة فقط", "مقارنة الاختلافات فقط"]
|
504 |
+
)
|
505 |
+
|
506 |
+
with col2:
|
507 |
+
ignore_options = st.multiselect(
|
508 |
+
"تجاهل",
|
509 |
+
options=["المسافات", "علامات الترقيم", "حالة الأحرف", "الأرقام"],
|
510 |
+
default=["المسافات"]
|
511 |
+
)
|
512 |
+
|
513 |
+
with col3:
|
514 |
+
similarity_threshold = st.slider(
|
515 |
+
"عتبة التشابه",
|
516 |
+
min_value=0.0,
|
517 |
+
max_value=1.0,
|
518 |
+
value=0.7,
|
519 |
+
step=0.05
|
520 |
+
)
|
521 |
+
|
522 |
+
# زر بدء المقارنة
|
523 |
+
if st.button("بدء المقارنة بين المستندات", use_container_width=True):
|
524 |
+
# عرض معلومات المستندات المختارة
|
525 |
+
st.markdown("### معلومات المستندات المختارة")
|
526 |
+
|
527 |
+
col1, col2 = st.columns(2)
|
528 |
+
|
529 |
+
with col1:
|
530 |
+
st.markdown(f"**المستند الأول:** {selected_doc1['name']}")
|
531 |
+
st.markdown(f"**الإصدار:** {selected_doc1['version']}")
|
532 |
+
st.markdown(f"**التاريخ:** {selected_doc1['date']}")
|
533 |
+
st.markdown(f"**المناقصة:** {selected_doc1['related_entity']}")
|
534 |
+
|
535 |
+
with col2:
|
536 |
+
st.markdown(f"**المستند الثاني:** {selected_doc2['name']}")
|
537 |
+
st.markdown(f"**الإصدار:** {selected_doc2['version']}")
|
538 |
+
st.markdown(f"**التاريخ:** {selected_doc2['date']}")
|
539 |
+
st.markdown(f"**المناقصة:** {selected_doc2['related_entity']}")
|
540 |
+
|
541 |
+
# الحصول على محتوى المستندات (في تطبيق حقيقي، سيتم استرجاع المحتوى من الملفات الفعلية)
|
542 |
+
doc1_content = self.sample_document_content.get(selected_doc1["id"], "محتوى المستند غير متوفر")
|
543 |
+
doc2_content = self.sample_document_content.get(selected_doc2["id"], "محتوى المستند غير متوفر")
|
544 |
+
|
545 |
+
# إجراء المقارنة
|
546 |
+
self.display_document_comparison(doc1_content, doc2_content, comparison_type, ignore_options, similarity_threshold)
|
547 |
+
|
548 |
+
def display_document_comparison(self, text1, text2, comparison_type, ignore_options, similarity_threshold):
|
549 |
+
"""عرض نتائج المقارنة بين مستندين"""
|
550 |
+
st.markdown("### نتائج المقارنة بين المستندين")
|
551 |
+
|
552 |
+
# تقسيم النصوص إلى أقسام (في هذا المثال، نستخدم العناوين كفواصل للأقسام)
|
553 |
+
sections1 = self.split_into_sections(text1)
|
554 |
+
sections2 = self.split_into_sections(text2)
|
555 |
+
|
556 |
+
# حساب نسبة التشابه الإجمالية
|
557 |
+
similarity = difflib.SequenceMatcher(None, text1, text2).ratio()
|
558 |
+
|
559 |
+
# عرض نسبة التشابه
|
560 |
+
st.markdown(f"**نسبة التشابه الإجمالية:** {similarity:.2%}")
|
561 |
+
|
562 |
+
# عرض مقارنة الأقسام
|
563 |
+
st.markdown("### مقارنة الأقسام")
|
564 |
+
|
565 |
+
# إنشاء جدول لمقارنة الأقسام
|
566 |
+
section_comparisons = []
|
567 |
+
|
568 |
+
for section1_title, section1_content in sections1.items():
|
569 |
+
best_match = None
|
570 |
+
best_similarity = 0
|
571 |
+
|
572 |
+
for section2_title, section2_content in sections2.items():
|
573 |
+
# حساب نسبة التشابه بين عناوين الأقسام
|
574 |
+
title_similarity = difflib.SequenceMatcher(None, section1_title, section2_title).ratio()
|
575 |
+
|
576 |
+
# حساب نسبة التشابه بين محتوى الأقسام
|
577 |
+
content_similarity = difflib.SequenceMatcher(None, section1_content, section2_content).ratio()
|
578 |
+
|
579 |
+
# حساب متوسط نسبة التشابه
|
580 |
+
avg_similarity = (title_similarity + content_similarity) / 2
|
581 |
+
|
582 |
+
if avg_similarity > best_similarity:
|
583 |
+
best_similarity = avg_similarity
|
584 |
+
best_match = {
|
585 |
+
"title": section2_title,
|
586 |
+
"content": section2_content,
|
587 |
+
"similarity": avg_similarity
|
588 |
+
}
|
589 |
+
|
590 |
+
# إضافة المقارنة إلى القائمة
|
591 |
+
if best_match and best_similarity >= similarity_threshold:
|
592 |
+
section_comparisons.append({
|
593 |
+
"section1_title": section1_title,
|
594 |
+
"section2_title": best_match["title"],
|
595 |
+
"similarity": best_similarity
|
596 |
+
})
|
597 |
+
else:
|
598 |
+
section_comparisons.append({
|
599 |
+
"section1_title": section1_title,
|
600 |
+
"section2_title": "غير موجود",
|
601 |
+
"similarity": 0
|
602 |
+
})
|
603 |
+
|
604 |
+
# إضافة الأقسام الموجودة في المستند الثاني فقط
|
605 |
+
for section2_title, section2_content in sections2.items():
|
606 |
+
if not any(comp["section2_title"] == section2_title for comp in section_comparisons):
|
607 |
+
section_comparisons.append({
|
608 |
+
"section1_title": "غير موجود",
|
609 |
+
"section2_title": section2_title,
|
610 |
+
"similarity": 0
|
611 |
+
})
|
612 |
+
|
613 |
+
# عرض جدول المقارنة
|
614 |
+
section_df = pd.DataFrame(section_comparisons)
|
615 |
+
section_df = section_df.rename(columns={
|
616 |
+
"section1_title": "القسم في المستند الأول",
|
617 |
+
"section2_title": "القسم في المستند الثاني",
|
618 |
+
"similarity": "نسبة التشابه"
|
619 |
+
})
|
620 |
+
|
621 |
+
# تنسيق نسبة التشابه
|
622 |
+
section_df["نسبة التشابه"] = section_df["نسبة التشابه"].apply(lambda x: f"{x:.2%}")
|
623 |
+
|
624 |
+
st.dataframe(
|
625 |
+
section_df,
|
626 |
+
use_container_width=True,
|
627 |
+
hide_index=True
|
628 |
+
)
|
629 |
+
|
630 |
+
# عرض تفاصيل المقارنة
|
631 |
+
st.markdown("### تفاصيل المقارنة")
|
632 |
+
|
633 |
+
# اختيار قسم للمقارنة التفصيلية
|
634 |
+
selected_section = st.selectbox(
|
635 |
+
"اختر قسماً للمقارنة التفصيلية",
|
636 |
+
options=[comp["section1_title"] for comp in section_comparisons if comp["section1_title"] != "غير موجود"]
|
637 |
+
)
|
638 |
+
|
639 |
+
# العثور على القسم المقابل في المستند الثاني
|
640 |
+
matching_comparison = next((comp for comp in section_comparisons if comp["section1_title"] == selected_section), None)
|
641 |
+
|
642 |
+
if matching_comparison and matching_comparison["section2_title"] != "غير موجود":
|
643 |
+
# الحصول على محتوى القسمين
|
644 |
+
section1_content = sections1[selected_section]
|
645 |
+
section2_content = sections2[matching_comparison["section2_title"]]
|
646 |
+
|
647 |
+
# عرض المقارنة التفصيلية
|
648 |
+
self.display_comparison(section1_content, section2_content)
|
649 |
+
else:
|
650 |
+
st.warning("القسم المحدد غير موجود في المستند الثاني")
|
651 |
+
|
652 |
+
def split_into_sections(self, text):
|
653 |
+
"""تقسيم النص إلى أقسام باستخدام العناوين"""
|
654 |
+
sections = {}
|
655 |
+
current_section = None
|
656 |
+
current_content = []
|
657 |
+
|
658 |
+
for line in text.splitlines():
|
659 |
+
# البحث عن العناوين (الأسطر التي تبدأ بـ #)
|
660 |
+
if line.strip().startswith('#'):
|
661 |
+
# حفظ القسم السابق إذا وجد
|
662 |
+
if current_section:
|
663 |
+
sections[current_section] = '\n'.join(current_content)
|
664 |
+
|
665 |
+
# بدء قسم جديد
|
666 |
+
current_section = line.strip()
|
667 |
+
current_content = []
|
668 |
+
elif current_section:
|
669 |
+
# إضافة السطر إلى محتوى القسم الحالي
|
670 |
+
current_content.append(line)
|
671 |
+
|
672 |
+
# حفظ القسم الأخير
|
673 |
+
if current_section:
|
674 |
+
sections[current_section] = '\n'.join(current_content)
|
675 |
+
|
676 |
+
return sections
|
677 |
+
|
678 |
+
def analyze_changes(self):
|
679 |
+
"""تحليل التغييرات في المستندات"""
|
680 |
+
st.markdown("### تحليل التغييرات في المستندات")
|
681 |
+
|
682 |
+
# اختيار المناقصة
|
683 |
+
tender_options = list(set([doc["related_entity"] for doc in self.documents_data]))
|
684 |
+
selected_tender = st.selectbox(
|
685 |
+
"اختر المناقصة",
|
686 |
+
options=tender_options,
|
687 |
+
key="analyze_tender"
|
688 |
+
)
|
689 |
+
|
690 |
+
# فلترة المستندات حسب المناقصة المختارة
|
691 |
+
filtered_docs = [doc for doc in self.documents_data if doc["related_entity"] == selected_tender]
|
692 |
+
|
693 |
+
# تجميع المستندات حسب النوع
|
694 |
+
doc_types = {}
|
695 |
+
for doc in filtered_docs:
|
696 |
+
if doc["type"] not in doc_types:
|
697 |
+
doc_types[doc["type"]] = []
|
698 |
+
doc_types[doc["type"]].append(doc)
|
699 |
+
|
700 |
+
# عرض تحليل التغييرات لكل نوع مستند
|
701 |
+
for doc_type, docs in doc_types.items():
|
702 |
+
if len(docs) > 1:
|
703 |
+
with st.expander(f"تحليل التغييرات في {doc_type}"):
|
704 |
+
# ترتيب المستندات حسب الإصدار
|
705 |
+
sorted_docs = sorted(docs, key=lambda x: x["version"])
|
706 |
+
|
707 |
+
# عرض معلومات الإصدارات
|
708 |
+
st.markdown(f"**عدد الإصدارات:** {len(sorted_docs)}")
|
709 |
+
st.markdown(f"**أول إصدار:** {sorted_docs[0]['version']} ({sorted_docs[0]['date']})")
|
710 |
+
st.markdown(f"**آخر إصدار:** {sorted_docs[-1]['version']} ({sorted_docs[-1]['date']})")
|
711 |
+
|
712 |
+
# حساب التغييرات بين الإصدارات
|
713 |
+
changes = []
|
714 |
+
for i in range(1, len(sorted_docs)):
|
715 |
+
prev_doc = sorted_docs[i-1]
|
716 |
+
curr_doc = sorted_docs[i]
|
717 |
+
|
718 |
+
# حساب التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
|
719 |
+
page_diff = curr_doc["pages"] - prev_doc["pages"]
|
720 |
+
size_diff = curr_doc["size"] - prev_doc["size"]
|
721 |
+
|
722 |
+
changes.append({
|
723 |
+
"from_version": prev_doc["version"],
|
724 |
+
"to_version": curr_doc["version"],
|
725 |
+
"date": curr_doc["date"],
|
726 |
+
"page_diff": page_diff,
|
727 |
+
"size_diff": size_diff
|
728 |
+
})
|
729 |
+
|
730 |
+
# عرض جدول التغييرات
|
731 |
+
changes_df = pd.DataFrame(changes)
|
732 |
+
changes_df = changes_df.rename(columns={
|
733 |
+
"from_version": "من الإصدار",
|
734 |
+
"to_version": "إلى الإصدار",
|
735 |
+
"date": "تاريخ التغيير",
|
736 |
+
"page_diff": "التغيير في عدد الصفحات",
|
737 |
+
"size_diff": "التغيير في الحجم (ميجابايت)"
|
738 |
+
})
|
739 |
+
|
740 |
+
st.dataframe(
|
741 |
+
changes_df,
|
742 |
+
use_container_width=True,
|
743 |
+
hide_index=True
|
744 |
+
)
|
745 |
+
|
746 |
+
# عرض رسم بياني للتغييرات
|
747 |
+
st.markdown("#### تطور حجم المستند عبر الإصدارات")
|
748 |
+
|
749 |
+
versions = [doc["version"] for doc in sorted_docs]
|
750 |
+
sizes = [doc["size"] for doc in sorted_docs]
|
751 |
+
|
752 |
+
chart_data = pd.DataFrame({
|
753 |
+
"الإصدار": versions,
|
754 |
+
"الحجم (ميجابايت)": sizes
|
755 |
+
})
|
756 |
+
|
757 |
+
st.line_chart(chart_data.set_index("الإصدار"))
|
758 |
+
|
759 |
+
# عرض رسم بياني لعدد الصفحات
|
760 |
+
st.markdown("#### تطور عدد الصفحات عبر الإصدارات")
|
761 |
+
|
762 |
+
pages = [doc["pages"] for doc in sorted_docs]
|
763 |
+
|
764 |
+
chart_data = pd.DataFrame({
|
765 |
+
"الإصدار": versions,
|
766 |
+
"عدد الصفحات": pages
|
767 |
+
})
|
768 |
+
|
769 |
+
st.line_chart(chart_data.set_index("الإصدار"))
|
770 |
+
|
771 |
+
# تحليل التغييرات الشاملة
|
772 |
+
st.markdown("### تحليل التغييرات الشاملة")
|
773 |
+
|
774 |
+
# حساب إجمالي التغييرات (في تطبيق حقيقي، سيتم تحليل المحتوى الفعلي)
|
775 |
+
total_docs = len(filtered_docs)
|
776 |
+
total_versions = sum(len(docs) for docs in doc_types.values())
|
777 |
+
avg_versions = total_versions / len(doc_types) if doc_types else 0
|
778 |
+
|
779 |
+
col1, col2, col3 = st.columns(3)
|
780 |
+
|
781 |
+
with col1:
|
782 |
+
self.ui.create_metric_card(
|
783 |
+
"إجمالي المستندات",
|
784 |
+
str(total_docs),
|
785 |
+
None,
|
786 |
+
self.ui.COLORS['primary']
|
787 |
+
)
|
788 |
+
|
789 |
+
with col2:
|
790 |
+
self.ui.create_metric_card(
|
791 |
+
"إجمالي الإصدارات",
|
792 |
+
str(total_versions),
|
793 |
+
None,
|
794 |
+
self.ui.COLORS['secondary']
|
795 |
+
)
|
796 |
+
|
797 |
+
with col3:
|
798 |
+
self.ui.create_metric_card(
|
799 |
+
"متوسط الإصدارات لكل نوع",
|
800 |
+
f"{avg_versions:.1f}",
|
801 |
+
None,
|
802 |
+
self.ui.COLORS['accent']
|
803 |
+
)
|
804 |
+
|
805 |
+
# عرض توزيع التغييرات حسب النوع
|
806 |
+
st.markdown("#### توزيع الإصدارات حسب نوع المستند")
|
807 |
+
|
808 |
+
type_counts = {doc_type: len(docs) for doc_type, docs in doc_types.items()}
|
809 |
+
|
810 |
+
chart_data = pd.DataFrame({
|
811 |
+
"نوع المستند": list(type_counts.keys()),
|
812 |
+
"عدد الإصدارات": list(type_counts.values())
|
813 |
+
})
|
814 |
+
|
815 |
+
st.bar_chart(chart_data.set_index("نوع المستند"))
|
816 |
+
|
817 |
+
def show_change_history(self):
|
818 |
+
"""عرض سجل التغييرات"""
|
819 |
+
st.markdown("### سجل التغييرات")
|
820 |
+
|
821 |
+
# إنشاء بيانات نموذجية لسجل التغييرات
|
822 |
+
change_history = [
|
823 |
+
{
|
824 |
+
"id": "CH001",
|
825 |
+
"document_id": "DOC001",
|
826 |
+
"document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
827 |
+
"from_version": "1.0",
|
828 |
+
"to_version": "1.1",
|
829 |
+
"change_date": "2025-02-10",
|
830 |
+
"change_type": "تحديث",
|
831 |
+
"changed_by": "أحمد محمد",
|
832 |
+
"description": "تحديث المواصفات الفنية وشروط التنفيذ",
|
833 |
+
"sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
|
834 |
+
},
|
835 |
+
{
|
836 |
+
"id": "CH002",
|
837 |
+
"document_id": "DOC002",
|
838 |
+
"document_name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
839 |
+
"from_version": "1.1",
|
840 |
+
"to_version": "2.0",
|
841 |
+
"change_date": "2025-03-05",
|
842 |
+
"change_type": "تحديث رئيسي",
|
843 |
+
"changed_by": "سارة عبدالله",
|
844 |
+
"description": "إضافة متطلبات الاستدامة وتحديث المواصفات الفنية",
|
845 |
+
"sections_changed": ["المواصفات الفنية", "الشروط العامة", "متطلبات الاستدامة"]
|
846 |
+
},
|
847 |
+
{
|
848 |
+
"id": "CH003",
|
849 |
+
"document_id": "DOC004",
|
850 |
+
"document_name": "جدول الكميات - مناقصة إنشاء مبنى إداري",
|
851 |
+
"from_version": "1.0",
|
852 |
+
"to_version": "1.1",
|
853 |
+
"change_date": "2025-02-20",
|
854 |
+
"change_type": "تحديث",
|
855 |
+
"changed_by": "خالد عمر",
|
856 |
+
"description": "تحديث الكميات وإضافة بنود جديدة",
|
857 |
+
"sections_changed": ["أعمال الهيكل الإنشائي", "أعمال التشطيبات", "أعمال الكهرباء"]
|
858 |
+
},
|
859 |
+
{
|
860 |
+
"id": "CH004",
|
861 |
+
"document_id": "DOC006",
|
862 |
+
"document_name": "المخططات - مناقصة إنشاء مبنى إداري",
|
863 |
+
"from_version": "1.0",
|
864 |
+
"to_version": "2.0",
|
865 |
+
"change_date": "2025-03-10",
|
866 |
+
"change_type": "تحديث رئيسي",
|
867 |
+
"changed_by": "محمد علي",
|
868 |
+
"description": "تحديث المخططات المعمارية والإنشائية",
|
869 |
+
"sections_changed": ["المخططات المعمارية", "المخططات الإنشائية", "مخططات الكهرباء"]
|
870 |
+
},
|
871 |
+
{
|
872 |
+
"id": "CH005",
|
873 |
+
"document_id": "DOC008",
|
874 |
+
"document_name": "كراسة الشروط - مناقصة صيانة طرق",
|
875 |
+
"from_version": "1.0",
|
876 |
+
"to_version": "1.1",
|
877 |
+
"change_date": "2025-03-15",
|
878 |
+
"change_type": "تحديث",
|
879 |
+
"changed_by": "فاطمة أحمد",
|
880 |
+
"description": "تحديث المواصفات الفنية وشروط التنفيذ",
|
881 |
+
"sections_changed": ["نطاق العمل", "المواصفات الفنية", "الشروط العامة"]
|
882 |
+
}
|
883 |
+
]
|
884 |
+
|
885 |
+
# إنشاء فلاتر للسجل
|
886 |
+
col1, col2, col3 = st.columns(3)
|
887 |
+
|
888 |
+
with col1:
|
889 |
+
document_filter = st.selectbox(
|
890 |
+
"المستند",
|
891 |
+
options=["الكل"] + list(set([ch["document_name"] for ch in change_history]))
|
892 |
+
)
|
893 |
+
|
894 |
+
with col2:
|
895 |
+
change_type_filter = st.selectbox(
|
896 |
+
"نوع التغيير",
|
897 |
+
options=["الكل"] + list(set([ch["change_type"] for ch in change_history]))
|
898 |
+
)
|
899 |
+
|
900 |
+
with col3:
|
901 |
+
date_range = st.date_input(
|
902 |
+
"نطاق التاريخ",
|
903 |
+
value=(
|
904 |
+
datetime.datetime.strptime("2025-01-01", "%Y-%m-%d").date(),
|
905 |
+
datetime.datetime.strptime("2025-12-31", "%Y-%m-%d").date()
|
906 |
+
)
|
907 |
+
)
|
908 |
+
|
909 |
+
# تطبيق الفلاتر
|
910 |
+
filtered_history = change_history
|
911 |
+
|
912 |
+
if document_filter != "الكل":
|
913 |
+
filtered_history = [ch for ch in filtered_history if ch["document_name"] == document_filter]
|
914 |
+
|
915 |
+
if change_type_filter != "الكل":
|
916 |
+
filtered_history = [ch for ch in filtered_history if ch["change_type"] == change_type_filter]
|
917 |
+
|
918 |
+
if len(date_range) == 2:
|
919 |
+
start_date, end_date = date_range
|
920 |
+
filtered_history = [
|
921 |
+
ch for ch in filtered_history
|
922 |
+
if start_date <= datetime.datetime.strptime(ch["change_date"], "%Y-%m-%d").date() <= end_date
|
923 |
+
]
|
924 |
+
|
925 |
+
# عرض سجل التغييرات
|
926 |
+
if not filtered_history:
|
927 |
+
st.info("لا توجد تغييرات تطابق الفلاتر المحددة")
|
928 |
+
else:
|
929 |
+
# تحويل البيانات إلى DataFrame
|
930 |
+
history_df = pd.DataFrame(filtered_history)
|
931 |
+
|
932 |
+
# إعادة ترتيب الأعمدة وتغيير أسمائها
|
933 |
+
display_df = history_df[[
|
934 |
+
"id", "document_name", "from_version", "to_version", "change_date", "change_type", "changed_by", "description"
|
935 |
+
]].rename(columns={
|
936 |
+
"id": "الرقم",
|
937 |
+
"document_name": "اسم المستند",
|
938 |
+
"from_version": "من الإصدار",
|
939 |
+
"to_version": "إلى الإصدار",
|
940 |
+
"change_date": "تاريخ التغيير",
|
941 |
+
"change_type": "نوع التغيير",
|
942 |
+
"changed_by": "بواسطة",
|
943 |
+
"description": "الوصف"
|
944 |
+
})
|
945 |
+
|
946 |
+
# عرض الجدول
|
947 |
+
st.dataframe(
|
948 |
+
display_df,
|
949 |
+
use_container_width=True,
|
950 |
+
hide_index=True
|
951 |
+
)
|
952 |
+
|
953 |
+
# عرض تفاصيل التغيير المحدد
|
954 |
+
st.markdown("### تفاصيل التغيير")
|
955 |
+
|
956 |
+
selected_change_id = st.selectbox(
|
957 |
+
"اختر تغييراً لعرض التفاصيل",
|
958 |
+
options=[ch["id"] for ch in filtered_history],
|
959 |
+
format_func=lambda x: next((f"{ch['id']} - {ch['document_name']} ({ch['from_version']} إلى {ch['to_version']})" for ch in filtered_history if ch["id"] == x), "")
|
960 |
+
)
|
961 |
+
|
962 |
+
# العثور على التغيير المحدد
|
963 |
+
selected_change = next((ch for ch in filtered_history if ch["id"] == selected_change_id), None)
|
964 |
+
|
965 |
+
if selected_change:
|
966 |
+
col1, col2 = st.columns(2)
|
967 |
+
|
968 |
+
with col1:
|
969 |
+
st.markdown(f"**المستند:** {selected_change['document_name']}")
|
970 |
+
st.markdown(f"**من الإصدار:** {selected_change['from_version']}")
|
971 |
+
st.markdown(f"**إلى الإصدار:** {selected_change['to_version']}")
|
972 |
+
st.markdown(f"**تاريخ التغيير:** {selected_change['change_date']}")
|
973 |
+
|
974 |
+
with col2:
|
975 |
+
st.markdown(f"**نوع التغيير:** {selected_change['change_type']}")
|
976 |
+
st.markdown(f"**بواسطة:** {selected_change['changed_by']}")
|
977 |
+
st.markdown(f"**الوصف:** {selected_change['description']}")
|
978 |
+
|
979 |
+
# عرض الأقسام التي تم تغييرها
|
980 |
+
st.markdown("#### الأقسام التي تم تغييرها")
|
981 |
+
|
982 |
+
for section in selected_change["sections_changed"]:
|
983 |
+
st.markdown(f"- {section}")
|
984 |
+
|
985 |
+
# أزرار الإجراءات
|
986 |
+
col1, col2, col3 = st.columns(3)
|
987 |
+
|
988 |
+
with col1:
|
989 |
+
if st.button("عرض التغييرات بالتفصيل", use_container_width=True):
|
990 |
+
st.success("تم فتح التغييرات بالتفصيل")
|
991 |
+
|
992 |
+
with col2:
|
993 |
+
if st.button("إنشاء تقرير", use_container_width=True):
|
994 |
+
st.success("تم إنشاء التقرير بنجاح")
|
995 |
+
|
996 |
+
with col3:
|
997 |
+
if st.button("تصدير التغييرات", use_container_width=True):
|
998 |
+
st.success("تم تصدير التغييرات بنجاح")
|
999 |
+
|
1000 |
+
# تشغيل التطبيق
|
1001 |
+
if __name__ == "__main__":
|
1002 |
+
doc_comparison_app = DocumentComparisonApp()
|
1003 |
+
doc_comparison_app.run()
|
modules/maps/maps_app.py
CHANGED
@@ -1,456 +1,456 @@
|
|
1 |
-
"""
|
2 |
-
وحدة الخرائط والمواقع - نظام تحليل المناقصات
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
import pandas as pd
|
7 |
-
import numpy as np
|
8 |
-
import folium
|
9 |
-
from streamlit_folium import folium_static
|
10 |
-
import json
|
11 |
-
import os
|
12 |
-
import sys
|
13 |
-
from pathlib import Path
|
14 |
-
|
15 |
-
# إضافة مسار المشروع للنظام
|
16 |
-
sys.path.append(str(Path(__file__).parent.parent))
|
17 |
-
|
18 |
-
# استيراد محسن واجهة المستخدم
|
19 |
-
from styling.enhanced_ui import UIEnhancer
|
20 |
-
|
21 |
-
class MapsApp:
|
22 |
-
"""تطبيق الخرائط والمواقع"""
|
23 |
-
|
24 |
-
def __init__(self):
|
25 |
-
"""تهيئة تطبيق الخرائط والمواقع"""
|
26 |
-
self.ui = UIEnhancer(page_title="الخرائط والمواقع - نظام تحليل المناقصات", page_icon="🗺️")
|
27 |
-
self.ui.apply_theme_colors()
|
28 |
-
|
29 |
-
# بيانات المشاريع (نموذجية)
|
30 |
-
self.projects_data = [
|
31 |
-
{
|
32 |
-
"id": "P001",
|
33 |
-
"name": "إنشاء مبنى إداري - الرياض",
|
34 |
-
"location": "الرياض",
|
35 |
-
"coordinates": [24.7136, 46.6753],
|
36 |
-
"status": "جاري التنفيذ",
|
37 |
-
"budget": 15000000,
|
38 |
-
"completion": 45,
|
39 |
-
"client": "وزارة الإسكان",
|
40 |
-
"start_date": "2024-10-15",
|
41 |
-
"end_date": "2025-12-30"
|
42 |
-
},
|
43 |
-
{
|
44 |
-
"id": "P002",
|
45 |
-
"name": "تطوير طريق الملك فهد - جدة",
|
46 |
-
"location": "جدة",
|
47 |
-
"coordinates": [21.5433, 39.1728],
|
48 |
-
"status": "قيد الدراسة",
|
49 |
-
"budget": 8500000,
|
50 |
-
"completion": 0,
|
51 |
-
"client": "أمانة جدة",
|
52 |
-
"start_date": "2025-05-01",
|
53 |
-
"end_date": "2026-02-28"
|
54 |
-
},
|
55 |
-
{
|
56 |
-
"id": "P003",
|
57 |
-
"name": "إنشاء مجمع سكني - الدمام",
|
58 |
-
"location": "الدمام",
|
59 |
-
"coordinates": [26.4207, 50.0888],
|
60 |
-
"status": "مكتمل",
|
61 |
-
"budget": 22000000,
|
62 |
-
"completion": 100,
|
63 |
-
"client": "شركة الإسكان للتطوير",
|
64 |
-
"start_date": "2023-08-10",
|
65 |
-
"end_date": "2025-01-15"
|
66 |
-
},
|
67 |
-
{
|
68 |
-
"id": "P004",
|
69 |
-
"name": "بناء مدرسة - أبها",
|
70 |
-
"location": "أبها",
|
71 |
-
"coordinates": [18.2164, 42.5053],
|
72 |
-
"status": "جاري التنفيذ",
|
73 |
-
"budget": 5200000,
|
74 |
-
"completion": 75,
|
75 |
-
"client": "وزارة التعليم",
|
76 |
-
"start_date": "2024-06-20",
|
77 |
-
"end_date": "2025-07-30"
|
78 |
-
},
|
79 |
-
{
|
80 |
-
"id": "P005",
|
81 |
-
"name": "تطوير شبكة مياه - المدينة المنورة",
|
82 |
-
"location": "المدينة المنورة",
|
83 |
-
"coordinates": [24.5247, 39.5692],
|
84 |
-
"status": "جاري التنفيذ",
|
85 |
-
"budget": 12800000,
|
86 |
-
"completion": 30,
|
87 |
-
"client": "شركة المياه الوطنية",
|
88 |
-
"start_date": "2024-11-05",
|
89 |
-
"end_date": "2026-03-15"
|
90 |
-
}
|
91 |
-
]
|
92 |
-
|
93 |
-
def run(self):
|
94 |
-
"""تشغيل تطبيق الخرائط والمواقع"""
|
95 |
-
# إنشاء قائمة العناصر
|
96 |
-
menu_items = [
|
97 |
-
{"name": "لوحة المعلومات", "icon": "house"},
|
98 |
-
{"name": "المناقصات والعقود", "icon": "file-text"},
|
99 |
-
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
100 |
-
{"name": "نظام التسعير", "icon": "calculator"},
|
101 |
-
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
102 |
-
{"name": "الموارد والتكاليف", "icon": "people"},
|
103 |
-
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
104 |
-
{"name": "إدارة المشاريع", "icon": "kanban"},
|
105 |
-
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
106 |
-
{"name": "الجدول الزمني", "icon": "calendar3"},
|
107 |
-
{"name": "الإشعارات", "icon": "bell"},
|
108 |
-
{"name": "مقارنة المستندات", "icon": "files"},
|
109 |
-
{"name": "المساعد الذكي", "icon": "robot"},
|
110 |
-
{"name": "التقارير", "icon": "bar-chart"},
|
111 |
-
{"name": "الإعدادات", "icon": "gear"}
|
112 |
-
]
|
113 |
-
|
114 |
-
# إنشاء الشريط الجانبي
|
115 |
-
selected = self.ui.create_sidebar(menu_items)
|
116 |
-
|
117 |
-
# إنشاء ترويسة الصفحة
|
118 |
-
self.ui.create_header("الخرائط والمواقع", "عرض وإدارة مواقع المشاريع")
|
119 |
-
|
120 |
-
# إنشاء علامات تبويب
|
121 |
-
tabs = st.tabs(["خريطة المشاريع", "تفاصيل المواقع", "إضافة موقع جديد", "تحليل المناطق"])
|
122 |
-
|
123 |
-
# علامة تبويب خريطة المشاريع
|
124 |
-
with tabs[0]:
|
125 |
-
self.show_projects_map()
|
126 |
-
|
127 |
-
# علامة تبويب تفاصيل المواقع
|
128 |
-
with tabs[1]:
|
129 |
-
self.show_location_details()
|
130 |
-
|
131 |
-
# علامة تبويب إضافة موقع جديد
|
132 |
-
with tabs[2]:
|
133 |
-
self.add_new_location()
|
134 |
-
|
135 |
-
# علامة تبويب تحليل المناطق
|
136 |
-
with tabs[3]:
|
137 |
-
self.analyze_regions()
|
138 |
-
|
139 |
-
def show_projects_map(self):
|
140 |
-
"""عرض خريطة المشاريع"""
|
141 |
-
# إنشاء فلاتر للخريطة
|
142 |
-
col1, col2, col3 = st.columns(3)
|
143 |
-
|
144 |
-
with col1:
|
145 |
-
status_filter = st.multiselect(
|
146 |
-
"حالة المشروع",
|
147 |
-
options=["الكل", "جاري التنفيذ", "قيد الدراسة", "مكتمل"],
|
148 |
-
default=["الكل"]
|
149 |
-
)
|
150 |
-
|
151 |
-
with col2:
|
152 |
-
location_filter = st.multiselect(
|
153 |
-
"الموقع",
|
154 |
-
options=["الكل"] + list(set([p["location"] for p in self.projects_data])),
|
155 |
-
default=["الكل"]
|
156 |
-
)
|
157 |
-
|
158 |
-
with col3:
|
159 |
-
budget_range = st.slider(
|
160 |
-
"نطاق الميزانية (مليون ريال)",
|
161 |
-
0.0, 25.0, (0.0, 25.0),
|
162 |
-
step=0.5
|
163 |
-
)
|
164 |
-
|
165 |
-
# تطبيق الفلاتر
|
166 |
-
filtered_projects = self.projects_data
|
167 |
-
|
168 |
-
if "الكل" not in status_filter and status_filter:
|
169 |
-
filtered_projects = [p for p in filtered_projects if p["status"] in status_filter]
|
170 |
-
|
171 |
-
if "الكل" not in location_filter and location_filter:
|
172 |
-
filtered_projects = [p for p in filtered_projects if p["location"] in location_filter]
|
173 |
-
|
174 |
-
filtered_projects = [p for p in filtered_projects if budget_range[0] * 1000000 <= p["budget"] <= budget_range[1] * 1000000]
|
175 |
-
|
176 |
-
# إنشاء الخريطة
|
177 |
-
st.markdown("### خريطة المشاريع")
|
178 |
-
|
179 |
-
# تحديد مركز الخريطة (وسط المملكة العربية السعودية تقريباً)
|
180 |
-
center = [24.0, 45.0]
|
181 |
-
|
182 |
-
# إنشاء خريطة folium
|
183 |
-
m = folium.Map(location=center, zoom_start=5, tiles="OpenStreetMap")
|
184 |
-
|
185 |
-
# إضافة المشاريع إلى الخريطة
|
186 |
-
for project in filtered_projects:
|
187 |
-
# تحديد لون العلامة بناءً على حالة المشروع
|
188 |
-
if project["status"] == "جاري التنفيذ":
|
189 |
-
color = "blue"
|
190 |
-
elif project["status"] == "قيد الدراسة":
|
191 |
-
color = "orange"
|
192 |
-
elif project["status"] == "مكتمل":
|
193 |
-
color = "green"
|
194 |
-
else:
|
195 |
-
color = "gray"
|
196 |
-
|
197 |
-
# إنشاء نص النافذة المنبثقة
|
198 |
-
popup_text = f"""
|
199 |
-
<div dir="rtl" style="text-align: right; width: 200px;">
|
200 |
-
<h4>{project['name']}</h4>
|
201 |
-
<p><strong>الحالة:</strong> {project['status']}</p>
|
202 |
-
<p><strong>الميزانية:</strong> {project['budget']:,} ريال</p>
|
203 |
-
<p><strong>نسبة الإنجاز:</strong> {project['completion']}%</p>
|
204 |
-
<p><strong>العميل:</strong> {project['client']}</p>
|
205 |
-
<p><strong>تاريخ البدء:</strong> {project['start_date']}</p>
|
206 |
-
<p><strong>تاريخ الانتهاء:</strong> {project['end_date']}</p>
|
207 |
-
<a href="#" onclick="alert('تم فتح تفاصيل المشروع');">عرض التفاصيل</a>
|
208 |
-
</div>
|
209 |
-
"""
|
210 |
-
|
211 |
-
# إضافة علامة للمشروع
|
212 |
-
folium.Marker(
|
213 |
-
location=project["coordinates"],
|
214 |
-
popup=folium.Popup(popup_text, max_width=300),
|
215 |
-
tooltip=project["name"],
|
216 |
-
icon=folium.Icon(color=color, icon="info-sign")
|
217 |
-
).add_to(m)
|
218 |
-
|
219 |
-
# عرض الخريطة
|
220 |
-
folium_static(m, width=1000, height=500)
|
221 |
-
|
222 |
-
# عرض إحصائيات المشاريع
|
223 |
-
st.markdown("### إحصائيات
|
224 |
-
|
225 |
-
col1, col2, col3, col4 = st.columns(4)
|
226 |
-
|
227 |
-
with col1:
|
228 |
-
self.ui.create_metric_card(
|
229 |
-
"إجمالي المشاريع",
|
230 |
-
str(len(filtered_projects)),
|
231 |
-
None,
|
232 |
-
self.ui.COLORS['primary']
|
233 |
-
)
|
234 |
-
|
235 |
-
with col2:
|
236 |
-
projects_in_progress = len([p for p in filtered_projects if p["status"] == "جاري التنفيذ"])
|
237 |
-
self.ui.create_metric_card(
|
238 |
-
"مشاريع
|
239 |
-
str(projects_in_progress),
|
240 |
-
None,
|
241 |
-
self.ui.COLORS['secondary']
|
242 |
-
)
|
243 |
-
|
244 |
-
with col3:
|
245 |
-
total_budget = sum([p["budget"] for p in filtered_projects])
|
246 |
-
self.ui.create_metric_card(
|
247 |
-
"إجمالي الميزانية",
|
248 |
-
f"{total_budget/1000000:.1f} مليون ريال",
|
249 |
-
None,
|
250 |
-
self.ui.COLORS['accent']
|
251 |
-
)
|
252 |
-
|
253 |
-
with col4:
|
254 |
-
avg_completion = np.mean([p["completion"] for p in filtered_projects])
|
255 |
-
self.ui.create_metric_card(
|
256 |
-
"متوسط نسبة الإنجاز",
|
257 |
-
f"{avg_completion:.1f}%",
|
258 |
-
None,
|
259 |
-
self.ui.COLORS['success']
|
260 |
-
)
|
261 |
-
|
262 |
-
def show_location_details(self):
|
263 |
-
"""عرض تفاصيل المواقع"""
|
264 |
-
st.markdown("### تفاصيل مواقع المشاريع")
|
265 |
-
|
266 |
-
# إنشاء جدول بيانات المشاريع
|
267 |
-
projects_df = pd.DataFrame(self.projects_data)
|
268 |
-
projects_df = projects_df.rename(columns={
|
269 |
-
"id": "رقم المشروع",
|
270 |
-
"name": "اسم المشروع",
|
271 |
-
"location": "الموقع",
|
272 |
-
"status": "الحالة",
|
273 |
-
"budget": "الميزانية (ريال)",
|
274 |
-
"completion": "نسبة الإنجاز (%)",
|
275 |
-
"client": "العميل",
|
276 |
-
"start_date": "تاريخ البدء",
|
277 |
-
"end_date": "تاريخ الانتهاء"
|
278 |
-
})
|
279 |
-
|
280 |
-
# حذف عمود الإحداثيات من العرض
|
281 |
-
projects_df = projects_df.drop(columns=["coordinates"])
|
282 |
-
|
283 |
-
# عرض الجدول
|
284 |
-
st.dataframe(
|
285 |
-
projects_df,
|
286 |
-
use_container_width=True,
|
287 |
-
hide_index=True
|
288 |
-
)
|
289 |
-
|
290 |
-
# إضافة خيار تصدير البيانات
|
291 |
-
col1, col2 = st.columns([1, 5])
|
292 |
-
with col1:
|
293 |
-
self.ui.create_button("تصدير البيانات", "primary")
|
294 |
-
|
295 |
-
# عرض تفاصيل مشروع محدد
|
296 |
-
st.markdown("### تفاصيل مشروع محدد")
|
297 |
-
|
298 |
-
selected_project = st.selectbox(
|
299 |
-
"اختر مشروعاً لعرض التفاصيل",
|
300 |
-
options=[p["name"] for p in self.projects_data]
|
301 |
-
)
|
302 |
-
|
303 |
-
# العثور على المشروع المحدد
|
304 |
-
project = next((p for p in self.projects_data if p["name"] == selected_project), None)
|
305 |
-
|
306 |
-
if project:
|
307 |
-
col1, col2 = st.columns([2, 1])
|
308 |
-
|
309 |
-
with col1:
|
310 |
-
# عرض تفاصيل المشروع
|
311 |
-
st.markdown(f"#### {project['name']}")
|
312 |
-
st.markdown(f"**الموقع:** {project['location']}")
|
313 |
-
st.markdown(f"**الحالة:** {project['status']}")
|
314 |
-
st.markdown(f"**الميزانية:** {project['budget']:,} ريال")
|
315 |
-
st.markdown(f"**نسبة الإنجاز:** {project['completion']}%")
|
316 |
-
st.markdown(f"**العميل:** {project['client']}")
|
317 |
-
st.markdown(f"**تاريخ البدء:** {project['start_date']}")
|
318 |
-
st.markdown(f"**تاريخ الانتهاء:** {project['end_date']}")
|
319 |
-
|
320 |
-
# أزرار الإجراءات
|
321 |
-
col1, col2, col3 = st.columns(3)
|
322 |
-
with col1:
|
323 |
-
self.ui.create_button("تعديل البيانات", "primary")
|
324 |
-
with col2:
|
325 |
-
self.ui.create_button("عرض المستندات", "secondary")
|
326 |
-
with col3:
|
327 |
-
self.ui.create_button("تقرير الموقع", "accent")
|
328 |
-
|
329 |
-
with col2:
|
330 |
-
# عرض خريطة مصغرة للمشروع
|
331 |
-
m = folium.Map(location=project["coordinates"], zoom_start=12)
|
332 |
-
folium.Marker(
|
333 |
-
location=project["coordinates"],
|
334 |
-
tooltip=project["name"],
|
335 |
-
icon=folium.Icon(color="red", icon="info-sign")
|
336 |
-
).add_to(m)
|
337 |
-
folium_static(m, width=300, height=300)
|
338 |
-
|
339 |
-
def add_new_location(self):
|
340 |
-
"""إضافة موقع جديد"""
|
341 |
-
st.markdown("### إضافة موقع مشروع جديد")
|
342 |
-
|
343 |
-
# نموذج إضافة موقع جديد
|
344 |
-
with st.form("new_location_form"):
|
345 |
-
col1, col2 = st.columns(2)
|
346 |
-
|
347 |
-
with col1:
|
348 |
-
project_id = st.text_input("رقم المشروع", value="P00" + str(len(self.projects_data) + 1))
|
349 |
-
project_name = st.text_input("اسم المشروع")
|
350 |
-
location = st.text_input("الموقع")
|
351 |
-
status = st.selectbox(
|
352 |
-
"الحالة",
|
353 |
-
options=["جاري التنفيذ", "قيد الدراسة", "مكتمل"]
|
354 |
-
)
|
355 |
-
budget = st.number_input("الميزانية (ريال)", min_value=0, step=100000)
|
356 |
-
|
357 |
-
with col2:
|
358 |
-
completion = st.slider("نسبة الإنجاز (%)", 0, 100, 0)
|
359 |
-
client = st.text_input("العميل")
|
360 |
-
start_date = st.date_input("تاريخ البدء")
|
361 |
-
end_date = st.date_input("تاريخ الانتهاء")
|
362 |
-
|
363 |
-
st.markdown("### تحديد الموقع على الخريطة")
|
364 |
-
st.markdown("انقر على الخريطة لتحديد موقع المشروع أو أدخل الإحداثيات يدوياً")
|
365 |
-
|
366 |
-
col1, col2 = st.columns(2)
|
367 |
-
|
368 |
-
with col1:
|
369 |
-
latitude = st.number_input("خط العرض", value=24.0, format="%.4f")
|
370 |
-
|
371 |
-
with col2:
|
372 |
-
longitude = st.number_input("خط الطول", value=45.0, format="%.4f")
|
373 |
-
|
374 |
-
# عرض الخريطة لتحديد الموقع
|
375 |
-
m = folium.Map(location=[latitude, longitude], zoom_start=5)
|
376 |
-
folium.Marker(
|
377 |
-
location=[latitude, longitude],
|
378 |
-
tooltip="موقع المشروع الجديد",
|
379 |
-
icon=folium.Icon(color="red", icon="info-sign")
|
380 |
-
).add_to(m)
|
381 |
-
folium_static(m, width=700, height=300)
|
382 |
-
|
383 |
-
# زر الإرسال
|
384 |
-
submit_button = st.form_submit_button("إضافة المشروع")
|
385 |
-
|
386 |
-
if submit_button:
|
387 |
-
# إضافة المشروع الجديد (في تطبيق حقيقي، سيتم حفظ البيانات في قاعدة البيانات)
|
388 |
-
st.success("تم إضافة المشروع بنجاح!")
|
389 |
-
|
390 |
-
# إعادة تعيين النموذج
|
391 |
-
st.experimental_rerun()
|
392 |
-
|
393 |
-
def analyze_regions(self):
|
394 |
-
"""تحليل المناطق"""
|
395 |
-
st.markdown("### تحليل المناطق")
|
396 |
-
|
397 |
-
# إنشاء بيانات المناطق (نموذجية)
|
398 |
-
regions_data = {
|
399 |
-
"المنطقة": ["الرياض", "مكة المكرمة", "المدينة المنورة", "القصيم", "المنطقة الشرقية", "عسير", "تبوك", "حائل", "الحدود الشمالية", "جازان", "نجران", "الباحة", "الجوف"],
|
400 |
-
"عدد المشاريع": [15, 12, 8, 5, 18, 7, 4, 3, 2, 6, 3, 2, 3],
|
401 |
-
"إجمالي الميزانية (مليون ريال)": [120, 95, 45, 30, 150, 40, 25, 18, 12, 35, 20, 15, 22],
|
402 |
-
"متوسط مدة المشروع (شهر)": [18, 16, 14, 12, 20, 15, 12, 10, 9, 14, 12, 10, 11]
|
403 |
-
}
|
404 |
-
|
405 |
-
regions_df = pd.DataFrame(regions_data)
|
406 |
-
|
407 |
-
# عرض خريطة حرارية للمناطق
|
408 |
-
st.markdown("#### توزيع المشاريع حسب المناطق")
|
409 |
-
|
410 |
-
# في تطبيق حقيقي، يمكن استخدام خريطة حرارية حقيقية للمملكة
|
411 |
-
st.image("https://via.placeholder.com/800x400?text=خريطة+حرارية+للمشاريع+حسب+المناطق", use_column_width=True)
|
412 |
-
|
413 |
-
# عرض إحصائيات المناطق
|
414 |
-
st.markdown("#### إحصائيات المناطق")
|
415 |
-
|
416 |
-
# عرض الجدول
|
417 |
-
st.dataframe(
|
418 |
-
regions_df,
|
419 |
-
use_container_width=True,
|
420 |
-
hide_index=True
|
421 |
-
)
|
422 |
-
|
423 |
-
# عرض رسوم بيانية للمقارنة
|
424 |
-
st.markdown("#### مقارنة المناطق")
|
425 |
-
|
426 |
-
chart_type = st.radio(
|
427 |
-
"نوع الرسم البياني",
|
428 |
-
options=["عدد المشاريع", "إجمالي الميزانية", "متوسط مدة المشروع"],
|
429 |
-
horizontal=True
|
430 |
-
)
|
431 |
-
|
432 |
-
if chart_type == "عدد المشاريع":
|
433 |
-
chart_data = regions_df[["المنطقة", "عدد المشاريع"]].sort_values(by="عدد المشاريع", ascending=False)
|
434 |
-
st.bar_chart(chart_data.set_index("المنطقة"))
|
435 |
-
elif chart_type == "إجمالي الميزانية":
|
436 |
-
chart_data = regions_df[["المنطقة", "إجمالي الميزانية (مليون ريال)"]].sort_values(by="إجمالي الميزانية (
|
437 |
-
st.bar_chart(chart_data.set_index("المنطقة"))
|
438 |
-
else:
|
439 |
-
chart_data = regions_df[["المنطقة", "متوسط مدة المشروع (شهر)"]].sort_values(by="متوسط مدة المشروع (شهر)", ascending=False)
|
440 |
-
st.bar_chart(chart_data.set_index("المنطقة"))
|
441 |
-
|
442 |
-
# تحليل الكثافة
|
443 |
-
st.markdown("#### تحليل كثافة المشاريع")
|
444 |
-
st.markdown("""
|
445 |
-
يوضح هذا التحليل توزيع المشاريع حسب المناطق الجغرافية، مما يساعد في:
|
446 |
-
- تحديد المناطق ذات النشاط العالي
|
447 |
-
- تحديد فرص النمو في
|
448 |
-
- تخطيط الموارد بناءً على التوزيع الجغرافي
|
449 |
-
""")
|
450 |
-
|
451 |
-
# في تطبيق حقيقي، يمكن إضافة تحليلات أكثر تفصيلاً
|
452 |
-
|
453 |
-
# تشغيل التطبيق
|
454 |
-
if __name__ == "__main__":
|
455 |
-
maps_app = MapsApp()
|
456 |
-
maps_app.run()
|
|
|
1 |
+
"""
|
2 |
+
وحدة الخرائط والمواقع - نظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import folium
|
9 |
+
from streamlit_folium import folium_static
|
10 |
+
import json
|
11 |
+
import os
|
12 |
+
import sys
|
13 |
+
from pathlib import Path
|
14 |
+
|
15 |
+
# إضافة مسار المشروع للنظام
|
16 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
17 |
+
|
18 |
+
# استيراد محسن واجهة المستخدم
|
19 |
+
from styling.enhanced_ui import UIEnhancer
|
20 |
+
|
21 |
+
class MapsApp:
|
22 |
+
"""تطبيق الخرائط والمواقع"""
|
23 |
+
|
24 |
+
def __init__(self):
|
25 |
+
"""تهيئة تطبيق الخرائط والمواقع"""
|
26 |
+
self.ui = UIEnhancer(page_title="الخرائط والمواقع - نظام تحليل المناقصات", page_icon="🗺️")
|
27 |
+
self.ui.apply_theme_colors()
|
28 |
+
|
29 |
+
# بيانات المشاريع (نموذجية)
|
30 |
+
self.projects_data = [
|
31 |
+
{
|
32 |
+
"id": "P001",
|
33 |
+
"name": "إنشاء مبنى إداري - الرياض",
|
34 |
+
"location": "الرياض",
|
35 |
+
"coordinates": [24.7136, 46.6753],
|
36 |
+
"status": "جاري التنفيذ",
|
37 |
+
"budget": 15000000,
|
38 |
+
"completion": 45,
|
39 |
+
"client": "وزارة الإسكان",
|
40 |
+
"start_date": "2024-10-15",
|
41 |
+
"end_date": "2025-12-30"
|
42 |
+
},
|
43 |
+
{
|
44 |
+
"id": "P002",
|
45 |
+
"name": "تطوير طريق الملك فهد - جدة",
|
46 |
+
"location": "جدة",
|
47 |
+
"coordinates": [21.5433, 39.1728],
|
48 |
+
"status": "قيد الدراسة",
|
49 |
+
"budget": 8500000,
|
50 |
+
"completion": 0,
|
51 |
+
"client": "أمانة جدة",
|
52 |
+
"start_date": "2025-05-01",
|
53 |
+
"end_date": "2026-02-28"
|
54 |
+
},
|
55 |
+
{
|
56 |
+
"id": "P003",
|
57 |
+
"name": "إنشاء مجمع سكني - الدمام",
|
58 |
+
"location": "الدمام",
|
59 |
+
"coordinates": [26.4207, 50.0888],
|
60 |
+
"status": "مكتمل",
|
61 |
+
"budget": 22000000,
|
62 |
+
"completion": 100,
|
63 |
+
"client": "شركة الإسكان للتطوير",
|
64 |
+
"start_date": "2023-08-10",
|
65 |
+
"end_date": "2025-01-15"
|
66 |
+
},
|
67 |
+
{
|
68 |
+
"id": "P004",
|
69 |
+
"name": "بناء مدرسة - أبها",
|
70 |
+
"location": "أبها",
|
71 |
+
"coordinates": [18.2164, 42.5053],
|
72 |
+
"status": "جاري التنفيذ",
|
73 |
+
"budget": 5200000,
|
74 |
+
"completion": 75,
|
75 |
+
"client": "وزارة التعليم",
|
76 |
+
"start_date": "2024-06-20",
|
77 |
+
"end_date": "2025-07-30"
|
78 |
+
},
|
79 |
+
{
|
80 |
+
"id": "P005",
|
81 |
+
"name": "تطوير شبكة مياه - المدينة المنورة",
|
82 |
+
"location": "المدينة المنورة",
|
83 |
+
"coordinates": [24.5247, 39.5692],
|
84 |
+
"status": "جاري التنفيذ",
|
85 |
+
"budget": 12800000,
|
86 |
+
"completion": 30,
|
87 |
+
"client": "شركة المياه الوطنية",
|
88 |
+
"start_date": "2024-11-05",
|
89 |
+
"end_date": "2026-03-15"
|
90 |
+
}
|
91 |
+
]
|
92 |
+
|
93 |
+
def run(self):
|
94 |
+
"""تشغيل تطبيق الخرائط والمواقع"""
|
95 |
+
# إنشاء قائمة العناصر
|
96 |
+
menu_items = [
|
97 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
98 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
99 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
100 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
101 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
102 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
103 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
104 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
105 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
106 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
107 |
+
{"name": "الإشعارات", "icon": "bell"},
|
108 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
109 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
110 |
+
{"name": "التقارير", "icon": "bar-chart"},
|
111 |
+
{"name": "الإعدادات", "icon": "gear"}
|
112 |
+
]
|
113 |
+
|
114 |
+
# إنشاء الشريط الجانبي
|
115 |
+
selected = self.ui.create_sidebar(menu_items)
|
116 |
+
|
117 |
+
# إنشاء ترويسة الصفحة
|
118 |
+
self.ui.create_header("الخرائط والمواقع", "عرض وإدارة مواقع المشاريع")
|
119 |
+
|
120 |
+
# إنشاء علامات تبويب للوظائف المختلفة
|
121 |
+
tabs = st.tabs(["خريطة المشاريع", "تفاصيل المواقع", "إضافة موقع جديد", "تحليل المناطق"])
|
122 |
+
|
123 |
+
# علامة تبويب خريطة المشاريع
|
124 |
+
with tabs[0]:
|
125 |
+
self.show_projects_map()
|
126 |
+
|
127 |
+
# علامة تبويب تفاصيل المواقع
|
128 |
+
with tabs[1]:
|
129 |
+
self.show_location_details()
|
130 |
+
|
131 |
+
# علامة تبويب إضافة موقع جديد
|
132 |
+
with tabs[2]:
|
133 |
+
self.add_new_location()
|
134 |
+
|
135 |
+
# علامة تبويب تحليل المناطق
|
136 |
+
with tabs[3]:
|
137 |
+
self.analyze_regions()
|
138 |
+
|
139 |
+
def show_projects_map(self):
|
140 |
+
"""عرض خريطة المشاريع"""
|
141 |
+
# إنشاء فلاتر للخريطة
|
142 |
+
col1, col2, col3 = st.columns(3)
|
143 |
+
|
144 |
+
with col1:
|
145 |
+
status_filter = st.multiselect(
|
146 |
+
"حالة المشروع",
|
147 |
+
options=["الكل", "جاري التنفيذ", "قيد الدراسة", "مكتمل"],
|
148 |
+
default=["الكل"]
|
149 |
+
)
|
150 |
+
|
151 |
+
with col2:
|
152 |
+
location_filter = st.multiselect(
|
153 |
+
"الموقع",
|
154 |
+
options=["الكل"] + list(set([p["location"] for p in self.projects_data])),
|
155 |
+
default=["الكل"]
|
156 |
+
)
|
157 |
+
|
158 |
+
with col3:
|
159 |
+
budget_range = st.slider(
|
160 |
+
"نطاق الميزانية (مليون ريال)",
|
161 |
+
0.0, 25.0, (0.0, 25.0),
|
162 |
+
step=0.5
|
163 |
+
)
|
164 |
+
|
165 |
+
# تطبيق الفلاتر
|
166 |
+
filtered_projects = self.projects_data
|
167 |
+
|
168 |
+
if "الكل" not in status_filter and status_filter:
|
169 |
+
filtered_projects = [p for p in filtered_projects if p["status"] in status_filter]
|
170 |
+
|
171 |
+
if "الكل" not in location_filter and location_filter:
|
172 |
+
filtered_projects = [p for p in filtered_projects if p["location"] in location_filter]
|
173 |
+
|
174 |
+
filtered_projects = [p for p in filtered_projects if budget_range[0] * 1000000 <= p["budget"] <= budget_range[1] * 1000000]
|
175 |
+
|
176 |
+
# إنشاء الخريطة
|
177 |
+
st.markdown("### خريطة المشاريع")
|
178 |
+
|
179 |
+
# تحديد مركز الخريطة (وسط المملكة العربية السعودية تقريباً)
|
180 |
+
center = [24.0, 45.0]
|
181 |
+
|
182 |
+
# إنشاء خريطة folium
|
183 |
+
m = folium.Map(location=center, zoom_start=5, tiles="OpenStreetMap")
|
184 |
+
|
185 |
+
# إضافة المشاريع إلى الخريطة
|
186 |
+
for project in filtered_projects:
|
187 |
+
# تحديد لون العلامة بناءً على حالة المشروع
|
188 |
+
if project["status"] == "جاري التنفيذ":
|
189 |
+
color = "blue"
|
190 |
+
elif project["status"] == "قيد الدراسة":
|
191 |
+
color = "orange"
|
192 |
+
elif project["status"] == "مكتمل":
|
193 |
+
color = "green"
|
194 |
+
else:
|
195 |
+
color = "gray"
|
196 |
+
|
197 |
+
# إنشاء نص النافذة المنبثقة
|
198 |
+
popup_text = f"""
|
199 |
+
<div dir="rtl" style="text-align: right; width: 200px;">
|
200 |
+
<h4>{project['name']}</h4>
|
201 |
+
<p><strong>الحالة:</strong> {project['status']}</p>
|
202 |
+
<p><strong>الميزانية:</strong> {project['budget']:,} ريال</p>
|
203 |
+
<p><strong>نسبة الإنجاز:</strong> {project['completion']}%</p>
|
204 |
+
<p><strong>العميل:</strong> {project['client']}</p>
|
205 |
+
<p><strong>تاريخ البدء:</strong> {project['start_date']}</p>
|
206 |
+
<p><strong>تاريخ الانتهاء:</strong> {project['end_date']}</p>
|
207 |
+
<a href="#" onclick="alert('تم فتح تفاصيل المشروع');">عرض التفاصيل</a>
|
208 |
+
</div>
|
209 |
+
"""
|
210 |
+
|
211 |
+
# إضافة علامة للمشروع
|
212 |
+
folium.Marker(
|
213 |
+
location=project["coordinates"],
|
214 |
+
popup=folium.Popup(popup_text, max_width=300),
|
215 |
+
tooltip=project["name"],
|
216 |
+
icon=folium.Icon(color=color, icon="info-sign")
|
217 |
+
).add_to(m)
|
218 |
+
|
219 |
+
# عرض الخريطة
|
220 |
+
folium_static(m, width=1000, height=500)
|
221 |
+
|
222 |
+
# عرض إحصائيات المشاريع
|
223 |
+
st.markdown("### إحصائيات ��لمشاريع")
|
224 |
+
|
225 |
+
col1, col2, col3, col4 = st.columns(4)
|
226 |
+
|
227 |
+
with col1:
|
228 |
+
self.ui.create_metric_card(
|
229 |
+
"إجمالي المشاريع",
|
230 |
+
str(len(filtered_projects)),
|
231 |
+
None,
|
232 |
+
self.ui.COLORS['primary']
|
233 |
+
)
|
234 |
+
|
235 |
+
with col2:
|
236 |
+
projects_in_progress = len([p for p in filtered_projects if p["status"] == "جاري التنفيذ"])
|
237 |
+
self.ui.create_metric_card(
|
238 |
+
"مشاريع جارية",
|
239 |
+
str(projects_in_progress),
|
240 |
+
None,
|
241 |
+
self.ui.COLORS['secondary']
|
242 |
+
)
|
243 |
+
|
244 |
+
with col3:
|
245 |
+
total_budget = sum([p["budget"] for p in filtered_projects])
|
246 |
+
self.ui.create_metric_card(
|
247 |
+
"إجمالي الميزانية",
|
248 |
+
f"{total_budget/1000000:.1f} مليون ريال",
|
249 |
+
None,
|
250 |
+
self.ui.COLORS['accent']
|
251 |
+
)
|
252 |
+
|
253 |
+
with col4:
|
254 |
+
avg_completion = np.mean([p["completion"] for p in filtered_projects])
|
255 |
+
self.ui.create_metric_card(
|
256 |
+
"متوسط نسبة الإنجاز",
|
257 |
+
f"{avg_completion:.1f}%",
|
258 |
+
None,
|
259 |
+
self.ui.COLORS['success']
|
260 |
+
)
|
261 |
+
|
262 |
+
def show_location_details(self):
|
263 |
+
"""عرض تفاصيل المواقع"""
|
264 |
+
st.markdown("### تفاصيل مواقع المشاريع")
|
265 |
+
|
266 |
+
# إنشاء جدول بيانات المشاريع
|
267 |
+
projects_df = pd.DataFrame(self.projects_data)
|
268 |
+
projects_df = projects_df.rename(columns={
|
269 |
+
"id": "رقم المشروع",
|
270 |
+
"name": "اسم المشروع",
|
271 |
+
"location": "الموقع",
|
272 |
+
"status": "الحالة",
|
273 |
+
"budget": "الميزانية (ريال)",
|
274 |
+
"completion": "نسبة الإنجاز (%)",
|
275 |
+
"client": "العميل",
|
276 |
+
"start_date": "تاريخ البدء",
|
277 |
+
"end_date": "تاريخ الانتهاء"
|
278 |
+
})
|
279 |
+
|
280 |
+
# حذف عمود الإحداثيات من العرض
|
281 |
+
projects_df = projects_df.drop(columns=["coordinates"])
|
282 |
+
|
283 |
+
# عرض الجدول
|
284 |
+
st.dataframe(
|
285 |
+
projects_df,
|
286 |
+
use_container_width=True,
|
287 |
+
hide_index=True
|
288 |
+
)
|
289 |
+
|
290 |
+
# إضافة خيار تصدير البيانات
|
291 |
+
col1, col2 = st.columns([1, 5])
|
292 |
+
with col1:
|
293 |
+
self.ui.create_button("تصدير البيانات", "primary")
|
294 |
+
|
295 |
+
# عرض تفاصيل مشروع محدد
|
296 |
+
st.markdown("### تفاصيل مشروع محدد")
|
297 |
+
|
298 |
+
selected_project = st.selectbox(
|
299 |
+
"اختر مشروعاً لعرض التفاصيل",
|
300 |
+
options=[p["name"] for p in self.projects_data]
|
301 |
+
)
|
302 |
+
|
303 |
+
# العثور على المشروع المحدد
|
304 |
+
project = next((p for p in self.projects_data if p["name"] == selected_project), None)
|
305 |
+
|
306 |
+
if project:
|
307 |
+
col1, col2 = st.columns([2, 1])
|
308 |
+
|
309 |
+
with col1:
|
310 |
+
# عرض تفاصيل المشروع
|
311 |
+
st.markdown(f"#### {project['name']}")
|
312 |
+
st.markdown(f"**الموقع:** {project['location']}")
|
313 |
+
st.markdown(f"**الحالة:** {project['status']}")
|
314 |
+
st.markdown(f"**الميزانية:** {project['budget']:,} ريال")
|
315 |
+
st.markdown(f"**نسبة الإنجاز:** {project['completion']}%")
|
316 |
+
st.markdown(f"**العميل:** {project['client']}")
|
317 |
+
st.markdown(f"**تاريخ البدء:** {project['start_date']}")
|
318 |
+
st.markdown(f"**تاريخ الانتهاء:** {project['end_date']}")
|
319 |
+
|
320 |
+
# أزرار الإجراءات
|
321 |
+
col1, col2, col3 = st.columns(3)
|
322 |
+
with col1:
|
323 |
+
self.ui.create_button("تعديل البيانات", "primary")
|
324 |
+
with col2:
|
325 |
+
self.ui.create_button("عرض المستندات", "secondary")
|
326 |
+
with col3:
|
327 |
+
self.ui.create_button("تقرير الموقع", "accent")
|
328 |
+
|
329 |
+
with col2:
|
330 |
+
# عرض خريطة مصغرة للمشروع
|
331 |
+
m = folium.Map(location=project["coordinates"], zoom_start=12)
|
332 |
+
folium.Marker(
|
333 |
+
location=project["coordinates"],
|
334 |
+
tooltip=project["name"],
|
335 |
+
icon=folium.Icon(color="red", icon="info-sign")
|
336 |
+
).add_to(m)
|
337 |
+
folium_static(m, width=300, height=300)
|
338 |
+
|
339 |
+
def add_new_location(self):
|
340 |
+
"""إضافة موقع جديد"""
|
341 |
+
st.markdown("### إضافة موقع مشروع جديد")
|
342 |
+
|
343 |
+
# نموذج إضافة موقع جديد
|
344 |
+
with st.form("new_location_form"):
|
345 |
+
col1, col2 = st.columns(2)
|
346 |
+
|
347 |
+
with col1:
|
348 |
+
project_id = st.text_input("رقم المشروع", value="P00" + str(len(self.projects_data) + 1))
|
349 |
+
project_name = st.text_input("اسم المشروع")
|
350 |
+
location = st.text_input("الموقع")
|
351 |
+
status = st.selectbox(
|
352 |
+
"الحالة",
|
353 |
+
options=["جاري التنفيذ", "قيد الدراسة", "مكتمل"]
|
354 |
+
)
|
355 |
+
budget = st.number_input("الميزانية (ريال)", min_value=0, step=100000)
|
356 |
+
|
357 |
+
with col2:
|
358 |
+
completion = st.slider("نسبة الإنجاز (%)", 0, 100, 0)
|
359 |
+
client = st.text_input("العميل")
|
360 |
+
start_date = st.date_input("تاريخ البدء")
|
361 |
+
end_date = st.date_input("تاريخ الانتهاء")
|
362 |
+
|
363 |
+
st.markdown("### تحديد الموقع على الخريطة")
|
364 |
+
st.markdown("انقر على الخريطة لتحديد موقع المشروع أو أدخل الإحداثيات يدوياً")
|
365 |
+
|
366 |
+
col1, col2 = st.columns(2)
|
367 |
+
|
368 |
+
with col1:
|
369 |
+
latitude = st.number_input("خط العرض", value=24.0, format="%.4f")
|
370 |
+
|
371 |
+
with col2:
|
372 |
+
longitude = st.number_input("خط الطول", value=45.0, format="%.4f")
|
373 |
+
|
374 |
+
# عرض الخريطة لتحديد الموقع
|
375 |
+
m = folium.Map(location=[latitude, longitude], zoom_start=5)
|
376 |
+
folium.Marker(
|
377 |
+
location=[latitude, longitude],
|
378 |
+
tooltip="موقع المشروع الجديد",
|
379 |
+
icon=folium.Icon(color="red", icon="info-sign")
|
380 |
+
).add_to(m)
|
381 |
+
folium_static(m, width=700, height=300)
|
382 |
+
|
383 |
+
# زر الإرسال
|
384 |
+
submit_button = st.form_submit_button("إضافة المشروع")
|
385 |
+
|
386 |
+
if submit_button:
|
387 |
+
# إضافة المشروع الجديد (في تطبيق حقيقي، سيتم حفظ البيانات في قاعدة البيانات)
|
388 |
+
st.success("تم إضافة المشروع بنجاح!")
|
389 |
+
|
390 |
+
# إعادة تعيين النموذج
|
391 |
+
st.experimental_rerun()
|
392 |
+
|
393 |
+
def analyze_regions(self):
|
394 |
+
"""تحليل المناطق"""
|
395 |
+
st.markdown("### تحليل المناطق")
|
396 |
+
|
397 |
+
# إنشاء بيانات المناطق (نموذجية)
|
398 |
+
regions_data = {
|
399 |
+
"المنطقة": ["الرياض", "مكة المكرمة", "المدينة المنورة", "القصيم", "المنطقة الشرقية", "عسير", "تبوك", "حائل", "الحدود الشمالية", "جازان", "نجران", "الباحة", "الجوف"],
|
400 |
+
"عدد المشاريع": [15, 12, 8, 5, 18, 7, 4, 3, 2, 6, 3, 2, 3],
|
401 |
+
"إجمالي الميزانية (مليون ريال)": [120, 95, 45, 30, 150, 40, 25, 18, 12, 35, 20, 15, 22],
|
402 |
+
"متوسط مدة المشروع (شهر)": [18, 16, 14, 12, 20, 15, 12, 10, 9, 14, 12, 10, 11]
|
403 |
+
}
|
404 |
+
|
405 |
+
regions_df = pd.DataFrame(regions_data)
|
406 |
+
|
407 |
+
# عرض خريطة حرارية للمناطق
|
408 |
+
st.markdown("#### توزيع المشاريع حسب المناطق")
|
409 |
+
|
410 |
+
# في تطبيق حقيقي، يمكن استخدام خريطة حرارية حقيقية للمملكة
|
411 |
+
st.image("https://via.placeholder.com/800x400?text=خريطة+حرارية+للمشاريع+حسب+المناطق", use_column_width=True)
|
412 |
+
|
413 |
+
# عرض إحصائيات المناطق
|
414 |
+
st.markdown("#### إحصائيات المناطق")
|
415 |
+
|
416 |
+
# عرض الجدول
|
417 |
+
st.dataframe(
|
418 |
+
regions_df,
|
419 |
+
use_container_width=True,
|
420 |
+
hide_index=True
|
421 |
+
)
|
422 |
+
|
423 |
+
# عرض رسوم بيانية للمقارنة
|
424 |
+
st.markdown("#### مقارنة المناطق")
|
425 |
+
|
426 |
+
chart_type = st.radio(
|
427 |
+
"نوع الرسم البياني",
|
428 |
+
options=["عدد المشاريع", "إجمالي الميزانية", "متوسط مدة المشروع"],
|
429 |
+
horizontal=True
|
430 |
+
)
|
431 |
+
|
432 |
+
if chart_type == "عدد المشاريع":
|
433 |
+
chart_data = regions_df[["المنطقة", "عدد المشاريع"]].sort_values(by="عدد المشاريع", ascending=False)
|
434 |
+
st.bar_chart(chart_data.set_index("المنطقة"))
|
435 |
+
elif chart_type == "إجمالي الميزانية":
|
436 |
+
chart_data = regions_df[["المنطقة", "إجمالي الميزانية (مليون ريال)"]].sort_values(by="إجمالي الميزانية (��ليون ريال)", ascending=False)
|
437 |
+
st.bar_chart(chart_data.set_index("المنطقة"))
|
438 |
+
else:
|
439 |
+
chart_data = regions_df[["المنطقة", "متوسط مدة المشروع (شهر)"]].sort_values(by="متوسط مدة المشروع (شهر)", ascending=False)
|
440 |
+
st.bar_chart(chart_data.set_index("المنطقة"))
|
441 |
+
|
442 |
+
# تحليل الكثافة
|
443 |
+
st.markdown("#### تحليل كثافة المشاريع")
|
444 |
+
st.markdown("""
|
445 |
+
يوضح هذا التحليل توزيع المشاريع حسب المناطق الجغرافية، مما يساعد في:
|
446 |
+
- تحديد المناطق ذات النشاط العالي
|
447 |
+
- تحديد فرص النمو في المناطق الأقل نشاطاً
|
448 |
+
- تخطيط الموارد بناءً على التوزيع الجغرافي
|
449 |
+
""")
|
450 |
+
|
451 |
+
# في تطبيق حقيقي، يمكن إضافة تحليلات أكثر تفصيلاً
|
452 |
+
|
453 |
+
# تشغيل التطبيق
|
454 |
+
if __name__ == "__main__":
|
455 |
+
maps_app = MapsApp()
|
456 |
+
maps_app.run()
|
modules/notifications/notifications_app.py
CHANGED
@@ -1,707 +1,707 @@
|
|
1 |
-
"""
|
2 |
-
وحدة الإشعارات الذكية - نظام تحليل المناقصات
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
import pandas as pd
|
7 |
-
import datetime
|
8 |
-
import json
|
9 |
-
import os
|
10 |
-
import sys
|
11 |
-
from pathlib import Path
|
12 |
-
|
13 |
-
# إضافة مسار المشروع للنظام
|
14 |
-
sys.path.append(str(Path(__file__).parent.parent))
|
15 |
-
|
16 |
-
# استيراد محسن واجهة المستخدم
|
17 |
-
from styling.enhanced_ui import UIEnhancer
|
18 |
-
|
19 |
-
class NotificationsApp:
|
20 |
-
"""تطبيق الإشعارات الذكية"""
|
21 |
-
|
22 |
-
def __init__(self):
|
23 |
-
"""تهيئة تطبيق الإشعارات الذكية"""
|
24 |
-
self.ui = UIEnhancer(page_title="الإشعارات الذكية - نظام تحليل المناقصات", page_icon="🔔")
|
25 |
-
|
26 |
-
# تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
|
27 |
-
if 'theme' not in st.session_state:
|
28 |
-
st.session_state.theme = 'light'
|
29 |
-
|
30 |
-
self.ui.apply_theme_colors()
|
31 |
-
|
32 |
-
# بيانات الإشعارات (نموذجية)
|
33 |
-
self.notifications_data = [
|
34 |
-
{
|
35 |
-
"id": "N001",
|
36 |
-
"title": "موعد تسليم مناقصة",
|
37 |
-
"message": "موعد تسليم مناقصة T-2025-001 (إنشاء مبنى إداري) بعد 5 أيام",
|
38 |
-
"type": "deadline",
|
39 |
-
"priority": "high",
|
40 |
-
"related_entity": "T-2025-001",
|
41 |
-
"created_at": "2025-03-25T10:30:00",
|
42 |
-
"is_read": False
|
43 |
-
},
|
44 |
-
{
|
45 |
-
"id": "N002",
|
46 |
-
"title": "ترسية مناقصة",
|
47 |
-
"message": "تم ترسية مناقصة T-2025-003 (توريد معدات) بنجاح",
|
48 |
-
"type": "award",
|
49 |
-
"priority": "medium",
|
50 |
-
"related_entity": "T-2025-003",
|
51 |
-
"created_at": "2025-03-28T14:15:00",
|
52 |
-
"is_read": True
|
53 |
-
},
|
54 |
-
{
|
55 |
-
"id": "N003",
|
56 |
-
"title": "تحديث مستندات",
|
57 |
-
"message": "تم تحديث مستندات مناقصة T-2025-002 (صيانة طرق)",
|
58 |
-
"type": "document",
|
59 |
-
"priority": "medium",
|
60 |
-
"related_entity": "T-2025-002",
|
61 |
-
"created_at": "2025-03-29T09:45:00",
|
62 |
-
"is_read": False
|
63 |
-
},
|
64 |
-
{
|
65 |
-
"id": "N004",
|
66 |
-
"title": "تغيير في المواصفات",
|
67 |
-
"message": "تم تغيير المواصفات الفنية لمناقصة T-2025-001 (إنشاء مبنى إداري)",
|
68 |
-
"type": "change",
|
69 |
-
"priority": "high",
|
70 |
-
"related_entity": "T-2025-001",
|
71 |
-
"created_at": "2025-03-27T11:20:00",
|
72 |
-
"is_read": False
|
73 |
-
},
|
74 |
-
{
|
75 |
-
"id": "N005",
|
76 |
-
"title": "تأخير في المشروع",
|
77 |
-
"message": "تأخير في تنفيذ مشروع P002 (تطوير طريق الملك فهد - جدة)",
|
78 |
-
"type": "delay",
|
79 |
-
"priority": "high",
|
80 |
-
"related_entity": "P002",
|
81 |
-
"created_at": "2025-03-26T16:10:00",
|
82 |
-
"is_read": True
|
83 |
-
},
|
84 |
-
{
|
85 |
-
"id": "N006",
|
86 |
-
"title": "اكتمال
|
87 |
-
"message": "اكتمال مرحلة الأساسات في مشروع P001 (إنشاء مبنى إداري - الرياض)",
|
88 |
-
"type": "milestone",
|
89 |
-
"priority": "low",
|
90 |
-
"related_entity": "P001",
|
91 |
-
"created_at": "2025-03-24T13:30:00",
|
92 |
-
"is_read": True
|
93 |
-
},
|
94 |
-
{
|
95 |
-
"id": "N007",
|
96 |
-
"title": "طلب معلومات إضافية",
|
97 |
-
"message": "طلب معلومات إضافية لمناقصة T-2025-004 (تجهيز مختبرات)",
|
98 |
-
"type": "request",
|
99 |
-
"priority": "medium",
|
100 |
-
"related_entity": "T-2025-004",
|
101 |
-
"created_at": "2025-03-30T08:15:00",
|
102 |
-
"is_read": False
|
103 |
-
},
|
104 |
-
{
|
105 |
-
"id": "N008",
|
106 |
-
"title": "تحديث أسعار المواد",
|
107 |
-
"message": "تم تحديث أسعار مواد البناء في قاعدة البيانات",
|
108 |
-
"type": "update",
|
109 |
-
"priority": "low",
|
110 |
-
"related_entity": "DB-MATERIALS",
|
111 |
-
"created_at": "2025-03-29T15:40:00",
|
112 |
-
"is_read": False
|
113 |
-
},
|
114 |
-
{
|
115 |
-
"id": "N009",
|
116 |
-
"title": "اجتماع فريق العمل",
|
117 |
-
"message": "اجتماع فريق العمل لمناقشة مناقصة T-2025-001 غداً الساعة 10:00 صباحاً",
|
118 |
-
"type": "meeting",
|
119 |
-
"priority": "medium",
|
120 |
-
"related_entity": "T-2025-001",
|
121 |
-
"created_at": "2025-03-28T16:20:00",
|
122 |
-
"is_read": True
|
123 |
-
},
|
124 |
-
{
|
125 |
-
"id": "N010",
|
126 |
-
"title": "تغيير في الميزانية",
|
127 |
-
"message": "تم تغيير الميزانية المخصصة لمشروع P004 (بناء مدرسة - أبها)",
|
128 |
-
"type": "budget",
|
129 |
-
"priority": "high",
|
130 |
-
"related_entity": "P004",
|
131 |
-
"created_at": "2025-03-25T14:50:00",
|
132 |
-
"is_read": False
|
133 |
-
}
|
134 |
-
]
|
135 |
-
|
136 |
-
# إعدادات الإشعارات (نموذجية)
|
137 |
-
self.notification_settings = {
|
138 |
-
"deadline": True,
|
139 |
-
"award": True,
|
140 |
-
"document": True,
|
141 |
-
"change": True,
|
142 |
-
"delay": True,
|
143 |
-
"milestone": True,
|
144 |
-
"request": True,
|
145 |
-
"update": True,
|
146 |
-
"meeting": True,
|
147 |
-
"budget": True,
|
148 |
-
"email_notifications": True,
|
149 |
-
"sms_notifications": False,
|
150 |
-
"push_notifications": True,
|
151 |
-
"notification_frequency": "realtime"
|
152 |
-
}
|
153 |
-
|
154 |
-
def run(self):
|
155 |
-
"""تشغيل تطبيق الإشعارات الذكية"""
|
156 |
-
# إضافة زر تبديل السمة في أعلى الصفحة
|
157 |
-
col1, col2, col3 = st.columns([1, 8, 1])
|
158 |
-
with col3:
|
159 |
-
if st.button("🌓 تبديل السمة"):
|
160 |
-
# تبديل السمة
|
161 |
-
if st.session_state.theme == "light":
|
162 |
-
st.session_state.theme = "dark"
|
163 |
-
else:
|
164 |
-
st.session_state.theme = "light"
|
165 |
-
|
166 |
-
# تطبيق السمة الجديدة
|
167 |
-
self.ui.theme_mode = st.session_state.theme
|
168 |
-
self.ui.apply_theme_colors()
|
169 |
-
st.rerun()
|
170 |
-
|
171 |
-
# إنشاء ترويسة الصفحة
|
172 |
-
self.ui.create_header("الإشعارات الذكية", "إدارة ومتابعة الإشعارات والتنبيهات")
|
173 |
-
|
174 |
-
# إنشاء علامات تبويب للوظائف المختلفة
|
175 |
-
tabs = st.tabs(["الإشعارات الحالية", "إعدادات الإشعارات", "إنشاء إشعار", "سجل الإشعارات"])
|
176 |
-
|
177 |
-
# علامة تبويب الإشعارات الحالية
|
178 |
-
with tabs[0]:
|
179 |
-
self.show_current_notifications()
|
180 |
-
|
181 |
-
# علامة تبويب إعدادات الإشعارات
|
182 |
-
with tabs[1]:
|
183 |
-
self.show_notification_settings()
|
184 |
-
|
185 |
-
# علامة تبويب إنشاء إشعار
|
186 |
-
with tabs[2]:
|
187 |
-
self.create_notification()
|
188 |
-
|
189 |
-
# علامة تبويب سجل الإشعارات
|
190 |
-
with tabs[3]:
|
191 |
-
self.show_notification_history()
|
192 |
-
|
193 |
-
def show_current_notifications(self):
|
194 |
-
"""عرض الإشعارات الحالية"""
|
195 |
-
st.markdown("### الإشعارات الحالية")
|
196 |
-
|
197 |
-
# إنشاء فلاتر للإشعارات
|
198 |
-
col1, col2, col3 = st.columns(3)
|
199 |
-
|
200 |
-
with col1:
|
201 |
-
type_filter = st.multiselect(
|
202 |
-
"نوع الإشعار",
|
203 |
-
options=["الكل", "موعد نهائي", "
|
204 |
-
default=["الكل"]
|
205 |
-
)
|
206 |
-
|
207 |
-
with col2:
|
208 |
-
priority_filter = st.multiselect(
|
209 |
-
"الأولوية",
|
210 |
-
options=["الكل", "عالية", "متوسطة", "منخفضة"],
|
211 |
-
default=["الكل"]
|
212 |
-
)
|
213 |
-
|
214 |
-
with col3:
|
215 |
-
read_filter = st.radio(
|
216 |
-
"الحالة",
|
217 |
-
options=["الكل", "غير مقروءة", "مقروءة"],
|
218 |
-
horizontal=True
|
219 |
-
)
|
220 |
-
|
221 |
-
# تطبيق الفلاتر
|
222 |
-
filtered_notifications = self.notifications_data
|
223 |
-
|
224 |
-
# تحويل أنواع الإشعارات من الإنجليزية إلى العربية للفلترة
|
225 |
-
type_mapping = {
|
226 |
-
"موعد نهائي": "deadline",
|
227 |
-
"ترسية": "award",
|
228 |
-
"مستند": "document",
|
229 |
-
"تغيير": "change",
|
230 |
-
"تأخير": "delay",
|
231 |
-
"مرحلة": "milestone",
|
232 |
-
"طلب": "request",
|
233 |
-
"تحديث": "update",
|
234 |
-
"اجتماع": "meeting",
|
235 |
-
"ميزانية": "budget"
|
236 |
-
}
|
237 |
-
|
238 |
-
# تحويل الأولويات من العربية إلى الإنجليزية للفلترة
|
239 |
-
priority_mapping = {
|
240 |
-
"عالية": "high",
|
241 |
-
"متوسطة": "medium",
|
242 |
-
"منخفضة": "low"
|
243 |
-
}
|
244 |
-
|
245 |
-
if "الكل" not in type_filter and type_filter:
|
246 |
-
filtered_types = [type_mapping[t] for t in type_filter if t in type_mapping]
|
247 |
-
filtered_notifications = [n for n in filtered_notifications if n["type"] in filtered_types]
|
248 |
-
|
249 |
-
if "الكل" not in priority_filter and priority_filter:
|
250 |
-
filtered_priorities = [priority_mapping[p] for p in priority_filter if p in priority_mapping]
|
251 |
-
filtered_notifications = [n for n in filtered_notifications if n["priority"] in filtered_priorities]
|
252 |
-
|
253 |
-
if read_filter == "غير مقروءة":
|
254 |
-
filtered_notifications = [n for n in filtered_notifications if not n["is_read"]]
|
255 |
-
elif read_filter == "مقروءة":
|
256 |
-
filtered_notifications = [n for n in filtered_notifications if n["is_read"]]
|
257 |
-
|
258 |
-
# عرض عدد الإشعارات غير المقروءة
|
259 |
-
unread_count = len([n for n in filtered_notifications if not n["is_read"]])
|
260 |
-
|
261 |
-
st.markdown(f"**عدد الإشعارات غير المقروءة:** {unread_count}")
|
262 |
-
|
263 |
-
# زر تحديث وتعليم الكل كمقروء
|
264 |
-
col1, col2 = st.columns([1, 1])
|
265 |
-
with col1:
|
266 |
-
if st.button("تحديث الإشعارات", use_container_width=True):
|
267 |
-
st.success("تم تحديث الإشعارات بنجاح")
|
268 |
-
|
269 |
-
with col2:
|
270 |
-
if st.button("تعليم الكل كمقروء", use_container_width=True):
|
271 |
-
st.success("تم تعليم جميع الإشعارات كمقروءة")
|
272 |
-
|
273 |
-
# عرض الإشعارات
|
274 |
-
if not filtered_notifications:
|
275 |
-
st.info("لا توجد إشعارات تطابق الفلاتر المحددة")
|
276 |
-
else:
|
277 |
-
for notification in filtered_notifications:
|
278 |
-
self.display_notification(notification)
|
279 |
-
|
280 |
-
def display_notification(self, notification):
|
281 |
-
"""عرض إشعار واحد"""
|
282 |
-
# تحديد لون الإشعار بناءً على الأولوية
|
283 |
-
if notification["priority"] == "high":
|
284 |
-
color = self.ui.COLORS['danger']
|
285 |
-
priority_text = "عالية"
|
286 |
-
elif notification["priority"] == "medium":
|
287 |
-
color = self.ui.COLORS['warning']
|
288 |
-
priority_text = "متوسطة"
|
289 |
-
else:
|
290 |
-
color = self.ui.COLORS['secondary']
|
291 |
-
priority_text = "منخفضة"
|
292 |
-
|
293 |
-
# تحويل نوع الإشعار إلى العربية
|
294 |
-
type_mapping = {
|
295 |
-
"deadline": "موعد نهائي",
|
296 |
-
"award": "ترسية",
|
297 |
-
"document": "مستند",
|
298 |
-
"change": "تغيير",
|
299 |
-
"delay": "تأخير",
|
300 |
-
"milestone": "مرحلة",
|
301 |
-
"request": "طلب",
|
302 |
-
"update": "تحديث",
|
303 |
-
"meeting": "اجتماع",
|
304 |
-
"budget": "ميزانية"
|
305 |
-
}
|
306 |
-
|
307 |
-
notification_type = type_mapping.get(notification["type"], notification["type"])
|
308 |
-
|
309 |
-
# تحويل التاريخ إلى تنسيق مناسب
|
310 |
-
created_at = datetime.datetime.fromisoformat(notification["created_at"])
|
311 |
-
formatted_date = created_at.strftime("%Y-%m-%d %H:%M")
|
312 |
-
|
313 |
-
# تحديد أيقونة الإشعار
|
314 |
-
icon_mapping = {
|
315 |
-
"deadline": "⏰",
|
316 |
-
"award": "🏆",
|
317 |
-
"document": "📄",
|
318 |
-
"change": "🔄",
|
319 |
-
"delay": "⚠️",
|
320 |
-
"milestone": "🏁",
|
321 |
-
"request": "❓",
|
322 |
-
"update": "🔄",
|
323 |
-
"meeting": "👥",
|
324 |
-
"budget": "💰"
|
325 |
-
}
|
326 |
-
|
327 |
-
icon = icon_mapping.get(notification["type"], "📌")
|
328 |
-
|
329 |
-
# إنشاء بطاقة الإشعار
|
330 |
-
st.markdown(
|
331 |
-
f"""
|
332 |
-
<div style="border-left: 5px solid {color}; padding: 10px; margin-bottom: 10px; background-color: {'#f8f9fa' if st.session_state.theme == 'light' else '#2b2b2b'}; border-radius: 5px; {'opacity: 0.7;' if notification['is_read'] else ''}">
|
333 |
-
<div style="display: flex; justify-content: space-between; align-items: center;">
|
334 |
-
<div>
|
335 |
-
<h4 style="margin: 0;">{icon} {notification['title']}</h4>
|
336 |
-
<p style="margin: 5px 0;">{notification['message']}</p>
|
337 |
-
<div style="display: flex; gap: 10px; font-size: 0.8em; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">
|
338 |
-
<span>النوع: {notification_type}</span>
|
339 |
-
<span>الأولوية: {priority_text}</span>
|
340 |
-
<span>التاريخ: {formatted_date}</span>
|
341 |
-
</div>
|
342 |
-
</div>
|
343 |
-
<div>
|
344 |
-
<button style="background: none; border: none; cursor: pointer; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">✓</button>
|
345 |
-
<button style="background: none; border: none; cursor: pointer; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">🗑️</button>
|
346 |
-
</div>
|
347 |
-
</div>
|
348 |
-
</div>
|
349 |
-
""",
|
350 |
-
unsafe_allow_html=True
|
351 |
-
)
|
352 |
-
|
353 |
-
def show_notification_settings(self):
|
354 |
-
"""عرض إعدادات الإشعارات"""
|
355 |
-
st.markdown("### إعدادات الإشعارات")
|
356 |
-
|
357 |
-
# إنشاء نموذج الإعدادات
|
358 |
-
with st.form("notification_settings_form"):
|
359 |
-
st.markdown("#### أنواع الإشعارات")
|
360 |
-
|
361 |
-
col1, col2 = st.columns(2)
|
362 |
-
|
363 |
-
with col1:
|
364 |
-
deadline = st.checkbox("المواعيد النهائية", value=self.notification_settings["deadline"])
|
365 |
-
award = st.checkbox("ترسية المناقصات", value=self.notification_settings["award"])
|
366 |
-
document = st.checkbox("تحديثات المستندات", value=self.notification_settings["document"])
|
367 |
-
change = st.checkbox("التغييرات في المواصفات", value=self.notification_settings["change"])
|
368 |
-
delay = st.checkbox("التأخيرات في المشاريع", value=self.notification_settings["delay"])
|
369 |
-
|
370 |
-
with col2:
|
371 |
-
milestone = st.checkbox("اكتمال المراحل", value=self.notification_settings["milestone"])
|
372 |
-
request = st.checkbox("طلبات المعلومات", value=self.notification_settings["request"])
|
373 |
-
update = st.checkbox("تحديثات النظام", value=self.notification_settings["update"])
|
374 |
-
meeting = st.checkbox("الاجتماعات", value=self.notification_settings["meeting"])
|
375 |
-
budget = st.checkbox("تغييرات الميزانية", value=self.notification_settings["budget"])
|
376 |
-
|
377 |
-
st.markdown("#### طرق الإشعار")
|
378 |
-
|
379 |
-
col1, col2, col3 = st.columns(3)
|
380 |
-
|
381 |
-
with col1:
|
382 |
-
email_notifications = st.checkbox("البريد الإلكتروني", value=self.notification_settings["email_notifications"])
|
383 |
-
|
384 |
-
with col2:
|
385 |
-
sms_notifications = st.checkbox("الرسائل النصية", value=self.notification_settings["sms_notifications"])
|
386 |
-
|
387 |
-
with col3:
|
388 |
-
push_notifications = st.checkbox("إشعارات الويب", value=self.notification_settings["push_notifications"])
|
389 |
-
|
390 |
-
st.markdown("#### تكرار الإشعارات")
|
391 |
-
|
392 |
-
notification_frequency = st.radio(
|
393 |
-
"تكرار الإشعارات",
|
394 |
-
options=["في الوقت الحقيقي", "مرة واحدة يومياً", "مرة واحدة أسبوعياً"],
|
395 |
-
index=0 if self.notification_settings["notification_frequency"] == "realtime" else 1 if self.notification_settings["notification_frequency"] == "daily" else 2,
|
396 |
-
horizontal=True
|
397 |
-
)
|
398 |
-
|
399 |
-
# زر حفظ الإعدادات
|
400 |
-
submit_button = st.form_submit_button("حفظ الإعدادات")
|
401 |
-
|
402 |
-
if submit_button:
|
403 |
-
# تحديث الإعدادات (في تطبيق حقيقي، سيتم حفظ الإعدادات في قاعدة البيانات)
|
404 |
-
self.notification_settings.update({
|
405 |
-
"deadline": deadline,
|
406 |
-
"award": award,
|
407 |
-
"document": document,
|
408 |
-
"change": change,
|
409 |
-
"delay": delay,
|
410 |
-
"milestone": milestone,
|
411 |
-
"request": request,
|
412 |
-
"update": update,
|
413 |
-
"meeting": meeting,
|
414 |
-
"budget": budget,
|
415 |
-
"email_notifications": email_notifications,
|
416 |
-
"sms_notifications": sms_notifications,
|
417 |
-
"push_notifications": push_notifications,
|
418 |
-
"notification_frequency": "realtime" if notification_frequency == "في الوقت الحقيقي" else "daily" if notification_frequency == "مرة واحدة يومياً" else "weekly"
|
419 |
-
})
|
420 |
-
|
421 |
-
st.success("تم حفظ الإعدادات بنجاح")
|
422 |
-
|
423 |
-
# إعدادات متقدمة
|
424 |
-
st.markdown("### إعدادات متقدمة")
|
425 |
-
|
426 |
-
with st.expander("إعدادات متقدمة"):
|
427 |
-
st.markdown("#### جدولة الإشعارات")
|
428 |
-
|
429 |
-
col1, col2 = st.columns(2)
|
430 |
-
|
431 |
-
with col1:
|
432 |
-
st.time_input("وقت الإشعارات اليومية", datetime.time(9, 0))
|
433 |
-
|
434 |
-
with col2:
|
435 |
-
st.selectbox(
|
436 |
-
"يوم الإشعارات الأسبوعية",
|
437 |
-
options=["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"],
|
438 |
-
index=0
|
439 |
-
)
|
440 |
-
|
441 |
-
st.markdown("#### فلترة الإشعارات")
|
442 |
-
|
443 |
-
min_priority = st.select_slider(
|
444 |
-
"الحد الأدنى للأولوية",
|
445 |
-
options=["منخفضة", "متوسطة", "عالية"],
|
446 |
-
value="منخفضة"
|
447 |
-
)
|
448 |
-
|
449 |
-
st.markdown("#### حفظ الإشعارات")
|
450 |
-
|
451 |
-
retention_period = st.slider(
|
452 |
-
"فترة الاحتفاظ بالإشعارات (بالأيام)",
|
453 |
-
min_value=7,
|
454 |
-
max_value=365,
|
455 |
-
value=90,
|
456 |
-
step=1
|
457 |
-
)
|
458 |
-
|
459 |
-
if st.button("حفظ الإعدادات المتقدمة"):
|
460 |
-
st.success("تم حفظ الإعدادات المتقدمة بنجاح")
|
461 |
-
|
462 |
-
def create_notification(self):
|
463 |
-
"""إنشاء إشعار جديد"""
|
464 |
-
st.markdown("### إنشاء إشعار جديد")
|
465 |
-
|
466 |
-
# إنشاء نموذج إشعار جديد
|
467 |
-
with st.form("new_notification_form"):
|
468 |
-
title = st.text_input("عنوان الإشعار")
|
469 |
-
message = st.text_area("نص الإشعار")
|
470 |
-
|
471 |
-
col1, col2 = st.columns(2)
|
472 |
-
|
473 |
-
with col1:
|
474 |
-
notification_type = st.selectbox(
|
475 |
-
"نوع الإشعار",
|
476 |
-
options=["موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"]
|
477 |
-
)
|
478 |
-
|
479 |
-
# تحويل نوع الإشعار إلى الإنجليزية
|
480 |
-
type_mapping = {
|
481 |
-
"موعد نهائي": "deadline",
|
482 |
-
"ترسية": "award",
|
483 |
-
"مستند": "document",
|
484 |
-
"تغيير": "change",
|
485 |
-
"تأخير": "delay",
|
486 |
-
"مرحلة": "milestone",
|
487 |
-
"طلب": "request",
|
488 |
-
"تحديث": "update",
|
489 |
-
"اجتماع": "meeting",
|
490 |
-
"ميزانية": "budget"
|
491 |
-
}
|
492 |
-
|
493 |
-
notification_type_en = type_mapping.get(notification_type, "deadline")
|
494 |
-
|
495 |
-
priority = st.selectbox(
|
496 |
-
"الأولوية",
|
497 |
-
options=["عالية", "متوسطة", "منخفضة"]
|
498 |
-
)
|
499 |
-
|
500 |
-
# تحويل الأولوية إلى الإنجليزية
|
501 |
-
priority_mapping = {
|
502 |
-
"عالية": "high",
|
503 |
-
"متوسطة": "medium",
|
504 |
-
"منخفضة": "low"
|
505 |
-
}
|
506 |
-
|
507 |
-
priority_en = priority_mapping.get(priority, "medium")
|
508 |
-
|
509 |
-
with col2:
|
510 |
-
related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة، رقم المشروع)")
|
511 |
-
|
512 |
-
notification_date = st.date_input(
|
513 |
-
"تاريخ الإشعار",
|
514 |
-
value=datetime.datetime.now().date()
|
515 |
-
)
|
516 |
-
|
517 |
-
notification_time = st.time_input(
|
518 |
-
"وقت الإشعار",
|
519 |
-
value=datetime.datetime.now().time()
|
520 |
-
)
|
521 |
-
|
522 |
-
# زر إنشاء الإشعار
|
523 |
-
submit_button = st.form_submit_button("إنشاء الإشعار")
|
524 |
-
|
525 |
-
if submit_button and title and message:
|
526 |
-
# إنشاء إشعار جديد (في تطبيق حقيقي، سيتم حفظ الإشعار في قاعدة البيانات)
|
527 |
-
new_id = f"N{len(self.notifications_data) + 1:03d}"
|
528 |
-
|
529 |
-
# تحويل التاريخ والوقت إلى تنسيق ISO
|
530 |
-
notification_datetime = datetime.datetime.combine(notification_date, notification_time)
|
531 |
-
notification_datetime_iso = notification_datetime.isoformat()
|
532 |
-
|
533 |
-
# إضافة الإشعار الجديد إلى قائمة الإشعارات
|
534 |
-
self.notifications_data.append({
|
535 |
-
"id": new_id,
|
536 |
-
"title": title,
|
537 |
-
"message": message,
|
538 |
-
"type": notification_type_en,
|
539 |
-
"priority": priority_en,
|
540 |
-
"related_entity": related_entity,
|
541 |
-
"created_at": notification_datetime_iso,
|
542 |
-
"is_read": False
|
543 |
-
})
|
544 |
-
|
545 |
-
st.success("تم إنشاء الإشعار بنجاح")
|
546 |
-
|
547 |
-
# عرض الإشعار الجديد
|
548 |
-
st.markdown("### الإشعار الجديد")
|
549 |
-
self.display_notification(self.notifications_data[-1])
|
550 |
-
|
551 |
-
# إنشاء إشعارات متعددة
|
552 |
-
st.markdown("### إنشاء إشعارات متعددة")
|
553 |
-
|
554 |
-
with st.expander("إنشاء إشعارات متعددة"):
|
555 |
-
st.markdown("#### تحميل ملف إشعارات")
|
556 |
-
|
557 |
-
uploaded_file = st.file_uploader("قم بتحميل ملف إشعارات (CSV, JSON)", type=["csv", "json"])
|
558 |
-
|
559 |
-
if uploaded_file is not None:
|
560 |
-
if st.button("استيراد الإشعارات"):
|
561 |
-
st.success("تم استيراد الإشعارات بنجاح")
|
562 |
-
|
563 |
-
st.markdown("#### إنشاء إشعارات من قالب")
|
564 |
-
|
565 |
-
template_type = st.selectbox(
|
566 |
-
"نوع القالب",
|
567 |
-
options=["إشعارات المواعيد النهائية", "إشعارات الاجتماعات", "إشعارات التحديثات"]
|
568 |
-
)
|
569 |
-
|
570 |
-
if st.button("إنشاء إشعارات من القالب"):
|
571 |
-
st.success("تم إنشاء الإشعارات من القالب بنجاح")
|
572 |
-
|
573 |
-
def show_notification_history(self):
|
574 |
-
"""عرض سجل الإشعارات"""
|
575 |
-
st.markdown("### سجل الإشعارات")
|
576 |
-
|
577 |
-
# إنشاء فلاتر للسجل
|
578 |
-
col1, col2 = st.columns(2)
|
579 |
-
|
580 |
-
with col1:
|
581 |
-
date_range = st.date_input(
|
582 |
-
"نطاق التاريخ",
|
583 |
-
value=(
|
584 |
-
datetime.datetime.now().date() - datetime.timedelta(days=30),
|
585 |
-
datetime.datetime.now().date()
|
586 |
-
)
|
587 |
-
)
|
588 |
-
|
589 |
-
with col2:
|
590 |
-
entity_filter = st.text_input("الكيان المرتبط")
|
591 |
-
|
592 |
-
# تحويل البيانات إلى DataFrame
|
593 |
-
notifications_df = pd.DataFrame(self.notifications_data)
|
594 |
-
|
595 |
-
# تحويل عمود التاريخ إلى نوع datetime
|
596 |
-
notifications_df["created_at"] = pd.to_datetime(notifications_df["created_at"])
|
597 |
-
|
598 |
-
# تطبيق فلتر التاريخ
|
599 |
-
if len(date_range) == 2:
|
600 |
-
start_date, end_date = date_range
|
601 |
-
start_date = pd.to_datetime(start_date)
|
602 |
-
end_date = pd.to_datetime(end_date) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
|
603 |
-
|
604 |
-
notifications_df = notifications_df[
|
605 |
-
(notifications_df["created_at"] >= start_date) &
|
606 |
-
(notifications_df["created_at"] <= end_date)
|
607 |
-
]
|
608 |
-
|
609 |
-
# تطبيق فلتر الكيان المرتبط
|
610 |
-
if entity_filter:
|
611 |
-
notifications_df = notifications_df[
|
612 |
-
notifications_df["related_entity"].str.contains(entity_filter, case=False)
|
613 |
-
]
|
614 |
-
|
615 |
-
# تحويل أنواع الإشعارات من الإنجليزية إلى العربية
|
616 |
-
type_mapping = {
|
617 |
-
"deadline": "موعد نهائي",
|
618 |
-
"award": "ترسية",
|
619 |
-
"document": "مستند",
|
620 |
-
"change": "
|
621 |
-
"delay": "تأخير",
|
622 |
-
"milestone": "مرحلة",
|
623 |
-
"request": "طلب",
|
624 |
-
"update": "تحديث",
|
625 |
-
"meeting": "اجتماع",
|
626 |
-
"budget": "ميزانية"
|
627 |
-
}
|
628 |
-
|
629 |
-
notifications_df["type_ar"] = notifications_df["type"].map(type_mapping)
|
630 |
-
|
631 |
-
# تحويل الأولويات من الإنجليزية إلى العربية
|
632 |
-
priority_mapping = {
|
633 |
-
"high": "عالية",
|
634 |
-
"medium": "متوسطة",
|
635 |
-
"low": "منخفضة"
|
636 |
-
}
|
637 |
-
|
638 |
-
notifications_df["priority_ar"] = notifications_df["priority"].map(priority_mapping)
|
639 |
-
|
640 |
-
# تحويل حالة القراءة إلى نص
|
641 |
-
notifications_df["is_read_text"] = notifications_df["is_read"].map({True: "مقروءة", False: "غير مقروءة"})
|
642 |
-
|
643 |
-
# تنسيق عمود التاريخ
|
644 |
-
notifications_df["created_at_formatted"] = notifications_df["created_at"].dt.strftime("%Y-%m-%d %H:%M")
|
645 |
-
|
646 |
-
# إنشاء DataFrame للعرض
|
647 |
-
display_df = notifications_df[[
|
648 |
-
"id", "title", "type_ar", "priority_ar", "related_entity",
|
649 |
-
"created_at_formatted", "is_read_text"
|
650 |
-
]].rename(columns={
|
651 |
-
"id": "الرقم",
|
652 |
-
"title": "العنوان",
|
653 |
-
"type_ar": "النوع",
|
654 |
-
"priority_ar": "الأولوية",
|
655 |
-
"related_entity": "الكيان المرتبط",
|
656 |
-
"created_at_formatted": "التاريخ",
|
657 |
-
"is_read_text": "الحالة"
|
658 |
-
})
|
659 |
-
|
660 |
-
# عرض الجدول
|
661 |
-
st.dataframe(display_df, use_container_width=True, hide_index=True)
|
662 |
-
|
663 |
-
# إحصائيات الإشعارات
|
664 |
-
st.markdown("### إحصائيات الإشعارات")
|
665 |
-
|
666 |
-
col1, col2, col3 = st.columns(3)
|
667 |
-
|
668 |
-
with col1:
|
669 |
-
total_count = len(notifications_df)
|
670 |
-
st.metric("إجمالي الإشعارات", total_count)
|
671 |
-
|
672 |
-
with col2:
|
673 |
-
read_count = len(notifications_df[notifications_df["is_read"] == True])
|
674 |
-
st.metric("الإشعارات المقروءة", read_count)
|
675 |
-
|
676 |
-
with col3:
|
677 |
-
unread_count = len(notifications_df[notifications_df["is_read"] == False])
|
678 |
-
st.metric("الإشعارات غير المقروءة", unread_count)
|
679 |
-
|
680 |
-
# رسم بياني لتوزيع الإشعارات حسب النوع
|
681 |
-
st.markdown("#### توزيع الإشعارات حسب النوع")
|
682 |
-
|
683 |
-
type_counts = notifications_df["type_ar"].value_counts().reset_index()
|
684 |
-
type_counts.columns = ["النوع", "العدد"]
|
685 |
-
|
686 |
-
st.bar_chart(type_counts, x="النوع", y="العدد")
|
687 |
-
|
688 |
-
# رسم بياني لتوزيع الإشعارات حسب الأولوية
|
689 |
-
st.markdown("#### توزيع الإشعارات حسب الأولوية")
|
690 |
-
|
691 |
-
priority_counts = notifications_df["priority_ar"].value_counts().reset_index()
|
692 |
-
priority_counts.columns = ["الأولوية", "العدد"]
|
693 |
-
|
694 |
-
st.bar_chart(priority_counts, x="الأولوية", y="العدد")
|
695 |
-
|
696 |
-
# خيارات التصدير
|
697 |
-
st.markdown("### تصدير البيانات")
|
698 |
-
|
699 |
-
col1, col2 = st.columns(2)
|
700 |
-
|
701 |
-
with col1:
|
702 |
-
if st.button("تصدير إلى CSV"):
|
703 |
-
st.success("تم تصدير البيانات إلى CSV بنجاح")
|
704 |
-
|
705 |
-
with col2:
|
706 |
-
if st.button("تصدير إلى Excel"):
|
707 |
-
st.success("تم تصدير البيانات إلى Excel بنجاح")
|
|
|
1 |
+
"""
|
2 |
+
وحدة الإشعارات الذكية - نظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import datetime
|
8 |
+
import json
|
9 |
+
import os
|
10 |
+
import sys
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# إضافة مسار المشروع للنظام
|
14 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
15 |
+
|
16 |
+
# استيراد محسن واجهة المستخدم
|
17 |
+
from styling.enhanced_ui import UIEnhancer
|
18 |
+
|
19 |
+
class NotificationsApp:
|
20 |
+
"""تطبيق الإشعارات الذكية"""
|
21 |
+
|
22 |
+
def __init__(self):
|
23 |
+
"""تهيئة تطبيق الإشعارات الذكية"""
|
24 |
+
self.ui = UIEnhancer(page_title="الإشعارات الذكية - نظام تحليل المناقصات", page_icon="🔔")
|
25 |
+
|
26 |
+
# تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
|
27 |
+
if 'theme' not in st.session_state:
|
28 |
+
st.session_state.theme = 'light'
|
29 |
+
|
30 |
+
self.ui.apply_theme_colors()
|
31 |
+
|
32 |
+
# بيانات الإشعارات (نموذجية)
|
33 |
+
self.notifications_data = [
|
34 |
+
{
|
35 |
+
"id": "N001",
|
36 |
+
"title": "موعد تسليم مناقصة",
|
37 |
+
"message": "موعد تسليم مناقصة T-2025-001 (إنشاء مبنى إداري) بعد 5 أيام",
|
38 |
+
"type": "deadline",
|
39 |
+
"priority": "high",
|
40 |
+
"related_entity": "T-2025-001",
|
41 |
+
"created_at": "2025-03-25T10:30:00",
|
42 |
+
"is_read": False
|
43 |
+
},
|
44 |
+
{
|
45 |
+
"id": "N002",
|
46 |
+
"title": "ترسية مناقصة",
|
47 |
+
"message": "تم ترسية مناقصة T-2025-003 (توريد معدات) بنجاح",
|
48 |
+
"type": "award",
|
49 |
+
"priority": "medium",
|
50 |
+
"related_entity": "T-2025-003",
|
51 |
+
"created_at": "2025-03-28T14:15:00",
|
52 |
+
"is_read": True
|
53 |
+
},
|
54 |
+
{
|
55 |
+
"id": "N003",
|
56 |
+
"title": "تحديث مستندات",
|
57 |
+
"message": "تم تحديث مستندات مناقصة T-2025-002 (صيانة طرق)",
|
58 |
+
"type": "document",
|
59 |
+
"priority": "medium",
|
60 |
+
"related_entity": "T-2025-002",
|
61 |
+
"created_at": "2025-03-29T09:45:00",
|
62 |
+
"is_read": False
|
63 |
+
},
|
64 |
+
{
|
65 |
+
"id": "N004",
|
66 |
+
"title": "تغيير في المواصفات",
|
67 |
+
"message": "تم تغيير المواصفات الفنية لمناقصة T-2025-001 (إنشاء مبنى إداري)",
|
68 |
+
"type": "change",
|
69 |
+
"priority": "high",
|
70 |
+
"related_entity": "T-2025-001",
|
71 |
+
"created_at": "2025-03-27T11:20:00",
|
72 |
+
"is_read": False
|
73 |
+
},
|
74 |
+
{
|
75 |
+
"id": "N005",
|
76 |
+
"title": "تأخير في المشروع",
|
77 |
+
"message": "تأخير في تنفيذ مشروع P002 (تطوير طريق الملك فهد - جدة)",
|
78 |
+
"type": "delay",
|
79 |
+
"priority": "high",
|
80 |
+
"related_entity": "P002",
|
81 |
+
"created_at": "2025-03-26T16:10:00",
|
82 |
+
"is_read": True
|
83 |
+
},
|
84 |
+
{
|
85 |
+
"id": "N006",
|
86 |
+
"title": "اكتمال م��حلة",
|
87 |
+
"message": "اكتمال مرحلة الأساسات في مشروع P001 (إنشاء مبنى إداري - الرياض)",
|
88 |
+
"type": "milestone",
|
89 |
+
"priority": "low",
|
90 |
+
"related_entity": "P001",
|
91 |
+
"created_at": "2025-03-24T13:30:00",
|
92 |
+
"is_read": True
|
93 |
+
},
|
94 |
+
{
|
95 |
+
"id": "N007",
|
96 |
+
"title": "طلب معلومات إضافية",
|
97 |
+
"message": "طلب معلومات إضافية لمناقصة T-2025-004 (تجهيز مختبرات)",
|
98 |
+
"type": "request",
|
99 |
+
"priority": "medium",
|
100 |
+
"related_entity": "T-2025-004",
|
101 |
+
"created_at": "2025-03-30T08:15:00",
|
102 |
+
"is_read": False
|
103 |
+
},
|
104 |
+
{
|
105 |
+
"id": "N008",
|
106 |
+
"title": "تحديث أسعار المواد",
|
107 |
+
"message": "تم تحديث أسعار مواد البناء في قاعدة البيانات",
|
108 |
+
"type": "update",
|
109 |
+
"priority": "low",
|
110 |
+
"related_entity": "DB-MATERIALS",
|
111 |
+
"created_at": "2025-03-29T15:40:00",
|
112 |
+
"is_read": False
|
113 |
+
},
|
114 |
+
{
|
115 |
+
"id": "N009",
|
116 |
+
"title": "اجتماع فريق العمل",
|
117 |
+
"message": "اجتماع فريق العمل لمناقشة مناقصة T-2025-001 غداً الساعة 10:00 صباحاً",
|
118 |
+
"type": "meeting",
|
119 |
+
"priority": "medium",
|
120 |
+
"related_entity": "T-2025-001",
|
121 |
+
"created_at": "2025-03-28T16:20:00",
|
122 |
+
"is_read": True
|
123 |
+
},
|
124 |
+
{
|
125 |
+
"id": "N010",
|
126 |
+
"title": "تغيير في الميزانية",
|
127 |
+
"message": "تم تغيير الميزانية المخصصة لمشروع P004 (بناء مدرسة - أبها)",
|
128 |
+
"type": "budget",
|
129 |
+
"priority": "high",
|
130 |
+
"related_entity": "P004",
|
131 |
+
"created_at": "2025-03-25T14:50:00",
|
132 |
+
"is_read": False
|
133 |
+
}
|
134 |
+
]
|
135 |
+
|
136 |
+
# إعدادات الإشعارات (نموذجية)
|
137 |
+
self.notification_settings = {
|
138 |
+
"deadline": True,
|
139 |
+
"award": True,
|
140 |
+
"document": True,
|
141 |
+
"change": True,
|
142 |
+
"delay": True,
|
143 |
+
"milestone": True,
|
144 |
+
"request": True,
|
145 |
+
"update": True,
|
146 |
+
"meeting": True,
|
147 |
+
"budget": True,
|
148 |
+
"email_notifications": True,
|
149 |
+
"sms_notifications": False,
|
150 |
+
"push_notifications": True,
|
151 |
+
"notification_frequency": "realtime"
|
152 |
+
}
|
153 |
+
|
154 |
+
def run(self):
|
155 |
+
"""تشغيل تطبيق الإشعارات الذكية"""
|
156 |
+
# إضافة زر تبديل السمة في أعلى الصفحة
|
157 |
+
col1, col2, col3 = st.columns([1, 8, 1])
|
158 |
+
with col3:
|
159 |
+
if st.button("🌓 تبديل السمة"):
|
160 |
+
# تبديل السمة
|
161 |
+
if st.session_state.theme == "light":
|
162 |
+
st.session_state.theme = "dark"
|
163 |
+
else:
|
164 |
+
st.session_state.theme = "light"
|
165 |
+
|
166 |
+
# تطبيق السمة الجديدة
|
167 |
+
self.ui.theme_mode = st.session_state.theme
|
168 |
+
self.ui.apply_theme_colors()
|
169 |
+
st.rerun()
|
170 |
+
|
171 |
+
# إنشاء ترويسة الصفحة
|
172 |
+
self.ui.create_header("الإشعارات الذكية", "إدارة ومتابعة الإشعارات والتنبيهات")
|
173 |
+
|
174 |
+
# إنشاء علامات تبويب للوظائف المختلفة
|
175 |
+
tabs = st.tabs(["الإشعارات الحالية", "إعدادات الإشعارات", "إنشاء إشعار", "سجل الإشعارات"])
|
176 |
+
|
177 |
+
# علامة تبويب الإشعارات الحالية
|
178 |
+
with tabs[0]:
|
179 |
+
self.show_current_notifications()
|
180 |
+
|
181 |
+
# علامة تبويب إعدادات الإشعارات
|
182 |
+
with tabs[1]:
|
183 |
+
self.show_notification_settings()
|
184 |
+
|
185 |
+
# علامة تبويب إنشاء إشعار
|
186 |
+
with tabs[2]:
|
187 |
+
self.create_notification()
|
188 |
+
|
189 |
+
# علامة تبويب سجل الإشعارات
|
190 |
+
with tabs[3]:
|
191 |
+
self.show_notification_history()
|
192 |
+
|
193 |
+
def show_current_notifications(self):
|
194 |
+
"""عرض الإشعارات الحالية"""
|
195 |
+
st.markdown("### الإشعارات الحالية")
|
196 |
+
|
197 |
+
# إنشاء فلاتر للإشعارات
|
198 |
+
col1, col2, col3 = st.columns(3)
|
199 |
+
|
200 |
+
with col1:
|
201 |
+
type_filter = st.multiselect(
|
202 |
+
"نوع الإشعار",
|
203 |
+
options=["الكل", "موعد نهائي", "تر��ية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"],
|
204 |
+
default=["الكل"]
|
205 |
+
)
|
206 |
+
|
207 |
+
with col2:
|
208 |
+
priority_filter = st.multiselect(
|
209 |
+
"الأولوية",
|
210 |
+
options=["الكل", "عالية", "متوسطة", "منخفضة"],
|
211 |
+
default=["الكل"]
|
212 |
+
)
|
213 |
+
|
214 |
+
with col3:
|
215 |
+
read_filter = st.radio(
|
216 |
+
"الحالة",
|
217 |
+
options=["الكل", "غير مقروءة", "مقروءة"],
|
218 |
+
horizontal=True
|
219 |
+
)
|
220 |
+
|
221 |
+
# تطبيق الفلاتر
|
222 |
+
filtered_notifications = self.notifications_data
|
223 |
+
|
224 |
+
# تحويل أنواع الإشعارات من الإنجليزية إلى العربية للفلترة
|
225 |
+
type_mapping = {
|
226 |
+
"موعد نهائي": "deadline",
|
227 |
+
"ترسية": "award",
|
228 |
+
"مستند": "document",
|
229 |
+
"تغيير": "change",
|
230 |
+
"تأخير": "delay",
|
231 |
+
"مرحلة": "milestone",
|
232 |
+
"طلب": "request",
|
233 |
+
"تحديث": "update",
|
234 |
+
"اجتماع": "meeting",
|
235 |
+
"ميزانية": "budget"
|
236 |
+
}
|
237 |
+
|
238 |
+
# تحويل الأولويات من العربية إلى الإنجليزية للفلترة
|
239 |
+
priority_mapping = {
|
240 |
+
"عالية": "high",
|
241 |
+
"متوسطة": "medium",
|
242 |
+
"منخفضة": "low"
|
243 |
+
}
|
244 |
+
|
245 |
+
if "الكل" not in type_filter and type_filter:
|
246 |
+
filtered_types = [type_mapping[t] for t in type_filter if t in type_mapping]
|
247 |
+
filtered_notifications = [n for n in filtered_notifications if n["type"] in filtered_types]
|
248 |
+
|
249 |
+
if "الكل" not in priority_filter and priority_filter:
|
250 |
+
filtered_priorities = [priority_mapping[p] for p in priority_filter if p in priority_mapping]
|
251 |
+
filtered_notifications = [n for n in filtered_notifications if n["priority"] in filtered_priorities]
|
252 |
+
|
253 |
+
if read_filter == "غير مقروءة":
|
254 |
+
filtered_notifications = [n for n in filtered_notifications if not n["is_read"]]
|
255 |
+
elif read_filter == "مقروءة":
|
256 |
+
filtered_notifications = [n for n in filtered_notifications if n["is_read"]]
|
257 |
+
|
258 |
+
# عرض عدد الإشعارات غير المقروءة
|
259 |
+
unread_count = len([n for n in filtered_notifications if not n["is_read"]])
|
260 |
+
|
261 |
+
st.markdown(f"**عدد الإشعارات غير المقروءة:** {unread_count}")
|
262 |
+
|
263 |
+
# زر تحديث وتعليم الكل كمقروء
|
264 |
+
col1, col2 = st.columns([1, 1])
|
265 |
+
with col1:
|
266 |
+
if st.button("تحديث الإشعارات", use_container_width=True):
|
267 |
+
st.success("تم تحديث الإشعارات بنجاح")
|
268 |
+
|
269 |
+
with col2:
|
270 |
+
if st.button("تعليم الكل كمقروء", use_container_width=True):
|
271 |
+
st.success("تم تعليم جميع الإشعارات كمقروءة")
|
272 |
+
|
273 |
+
# عرض الإشعارات
|
274 |
+
if not filtered_notifications:
|
275 |
+
st.info("لا توجد إشعارات تطابق الفلاتر المحددة")
|
276 |
+
else:
|
277 |
+
for notification in filtered_notifications:
|
278 |
+
self.display_notification(notification)
|
279 |
+
|
280 |
+
def display_notification(self, notification):
|
281 |
+
"""عرض إشعار واحد"""
|
282 |
+
# تحديد لون الإشعار بناءً على الأولوية
|
283 |
+
if notification["priority"] == "high":
|
284 |
+
color = self.ui.COLORS['danger']
|
285 |
+
priority_text = "عالية"
|
286 |
+
elif notification["priority"] == "medium":
|
287 |
+
color = self.ui.COLORS['warning']
|
288 |
+
priority_text = "متوسطة"
|
289 |
+
else:
|
290 |
+
color = self.ui.COLORS['secondary']
|
291 |
+
priority_text = "منخفضة"
|
292 |
+
|
293 |
+
# تحويل نوع الإشعار إلى العربية
|
294 |
+
type_mapping = {
|
295 |
+
"deadline": "موعد نهائي",
|
296 |
+
"award": "ترسية",
|
297 |
+
"document": "مستند",
|
298 |
+
"change": "تغيير",
|
299 |
+
"delay": "تأخير",
|
300 |
+
"milestone": "مرحلة",
|
301 |
+
"request": "طلب",
|
302 |
+
"update": "تحديث",
|
303 |
+
"meeting": "اجتماع",
|
304 |
+
"budget": "ميزانية"
|
305 |
+
}
|
306 |
+
|
307 |
+
notification_type = type_mapping.get(notification["type"], notification["type"])
|
308 |
+
|
309 |
+
# تحويل التاريخ إلى تنسيق مناسب
|
310 |
+
created_at = datetime.datetime.fromisoformat(notification["created_at"])
|
311 |
+
formatted_date = created_at.strftime("%Y-%m-%d %H:%M")
|
312 |
+
|
313 |
+
# تحديد أيقونة الإشعار
|
314 |
+
icon_mapping = {
|
315 |
+
"deadline": "⏰",
|
316 |
+
"award": "🏆",
|
317 |
+
"document": "📄",
|
318 |
+
"change": "🔄",
|
319 |
+
"delay": "⚠️",
|
320 |
+
"milestone": "🏁",
|
321 |
+
"request": "❓",
|
322 |
+
"update": "🔄",
|
323 |
+
"meeting": "👥",
|
324 |
+
"budget": "💰"
|
325 |
+
}
|
326 |
+
|
327 |
+
icon = icon_mapping.get(notification["type"], "📌")
|
328 |
+
|
329 |
+
# إنشاء بطاقة الإشعار
|
330 |
+
st.markdown(
|
331 |
+
f"""
|
332 |
+
<div style="border-left: 5px solid {color}; padding: 10px; margin-bottom: 10px; background-color: {'#f8f9fa' if st.session_state.theme == 'light' else '#2b2b2b'}; border-radius: 5px; {'opacity: 0.7;' if notification['is_read'] else ''}">
|
333 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
334 |
+
<div>
|
335 |
+
<h4 style="margin: 0;">{icon} {notification['title']}</h4>
|
336 |
+
<p style="margin: 5px 0;">{notification['message']}</p>
|
337 |
+
<div style="display: flex; gap: 10px; font-size: 0.8em; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">
|
338 |
+
<span>النوع: {notification_type}</span>
|
339 |
+
<span>الأولوية: {priority_text}</span>
|
340 |
+
<span>التاريخ: {formatted_date}</span>
|
341 |
+
</div>
|
342 |
+
</div>
|
343 |
+
<div>
|
344 |
+
<button style="background: none; border: none; cursor: pointer; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">✓</button>
|
345 |
+
<button style="background: none; border: none; cursor: pointer; color: {'#6c757d' if st.session_state.theme == 'light' else '#adb5bd'};">🗑️</button>
|
346 |
+
</div>
|
347 |
+
</div>
|
348 |
+
</div>
|
349 |
+
""",
|
350 |
+
unsafe_allow_html=True
|
351 |
+
)
|
352 |
+
|
353 |
+
def show_notification_settings(self):
|
354 |
+
"""عرض إعدادات الإشعارات"""
|
355 |
+
st.markdown("### إعدادات الإشعارات")
|
356 |
+
|
357 |
+
# إنشاء نموذج الإعدادات
|
358 |
+
with st.form("notification_settings_form"):
|
359 |
+
st.markdown("#### أنواع الإشعارات")
|
360 |
+
|
361 |
+
col1, col2 = st.columns(2)
|
362 |
+
|
363 |
+
with col1:
|
364 |
+
deadline = st.checkbox("المواعيد النهائية", value=self.notification_settings["deadline"])
|
365 |
+
award = st.checkbox("ترسية المناقصات", value=self.notification_settings["award"])
|
366 |
+
document = st.checkbox("تحديثات المستندات", value=self.notification_settings["document"])
|
367 |
+
change = st.checkbox("التغييرات في المواصفات", value=self.notification_settings["change"])
|
368 |
+
delay = st.checkbox("التأخيرات في المشاريع", value=self.notification_settings["delay"])
|
369 |
+
|
370 |
+
with col2:
|
371 |
+
milestone = st.checkbox("اكتمال المراحل", value=self.notification_settings["milestone"])
|
372 |
+
request = st.checkbox("طلبات المعلومات", value=self.notification_settings["request"])
|
373 |
+
update = st.checkbox("تحديثات النظام", value=self.notification_settings["update"])
|
374 |
+
meeting = st.checkbox("الاجتماعات", value=self.notification_settings["meeting"])
|
375 |
+
budget = st.checkbox("تغييرات الميزانية", value=self.notification_settings["budget"])
|
376 |
+
|
377 |
+
st.markdown("#### طرق الإشعار")
|
378 |
+
|
379 |
+
col1, col2, col3 = st.columns(3)
|
380 |
+
|
381 |
+
with col1:
|
382 |
+
email_notifications = st.checkbox("البريد الإلكتروني", value=self.notification_settings["email_notifications"])
|
383 |
+
|
384 |
+
with col2:
|
385 |
+
sms_notifications = st.checkbox("الرسائل النصية", value=self.notification_settings["sms_notifications"])
|
386 |
+
|
387 |
+
with col3:
|
388 |
+
push_notifications = st.checkbox("إشعارات الويب", value=self.notification_settings["push_notifications"])
|
389 |
+
|
390 |
+
st.markdown("#### تكرار الإشعارات")
|
391 |
+
|
392 |
+
notification_frequency = st.radio(
|
393 |
+
"تكرار الإشعارات",
|
394 |
+
options=["في الوقت الحقيقي", "مرة واحدة يومياً", "مرة واحدة أسبوعياً"],
|
395 |
+
index=0 if self.notification_settings["notification_frequency"] == "realtime" else 1 if self.notification_settings["notification_frequency"] == "daily" else 2,
|
396 |
+
horizontal=True
|
397 |
+
)
|
398 |
+
|
399 |
+
# زر حفظ الإعدادات
|
400 |
+
submit_button = st.form_submit_button("حفظ الإعدادات")
|
401 |
+
|
402 |
+
if submit_button:
|
403 |
+
# تحديث الإعدادات (في تطبيق حقيقي، سيتم حفظ الإعدادات في قاعدة البيانات)
|
404 |
+
self.notification_settings.update({
|
405 |
+
"deadline": deadline,
|
406 |
+
"award": award,
|
407 |
+
"document": document,
|
408 |
+
"change": change,
|
409 |
+
"delay": delay,
|
410 |
+
"milestone": milestone,
|
411 |
+
"request": request,
|
412 |
+
"update": update,
|
413 |
+
"meeting": meeting,
|
414 |
+
"budget": budget,
|
415 |
+
"email_notifications": email_notifications,
|
416 |
+
"sms_notifications": sms_notifications,
|
417 |
+
"push_notifications": push_notifications,
|
418 |
+
"notification_frequency": "realtime" if notification_frequency == "في الوقت الحقيقي" else "daily" if notification_frequency == "مرة واحدة يومياً" else "weekly"
|
419 |
+
})
|
420 |
+
|
421 |
+
st.success("تم حفظ الإعدادات بنجاح")
|
422 |
+
|
423 |
+
# إعدادات متقدمة
|
424 |
+
st.markdown("### إعدادات متقدمة")
|
425 |
+
|
426 |
+
with st.expander("إعدادات متقدمة"):
|
427 |
+
st.markdown("#### جدولة الإشعارات")
|
428 |
+
|
429 |
+
col1, col2 = st.columns(2)
|
430 |
+
|
431 |
+
with col1:
|
432 |
+
st.time_input("وقت الإشعارات اليومية", datetime.time(9, 0))
|
433 |
+
|
434 |
+
with col2:
|
435 |
+
st.selectbox(
|
436 |
+
"يوم الإشعارات الأسبوعية",
|
437 |
+
options=["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت"],
|
438 |
+
index=0
|
439 |
+
)
|
440 |
+
|
441 |
+
st.markdown("#### فلترة الإشعارات")
|
442 |
+
|
443 |
+
min_priority = st.select_slider(
|
444 |
+
"الحد الأدنى للأولوية",
|
445 |
+
options=["منخفضة", "متوسطة", "عالية"],
|
446 |
+
value="منخفضة"
|
447 |
+
)
|
448 |
+
|
449 |
+
st.markdown("#### حفظ الإشعارات")
|
450 |
+
|
451 |
+
retention_period = st.slider(
|
452 |
+
"فترة الاحتفاظ بالإشعارات (بالأيام)",
|
453 |
+
min_value=7,
|
454 |
+
max_value=365,
|
455 |
+
value=90,
|
456 |
+
step=1
|
457 |
+
)
|
458 |
+
|
459 |
+
if st.button("حفظ الإعدادات المتقدمة"):
|
460 |
+
st.success("تم حفظ الإعدادات المتقدمة بنجاح")
|
461 |
+
|
462 |
+
def create_notification(self):
|
463 |
+
"""إنشاء إشعار جديد"""
|
464 |
+
st.markdown("### إنشاء إشعار جديد")
|
465 |
+
|
466 |
+
# إنشاء نموذج إشعار جديد
|
467 |
+
with st.form("new_notification_form"):
|
468 |
+
title = st.text_input("عنوان الإشعار")
|
469 |
+
message = st.text_area("نص الإشعار")
|
470 |
+
|
471 |
+
col1, col2 = st.columns(2)
|
472 |
+
|
473 |
+
with col1:
|
474 |
+
notification_type = st.selectbox(
|
475 |
+
"نوع الإشعار",
|
476 |
+
options=["موعد نهائي", "ترسية", "مستند", "تغيير", "تأخير", "مرحلة", "طلب", "تحديث", "اجتماع", "ميزانية"]
|
477 |
+
)
|
478 |
+
|
479 |
+
# تحويل نوع الإشعار إلى الإنجليزية
|
480 |
+
type_mapping = {
|
481 |
+
"موعد نهائي": "deadline",
|
482 |
+
"ترسية": "award",
|
483 |
+
"مستند": "document",
|
484 |
+
"تغيير": "change",
|
485 |
+
"تأخير": "delay",
|
486 |
+
"مرحلة": "milestone",
|
487 |
+
"طلب": "request",
|
488 |
+
"تحديث": "update",
|
489 |
+
"اجتماع": "meeting",
|
490 |
+
"ميزانية": "budget"
|
491 |
+
}
|
492 |
+
|
493 |
+
notification_type_en = type_mapping.get(notification_type, "deadline")
|
494 |
+
|
495 |
+
priority = st.selectbox(
|
496 |
+
"الأولوية",
|
497 |
+
options=["عالية", "متوسطة", "منخفضة"]
|
498 |
+
)
|
499 |
+
|
500 |
+
# تحويل الأولوية إلى الإنجليزية
|
501 |
+
priority_mapping = {
|
502 |
+
"عالية": "high",
|
503 |
+
"متوسطة": "medium",
|
504 |
+
"منخفضة": "low"
|
505 |
+
}
|
506 |
+
|
507 |
+
priority_en = priority_mapping.get(priority, "medium")
|
508 |
+
|
509 |
+
with col2:
|
510 |
+
related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة، رقم المشروع)")
|
511 |
+
|
512 |
+
notification_date = st.date_input(
|
513 |
+
"تاريخ الإشعار",
|
514 |
+
value=datetime.datetime.now().date()
|
515 |
+
)
|
516 |
+
|
517 |
+
notification_time = st.time_input(
|
518 |
+
"وقت الإشعار",
|
519 |
+
value=datetime.datetime.now().time()
|
520 |
+
)
|
521 |
+
|
522 |
+
# زر إنشاء الإشعار
|
523 |
+
submit_button = st.form_submit_button("إنشاء الإشعار")
|
524 |
+
|
525 |
+
if submit_button and title and message:
|
526 |
+
# إنشاء إشعار جديد (في تطبيق حقيقي، سيتم حفظ الإشعار في قاعدة البيانات)
|
527 |
+
new_id = f"N{len(self.notifications_data) + 1:03d}"
|
528 |
+
|
529 |
+
# تحويل التاريخ والوقت إلى تنسيق ISO
|
530 |
+
notification_datetime = datetime.datetime.combine(notification_date, notification_time)
|
531 |
+
notification_datetime_iso = notification_datetime.isoformat()
|
532 |
+
|
533 |
+
# إضافة الإشعار الجديد إلى قائمة الإشعارات
|
534 |
+
self.notifications_data.append({
|
535 |
+
"id": new_id,
|
536 |
+
"title": title,
|
537 |
+
"message": message,
|
538 |
+
"type": notification_type_en,
|
539 |
+
"priority": priority_en,
|
540 |
+
"related_entity": related_entity,
|
541 |
+
"created_at": notification_datetime_iso,
|
542 |
+
"is_read": False
|
543 |
+
})
|
544 |
+
|
545 |
+
st.success("تم إنشاء الإشعار بنجاح")
|
546 |
+
|
547 |
+
# عرض الإشعار الجديد
|
548 |
+
st.markdown("### الإشعار الجديد")
|
549 |
+
self.display_notification(self.notifications_data[-1])
|
550 |
+
|
551 |
+
# إنشاء إشعارات متعددة
|
552 |
+
st.markdown("### إنشاء إشعارات متعددة")
|
553 |
+
|
554 |
+
with st.expander("إنشاء إشعارات متعددة"):
|
555 |
+
st.markdown("#### تحميل ملف إشعارات")
|
556 |
+
|
557 |
+
uploaded_file = st.file_uploader("قم بتحميل ملف إشعارات (CSV, JSON)", type=["csv", "json"])
|
558 |
+
|
559 |
+
if uploaded_file is not None:
|
560 |
+
if st.button("استيراد الإشعارات"):
|
561 |
+
st.success("تم استيراد الإشعارات بنجاح")
|
562 |
+
|
563 |
+
st.markdown("#### إنشاء إشعارات من قالب")
|
564 |
+
|
565 |
+
template_type = st.selectbox(
|
566 |
+
"نوع القالب",
|
567 |
+
options=["إشعارات المواعيد النهائية", "إشعارات الاجتماعات", "إشعارات التحديثات"]
|
568 |
+
)
|
569 |
+
|
570 |
+
if st.button("إنشاء إشعارات من القالب"):
|
571 |
+
st.success("تم إنشاء الإشعارات من القالب بنجاح")
|
572 |
+
|
573 |
+
def show_notification_history(self):
|
574 |
+
"""عرض سجل الإشعارات"""
|
575 |
+
st.markdown("### سجل الإشعارات")
|
576 |
+
|
577 |
+
# إنشاء فلاتر للسجل
|
578 |
+
col1, col2 = st.columns(2)
|
579 |
+
|
580 |
+
with col1:
|
581 |
+
date_range = st.date_input(
|
582 |
+
"نطاق التاريخ",
|
583 |
+
value=(
|
584 |
+
datetime.datetime.now().date() - datetime.timedelta(days=30),
|
585 |
+
datetime.datetime.now().date()
|
586 |
+
)
|
587 |
+
)
|
588 |
+
|
589 |
+
with col2:
|
590 |
+
entity_filter = st.text_input("الكيان المرتبط")
|
591 |
+
|
592 |
+
# تحويل البيانات إلى DataFrame
|
593 |
+
notifications_df = pd.DataFrame(self.notifications_data)
|
594 |
+
|
595 |
+
# تحويل عمود التاريخ إلى نوع datetime
|
596 |
+
notifications_df["created_at"] = pd.to_datetime(notifications_df["created_at"])
|
597 |
+
|
598 |
+
# تطبيق فلتر التاريخ
|
599 |
+
if len(date_range) == 2:
|
600 |
+
start_date, end_date = date_range
|
601 |
+
start_date = pd.to_datetime(start_date)
|
602 |
+
end_date = pd.to_datetime(end_date) + pd.Timedelta(days=1) - pd.Timedelta(seconds=1)
|
603 |
+
|
604 |
+
notifications_df = notifications_df[
|
605 |
+
(notifications_df["created_at"] >= start_date) &
|
606 |
+
(notifications_df["created_at"] <= end_date)
|
607 |
+
]
|
608 |
+
|
609 |
+
# تطبيق فلتر الكيان المرتبط
|
610 |
+
if entity_filter:
|
611 |
+
notifications_df = notifications_df[
|
612 |
+
notifications_df["related_entity"].str.contains(entity_filter, case=False)
|
613 |
+
]
|
614 |
+
|
615 |
+
# تحويل أنواع الإشعارات من الإنجليزية إلى العربية
|
616 |
+
type_mapping = {
|
617 |
+
"deadline": "موعد نهائي",
|
618 |
+
"award": "ترسية",
|
619 |
+
"document": "مستند",
|
620 |
+
"change": "ت��يير",
|
621 |
+
"delay": "تأخير",
|
622 |
+
"milestone": "مرحلة",
|
623 |
+
"request": "طلب",
|
624 |
+
"update": "تحديث",
|
625 |
+
"meeting": "اجتماع",
|
626 |
+
"budget": "ميزانية"
|
627 |
+
}
|
628 |
+
|
629 |
+
notifications_df["type_ar"] = notifications_df["type"].map(type_mapping)
|
630 |
+
|
631 |
+
# تحويل الأولويات من الإنجليزية إلى العربية
|
632 |
+
priority_mapping = {
|
633 |
+
"high": "عالية",
|
634 |
+
"medium": "متوسطة",
|
635 |
+
"low": "منخفضة"
|
636 |
+
}
|
637 |
+
|
638 |
+
notifications_df["priority_ar"] = notifications_df["priority"].map(priority_mapping)
|
639 |
+
|
640 |
+
# تحويل حالة القراءة إلى نص
|
641 |
+
notifications_df["is_read_text"] = notifications_df["is_read"].map({True: "مقروءة", False: "غير مقروءة"})
|
642 |
+
|
643 |
+
# تنسيق عمود التاريخ
|
644 |
+
notifications_df["created_at_formatted"] = notifications_df["created_at"].dt.strftime("%Y-%m-%d %H:%M")
|
645 |
+
|
646 |
+
# إنشاء DataFrame للعرض
|
647 |
+
display_df = notifications_df[[
|
648 |
+
"id", "title", "type_ar", "priority_ar", "related_entity",
|
649 |
+
"created_at_formatted", "is_read_text"
|
650 |
+
]].rename(columns={
|
651 |
+
"id": "الرقم",
|
652 |
+
"title": "العنوان",
|
653 |
+
"type_ar": "النوع",
|
654 |
+
"priority_ar": "الأولوية",
|
655 |
+
"related_entity": "الكيان المرتبط",
|
656 |
+
"created_at_formatted": "التاريخ",
|
657 |
+
"is_read_text": "الحالة"
|
658 |
+
})
|
659 |
+
|
660 |
+
# عرض الجدول
|
661 |
+
st.dataframe(display_df, use_container_width=True, hide_index=True)
|
662 |
+
|
663 |
+
# إحصائيات الإشعارات
|
664 |
+
st.markdown("### إحصائيات الإشعارات")
|
665 |
+
|
666 |
+
col1, col2, col3 = st.columns(3)
|
667 |
+
|
668 |
+
with col1:
|
669 |
+
total_count = len(notifications_df)
|
670 |
+
st.metric("إجمالي الإشعارات", total_count)
|
671 |
+
|
672 |
+
with col2:
|
673 |
+
read_count = len(notifications_df[notifications_df["is_read"] == True])
|
674 |
+
st.metric("الإشعارات المقروءة", read_count)
|
675 |
+
|
676 |
+
with col3:
|
677 |
+
unread_count = len(notifications_df[notifications_df["is_read"] == False])
|
678 |
+
st.metric("الإشعارات غير المقروءة", unread_count)
|
679 |
+
|
680 |
+
# رسم بياني لتوزيع الإشعارات حسب النوع
|
681 |
+
st.markdown("#### توزيع الإشعارات حسب النوع")
|
682 |
+
|
683 |
+
type_counts = notifications_df["type_ar"].value_counts().reset_index()
|
684 |
+
type_counts.columns = ["النوع", "العدد"]
|
685 |
+
|
686 |
+
st.bar_chart(type_counts, x="النوع", y="العدد")
|
687 |
+
|
688 |
+
# رسم بياني لتوزيع الإشعارات حسب الأولوية
|
689 |
+
st.markdown("#### توزيع الإشعارات حسب الأولوية")
|
690 |
+
|
691 |
+
priority_counts = notifications_df["priority_ar"].value_counts().reset_index()
|
692 |
+
priority_counts.columns = ["الأولوية", "العدد"]
|
693 |
+
|
694 |
+
st.bar_chart(priority_counts, x="الأولوية", y="العدد")
|
695 |
+
|
696 |
+
# خيارات التصدير
|
697 |
+
st.markdown("### تصدير البيانات")
|
698 |
+
|
699 |
+
col1, col2 = st.columns(2)
|
700 |
+
|
701 |
+
with col1:
|
702 |
+
if st.button("تصدير إلى CSV"):
|
703 |
+
st.success("تم تصدير البيانات إلى CSV بنجاح")
|
704 |
+
|
705 |
+
with col2:
|
706 |
+
if st.button("تصدير إلى Excel"):
|
707 |
+
st.success("تم تصدير البيانات إلى Excel بنجاح")
|
modules/pricing/price_analyzer.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
modules/pricing/pricing_app.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
from pathlib import Path
|
2 |
-
|
3 |
-
|
|
|
4 |
from pricing_system.modules.analysis import smart_price_analysis as analysis_utils
|
5 |
from pricing_system.modules.catalogs import materials_catalog, equipment_catalog
|
6 |
from pricing_system.modules.indirect_support import overheads
|
@@ -8,152 +9,166 @@ from pricing_system.modules.pricing_strategies import balanced_pricing, profit_o
|
|
8 |
|
9 |
class PricingApp:
|
10 |
"""وحدة التسعير"""
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
def run(self):
|
17 |
-
self.render()
|
18 |
-
|
19 |
-
|
20 |
-
def _render_pricing_scenarios_tab(self):
|
21 |
-
st.markdown("### سيناريوهات التسعير")
|
22 |
-
balanced_pricing.render_balanced_strategy()
|
23 |
|
24 |
-
|
25 |
-
|
26 |
|
27 |
-
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
|
31 |
-
|
32 |
-
|
33 |
|
34 |
|
35 |
def run(self):
|
36 |
-
|
|
|
37 |
|
38 |
-
|
39 |
-
|
40 |
|
41 |
tabs = st.tabs([
|
42 |
-
"لوحة التحكم",
|
43 |
"جدول الكميات",
|
44 |
"تحليل التكاليف",
|
45 |
"سيناريوهات التسعير",
|
46 |
-
"
|
47 |
-
"المحتوى المحلي",
|
48 |
-
"تسعير غير متزن",
|
49 |
-
"التقارير"
|
50 |
])
|
51 |
|
52 |
with tabs[0]:
|
53 |
-
self._render_dashboard_tab()
|
54 |
-
with tabs[1]:
|
55 |
self._render_bill_of_quantities_tab()
|
56 |
-
with tabs[
|
57 |
self._render_cost_analysis_tab()
|
58 |
-
with tabs[
|
59 |
self._render_pricing_scenarios_tab()
|
60 |
-
with tabs[
|
61 |
-
self._render_competitive_analysis_tab()
|
62 |
-
with tabs[5]:
|
63 |
self._render_local_content_tab()
|
64 |
-
with tabs[6]:
|
65 |
-
self._render_unbalanced_pricing_tab()
|
66 |
-
with tabs[7]:
|
67 |
-
self._render_reports_tab()
|
68 |
|
69 |
-
def
|
70 |
-
|
71 |
-
st.
|
72 |
|
73 |
-
|
74 |
-
|
75 |
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
column_config={
|
82 |
-
'code': 'الكود',
|
83 |
-
'description': 'الوصف',
|
84 |
-
'unit': 'الوحدة',
|
85 |
-
'quantity': 'الكمية',
|
86 |
-
'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
|
87 |
-
'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
|
88 |
-
'category': 'الفئة'
|
89 |
-
},
|
90 |
-
hide_index=True,
|
91 |
-
use_container_width=True
|
92 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
else:
|
94 |
-
st.warning("لا توجد
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
st.markdown("### إضافة بند جديد")
|
|
|
97 |
col1, col2 = st.columns(2)
|
98 |
with col1:
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
with col2:
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
if new_code and new_description and new_quantity > 0 and new_unit_price > 0:
|
120 |
-
new_total_price = new_quantity * new_unit_price
|
121 |
-
new_id = max([item['id'] for item in st.session_state.bill_of_quantities], default=0) + 1
|
122 |
-
st.session_state.bill_of_quantities.append({
|
123 |
-
'id': new_id,
|
124 |
-
'code': new_code,
|
125 |
-
'description': new_description,
|
126 |
-
'unit': new_unit,
|
127 |
-
'quantity': new_quantity,
|
128 |
-
'unit_price': new_unit_price,
|
129 |
-
'total_price': new_total_price,
|
130 |
-
'category': new_category
|
131 |
-
})
|
132 |
-
st.success(f"تمت إضافة البند بنجاح: {new_description}")
|
133 |
st.rerun()
|
134 |
-
|
135 |
-
st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
|
136 |
|
137 |
def _render_cost_analysis_tab(self):
|
138 |
st.markdown("### تحليل التكاليف")
|
139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
|
141 |
def _render_pricing_scenarios_tab(self):
|
142 |
st.markdown("### سيناريوهات التسعير")
|
143 |
-
|
144 |
-
|
145 |
-
def _render_competitive_analysis_tab(self):
|
146 |
-
st.markdown("### المقارنة التنافسية")
|
147 |
-
st.info("سيتم عرض مقارنة الأسعار والمنافسين هنا.")
|
148 |
|
149 |
def _render_local_content_tab(self):
|
150 |
st.markdown("### المحتوى المحلي")
|
151 |
-
overheads.render_local_content_ui()
|
152 |
-
|
153 |
-
def _render_unbalanced_pricing_tab(self):
|
154 |
-
st.markdown("### التسعير غير المتزن")
|
155 |
-
st.info("سيتم عرض استراتيجية التسعير غير المتزن هنا لاحقًا.")
|
156 |
-
|
157 |
-
def _render_reports_tab(self):
|
158 |
-
st.markdown("### التقارير")
|
159 |
-
st.info("يمكنك هنا تنزيل التقارير الخاصة بالتسعير وجدول الكميات.")
|
|
|
1 |
from pathlib import Path
|
2 |
+
import streamlit as st
|
3 |
+
import pandas as pd
|
4 |
+
from datetime import datetime
|
5 |
from pricing_system.modules.analysis import smart_price_analysis as analysis_utils
|
6 |
from pricing_system.modules.catalogs import materials_catalog, equipment_catalog
|
7 |
from pricing_system.modules.indirect_support import overheads
|
|
|
9 |
|
10 |
class PricingApp:
|
11 |
"""وحدة التسعير"""
|
12 |
+
def __init__(self):
|
13 |
+
"""تهيئة وحدة التسعير"""
|
14 |
+
if 'project_data' not in st.session_state:
|
15 |
+
st.session_state.project_data = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
+
if 'bill_of_quantities' not in st.session_state:
|
18 |
+
st.session_state.bill_of_quantities = []
|
19 |
|
20 |
+
# Maintain existing session state for indirect costs and risks if available.
|
21 |
+
if 'indirect_costs' not in st.session_state:
|
22 |
+
st.session_state.indirect_costs = {
|
23 |
+
'overhead': 0.10, # نسبة المصاريف العمومية والإدارية
|
24 |
+
'profit': 0.15, # نسبة الربح
|
25 |
+
'contingency': 0.05, # نسبة الطوارئ
|
26 |
+
'bonds': 0.02, # نسبة الضمانات
|
27 |
+
'insurance': 0.03 # نسبة التأمين
|
28 |
+
}
|
29 |
|
30 |
+
if 'risks' not in st.session_state:
|
31 |
+
st.session_state.risks = []
|
32 |
|
33 |
|
34 |
def run(self):
|
35 |
+
"""تشغيل وحدة التسعير"""
|
36 |
+
st.title("وحدة التسعير")
|
37 |
|
38 |
+
# اختيار المشروع
|
39 |
+
self._select_project()
|
40 |
|
41 |
tabs = st.tabs([
|
|
|
42 |
"جدول الكميات",
|
43 |
"تحليل التكاليف",
|
44 |
"سيناريوهات التسعير",
|
45 |
+
"المحتوى المحلي"
|
|
|
|
|
|
|
46 |
])
|
47 |
|
48 |
with tabs[0]:
|
|
|
|
|
49 |
self._render_bill_of_quantities_tab()
|
50 |
+
with tabs[1]:
|
51 |
self._render_cost_analysis_tab()
|
52 |
+
with tabs[2]:
|
53 |
self._render_pricing_scenarios_tab()
|
54 |
+
with tabs[3]:
|
|
|
|
|
55 |
self._render_local_content_tab()
|
|
|
|
|
|
|
|
|
56 |
|
57 |
+
def _select_project(self):
|
58 |
+
"""اختيار المشروع"""
|
59 |
+
st.sidebar.markdown("### اختيار المشروع")
|
60 |
|
61 |
+
# جلب المشاريع من قاعدة البيانات
|
62 |
+
projects = self._get_projects_from_db()
|
63 |
|
64 |
+
if projects:
|
65 |
+
project_names = [p['name'] for p in projects]
|
66 |
+
selected_project = st.sidebar.selectbox(
|
67 |
+
"اختر المشروع",
|
68 |
+
project_names
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
)
|
70 |
+
|
71 |
+
# تحديث بيانات المشروع المحدد
|
72 |
+
project = next((p for p in projects if p['name'] == selected_project), None)
|
73 |
+
if project:
|
74 |
+
st.session_state.current_project = project
|
75 |
+
st.session_state.bill_of_quantities = project.get('boq_items', [])
|
76 |
else:
|
77 |
+
st.sidebar.warning("لا توجد مشاريع متاحة")
|
78 |
+
|
79 |
+
def _get_projects_from_db(self):
|
80 |
+
"""جلب المشاريع من قاعدة البيانات"""
|
81 |
+
# هنا يتم جلب المشاريع من قاعدة البيانات
|
82 |
+
# هذه بيانات تجريبية للتوضيح
|
83 |
+
return [
|
84 |
+
{
|
85 |
+
'id': 1,
|
86 |
+
'name': 'مشروع تطوير الطريق',
|
87 |
+
'client': 'وزارة النقل',
|
88 |
+
'boq_items': [
|
89 |
+
{
|
90 |
+
'code': 'A-001',
|
91 |
+
'description': 'أعمال الحفر',
|
92 |
+
'unit': 'م3',
|
93 |
+
'quantity': 1000,
|
94 |
+
'unit_price': 50,
|
95 |
+
'total_price': 50000
|
96 |
+
}
|
97 |
+
]
|
98 |
+
}
|
99 |
+
]
|
100 |
|
101 |
+
def _render_bill_of_quantities_tab(self):
|
102 |
+
"""عرض تبويب جدول الكميات"""
|
103 |
+
st.markdown("### جدول الكميات")
|
104 |
+
|
105 |
+
# عرض البنود الحالية
|
106 |
+
if st.session_state.bill_of_quantities:
|
107 |
+
df = pd.DataFrame(st.session_state.bill_of_quantities)
|
108 |
+
st.dataframe(df, use_container_width=True)
|
109 |
+
|
110 |
+
# إضافة بند جديد
|
111 |
st.markdown("### إضافة بند جديد")
|
112 |
+
|
113 |
col1, col2 = st.columns(2)
|
114 |
with col1:
|
115 |
+
code = st.text_input("كود البند")
|
116 |
+
description = st.text_area("وصف البند")
|
117 |
+
|
118 |
with col2:
|
119 |
+
unit = st.selectbox("الوحدة", ["م3", "م2", "متر طولي", "عدد"])
|
120 |
+
quantity = st.number_input("الكمية", min_value=0.0)
|
121 |
+
unit_price = st.number_input("سعر الوحدة", min_value=0.0)
|
122 |
+
|
123 |
+
if st.button("إضافة البند"):
|
124 |
+
if code and description and quantity > 0 and unit_price > 0:
|
125 |
+
new_item = {
|
126 |
+
'code': code,
|
127 |
+
'description': description,
|
128 |
+
'unit': unit,
|
129 |
+
'quantity': quantity,
|
130 |
+
'unit_price': unit_price,
|
131 |
+
'total_price': quantity * unit_price
|
132 |
+
}
|
133 |
+
st.session_state.bill_of_quantities.append(new_item)
|
134 |
+
st.success("تم إضافة البند بنجاح")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
st.rerun()
|
136 |
+
|
|
|
137 |
|
138 |
def _render_cost_analysis_tab(self):
|
139 |
st.markdown("### تحليل التكاليف")
|
140 |
+
|
141 |
+
if len(st.session_state.bill_of_quantities) > 0:
|
142 |
+
# تحليل التكاليف حسب الفئة
|
143 |
+
category_costs = {}
|
144 |
+
total_cost = 0
|
145 |
+
|
146 |
+
for item in st.session_state.bill_of_quantities:
|
147 |
+
category = item.get('category', 'غير مصنف') # Handle missing category gracefully
|
148 |
+
cost = item['total_price']
|
149 |
+
|
150 |
+
if category in category_costs:
|
151 |
+
category_costs[category] += cost
|
152 |
+
else:
|
153 |
+
category_costs[category] = cost
|
154 |
+
|
155 |
+
total_cost += cost
|
156 |
+
|
157 |
+
# عرض إجمالي التكاليف
|
158 |
+
st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
|
159 |
+
|
160 |
+
# عرض التكاليف حسب الفئة
|
161 |
+
st.markdown("#### التكاليف حسب الفئة")
|
162 |
+
for category, cost in category_costs.items():
|
163 |
+
percentage = (cost / total_cost) * 100
|
164 |
+
st.write(f"- {category}: {cost:,.2f} ريال ({percentage:.1f}%)")
|
165 |
+
else:
|
166 |
+
st.warning("لا توجد بنود في جدول الكميات")
|
167 |
|
168 |
def _render_pricing_scenarios_tab(self):
|
169 |
st.markdown("### سيناريوهات التسعير")
|
170 |
+
balanced_pricing.render_balanced_strategy()
|
|
|
|
|
|
|
|
|
171 |
|
172 |
def _render_local_content_tab(self):
|
173 |
st.markdown("### المحتوى المحلي")
|
174 |
+
overheads.render_local_content_ui()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/pricing/pricing_engine.py
CHANGED
@@ -1,430 +1,430 @@
|
|
1 |
-
"""
|
2 |
-
وحدة التسعير المتكامل لنظام إدارة المناقصات - Hybrid Face
|
3 |
-
"""
|
4 |
-
|
5 |
-
import os
|
6 |
-
import logging
|
7 |
-
import threading
|
8 |
-
import datetime
|
9 |
-
import json
|
10 |
-
import math
|
11 |
-
from pathlib import Path
|
12 |
-
|
13 |
-
# تهيئة السجل
|
14 |
-
logging.basicConfig(
|
15 |
-
level=logging.INFO,
|
16 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
-
)
|
18 |
-
logger = logging.getLogger('pricing')
|
19 |
-
|
20 |
-
class PricingEngine:
|
21 |
-
"""محرك التسعير المتكامل"""
|
22 |
-
|
23 |
-
def __init__(self, config=None, db=None):
|
24 |
-
"""تهيئة محرك التسعير"""
|
25 |
-
self.config = config
|
26 |
-
self.db = db
|
27 |
-
self.pricing_in_progress = False
|
28 |
-
self.current_project = None
|
29 |
-
self.pricing_results = {}
|
30 |
-
|
31 |
-
# إنشاء مجلد التسعير إذا لم يكن موجوداً
|
32 |
-
if config and hasattr(config, 'EXPORTS_PATH'):
|
33 |
-
self.exports_path = Path(config.EXPORTS_PATH)
|
34 |
-
else:
|
35 |
-
self.exports_path = Path('data/exports')
|
36 |
-
|
37 |
-
if not self.exports_path.exists():
|
38 |
-
self.exports_path.mkdir(parents=True, exist_ok=True)
|
39 |
-
|
40 |
-
def calculate_pricing(self, project_id, strategy="comprehensive", callback=None):
|
41 |
-
"""حساب التسعير للمشروع"""
|
42 |
-
if self.pricing_in_progress:
|
43 |
-
logger.warning("هناك عملية تسعير جارية بالفعل")
|
44 |
-
return False
|
45 |
-
|
46 |
-
self.pricing_in_progress = True
|
47 |
-
self.current_project = project_id
|
48 |
-
self.pricing_results = {
|
49 |
-
"project_id": project_id,
|
50 |
-
"strategy": strategy,
|
51 |
-
"pricing_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
52 |
-
"status": "جاري التسعير",
|
53 |
-
"direct_costs": {},
|
54 |
-
"indirect_costs": {},
|
55 |
-
"risk_costs": {},
|
56 |
-
"summary": {}
|
57 |
-
}
|
58 |
-
|
59 |
-
# بدء التسعير في خيط منفصل
|
60 |
-
thread = threading.Thread(
|
61 |
-
target=self._calculate_pricing_thread,
|
62 |
-
args=(project_id, strategy, callback)
|
63 |
-
)
|
64 |
-
thread.daemon = True
|
65 |
-
thread.start()
|
66 |
-
|
67 |
-
return True
|
68 |
-
|
69 |
-
def _calculate_pricing_thread(self, project_id, strategy, callback):
|
70 |
-
"""خيط حساب التسعير"""
|
71 |
-
try:
|
72 |
-
# محاكاة جلب بيانات المشروع من قاعدة البيانات
|
73 |
-
project_data = self._get_project_data(project_id)
|
74 |
-
|
75 |
-
if not project_data:
|
76 |
-
logger.error(f"لم يتم العثور على بيانات المشروع: {project_id}")
|
77 |
-
self.pricing_results["status"] = "فشل التسعير"
|
78 |
-
self.pricing_results["error"] = "لم يتم العثور على بيانات المشروع"
|
79 |
-
return
|
80 |
-
|
81 |
-
# حساب التكاليف المباشرة
|
82 |
-
self._calculate_direct_costs(project_data)
|
83 |
-
|
84 |
-
# حساب التكاليف غير المباشرة
|
85 |
-
self._calculate_indirect_costs(project_data, strategy)
|
86 |
-
|
87 |
-
# حساب تكاليف المخاطر
|
88 |
-
self._calculate_risk_costs(project_data, strategy)
|
89 |
-
|
90 |
-
# حساب ملخص التسعير
|
91 |
-
self._calculate_pricing_summary(strategy)
|
92 |
-
|
93 |
-
# تحديث حالة التسعير
|
94 |
-
self.pricing_results["status"] = "اكتمل التسعير"
|
95 |
-
self.pricing_results["pricing_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
96 |
-
|
97 |
-
logger.info(f"اكتمل تسعير المشروع: {project_id}")
|
98 |
-
|
99 |
-
except Exception as e:
|
100 |
-
logger.error(f"خطأ في تسعير المشروع: {str(e)}")
|
101 |
-
self.pricing_results["status"] = "فشل التسعير"
|
102 |
-
self.pricing_results["error"] = str(e)
|
103 |
-
|
104 |
-
finally:
|
105 |
-
self.pricing_in_progress = False
|
106 |
-
|
107 |
-
# استدعاء دالة الاستجابة إذا تم توفيرها
|
108 |
-
if callback and callable(callback):
|
109 |
-
callback(self.pricing_results)
|
110 |
-
|
111 |
-
def _get_project_data(self, project_id):
|
112 |
-
"""الحصول على بيانات المشروع"""
|
113 |
-
# في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
|
114 |
-
# هنا نقوم بمحاكاة البيانات للتوضيح
|
115 |
-
|
116 |
-
return {
|
117 |
-
"id": project_id,
|
118 |
-
"name": "مشروع الطرق السريعة",
|
119 |
-
"client": "وزارة النقل",
|
120 |
-
"items": [
|
121 |
-
{"id": 1, "name": "أعمال الحفر", "unit": "م³", "quantity": 1500, "unit_cost": 45},
|
122 |
-
{"id": 2, "name": "أعمال الخرسانة", "unit": "م³", "quantity": 750, "unit_cost": 1200},
|
123 |
-
{"id": 3, "name": "أعمال الأسفلت", "unit": "م²", "quantity": 5000, "unit_cost": 120},
|
124 |
-
{"id": 4, "name": "أعمال الإنارة", "unit": "عدد", "quantity": 50, "unit_cost": 3500}
|
125 |
-
],
|
126 |
-
"resources": {
|
127 |
-
"materials": [
|
128 |
-
{"id": 1, "name": "أسمنت", "unit": "طن", "quantity": 300, "unit_cost": 950},
|
129 |
-
{"id": 2, "name": "حديد تسليح", "unit": "طن", "quantity": 120, "unit_cost": 3200},
|
130 |
-
{"id": 3, "name": "رمل", "unit": "م³", "quantity": 450, "unit_cost": 75},
|
131 |
-
{"id": 4, "name": "أسفلت", "unit": "طن", "quantity": 200, "unit_cost": 1800}
|
132 |
-
],
|
133 |
-
"equipment": [
|
134 |
-
{"id": 1, "name": "حفارة", "unit": "يوم", "quantity": 45, "unit_cost": 1500},
|
135 |
-
{"id": 2, "name": "لودر", "unit": "يوم", "quantity": 30, "unit_cost": 1200},
|
136 |
-
{"id": 3, "name": "شاحنة نقل", "unit": "يوم", "quantity": 60, "unit_cost": 800},
|
137 |
-
{"id": 4, "name": "خلاطة خرسانة", "unit": "يوم", "quantity": 40, "unit_cost": 600}
|
138 |
-
],
|
139 |
-
"labor": [
|
140 |
-
{"id": 1, "name": "عمال", "unit": "يوم", "quantity": 1200, "unit_cost": 150},
|
141 |
-
{"id": 2, "name": "فنيون", "unit": "يوم", "quantity": 600, "unit_cost": 300},
|
142 |
-
{"id": 3, "name": "مهندسون", "unit": "يوم", "quantity": 180, "unit_cost": 800}
|
143 |
-
]
|
144 |
-
},
|
145 |
-
"risks": [
|
146 |
-
{"id": 1, "name": "تأخر توريد المواد", "probability": "متوسط", "impact": "عالي", "cost_impact": 0.05},
|
147 |
-
{"id": 2, "name": "تغير أسعار المواد", "probability": "عالي", "impact": "عالي", "cost_impact": 0.08},
|
148 |
-
{"id": 3, "name": "ظروف جوية غير مواتية", "probability": "منخفض", "impact": "متوسط", "cost_impact": 0.03},
|
149 |
-
{"id": 4, "name": "نقص العمالة", "probability": "متوسط", "impact": "متوسط", "cost_impact": 0.04}
|
150 |
-
],
|
151 |
-
"project_duration": 180, # بالأيام
|
152 |
-
"location": "المنطقة الشرقية"
|
153 |
-
}
|
154 |
-
|
155 |
-
def _calculate_direct_costs(self, project_data):
|
156 |
-
"""حساب التكاليف المباشرة"""
|
157 |
-
# حساب تكاليف البنود
|
158 |
-
items_cost = 0
|
159 |
-
items_details = []
|
160 |
-
|
161 |
-
for item in project_data["items"]:
|
162 |
-
total_cost = item["quantity"] * item["unit_cost"]
|
163 |
-
items_cost += total_cost
|
164 |
-
|
165 |
-
items_details.append({
|
166 |
-
"id": item["id"],
|
167 |
-
"name": item["name"],
|
168 |
-
"unit": item["unit"],
|
169 |
-
"quantity": item["quantity"],
|
170 |
-
"unit_cost": item["unit_cost"],
|
171 |
-
"total_cost": total_cost
|
172 |
-
})
|
173 |
-
|
174 |
-
# حساب تكاليف الموارد
|
175 |
-
materials_cost = 0
|
176 |
-
equipment_cost = 0
|
177 |
-
labor_cost = 0
|
178 |
-
|
179 |
-
for material in project_data["resources"]["materials"]:
|
180 |
-
materials_cost += material["quantity"] * material["unit_cost"]
|
181 |
-
|
182 |
-
for equipment in project_data["resources"]["equipment"]:
|
183 |
-
equipment_cost += equipment["quantity"] * equipment["unit_cost"]
|
184 |
-
|
185 |
-
for labor in project_data["resources"]["labor"]:
|
186 |
-
labor_cost += labor["quantity"] * labor["unit_cost"]
|
187 |
-
|
188 |
-
resources_cost = materials_cost + equipment_cost + labor_cost
|
189 |
-
|
190 |
-
# تخزين نتائج التكاليف المباشرة
|
191 |
-
self.pricing_results["direct_costs"] = {
|
192 |
-
"items": {
|
193 |
-
"total": items_cost,
|
194 |
-
"details": items_details
|
195 |
-
},
|
196 |
-
"resources": {
|
197 |
-
"total": resources_cost,
|
198 |
-
"materials": materials_cost,
|
199 |
-
"equipment": equipment_cost,
|
200 |
-
"labor": labor_cost
|
201 |
-
},
|
202 |
-
"total_direct_costs": items_cost
|
203 |
-
}
|
204 |
-
|
205 |
-
def _calculate_indirect_costs(self, project_data, strategy):
|
206 |
-
"""حساب التكاليف غير المباشرة"""
|
207 |
-
direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
|
208 |
-
|
209 |
-
# تحديد نسب التكاليف غير المباشرة بناءً على استراتيجية التسعير
|
210 |
-
if strategy == "comprehensive":
|
211 |
-
overhead_rate = 0.15 # 15% نفقات عامة
|
212 |
-
profit_rate = 0.10 # 10% ربح
|
213 |
-
admin_rate = 0.05 # 5% تكاليف إدارية
|
214 |
-
elif strategy == "competitive":
|
215 |
-
overhead_rate = 0.12 # 12% نفقات عامة
|
216 |
-
profit_rate = 0.07 # 7% ربح
|
217 |
-
admin_rate = 0.04 # 4% تكاليف إدارية
|
218 |
-
else: # balanced
|
219 |
-
overhead_rate = 0.13 # 13% نفقات عامة
|
220 |
-
profit_rate = 0.08 # 8% ربح
|
221 |
-
admin_rate = 0.045 # 4.5% تكاليف إدارية
|
222 |
-
|
223 |
-
# حساب التكاليف غير المباشرة
|
224 |
-
overhead_cost = direct_costs * overhead_rate
|
225 |
-
profit_cost = direct_costs * profit_rate
|
226 |
-
admin_cost = direct_costs * admin_rate
|
227 |
-
|
228 |
-
# تكاليف إضافية
|
229 |
-
mobilization_cost = direct_costs * 0.03 # 3% تكاليف التجهيز
|
230 |
-
bonds_insurance_cost = direct_costs * 0.02 # 2% تكاليف الضمانات والتأمين
|
231 |
-
|
232 |
-
# إجمالي التكاليف غير المباشرة
|
233 |
-
total_indirect_costs = overhead_cost + profit_cost + admin_cost + mobilization_cost + bonds_insurance_cost
|
234 |
-
|
235 |
-
# تخزين نتائج التكاليف غير المباشرة
|
236 |
-
self.pricing_results["indirect_costs"] = {
|
237 |
-
"overhead": {
|
238 |
-
"rate": overhead_rate,
|
239 |
-
"cost": overhead_cost
|
240 |
-
},
|
241 |
-
"profit": {
|
242 |
-
"rate": profit_rate,
|
243 |
-
"cost": profit_cost
|
244 |
-
},
|
245 |
-
"administrative": {
|
246 |
-
"rate": admin_rate,
|
247 |
-
"cost": admin_cost
|
248 |
-
},
|
249 |
-
"mobilization": {
|
250 |
-
"rate": 0.03,
|
251 |
-
"cost": mobilization_cost
|
252 |
-
},
|
253 |
-
"bonds_insurance": {
|
254 |
-
"rate": 0.02,
|
255 |
-
"cost": bonds_insurance_cost
|
256 |
-
},
|
257 |
-
"total_indirect_costs": total_indirect_costs
|
258 |
-
}
|
259 |
-
|
260 |
-
def _calculate_risk_costs(self, project_data, strategy):
|
261 |
-
"""حساب تكاليف المخاطر"""
|
262 |
-
direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
|
263 |
-
|
264 |
-
# تحويل احتمالية وتأثير المخاطر إلى قيم رقمية
|
265 |
-
probability_map = {
|
266 |
-
"منخفض": 0.3,
|
267 |
-
"متوسط": 0.5,
|
268 |
-
"عالي": 0.7
|
269 |
-
}
|
270 |
-
|
271 |
-
impact_map = {
|
272 |
-
"منخفض": 0.3,
|
273 |
-
"متوسط": 0.5,
|
274 |
-
"عالي": 0.7
|
275 |
-
}
|
276 |
-
|
277 |
-
# حساب تكاليف المخاطر
|
278 |
-
risk_costs = []
|
279 |
-
total_risk_cost = 0
|
280 |
-
|
281 |
-
for risk in project_data["risks"]:
|
282 |
-
probability = probability_map.get(risk["probability"], 0.5)
|
283 |
-
impact = impact_map.get(risk["impact"], 0.5)
|
284 |
-
|
285 |
-
# حساب درجة المخاطرة
|
286 |
-
risk_score = probability * impact
|
287 |
-
|
288 |
-
# حساب تكلفة المخاطرة
|
289 |
-
risk_cost = direct_costs * risk["cost_impact"] * risk_score
|
290 |
-
|
291 |
-
# تعديل تكلفة المخاطرة بناءً على استراتيجية التسعير
|
292 |
-
if strategy == "comprehensive":
|
293 |
-
risk_cost_factor = 1.0 # تغطية كاملة للمخاطر
|
294 |
-
elif strategy == "competitive":
|
295 |
-
risk_cost_factor = 0.7 # تغطية جزئية للمخاطر
|
296 |
-
else: # balanced
|
297 |
-
risk_cost_factor = 0.85 # تغطية متوازنة للمخاطر
|
298 |
-
|
299 |
-
adjusted_risk_cost = risk_cost * risk_cost_factor
|
300 |
-
total_risk_cost += adjusted_risk_cost
|
301 |
-
|
302 |
-
risk_costs.append({
|
303 |
-
"id": risk["id"],
|
304 |
-
"name": risk["name"],
|
305 |
-
"probability": risk["probability"],
|
306 |
-
"impact": risk["impact"],
|
307 |
-
"risk_score": risk_score,
|
308 |
-
"cost_impact": risk["cost_impact"],
|
309 |
-
"risk_cost": risk_cost,
|
310 |
-
"adjusted_risk_cost": adjusted_risk_cost
|
311 |
-
})
|
312 |
-
|
313 |
-
# تخزين نتائج تكاليف المخاطر
|
314 |
-
self.pricing_results["risk_costs"] = {
|
315 |
-
"risks": risk_costs,
|
316 |
-
"total_risk_cost": total_risk_cost,
|
317 |
-
"strategy_factor": 1.0 if strategy == "comprehensive" else (0.7 if strategy == "competitive" else 0.85)
|
318 |
-
}
|
319 |
-
|
320 |
-
def _calculate_pricing_summary(self, strategy):
|
321 |
-
"""حساب ملخص التسعير"""
|
322 |
-
direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
|
323 |
-
indirect_costs = self.pricing_results["indirect_costs"]["total_indirect_costs"]
|
324 |
-
risk_costs = self.pricing_results["risk_costs"]["total_risk_cost"]
|
325 |
-
|
326 |
-
# حساب إجمالي التكاليف
|
327 |
-
total_costs = direct_costs + indirect_costs + risk_costs
|
328 |
-
|
329 |
-
# حساب ضريبة القيمة المضافة (15%)
|
330 |
-
vat = total_costs * 0.15
|
331 |
-
|
332 |
-
# حساب السعر النهائي
|
333 |
-
final_price = total_costs + vat
|
334 |
-
|
335 |
-
# تخزين ملخص التسعير
|
336 |
-
self.pricing_results["summary"] = {
|
337 |
-
"direct_costs": direct_costs,
|
338 |
-
"indirect_costs": indirect_costs,
|
339 |
-
"risk_costs": risk_costs,
|
340 |
-
"total_costs": total_costs,
|
341 |
-
"vat": {
|
342 |
-
"rate": 0.15,
|
343 |
-
"amount": vat
|
344 |
-
},
|
345 |
-
"final_price": final_price,
|
346 |
-
"strategy": strategy,
|
347 |
-
"pricing_notes": self._generate_pricing_notes(strategy)
|
348 |
-
}
|
349 |
-
|
350 |
-
def _generate_pricing_notes(self, strategy):
|
351 |
-
"""توليد ملاحظات التسعير"""
|
352 |
-
if strategy == "comprehensive":
|
353 |
-
return [
|
354 |
-
"تم تطبيق استراتيجية التسعير الشاملة التي تغطي جميع التكاليف والمخاطر",
|
355 |
-
"تم تضمين هامش ربح مناسب (10%) لضمان ربحية المشروع",
|
356 |
-
"تم تغطية جميع المخاطر المحتملة بشكل كامل",
|
357 |
-
"يوصى بمراجعة أسعار المواد قبل تقديم العرض النهائي"
|
358 |
-
]
|
359 |
-
elif strategy == "competitive":
|
360 |
-
return [
|
361 |
-
"تم تطبيق استراتيجية التسعير التنافسية لزيادة فرص الفوز بالمناقصة",
|
362 |
-
"تم تخفيض هامش الربح (7%) لتقديم سعر تنافسي",
|
363 |
-
"تم تغطية المخاطر بشكل جزئي، مما يتطلب إدارة مخاطر فعالة أثناء التنفيذ",
|
364 |
-
"يجب مراقبة التكاليف بدقة أثناء تنفيذ المشروع لضمان الربحية"
|
365 |
-
]
|
366 |
-
else: # balanced
|
367 |
-
return [
|
368 |
-
"تم تطبيق استراتيجية التسعير المتوازنة التي توازن بين الربحية والتنافسية",
|
369 |
-
"تم تضمين هامش ربح معقول (8%) يوازن بين الربحية والتنافسية",
|
370 |
-
"تم تغطية المخاطر الرئيسية بشكل مناسب",
|
371 |
-
"يوصى بمراجعة بنود التكلفة العالية قبل تقديم العرض النهائي"
|
372 |
-
]
|
373 |
-
|
374 |
-
def get_pricing_status(self):
|
375 |
-
"""الحصول على حالة التسعير الحالي"""
|
376 |
-
if not self.pricing_in_progress:
|
377 |
-
if not self.pricing_results:
|
378 |
-
return {"status": "لا يوجد تسعير جارٍ"}
|
379 |
-
else:
|
380 |
-
return {"status": self.pricing_results.get("status", "غير معروف")}
|
381 |
-
|
382 |
-
return {
|
383 |
-
"status": "جاري التسعير",
|
384 |
-
"project_id": self.current_project,
|
385 |
-
"start_time": self.pricing_results.get("pricing_start_time")
|
386 |
-
}
|
387 |
-
|
388 |
-
def get_pricing_results(self):
|
389 |
-
"""الحصول على نتائج التسعير"""
|
390 |
-
return self.pricing_results
|
391 |
-
|
392 |
-
def export_pricing_results(self, output_path=None):
|
393 |
-
"""تصدير نتائج التسعير إلى ملف JSON"""
|
394 |
-
if not self.pricing_results:
|
395 |
-
logger.warning("لا توجد نتائج تسعير للتصدير")
|
396 |
-
return None
|
397 |
-
|
398 |
-
if not output_path:
|
399 |
-
# إنشاء اسم ملف افتراضي
|
400 |
-
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
401 |
-
filename = f"pricing_results_{timestamp}.json"
|
402 |
-
output_path = os.path.join(self.exports_path, filename)
|
403 |
-
|
404 |
-
try:
|
405 |
-
with open(output_path, 'w', encoding='utf-8') as f:
|
406 |
-
json.dump(self.pricing_results, f, ensure_ascii=False, indent=4)
|
407 |
-
|
408 |
-
logger.info(f"تم تصدير نتائج التسعير إلى: {output_path}")
|
409 |
-
return output_path
|
410 |
-
|
411 |
-
except Exception as e:
|
412 |
-
logger.error(f"خطأ في تصدير نتائج التسعير: {str(e)}")
|
413 |
-
return None
|
414 |
-
|
415 |
-
def import_pricing_results(self, input_path):
|
416 |
-
"""استيراد نتائج التسعير من ملف JSON"""
|
417 |
-
if not os.path.exists(input_path):
|
418 |
-
logger.error(f"ملف نتائج التسعير غير موجود: {input_path}")
|
419 |
-
return False
|
420 |
-
|
421 |
-
try:
|
422 |
-
with open(input_path, 'r', encoding='utf-8') as f:
|
423 |
-
self.pricing_results = json.load(f)
|
424 |
-
|
425 |
-
logger.info(f"تم استيراد نتائج التسعير من: {input_path}")
|
426 |
-
return True
|
427 |
-
|
428 |
-
except Exception as e:
|
429 |
-
logger.error(f"خطأ في استيراد نتائج التسعير: {str(e)}")
|
430 |
-
return False
|
|
|
1 |
+
"""
|
2 |
+
وحدة التسعير المتكامل لنظام إدارة المناقصات - Hybrid Face
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
import logging
|
7 |
+
import threading
|
8 |
+
import datetime
|
9 |
+
import json
|
10 |
+
import math
|
11 |
+
from pathlib import Path
|
12 |
+
|
13 |
+
# تهيئة السجل
|
14 |
+
logging.basicConfig(
|
15 |
+
level=logging.INFO,
|
16 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
+
)
|
18 |
+
logger = logging.getLogger('pricing')
|
19 |
+
|
20 |
+
class PricingEngine:
|
21 |
+
"""محرك التسعير المتكامل"""
|
22 |
+
|
23 |
+
def __init__(self, config=None, db=None):
|
24 |
+
"""تهيئة محرك التسعير"""
|
25 |
+
self.config = config
|
26 |
+
self.db = db
|
27 |
+
self.pricing_in_progress = False
|
28 |
+
self.current_project = None
|
29 |
+
self.pricing_results = {}
|
30 |
+
|
31 |
+
# إنشاء مجلد التسعير إذا لم يكن موجوداً
|
32 |
+
if config and hasattr(config, 'EXPORTS_PATH'):
|
33 |
+
self.exports_path = Path(config.EXPORTS_PATH)
|
34 |
+
else:
|
35 |
+
self.exports_path = Path('data/exports')
|
36 |
+
|
37 |
+
if not self.exports_path.exists():
|
38 |
+
self.exports_path.mkdir(parents=True, exist_ok=True)
|
39 |
+
|
40 |
+
def calculate_pricing(self, project_id, strategy="comprehensive", callback=None):
|
41 |
+
"""حساب التسعير للمشروع"""
|
42 |
+
if self.pricing_in_progress:
|
43 |
+
logger.warning("هناك عملية تسعير جارية بالفعل")
|
44 |
+
return False
|
45 |
+
|
46 |
+
self.pricing_in_progress = True
|
47 |
+
self.current_project = project_id
|
48 |
+
self.pricing_results = {
|
49 |
+
"project_id": project_id,
|
50 |
+
"strategy": strategy,
|
51 |
+
"pricing_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
52 |
+
"status": "جاري التسعير",
|
53 |
+
"direct_costs": {},
|
54 |
+
"indirect_costs": {},
|
55 |
+
"risk_costs": {},
|
56 |
+
"summary": {}
|
57 |
+
}
|
58 |
+
|
59 |
+
# بدء التسعير في خيط منفصل
|
60 |
+
thread = threading.Thread(
|
61 |
+
target=self._calculate_pricing_thread,
|
62 |
+
args=(project_id, strategy, callback)
|
63 |
+
)
|
64 |
+
thread.daemon = True
|
65 |
+
thread.start()
|
66 |
+
|
67 |
+
return True
|
68 |
+
|
69 |
+
def _calculate_pricing_thread(self, project_id, strategy, callback):
|
70 |
+
"""خيط حساب التسعير"""
|
71 |
+
try:
|
72 |
+
# محاكاة جلب بيانات المشروع من قاعدة البيانات
|
73 |
+
project_data = self._get_project_data(project_id)
|
74 |
+
|
75 |
+
if not project_data:
|
76 |
+
logger.error(f"لم يتم العثور على بيانات المشروع: {project_id}")
|
77 |
+
self.pricing_results["status"] = "فشل التسعير"
|
78 |
+
self.pricing_results["error"] = "لم يتم العثور على بيانات المشروع"
|
79 |
+
return
|
80 |
+
|
81 |
+
# حساب التكاليف المباشرة
|
82 |
+
self._calculate_direct_costs(project_data)
|
83 |
+
|
84 |
+
# حساب التكاليف غير المباشرة
|
85 |
+
self._calculate_indirect_costs(project_data, strategy)
|
86 |
+
|
87 |
+
# حساب تكاليف المخاطر
|
88 |
+
self._calculate_risk_costs(project_data, strategy)
|
89 |
+
|
90 |
+
# حساب ملخص التسعير
|
91 |
+
self._calculate_pricing_summary(strategy)
|
92 |
+
|
93 |
+
# تحديث حالة التسعير
|
94 |
+
self.pricing_results["status"] = "اكتمل التسعير"
|
95 |
+
self.pricing_results["pricing_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
96 |
+
|
97 |
+
logger.info(f"اكتمل تسعير المشروع: {project_id}")
|
98 |
+
|
99 |
+
except Exception as e:
|
100 |
+
logger.error(f"خطأ في تسعير المشروع: {str(e)}")
|
101 |
+
self.pricing_results["status"] = "فشل التسعير"
|
102 |
+
self.pricing_results["error"] = str(e)
|
103 |
+
|
104 |
+
finally:
|
105 |
+
self.pricing_in_progress = False
|
106 |
+
|
107 |
+
# استدعاء دالة الاستجابة إذا تم توفيرها
|
108 |
+
if callback and callable(callback):
|
109 |
+
callback(self.pricing_results)
|
110 |
+
|
111 |
+
def _get_project_data(self, project_id):
|
112 |
+
"""الحصول على بيانات المشروع"""
|
113 |
+
# في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
|
114 |
+
# هنا نقوم بمحاكاة البيانات للتوضيح
|
115 |
+
|
116 |
+
return {
|
117 |
+
"id": project_id,
|
118 |
+
"name": "مشروع الطرق السريعة",
|
119 |
+
"client": "وزارة النقل",
|
120 |
+
"items": [
|
121 |
+
{"id": 1, "name": "أعمال الحفر", "unit": "م³", "quantity": 1500, "unit_cost": 45},
|
122 |
+
{"id": 2, "name": "أعمال الخرسانة", "unit": "م³", "quantity": 750, "unit_cost": 1200},
|
123 |
+
{"id": 3, "name": "أعمال الأسفلت", "unit": "م²", "quantity": 5000, "unit_cost": 120},
|
124 |
+
{"id": 4, "name": "أعمال الإنارة", "unit": "عدد", "quantity": 50, "unit_cost": 3500}
|
125 |
+
],
|
126 |
+
"resources": {
|
127 |
+
"materials": [
|
128 |
+
{"id": 1, "name": "أسمنت", "unit": "طن", "quantity": 300, "unit_cost": 950},
|
129 |
+
{"id": 2, "name": "حديد تسليح", "unit": "طن", "quantity": 120, "unit_cost": 3200},
|
130 |
+
{"id": 3, "name": "رمل", "unit": "م³", "quantity": 450, "unit_cost": 75},
|
131 |
+
{"id": 4, "name": "أسفلت", "unit": "طن", "quantity": 200, "unit_cost": 1800}
|
132 |
+
],
|
133 |
+
"equipment": [
|
134 |
+
{"id": 1, "name": "حفارة", "unit": "يوم", "quantity": 45, "unit_cost": 1500},
|
135 |
+
{"id": 2, "name": "لودر", "unit": "يوم", "quantity": 30, "unit_cost": 1200},
|
136 |
+
{"id": 3, "name": "شاحنة نقل", "unit": "يوم", "quantity": 60, "unit_cost": 800},
|
137 |
+
{"id": 4, "name": "خلاطة خرسانة", "unit": "يوم", "quantity": 40, "unit_cost": 600}
|
138 |
+
],
|
139 |
+
"labor": [
|
140 |
+
{"id": 1, "name": "عمال", "unit": "يوم", "quantity": 1200, "unit_cost": 150},
|
141 |
+
{"id": 2, "name": "فنيون", "unit": "يوم", "quantity": 600, "unit_cost": 300},
|
142 |
+
{"id": 3, "name": "مهندسون", "unit": "يوم", "quantity": 180, "unit_cost": 800}
|
143 |
+
]
|
144 |
+
},
|
145 |
+
"risks": [
|
146 |
+
{"id": 1, "name": "تأخر توريد المواد", "probability": "متوسط", "impact": "عالي", "cost_impact": 0.05},
|
147 |
+
{"id": 2, "name": "تغير أسعار المواد", "probability": "عالي", "impact": "عالي", "cost_impact": 0.08},
|
148 |
+
{"id": 3, "name": "ظروف جوية غير مواتية", "probability": "منخفض", "impact": "متوسط", "cost_impact": 0.03},
|
149 |
+
{"id": 4, "name": "نقص العمالة", "probability": "متوسط", "impact": "متوسط", "cost_impact": 0.04}
|
150 |
+
],
|
151 |
+
"project_duration": 180, # بالأيام
|
152 |
+
"location": "المنطقة الشرقية"
|
153 |
+
}
|
154 |
+
|
155 |
+
def _calculate_direct_costs(self, project_data):
|
156 |
+
"""حساب التكاليف المباشرة"""
|
157 |
+
# حساب تكاليف البنود
|
158 |
+
items_cost = 0
|
159 |
+
items_details = []
|
160 |
+
|
161 |
+
for item in project_data["items"]:
|
162 |
+
total_cost = item["quantity"] * item["unit_cost"]
|
163 |
+
items_cost += total_cost
|
164 |
+
|
165 |
+
items_details.append({
|
166 |
+
"id": item["id"],
|
167 |
+
"name": item["name"],
|
168 |
+
"unit": item["unit"],
|
169 |
+
"quantity": item["quantity"],
|
170 |
+
"unit_cost": item["unit_cost"],
|
171 |
+
"total_cost": total_cost
|
172 |
+
})
|
173 |
+
|
174 |
+
# حساب تكاليف الموارد
|
175 |
+
materials_cost = 0
|
176 |
+
equipment_cost = 0
|
177 |
+
labor_cost = 0
|
178 |
+
|
179 |
+
for material in project_data["resources"]["materials"]:
|
180 |
+
materials_cost += material["quantity"] * material["unit_cost"]
|
181 |
+
|
182 |
+
for equipment in project_data["resources"]["equipment"]:
|
183 |
+
equipment_cost += equipment["quantity"] * equipment["unit_cost"]
|
184 |
+
|
185 |
+
for labor in project_data["resources"]["labor"]:
|
186 |
+
labor_cost += labor["quantity"] * labor["unit_cost"]
|
187 |
+
|
188 |
+
resources_cost = materials_cost + equipment_cost + labor_cost
|
189 |
+
|
190 |
+
# تخزين نتائج التكاليف المباشرة
|
191 |
+
self.pricing_results["direct_costs"] = {
|
192 |
+
"items": {
|
193 |
+
"total": items_cost,
|
194 |
+
"details": items_details
|
195 |
+
},
|
196 |
+
"resources": {
|
197 |
+
"total": resources_cost,
|
198 |
+
"materials": materials_cost,
|
199 |
+
"equipment": equipment_cost,
|
200 |
+
"labor": labor_cost
|
201 |
+
},
|
202 |
+
"total_direct_costs": items_cost
|
203 |
+
}
|
204 |
+
|
205 |
+
def _calculate_indirect_costs(self, project_data, strategy):
|
206 |
+
"""حساب التكاليف غير المباشرة"""
|
207 |
+
direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
|
208 |
+
|
209 |
+
# تحديد نسب التكاليف غير المباشرة بناءً على استراتيجية التسعير
|
210 |
+
if strategy == "comprehensive":
|
211 |
+
overhead_rate = 0.15 # 15% نفقات عامة
|
212 |
+
profit_rate = 0.10 # 10% ربح
|
213 |
+
admin_rate = 0.05 # 5% تكاليف إدارية
|
214 |
+
elif strategy == "competitive":
|
215 |
+
overhead_rate = 0.12 # 12% نفقات عامة
|
216 |
+
profit_rate = 0.07 # 7% ربح
|
217 |
+
admin_rate = 0.04 # 4% تكاليف إدارية
|
218 |
+
else: # balanced
|
219 |
+
overhead_rate = 0.13 # 13% نفقات عامة
|
220 |
+
profit_rate = 0.08 # 8% ربح
|
221 |
+
admin_rate = 0.045 # 4.5% تكاليف إدارية
|
222 |
+
|
223 |
+
# حساب التكاليف غير المباشرة
|
224 |
+
overhead_cost = direct_costs * overhead_rate
|
225 |
+
profit_cost = direct_costs * profit_rate
|
226 |
+
admin_cost = direct_costs * admin_rate
|
227 |
+
|
228 |
+
# تكاليف إضافية
|
229 |
+
mobilization_cost = direct_costs * 0.03 # 3% تكاليف التجهيز
|
230 |
+
bonds_insurance_cost = direct_costs * 0.02 # 2% تكاليف الضمانات والتأمين
|
231 |
+
|
232 |
+
# إجمالي التكاليف غير المباشرة
|
233 |
+
total_indirect_costs = overhead_cost + profit_cost + admin_cost + mobilization_cost + bonds_insurance_cost
|
234 |
+
|
235 |
+
# تخزين نتائج التكاليف غير المباشرة
|
236 |
+
self.pricing_results["indirect_costs"] = {
|
237 |
+
"overhead": {
|
238 |
+
"rate": overhead_rate,
|
239 |
+
"cost": overhead_cost
|
240 |
+
},
|
241 |
+
"profit": {
|
242 |
+
"rate": profit_rate,
|
243 |
+
"cost": profit_cost
|
244 |
+
},
|
245 |
+
"administrative": {
|
246 |
+
"rate": admin_rate,
|
247 |
+
"cost": admin_cost
|
248 |
+
},
|
249 |
+
"mobilization": {
|
250 |
+
"rate": 0.03,
|
251 |
+
"cost": mobilization_cost
|
252 |
+
},
|
253 |
+
"bonds_insurance": {
|
254 |
+
"rate": 0.02,
|
255 |
+
"cost": bonds_insurance_cost
|
256 |
+
},
|
257 |
+
"total_indirect_costs": total_indirect_costs
|
258 |
+
}
|
259 |
+
|
260 |
+
def _calculate_risk_costs(self, project_data, strategy):
|
261 |
+
"""حساب تكاليف المخاطر"""
|
262 |
+
direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
|
263 |
+
|
264 |
+
# تحويل احتمالية وتأثير المخاطر إلى قيم رقمية
|
265 |
+
probability_map = {
|
266 |
+
"منخفض": 0.3,
|
267 |
+
"متوسط": 0.5,
|
268 |
+
"عالي": 0.7
|
269 |
+
}
|
270 |
+
|
271 |
+
impact_map = {
|
272 |
+
"منخفض": 0.3,
|
273 |
+
"متوسط": 0.5,
|
274 |
+
"عالي": 0.7
|
275 |
+
}
|
276 |
+
|
277 |
+
# حساب تكاليف المخاطر
|
278 |
+
risk_costs = []
|
279 |
+
total_risk_cost = 0
|
280 |
+
|
281 |
+
for risk in project_data["risks"]:
|
282 |
+
probability = probability_map.get(risk["probability"], 0.5)
|
283 |
+
impact = impact_map.get(risk["impact"], 0.5)
|
284 |
+
|
285 |
+
# حساب درجة المخاطرة
|
286 |
+
risk_score = probability * impact
|
287 |
+
|
288 |
+
# حساب تكلفة المخاطرة
|
289 |
+
risk_cost = direct_costs * risk["cost_impact"] * risk_score
|
290 |
+
|
291 |
+
# تعديل تكلفة المخاطرة بناءً على استراتيجية التسعير
|
292 |
+
if strategy == "comprehensive":
|
293 |
+
risk_cost_factor = 1.0 # تغطية كاملة للمخاطر
|
294 |
+
elif strategy == "competitive":
|
295 |
+
risk_cost_factor = 0.7 # تغطية جزئية للمخاطر
|
296 |
+
else: # balanced
|
297 |
+
risk_cost_factor = 0.85 # تغطية متوازنة للمخاطر
|
298 |
+
|
299 |
+
adjusted_risk_cost = risk_cost * risk_cost_factor
|
300 |
+
total_risk_cost += adjusted_risk_cost
|
301 |
+
|
302 |
+
risk_costs.append({
|
303 |
+
"id": risk["id"],
|
304 |
+
"name": risk["name"],
|
305 |
+
"probability": risk["probability"],
|
306 |
+
"impact": risk["impact"],
|
307 |
+
"risk_score": risk_score,
|
308 |
+
"cost_impact": risk["cost_impact"],
|
309 |
+
"risk_cost": risk_cost,
|
310 |
+
"adjusted_risk_cost": adjusted_risk_cost
|
311 |
+
})
|
312 |
+
|
313 |
+
# تخزين نتائج تكاليف المخاطر
|
314 |
+
self.pricing_results["risk_costs"] = {
|
315 |
+
"risks": risk_costs,
|
316 |
+
"total_risk_cost": total_risk_cost,
|
317 |
+
"strategy_factor": 1.0 if strategy == "comprehensive" else (0.7 if strategy == "competitive" else 0.85)
|
318 |
+
}
|
319 |
+
|
320 |
+
def _calculate_pricing_summary(self, strategy):
|
321 |
+
"""حساب ملخص التسعير"""
|
322 |
+
direct_costs = self.pricing_results["direct_costs"]["total_direct_costs"]
|
323 |
+
indirect_costs = self.pricing_results["indirect_costs"]["total_indirect_costs"]
|
324 |
+
risk_costs = self.pricing_results["risk_costs"]["total_risk_cost"]
|
325 |
+
|
326 |
+
# حساب إجمالي التكاليف
|
327 |
+
total_costs = direct_costs + indirect_costs + risk_costs
|
328 |
+
|
329 |
+
# حساب ضريبة القيمة المضافة (15%)
|
330 |
+
vat = total_costs * 0.15
|
331 |
+
|
332 |
+
# حساب السعر النهائي
|
333 |
+
final_price = total_costs + vat
|
334 |
+
|
335 |
+
# تخزين ملخص التسعير
|
336 |
+
self.pricing_results["summary"] = {
|
337 |
+
"direct_costs": direct_costs,
|
338 |
+
"indirect_costs": indirect_costs,
|
339 |
+
"risk_costs": risk_costs,
|
340 |
+
"total_costs": total_costs,
|
341 |
+
"vat": {
|
342 |
+
"rate": 0.15,
|
343 |
+
"amount": vat
|
344 |
+
},
|
345 |
+
"final_price": final_price,
|
346 |
+
"strategy": strategy,
|
347 |
+
"pricing_notes": self._generate_pricing_notes(strategy)
|
348 |
+
}
|
349 |
+
|
350 |
+
def _generate_pricing_notes(self, strategy):
|
351 |
+
"""توليد ملاحظات التسعير"""
|
352 |
+
if strategy == "comprehensive":
|
353 |
+
return [
|
354 |
+
"تم تطبيق استراتيجية التسعير الشاملة التي تغطي جميع التكاليف والمخاطر",
|
355 |
+
"تم تضمين هامش ربح مناسب (10%) لضمان ربحية المشروع",
|
356 |
+
"تم تغطية جميع المخاطر المحتملة بشكل كامل",
|
357 |
+
"يوصى بمراجعة أسعار المواد قبل تقديم العرض النهائي"
|
358 |
+
]
|
359 |
+
elif strategy == "competitive":
|
360 |
+
return [
|
361 |
+
"تم تطبيق استراتيجية التسعير التنافسية لزيادة فرص الفوز بالمناقصة",
|
362 |
+
"تم تخفيض هامش الربح (7%) لتقديم سعر تنافسي",
|
363 |
+
"تم تغطية المخاطر بشكل جزئي، مما يتطلب إدارة مخاطر فعالة أثناء التنفيذ",
|
364 |
+
"يجب مراقبة التكاليف بدقة أثناء تنفيذ المشروع لضمان الربحية"
|
365 |
+
]
|
366 |
+
else: # balanced
|
367 |
+
return [
|
368 |
+
"تم تطبيق استراتيجية التسعير المتوازنة التي توازن بين الربحية والتنافسية",
|
369 |
+
"تم تضمين هامش ربح معقول (8%) يوازن بين الربحية والتنافسية",
|
370 |
+
"تم تغطية المخاطر الرئيسية بشكل مناسب",
|
371 |
+
"يوصى بمراجعة بنود التكلفة العالية قبل تقديم العرض النهائي"
|
372 |
+
]
|
373 |
+
|
374 |
+
def get_pricing_status(self):
|
375 |
+
"""الحصول على حالة التسعير الحالي"""
|
376 |
+
if not self.pricing_in_progress:
|
377 |
+
if not self.pricing_results:
|
378 |
+
return {"status": "لا يوجد تسعير جارٍ"}
|
379 |
+
else:
|
380 |
+
return {"status": self.pricing_results.get("status", "غير معروف")}
|
381 |
+
|
382 |
+
return {
|
383 |
+
"status": "جاري التسعير",
|
384 |
+
"project_id": self.current_project,
|
385 |
+
"start_time": self.pricing_results.get("pricing_start_time")
|
386 |
+
}
|
387 |
+
|
388 |
+
def get_pricing_results(self):
|
389 |
+
"""الحصول على نتائج التسعير"""
|
390 |
+
return self.pricing_results
|
391 |
+
|
392 |
+
def export_pricing_results(self, output_path=None):
|
393 |
+
"""تصدير نتائج التسعير إلى ملف JSON"""
|
394 |
+
if not self.pricing_results:
|
395 |
+
logger.warning("لا توجد نتائج تسعير للتصدير")
|
396 |
+
return None
|
397 |
+
|
398 |
+
if not output_path:
|
399 |
+
# إنشاء اسم ملف افتراضي
|
400 |
+
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
401 |
+
filename = f"pricing_results_{timestamp}.json"
|
402 |
+
output_path = os.path.join(self.exports_path, filename)
|
403 |
+
|
404 |
+
try:
|
405 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
406 |
+
json.dump(self.pricing_results, f, ensure_ascii=False, indent=4)
|
407 |
+
|
408 |
+
logger.info(f"تم تصدير نتائج التسعير إلى: {output_path}")
|
409 |
+
return output_path
|
410 |
+
|
411 |
+
except Exception as e:
|
412 |
+
logger.error(f"خطأ في تصدير نتائج التسعير: {str(e)}")
|
413 |
+
return None
|
414 |
+
|
415 |
+
def import_pricing_results(self, input_path):
|
416 |
+
"""استيراد نتائج التسعير من ملف JSON"""
|
417 |
+
if not os.path.exists(input_path):
|
418 |
+
logger.error(f"ملف نتائج التسعير غير موجود: {input_path}")
|
419 |
+
return False
|
420 |
+
|
421 |
+
try:
|
422 |
+
with open(input_path, 'r', encoding='utf-8') as f:
|
423 |
+
self.pricing_results = json.load(f)
|
424 |
+
|
425 |
+
logger.info(f"تم استيراد نتائج التسعير من: {input_path}")
|
426 |
+
return True
|
427 |
+
|
428 |
+
except Exception as e:
|
429 |
+
logger.error(f"خطأ في استيراد نتائج التسعير: {str(e)}")
|
430 |
+
return False
|
modules/project_management/project_management_app.py
CHANGED
@@ -1,666 +1,666 @@
|
|
1 |
-
"""
|
2 |
-
وحدة إدارة المشاريع - نظام تحليل المناقصات
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
import pandas as pd
|
7 |
-
import numpy as np
|
8 |
-
from datetime import datetime, timedelta
|
9 |
-
import os
|
10 |
-
import time
|
11 |
-
import io
|
12 |
-
import sys
|
13 |
-
from pathlib import Path
|
14 |
-
|
15 |
-
# إضافة مسار المشروع للنظام
|
16 |
-
sys.path.append(str(Path(__file__).parent.parent))
|
17 |
-
|
18 |
-
# استيراد محسن واجهة المستخدم
|
19 |
-
from styling.enhanced_ui import UIEnhancer
|
20 |
-
|
21 |
-
class ProjectsApp:
|
22 |
-
"""وحدة إدارة المشاريع"""
|
23 |
-
|
24 |
-
def __init__(self):
|
25 |
-
"""تهيئة وحدة إدارة المشاريع"""
|
26 |
-
self.ui = UIEnhancer(page_title="إدارة المشاريع - نظام تحليل المناقصات", page_icon="📋")
|
27 |
-
self.ui.apply_theme_colors()
|
28 |
-
|
29 |
-
# تهيئة البيانات المبدئية
|
30 |
-
if 'projects' not in st.session_state:
|
31 |
-
st.session_state.projects = self._generate_sample_projects()
|
32 |
-
|
33 |
-
def run(self):
|
34 |
-
"""تشغيل وحدة إدارة المشاريع"""
|
35 |
-
# إنشاء قائمة العناصر
|
36 |
-
menu_items = [
|
37 |
-
{"name": "لوحة المعلومات", "icon": "house"},
|
38 |
-
{"name": "المناقصات والعقود", "icon": "file-text"},
|
39 |
-
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
40 |
-
{"name": "نظام التسعير", "icon": "calculator"},
|
41 |
-
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
42 |
-
{"name": "الموارد والتكاليف", "icon": "people"},
|
43 |
-
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
44 |
-
{"name": "إدارة المشاريع", "icon": "kanban"},
|
45 |
-
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
46 |
-
{"name": "الجدول الزمني", "icon": "calendar3"},
|
47 |
-
{"name": "الإشعارات", "icon": "bell"},
|
48 |
-
{"name": "مقارنة المستندات", "icon": "files"},
|
49 |
-
{"name": "الترجمة", "icon": "translate"},
|
50 |
-
{"name": "المساعد الذكي", "icon": "robot"},
|
51 |
-
{"name": "التقارير", "icon": "bar-chart"},
|
52 |
-
{"name": "الإعدادات", "icon": "gear"}
|
53 |
-
]
|
54 |
-
|
55 |
-
# إنشاء الشريط الجانبي
|
56 |
-
selected = self.ui.create_sidebar(menu_items)
|
57 |
-
|
58 |
-
# إنشاء ترويسة الصفحة
|
59 |
-
self.ui.create_header("إدارة المشاريع", "إدارة ومتابعة المشاريع والمناقصات")
|
60 |
-
|
61 |
-
# عرض واجهة وحدة إدارة المشاريع
|
62 |
-
tabs = st.tabs([
|
63 |
-
"قائمة المشاريع",
|
64 |
-
"إضافة مشروع جديد",
|
65 |
-
"تفاصيل المشروع",
|
66 |
-
"متابعة المشاريع"
|
67 |
-
])
|
68 |
-
|
69 |
-
with tabs[0]:
|
70 |
-
self._render_projects_list_tab()
|
71 |
-
|
72 |
-
with tabs[1]:
|
73 |
-
self._render_add_project_tab()
|
74 |
-
|
75 |
-
with tabs[2]:
|
76 |
-
self._render_project_details_tab()
|
77 |
-
|
78 |
-
with tabs[3]:
|
79 |
-
self._render_projects_tracking_tab()
|
80 |
-
|
81 |
-
def _render_projects_list_tab(self):
|
82 |
-
"""عرض تبويب قائمة المشاريع"""
|
83 |
-
|
84 |
-
st.markdown("### قائمة المشاريع")
|
85 |
-
|
86 |
-
# فلترة المشاريع
|
87 |
-
col1, col2, col3 = st.columns(3)
|
88 |
-
|
89 |
-
with col1:
|
90 |
-
search_term = st.text_input("البحث في المشاريع", key="project_search")
|
91 |
-
|
92 |
-
with col2:
|
93 |
-
status_filter = st.multiselect(
|
94 |
-
"حالة المشروع",
|
95 |
-
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
96 |
-
default=["جديد", "قيد التسعير", "تم التقديم"],
|
97 |
-
key="project_status_filter"
|
98 |
-
)
|
99 |
-
|
100 |
-
with col3:
|
101 |
-
client_filter = st.multiselect(
|
102 |
-
"الجهة المالكة",
|
103 |
-
list(set([p['client'] for p in st.session_state.projects])),
|
104 |
-
key="project_client_filter"
|
105 |
-
)
|
106 |
-
|
107 |
-
# تطبيق الفلترة
|
108 |
-
filtered_projects = st.session_state.projects
|
109 |
-
|
110 |
-
if search_term:
|
111 |
-
filtered_projects = [p for p in filtered_projects if search_term.lower() in p['name'].lower() or search_term in p['number']]
|
112 |
-
|
113 |
-
if status_filter:
|
114 |
-
filtered_projects = [p for p in filtered_projects if p['status'] in status_filter]
|
115 |
-
|
116 |
-
if client_filter:
|
117 |
-
filtered_projects = [p for p in filtered_projects if p['client'] in client_filter]
|
118 |
-
|
119 |
-
# تحويل المشاريع المفلترة إلى DataFrame للعرض
|
120 |
-
if filtered_projects:
|
121 |
-
projects_df = pd.DataFrame(filtered_projects)
|
122 |
-
|
123 |
-
# اختيار
|
124 |
-
display_columns = [
|
125 |
-
'name', 'number', 'client', 'location', 'status',
|
126 |
-
'submission_date', 'tender_type', 'created_at'
|
127 |
-
]
|
128 |
-
|
129 |
-
# تغيير أسماء الأعمدة للعرض
|
130 |
-
column_names = {
|
131 |
-
'name': 'اسم المشروع',
|
132 |
-
'number': 'رقم المناقصة',
|
133 |
-
'client': 'الجهة المالكة',
|
134 |
-
'location': 'الموقع',
|
135 |
-
'status': 'الحالة',
|
136 |
-
'submission_date': 'تاريخ التقديم',
|
137 |
-
'tender_type': 'نوع المناقصة',
|
138 |
-
'created_at': 'تاريخ الإنشاء'
|
139 |
-
}
|
140 |
-
|
141 |
-
display_df = projects_df[display_columns].rename(columns=column_names)
|
142 |
-
|
143 |
-
# تنسيق التواريخ
|
144 |
-
date_columns = ['تاريخ التقديم', 'تاريخ الإنشاء']
|
145 |
-
for col in date_columns:
|
146 |
-
if col in display_df.columns:
|
147 |
-
display_df[col] = pd.to_datetime(display_df[col]).dt.strftime('%Y-%m-%d')
|
148 |
-
|
149 |
-
# عرض الجدول
|
150 |
-
st.dataframe(display_df, use_container_width=True, hide_index=True)
|
151 |
-
|
152 |
-
# زر تصدير المشاريع
|
153 |
-
if st.button("تصدير المشاريع إلى Excel"):
|
154 |
-
# محاكاة التصدير
|
155 |
-
st.success("تم تصدير المشاريع بنجاح!")
|
156 |
-
else:
|
157 |
-
st.info("لا توجد مشاريع تطابق معايير البحث.")
|
158 |
-
|
159 |
-
def _render_add_project_tab(self):
|
160 |
-
"""عرض تبويب إضافة مشروع جديد"""
|
161 |
-
|
162 |
-
st.markdown("### إضافة مشروع جديد")
|
163 |
-
|
164 |
-
# نموذج إدخال بيانات المشروع
|
165 |
-
with st.form("new_project_form"):
|
166 |
-
col1, col2 = st.columns(2)
|
167 |
-
|
168 |
-
with col1:
|
169 |
-
project_name = st.text_input("اسم المشروع", key="new_project_name")
|
170 |
-
client = st.text_input("الجهة المالكة", key="new_project_client")
|
171 |
-
location = st.text_input("الموقع", key="new_project_location")
|
172 |
-
tender_type = st.selectbox(
|
173 |
-
"نوع المناقصة",
|
174 |
-
["عامة", "خاصة", "أمر مباشر"],
|
175 |
-
key="new_project_tender_type"
|
176 |
-
)
|
177 |
-
|
178 |
-
with col2:
|
179 |
-
tender_number = st.text_input("رقم المناقصة", key="new_project_number")
|
180 |
-
submission_date = st.date_input("تاريخ التقديم", key="new_project_submission_date")
|
181 |
-
pricing_method = st.selectbox(
|
182 |
-
"طريقة التسعير",
|
183 |
-
["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
|
184 |
-
key="new_project_pricing_method"
|
185 |
-
)
|
186 |
-
status = st.selectbox(
|
187 |
-
"حالة المشروع",
|
188 |
-
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
189 |
-
index=0,
|
190 |
-
key="new_project_status"
|
191 |
-
)
|
192 |
-
|
193 |
-
description = st.text_area("وصف المشروع", key="new_project_description")
|
194 |
-
|
195 |
-
submitted = st.form_submit_button("إضافة المشروع")
|
196 |
-
|
197 |
-
if submitted:
|
198 |
-
# التحقق من تعبئة الحقول الإلزامية
|
199 |
-
if not project_name or not tender_number or not client:
|
200 |
-
st.error("يرجى تعبئة جميع الحقول الإلزامية (اسم المشروع، رقم المناقصة، الجهة المالكة).")
|
201 |
-
else:
|
202 |
-
# إنشاء مشروع جديد
|
203 |
-
new_project = {
|
204 |
-
'id': len(st.session_state.projects) + 1,
|
205 |
-
'name': project_name,
|
206 |
-
'number': tender_number,
|
207 |
-
'client': client,
|
208 |
-
'location': location,
|
209 |
-
'description': description,
|
210 |
-
'status': status,
|
211 |
-
'tender_type': tender_type,
|
212 |
-
'pricing_method': pricing_method,
|
213 |
-
'submission_date': submission_date,
|
214 |
-
'created_at': datetime.now(),
|
215 |
-
'created_by_id': 1 # معرف المستخدم الحالي
|
216 |
-
}
|
217 |
-
|
218 |
-
# إضافة المشروع إلى قائمة المشاريع
|
219 |
-
st.session_state.projects.append(new_project)
|
220 |
-
|
221 |
-
# رسالة نجاح
|
222 |
-
st.success(f"تم إضافة المشروع [{project_name}] بنجاح!")
|
223 |
-
|
224 |
-
# تعيين المشروع الحالي
|
225 |
-
st.session_state.current_project = new_project
|
226 |
-
|
227 |
-
def _render_project_details_tab(self):
|
228 |
-
"""عرض تبويب تفاصيل المشروع"""
|
229 |
-
|
230 |
-
st.markdown("### تفاصيل المشروع")
|
231 |
-
|
232 |
-
# التحقق من وجود مشروع حالي
|
233 |
-
if 'current_project' not in st.session_state or st.session_state.current_project is None:
|
234 |
-
# إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع
|
235 |
-
project_names = [p['name'] for p in st.session_state.projects]
|
236 |
-
selected_project_name = st.selectbox("اختر المشروع", project_names)
|
237 |
-
|
238 |
-
if selected_project_name:
|
239 |
-
selected_project = next((p for p in st.session_state.projects if p['name'] == selected_project_name), None)
|
240 |
-
if selected_project:
|
241 |
-
st.session_state.current_project = selected_project
|
242 |
-
else:
|
243 |
-
st.warning("لم يتم العثور على المشروع المحدد.")
|
244 |
-
return
|
245 |
-
else:
|
246 |
-
st.info("يرجى اختيار مشروع لعرض تفاصيله.")
|
247 |
-
return
|
248 |
-
|
249 |
-
# عرض تفاصيل المشروع
|
250 |
-
project = st.session_state.current_project
|
251 |
-
|
252 |
-
# عرض معلومات المشروع الأساسية
|
253 |
-
col1, col2, col3 = st.columns(3)
|
254 |
-
|
255 |
-
with col1:
|
256 |
-
st.markdown(f"**اسم المشروع**: {project['name']}")
|
257 |
-
st.markdown(f"**رقم المناقصة**: {project['number']}")
|
258 |
-
st.markdown(f"**الجهة المالكة**: {project['client']}")
|
259 |
-
|
260 |
-
with col2:
|
261 |
-
st.markdown(f"**الموقع**: {project['location']}")
|
262 |
-
st.markdown(f"**نوع المناقصة**: {project['tender_type']}")
|
263 |
-
st.markdown(f"**حالة المشروع**: {project['status']}")
|
264 |
-
|
265 |
-
with col3:
|
266 |
-
st.markdown(f"**طريقة التسعير**: {project['pricing_method']}")
|
267 |
-
st.markdown(f"**تاريخ التقديم**: {project['submission_date'].strftime('%Y-%m-%d') if isinstance(project['submission_date'], datetime) else project['submission_date']}")
|
268 |
-
st.markdown(f"**تاريخ الإنشاء**: {project['created_at'].strftime('%Y-%m-%d') if isinstance(project['created_at'], datetime) else project['created_at']}")
|
269 |
-
|
270 |
-
# عرض وصف المشروع
|
271 |
-
st.markdown("#### وصف المشروع")
|
272 |
-
st.text_area("", value=project.get('description', ''), disabled=True, height=100)
|
273 |
-
|
274 |
-
# عرض المستندات المرتبطة بالمشروع
|
275 |
-
st.markdown("#### مستندات المشروع")
|
276 |
-
|
277 |
-
if 'documents' in project and project['documents']:
|
278 |
-
docs_df = pd.DataFrame(project['documents'])
|
279 |
-
st.dataframe(docs_df, use_container_width=True, hide_index=True)
|
280 |
-
else:
|
281 |
-
st.info("لا توجد مستندات مرتبطة بهذا المشروع حاليًا.")
|
282 |
-
|
283 |
-
# زر إضافة مستندات
|
284 |
-
if st.button("إضافة مستندات"):
|
285 |
-
st.session_state.upload_documents = True
|
286 |
-
|
287 |
-
# واجهة تحميل المستندات
|
288 |
-
if 'upload_documents' in st.session_state and st.session_state.upload_documents:
|
289 |
-
st.markdown("#### تحميل مستندات جديدة")
|
290 |
-
|
291 |
-
uploaded_file = st.file_uploader("اختر ملفًا", type=['pdf', 'docx', 'xlsx', 'png', 'jpg', 'dwg'])
|
292 |
-
doc_type = st.selectbox("نوع المستند", ["كراسة شروط", "عقد", "مخططات", "جدول كميات", "مواصفات فنية", "تعديلات وملاحق"])
|
293 |
-
|
294 |
-
if uploaded_file and st.button("تحميل المستند"):
|
295 |
-
# محاكاة تحميل المستند
|
296 |
-
with st.spinner("جاري تحميل المستند..."):
|
297 |
-
time.sleep(2)
|
298 |
-
|
299 |
-
# إنشاء مستند جديد
|
300 |
-
new_document = {
|
301 |
-
'filename': uploaded_file.name,
|
302 |
-
'type': doc_type,
|
303 |
-
'upload_date': datetime.now().strftime('%Y-%m-%d'),
|
304 |
-
'size': f"{uploaded_file.size / 1024:.1f} KB"
|
305 |
-
}
|
306 |
-
|
307 |
-
# إضافة المستند إلى المشروع
|
308 |
-
if 'documents' not in project:
|
309 |
-
project['documents'] = []
|
310 |
-
|
311 |
-
project['documents'].append(new_document)
|
312 |
-
|
313 |
-
st.success(f"تم تحميل المستند [{uploaded_file.name}] بنجاح!")
|
314 |
-
st.session_state.upload_documents = False
|
315 |
-
st.experimental_rerun()
|
316 |
-
|
317 |
-
# عرض البنود والكميات
|
318 |
-
st.markdown("#### بنود وكميات المشروع")
|
319 |
-
|
320 |
-
if 'items' in project and project['items']:
|
321 |
-
items_df = pd.DataFrame(project['items'])
|
322 |
-
st.dataframe(items_df, use_container_width=True, hide_index=True)
|
323 |
-
|
324 |
-
# زر لتحويل البنود إلى وحدة التسعير
|
325 |
-
if st.button("تحويل البنود إلى وحدة التسعير"):
|
326 |
-
if 'manual_items' not in st.session_state:
|
327 |
-
st.session_state.manual_items = pd.DataFrame()
|
328 |
-
|
329 |
-
st.session_state.manual_items = items_df.copy()
|
330 |
-
st.success("تم تحويل البنود إلى وحدة التسعير بنجاح!")
|
331 |
-
else:
|
332 |
-
st.info("لا توجد بنود وكميات لهذا المشروع حاليًا.")
|
333 |
-
|
334 |
-
# زر استيراد البنود من وحدة تحليل المستندات
|
335 |
-
if st.button("استيراد البنود من تحليل المستندات"):
|
336 |
-
st.warning("ميزة استيراد البنود من تحليل المستندات قيد التطوير.")
|
337 |
-
|
338 |
-
# أزرار الإجراءات
|
339 |
-
col1, col2, col3 = st.columns(3)
|
340 |
-
|
341 |
-
with col1:
|
342 |
-
if st.button("تعديل المشروع"):
|
343 |
-
st.session_state.edit_project = True
|
344 |
-
st.experimental_rerun()
|
345 |
-
|
346 |
-
with col2:
|
347 |
-
if st.button("تصدير بيانات المشروع"):
|
348 |
-
st.success("تم تصدير بيانات المشروع بنجاح!")
|
349 |
-
|
350 |
-
with col3:
|
351 |
-
if st.button("إرسال للاعتماد"):
|
352 |
-
st.success("تم إرسال المشروع للاعتماد بنجاح!")
|
353 |
-
|
354 |
-
# نموذج تعديل المشروع
|
355 |
-
if 'edit_project' in st.session_state and st.session_state.edit_project:
|
356 |
-
st.markdown("#### تعديل المشروع")
|
357 |
-
|
358 |
-
with st.form("edit_project_form"):
|
359 |
-
col1, col2 = st.columns(2)
|
360 |
-
|
361 |
-
with col1:
|
362 |
-
project_name = st.text_input("اسم المشروع", value=project['name'])
|
363 |
-
client = st.text_input("الجهة المالكة", value=project['client'])
|
364 |
-
location = st.text_input("الموقع", value=project['location'])
|
365 |
-
tender_type = st.selectbox(
|
366 |
-
"نوع المناقصة",
|
367 |
-
["عامة", "خاصة", "أمر مباشر"],
|
368 |
-
index=["عامة", "خاصة", "أمر مباشر"].index(project['tender_type'])
|
369 |
-
)
|
370 |
-
|
371 |
-
with col2:
|
372 |
-
tender_number = st.text_input("رقم المناقصة", value=project['number'])
|
373 |
-
submission_date = st.date_input(
|
374 |
-
"تاريخ التقديم",
|
375 |
-
value=datetime.strptime(project['submission_date'], "%Y-%m-%d") if isinstance(project['submission_date'], str) else project['submission_date']
|
376 |
-
)
|
377 |
-
pricing_method = st.selectbox(
|
378 |
-
"طريقة التسعير",
|
379 |
-
["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
|
380 |
-
index=["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"].index(project['pricing_method'])
|
381 |
-
)
|
382 |
-
status = st.selectbox(
|
383 |
-
"حالة المشروع",
|
384 |
-
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
385 |
-
index=["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"].index(project['status'])
|
386 |
-
)
|
387 |
-
|
388 |
-
description = st.text_area("وصف المشروع", value=project.get('description', ''))
|
389 |
-
|
390 |
-
col1, col2 = st.columns(2)
|
391 |
-
|
392 |
-
with col1:
|
393 |
-
submit = st.form_submit_button("حفظ التعديلات")
|
394 |
-
|
395 |
-
with col2:
|
396 |
-
cancel = st.form_submit_button("إلغاء")
|
397 |
-
|
398 |
-
if submit:
|
399 |
-
# تحديث بيانات المشروع
|
400 |
-
project['name'] = project_name
|
401 |
-
project['number'] = tender_number
|
402 |
-
project['client'] = client
|
403 |
-
project['location'] = location
|
404 |
-
project['description'] = description
|
405 |
-
project['status'] = status
|
406 |
-
project['tender_type'] = tender_type
|
407 |
-
project['pricing_method'] = pricing_method
|
408 |
-
project['submission_date'] = submission_date
|
409 |
-
|
410 |
-
st.success("تم تحديث بيانات المشروع بنجاح!")
|
411 |
-
st.session_state.edit_project = False
|
412 |
-
st.experimental_rerun()
|
413 |
-
|
414 |
-
elif cancel:
|
415 |
-
st.session_state.edit_project = False
|
416 |
-
st.experimental_rerun()
|
417 |
-
|
418 |
-
def _render_projects_tracking_tab(self):
|
419 |
-
"""عرض تبويب متابعة المشاريع"""
|
420 |
-
|
421 |
-
st.markdown("### متابعة المشاريع")
|
422 |
-
|
423 |
-
# عرض إحصائيات المشاريع
|
424 |
-
col1, col2, col3, col4 = st.columns(4)
|
425 |
-
|
426 |
-
projects = st.session_state.projects
|
427 |
-
|
428 |
-
with col1:
|
429 |
-
total_projects = len(projects)
|
430 |
-
self.ui.create_metric_card("إجمالي المشاريع", str(total_projects), None, self.ui.COLORS['primary'])
|
431 |
-
|
432 |
-
with col2:
|
433 |
-
active_projects = len([p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]])
|
434 |
-
self.ui.create_metric_card("المشاريع النشطة", str(active_projects), None, self.ui.COLORS['success'])
|
435 |
-
|
436 |
-
with col3:
|
437 |
-
pending_submission = len([p for p in projects if p['status'] in ["جديد", "قيد التسعير"]])
|
438 |
-
self.ui.create_metric_card("مشاريع قيد التسعير", str(pending_submission), None, self.ui.COLORS['warning'])
|
439 |
-
|
440 |
-
with col4:
|
441 |
-
completed_projects = len([p for p in projects if p['status'] in ["منتهي"]])
|
442 |
-
self.ui.create_metric_card("المشاريع المنتهية", str(completed_projects), None, self.ui.COLORS['info'])
|
443 |
-
|
444 |
-
# عرض رسم بياني لحالة المشاريع
|
445 |
-
st.markdown("#### توزيع المشاريع حسب الحالة")
|
446 |
-
|
447 |
-
status_counts = {}
|
448 |
-
for p in projects:
|
449 |
-
status = p['status']
|
450 |
-
status_counts[status] = status_counts.get(status, 0) + 1
|
451 |
-
|
452 |
-
status_df = pd.DataFrame({
|
453 |
-
'الحالة': list(status_counts.keys()),
|
454 |
-
'عدد المشاريع': list(status_counts.values())
|
455 |
-
})
|
456 |
-
|
457 |
-
st.bar_chart(status_df.set_index('الحالة'))
|
458 |
-
|
459 |
-
# عرض المشاريع قيد المتابعة
|
460 |
-
st.markdown("#### المشاريع قيد المتابعة")
|
461 |
-
|
462 |
-
# عرض المشاريع النشطة المرتبة حسب تاريخ التقديم
|
463 |
-
active_projects_list = [p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]]
|
464 |
-
|
465 |
-
if active_projects_list:
|
466 |
-
# تحويل التواريخ إلى كائنات تاريخ إذا كانت نصوصًا
|
467 |
-
for p in active_projects_list:
|
468 |
-
if isinstance(p['submission_date'], str):
|
469 |
-
p['submission_date'] = datetime.strptime(p['submission_date'], "%Y-%m-%d")
|
470 |
-
|
471 |
-
# ترتيب المشاريع حسب تاريخ التقديم
|
472 |
-
active_projects_list.sort(key=lambda x: x['submission_date'])
|
473 |
-
|
474 |
-
# تحويل إلى DataFrame
|
475 |
-
active_df = pd.DataFrame(active_projects_list)
|
476 |
-
|
477 |
-
# اختيار وترتيب الأعمدة
|
478 |
-
display_columns = [
|
479 |
-
'name', 'number', 'client', 'status',
|
480 |
-
'submission_date', 'tender_type'
|
481 |
-
]
|
482 |
-
|
483 |
-
# تغيير أسماء الأعمدة
|
484 |
-
column_names = {
|
485 |
-
'name': 'اسم المشروع',
|
486 |
-
'number': 'رقم المناقصة',
|
487 |
-
'client': 'الجهة المالكة',
|
488 |
-
'status': 'الحالة',
|
489 |
-
'submission_date': 'تاريخ التقديم',
|
490 |
-
'tender_type': 'نوع المناقصة'
|
491 |
-
}
|
492 |
-
|
493 |
-
# تنسيق البيانات
|
494 |
-
display_df = active_df[display_columns].rename(columns=column_names)
|
495 |
-
display_df['تاريخ التقديم'] = pd.to_datetime(display_df['تاريخ التقديم']).dt.strftime('%Y-%m-%d')
|
496 |
-
|
497 |
-
# عرض الجدول
|
498 |
-
st.dataframe(display_df, use_container_width=True, hide_index=True)
|
499 |
-
else:
|
500 |
-
st.info("لا توجد مشاريع نشطة حاليًا.")
|
501 |
-
|
502 |
-
# عرض المشاريع المقبلة
|
503 |
-
st.markdown("#### المواعيد المقبلة")
|
504 |
-
|
505 |
-
upcoming_events = []
|
506 |
-
today = datetime.now().date()
|
507 |
-
|
508 |
-
for p in projects:
|
509 |
-
submission_date = p['submission_date']
|
510 |
-
if isinstance(submission_date, str):
|
511 |
-
submission_date = datetime.strptime(submission_date, "%Y-%m-%d").date()
|
512 |
-
elif isinstance(submission_date, datetime):
|
513 |
-
submission_date = submission_date.date()
|
514 |
-
|
515 |
-
# المشاريع التي موعد تقديمها خلال الأسبوعين القادمين
|
516 |
-
if today <= submission_date <= today + timedelta(days=14) and p['status'] in ["قيد التسعير"]:
|
517 |
-
days_left = (submission_date - today).days
|
518 |
-
upcoming_events.append({
|
519 |
-
'المشروع': p['name'],
|
520 |
-
'الحدث': 'موعد تقديم المناقصة',
|
521 |
-
'التاريخ': submission_date.strftime('%Y-%m-%d'),
|
522 |
-
'الأيام المتبقية': days_left
|
523 |
-
})
|
524 |
-
|
525 |
-
if upcoming_events:
|
526 |
-
events_df = pd.DataFrame(upcoming_events)
|
527 |
-
st.dataframe(events_df, use_container_width=True, hide_index=True)
|
528 |
-
else:
|
529 |
-
st.info("لا توجد مواعيد قريبة.")
|
530 |
-
|
531 |
-
def _generate_sample_projects(self):
|
532 |
-
"""توليد بيانات افتراضية للمشاريع"""
|
533 |
-
|
534 |
-
projects = [
|
535 |
-
{
|
536 |
-
'id': 1,
|
537 |
-
'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية",
|
538 |
-
'number': "SHPD-2025-001",
|
539 |
-
'client': "وزارة الصحة",
|
540 |
-
'location': "الدمام، المنطقة الشرقية",
|
541 |
-
'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.",
|
542 |
-
'status': "قيد التسعير",
|
543 |
-
'tender_type': "عامة",
|
544 |
-
'pricing_method': "قياسي",
|
545 |
-
'submission_date': (datetime.now() + timedelta(days=5)),
|
546 |
-
'created_at': datetime.now() - timedelta(days=10),
|
547 |
-
'created_by_id': 1,
|
548 |
-
'documents': [
|
549 |
-
{
|
550 |
-
'filename': "كراسة الشروط والمواصفات.pdf",
|
551 |
-
'type': "كراسة شروط",
|
552 |
-
'upload_date': (datetime.now() - timedelta(days=9)).strftime('%Y-%m-%d'),
|
553 |
-
'size': "5.2 MB"
|
554 |
-
},
|
555 |
-
{
|
556 |
-
'filename': "المخططات الهندسية.dwg",
|
557 |
-
'type': "مخططات",
|
558 |
-
'upload_date': (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'),
|
559 |
-
'size': "25.7 MB"
|
560 |
-
},
|
561 |
-
{
|
562 |
-
'filename': "جدول الكميات.xlsx",
|
563 |
-
'type': "جدول كميات",
|
564 |
-
'upload_date': (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'),
|
565 |
-
'size': "1.8 MB"
|
566 |
-
}
|
567 |
-
],
|
568 |
-
'items': [
|
569 |
-
{
|
570 |
-
'رقم البند': "A1",
|
571 |
-
'وصف البند': "أعمال الحفر والردم",
|
572 |
-
'الوحدة': "م3",
|
573 |
-
'الكمية': 12500
|
574 |
-
},
|
575 |
-
{
|
576 |
-
'رقم البند': "A2",
|
577 |
-
'وصف البند': "أعمال الخرسانة المسلحة للأساسات",
|
578 |
-
'الوحدة': "م3",
|
579 |
-
'الكمية': 3500
|
580 |
-
},
|
581 |
-
{
|
582 |
-
'رقم البند': "A3",
|
583 |
-
'وصف البند': "أعمال حديد التسليح",
|
584 |
-
'الوحدة': "طن",
|
585 |
-
'الكمية': 450
|
586 |
-
}
|
587 |
-
]
|
588 |
-
},
|
589 |
-
{
|
590 |
-
'id': 2,
|
591 |
-
'name': "صيانة وتطوير طريق الملك عبدالله",
|
592 |
-
'number': "MOT-2025-042",
|
593 |
-
'client': "وزارة النقل",
|
594 |
-
'location': "الرياض، المنطقة الوسطى",
|
595 |
-
'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.",
|
596 |
-
'status': "تم التقديم",
|
597 |
-
'tender_type': "عامة",
|
598 |
-
'pricing_method': "غير متزن",
|
599 |
-
'submission_date': (datetime.now() - timedelta(days=15)),
|
600 |
-
'created_at': datetime.now() - timedelta(days=45),
|
601 |
-
'created_by_id': 1
|
602 |
-
},
|
603 |
-
{
|
604 |
-
'id': 3,
|
605 |
-
'name': "إنشاء محطة معالجة مياه الصرف الصحي",
|
606 |
-
'number': "SWPC-2025-007",
|
607 |
-
'client': "شركة المياه الوطنية",
|
608 |
-
'location': "جدة، المنطقة الغربية",
|
609 |
-
'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.",
|
610 |
-
'status': "تمت الترسية",
|
611 |
-
'tender_type': "عامة",
|
612 |
-
'pricing_method': "قياسي",
|
613 |
-
'submission_date': (datetime.now() - timedelta(days=90)),
|
614 |
-
'created_at': datetime.now() - timedelta(days=120),
|
615 |
-
'created_by_id': 1
|
616 |
-
},
|
617 |
-
{
|
618 |
-
'id': 4,
|
619 |
-
'name': "إنشاء منتزه الملك سلمان",
|
620 |
-
'number': "RAM-2025-015",
|
621 |
-
'client': "أمانة منطقة الرياض",
|
622 |
-
'location': "الرياض، المنطقة الوسطى",
|
623 |
-
'description': "إنشاء منتزه الملك سلمان على مساحة 500,000 متر مربع، ويشمل المشروع أعمال التشجير والتنسيق والمسطحات المائية والمباني الخدمية.",
|
624 |
-
'status': "قيد التنفيذ",
|
625 |
-
'tender_type': "عامة",
|
626 |
-
'pricing_method': "قياسي",
|
627 |
-
'submission_date': (datetime.now() - timedelta(days=180)),
|
628 |
-
'created_at': datetime.now() - timedelta(days=210),
|
629 |
-
'created_by_id': 1
|
630 |
-
},
|
631 |
-
{
|
632 |
-
'id': 5,
|
633 |
-
'name': "إنشاء مبنى مختبرات كلية العلوم",
|
634 |
-
'number': "KSU-2025-032",
|
635 |
-
'client': "جامعة الملك سعود",
|
636 |
-
'location': "الرياض، المنطقة الوسطى",
|
637 |
-
'description': "إنشاء مبنى المختبرات الجديد لكلية العلوم بمساحة 8,000 متر مربع، ويتكون من 3 طوابق ويشمل تجهيز المعامل والمختبرات العلمية.",
|
638 |
-
'status': "جديد",
|
639 |
-
'tender_type': "خاصة",
|
640 |
-
'pricing_method': "تنافسي",
|
641 |
-
'submission_date': (datetime.now() + timedelta(days=10)),
|
642 |
-
'created_at': datetime.now() - timedelta(days=5),
|
643 |
-
'created_by_id': 1
|
644 |
-
},
|
645 |
-
{
|
646 |
-
'id': 6,
|
647 |
-
'name': "توريد وتركيب أنظمة الطاقة الشمسية",
|
648 |
-
'number': "SEC-2025-098",
|
649 |
-
'client': "الشركة السعودية للكهرباء",
|
650 |
-
'location': "تبوك، المنطقة الشمالية",
|
651 |
-
'description': "توريد وتركيب أنظمة الطاقة الشمسية بقدرة 5 ميجاوات، مع جميع الأعمال المدنية والكهربائية.",
|
652 |
-
'status': "جديد",
|
653 |
-
'tender_type': "عامة",
|
654 |
-
'pricing_method': "قياسي",
|
655 |
-
'submission_date': (datetime.now() + timedelta(days=20)),
|
656 |
-
'created_at': datetime.now() - timedelta(days=2),
|
657 |
-
'created_by_id': 1
|
658 |
-
}
|
659 |
-
]
|
660 |
-
|
661 |
-
return projects
|
662 |
-
|
663 |
-
# تشغيل التطبيق
|
664 |
-
if __name__ == "__main__":
|
665 |
-
projects_app = ProjectsApp()
|
666 |
-
projects_app.run()
|
|
|
1 |
+
"""
|
2 |
+
وحدة إدارة المشاريع - نظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
from datetime import datetime, timedelta
|
9 |
+
import os
|
10 |
+
import time
|
11 |
+
import io
|
12 |
+
import sys
|
13 |
+
from pathlib import Path
|
14 |
+
|
15 |
+
# إضافة مسار المشروع للنظام
|
16 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
17 |
+
|
18 |
+
# استيراد محسن واجهة المستخدم
|
19 |
+
from styling.enhanced_ui import UIEnhancer
|
20 |
+
|
21 |
+
class ProjectsApp:
|
22 |
+
"""وحدة إدارة المشاريع"""
|
23 |
+
|
24 |
+
def __init__(self):
|
25 |
+
"""تهيئة وحدة إدارة المشاريع"""
|
26 |
+
self.ui = UIEnhancer(page_title="إدارة المشاريع - نظام تحليل المناقصات", page_icon="📋")
|
27 |
+
self.ui.apply_theme_colors()
|
28 |
+
|
29 |
+
# تهيئة البيانات المبدئية
|
30 |
+
if 'projects' not in st.session_state:
|
31 |
+
st.session_state.projects = self._generate_sample_projects()
|
32 |
+
|
33 |
+
def run(self):
|
34 |
+
"""تشغيل وحدة إدارة المشاريع"""
|
35 |
+
# إنشاء قائمة العناصر
|
36 |
+
menu_items = [
|
37 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
38 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
39 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
40 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
41 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
42 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
43 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
44 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
45 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
46 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
47 |
+
{"name": "الإشعارات", "icon": "bell"},
|
48 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
49 |
+
{"name": "الترجمة", "icon": "translate"},
|
50 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
51 |
+
{"name": "التقارير", "icon": "bar-chart"},
|
52 |
+
{"name": "الإعدادات", "icon": "gear"}
|
53 |
+
]
|
54 |
+
|
55 |
+
# إنشاء الشريط الجانبي
|
56 |
+
selected = self.ui.create_sidebar(menu_items)
|
57 |
+
|
58 |
+
# إنشاء ترويسة الصفحة
|
59 |
+
self.ui.create_header("إدارة المشاريع", "إدارة ومتابعة المشاريع والمناقصات")
|
60 |
+
|
61 |
+
# عرض واجهة وحدة إدارة المشاريع
|
62 |
+
tabs = st.tabs([
|
63 |
+
"قائمة المشاريع",
|
64 |
+
"إضافة مشروع جديد",
|
65 |
+
"تفاصيل المشروع",
|
66 |
+
"متابعة المشاريع"
|
67 |
+
])
|
68 |
+
|
69 |
+
with tabs[0]:
|
70 |
+
self._render_projects_list_tab()
|
71 |
+
|
72 |
+
with tabs[1]:
|
73 |
+
self._render_add_project_tab()
|
74 |
+
|
75 |
+
with tabs[2]:
|
76 |
+
self._render_project_details_tab()
|
77 |
+
|
78 |
+
with tabs[3]:
|
79 |
+
self._render_projects_tracking_tab()
|
80 |
+
|
81 |
+
def _render_projects_list_tab(self):
|
82 |
+
"""عرض تبويب قائمة المشاريع"""
|
83 |
+
|
84 |
+
st.markdown("### قائمة المشاريع")
|
85 |
+
|
86 |
+
# فلترة المشاريع
|
87 |
+
col1, col2, col3 = st.columns(3)
|
88 |
+
|
89 |
+
with col1:
|
90 |
+
search_term = st.text_input("البحث في المشاريع", key="project_search")
|
91 |
+
|
92 |
+
with col2:
|
93 |
+
status_filter = st.multiselect(
|
94 |
+
"حالة المشروع",
|
95 |
+
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
96 |
+
default=["جديد", "قيد التسعير", "تم التقديم"],
|
97 |
+
key="project_status_filter"
|
98 |
+
)
|
99 |
+
|
100 |
+
with col3:
|
101 |
+
client_filter = st.multiselect(
|
102 |
+
"الجهة المالكة",
|
103 |
+
list(set([p['client'] for p in st.session_state.projects])),
|
104 |
+
key="project_client_filter"
|
105 |
+
)
|
106 |
+
|
107 |
+
# تطبيق الفلترة
|
108 |
+
filtered_projects = st.session_state.projects
|
109 |
+
|
110 |
+
if search_term:
|
111 |
+
filtered_projects = [p for p in filtered_projects if search_term.lower() in p['name'].lower() or search_term in p['number']]
|
112 |
+
|
113 |
+
if status_filter:
|
114 |
+
filtered_projects = [p for p in filtered_projects if p['status'] in status_filter]
|
115 |
+
|
116 |
+
if client_filter:
|
117 |
+
filtered_projects = [p for p in filtered_projects if p['client'] in client_filter]
|
118 |
+
|
119 |
+
# تحويل المشاريع المفلترة إلى DataFrame للعرض
|
120 |
+
if filtered_projects:
|
121 |
+
projects_df = pd.DataFrame(filtered_projects)
|
122 |
+
|
123 |
+
# اختيار وترتيب الأعمدة
|
124 |
+
display_columns = [
|
125 |
+
'name', 'number', 'client', 'location', 'status',
|
126 |
+
'submission_date', 'tender_type', 'created_at'
|
127 |
+
]
|
128 |
+
|
129 |
+
# تغيير أسماء الأعمدة للعرض
|
130 |
+
column_names = {
|
131 |
+
'name': 'اسم المشروع',
|
132 |
+
'number': 'رقم المناقصة',
|
133 |
+
'client': 'الجهة المالكة',
|
134 |
+
'location': 'الموقع',
|
135 |
+
'status': 'الحالة',
|
136 |
+
'submission_date': 'تاريخ التقديم',
|
137 |
+
'tender_type': 'نوع المناقصة',
|
138 |
+
'created_at': 'تاريخ الإنشاء'
|
139 |
+
}
|
140 |
+
|
141 |
+
display_df = projects_df[display_columns].rename(columns=column_names)
|
142 |
+
|
143 |
+
# تنسيق التواريخ
|
144 |
+
date_columns = ['تاريخ التقديم', 'تاريخ الإنشاء']
|
145 |
+
for col in date_columns:
|
146 |
+
if col in display_df.columns:
|
147 |
+
display_df[col] = pd.to_datetime(display_df[col]).dt.strftime('%Y-%m-%d')
|
148 |
+
|
149 |
+
# عرض الجدول
|
150 |
+
st.dataframe(display_df, use_container_width=True, hide_index=True)
|
151 |
+
|
152 |
+
# زر تصدير المشاريع
|
153 |
+
if st.button("تصدير المشاريع إلى Excel"):
|
154 |
+
# محاكاة التصدير
|
155 |
+
st.success("تم تصدير المشاريع بنجاح!")
|
156 |
+
else:
|
157 |
+
st.info("لا توجد مشاريع تطابق معايير البحث.")
|
158 |
+
|
159 |
+
def _render_add_project_tab(self):
|
160 |
+
"""عرض تبويب إضافة مشروع جديد"""
|
161 |
+
|
162 |
+
st.markdown("### إضافة مشروع جديد")
|
163 |
+
|
164 |
+
# نموذج إدخال بيانات المشروع
|
165 |
+
with st.form("new_project_form"):
|
166 |
+
col1, col2 = st.columns(2)
|
167 |
+
|
168 |
+
with col1:
|
169 |
+
project_name = st.text_input("اسم المشروع", key="new_project_name")
|
170 |
+
client = st.text_input("الجهة المالكة", key="new_project_client")
|
171 |
+
location = st.text_input("الموقع", key="new_project_location")
|
172 |
+
tender_type = st.selectbox(
|
173 |
+
"نوع المناقصة",
|
174 |
+
["عامة", "خاصة", "أمر مباشر"],
|
175 |
+
key="new_project_tender_type"
|
176 |
+
)
|
177 |
+
|
178 |
+
with col2:
|
179 |
+
tender_number = st.text_input("رقم المناقصة", key="new_project_number")
|
180 |
+
submission_date = st.date_input("تاريخ التقديم", key="new_project_submission_date")
|
181 |
+
pricing_method = st.selectbox(
|
182 |
+
"طريقة التسعير",
|
183 |
+
["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
|
184 |
+
key="new_project_pricing_method"
|
185 |
+
)
|
186 |
+
status = st.selectbox(
|
187 |
+
"حالة المشروع",
|
188 |
+
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
189 |
+
index=0,
|
190 |
+
key="new_project_status"
|
191 |
+
)
|
192 |
+
|
193 |
+
description = st.text_area("وصف المشروع", key="new_project_description")
|
194 |
+
|
195 |
+
submitted = st.form_submit_button("إضافة المشروع")
|
196 |
+
|
197 |
+
if submitted:
|
198 |
+
# التحقق من تعبئة الحقول الإلزامية
|
199 |
+
if not project_name or not tender_number or not client:
|
200 |
+
st.error("يرجى تعبئة جميع الحقول الإلزامية (اسم المشروع، رقم المناقصة، الجهة المالكة).")
|
201 |
+
else:
|
202 |
+
# إنشاء مشروع جديد
|
203 |
+
new_project = {
|
204 |
+
'id': len(st.session_state.projects) + 1,
|
205 |
+
'name': project_name,
|
206 |
+
'number': tender_number,
|
207 |
+
'client': client,
|
208 |
+
'location': location,
|
209 |
+
'description': description,
|
210 |
+
'status': status,
|
211 |
+
'tender_type': tender_type,
|
212 |
+
'pricing_method': pricing_method,
|
213 |
+
'submission_date': submission_date,
|
214 |
+
'created_at': datetime.now(),
|
215 |
+
'created_by_id': 1 # معرف المستخدم الحالي
|
216 |
+
}
|
217 |
+
|
218 |
+
# إضافة المشروع إلى قائمة المشاريع
|
219 |
+
st.session_state.projects.append(new_project)
|
220 |
+
|
221 |
+
# رسالة نجاح
|
222 |
+
st.success(f"تم إضافة المشروع [{project_name}] بنجاح!")
|
223 |
+
|
224 |
+
# تعيين المشروع الحالي
|
225 |
+
st.session_state.current_project = new_project
|
226 |
+
|
227 |
+
def _render_project_details_tab(self):
|
228 |
+
"""عرض تبويب تفاصيل المشروع"""
|
229 |
+
|
230 |
+
st.markdown("### تفاصيل المشروع")
|
231 |
+
|
232 |
+
# التحقق من وجود مشروع حالي
|
233 |
+
if 'current_project' not in st.session_state or st.session_state.current_project is None:
|
234 |
+
# إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع
|
235 |
+
project_names = [p['name'] for p in st.session_state.projects]
|
236 |
+
selected_project_name = st.selectbox("اختر المشروع", project_names)
|
237 |
+
|
238 |
+
if selected_project_name:
|
239 |
+
selected_project = next((p for p in st.session_state.projects if p['name'] == selected_project_name), None)
|
240 |
+
if selected_project:
|
241 |
+
st.session_state.current_project = selected_project
|
242 |
+
else:
|
243 |
+
st.warning("لم يتم العثور على المشروع المحدد.")
|
244 |
+
return
|
245 |
+
else:
|
246 |
+
st.info("يرجى اختيار مشروع لعرض تفاصيله.")
|
247 |
+
return
|
248 |
+
|
249 |
+
# عرض تفاصيل المشروع
|
250 |
+
project = st.session_state.current_project
|
251 |
+
|
252 |
+
# عرض معلومات المشروع الأساسية
|
253 |
+
col1, col2, col3 = st.columns(3)
|
254 |
+
|
255 |
+
with col1:
|
256 |
+
st.markdown(f"**اسم المشروع**: {project['name']}")
|
257 |
+
st.markdown(f"**رقم المناقصة**: {project['number']}")
|
258 |
+
st.markdown(f"**الجهة المالكة**: {project['client']}")
|
259 |
+
|
260 |
+
with col2:
|
261 |
+
st.markdown(f"**الموقع**: {project['location']}")
|
262 |
+
st.markdown(f"**نوع المناقصة**: {project['tender_type']}")
|
263 |
+
st.markdown(f"**حالة المشروع**: {project['status']}")
|
264 |
+
|
265 |
+
with col3:
|
266 |
+
st.markdown(f"**طريقة التسعير**: {project['pricing_method']}")
|
267 |
+
st.markdown(f"**تاريخ التقديم**: {project['submission_date'].strftime('%Y-%m-%d') if isinstance(project['submission_date'], datetime) else project['submission_date']}")
|
268 |
+
st.markdown(f"**تاريخ الإنشاء**: {project['created_at'].strftime('%Y-%m-%d') if isinstance(project['created_at'], datetime) else project['created_at']}")
|
269 |
+
|
270 |
+
# عرض وصف المشروع
|
271 |
+
st.markdown("#### وصف المشروع")
|
272 |
+
st.text_area("", value=project.get('description', ''), disabled=True, height=100)
|
273 |
+
|
274 |
+
# عرض المستندات المرتبطة بالمشروع
|
275 |
+
st.markdown("#### مستندات المشروع")
|
276 |
+
|
277 |
+
if 'documents' in project and project['documents']:
|
278 |
+
docs_df = pd.DataFrame(project['documents'])
|
279 |
+
st.dataframe(docs_df, use_container_width=True, hide_index=True)
|
280 |
+
else:
|
281 |
+
st.info("لا توجد مستندات مرتبطة بهذا المشروع حاليًا.")
|
282 |
+
|
283 |
+
# زر إضافة مستندات
|
284 |
+
if st.button("إضافة مستندات"):
|
285 |
+
st.session_state.upload_documents = True
|
286 |
+
|
287 |
+
# واجهة تحميل المستندات
|
288 |
+
if 'upload_documents' in st.session_state and st.session_state.upload_documents:
|
289 |
+
st.markdown("#### تحميل مستندات جديدة")
|
290 |
+
|
291 |
+
uploaded_file = st.file_uploader("اختر ملفًا", type=['pdf', 'docx', 'xlsx', 'png', 'jpg', 'dwg'])
|
292 |
+
doc_type = st.selectbox("نوع المستند", ["كراسة شروط", "عقد", "مخططات", "جدول كميات", "مواصفات فنية", "تعديلات وملاحق"])
|
293 |
+
|
294 |
+
if uploaded_file and st.button("تحميل المستند"):
|
295 |
+
# محاكاة تحميل المستند
|
296 |
+
with st.spinner("جاري تحميل المستند..."):
|
297 |
+
time.sleep(2)
|
298 |
+
|
299 |
+
# إنشاء مستند جديد
|
300 |
+
new_document = {
|
301 |
+
'filename': uploaded_file.name,
|
302 |
+
'type': doc_type,
|
303 |
+
'upload_date': datetime.now().strftime('%Y-%m-%d'),
|
304 |
+
'size': f"{uploaded_file.size / 1024:.1f} KB"
|
305 |
+
}
|
306 |
+
|
307 |
+
# إضافة المستند إلى المشروع
|
308 |
+
if 'documents' not in project:
|
309 |
+
project['documents'] = []
|
310 |
+
|
311 |
+
project['documents'].append(new_document)
|
312 |
+
|
313 |
+
st.success(f"تم تحميل المستند [{uploaded_file.name}] بنجاح!")
|
314 |
+
st.session_state.upload_documents = False
|
315 |
+
st.experimental_rerun()
|
316 |
+
|
317 |
+
# عرض البنود والكميات
|
318 |
+
st.markdown("#### بنود وكميات المشروع")
|
319 |
+
|
320 |
+
if 'items' in project and project['items']:
|
321 |
+
items_df = pd.DataFrame(project['items'])
|
322 |
+
st.dataframe(items_df, use_container_width=True, hide_index=True)
|
323 |
+
|
324 |
+
# زر لتحويل البنود إلى وحدة التسعير
|
325 |
+
if st.button("تحويل البنود إلى وحدة التسعير"):
|
326 |
+
if 'manual_items' not in st.session_state:
|
327 |
+
st.session_state.manual_items = pd.DataFrame()
|
328 |
+
|
329 |
+
st.session_state.manual_items = items_df.copy()
|
330 |
+
st.success("تم تحويل البنود إلى وحدة التسعير بنجاح!")
|
331 |
+
else:
|
332 |
+
st.info("لا توجد بنود وكميات لهذا المشروع حاليًا.")
|
333 |
+
|
334 |
+
# زر استيراد البنود من وحدة تحليل المستندات
|
335 |
+
if st.button("استيراد البنود من تحليل المستندات"):
|
336 |
+
st.warning("ميزة استيراد البنود من تحليل المستندات قيد التطوير.")
|
337 |
+
|
338 |
+
# أزرار الإجراءات
|
339 |
+
col1, col2, col3 = st.columns(3)
|
340 |
+
|
341 |
+
with col1:
|
342 |
+
if st.button("تعديل المشروع"):
|
343 |
+
st.session_state.edit_project = True
|
344 |
+
st.experimental_rerun()
|
345 |
+
|
346 |
+
with col2:
|
347 |
+
if st.button("تصدير بيانات المشروع"):
|
348 |
+
st.success("تم تصدير بيانات المشروع بنجاح!")
|
349 |
+
|
350 |
+
with col3:
|
351 |
+
if st.button("إرسال للاعتماد"):
|
352 |
+
st.success("تم إرسال المشروع للاعتماد بنجاح!")
|
353 |
+
|
354 |
+
# نموذج تعديل المشروع
|
355 |
+
if 'edit_project' in st.session_state and st.session_state.edit_project:
|
356 |
+
st.markdown("#### تعديل المشروع")
|
357 |
+
|
358 |
+
with st.form("edit_project_form"):
|
359 |
+
col1, col2 = st.columns(2)
|
360 |
+
|
361 |
+
with col1:
|
362 |
+
project_name = st.text_input("اسم المشروع", value=project['name'])
|
363 |
+
client = st.text_input("الجهة المالكة", value=project['client'])
|
364 |
+
location = st.text_input("الموقع", value=project['location'])
|
365 |
+
tender_type = st.selectbox(
|
366 |
+
"نوع المناقصة",
|
367 |
+
["عامة", "خاصة", "أمر مباشر"],
|
368 |
+
index=["عامة", "خاصة", "أمر مباشر"].index(project['tender_type'])
|
369 |
+
)
|
370 |
+
|
371 |
+
with col2:
|
372 |
+
tender_number = st.text_input("رقم المناقصة", value=project['number'])
|
373 |
+
submission_date = st.date_input(
|
374 |
+
"تاريخ التقديم",
|
375 |
+
value=datetime.strptime(project['submission_date'], "%Y-%m-%d") if isinstance(project['submission_date'], str) else project['submission_date']
|
376 |
+
)
|
377 |
+
pricing_method = st.selectbox(
|
378 |
+
"طريقة التسعير",
|
379 |
+
["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
|
380 |
+
index=["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"].index(project['pricing_method'])
|
381 |
+
)
|
382 |
+
status = st.selectbox(
|
383 |
+
"حالة المشروع",
|
384 |
+
["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
|
385 |
+
index=["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"].index(project['status'])
|
386 |
+
)
|
387 |
+
|
388 |
+
description = st.text_area("وصف المشروع", value=project.get('description', ''))
|
389 |
+
|
390 |
+
col1, col2 = st.columns(2)
|
391 |
+
|
392 |
+
with col1:
|
393 |
+
submit = st.form_submit_button("حفظ التعديلات")
|
394 |
+
|
395 |
+
with col2:
|
396 |
+
cancel = st.form_submit_button("إلغاء")
|
397 |
+
|
398 |
+
if submit:
|
399 |
+
# تحديث بيانات المشروع
|
400 |
+
project['name'] = project_name
|
401 |
+
project['number'] = tender_number
|
402 |
+
project['client'] = client
|
403 |
+
project['location'] = location
|
404 |
+
project['description'] = description
|
405 |
+
project['status'] = status
|
406 |
+
project['tender_type'] = tender_type
|
407 |
+
project['pricing_method'] = pricing_method
|
408 |
+
project['submission_date'] = submission_date
|
409 |
+
|
410 |
+
st.success("تم تحديث بيانات المشروع بنجاح!")
|
411 |
+
st.session_state.edit_project = False
|
412 |
+
st.experimental_rerun()
|
413 |
+
|
414 |
+
elif cancel:
|
415 |
+
st.session_state.edit_project = False
|
416 |
+
st.experimental_rerun()
|
417 |
+
|
418 |
+
def _render_projects_tracking_tab(self):
|
419 |
+
"""عرض تبويب متابعة المشاريع"""
|
420 |
+
|
421 |
+
st.markdown("### متابعة المشاريع")
|
422 |
+
|
423 |
+
# عرض إحصائيات المشاريع
|
424 |
+
col1, col2, col3, col4 = st.columns(4)
|
425 |
+
|
426 |
+
projects = st.session_state.projects
|
427 |
+
|
428 |
+
with col1:
|
429 |
+
total_projects = len(projects)
|
430 |
+
self.ui.create_metric_card("إجمالي المشاريع", str(total_projects), None, self.ui.COLORS['primary'])
|
431 |
+
|
432 |
+
with col2:
|
433 |
+
active_projects = len([p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]])
|
434 |
+
self.ui.create_metric_card("المشاريع النشطة", str(active_projects), None, self.ui.COLORS['success'])
|
435 |
+
|
436 |
+
with col3:
|
437 |
+
pending_submission = len([p for p in projects if p['status'] in ["جديد", "قيد التسعير"]])
|
438 |
+
self.ui.create_metric_card("مشاريع قيد التسعير", str(pending_submission), None, self.ui.COLORS['warning'])
|
439 |
+
|
440 |
+
with col4:
|
441 |
+
completed_projects = len([p for p in projects if p['status'] in ["منتهي"]])
|
442 |
+
self.ui.create_metric_card("المشاريع المنتهية", str(completed_projects), None, self.ui.COLORS['info'])
|
443 |
+
|
444 |
+
# عرض رسم بياني لحالة المشاريع
|
445 |
+
st.markdown("#### توزيع المشاريع حسب الحالة")
|
446 |
+
|
447 |
+
status_counts = {}
|
448 |
+
for p in projects:
|
449 |
+
status = p['status']
|
450 |
+
status_counts[status] = status_counts.get(status, 0) + 1
|
451 |
+
|
452 |
+
status_df = pd.DataFrame({
|
453 |
+
'الحالة': list(status_counts.keys()),
|
454 |
+
'عدد المشاريع': list(status_counts.values())
|
455 |
+
})
|
456 |
+
|
457 |
+
st.bar_chart(status_df.set_index('الحالة'))
|
458 |
+
|
459 |
+
# عرض المشاريع قيد المتابعة
|
460 |
+
st.markdown("#### المشاريع قيد المتابعة")
|
461 |
+
|
462 |
+
# عرض المشاريع النشطة المرتبة حسب تاريخ التقديم
|
463 |
+
active_projects_list = [p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]]
|
464 |
+
|
465 |
+
if active_projects_list:
|
466 |
+
# تحويل التواريخ إلى كائنات تاريخ إذا كانت نصوصًا
|
467 |
+
for p in active_projects_list:
|
468 |
+
if isinstance(p['submission_date'], str):
|
469 |
+
p['submission_date'] = datetime.strptime(p['submission_date'], "%Y-%m-%d")
|
470 |
+
|
471 |
+
# ترتيب المشاريع حسب تاريخ التقديم
|
472 |
+
active_projects_list.sort(key=lambda x: x['submission_date'])
|
473 |
+
|
474 |
+
# تحويل إلى DataFrame
|
475 |
+
active_df = pd.DataFrame(active_projects_list)
|
476 |
+
|
477 |
+
# اختيار وترتيب الأعمدة
|
478 |
+
display_columns = [
|
479 |
+
'name', 'number', 'client', 'status',
|
480 |
+
'submission_date', 'tender_type'
|
481 |
+
]
|
482 |
+
|
483 |
+
# تغيير أسماء الأعمدة
|
484 |
+
column_names = {
|
485 |
+
'name': 'اسم المشروع',
|
486 |
+
'number': 'رقم المناقصة',
|
487 |
+
'client': 'الجهة المالكة',
|
488 |
+
'status': 'الحالة',
|
489 |
+
'submission_date': 'تاريخ التقديم',
|
490 |
+
'tender_type': 'نوع المناقصة'
|
491 |
+
}
|
492 |
+
|
493 |
+
# تنسيق البيانات
|
494 |
+
display_df = active_df[display_columns].rename(columns=column_names)
|
495 |
+
display_df['تاريخ التقديم'] = pd.to_datetime(display_df['تاريخ التقديم']).dt.strftime('%Y-%m-%d')
|
496 |
+
|
497 |
+
# عرض الجدول
|
498 |
+
st.dataframe(display_df, use_container_width=True, hide_index=True)
|
499 |
+
else:
|
500 |
+
st.info("لا توجد مشاريع نشطة حاليًا.")
|
501 |
+
|
502 |
+
# عرض المشاريع المقبلة
|
503 |
+
st.markdown("#### المواعيد المقبلة")
|
504 |
+
|
505 |
+
upcoming_events = []
|
506 |
+
today = datetime.now().date()
|
507 |
+
|
508 |
+
for p in projects:
|
509 |
+
submission_date = p['submission_date']
|
510 |
+
if isinstance(submission_date, str):
|
511 |
+
submission_date = datetime.strptime(submission_date, "%Y-%m-%d").date()
|
512 |
+
elif isinstance(submission_date, datetime):
|
513 |
+
submission_date = submission_date.date()
|
514 |
+
|
515 |
+
# المشاريع التي موعد تقديمها خلال الأسبوعين القادمين
|
516 |
+
if today <= submission_date <= today + timedelta(days=14) and p['status'] in ["قيد التسعير"]:
|
517 |
+
days_left = (submission_date - today).days
|
518 |
+
upcoming_events.append({
|
519 |
+
'المشروع': p['name'],
|
520 |
+
'الحدث': 'موعد تقديم المناقصة',
|
521 |
+
'التاريخ': submission_date.strftime('%Y-%m-%d'),
|
522 |
+
'الأيام المتبقية': days_left
|
523 |
+
})
|
524 |
+
|
525 |
+
if upcoming_events:
|
526 |
+
events_df = pd.DataFrame(upcoming_events)
|
527 |
+
st.dataframe(events_df, use_container_width=True, hide_index=True)
|
528 |
+
else:
|
529 |
+
st.info("لا توجد مواعيد قريبة.")
|
530 |
+
|
531 |
+
def _generate_sample_projects(self):
|
532 |
+
"""توليد بيانات افتراضية للمشاريع"""
|
533 |
+
|
534 |
+
projects = [
|
535 |
+
{
|
536 |
+
'id': 1,
|
537 |
+
'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية",
|
538 |
+
'number': "SHPD-2025-001",
|
539 |
+
'client': "وزارة الصحة",
|
540 |
+
'location': "الدمام، المنطقة الشرقية",
|
541 |
+
'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.",
|
542 |
+
'status': "قيد التسعير",
|
543 |
+
'tender_type': "عامة",
|
544 |
+
'pricing_method': "قياسي",
|
545 |
+
'submission_date': (datetime.now() + timedelta(days=5)),
|
546 |
+
'created_at': datetime.now() - timedelta(days=10),
|
547 |
+
'created_by_id': 1,
|
548 |
+
'documents': [
|
549 |
+
{
|
550 |
+
'filename': "كراسة الشروط والمواصفات.pdf",
|
551 |
+
'type': "كراسة شروط",
|
552 |
+
'upload_date': (datetime.now() - timedelta(days=9)).strftime('%Y-%m-%d'),
|
553 |
+
'size': "5.2 MB"
|
554 |
+
},
|
555 |
+
{
|
556 |
+
'filename': "المخططات الهندسية.dwg",
|
557 |
+
'type': "مخططات",
|
558 |
+
'upload_date': (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'),
|
559 |
+
'size': "25.7 MB"
|
560 |
+
},
|
561 |
+
{
|
562 |
+
'filename': "جدول الكميات.xlsx",
|
563 |
+
'type': "جدول كميات",
|
564 |
+
'upload_date': (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'),
|
565 |
+
'size': "1.8 MB"
|
566 |
+
}
|
567 |
+
],
|
568 |
+
'items': [
|
569 |
+
{
|
570 |
+
'رقم البند': "A1",
|
571 |
+
'وصف البند': "أعمال الحفر والردم",
|
572 |
+
'الوحدة': "م3",
|
573 |
+
'الكمية': 12500
|
574 |
+
},
|
575 |
+
{
|
576 |
+
'رقم البند': "A2",
|
577 |
+
'وصف البند': "أعمال الخرسانة المسلحة للأساسات",
|
578 |
+
'الوحدة': "م3",
|
579 |
+
'الكمية': 3500
|
580 |
+
},
|
581 |
+
{
|
582 |
+
'رقم البند': "A3",
|
583 |
+
'وصف البند': "أعمال حديد التسليح",
|
584 |
+
'الوحدة': "طن",
|
585 |
+
'الكمية': 450
|
586 |
+
}
|
587 |
+
]
|
588 |
+
},
|
589 |
+
{
|
590 |
+
'id': 2,
|
591 |
+
'name': "صيانة وتطوير طريق الملك عبدالله",
|
592 |
+
'number': "MOT-2025-042",
|
593 |
+
'client': "وزارة النقل",
|
594 |
+
'location': "الرياض، المنطقة الوسطى",
|
595 |
+
'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.",
|
596 |
+
'status': "تم التقديم",
|
597 |
+
'tender_type': "عامة",
|
598 |
+
'pricing_method': "غير متزن",
|
599 |
+
'submission_date': (datetime.now() - timedelta(days=15)),
|
600 |
+
'created_at': datetime.now() - timedelta(days=45),
|
601 |
+
'created_by_id': 1
|
602 |
+
},
|
603 |
+
{
|
604 |
+
'id': 3,
|
605 |
+
'name': "إنشاء محطة معالجة مياه الصرف الصحي",
|
606 |
+
'number': "SWPC-2025-007",
|
607 |
+
'client': "شركة المياه الوطنية",
|
608 |
+
'location': "جدة، المنطقة الغربية",
|
609 |
+
'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.",
|
610 |
+
'status': "تمت الترسية",
|
611 |
+
'tender_type': "عامة",
|
612 |
+
'pricing_method': "قياسي",
|
613 |
+
'submission_date': (datetime.now() - timedelta(days=90)),
|
614 |
+
'created_at': datetime.now() - timedelta(days=120),
|
615 |
+
'created_by_id': 1
|
616 |
+
},
|
617 |
+
{
|
618 |
+
'id': 4,
|
619 |
+
'name': "إنشاء منتزه الملك سلمان",
|
620 |
+
'number': "RAM-2025-015",
|
621 |
+
'client': "أمانة منطقة الرياض",
|
622 |
+
'location': "الرياض، المنطقة الوسطى",
|
623 |
+
'description': "إنشاء منتزه الملك سلمان على مساحة 500,000 متر مربع، ويشمل المشروع أعمال التشجير والتنسيق والمسطحات المائية والمباني الخدمية.",
|
624 |
+
'status': "قيد التنفيذ",
|
625 |
+
'tender_type': "عامة",
|
626 |
+
'pricing_method': "قياسي",
|
627 |
+
'submission_date': (datetime.now() - timedelta(days=180)),
|
628 |
+
'created_at': datetime.now() - timedelta(days=210),
|
629 |
+
'created_by_id': 1
|
630 |
+
},
|
631 |
+
{
|
632 |
+
'id': 5,
|
633 |
+
'name': "إنشاء مبنى مختبرات كلية العلوم",
|
634 |
+
'number': "KSU-2025-032",
|
635 |
+
'client': "جامعة الملك سعود",
|
636 |
+
'location': "الرياض، المنطقة الوسطى",
|
637 |
+
'description': "إنشاء مبنى المختبرات الجديد لكلية العلوم بمساحة 8,000 متر مربع، ويتكون من 3 طوابق ويشمل تجهيز المعامل والمختبرات العلمية.",
|
638 |
+
'status': "جديد",
|
639 |
+
'tender_type': "خاصة",
|
640 |
+
'pricing_method': "تنافسي",
|
641 |
+
'submission_date': (datetime.now() + timedelta(days=10)),
|
642 |
+
'created_at': datetime.now() - timedelta(days=5),
|
643 |
+
'created_by_id': 1
|
644 |
+
},
|
645 |
+
{
|
646 |
+
'id': 6,
|
647 |
+
'name': "توريد وتركيب أنظمة الطاقة الشمسية",
|
648 |
+
'number': "SEC-2025-098",
|
649 |
+
'client': "الشركة السعودية للكهرباء",
|
650 |
+
'location': "تبوك، المنطقة الشمالية",
|
651 |
+
'description': "توريد وتركيب أنظمة الطاقة الشمسية بقدرة 5 ميجاوات، مع جميع الأعمال المدنية والكهربائية.",
|
652 |
+
'status': "جديد",
|
653 |
+
'tender_type': "عامة",
|
654 |
+
'pricing_method': "قياسي",
|
655 |
+
'submission_date': (datetime.now() + timedelta(days=20)),
|
656 |
+
'created_at': datetime.now() - timedelta(days=2),
|
657 |
+
'created_by_id': 1
|
658 |
+
}
|
659 |
+
]
|
660 |
+
|
661 |
+
return projects
|
662 |
+
|
663 |
+
# تشغيل التطبيق
|
664 |
+
if __name__ == "__main__":
|
665 |
+
projects_app = ProjectsApp()
|
666 |
+
projects_app.run()
|
modules/reports/reports_app.py
CHANGED
@@ -1,405 +1,405 @@
|
|
1 |
-
import streamlit as st
|
2 |
-
import pandas as pd
|
3 |
-
import plotly.express as px
|
4 |
-
from datetime import datetime, timedelta
|
5 |
-
import time
|
6 |
-
|
7 |
-
class ReportsApp:
|
8 |
-
"""وحدة التقارير والتحليلات"""
|
9 |
-
|
10 |
-
def __init__(self):
|
11 |
-
"""تهيئة وحدة التقارير والتحليلات"""
|
12 |
-
# تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
|
13 |
-
if 'theme' not in st.session_state:
|
14 |
-
st.session_state.theme = 'light'
|
15 |
-
|
16 |
-
def run(self):
|
17 |
-
"""
|
18 |
-
تشغيل وحدة التقارير والتحليلات
|
19 |
-
|
20 |
-
هذه الدالة هي نقطة الدخول الرئيسية لوحدة التقارير والتحليلات.
|
21 |
-
تقوم بتهيئة واجهة المستخدم وعرض الوظائف المختلفة للتقارير والتحليلات.
|
22 |
-
"""
|
23 |
-
try:
|
24 |
-
# تعيين عنوان الصفحة
|
25 |
-
st.set_page_config(
|
26 |
-
page_title="وحدة التقارير والتحليلات - نظام المناقصات",
|
27 |
-
page_icon="📊",
|
28 |
-
layout="wide",
|
29 |
-
initial_sidebar_state="expanded"
|
30 |
-
)
|
31 |
-
|
32 |
-
# تطبيق التنسيق المخصص
|
33 |
-
st.markdown("""
|
34 |
-
<style>
|
35 |
-
.module-title {
|
36 |
-
color: #2c3e50;
|
37 |
-
text-align: center;
|
38 |
-
font-size: 2.5rem;
|
39 |
-
margin-bottom: 1rem;
|
40 |
-
padding-bottom: 1rem;
|
41 |
-
border-bottom: 2px solid #3498db;
|
42 |
-
}
|
43 |
-
.stTabs [data-baseweb="tab-list"] {
|
44 |
-
gap: 10px;
|
45 |
-
}
|
46 |
-
.stTabs [data-baseweb="tab"] {
|
47 |
-
height: 50px;
|
48 |
-
white-space: pre-wrap;
|
49 |
-
background-color: #f8f9fa;
|
50 |
-
border-radius: 4px 4px 0px 0px;
|
51 |
-
gap: 1px;
|
52 |
-
padding-top: 10px;
|
53 |
-
padding-bottom: 10px;
|
54 |
-
}
|
55 |
-
.stTabs [aria-selected="true"] {
|
56 |
-
background-color: #3498db;
|
57 |
-
color: white;
|
58 |
-
}
|
59 |
-
</style>
|
60 |
-
""", unsafe_allow_html=True)
|
61 |
-
|
62 |
-
# إضافة زر تبديل السمة في أعلى الصفحة
|
63 |
-
col1, col2, col3 = st.columns([1, 8, 1])
|
64 |
-
with col3:
|
65 |
-
if st.button("🌓 تبديل السمة"):
|
66 |
-
# تبديل السمة
|
67 |
-
if st.session_state.theme == "light":
|
68 |
-
st.session_state.theme = "dark"
|
69 |
-
else:
|
70 |
-
st.session_state.theme = "light"
|
71 |
-
|
72 |
-
# تطبيق السمة الجديدة وإعادة تشغيل التطبيق
|
73 |
-
st.rerun()
|
74 |
-
|
75 |
-
# عرض الشريط الجانبي
|
76 |
-
with st.sidebar:
|
77 |
-
st.image("/home/ubuntu/tender_system/tender_system/assets/images/logo.png", width=200)
|
78 |
-
st.markdown("## نظام تحليل المناقصات")
|
79 |
-
st.markdown("### وحدة التقارير والتحليلات")
|
80 |
-
|
81 |
-
st.markdown("---")
|
82 |
-
|
83 |
-
# إضافة خيارات تصفية التقارير
|
84 |
-
st.markdown("### خيارات التصفية")
|
85 |
-
|
86 |
-
# تصفية حسب الفترة الزمنية
|
87 |
-
date_range = st.selectbox(
|
88 |
-
"الفترة الزمنية",
|
89 |
-
["آخر 7 أيام", "آخر 30 يوم", "آخر 90 يوم", "آخر 365 يوم", "كل الفترات"]
|
90 |
-
)
|
91 |
-
|
92 |
-
# تصفية حسب نوع المشروع
|
93 |
-
project_type = st.multiselect(
|
94 |
-
"نوع المشروع",
|
95 |
-
["مباني", "طرق", "جسور", "أنفاق", "بنية تحتية", "أخرى"],
|
96 |
-
default=["مباني", "طرق", "جسور", "أنفاق", "بنية تحتية", "أخرى"]
|
97 |
-
)
|
98 |
-
|
99 |
-
# تصفية حسب حالة المشروع
|
100 |
-
project_status = st.multiselect(
|
101 |
-
"حالة المشروع",
|
102 |
-
["جديد", "قيد التقديم", "تم التقديم", "فائز", "خاسر", "ملغي"],
|
103 |
-
default=["جديد", "قيد التقديم", "تم التقديم", "فائز", "خاسر"]
|
104 |
-
)
|
105 |
-
|
106 |
-
# زر تطبيق التصفية
|
107 |
-
if st.button("تطبيق التصفية"):
|
108 |
-
st.success("تم تطبيق التصفية بنجاح!")
|
109 |
-
|
110 |
-
st.markdown("---")
|
111 |
-
|
112 |
-
# إضافة معلومات المستخدم
|
113 |
-
st.markdown("### معلومات المستخدم")
|
114 |
-
st.markdown("**المستخدم:** مهندس تامر الجوهري")
|
115 |
-
st.markdown("**الدور:** محلل مناقصات")
|
116 |
-
st.markdown("**تاريخ آخر دخول:** " + datetime.now().strftime("%Y-%m-%d %H:%M"))
|
117 |
-
|
118 |
-
# عرض واجهة وحدة التقارير والتحليلات
|
119 |
-
self.render()
|
120 |
-
|
121 |
-
# إضافة معلومات في أسفل الصفحة
|
122 |
-
st.markdown("---")
|
123 |
-
st.markdown("### نظام تحليل المناقصات - وحدة التقارير والتحليلات")
|
124 |
-
st.markdown("**الإصدار:** 2.0.0")
|
125 |
-
st.markdown("**تاريخ التحديث:** 2025-03-31")
|
126 |
-
st.markdown("**جميع الحقوق محفوظة © 2025**")
|
127 |
-
|
128 |
-
return True
|
129 |
-
|
130 |
-
except Exception as e:
|
131 |
-
st.error(f"حدث خطأ أثناء تشغيل وحدة التقارير والتحليلات: {str(e)}")
|
132 |
-
return False
|
133 |
-
|
134 |
-
def render(self):
|
135 |
-
"""عرض واجهة وحدة التقارير والتحليلات"""
|
136 |
-
|
137 |
-
st.markdown("<h1 class='module-title'>وحدة التقارير والتحليلات</h1>", unsafe_allow_html=True)
|
138 |
-
|
139 |
-
tabs = st.tabs(["لوحة المعلومات", "تقارير المشاريع", "تقارير التسعير", "تقارير المخاطر", "التقارير المخصصة"])
|
140 |
-
|
141 |
-
with tabs[0]:
|
142 |
-
self._render_dashboard_tab()
|
143 |
-
|
144 |
-
with tabs[1]:
|
145 |
-
self._render_projects_reports_tab()
|
146 |
-
|
147 |
-
with tabs[2]:
|
148 |
-
self._render_pricing_reports_tab()
|
149 |
-
|
150 |
-
with tabs[3]:
|
151 |
-
self._render_risk_reports_tab()
|
152 |
-
|
153 |
-
with tabs[4]:
|
154 |
-
self._render_custom_reports_tab()
|
155 |
-
|
156 |
-
def _render_dashboard_tab(self):
|
157 |
-
"""عرض تبويب لوحة المعلومات"""
|
158 |
-
|
159 |
-
st.markdown("### لوحة معلومات النظام")
|
160 |
-
|
161 |
-
col1, col2, col3, col4 = st.columns(4)
|
162 |
-
|
163 |
-
with col1:
|
164 |
-
total_projects = self._get_total_projects()
|
165 |
-
st.metric("إجمالي المشاريع", total_projects)
|
166 |
-
|
167 |
-
with col2:
|
168 |
-
active_projects = self._get_active_projects()
|
169 |
-
st.metric("المشاريع النشطة", active_projects, delta=f"{active_projects/total_projects*100:.1f}%" if total_projects > 0 else "0%")
|
170 |
-
|
171 |
-
with col3:
|
172 |
-
won_projects = self._get_won_projects()
|
173 |
-
st.metric("المشاريع المرساة", won_projects, delta=f"{won_projects/total_projects*100:.1f}%" if total_projects > 0 else "0%")
|
174 |
-
|
175 |
-
with col4:
|
176 |
-
avg_local_content = self._get_avg_local_content()
|
177 |
-
st.metric("متوسط المحتوى المحلي", f"{avg_local_content:.1f}%", delta=f"{avg_local_content-70:.1f}%" if avg_local_content > 0 else "0%")
|
178 |
-
|
179 |
-
st.markdown("#### توزيع المشاريع حسب الحالة")
|
180 |
-
project_status_data = self._get_project_status_data()
|
181 |
-
fig = px.pie(project_status_data, values='count', names='status', title='توزيع المشاريع حسب الحالة', hole=0.4)
|
182 |
-
st.plotly_chart(fig, use_container_width=True)
|
183 |
-
|
184 |
-
st.markdown("#### اتجاه المشاريع الشهري")
|
185 |
-
monthly_data = self._get_monthly_project_data()
|
186 |
-
fig = px.line(monthly_data, x='month', y=['new', 'submitted', 'won'], title='اتجاه المشاريع الشهري')
|
187 |
-
st.plotly_chart(fig, use_container_width=True)
|
188 |
-
|
189 |
-
col1, col2 = st.columns(2)
|
190 |
-
|
191 |
-
with col1:
|
192 |
-
st.markdown("#### توزيع المشاريع حسب النوع")
|
193 |
-
project_type_data = self._get_project_type_data()
|
194 |
-
fig = px.bar(project_type_data, x='type', y='count', title='توزيع المشاريع حسب النوع')
|
195 |
-
st.plotly_chart(fig, use_container_width=True)
|
196 |
-
|
197 |
-
with col2:
|
198 |
-
st.markdown("#### توزيع المشاريع حسب الموقع")
|
199 |
-
project_location_data = self._get_project_location_data()
|
200 |
-
fig = px.bar(project_location_data, x='location', y='count', title='توزيع المشاريع حسب الموقع')
|
201 |
-
st.plotly_chart(fig, use_container_width=True)
|
202 |
-
|
203 |
-
st.markdown("#### أحدث المشاريع")
|
204 |
-
latest_projects = self._get_latest_projects()
|
205 |
-
st.dataframe(latest_projects)
|
206 |
-
|
207 |
-
def _render_projects_reports_tab(self):
|
208 |
-
"""عرض تبويب تقارير المشاريع"""
|
209 |
-
|
210 |
-
st.markdown("### تقارير المشاريع")
|
211 |
-
|
212 |
-
report_type = st.selectbox(
|
213 |
-
"نوع التقرير",
|
214 |
-
["تقرير حالة المشاريع", "تقرير أداء المشاريع", "تقرير المشاريع المتأخرة", "تقرير المشاريع المكتملة"]
|
215 |
-
)
|
216 |
-
|
217 |
-
if report_type == "تقرير حالة المشاريع":
|
218 |
-
self._render_project_status_report()
|
219 |
-
elif report_type == "تقرير أداء المشاريع":
|
220 |
-
self._render_project_performance_report()
|
221 |
-
elif report_type == "تقرير المشاريع المتأخرة":
|
222 |
-
self._render_delayed_projects_report()
|
223 |
-
elif report_type == "تقرير المشاريع المكتملة":
|
224 |
-
self._render_completed_projects_report()
|
225 |
-
|
226 |
-
def _render_pricing_reports_tab(self):
|
227 |
-
"""عرض تبويب تقارير التسعير"""
|
228 |
-
|
229 |
-
st.markdown("### تقارير التسعير")
|
230 |
-
|
231 |
-
report_type = st.selectbox(
|
232 |
-
"نوع التقرير",
|
233 |
-
["تقرير تحليل الأسعار", "تقرير مقارنة الأسعار", "تقرير اتجاهات الأسعار", "تقرير تحليل المنافسين"]
|
234 |
-
)
|
235 |
-
|
236 |
-
if report_type == "تقرير تحليل الأسعار":
|
237 |
-
self._render_price_analysis_report()
|
238 |
-
elif report_type == "تقرير مقارنة الأسعار":
|
239 |
-
self._render_price_comparison_report()
|
240 |
-
elif report_type == "تقرير اتجاهات الأسعار":
|
241 |
-
self._render_price_trends_report()
|
242 |
-
elif report_type == "تقرير تحليل المنافسين":
|
243 |
-
self._render_competitors_analysis_report()
|
244 |
-
|
245 |
-
def _render_risk_reports_tab(self):
|
246 |
-
"""عرض تبويب تقارير المخاطر"""
|
247 |
-
|
248 |
-
st.markdown("### تقارير المخاطر")
|
249 |
-
|
250 |
-
report_type = st.selectbox(
|
251 |
-
"نوع التقرير",
|
252 |
-
["تقرير تحليل المخاطر", "تقرير مصفوفة المخاطر", "تقرير متابعة المخاطر", "تقرير استراتيجيات التخفيف"]
|
253 |
-
)
|
254 |
-
|
255 |
-
if report_type == "تقرير تحليل المخاطر":
|
256 |
-
self._render_risk_analysis_report()
|
257 |
-
elif report_type == "تقرير مصفوفة المخاطر":
|
258 |
-
self._render_risk_matrix_report()
|
259 |
-
elif report_type == "تقرير متابعة المخاطر":
|
260 |
-
self._render_risk_monitoring_report()
|
261 |
-
elif report_type == "تقرير استراتيجيات التخفيف":
|
262 |
-
self._render_risk_mitigation_report()
|
263 |
-
|
264 |
-
def _render_custom_reports_tab(self):
|
265 |
-
"""عرض تبويب التقارير المخصصة"""
|
266 |
-
|
267 |
-
st.markdown("### التقارير المخصصة")
|
268 |
-
|
269 |
-
st.markdown("#### إنشاء تقرير مخصص")
|
270 |
-
|
271 |
-
col1, col2 = st.columns(2)
|
272 |
-
|
273 |
-
with col1:
|
274 |
-
report_name = st.text_input("اسم التقرير")
|
275 |
-
report_description = st.text_area("وصف التقرير")
|
276 |
-
|
277 |
-
with col2:
|
278 |
-
report_fields = st.multiselect(
|
279 |
-
"حقول التقرير",
|
280 |
-
["رقم المشروع", "اسم المشروع", "نوع المشروع", "حالة المشروع", "تاريخ البدء", "تاريخ الانتهاء", "الميزانية", "التكلفة الفعلية", "نسبة الإنجاز", "المخاطر", "الموقع", "المالك", "المقاول"]
|
281 |
-
)
|
282 |
-
|
283 |
-
report_filters = st.multiselect(
|
284 |
-
"تصفية التقرير",
|
285 |
-
["نوع المشروع", "حالة المشروع", "الفترة الزمنية", "الميزانية", "الموقع", "المالك", "المقاول"]
|
286 |
-
)
|
287 |
-
|
288 |
-
if st.button("إنشاء التقرير"):
|
289 |
-
if report_name and report_description and report_fields:
|
290 |
-
with st.spinner("جاري إنشاء التقرير..."):
|
291 |
-
time.sleep(2) # محاكاة وقت المعالجة
|
292 |
-
st.success("تم إنشاء التقرير بنجاح!")
|
293 |
-
|
294 |
-
# عرض التقرير المخصص (محاكاة)
|
295 |
-
custom_report_data = self._generate_custom_report(report_fields)
|
296 |
-
st.dataframe(custom_report_data)
|
297 |
-
|
298 |
-
# تصدير التقرير
|
299 |
-
st.download_button(
|
300 |
-
label="تصدير التقرير (Excel)",
|
301 |
-
data=self._export_to_excel(custom_report_data),
|
302 |
-
file_name=f"{report_name}.xlsx",
|
303 |
-
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
304 |
-
)
|
305 |
-
else:
|
306 |
-
st.warning("يرجى ملء جميع الحقول المطلوبة")
|
307 |
-
|
308 |
-
st.markdown("#### التقارير المخصصة المحفوظة")
|
309 |
-
|
310 |
-
saved_reports = [
|
311 |
-
{"id": 1, "name": "تقرير المشاريع المتأخرة في الرياض", "created_at": "2025-03-15", "last_run": "2025-03-30"},
|
312 |
-
{"id": 2, "name": "تقرير مشاريع الطرق ذات المخاطر العالية", "created_at": "2025-03-10", "last_run": "2025-03-28"},
|
313 |
-
{"id": 3, "name": "
|
314 |
-
]
|
315 |
-
|
316 |
-
saved_reports_df = pd.DataFrame(saved_reports)
|
317 |
-
st.dataframe(saved_reports_df)
|
318 |
-
|
319 |
-
# تنفيذ دوال الحصول على البيانات
|
320 |
-
|
321 |
-
def _get_total_projects(self):
|
322 |
-
"""الحصول على إجمالي عدد المشاريع"""
|
323 |
-
# محاكاة البيانات
|
324 |
-
return 120
|
325 |
-
|
326 |
-
def _get_active_projects(self):
|
327 |
-
"""الحصول على عدد المشاريع النشطة"""
|
328 |
-
# محاكاة البيانات
|
329 |
-
return 45
|
330 |
-
|
331 |
-
def _get_won_projects(self):
|
332 |
-
"""الحصول على عدد المشاريع المرساة"""
|
333 |
-
# محاكاة البيانات
|
334 |
-
return 30
|
335 |
-
|
336 |
-
def _get_avg_local_content(self):
|
337 |
-
"""الحصول على متوسط المحتوى المحلي"""
|
338 |
-
# محاكاة البيانات
|
339 |
-
return 75.5
|
340 |
-
|
341 |
-
def _get_project_status_data(self):
|
342 |
-
"""الحصول على بيانات توزيع المشاريع حسب الحالة"""
|
343 |
-
# محاكاة البيانات
|
344 |
-
data = {
|
345 |
-
'status': ['جديد', 'قيد التقديم', 'تم التقديم', 'فائز', 'خاسر', 'ملغي'],
|
346 |
-
'count': [25, 20, 15, 30, 25, 5]
|
347 |
-
}
|
348 |
-
return pd.DataFrame(data)
|
349 |
-
|
350 |
-
def _get_monthly_project_data(self):
|
351 |
-
"""الحصول على بيانات اتجاه المشاريع الشهري"""
|
352 |
-
# محاكاة البيانات
|
353 |
-
data = {
|
354 |
-
'month': ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو'],
|
355 |
-
'new': [10, 15, 12, 8, 20, 18],
|
356 |
-
'submitted': [8, 12, 10, 6, 15, 14],
|
357 |
-
'won': [5, 8, 6, 4, 10, 9]
|
358 |
-
}
|
359 |
-
return pd.DataFrame(data)
|
360 |
-
|
361 |
-
def _get_project_type_data(self):
|
362 |
-
"""الحصول على بيانات توزيع المشاريع حسب النوع"""
|
363 |
-
# محاكاة البيانات
|
364 |
-
data = {
|
365 |
-
'type': ['مباني', 'طرق', 'جسور', 'أنفاق', 'بنية تحتية', 'أخرى'],
|
366 |
-
'count': [40, 30, 15, 10, 20, 5]
|
367 |
-
}
|
368 |
-
return pd.DataFrame(data)
|
369 |
-
|
370 |
-
def _get_project_location_data(self):
|
371 |
-
"""الحصول على بيانات توزيع المشاريع حسب الموقع"""
|
372 |
-
# محاكاة البيانات
|
373 |
-
data = {
|
374 |
-
'location': ['الرياض', 'جدة', 'الدمام', 'مكة', 'المدينة', 'أخرى'],
|
375 |
-
'count': [35, 25, 20, 15, 10, 15]
|
376 |
-
}
|
377 |
-
return pd.DataFrame(data)
|
378 |
-
|
379 |
-
def _get_latest_projects(self):
|
380 |
-
"""الحصول على بيانات أحدث المشاريع"""
|
381 |
-
# محاكاة البيانات
|
382 |
-
data = {
|
383 |
-
'رقم المشروع': ['P-2025-001', 'P-2025-002', 'P-2025-003', 'P-2025-004', 'P-2025-005'],
|
384 |
-
'اسم المشروع': ['إنشاء مبنى إداري', 'تطوير شبكة طرق', 'إنشاء جسر', 'بناء مدرسة', 'تطوير شبكة مياه'],
|
385 |
-
'نوع المشروع': ['مباني', 'طرق', 'جسور', 'مباني', 'بنية تحتية'],
|
386 |
-
'حالة المشروع': ['جديد', 'قيد التقديم', 'تم التقديم', 'فائز', 'جديد'],
|
387 |
-
'تاريخ الإضافة': ['2025-03-30', '2025-03-28', '2025-03-25', '2025-03-20', '2025-03-18']
|
388 |
-
}
|
389 |
-
return pd.DataFrame(data)
|
390 |
-
|
391 |
-
# تنفيذ دوال عرض التقارير
|
392 |
-
|
393 |
-
def _render_project_status_report(self):
|
394 |
-
"""عرض تقرير حالة المشاريع"""
|
395 |
-
|
396 |
-
st.markdown("#### تقرير حالة المشاريع")
|
397 |
-
|
398 |
-
# محاكاة بيانات التقرير
|
399 |
-
data = {
|
400 |
-
'رقم المشروع': ['P-2025-001', 'P-2025-002', 'P-2025-003', 'P-2025-004', 'P-2025-005', 'P-2025-006', 'P-2025-007', 'P-2025-008', 'P-2025-009', 'P-2025-010'],
|
401 |
-
'اسم المشروع': ['إنشاء مبنى إداري', 'تطوير شبكة طرق', 'إنشاء جسر', 'بناء مدرسة', 'تطوير شبكة مياه', 'إنشاء مستشفى', 'بناء مركز تجاري', 'تطوير حديقة عامة', 'إنشاء مصنع', 'تطوير مطار'],
|
402 |
-
'نوع المشروع': ['مباني', 'طرق', 'جسور', 'مباني', 'بنية تحتية', 'مباني', 'مباني', 'أخرى', 'مباني', 'بنية تحتية'],
|
403 |
-
'
|
404 |
-
(Content truncated due to size limit. Use line ranges to read in chunks)
|
405 |
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import plotly.express as px
|
4 |
+
from datetime import datetime, timedelta
|
5 |
+
import time
|
6 |
+
|
7 |
+
class ReportsApp:
|
8 |
+
"""وحدة التقارير والتحليلات"""
|
9 |
+
|
10 |
+
def __init__(self):
|
11 |
+
"""تهيئة وحدة التقارير والتحليلات"""
|
12 |
+
# تهيئة متغير السمة في حالة الجلسة إذا لم يكن موجوداً
|
13 |
+
if 'theme' not in st.session_state:
|
14 |
+
st.session_state.theme = 'light'
|
15 |
+
|
16 |
+
def run(self):
|
17 |
+
"""
|
18 |
+
تشغيل وحدة التقارير والتحليلات
|
19 |
+
|
20 |
+
هذه الدالة هي نقطة الدخول الرئيسية لوحدة التقارير والتحليلات.
|
21 |
+
تقوم بتهيئة واجهة المستخدم وعرض الوظائف المختلفة للتقارير والتحليلات.
|
22 |
+
"""
|
23 |
+
try:
|
24 |
+
# تعيين عنوان الصفحة
|
25 |
+
st.set_page_config(
|
26 |
+
page_title="وحدة التقارير والتحليلات - نظام المناقصات",
|
27 |
+
page_icon="📊",
|
28 |
+
layout="wide",
|
29 |
+
initial_sidebar_state="expanded"
|
30 |
+
)
|
31 |
+
|
32 |
+
# تطبيق التنسيق المخصص
|
33 |
+
st.markdown("""
|
34 |
+
<style>
|
35 |
+
.module-title {
|
36 |
+
color: #2c3e50;
|
37 |
+
text-align: center;
|
38 |
+
font-size: 2.5rem;
|
39 |
+
margin-bottom: 1rem;
|
40 |
+
padding-bottom: 1rem;
|
41 |
+
border-bottom: 2px solid #3498db;
|
42 |
+
}
|
43 |
+
.stTabs [data-baseweb="tab-list"] {
|
44 |
+
gap: 10px;
|
45 |
+
}
|
46 |
+
.stTabs [data-baseweb="tab"] {
|
47 |
+
height: 50px;
|
48 |
+
white-space: pre-wrap;
|
49 |
+
background-color: #f8f9fa;
|
50 |
+
border-radius: 4px 4px 0px 0px;
|
51 |
+
gap: 1px;
|
52 |
+
padding-top: 10px;
|
53 |
+
padding-bottom: 10px;
|
54 |
+
}
|
55 |
+
.stTabs [aria-selected="true"] {
|
56 |
+
background-color: #3498db;
|
57 |
+
color: white;
|
58 |
+
}
|
59 |
+
</style>
|
60 |
+
""", unsafe_allow_html=True)
|
61 |
+
|
62 |
+
# إضافة زر تبديل السمة في أعلى الصفحة
|
63 |
+
col1, col2, col3 = st.columns([1, 8, 1])
|
64 |
+
with col3:
|
65 |
+
if st.button("🌓 تبديل السمة"):
|
66 |
+
# تبديل السمة
|
67 |
+
if st.session_state.theme == "light":
|
68 |
+
st.session_state.theme = "dark"
|
69 |
+
else:
|
70 |
+
st.session_state.theme = "light"
|
71 |
+
|
72 |
+
# تطبيق السمة الجديدة وإعادة تشغيل التطبيق
|
73 |
+
st.rerun()
|
74 |
+
|
75 |
+
# عرض الشريط الجانبي
|
76 |
+
with st.sidebar:
|
77 |
+
st.image("/home/ubuntu/tender_system/tender_system/assets/images/logo.png", width=200)
|
78 |
+
st.markdown("## نظام تحليل المناقصات")
|
79 |
+
st.markdown("### وحدة التقارير والتحليلات")
|
80 |
+
|
81 |
+
st.markdown("---")
|
82 |
+
|
83 |
+
# إضافة خيارات تصفية التقارير
|
84 |
+
st.markdown("### خيارات التصفية")
|
85 |
+
|
86 |
+
# تصفية حسب الفترة الزمنية
|
87 |
+
date_range = st.selectbox(
|
88 |
+
"الفترة الزمنية",
|
89 |
+
["آخر 7 أيام", "آخر 30 يوم", "آخر 90 يوم", "آخر 365 يوم", "كل الفترات"]
|
90 |
+
)
|
91 |
+
|
92 |
+
# تصفية حسب نوع المشروع
|
93 |
+
project_type = st.multiselect(
|
94 |
+
"نوع المشروع",
|
95 |
+
["مباني", "طرق", "جسور", "أنفاق", "بنية تحتية", "أخرى"],
|
96 |
+
default=["مباني", "طرق", "جسور", "أنفاق", "بنية تحتية", "أخرى"]
|
97 |
+
)
|
98 |
+
|
99 |
+
# تصفية حسب حالة المشروع
|
100 |
+
project_status = st.multiselect(
|
101 |
+
"حالة المشروع",
|
102 |
+
["جديد", "قيد التقديم", "تم التقديم", "فائز", "خاسر", "ملغي"],
|
103 |
+
default=["جديد", "قيد التقديم", "تم التقديم", "فائز", "خاسر"]
|
104 |
+
)
|
105 |
+
|
106 |
+
# زر تطبيق التصفية
|
107 |
+
if st.button("تطبيق التصفية"):
|
108 |
+
st.success("تم تطبيق التصفية بنجاح!")
|
109 |
+
|
110 |
+
st.markdown("---")
|
111 |
+
|
112 |
+
# إضافة معلومات المستخدم
|
113 |
+
st.markdown("### معلومات المستخدم")
|
114 |
+
st.markdown("**المستخدم:** مهندس تامر الجوهري")
|
115 |
+
st.markdown("**الدور:** محلل مناقصات")
|
116 |
+
st.markdown("**تاريخ آخر دخول:** " + datetime.now().strftime("%Y-%m-%d %H:%M"))
|
117 |
+
|
118 |
+
# عرض واجهة وحدة التقارير والتحليلات
|
119 |
+
self.render()
|
120 |
+
|
121 |
+
# إضافة معلومات في أسفل الصفحة
|
122 |
+
st.markdown("---")
|
123 |
+
st.markdown("### نظام تحليل المناقصات - وحدة التقارير والتحليلات")
|
124 |
+
st.markdown("**الإصدار:** 2.0.0")
|
125 |
+
st.markdown("**تاريخ التحديث:** 2025-03-31")
|
126 |
+
st.markdown("**جميع الحقوق محفوظة © 2025**")
|
127 |
+
|
128 |
+
return True
|
129 |
+
|
130 |
+
except Exception as e:
|
131 |
+
st.error(f"حدث خطأ أثناء تشغيل وحدة التقارير والتحليلات: {str(e)}")
|
132 |
+
return False
|
133 |
+
|
134 |
+
def render(self):
|
135 |
+
"""عرض واجهة وحدة التقارير والتحليلات"""
|
136 |
+
|
137 |
+
st.markdown("<h1 class='module-title'>وحدة التقارير والتحليلات</h1>", unsafe_allow_html=True)
|
138 |
+
|
139 |
+
tabs = st.tabs(["لوحة المعلومات", "تقارير المشاريع", "تقارير التسعير", "تقارير المخاطر", "التقارير المخصصة"])
|
140 |
+
|
141 |
+
with tabs[0]:
|
142 |
+
self._render_dashboard_tab()
|
143 |
+
|
144 |
+
with tabs[1]:
|
145 |
+
self._render_projects_reports_tab()
|
146 |
+
|
147 |
+
with tabs[2]:
|
148 |
+
self._render_pricing_reports_tab()
|
149 |
+
|
150 |
+
with tabs[3]:
|
151 |
+
self._render_risk_reports_tab()
|
152 |
+
|
153 |
+
with tabs[4]:
|
154 |
+
self._render_custom_reports_tab()
|
155 |
+
|
156 |
+
def _render_dashboard_tab(self):
|
157 |
+
"""عرض تبويب لوحة المعلومات"""
|
158 |
+
|
159 |
+
st.markdown("### لوحة معلومات النظام")
|
160 |
+
|
161 |
+
col1, col2, col3, col4 = st.columns(4)
|
162 |
+
|
163 |
+
with col1:
|
164 |
+
total_projects = self._get_total_projects()
|
165 |
+
st.metric("إجمالي المشاريع", total_projects)
|
166 |
+
|
167 |
+
with col2:
|
168 |
+
active_projects = self._get_active_projects()
|
169 |
+
st.metric("المشاريع النشطة", active_projects, delta=f"{active_projects/total_projects*100:.1f}%" if total_projects > 0 else "0%")
|
170 |
+
|
171 |
+
with col3:
|
172 |
+
won_projects = self._get_won_projects()
|
173 |
+
st.metric("المشاريع المرساة", won_projects, delta=f"{won_projects/total_projects*100:.1f}%" if total_projects > 0 else "0%")
|
174 |
+
|
175 |
+
with col4:
|
176 |
+
avg_local_content = self._get_avg_local_content()
|
177 |
+
st.metric("متوسط المحتوى المحلي", f"{avg_local_content:.1f}%", delta=f"{avg_local_content-70:.1f}%" if avg_local_content > 0 else "0%")
|
178 |
+
|
179 |
+
st.markdown("#### توزيع المشاريع حسب الحالة")
|
180 |
+
project_status_data = self._get_project_status_data()
|
181 |
+
fig = px.pie(project_status_data, values='count', names='status', title='توزيع المشاريع حسب الحالة', hole=0.4)
|
182 |
+
st.plotly_chart(fig, use_container_width=True)
|
183 |
+
|
184 |
+
st.markdown("#### اتجاه المشاريع الشهري")
|
185 |
+
monthly_data = self._get_monthly_project_data()
|
186 |
+
fig = px.line(monthly_data, x='month', y=['new', 'submitted', 'won'], title='اتجاه المشاريع الشهري')
|
187 |
+
st.plotly_chart(fig, use_container_width=True)
|
188 |
+
|
189 |
+
col1, col2 = st.columns(2)
|
190 |
+
|
191 |
+
with col1:
|
192 |
+
st.markdown("#### توزيع المشاريع حسب النوع")
|
193 |
+
project_type_data = self._get_project_type_data()
|
194 |
+
fig = px.bar(project_type_data, x='type', y='count', title='توزيع المشاريع حسب النوع')
|
195 |
+
st.plotly_chart(fig, use_container_width=True)
|
196 |
+
|
197 |
+
with col2:
|
198 |
+
st.markdown("#### توزيع المشاريع حسب الموقع")
|
199 |
+
project_location_data = self._get_project_location_data()
|
200 |
+
fig = px.bar(project_location_data, x='location', y='count', title='توزيع المشاريع حسب الموقع')
|
201 |
+
st.plotly_chart(fig, use_container_width=True)
|
202 |
+
|
203 |
+
st.markdown("#### أحدث المشاريع")
|
204 |
+
latest_projects = self._get_latest_projects()
|
205 |
+
st.dataframe(latest_projects)
|
206 |
+
|
207 |
+
def _render_projects_reports_tab(self):
|
208 |
+
"""عرض تبويب تقارير المشاريع"""
|
209 |
+
|
210 |
+
st.markdown("### تقارير المشاريع")
|
211 |
+
|
212 |
+
report_type = st.selectbox(
|
213 |
+
"نوع التقرير",
|
214 |
+
["تقرير حالة المشاريع", "تقرير أداء المشاريع", "تقرير المشاريع المتأخرة", "تقرير المشاريع المكتملة"]
|
215 |
+
)
|
216 |
+
|
217 |
+
if report_type == "تقرير حالة المشاريع":
|
218 |
+
self._render_project_status_report()
|
219 |
+
elif report_type == "تقرير أداء المشاريع":
|
220 |
+
self._render_project_performance_report()
|
221 |
+
elif report_type == "تقرير المشاريع المتأخرة":
|
222 |
+
self._render_delayed_projects_report()
|
223 |
+
elif report_type == "تقرير المشاريع المكتملة":
|
224 |
+
self._render_completed_projects_report()
|
225 |
+
|
226 |
+
def _render_pricing_reports_tab(self):
|
227 |
+
"""عرض تبويب تقارير التسعير"""
|
228 |
+
|
229 |
+
st.markdown("### تقارير التسعير")
|
230 |
+
|
231 |
+
report_type = st.selectbox(
|
232 |
+
"نوع التقرير",
|
233 |
+
["تقرير تحليل الأسعار", "تقرير مقارنة الأسعار", "تقرير اتجاهات الأسعار", "تقرير تحليل المنافسين"]
|
234 |
+
)
|
235 |
+
|
236 |
+
if report_type == "تقرير تحليل الأسعار":
|
237 |
+
self._render_price_analysis_report()
|
238 |
+
elif report_type == "تقرير مقارنة الأسعار":
|
239 |
+
self._render_price_comparison_report()
|
240 |
+
elif report_type == "تقرير اتجاهات الأسعار":
|
241 |
+
self._render_price_trends_report()
|
242 |
+
elif report_type == "تقرير تحليل المنافسين":
|
243 |
+
self._render_competitors_analysis_report()
|
244 |
+
|
245 |
+
def _render_risk_reports_tab(self):
|
246 |
+
"""عرض تبويب تقارير المخاطر"""
|
247 |
+
|
248 |
+
st.markdown("### تقارير المخاطر")
|
249 |
+
|
250 |
+
report_type = st.selectbox(
|
251 |
+
"نوع التقرير",
|
252 |
+
["تقرير تحليل المخاطر", "تقرير مصفوفة المخاطر", "تقرير متابعة المخاطر", "تقرير استراتيجيات التخفيف"]
|
253 |
+
)
|
254 |
+
|
255 |
+
if report_type == "تقرير تحليل المخاطر":
|
256 |
+
self._render_risk_analysis_report()
|
257 |
+
elif report_type == "تقرير مصفوفة المخاطر":
|
258 |
+
self._render_risk_matrix_report()
|
259 |
+
elif report_type == "تقرير متابعة المخاطر":
|
260 |
+
self._render_risk_monitoring_report()
|
261 |
+
elif report_type == "تقرير استراتيجيات التخفيف":
|
262 |
+
self._render_risk_mitigation_report()
|
263 |
+
|
264 |
+
def _render_custom_reports_tab(self):
|
265 |
+
"""عرض تبويب التقارير المخصصة"""
|
266 |
+
|
267 |
+
st.markdown("### التقارير المخصصة")
|
268 |
+
|
269 |
+
st.markdown("#### إنشاء تقرير مخصص")
|
270 |
+
|
271 |
+
col1, col2 = st.columns(2)
|
272 |
+
|
273 |
+
with col1:
|
274 |
+
report_name = st.text_input("اسم التقرير")
|
275 |
+
report_description = st.text_area("وصف التقرير")
|
276 |
+
|
277 |
+
with col2:
|
278 |
+
report_fields = st.multiselect(
|
279 |
+
"حقول التقرير",
|
280 |
+
["رقم المشروع", "اسم المشروع", "نوع المشروع", "حالة المشروع", "تاريخ البدء", "تاريخ الانتهاء", "الميزانية", "التكلفة الفعلية", "نسبة الإنجاز", "المخاطر", "الموقع", "المالك", "المقاول"]
|
281 |
+
)
|
282 |
+
|
283 |
+
report_filters = st.multiselect(
|
284 |
+
"تصفية التقرير",
|
285 |
+
["نوع المشروع", "حالة المشروع", "الفترة الزمنية", "الميزانية", "الموقع", "المالك", "المقاول"]
|
286 |
+
)
|
287 |
+
|
288 |
+
if st.button("إنشاء التقرير"):
|
289 |
+
if report_name and report_description and report_fields:
|
290 |
+
with st.spinner("جاري إنشاء التقرير..."):
|
291 |
+
time.sleep(2) # محاكاة وقت المعالجة
|
292 |
+
st.success("تم إنشاء التقرير بنجاح!")
|
293 |
+
|
294 |
+
# عرض التقرير المخصص (محاكاة)
|
295 |
+
custom_report_data = self._generate_custom_report(report_fields)
|
296 |
+
st.dataframe(custom_report_data)
|
297 |
+
|
298 |
+
# تصدير التقرير
|
299 |
+
st.download_button(
|
300 |
+
label="تصدير التقرير (Excel)",
|
301 |
+
data=self._export_to_excel(custom_report_data),
|
302 |
+
file_name=f"{report_name}.xlsx",
|
303 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
304 |
+
)
|
305 |
+
else:
|
306 |
+
st.warning("يرجى ملء جميع الحقول المطلوبة")
|
307 |
+
|
308 |
+
st.markdown("#### التقارير المخصصة المحفوظة")
|
309 |
+
|
310 |
+
saved_reports = [
|
311 |
+
{"id": 1, "name": "تقرير المشاريع المتأخرة في الرياض", "created_at": "2025-03-15", "last_run": "2025-03-30"},
|
312 |
+
{"id": 2, "name": "تقرير مشاريع الطرق ذات المخاطر العالية", "created_at": "2025-03-10", "last_run": "2025-03-28"},
|
313 |
+
{"id": 3, "name": "تقرير المشاريع المكتملة في الربع الأول", "created_at": "2025-03-05", "last_run": "2025-03-25"}
|
314 |
+
]
|
315 |
+
|
316 |
+
saved_reports_df = pd.DataFrame(saved_reports)
|
317 |
+
st.dataframe(saved_reports_df)
|
318 |
+
|
319 |
+
# تنفيذ دوال الحصول على البيانات
|
320 |
+
|
321 |
+
def _get_total_projects(self):
|
322 |
+
"""الحصول على إجمالي عدد المشاريع"""
|
323 |
+
# محاكاة البيانات
|
324 |
+
return 120
|
325 |
+
|
326 |
+
def _get_active_projects(self):
|
327 |
+
"""الحصول على عدد المشاريع النشطة"""
|
328 |
+
# محاكاة البيانات
|
329 |
+
return 45
|
330 |
+
|
331 |
+
def _get_won_projects(self):
|
332 |
+
"""الحصول على عدد المشاريع المرساة"""
|
333 |
+
# محاكاة البيانات
|
334 |
+
return 30
|
335 |
+
|
336 |
+
def _get_avg_local_content(self):
|
337 |
+
"""الحصول على متوسط المحتوى المحلي"""
|
338 |
+
# محاكاة البيانات
|
339 |
+
return 75.5
|
340 |
+
|
341 |
+
def _get_project_status_data(self):
|
342 |
+
"""الحصول على بيانات توزيع المشاريع حسب الحالة"""
|
343 |
+
# محاكاة البيانات
|
344 |
+
data = {
|
345 |
+
'status': ['جديد', 'قيد التقديم', 'تم التقديم', 'فائز', 'خاسر', 'ملغي'],
|
346 |
+
'count': [25, 20, 15, 30, 25, 5]
|
347 |
+
}
|
348 |
+
return pd.DataFrame(data)
|
349 |
+
|
350 |
+
def _get_monthly_project_data(self):
|
351 |
+
"""الحصول على بيانات اتجاه المشاريع الشهري"""
|
352 |
+
# محاكاة البيانات
|
353 |
+
data = {
|
354 |
+
'month': ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو'],
|
355 |
+
'new': [10, 15, 12, 8, 20, 18],
|
356 |
+
'submitted': [8, 12, 10, 6, 15, 14],
|
357 |
+
'won': [5, 8, 6, 4, 10, 9]
|
358 |
+
}
|
359 |
+
return pd.DataFrame(data)
|
360 |
+
|
361 |
+
def _get_project_type_data(self):
|
362 |
+
"""الحصول على بيانات توزيع المشاريع حسب النوع"""
|
363 |
+
# محاكاة البيانات
|
364 |
+
data = {
|
365 |
+
'type': ['مباني', 'طرق', 'جسور', 'أنفاق', 'بنية تحتية', 'أخرى'],
|
366 |
+
'count': [40, 30, 15, 10, 20, 5]
|
367 |
+
}
|
368 |
+
return pd.DataFrame(data)
|
369 |
+
|
370 |
+
def _get_project_location_data(self):
|
371 |
+
"""الحصول على بيانات توزيع المشاريع حسب الموقع"""
|
372 |
+
# محاكاة البيانات
|
373 |
+
data = {
|
374 |
+
'location': ['الرياض', 'جدة', 'الدمام', 'مكة', 'المدينة', 'أخرى'],
|
375 |
+
'count': [35, 25, 20, 15, 10, 15]
|
376 |
+
}
|
377 |
+
return pd.DataFrame(data)
|
378 |
+
|
379 |
+
def _get_latest_projects(self):
|
380 |
+
"""الحصول على بيانات أحدث المشاريع"""
|
381 |
+
# محاكاة البيانات
|
382 |
+
data = {
|
383 |
+
'رقم المشروع': ['P-2025-001', 'P-2025-002', 'P-2025-003', 'P-2025-004', 'P-2025-005'],
|
384 |
+
'اسم المشروع': ['إنشاء مبنى إداري', 'تطوير شبكة طرق', 'إنشاء جسر', 'بناء مدرسة', 'تطوير شبكة مياه'],
|
385 |
+
'نوع المشروع': ['مباني', 'طرق', 'جسور', 'مباني', 'بنية تحتية'],
|
386 |
+
'حالة المشروع': ['جديد', 'قيد التقديم', 'تم التقديم', 'فائز', 'جديد'],
|
387 |
+
'تاريخ الإضافة': ['2025-03-30', '2025-03-28', '2025-03-25', '2025-03-20', '2025-03-18']
|
388 |
+
}
|
389 |
+
return pd.DataFrame(data)
|
390 |
+
|
391 |
+
# تنفيذ دوال عرض التقارير
|
392 |
+
|
393 |
+
def _render_project_status_report(self):
|
394 |
+
"""عرض تقرير حالة المشاريع"""
|
395 |
+
|
396 |
+
st.markdown("#### تقرير حالة المشاريع")
|
397 |
+
|
398 |
+
# محاكاة بيانات التقرير
|
399 |
+
data = {
|
400 |
+
'رقم المشروع': ['P-2025-001', 'P-2025-002', 'P-2025-003', 'P-2025-004', 'P-2025-005', 'P-2025-006', 'P-2025-007', 'P-2025-008', 'P-2025-009', 'P-2025-010'],
|
401 |
+
'اسم المشروع': ['إنشاء مبنى إداري', 'تطوير شبكة طرق', 'إنشاء جسر', 'بناء مدرسة', 'تطوير شبكة مياه', 'إنشاء مستشفى', 'بناء مركز تجاري', 'تطوير حديقة عامة', 'إنشاء مصنع', 'تطوير مطار'],
|
402 |
+
'نوع المشروع': ['مباني', 'طرق', 'جسور', 'مباني', 'بنية تحتية', 'مباني', 'مباني', 'أخرى', 'مباني', 'بنية تحتية'],
|
403 |
+
'
|
404 |
+
(Content truncated due to size limit. Use line ranges to read in chunks)
|
405 |
|
modules/resources/resources_app.py
CHANGED
@@ -18,15 +18,15 @@ from pathlib import Path
|
|
18 |
|
19 |
class ResourcesApp:
|
20 |
"""وحدة الموارد"""
|
21 |
-
|
22 |
def __init__(self):
|
23 |
"""تهيئة وحدة الموارد"""
|
24 |
-
|
25 |
# تهيئة حالة الجلسة
|
26 |
if 'resources_data' not in st.session_state:
|
27 |
# إنشاء بيانات افتراضية للموارد البشرية
|
28 |
np.random.seed(42)
|
29 |
-
|
30 |
# إنشاء بيانات الموظفين
|
31 |
n_employees = 50
|
32 |
employee_ids = [f"EMP-{i+1:03d}" for i in range(n_employees)]
|
@@ -47,7 +47,7 @@ class ResourcesApp:
|
|
47 |
employee_salaries = np.random.randint(5000, 25000, n_employees)
|
48 |
employee_experiences = np.random.randint(1, 20, n_employees)
|
49 |
employee_availabilities = np.random.choice([True, False], n_employees, p=[0.7, 0.3])
|
50 |
-
|
51 |
# إنشاء DataFrame للموظفين
|
52 |
employees_data = {
|
53 |
"رقم الموظف": employee_ids,
|
@@ -58,7 +58,7 @@ class ResourcesApp:
|
|
58 |
"سنوات الخبرة": employee_experiences,
|
59 |
"متاح": employee_availabilities
|
60 |
}
|
61 |
-
|
62 |
# إنشاء بيانات المعدات
|
63 |
n_equipment = 30
|
64 |
equipment_ids = [f"EQP-{i+1:03d}" for i in range(n_equipment)]
|
@@ -74,7 +74,7 @@ class ResourcesApp:
|
|
74 |
equipment_availabilities = np.random.choice([True, False], n_equipment, p=[0.8, 0.2])
|
75 |
equipment_conditions = np.random.choice(["ممتاز", "جيد", "متوسط", "سيء"], n_equipment, p=[0.4, 0.3, 0.2, 0.1])
|
76 |
equipment_locations = np.random.choice(["المستودع", "موقع العمل 1", "موقع العمل 2", "موقع العمل 3", "في الصيانة"], n_equipment)
|
77 |
-
|
78 |
# إنشاء DataFrame للمعدات
|
79 |
equipment_data = {
|
80 |
"رقم المعدة": equipment_ids,
|
@@ -85,7 +85,7 @@ class ResourcesApp:
|
|
85 |
"الحالة": equipment_conditions,
|
86 |
"الموقع": equipment_locations
|
87 |
}
|
88 |
-
|
89 |
# إنشاء بيانات المواد
|
90 |
n_materials = 40
|
91 |
material_ids = [f"MAT-{i+1:03d}" for i in range(n_materials)]
|
@@ -102,7 +102,7 @@ class ResourcesApp:
|
|
102 |
material_costs = np.random.randint(50, 5000, n_materials)
|
103 |
material_suppliers = np.random.choice(["المورد 1", "المورد 2", "المورد 3", "المورد 4", "المورد 5"], n_materials)
|
104 |
material_lead_times = np.random.randint(1, 30, n_materials)
|
105 |
-
|
106 |
# إنشاء DataFrame للمواد
|
107 |
materials_data = {
|
108 |
"رقم المادة": material_ids,
|
@@ -113,7 +113,7 @@ class ResourcesApp:
|
|
113 |
"المورد": material_suppliers,
|
114 |
"مدة التوريد (يوم)": material_lead_times
|
115 |
}
|
116 |
-
|
117 |
# إنشاء بيانات المشاريع
|
118 |
n_projects = 10
|
119 |
project_ids = [f"PRJ-{i+1:03d}" for i in range(n_projects)]
|
@@ -134,7 +134,7 @@ class ResourcesApp:
|
|
134 |
]
|
135 |
project_budgets = np.random.randint(1000000, 50000000, n_projects)
|
136 |
project_statuses = np.random.choice(["قيد التنفيذ", "مكتمل", "متوقف", "مخطط"], n_projects)
|
137 |
-
|
138 |
# إنشاء DataFrame للمشاريع
|
139 |
projects_data = {
|
140 |
"رقم المشروع": project_ids,
|
@@ -145,7 +145,7 @@ class ResourcesApp:
|
|
145 |
"الميزانية": project_budgets,
|
146 |
"الحالة": project_statuses
|
147 |
}
|
148 |
-
|
149 |
# إنشاء بيانات تخصيص الموارد للمشاريع
|
150 |
n_allocations = 100
|
151 |
allocation_ids = [f"ALLOC-{i+1:03d}" for i in range(n_allocations)]
|
@@ -159,7 +159,7 @@ class ResourcesApp:
|
|
159 |
allocation_resource_ids.append(np.random.choice(equipment_ids))
|
160 |
else:
|
161 |
allocation_resource_ids.append(np.random.choice(material_ids))
|
162 |
-
|
163 |
allocation_start_dates = [
|
164 |
(datetime.now() - timedelta(days=np.random.randint(0, 90))).strftime("%Y-%m-%d")
|
165 |
for _ in range(n_allocations)
|
@@ -170,7 +170,7 @@ class ResourcesApp:
|
|
170 |
]
|
171 |
allocation_quantities = np.random.randint(1, 10, n_allocations)
|
172 |
allocation_costs = np.random.randint(5000, 50000, n_allocations)
|
173 |
-
|
174 |
# إنشاء DataFrame لتخصيص الموارد
|
175 |
allocations_data = {
|
176 |
"رقم التخصيص": allocation_ids,
|
@@ -182,7 +182,7 @@ class ResourcesApp:
|
|
182 |
"الكمية": allocation_quantities,
|
183 |
"التكلفة": allocation_costs
|
184 |
}
|
185 |
-
|
186 |
# تخزين البيانات في حالة الجلسة
|
187 |
st.session_state.resources_data = {
|
188 |
"employees": pd.DataFrame(employees_data),
|
@@ -191,22 +191,22 @@ class ResourcesApp:
|
|
191 |
"projects": pd.DataFrame(projects_data),
|
192 |
"allocations": pd.DataFrame(allocations_data)
|
193 |
}
|
194 |
-
|
195 |
def run(self):
|
196 |
"""
|
197 |
تشغيل وحدة الموارد
|
198 |
-
|
199 |
هذه الدالة هي نقطة الدخول الرئيسية لوحدة الموارد وتقوم بتهيئة واجهة المستخدم
|
200 |
وعرض جميع العناصر المطلوبة.
|
201 |
"""
|
202 |
# استدعاء دالة العرض الرئيسية
|
203 |
self.render()
|
204 |
-
|
205 |
def render(self):
|
206 |
"""عرض واجهة وحدة الموارد"""
|
207 |
-
|
208 |
st.markdown("<h1 class='module-title'>وحدة الموارد</h1>", unsafe_allow_html=True)
|
209 |
-
|
210 |
tabs = st.tabs([
|
211 |
"لوحة المعلومات",
|
212 |
"الموارد البشرية",
|
@@ -215,68 +215,68 @@ class ResourcesApp:
|
|
215 |
"تخصيص الموارد",
|
216 |
"تخطيط الموارد"
|
217 |
])
|
218 |
-
|
219 |
with tabs[0]:
|
220 |
self._render_dashboard_tab()
|
221 |
-
|
222 |
with tabs[1]:
|
223 |
self._render_human_resources_tab()
|
224 |
-
|
225 |
with tabs[2]:
|
226 |
self._render_equipment_tab()
|
227 |
-
|
228 |
with tabs[3]:
|
229 |
self._render_materials_tab()
|
230 |
-
|
231 |
with tabs[4]:
|
232 |
self._render_resource_allocation_tab()
|
233 |
-
|
234 |
with tabs[5]:
|
235 |
self._render_resource_planning_tab()
|
236 |
-
|
237 |
def _render_dashboard_tab(self):
|
238 |
"""عرض تبويب لوحة المعلومات"""
|
239 |
-
|
240 |
st.markdown("### لوحة معلومات الموارد")
|
241 |
-
|
242 |
# استخراج البيانات
|
243 |
employees_df = st.session_state.resources_data["employees"]
|
244 |
equipment_df = st.session_state.resources_data["equipment"]
|
245 |
materials_df = st.session_state.resources_data["materials"]
|
246 |
projects_df = st.session_state.resources_data["projects"]
|
247 |
allocations_df = st.session_state.resources_data["allocations"]
|
248 |
-
|
249 |
# عرض مؤشرات الأداء الرئيسية
|
250 |
st.markdown("#### مؤشرات الأداء الرئيسية")
|
251 |
-
|
252 |
col1, col2, col3, col4 = st.columns(4)
|
253 |
-
|
254 |
with col1:
|
255 |
total_employees = len(employees_df)
|
256 |
available_employees = len(employees_df[employees_df["متاح"] == True])
|
257 |
st.metric("الموظفون", f"{available_employees}/{total_employees}")
|
258 |
-
|
259 |
with col2:
|
260 |
total_equipment = len(equipment_df)
|
261 |
available_equipment = len(equipment_df[equipment_df["متاحة"] == True])
|
262 |
st.metric("المعدات", f"{available_equipment}/{total_equipment}")
|
263 |
-
|
264 |
with col3:
|
265 |
total_materials = len(materials_df)
|
266 |
low_stock_materials = len(materials_df[materials_df["الكمية المتاحة"] < 50])
|
267 |
st.metric("المواد", f"{total_materials}", f"-{low_stock_materials} منخفضة المخزون")
|
268 |
-
|
269 |
with col4:
|
270 |
total_projects = len(projects_df)
|
271 |
active_projects = len(projects_df[projects_df["الحالة"] == "قيد التنفيذ"])
|
272 |
st.metric("المشاريع النشطة", f"{active_projects}/{total_projects}")
|
273 |
-
|
274 |
# عرض توزيع الموارد البشرية حسب القسم
|
275 |
st.markdown("#### توزيع الموارد البشرية حسب القسم")
|
276 |
-
|
277 |
dept_counts = employees_df["القسم"].value_counts().reset_index()
|
278 |
dept_counts.columns = ["القسم", "العدد"]
|
279 |
-
|
280 |
fig = px.pie(
|
281 |
dept_counts,
|
282 |
values="العدد",
|
@@ -284,15 +284,15 @@ class ResourcesApp:
|
|
284 |
title="توزيع الموظفين حسب القسم",
|
285 |
color="القسم"
|
286 |
)
|
287 |
-
|
288 |
st.plotly_chart(fig, use_container_width=True)
|
289 |
-
|
290 |
# عرض توزيع المعدات حسب النوع
|
291 |
st.markdown("#### توزيع المعدات حسب النوع")
|
292 |
-
|
293 |
type_counts = equipment_df["النوع"].value_counts().reset_index()
|
294 |
type_counts.columns = ["النوع", "العدد"]
|
295 |
-
|
296 |
fig = px.bar(
|
297 |
type_counts,
|
298 |
x="النوع",
|
@@ -301,15 +301,15 @@ class ResourcesApp:
|
|
301 |
color="النوع",
|
302 |
text_auto=True
|
303 |
)
|
304 |
-
|
305 |
st.plotly_chart(fig, use_container_width=True)
|
306 |
-
|
307 |
# عرض توزيع المواد حسب المورد
|
308 |
st.markdown("#### توزيع المواد حسب المورد")
|
309 |
-
|
310 |
supplier_counts = materials_df["المورد"].value_counts().reset_index()
|
311 |
supplier_counts.columns = ["المورد", "العدد"]
|
312 |
-
|
313 |
fig = px.pie(
|
314 |
supplier_counts,
|
315 |
values="العدد",
|
@@ -317,27 +317,27 @@ class ResourcesApp:
|
|
317 |
title="توزيع المواد حسب المورد",
|
318 |
color="المورد"
|
319 |
)
|
320 |
-
|
321 |
st.plotly_chart(fig, use_container_width=True)
|
322 |
-
|
323 |
# عرض توزيع تكاليف الموارد
|
324 |
st.markdown("#### توزيع تكاليف الموارد")
|
325 |
-
|
326 |
# حساب إجمالي تكاليف الموظفين
|
327 |
total_employee_cost = employees_df["التكلفة الشهرية"].sum()
|
328 |
-
|
329 |
# حساب إجمالي تكاليف المعدات (افتراضياً لشهر واحد)
|
330 |
total_equipment_cost = equipment_df["التكلفة اليومية"].sum() * 30
|
331 |
-
|
332 |
# حساب إجمالي تكاليف المواد
|
333 |
total_material_cost = (materials_df["الكمية المتاحة"] * materials_df["تكلفة الوحدة"]).sum()
|
334 |
-
|
335 |
# إنشاء DataFrame لتوزيع التكاليف
|
336 |
cost_distribution = pd.DataFrame({
|
337 |
"نوع المورد": ["الموظفون", "المعدات", "المواد"],
|
338 |
"التكلفة": [total_employee_cost, total_equipment_cost, total_material_cost]
|
339 |
})
|
340 |
-
|
341 |
fig = px.pie(
|
342 |
cost_distribution,
|
343 |
values="التكلفة",
|
@@ -350,30 +350,59 @@ class ResourcesApp:
|
|
350 |
"المواد": "#f39c12"
|
351 |
}
|
352 |
)
|
353 |
-
|
354 |
st.plotly_chart(fig, use_container_width=True)
|
355 |
-
|
356 |
# عرض تخصيص الموارد للمشاريع
|
357 |
st.markdown("#### تخصيص الموارد للمشاريع")
|
358 |
-
|
359 |
# حساب عدد الموارد المخصصة لكل مشروع
|
360 |
project_allocations = allocations_df["رقم المشروع"].value_counts().reset_index()
|
361 |
project_allocations.columns = ["رقم المشروع", "عدد الموارد المخصصة"]
|
362 |
-
|
363 |
# دمج بيانات المشاريع مع بيانات التخصيص
|
364 |
project_allocations = project_allocations.merge(
|
365 |
projects_df[["رقم المشروع", "اسم المشروع", "الحالة"]],
|
366 |
on="رقم المشروع",
|
367 |
how="left"
|
368 |
)
|
369 |
-
|
370 |
fig = px.bar(
|
371 |
project_allocations,
|
372 |
x="اسم المشروع",
|
373 |
-
y="
|
374 |
title="توزيع الموارد على المشاريع",
|
375 |
-
color="اسم المورد",
|
376 |
text_auto=True
|
377 |
-
)
|
378 |
st.plotly_chart(fig, use_container_width=True)
|
379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
class ResourcesApp:
|
20 |
"""وحدة الموارد"""
|
21 |
+
|
22 |
def __init__(self):
|
23 |
"""تهيئة وحدة الموارد"""
|
24 |
+
|
25 |
# تهيئة حالة الجلسة
|
26 |
if 'resources_data' not in st.session_state:
|
27 |
# إنشاء بيانات افتراضية للموارد البشرية
|
28 |
np.random.seed(42)
|
29 |
+
|
30 |
# إنشاء بيانات الموظفين
|
31 |
n_employees = 50
|
32 |
employee_ids = [f"EMP-{i+1:03d}" for i in range(n_employees)]
|
|
|
47 |
employee_salaries = np.random.randint(5000, 25000, n_employees)
|
48 |
employee_experiences = np.random.randint(1, 20, n_employees)
|
49 |
employee_availabilities = np.random.choice([True, False], n_employees, p=[0.7, 0.3])
|
50 |
+
|
51 |
# إنشاء DataFrame للموظفين
|
52 |
employees_data = {
|
53 |
"رقم الموظف": employee_ids,
|
|
|
58 |
"سنوات الخبرة": employee_experiences,
|
59 |
"متاح": employee_availabilities
|
60 |
}
|
61 |
+
|
62 |
# إنشاء بيانات المعدات
|
63 |
n_equipment = 30
|
64 |
equipment_ids = [f"EQP-{i+1:03d}" for i in range(n_equipment)]
|
|
|
74 |
equipment_availabilities = np.random.choice([True, False], n_equipment, p=[0.8, 0.2])
|
75 |
equipment_conditions = np.random.choice(["ممتاز", "جيد", "متوسط", "سيء"], n_equipment, p=[0.4, 0.3, 0.2, 0.1])
|
76 |
equipment_locations = np.random.choice(["المستودع", "موقع العمل 1", "موقع العمل 2", "موقع العمل 3", "في الصيانة"], n_equipment)
|
77 |
+
|
78 |
# إنشاء DataFrame للمعدات
|
79 |
equipment_data = {
|
80 |
"رقم المعدة": equipment_ids,
|
|
|
85 |
"الحالة": equipment_conditions,
|
86 |
"الموقع": equipment_locations
|
87 |
}
|
88 |
+
|
89 |
# إنشاء بيانات المواد
|
90 |
n_materials = 40
|
91 |
material_ids = [f"MAT-{i+1:03d}" for i in range(n_materials)]
|
|
|
102 |
material_costs = np.random.randint(50, 5000, n_materials)
|
103 |
material_suppliers = np.random.choice(["المورد 1", "المورد 2", "المورد 3", "المورد 4", "المورد 5"], n_materials)
|
104 |
material_lead_times = np.random.randint(1, 30, n_materials)
|
105 |
+
|
106 |
# إنشاء DataFrame للمواد
|
107 |
materials_data = {
|
108 |
"رقم المادة": material_ids,
|
|
|
113 |
"المورد": material_suppliers,
|
114 |
"مدة التوريد (يوم)": material_lead_times
|
115 |
}
|
116 |
+
|
117 |
# إنشاء بيانات المشاريع
|
118 |
n_projects = 10
|
119 |
project_ids = [f"PRJ-{i+1:03d}" for i in range(n_projects)]
|
|
|
134 |
]
|
135 |
project_budgets = np.random.randint(1000000, 50000000, n_projects)
|
136 |
project_statuses = np.random.choice(["قيد التنفيذ", "مكتمل", "متوقف", "مخطط"], n_projects)
|
137 |
+
|
138 |
# إنشاء DataFrame للمشاريع
|
139 |
projects_data = {
|
140 |
"رقم المشروع": project_ids,
|
|
|
145 |
"الميزانية": project_budgets,
|
146 |
"الحالة": project_statuses
|
147 |
}
|
148 |
+
|
149 |
# إنشاء بيانات تخصيص الموارد للمشاريع
|
150 |
n_allocations = 100
|
151 |
allocation_ids = [f"ALLOC-{i+1:03d}" for i in range(n_allocations)]
|
|
|
159 |
allocation_resource_ids.append(np.random.choice(equipment_ids))
|
160 |
else:
|
161 |
allocation_resource_ids.append(np.random.choice(material_ids))
|
162 |
+
|
163 |
allocation_start_dates = [
|
164 |
(datetime.now() - timedelta(days=np.random.randint(0, 90))).strftime("%Y-%m-%d")
|
165 |
for _ in range(n_allocations)
|
|
|
170 |
]
|
171 |
allocation_quantities = np.random.randint(1, 10, n_allocations)
|
172 |
allocation_costs = np.random.randint(5000, 50000, n_allocations)
|
173 |
+
|
174 |
# إنشاء DataFrame لتخصيص الموارد
|
175 |
allocations_data = {
|
176 |
"رقم التخصيص": allocation_ids,
|
|
|
182 |
"الكمية": allocation_quantities,
|
183 |
"التكلفة": allocation_costs
|
184 |
}
|
185 |
+
|
186 |
# تخزين البيانات في حالة الجلسة
|
187 |
st.session_state.resources_data = {
|
188 |
"employees": pd.DataFrame(employees_data),
|
|
|
191 |
"projects": pd.DataFrame(projects_data),
|
192 |
"allocations": pd.DataFrame(allocations_data)
|
193 |
}
|
194 |
+
|
195 |
def run(self):
|
196 |
"""
|
197 |
تشغيل وحدة الموارد
|
198 |
+
|
199 |
هذه الدالة هي نقطة الدخول الرئيسية لوحدة الموارد وتقوم بتهيئة واجهة المستخدم
|
200 |
وعرض جميع العناصر المطلوبة.
|
201 |
"""
|
202 |
# استدعاء دالة العرض الرئيسية
|
203 |
self.render()
|
204 |
+
|
205 |
def render(self):
|
206 |
"""عرض واجهة وحدة الموارد"""
|
207 |
+
|
208 |
st.markdown("<h1 class='module-title'>وحدة الموارد</h1>", unsafe_allow_html=True)
|
209 |
+
|
210 |
tabs = st.tabs([
|
211 |
"لوحة المعلومات",
|
212 |
"الموارد البشرية",
|
|
|
215 |
"تخصيص الموارد",
|
216 |
"تخطيط الموارد"
|
217 |
])
|
218 |
+
|
219 |
with tabs[0]:
|
220 |
self._render_dashboard_tab()
|
221 |
+
|
222 |
with tabs[1]:
|
223 |
self._render_human_resources_tab()
|
224 |
+
|
225 |
with tabs[2]:
|
226 |
self._render_equipment_tab()
|
227 |
+
|
228 |
with tabs[3]:
|
229 |
self._render_materials_tab()
|
230 |
+
|
231 |
with tabs[4]:
|
232 |
self._render_resource_allocation_tab()
|
233 |
+
|
234 |
with tabs[5]:
|
235 |
self._render_resource_planning_tab()
|
236 |
+
|
237 |
def _render_dashboard_tab(self):
|
238 |
"""عرض تبويب لوحة المعلومات"""
|
239 |
+
|
240 |
st.markdown("### لوحة معلومات الموارد")
|
241 |
+
|
242 |
# استخراج البيانات
|
243 |
employees_df = st.session_state.resources_data["employees"]
|
244 |
equipment_df = st.session_state.resources_data["equipment"]
|
245 |
materials_df = st.session_state.resources_data["materials"]
|
246 |
projects_df = st.session_state.resources_data["projects"]
|
247 |
allocations_df = st.session_state.resources_data["allocations"]
|
248 |
+
|
249 |
# عرض مؤشرات الأداء الرئيسية
|
250 |
st.markdown("#### مؤشرات الأداء الرئيسية")
|
251 |
+
|
252 |
col1, col2, col3, col4 = st.columns(4)
|
253 |
+
|
254 |
with col1:
|
255 |
total_employees = len(employees_df)
|
256 |
available_employees = len(employees_df[employees_df["متاح"] == True])
|
257 |
st.metric("الموظفون", f"{available_employees}/{total_employees}")
|
258 |
+
|
259 |
with col2:
|
260 |
total_equipment = len(equipment_df)
|
261 |
available_equipment = len(equipment_df[equipment_df["متاحة"] == True])
|
262 |
st.metric("المعدات", f"{available_equipment}/{total_equipment}")
|
263 |
+
|
264 |
with col3:
|
265 |
total_materials = len(materials_df)
|
266 |
low_stock_materials = len(materials_df[materials_df["الكمية المتاحة"] < 50])
|
267 |
st.metric("المواد", f"{total_materials}", f"-{low_stock_materials} منخفضة المخزون")
|
268 |
+
|
269 |
with col4:
|
270 |
total_projects = len(projects_df)
|
271 |
active_projects = len(projects_df[projects_df["الحالة"] == "قيد التنفيذ"])
|
272 |
st.metric("المشاريع النشطة", f"{active_projects}/{total_projects}")
|
273 |
+
|
274 |
# عرض توزيع الموارد البشرية حسب القسم
|
275 |
st.markdown("#### توزيع الموارد البشرية حسب القسم")
|
276 |
+
|
277 |
dept_counts = employees_df["القسم"].value_counts().reset_index()
|
278 |
dept_counts.columns = ["القسم", "العدد"]
|
279 |
+
|
280 |
fig = px.pie(
|
281 |
dept_counts,
|
282 |
values="العدد",
|
|
|
284 |
title="توزيع الموظفين حسب القسم",
|
285 |
color="القسم"
|
286 |
)
|
287 |
+
|
288 |
st.plotly_chart(fig, use_container_width=True)
|
289 |
+
|
290 |
# عرض توزيع المعدات حسب النوع
|
291 |
st.markdown("#### توزيع المعدات حسب النوع")
|
292 |
+
|
293 |
type_counts = equipment_df["النوع"].value_counts().reset_index()
|
294 |
type_counts.columns = ["النوع", "العدد"]
|
295 |
+
|
296 |
fig = px.bar(
|
297 |
type_counts,
|
298 |
x="النوع",
|
|
|
301 |
color="النوع",
|
302 |
text_auto=True
|
303 |
)
|
304 |
+
|
305 |
st.plotly_chart(fig, use_container_width=True)
|
306 |
+
|
307 |
# عرض توزيع المواد حسب المورد
|
308 |
st.markdown("#### توزيع المواد حسب المورد")
|
309 |
+
|
310 |
supplier_counts = materials_df["المورد"].value_counts().reset_index()
|
311 |
supplier_counts.columns = ["المورد", "العدد"]
|
312 |
+
|
313 |
fig = px.pie(
|
314 |
supplier_counts,
|
315 |
values="العدد",
|
|
|
317 |
title="توزيع المواد حسب المورد",
|
318 |
color="المورد"
|
319 |
)
|
320 |
+
|
321 |
st.plotly_chart(fig, use_container_width=True)
|
322 |
+
|
323 |
# عرض توزيع تكاليف الموارد
|
324 |
st.markdown("#### توزيع تكاليف الموارد")
|
325 |
+
|
326 |
# حساب إجمالي تكاليف الموظفين
|
327 |
total_employee_cost = employees_df["التكلفة الشهرية"].sum()
|
328 |
+
|
329 |
# حساب إجمالي تكاليف المعدات (افتراضياً لشهر واحد)
|
330 |
total_equipment_cost = equipment_df["التكلفة اليومية"].sum() * 30
|
331 |
+
|
332 |
# حساب إجمالي تكاليف المواد
|
333 |
total_material_cost = (materials_df["الكمية المتاحة"] * materials_df["تكلفة الوحدة"]).sum()
|
334 |
+
|
335 |
# إنشاء DataFrame لتوزيع التكاليف
|
336 |
cost_distribution = pd.DataFrame({
|
337 |
"نوع المورد": ["الموظفون", "المعدات", "المواد"],
|
338 |
"التكلفة": [total_employee_cost, total_equipment_cost, total_material_cost]
|
339 |
})
|
340 |
+
|
341 |
fig = px.pie(
|
342 |
cost_distribution,
|
343 |
values="التكلفة",
|
|
|
350 |
"المواد": "#f39c12"
|
351 |
}
|
352 |
)
|
353 |
+
|
354 |
st.plotly_chart(fig, use_container_width=True)
|
355 |
+
|
356 |
# عرض تخصيص الموارد للمشاريع
|
357 |
st.markdown("#### تخصيص الموارد للمشاريع")
|
358 |
+
|
359 |
# حساب عدد الموارد المخصصة لكل مشروع
|
360 |
project_allocations = allocations_df["رقم المشروع"].value_counts().reset_index()
|
361 |
project_allocations.columns = ["رقم المشروع", "عدد الموارد المخصصة"]
|
362 |
+
|
363 |
# دمج بيانات المشاريع مع بيانات التخصيص
|
364 |
project_allocations = project_allocations.merge(
|
365 |
projects_df[["رقم المشروع", "اسم المشروع", "الحالة"]],
|
366 |
on="رقم المشروع",
|
367 |
how="left"
|
368 |
)
|
369 |
+
|
370 |
fig = px.bar(
|
371 |
project_allocations,
|
372 |
x="اسم المشروع",
|
373 |
+
y="عدد الموارد المخصصة",
|
374 |
title="توزيع الموارد على المشاريع",
|
|
|
375 |
text_auto=True
|
376 |
+
)
|
377 |
st.plotly_chart(fig, use_container_width=True)
|
378 |
|
379 |
+
|
380 |
+
def _render_human_resources_tab(self):
|
381 |
+
"""عرض تبويب الموارد البشرية"""
|
382 |
+
st.markdown("### الموارد البشرية")
|
383 |
+
# إضافة محتوى تبويب الموارد البشرية هنا ...
|
384 |
+
pass
|
385 |
+
|
386 |
+
def _render_equipment_tab(self):
|
387 |
+
"""عرض تبويب المعدات"""
|
388 |
+
st.markdown("### المعدات")
|
389 |
+
# إضافة محتوى تبويب المعدات هنا ...
|
390 |
+
pass
|
391 |
+
|
392 |
+
def _render_materials_tab(self):
|
393 |
+
"""عرض تبويب المواد"""
|
394 |
+
st.markdown("### المواد")
|
395 |
+
# إضافة محتوى تبويب المواد هنا ...
|
396 |
+
pass
|
397 |
+
|
398 |
+
def _render_resource_allocation_tab(self):
|
399 |
+
"""عرض تبويب تخصيص الموارد"""
|
400 |
+
st.markdown("### تخصيص الموارد")
|
401 |
+
# إضافة محتوى تبويب تخصيص الموارد هنا ...
|
402 |
+
pass
|
403 |
+
|
404 |
+
def _render_resource_planning_tab(self):
|
405 |
+
"""عرض تبويب تخطيط الموارد"""
|
406 |
+
st.markdown("### تخطيط الموارد")
|
407 |
+
# إضافة محتوى تبويب تخطيط الموارد هنا ...
|
408 |
+
pass
|
modules/risk_analysis/risk_analyzer.py
CHANGED
@@ -14,6 +14,7 @@ import pandas as pd
|
|
14 |
import numpy as np
|
15 |
import matplotlib.pyplot as plt
|
16 |
import sys
|
|
|
17 |
|
18 |
# إضافة مسار المشروع للنظام
|
19 |
sys.path.append(str(Path(__file__).parent.parent))
|
@@ -28,526 +29,168 @@ logging.basicConfig(
|
|
28 |
)
|
29 |
logger = logging.getLogger('risk_analysis')
|
30 |
|
|
|
|
|
|
|
|
|
31 |
class RiskAnalyzer:
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
self.exports_path = Path('data/exports')
|
47 |
-
|
48 |
-
if not self.exports_path.exists():
|
49 |
-
self.exports_path.mkdir(parents=True, exist_ok=True)
|
50 |
-
|
51 |
-
def analyze_risks(self, project_id, method="comprehensive", callback=None):
|
52 |
-
"""تحليل مخاطر المشروع"""
|
53 |
-
if self.analysis_in_progress:
|
54 |
-
logger.warning("هناك عملية تحليل مخاطر جارية بالفعل")
|
55 |
-
return False
|
56 |
-
|
57 |
-
self.analysis_in_progress = True
|
58 |
-
self.current_project = project_id
|
59 |
-
self.analysis_results = {
|
60 |
-
"project_id": project_id,
|
61 |
-
"method": method,
|
62 |
-
"analysis_start_time": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
63 |
-
"status": "جاري التحليل",
|
64 |
-
"identified_risks": [],
|
65 |
-
"risk_categories": {},
|
66 |
-
"risk_matrix": {},
|
67 |
-
"mitigation_strategies": [],
|
68 |
-
"summary": {}
|
69 |
-
}
|
70 |
-
|
71 |
-
# بدء التحليل في خيط منفصل
|
72 |
-
thread = threading.Thread(
|
73 |
-
target=self._analyze_risks_thread,
|
74 |
-
args=(project_id, method, callback)
|
75 |
-
)
|
76 |
-
thread.daemon = True
|
77 |
-
thread.start()
|
78 |
-
|
79 |
-
return True
|
80 |
-
|
81 |
-
def _analyze_risks_thread(self, project_id, method, callback):
|
82 |
-
"""خيط تحليل المخاطر"""
|
83 |
-
try:
|
84 |
-
# محاكاة جلب بيانات المشروع من قاعدة البيانات
|
85 |
-
project_data = self._get_project_data(project_id)
|
86 |
-
|
87 |
-
if not project_data:
|
88 |
-
logger.error(f"لم يتم العثور على بيانات المشروع: {project_id}")
|
89 |
-
self.analysis_results["status"] = "فشل التحليل"
|
90 |
-
self.analysis_results["error"] = "لم يتم العثور على بيانات المشروع"
|
91 |
-
return
|
92 |
-
|
93 |
-
# تحديد المخاطر
|
94 |
-
self._identify_risks(project_data, method)
|
95 |
-
|
96 |
-
# تصنيف المخاطر
|
97 |
-
self._categorize_risks()
|
98 |
-
|
99 |
-
# إنشاء مصفوفة المخاطر
|
100 |
-
self._create_risk_matrix()
|
101 |
-
|
102 |
-
# تطوير استراتيجيات التخفيف
|
103 |
-
self._develop_mitigation_strategies(method)
|
104 |
-
|
105 |
-
# إنشاء ملخص التحليل
|
106 |
-
self._create_analysis_summary(method)
|
107 |
-
|
108 |
-
# تحديث حالة التحليل
|
109 |
-
self.analysis_results["status"] = "اكتمل التحليل"
|
110 |
-
self.analysis_results["analysis_end_time"] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
111 |
-
|
112 |
-
logger.info(f"اكتمل تحليل مخاطر المشروع: {project_id}")
|
113 |
-
|
114 |
-
except Exception as e:
|
115 |
-
logger.error(f"خطأ في تحليل مخاطر المشروع: {str(e)}")
|
116 |
-
self.analysis_results["status"] = "فشل التحليل"
|
117 |
-
self.analysis_results["error"] = str(e)
|
118 |
-
|
119 |
-
finally:
|
120 |
-
self.analysis_in_progress = False
|
121 |
-
|
122 |
-
# استدعاء دالة الاستجابة إذا تم توفيرها
|
123 |
-
if callback and callable(callback):
|
124 |
-
callback(self.analysis_results)
|
125 |
-
|
126 |
-
def _get_project_data(self, project_id):
|
127 |
-
"""الحصول على بيانات المشروع"""
|
128 |
-
# في التطبيق الفعلي، سيتم جلب البيانات من قاعدة البيانات
|
129 |
-
# هنا نقوم بمحاكاة البيانات للتوضيح
|
130 |
-
|
131 |
-
return {
|
132 |
-
"id": project_id,
|
133 |
-
"name": "مشروع الطرق السريعة",
|
134 |
-
"client": "وزارة النقل",
|
135 |
-
"description": "إنشاء طرق سريعة بطول 50 كم في المنطقة الشرقية",
|
136 |
-
"start_date": "2025-05-01",
|
137 |
-
"end_date": "2025-11-30",
|
138 |
-
"status": "تخطيط",
|
139 |
-
"budget": 50000000,
|
140 |
-
"location": "المنطقة الشرقية",
|
141 |
-
"project_type": "بنية تحتية",
|
142 |
-
"complexity": "متوسط",
|
143 |
-
"existing_risks": [
|
144 |
-
{"id": 1, "name": "تأخر توريد المواد", "probability": "متوسط", "impact": "عالي", "category": "توريد"},
|
145 |
-
{"id": 2, "name": "تغير أسعار المواد", "probability": "عالي", "impact": "عالي", "category": "مالي"},
|
146 |
-
{"id": 3, "name": "ظروف جوية غير مواتية", "probability": "منخفض", "impact": "متوسط", "category": "بيئي"},
|
147 |
-
{"id": 4, "name": "نقص العمالة", "probability": "متوسط", "impact": "متوسط", "category": "موارد بشرية"}
|
148 |
-
]
|
149 |
-
}
|
150 |
-
|
151 |
-
def _identify_risks(self, project_data, method):
|
152 |
-
"""تحديد المخاطر"""
|
153 |
-
# دمج المخاطر الموجودة
|
154 |
-
identified_risks = []
|
155 |
-
for risk in project_data["existing_risks"]:
|
156 |
-
identified_risks.append({
|
157 |
-
"id": risk["id"],
|
158 |
-
"name": risk["name"],
|
159 |
-
"description": f"مخاطر {risk['name']} في المشروع",
|
160 |
-
"category": risk["category"],
|
161 |
-
"probability": risk["probability"],
|
162 |
-
"impact": risk["impact"],
|
163 |
-
"risk_score": self._calculate_risk_score(risk["probability"], risk["impact"]),
|
164 |
-
"source": "existing"
|
165 |
-
})
|
166 |
-
|
167 |
-
# إضافة مخاطر إضافية بناءً على نوع المشروع وموقعه وتعقيده
|
168 |
-
additional_risks = self._generate_additional_risks(project_data, method)
|
169 |
-
identified_risks.extend(additional_risks)
|
170 |
-
|
171 |
-
# تخزين المخاطر المحددة
|
172 |
-
self.analysis_results["identified_risks"] = identified_risks
|
173 |
-
|
174 |
-
def _generate_additional_risks(self, project_data, method):
|
175 |
-
"""توليد مخاطر إضافية بناءً على بيانات المشروع"""
|
176 |
-
additional_risks = []
|
177 |
-
|
178 |
-
# مخاطر مرتبطة بنوع المشروع
|
179 |
-
if project_data["project_type"] == "بنية تحتية":
|
180 |
-
additional_risks.extend([
|
181 |
-
{
|
182 |
-
"id": 101,
|
183 |
-
"name": "مشاكل جيوتقنية",
|
184 |
-
"description": "مشاكل غير متوقعة في التربة أو الظروف الجيولوجية",
|
185 |
-
"category": "فني",
|
186 |
-
"probability": "متوسط",
|
187 |
-
"impact": "عالي",
|
188 |
-
"risk_score": self._calculate_risk_score("متوسط", "عالي"),
|
189 |
-
"source": "generated"
|
190 |
-
},
|
191 |
-
{
|
192 |
-
"id": 102,
|
193 |
-
"name": "تعارض مع مرافق قائمة",
|
194 |
-
"description": "تعارض أعمال الحفر مع خطوط المرافق القائمة (كهرباء، مياه، اتصالات)",
|
195 |
-
"category": "فني",
|
196 |
-
"probability": "متوسط",
|
197 |
-
"impact": "متوسط",
|
198 |
-
"risk_score": self._calculate_risk_score("متوسط", "متوسط"),
|
199 |
-
"source": "generated"
|
200 |
-
}
|
201 |
-
])
|
202 |
-
|
203 |
-
# مخاطر مرتبطة بالموقع
|
204 |
-
if project_data["location"] == "المنطقة الشرقية":
|
205 |
-
additional_risks.extend([
|
206 |
-
{
|
207 |
-
"id": 201,
|
208 |
-
"name": "ارتفاع درجات الحرارة",
|
209 |
-
"description": "تأثير ارتفاع درجات الحرارة على إنتاجية العمل وجودة المواد",
|
210 |
-
"category": "بيئي",
|
211 |
-
"probability": "عالي",
|
212 |
-
"impact": "متوسط",
|
213 |
-
"risk_score": self._calculate_risk_score("عالي", "متوسط"),
|
214 |
-
"source": "generated"
|
215 |
-
},
|
216 |
-
{
|
217 |
-
"id": 202,
|
218 |
-
"name": "رطوبة عالية",
|
219 |
-
"description": "تأثير الرطوبة العالية على جودة المواد وتقنيات البناء",
|
220 |
-
"category": "بيئي",
|
221 |
-
"probability": "عالي",
|
222 |
-
"impact": "منخفض",
|
223 |
-
"risk_score": self._calculate_risk_score("عالي", "منخفض"),
|
224 |
-
"source": "generated"
|
225 |
-
}
|
226 |
-
])
|
227 |
-
|
228 |
-
# مخاطر مرتبطة بتعقيد المشروع
|
229 |
-
if project_data["complexity"] in ["متوسط", "عالي"]:
|
230 |
-
additional_risks.extend([
|
231 |
-
{
|
232 |
-
"id": 301,
|
233 |
-
"name": "تغييرات في نطاق العمل",
|
234 |
-
"description": "طلبات تغيير من العميل أو تعديلات في متطلبات المشروع",
|
235 |
-
"category": "إداري",
|
236 |
-
"probability": "عالي",
|
237 |
-
"impact": "عالي",
|
238 |
-
"risk_score": self._calculate_risk_score("عالي", "عالي"),
|
239 |
-
"source": "generated"
|
240 |
-
},
|
241 |
-
{
|
242 |
-
"id": 302,
|
243 |
-
"name": "تأخر الموافقات",
|
244 |
-
"description": "تأخر الحصول على الموافقات والتصاريح اللازمة",
|
245 |
-
"category": "تنظيمي",
|
246 |
-
"probability": "متوسط",
|
247 |
-
"impact": "عالي",
|
248 |
-
"risk_score": self._calculate_risk_score("متوسط", "عالي"),
|
249 |
-
"source": "generated"
|
250 |
-
}
|
251 |
-
])
|
252 |
-
|
253 |
-
# إضافة مخاطر إضافية إذا كانت طريقة التحليل شاملة
|
254 |
-
if method == "comprehensive":
|
255 |
-
additional_risks.extend([
|
256 |
-
{
|
257 |
-
"id": 401,
|
258 |
-
"name": "مخاطر سياسية",
|
259 |
-
"description": "تغييرات في السياسات الحكومية أو اللوائح التنظيمية",
|
260 |
-
"category": "خارجي",
|
261 |
-
"probability": "منخفض",
|
262 |
-
"impact": "عالي",
|
263 |
-
"risk_score": self._calculate_risk_score("منخفض", "عالي"),
|
264 |
-
"source": "generated"
|
265 |
-
},
|
266 |
-
{
|
267 |
-
"id": 402,
|
268 |
-
"name": "مخاطر اقتصادية",
|
269 |
-
"description": "تقلبات في أسعار العملات أو التضخم",
|
270 |
-
"category": "مالي",
|
271 |
-
"probability": "متوسط",
|
272 |
-
"impact": "متوسط",
|
273 |
-
"risk_score": self._calculate_risk_score("متوسط", "متوسط"),
|
274 |
-
"source": "generated"
|
275 |
-
},
|
276 |
-
{
|
277 |
-
"id": 403,
|
278 |
-
"name": "مخاطر تقنية",
|
279 |
-
"description": "مشاكل في التقنيات الجديدة أو المعدات",
|
280 |
-
"category": "فني",
|
281 |
-
"probability": "متوسط",
|
282 |
-
"impact": "متوسط",
|
283 |
-
"risk_score": self._calculate_risk_score("متوسط", "متوسط"),
|
284 |
-
"source": "generated"
|
285 |
-
}
|
286 |
-
])
|
287 |
-
|
288 |
-
return additional_risks
|
289 |
-
|
290 |
-
def _calculate_risk_score(self, probability, impact):
|
291 |
-
"""حساب درجة المخاطرة"""
|
292 |
-
# تحويل القيم النصية إلى قيم رقمية
|
293 |
-
probability_values = {"منخفض": 1, "متوسط": 2, "عالي": 3}
|
294 |
-
impact_values = {"منخفض": 1, "متوسط": 2, "عالي": 3}
|
295 |
-
|
296 |
-
# حساب درجة المخاطرة
|
297 |
-
p_value = probability_values.get(probability, 1)
|
298 |
-
i_value = impact_values.get(impact, 1)
|
299 |
-
|
300 |
-
return p_value * i_value
|
301 |
-
|
302 |
-
def _categorize_risks(self):
|
303 |
-
"""تصنيف المخاطر"""
|
304 |
-
categories = {}
|
305 |
-
|
306 |
-
for risk in self.analysis_results["identified_risks"]:
|
307 |
-
category = risk["category"]
|
308 |
-
if category not in categories:
|
309 |
-
categories[category] = []
|
310 |
-
|
311 |
-
categories[category].append(risk)
|
312 |
-
|
313 |
-
# حساب إحصائيات لكل فئة
|
314 |
-
for category, risks in categories.items():
|
315 |
-
total_score = sum(risk["risk_score"] for risk in risks)
|
316 |
-
avg_score = total_score / len(risks) if risks else 0
|
317 |
-
max_score = max(risk["risk_score"] for risk in risks) if risks else 0
|
318 |
-
|
319 |
-
categories[category] = {
|
320 |
-
"risks": risks,
|
321 |
-
"count": len(risks),
|
322 |
-
"total_score": total_score,
|
323 |
-
"avg_score": avg_score,
|
324 |
-
"max_score": max_score
|
325 |
-
}
|
326 |
-
|
327 |
-
self.analysis_results["risk_categories"] = categories
|
328 |
-
|
329 |
-
def _create_risk_matrix(self):
|
330 |
-
"""إنشاء مصفوفة المخاطر"""
|
331 |
-
matrix = {
|
332 |
-
"high_impact": {"high_prob": [], "medium_prob": [], "low_prob": []},
|
333 |
-
"medium_impact": {"high_prob": [], "medium_prob": [], "low_prob": []},
|
334 |
-
"low_impact": {"high_prob": [], "medium_prob": [], "low_prob": []}
|
335 |
-
}
|
336 |
-
|
337 |
-
for risk in self.analysis_results["identified_risks"]:
|
338 |
-
impact = risk["impact"].lower()
|
339 |
-
probability = risk["probability"].lower()
|
340 |
-
|
341 |
-
impact_key = f"{impact}_impact"
|
342 |
-
if impact == "عالي":
|
343 |
-
impact_key = "high_impact"
|
344 |
-
elif impact == "متوسط":
|
345 |
-
impact_key = "medium_impact"
|
346 |
-
else:
|
347 |
-
impact_key = "low_impact"
|
348 |
-
|
349 |
-
prob_key = f"{probability}_prob"
|
350 |
-
if probability == "عالي":
|
351 |
-
prob_key = "high_prob"
|
352 |
-
elif probability == "متوسط":
|
353 |
-
prob_key = "medium_prob"
|
354 |
-
else:
|
355 |
-
prob_key = "low_prob"
|
356 |
-
|
357 |
-
if impact_key in matrix and prob_key in matrix[impact_key]:
|
358 |
-
matrix[impact_key][prob_key].append(risk)
|
359 |
-
|
360 |
-
self.analysis_results["risk_matrix"] = matrix
|
361 |
-
|
362 |
-
def _develop_mitigation_strategies(self, method):
|
363 |
-
"""تطوير استراتيجيات التخفيف"""
|
364 |
-
strategies = []
|
365 |
-
|
366 |
-
# استراتيجيات للمخاطر ذات الأولوية العالية
|
367 |
-
high_priority_risks = []
|
368 |
-
|
369 |
-
# المخاطر ذات التأثير العالي واحتمالية عالية
|
370 |
-
high_priority_risks.extend(self.analysis_results["risk_matrix"]["high_impact"]["high_prob"])
|
371 |
-
|
372 |
-
# المخاطر ذات التأثير العالي واحتمالية متوسطة
|
373 |
-
high_priority_risks.extend(self.analysis_results["risk_matrix"]["high_impact"]["medium_prob"])
|
374 |
-
|
375 |
-
# المخاطر ذات التأثير المتوسط واحتمالية عالية
|
376 |
-
high_priority_risks.extend(self.analysis_results["risk_matrix"]["medium_impact"]["high_prob"])
|
377 |
-
|
378 |
-
for risk in high_priority_risks:
|
379 |
-
strategy = self._generate_mitigation_strategy(risk)
|
380 |
-
strategies.append(strategy)
|
381 |
-
|
382 |
-
# إذا كانت طريقة التحليل شاملة، أضف استراتيجيات للمخاطر ذات الأولوية المتوسطة
|
383 |
-
if method == "comprehensive":
|
384 |
-
medium_priority_risks = []
|
385 |
-
|
386 |
-
# المخاطر ذات التأثير العالي واحتمالية منخفضة
|
387 |
-
medium_priority_risks.extend(self.analysis_results["risk_matrix"]["high_impact"]["low_prob"])
|
388 |
-
|
389 |
-
# المخاطر ذات التأثير المتوسط واحتمالية متوسطة
|
390 |
-
medium_priority_risks.extend(self.analysis_results["risk_matrix"]["medium_impact"]["medium_prob"])
|
391 |
-
|
392 |
-
# المخاطر ذات التأثير المنخفض واحتمالية عالية
|
393 |
-
medium_priority_risks.extend(self.analysis_results["risk_matrix"]["low_impact"]["high_prob"])
|
394 |
-
|
395 |
-
for risk in medium_priority_risks:
|
396 |
-
strategy = self._generate_mitigation_strategy(risk)
|
397 |
-
strategies.append(strategy)
|
398 |
-
|
399 |
-
self.analysis_results["mitigation_strategies"] = strategies
|
400 |
-
|
401 |
-
def _generate_mitigation_strategy(self, risk):
|
402 |
-
"""توليد استراتيجية تخفيف للمخاطر"""
|
403 |
-
strategy_templates = {
|
404 |
-
"توريد": [
|
405 |
-
"إنشاء قائمة بموردين بديلين",
|
406 |
-
"التعاقد المسبق مع الموردين",
|
407 |
-
"تخزين المواد الحرجة مسبقًا",
|
408 |
-
"وضع خطة للتوريد المرحلي"
|
409 |
-
],
|
410 |
-
"مالي": [
|
411 |
-
"تضمين بند تعديل الأسعار في العقود",
|
412 |
-
"تخصيص ميزانية احتياطية",
|
413 |
-
"التأمين ضد المخاطر المالية",
|
414 |
-
"تحديث دراسة الجدوى بانتظام"
|
415 |
-
],
|
416 |
-
"بيئي": [
|
417 |
-
"وضع خطة للطوارئ البيئية",
|
418 |
-
"جدولة الأنشطة الحرجة في المواسم المناسبة",
|
419 |
-
"توفير معدات حماية إضافية",
|
420 |
-
"تطبيق تقنيات مقاومة للظروف البيئية"
|
421 |
-
],
|
422 |
-
"موارد بشرية": [
|
423 |
-
"التعاقد مع شركات توظيف إضافية",
|
424 |
-
"تدريب فرق العمل على مهام متعددة",
|
425 |
-
"وضع خطة للحوافز والمكافآت",
|
426 |
-
"تطوير برنامج للاحتفاظ بالموظفين"
|
427 |
-
],
|
428 |
-
"فني": [
|
429 |
-
"إجراء اختبارات إضافية قبل التنفيذ",
|
430 |
-
"الاستعانة بخبراء متخصصين",
|
431 |
-
"تطبيق منهجية مراجعة التصميم",
|
432 |
-
"إعداد خطط بديلة للحلول التقنية"
|
433 |
-
],
|
434 |
-
"إداري": [
|
435 |
-
"تطبيق إجراءات إدارة التغيير",
|
436 |
-
"عقد اجتماعات دورية مع أصحاب المصلحة",
|
437 |
-
"توثيق متطلبات المشروع بشكل تفصيلي",
|
438 |
-
"تحديد نطاق العمل بوضوح في العقود"
|
439 |
-
],
|
440 |
-
"تنظيمي": [
|
441 |
-
"التواصل المبكر مع الجهات التنظيمية",
|
442 |
-
"تعيين مستشار قانوني متخصص",
|
443 |
-
"متابعة التحديثات التنظيمية بانتظام",
|
444 |
-
"تخصي�� وقت إضافي للحصول على الموافقات"
|
445 |
-
],
|
446 |
-
"خارجي": [
|
447 |
-
"متابعة التطورات السياسية والاقتصادية",
|
448 |
-
"وضع خطط بديلة للسيناريوهات المختلفة",
|
449 |
-
"التأمين ضد المخاطر الخارجية",
|
450 |
-
"تنويع مصادر التوريد والتمويل"
|
451 |
-
]
|
452 |
-
}
|
453 |
-
|
454 |
-
# اختيار استراتيجيات مناسبة بناءً على فئة المخاطر
|
455 |
-
category = risk["category"]
|
456 |
-
templates = strategy_templates.get(category, strategy_templates["إداري"])
|
457 |
-
|
458 |
-
# اختيار استراتيجية عشوائية من القائمة
|
459 |
-
import random
|
460 |
-
strategy_text = random.choice(templates)
|
461 |
-
|
462 |
-
return {
|
463 |
-
"risk_id": risk["id"],
|
464 |
-
"risk_name": risk["name"],
|
465 |
-
"strategy": strategy_text,
|
466 |
-
"priority": "عالية" if risk["risk_score"] >= 6 else "متوسطة" if risk["risk_score"] >= 3 else "منخفضة",
|
467 |
-
"responsible": "مدير المشروع",
|
468 |
-
"timeline": "قبل بدء المشروع"
|
469 |
}
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
low_risks = len([r for r in self.analysis_results["identified_risks"] if r["risk_score"] < 3])
|
477 |
-
|
478 |
-
# حساب توزيع المخاطر حسب الفئة
|
479 |
-
category_distribution = {}
|
480 |
-
for risk in self.analysis_results["identified_risks"]:
|
481 |
-
category = risk["category"]
|
482 |
-
if category not in category_distribution:
|
483 |
-
category_distribution[category] = 0
|
484 |
-
category_distribution[category] += 1
|
485 |
-
|
486 |
-
# حساب متوسط درجة المخاطرة
|
487 |
-
avg_risk_score = sum(risk["risk_score"] for risk in self.analysis_results["identified_risks"]) / total_risks if total_risks > 0 else 0
|
488 |
-
|
489 |
-
# إنشاء الملخص
|
490 |
-
summary = {
|
491 |
-
"total_risks": total_risks,
|
492 |
-
"high_risks": high_risks,
|
493 |
-
"medium_risks": medium_risks,
|
494 |
-
"low_risks": low_risks,
|
495 |
-
"category_distribution": category_distribution,
|
496 |
-
"avg_risk_score": avg_risk_score,
|
497 |
-
"analysis_method": method,
|
498 |
-
"recommendations": self._generate_recommendations(high_risks, medium_risks, low_risks, category_distribution)
|
499 |
}
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
551 |
|
552 |
|
553 |
class RiskAnalysisApp:
|
@@ -732,13 +375,6 @@ class RiskAnalysisApp:
|
|
732 |
st.markdown(f"**نوع المشروع**: {project['project_type']}")
|
733 |
st.markdown(f"**مستوى التعقيد**: {project['complexity']}")
|
734 |
|
735 |
-
# اختيار طريقة التحليل
|
736 |
-
analysis_method = st.radio(
|
737 |
-
"طريقة التحليل",
|
738 |
-
["أساسي", "شامل"],
|
739 |
-
format_func=lambda x: "تحليل أساسي" if x == "أساسي" else "تحليل شامل"
|
740 |
-
)
|
741 |
-
|
742 |
# زر بدء التحليل
|
743 |
if st.button("بدء تحليل المخاطر"):
|
744 |
with st.spinner("جاري تحليل مخاطر المشروع..."):
|
@@ -746,69 +382,15 @@ class RiskAnalysisApp:
|
|
746 |
import time
|
747 |
time.sleep(2)
|
748 |
|
749 |
-
# إجراء تحليل المخاطر
|
750 |
-
self.risk_analyzer.
|
751 |
-
|
752 |
-
# الحصول على نتائج التحليل
|
753 |
-
results = self.risk_analyzer.get_analysis_results()
|
754 |
|
755 |
# تخزين النتائج في حالة الجلسة
|
756 |
-
st.session_state.risk_analysis_results[str(project_id)] = results
|
757 |
|
758 |
st.success("تم الانتهاء من تحليل المخاطر بنجاح!")
|
759 |
st.experimental_rerun()
|
760 |
|
761 |
-
# عرض نتائج التحليل إذا كانت متوفرة
|
762 |
-
if str(project_id) in st.session_state.risk_analysis_results:
|
763 |
-
results = st.session_state.risk_analysis_results[str(project_id)]
|
764 |
-
|
765 |
-
st.markdown("#### ملخص نتائج التحليل")
|
766 |
-
|
767 |
-
if "summary" in results:
|
768 |
-
summary = results["summary"]
|
769 |
-
|
770 |
-
col1, col2, col3 = st.columns(3)
|
771 |
-
|
772 |
-
with col1:
|
773 |
-
self.ui.create_metric_card("إجمالي المخاطر", str(summary["total_risks"]), None, self.ui.COLORS['primary'])
|
774 |
-
|
775 |
-
with col2:
|
776 |
-
self.ui.create_metric_card("مخاطر عالية", str(summary["high_risks"]), None, self.ui.COLORS['danger'])
|
777 |
-
|
778 |
-
with col3:
|
779 |
-
self.ui.create_metric_card("مخاطر متوسطة", str(summary["medium_risks"]), None, self.ui.COLORS['warning'])
|
780 |
-
|
781 |
-
# عرض توزيع المخاطر حسب الفئة
|
782 |
-
st.markdown("#### توزيع المخاطر حسب الفئة")
|
783 |
-
|
784 |
-
if "category_distribution" in summary:
|
785 |
-
category_df = pd.DataFrame({
|
786 |
-
'الفئة': list(summary["category_distribution"].keys()),
|
787 |
-
'عدد المخاطر': list(summary["category_distribution"].values())
|
788 |
-
})
|
789 |
-
|
790 |
-
st.bar_chart(category_df.set_index('الفئة'))
|
791 |
-
|
792 |
-
# عرض التوصيات
|
793 |
-
st.markdown("#### التوصيات")
|
794 |
-
|
795 |
-
if "recommendations" in summary:
|
796 |
-
for i, recommendation in enumerate(summary["recommendations"]):
|
797 |
-
st.markdown(f"{i+1}. {recommendation}")
|
798 |
-
|
799 |
-
# زر تصدير النتائج
|
800 |
-
if st.button("تصدير نتائج التحليل"):
|
801 |
-
with st.spinner("جاري تصدير النتائج..."):
|
802 |
-
# محاكاة وقت المعالجة
|
803 |
-
time.sleep(1)
|
804 |
-
|
805 |
-
# تصدير النتائج
|
806 |
-
export_path = self.risk_analyzer.export_analysis_results()
|
807 |
-
|
808 |
-
if export_path:
|
809 |
-
st.success(f"تم تصدير نتائج التحليل بنجاح!")
|
810 |
-
else:
|
811 |
-
st.error("حدث خطأ أثناء تصدير النتائج.")
|
812 |
|
813 |
def _render_risk_register_tab(self):
|
814 |
"""عرض تبويب سجل المخاطر"""
|
@@ -826,12 +408,12 @@ class RiskAnalysisApp:
|
|
826 |
if selected_project_option == "جميع المشاريع":
|
827 |
# جمع المخاطر من جميع المشاريع
|
828 |
for project_id, results in st.session_state.risk_analysis_results.items():
|
829 |
-
if "
|
830 |
project = next((p for p in st.session_state.projects if p["id"] == int(project_id)), None)
|
831 |
project_name = project["name"] if project else f"مشروع {project_id}"
|
832 |
|
833 |
-
for
|
834 |
-
risk_copy =
|
835 |
risk_copy["project_name"] = project_name
|
836 |
all_risks.append(risk_copy)
|
837 |
else:
|
@@ -843,9 +425,9 @@ class RiskAnalysisApp:
|
|
843 |
# جمع المخاطر من المشروع المحدد
|
844 |
if str(project_id) in st.session_state.risk_analysis_results:
|
845 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
846 |
-
if "
|
847 |
-
for
|
848 |
-
risk_copy =
|
849 |
risk_copy["project_name"] = project["name"]
|
850 |
all_risks.append(risk_copy)
|
851 |
|
@@ -862,14 +444,14 @@ class RiskAnalysisApp:
|
|
862 |
with col2:
|
863 |
probability_filter = st.multiselect(
|
864 |
"الاحتمالية",
|
865 |
-
["
|
866 |
key="risk_register_probability"
|
867 |
)
|
868 |
|
869 |
with col3:
|
870 |
impact_filter = st.multiselect(
|
871 |
"التأثير",
|
872 |
-
["
|
873 |
key="risk_register_impact"
|
874 |
)
|
875 |
|
@@ -888,22 +470,10 @@ class RiskAnalysisApp:
|
|
888 |
# عرض سجل المخاطر
|
889 |
if filtered_risks:
|
890 |
# تحويل المخاطر إلى DataFrame
|
891 |
-
|
892 |
-
for risk in filtered_risks:
|
893 |
-
risk_data.append({
|
894 |
-
'المشروع': risk.get("project_name", ""),
|
895 |
-
'اسم المخاطرة': risk["name"],
|
896 |
-
'الوصف': risk.get("description", ""),
|
897 |
-
'الفئة': risk["category"],
|
898 |
-
'الاحتمالية': risk["probability"],
|
899 |
-
'التأثير': risk["impact"],
|
900 |
-
'درجة المخاطرة': risk["risk_score"]
|
901 |
-
})
|
902 |
-
|
903 |
-
risk_df = pd.DataFrame(risk_data)
|
904 |
|
905 |
# ترتيب المخاطر حسب درجة المخاطرة (تنازليًا)
|
906 |
-
risk_df = risk_df.sort_values(by='
|
907 |
|
908 |
# عرض الجدول
|
909 |
st.dataframe(risk_df, use_container_width=True, hide_index=True)
|
@@ -936,92 +506,14 @@ class RiskAnalysisApp:
|
|
936 |
if str(project_id) in st.session_state.risk_analysis_results:
|
937 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
938 |
|
939 |
-
if "
|
940 |
-
|
941 |
-
|
942 |
-
# إنشاء مصفوفة المخاطر
|
943 |
-
st.markdown("#### مصفوفة احتمالية وتأثير المخاطر")
|
944 |
-
|
945 |
-
# إنشاء بيانات المصفوفة
|
946 |
-
matrix_data = [
|
947 |
-
[len(matrix["high_impact"]["high_prob"]), len(matrix["high_impact"]["medium_prob"]), len(matrix["high_impact"]["low_prob"])],
|
948 |
-
[len(matrix["medium_impact"]["high_prob"]), len(matrix["medium_impact"]["medium_prob"]), len(matrix["medium_impact"]["low_prob"])],
|
949 |
-
[len(matrix["low_impact"]["high_prob"]), len(matrix["low_impact"]["medium_prob"]), len(matrix["low_impact"]["low_prob"])]
|
950 |
-
]
|
951 |
-
|
952 |
-
# تحويل البيانات إلى DataFrame
|
953 |
-
matrix_df = pd.DataFrame(
|
954 |
-
matrix_data,
|
955 |
-
columns=["احتمالية عالية", "احتمالية متوسطة", "احتمالية منخفضة"],
|
956 |
-
index=["تأثير عالي", "تأثير متوسط", "تأثير منخفض"]
|
957 |
-
)
|
958 |
-
|
959 |
-
# عرض المصفوفة كجدول
|
960 |
-
st.dataframe(matrix_df)
|
961 |
-
|
962 |
-
# عرض تفاصيل المخاطر في كل خلية
|
963 |
-
st.markdown("#### تفاصيل المخاطر في المصفوفة")
|
964 |
-
|
965 |
-
# إنشاء تبويبات للخلايا المختلفة
|
966 |
-
matrix_tabs = st.tabs([
|
967 |
-
"تأثير عالي / احتمالية عالية",
|
968 |
-
"تأثير عالي / احتمالية متوسطة",
|
969 |
-
"تأثير متوسط / احتمالية عالية",
|
970 |
-
"تأثير متوسط / احتمالية متوسطة",
|
971 |
-
"أخرى"
|
972 |
-
])
|
973 |
-
|
974 |
-
# عرض المخاطر في كل تبويب
|
975 |
-
with matrix_tabs[0]:
|
976 |
-
self._display_cell_risks(matrix["high_impact"]["high_prob"], "تأثير عالي / احتمالية عالية")
|
977 |
-
|
978 |
-
with matrix_tabs[1]:
|
979 |
-
self._display_cell_risks(matrix["high_impact"]["medium_prob"], "تأثير عالي / احتمالية متوسطة")
|
980 |
-
|
981 |
-
with matrix_tabs[2]:
|
982 |
-
self._display_cell_risks(matrix["medium_impact"]["high_prob"], "تأثير متوسط / احتمالية عالية")
|
983 |
-
|
984 |
-
with matrix_tabs[3]:
|
985 |
-
self._display_cell_risks(matrix["medium_impact"]["medium_prob"], "تأثير متوسط / احتمالية متوسطة")
|
986 |
-
|
987 |
-
with matrix_tabs[4]:
|
988 |
-
# جمع المخاطر الأخرى
|
989 |
-
other_risks = []
|
990 |
-
other_risks.extend(matrix["high_impact"]["low_prob"])
|
991 |
-
other_risks.extend(matrix["medium_impact"]["low_prob"])
|
992 |
-
other_risks.extend(matrix["low_impact"]["high_prob"])
|
993 |
-
other_risks.extend(matrix["low_impact"]["medium_prob"])
|
994 |
-
other_risks.extend(matrix["low_impact"]["low_prob"])
|
995 |
-
|
996 |
-
self._display_cell_risks(other_risks, "مخاطر أخرى")
|
997 |
else:
|
998 |
st.warning("لم يتم العثور على مصفوفة المخاطر للمشروع المحدد.")
|
999 |
else:
|
1000 |
st.warning("لم يتم إجراء تحليل للمخاطر لهذا المشروع بعد.")
|
1001 |
|
1002 |
-
def _display_cell_risks(self, risks, cell_title):
|
1003 |
-
"""عرض المخاطر في خلية من مصفوفة المخاطر"""
|
1004 |
-
|
1005 |
-
if risks:
|
1006 |
-
st.markdown(f"##### {cell_title} ({len(risks)} مخاطر)")
|
1007 |
-
|
1008 |
-
# تحويل المخاطر إلى DataFrame
|
1009 |
-
risk_data = []
|
1010 |
-
for risk in risks:
|
1011 |
-
risk_data.append({
|
1012 |
-
'اسم المخاطرة': risk["name"],
|
1013 |
-
'الوصف': risk.get("description", ""),
|
1014 |
-
'الفئة': risk["category"],
|
1015 |
-
'درجة المخاطرة': risk["risk_score"]
|
1016 |
-
})
|
1017 |
-
|
1018 |
-
risk_df = pd.DataFrame(risk_data)
|
1019 |
-
|
1020 |
-
# عرض الجدول
|
1021 |
-
st.dataframe(risk_df, use_container_width=True, hide_index=True)
|
1022 |
-
else:
|
1023 |
-
st.info(f"لا توجد مخاطر في خلية {cell_title}.")
|
1024 |
-
|
1025 |
def _render_mitigation_strategies_tab(self):
|
1026 |
"""عرض تبويب استراتيجيات التخفيف"""
|
1027 |
|
@@ -1041,54 +533,9 @@ class RiskAnalysisApp:
|
|
1041 |
if str(project_id) in st.session_state.risk_analysis_results:
|
1042 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
1043 |
|
1044 |
-
if "
|
1045 |
-
|
1046 |
-
|
1047 |
-
# فلترة الاستراتيجيات
|
1048 |
-
priority_filter = st.multiselect(
|
1049 |
-
"الأولوية",
|
1050 |
-
["عالية", "متوسطة", "منخفضة"],
|
1051 |
-
default=["عالية"],
|
1052 |
-
key="mitigation_priority"
|
1053 |
-
)
|
1054 |
-
|
1055 |
-
# تطبيق الفلترة
|
1056 |
-
filtered_strategies = strategies
|
1057 |
-
if priority_filter:
|
1058 |
-
filtered_strategies = [s for s in filtered_strategies if s["priority"] in priority_filter]
|
1059 |
-
|
1060 |
-
# عرض استراتيجيات التخفيف
|
1061 |
-
if filtered_strategies:
|
1062 |
-
# تحويل الاستراتيجيات إلى DataFrame
|
1063 |
-
strategy_data = []
|
1064 |
-
for strategy in filtered_strategies:
|
1065 |
-
strategy_data.append({
|
1066 |
-
'المخاطرة': strategy["risk_name"],
|
1067 |
-
'استراتيجية التخفيف': strategy["strategy"],
|
1068 |
-
'الأولوية': strategy["priority"],
|
1069 |
-
'المسؤول': strategy["responsible"],
|
1070 |
-
'الإطار الزمني': strategy["timeline"]
|
1071 |
-
})
|
1072 |
-
|
1073 |
-
strategy_df = pd.DataFrame(strategy_data)
|
1074 |
-
|
1075 |
-
# ترتيب الاستراتيجيات حسب الأولوية
|
1076 |
-
priority_order = {"عالية": 0, "متوسطة": 1, "منخفضة": 2}
|
1077 |
-
strategy_df["priority_order"] = strategy_df["الأولوية"].map(priority_order)
|
1078 |
-
strategy_df = strategy_df.sort_values(by="priority_order")
|
1079 |
-
strategy_df = strategy_df.drop(columns=["priority_order"])
|
1080 |
-
|
1081 |
-
# عرض الجدول
|
1082 |
-
st.dataframe(strategy_df, use_container_width=True, hide_index=True)
|
1083 |
-
|
1084 |
-
# زر تصدير استراتيجيات التخفيف
|
1085 |
-
if st.button("تصدير استراتيجيات التخفيف"):
|
1086 |
-
with st.spinner("جاري تصدير استراتيجيات التخفيف..."):
|
1087 |
-
# محاكاة وقت المعالجة
|
1088 |
-
time.sleep(1)
|
1089 |
-
st.success("تم تصدير استراتيجيات التخفيف بنجاح!")
|
1090 |
-
else:
|
1091 |
-
st.info("لا توجد استراتيجيات تخفيف تطابق معايير الفلترة.")
|
1092 |
else:
|
1093 |
st.warning("لم يتم العثور على استراتيجيات تخفيف للمشروع المحدد.")
|
1094 |
else:
|
@@ -1151,4 +598,4 @@ class RiskAnalysisApp:
|
|
1151 |
# تشغيل التطبيق
|
1152 |
if __name__ == "__main__":
|
1153 |
risk_app = RiskAnalysisApp()
|
1154 |
-
risk_app.run()
|
|
|
14 |
import numpy as np
|
15 |
import matplotlib.pyplot as plt
|
16 |
import sys
|
17 |
+
import plotly.express as px
|
18 |
|
19 |
# إضافة مسار المشروع للنظام
|
20 |
sys.path.append(str(Path(__file__).parent.parent))
|
|
|
29 |
)
|
30 |
logger = logging.getLogger('risk_analysis')
|
31 |
|
32 |
+
"""
|
33 |
+
محلل المخاطر المتقدم للمشاريع
|
34 |
+
"""
|
35 |
+
|
36 |
class RiskAnalyzer:
|
37 |
+
def __init__(self):
|
38 |
+
self.risk_categories = [
|
39 |
+
"مخاطر السوق",
|
40 |
+
"مخاطر التنفيذ",
|
41 |
+
"مخاطر العقود",
|
42 |
+
"مخاطر التمويل",
|
43 |
+
"مخاطر الموارد"
|
44 |
+
]
|
45 |
+
|
46 |
+
self.impact_levels = {
|
47 |
+
"منخفض": 1,
|
48 |
+
"متوسط": 2,
|
49 |
+
"عالي": 3,
|
50 |
+
"حرج": 4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
}
|
52 |
+
|
53 |
+
self.probability_levels = {
|
54 |
+
"نادر": 1,
|
55 |
+
"محتمل": 2,
|
56 |
+
"مرجح": 3,
|
57 |
+
"شبه مؤكد": 4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
}
|
59 |
+
|
60 |
+
def analyze_risks(self, project_data):
|
61 |
+
"""تحليل المخاطر للمشروع"""
|
62 |
+
risks = []
|
63 |
+
|
64 |
+
# تحليل مخاطر السوق
|
65 |
+
market_risks = self._analyze_market_risks(project_data)
|
66 |
+
risks.extend(market_risks)
|
67 |
+
|
68 |
+
# تحليل مخاطر التنفيذ
|
69 |
+
execution_risks = self._analyze_execution_risks(project_data)
|
70 |
+
risks.extend(execution_risks)
|
71 |
+
|
72 |
+
# تحليل المخاطر المالية
|
73 |
+
financial_risks = self._analyze_financial_risks(project_data)
|
74 |
+
risks.extend(financial_risks)
|
75 |
+
|
76 |
+
return pd.DataFrame(risks)
|
77 |
+
|
78 |
+
def calculate_risk_score(self, probability, impact):
|
79 |
+
"""حساب درجة الخطر"""
|
80 |
+
return self.probability_levels[probability] * self.impact_levels[impact]
|
81 |
+
|
82 |
+
def render_risk_analysis(self, project_data):
|
83 |
+
"""عرض تحليل المخاطر"""
|
84 |
+
st.header("تحليل المخاطر")
|
85 |
+
|
86 |
+
# تحليل المخاطر
|
87 |
+
risks_df = self.analyze_risks(project_data)
|
88 |
+
|
89 |
+
# عرض مصفوفة المخاطر
|
90 |
+
self._render_risk_matrix(risks_df)
|
91 |
+
|
92 |
+
# عرض تفاصيل المخاطر
|
93 |
+
self._render_risk_details(risks_df)
|
94 |
+
|
95 |
+
# عرض خطة الاستجابة للمخاطر
|
96 |
+
self._render_risk_response_plan(risks_df)
|
97 |
+
|
98 |
+
def _render_risk_matrix(self, risks_df):
|
99 |
+
"""عرض مصفوفة المخاطر"""
|
100 |
+
st.subheader("مصفوفة المخاطر")
|
101 |
+
|
102 |
+
# إنشاء مصفوفة المخاطر
|
103 |
+
matrix_data = np.zeros((4, 4))
|
104 |
+
for _, risk in risks_df.iterrows():
|
105 |
+
prob_idx = self.probability_levels[risk['probability']] - 1
|
106 |
+
impact_idx = self.impact_levels[risk['impact']] - 1
|
107 |
+
matrix_data[prob_idx, impact_idx] += 1
|
108 |
+
|
109 |
+
fig = px.imshow(
|
110 |
+
matrix_data,
|
111 |
+
labels=dict(x="التأثير", y="الاحتمالية"),
|
112 |
+
x=list(self.impact_levels.keys()),
|
113 |
+
y=list(self.probability_levels.keys())
|
114 |
+
)
|
115 |
+
st.plotly_chart(fig)
|
116 |
+
|
117 |
+
def _render_risk_details(self, risks_df):
|
118 |
+
"""عرض تفاصيل المخاطر"""
|
119 |
+
st.subheader("تفاصيل المخاطر")
|
120 |
+
|
121 |
+
# تصنيف المخاطر حسب درجة الخطورة
|
122 |
+
risks_df['risk_score'] = risks_df.apply(
|
123 |
+
lambda x: self.calculate_risk_score(x['probability'], x['impact']),
|
124 |
+
axis=1
|
125 |
+
)
|
126 |
+
|
127 |
+
# عرض المخاطر مرتبة حسب درجة الخطورة
|
128 |
+
st.dataframe(
|
129 |
+
risks_df.sort_values('risk_score', ascending=False),
|
130 |
+
use_container_width=True
|
131 |
+
)
|
132 |
+
|
133 |
+
def _render_risk_response_plan(self, risks_df):
|
134 |
+
"""عرض خطة الاستجابة للمخاطر"""
|
135 |
+
st.subheader("خطة الاستجابة للمخاطر")
|
136 |
+
|
137 |
+
# عرض استراتيجيات الاستجابة للمخاطر العالية
|
138 |
+
high_risks = risks_df[risks_df['risk_score'] >= 9]
|
139 |
+
|
140 |
+
for _, risk in high_risks.iterrows():
|
141 |
+
with st.expander(f"{risk['category']} - {risk['description']}"):
|
142 |
+
st.write("**استراتيجية الاستجابة:**", risk['response_strategy'])
|
143 |
+
st.write("**خطة العمل:**", risk['action_plan'])
|
144 |
+
st.write("**المسؤول:**", risk['responsible'])
|
145 |
+
st.write("**الموعد النهائي:**", risk['deadline'])
|
146 |
+
|
147 |
+
def _analyze_market_risks(self, project_data):
|
148 |
+
"""تحليل مخاطر السوق"""
|
149 |
+
return [
|
150 |
+
{
|
151 |
+
'category': 'مخاطر السوق',
|
152 |
+
'description': 'تقلبات أسعار المواد الخام',
|
153 |
+
'probability': 'مرجح',
|
154 |
+
'impact': 'عالي',
|
155 |
+
'response_strategy': 'تحوط',
|
156 |
+
'action_plan': 'التعاقد المسبق مع الموردين وتثبيت الأسعار',
|
157 |
+
'responsible': 'مدير المشتريات',
|
158 |
+
'deadline': '2024-03-01'
|
159 |
+
},
|
160 |
+
# إضافة المزيد من المخاطر
|
161 |
+
]
|
162 |
+
|
163 |
+
def _analyze_execution_risks(self, project_data):
|
164 |
+
"""تحليل مخاطر التنفيذ"""
|
165 |
+
return [
|
166 |
+
{
|
167 |
+
'category': 'مخاطر التنفيذ',
|
168 |
+
'description': 'تأخر في جدول التنفيذ',
|
169 |
+
'probability': 'محتمل',
|
170 |
+
'impact': 'عالي',
|
171 |
+
'response_strategy': 'تخفيف',
|
172 |
+
'action_plan': 'إعداد خطة تسريع وتحديد المسار الحرج',
|
173 |
+
'responsible': 'مدير المشروع',
|
174 |
+
'deadline': '2024-02-15'
|
175 |
+
},
|
176 |
+
# إضافة المزيد من المخاطر
|
177 |
+
]
|
178 |
+
|
179 |
+
def _analyze_financial_risks(self, project_data):
|
180 |
+
"""تحليل المخاطر المالية"""
|
181 |
+
return [
|
182 |
+
{
|
183 |
+
'category': 'مخاطر التمويل',
|
184 |
+
'description': 'تأخر الدفعات',
|
185 |
+
'probability': 'محتمل',
|
186 |
+
'impact': 'عالي',
|
187 |
+
'response_strategy': 'نقل',
|
188 |
+
'action_plan': 'التأمين على مخاطر عدم السداد',
|
189 |
+
'responsible': 'المدير المالي',
|
190 |
+
'deadline': '2024-02-01'
|
191 |
+
},
|
192 |
+
# إضافة المزيد من المخاطر
|
193 |
+
]
|
194 |
|
195 |
|
196 |
class RiskAnalysisApp:
|
|
|
375 |
st.markdown(f"**نوع المشروع**: {project['project_type']}")
|
376 |
st.markdown(f"**مستوى التعقيد**: {project['complexity']}")
|
377 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
378 |
# زر بدء التحليل
|
379 |
if st.button("بدء تحليل المخاطر"):
|
380 |
with st.spinner("جاري تحليل مخاطر المشروع..."):
|
|
|
382 |
import time
|
383 |
time.sleep(2)
|
384 |
|
385 |
+
# إجراء تحليل المخاطر (Using the new RiskAnalyzer)
|
386 |
+
results = self.risk_analyzer.render_risk_analysis(project)
|
|
|
|
|
|
|
387 |
|
388 |
# تخزين النتائج في حالة الجلسة
|
389 |
+
st.session_state.risk_analysis_results[str(project_id)] = {"analysis_results": results}
|
390 |
|
391 |
st.success("تم الانتهاء من تحليل المخاطر بنجاح!")
|
392 |
st.experimental_rerun()
|
393 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
394 |
|
395 |
def _render_risk_register_tab(self):
|
396 |
"""عرض تبويب سجل المخاطر"""
|
|
|
408 |
if selected_project_option == "جميع المشاريع":
|
409 |
# جمع المخاطر من جميع المشاريع
|
410 |
for project_id, results in st.session_state.risk_analysis_results.items():
|
411 |
+
if "analysis_results" in results:
|
412 |
project = next((p for p in st.session_state.projects if p["id"] == int(project_id)), None)
|
413 |
project_name = project["name"] if project else f"مشروع {project_id}"
|
414 |
|
415 |
+
for index, row in results["analysis_results"].iterrows():
|
416 |
+
risk_copy = row.to_dict()
|
417 |
risk_copy["project_name"] = project_name
|
418 |
all_risks.append(risk_copy)
|
419 |
else:
|
|
|
425 |
# جمع المخاطر من المشروع المحدد
|
426 |
if str(project_id) in st.session_state.risk_analysis_results:
|
427 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
428 |
+
if "analysis_results" in results:
|
429 |
+
for index, row in results["analysis_results"].iterrows():
|
430 |
+
risk_copy = row.to_dict()
|
431 |
risk_copy["project_name"] = project["name"]
|
432 |
all_risks.append(risk_copy)
|
433 |
|
|
|
444 |
with col2:
|
445 |
probability_filter = st.multiselect(
|
446 |
"الاحتمالية",
|
447 |
+
list(set(risk["probability"] for risk in all_risks)) if all_risks else [],
|
448 |
key="risk_register_probability"
|
449 |
)
|
450 |
|
451 |
with col3:
|
452 |
impact_filter = st.multiselect(
|
453 |
"التأثير",
|
454 |
+
list(set(risk["impact"] for risk in all_risks)) if all_risks else [],
|
455 |
key="risk_register_impact"
|
456 |
)
|
457 |
|
|
|
470 |
# عرض سجل المخاطر
|
471 |
if filtered_risks:
|
472 |
# تحويل المخاطر إلى DataFrame
|
473 |
+
risk_df = pd.DataFrame(filtered_risks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
474 |
|
475 |
# ترتيب المخاطر حسب درجة المخاطرة (تنازليًا)
|
476 |
+
risk_df = risk_df.sort_values(by='risk_score', ascending=False)
|
477 |
|
478 |
# عرض الجدول
|
479 |
st.dataframe(risk_df, use_container_width=True, hide_index=True)
|
|
|
506 |
if str(project_id) in st.session_state.risk_analysis_results:
|
507 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
508 |
|
509 |
+
if "analysis_results" in results:
|
510 |
+
risks_df = results["analysis_results"]
|
511 |
+
self.risk_analyzer._render_risk_matrix(risks_df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
512 |
else:
|
513 |
st.warning("لم يتم العثور على مصفوفة المخاطر للمشروع المحدد.")
|
514 |
else:
|
515 |
st.warning("لم يتم إجراء تحليل للمخاطر لهذا المشروع بعد.")
|
516 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
517 |
def _render_mitigation_strategies_tab(self):
|
518 |
"""عرض تبويب استراتيجيات التخفيف"""
|
519 |
|
|
|
533 |
if str(project_id) in st.session_state.risk_analysis_results:
|
534 |
results = st.session_state.risk_analysis_results[str(project_id)]
|
535 |
|
536 |
+
if "analysis_results" in results and not results["analysis_results"].empty:
|
537 |
+
risks_df = results["analysis_results"]
|
538 |
+
self.risk_analyzer._render_risk_response_plan(risks_df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
539 |
else:
|
540 |
st.warning("لم يتم العثور على استراتيجيات تخفيف للمشروع المحدد.")
|
541 |
else:
|
|
|
598 |
# تشغيل التطبيق
|
599 |
if __name__ == "__main__":
|
600 |
risk_app = RiskAnalysisApp()
|
601 |
+
risk_app.run()
|
modules/scheduling/schedule_app.py
ADDED
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import streamlit as st
|
3 |
+
import pandas as pd
|
4 |
+
import plotly.figure_factory as ff
|
5 |
+
from datetime import datetime, timedelta
|
6 |
+
import plotly.express as px
|
7 |
+
import numpy as np
|
8 |
+
import io
|
9 |
+
import openpyxl
|
10 |
+
|
11 |
+
class ScheduleApp:
|
12 |
+
def __init__(self):
|
13 |
+
if 'saved_pricing' not in st.session_state:
|
14 |
+
st.session_state.saved_pricing = []
|
15 |
+
if 'uploaded_files' not in st.session_state:
|
16 |
+
st.session_state.uploaded_files = {}
|
17 |
+
|
18 |
+
def run(self):
|
19 |
+
st.title("الجدول الزمني للمشروع")
|
20 |
+
|
21 |
+
# إضافة تبويب للملفات
|
22 |
+
tabs = st.tabs(["جدول الكميات", "ملفات المشروع"])
|
23 |
+
|
24 |
+
with tabs[0]:
|
25 |
+
self._handle_boq_tab()
|
26 |
+
|
27 |
+
with tabs[1]:
|
28 |
+
self._handle_project_files()
|
29 |
+
|
30 |
+
def _handle_project_files(self):
|
31 |
+
st.subheader("إدارة ملفات المشروع")
|
32 |
+
|
33 |
+
# رفع الملفات
|
34 |
+
uploaded_file = st.file_uploader(
|
35 |
+
"قم برفع ملفات المشروع",
|
36 |
+
type=['xls', 'xlsx', 'xml', 'xer', 'pmxml', 'mpp', 'vsdx'],
|
37 |
+
help="يمكنك رفع ملفات من برامج مثل Primavera P6, Microsoft Project, Power BI, Visio"
|
38 |
+
)
|
39 |
+
|
40 |
+
if uploaded_file:
|
41 |
+
try:
|
42 |
+
# قراءة وتحليل الملف
|
43 |
+
if uploaded_file.name.endswith(('.xls', '.xlsx')):
|
44 |
+
df = pd.read_excel(uploaded_file)
|
45 |
+
st.session_state.uploaded_files[uploaded_file.name] = {
|
46 |
+
'data': df,
|
47 |
+
'type': 'excel',
|
48 |
+
'upload_time': datetime.now()
|
49 |
+
}
|
50 |
+
|
51 |
+
# عرض معلومات الملف
|
52 |
+
st.success(f"تم استيراد الملف: {uploaded_file.name}")
|
53 |
+
st.write("معلومات الملف:")
|
54 |
+
st.write(f"- عدد الأنشطة: {len(df)}")
|
55 |
+
st.write(f"- الأعمدة: {', '.join(df.columns)}")
|
56 |
+
|
57 |
+
# عرض البيانات في جدول
|
58 |
+
st.dataframe(df, use_container_width=True)
|
59 |
+
|
60 |
+
# إنشاء مخطط جانت تفاعلي
|
61 |
+
if 'Start' in df.columns and 'Finish' in df.columns:
|
62 |
+
self._create_interactive_gantt(df)
|
63 |
+
|
64 |
+
else:
|
65 |
+
st.info(f"تم استلام الملف {uploaded_file.name}. سيتم إضافة دعم لهذا النوع من الملفات قريباً.")
|
66 |
+
|
67 |
+
except Exception as e:
|
68 |
+
st.error(f"حدث خطأ أثناء معالجة الملف: {str(e)}")
|
69 |
+
|
70 |
+
# عرض الملفات المحفوظة
|
71 |
+
if st.session_state.uploaded_files:
|
72 |
+
st.subheader("الملفات المحفوظة")
|
73 |
+
for filename, file_info in st.session_state.uploaded_files.items():
|
74 |
+
with st.expander(filename):
|
75 |
+
st.write(f"نوع الملف: {file_info['type']}")
|
76 |
+
st.write(f"تاريخ الرفع: {file_info['upload_time']}")
|
77 |
+
if file_info['type'] == 'excel':
|
78 |
+
st.dataframe(file_info['data'], use_container_width=True)
|
79 |
+
|
80 |
+
def _create_interactive_gantt(self, df):
|
81 |
+
st.subheader("مخطط جانت التفاعلي")
|
82 |
+
|
83 |
+
# تحضير البيانات للمخطط
|
84 |
+
df['Start'] = pd.to_datetime(df['Start'])
|
85 |
+
df['Finish'] = pd.to_datetime(df['Finish'])
|
86 |
+
|
87 |
+
fig = ff.create_gantt(
|
88 |
+
df,
|
89 |
+
colors={
|
90 |
+
'Task': '#2196F3',
|
91 |
+
'Complete': '#4CAF50'
|
92 |
+
},
|
93 |
+
index_col='Resource',
|
94 |
+
show_colorbar=True,
|
95 |
+
group_tasks=True,
|
96 |
+
showgrid_x=True,
|
97 |
+
showgrid_y=True
|
98 |
+
)
|
99 |
+
|
100 |
+
fig.update_layout(
|
101 |
+
title="مخطط جانت للمشروع",
|
102 |
+
xaxis_title="التاريخ",
|
103 |
+
yaxis_title="الأنشطة",
|
104 |
+
height=600
|
105 |
+
)
|
106 |
+
|
107 |
+
st.plotly_chart(fig, use_container_width=True)
|
108 |
+
|
109 |
+
def _handle_boq_tab(self):
|
110 |
+
# نفس الكود السابق لمعالجة جدول الكميات
|
111 |
+
source_type = st.radio("اختر مصدر جدول الكميات:",
|
112 |
+
["جدول كميات محفوظ", "رفع جدول كميات جديد"],
|
113 |
+
key="boq_source")
|
114 |
+
|
115 |
+
if source_type == "جدول كميات محفوظ":
|
116 |
+
self._handle_saved_boq()
|
117 |
+
else:
|
118 |
+
self._handle_new_boq()
|
119 |
+
|
120 |
+
def _handle_saved_boq(self):
|
121 |
+
if not st.session_state.saved_pricing:
|
122 |
+
st.warning("لا توجد جداول كميات محفوظة.")
|
123 |
+
return
|
124 |
+
|
125 |
+
projects = [(p['project_name'], i) for i, p in enumerate(st.session_state.saved_pricing)]
|
126 |
+
selected_project_name = st.selectbox("اختر المش��وع", [p[0] for p in projects])
|
127 |
+
project_index = next(p[1] for p in projects if p[0] == selected_project_name)
|
128 |
+
project = st.session_state.saved_pricing[project_index]
|
129 |
+
|
130 |
+
self._display_project_schedule(project)
|
131 |
+
|
132 |
+
def _handle_new_boq(self):
|
133 |
+
uploaded_file = st.file_uploader("قم برفع ملف Excel لجدول الكميات", type=['xlsx', 'xls'])
|
134 |
+
if uploaded_file:
|
135 |
+
try:
|
136 |
+
df = pd.read_excel(uploaded_file)
|
137 |
+
project = self._create_project_from_boq(df)
|
138 |
+
self._display_project_schedule(project)
|
139 |
+
except Exception as e:
|
140 |
+
st.error(f"حدث خطأ أثناء قراءة الملف: {str(e)}")
|
141 |
+
|
142 |
+
def _create_project_from_boq(self, df):
|
143 |
+
return {
|
144 |
+
'project_name': 'مشروع جديد',
|
145 |
+
'items': [row.to_dict() for _, row in df.iterrows()],
|
146 |
+
'total_price': df.get('الإجمالي', df.get('total_price', 0)).sum(),
|
147 |
+
'project_duration': 180
|
148 |
+
}
|
149 |
+
|
150 |
+
def _display_project_schedule(self, project):
|
151 |
+
if not project:
|
152 |
+
return
|
153 |
+
|
154 |
+
st.subheader("تفاصيل المشروع")
|
155 |
+
col1, col2 = st.columns(2)
|
156 |
+
with col1:
|
157 |
+
st.write(f"اسم المشروع: {project['project_name']}")
|
158 |
+
st.write(f"إجمالي القيمة: {project['total_price']:,.2f} ريال")
|
159 |
+
with col2:
|
160 |
+
project['project_duration'] = st.number_input(
|
161 |
+
"مدة المشروع (بالأيام)",
|
162 |
+
min_value=30,
|
163 |
+
max_value=1800,
|
164 |
+
value=project.get('project_duration', 180)
|
165 |
+
)
|
166 |
+
|
167 |
+
self._generate_and_display_schedule(project)
|
168 |
+
|
169 |
+
def _generate_and_display_schedule(self, project):
|
170 |
+
if 'schedule_items' not in project:
|
171 |
+
self._initialize_schedule_items(project)
|
172 |
+
|
173 |
+
st.subheader("تحرير الجدول الزمني")
|
174 |
+
edited_df = self._edit_schedule(project['schedule_items'])
|
175 |
+
project['schedule_items'] = edited_df.to_dict('records')
|
176 |
+
|
177 |
+
self._display_gantt_chart(project['schedule_items'])
|
178 |
+
self._display_progress_report(project['schedule_items'])
|
179 |
+
|
180 |
+
def _initialize_schedule_items(self, project):
|
181 |
+
project['schedule_items'] = []
|
182 |
+
for item in project['items']:
|
183 |
+
relative_duration = int((item['total_price'] / project['total_price']) * project['project_duration'])
|
184 |
+
schedule_item = {
|
185 |
+
'Task': item.get('description', ''),
|
186 |
+
'Start': datetime.now().strftime('%Y-%m-%d'),
|
187 |
+
'Finish': (datetime.now() + timedelta(days=relative_duration)).strftime('%Y-%m-%d'),
|
188 |
+
'Duration': relative_duration,
|
189 |
+
'Dependencies': '',
|
190 |
+
'Progress': 0,
|
191 |
+
'Resource': ''
|
192 |
+
}
|
193 |
+
project['schedule_items'].append(schedule_item)
|
194 |
+
|
195 |
+
def _edit_schedule(self, schedule_items):
|
196 |
+
return st.data_editor(
|
197 |
+
pd.DataFrame(schedule_items),
|
198 |
+
column_config={
|
199 |
+
"Task": "البند",
|
200 |
+
"Start": st.column_config.DateColumn("تاريخ البداية"),
|
201 |
+
"Finish": st.column_config.DateColumn("تاريخ النهاية"),
|
202 |
+
"Duration": "المدة (أيام)",
|
203 |
+
"Dependencies": "الاعتماديات",
|
204 |
+
"Progress": st.column_config.NumberColumn("نسبة الإنجاز %", min_value=0, max_value=100),
|
205 |
+
"Resource": "الموارد"
|
206 |
+
},
|
207 |
+
use_container_width=True,
|
208 |
+
hide_index=True
|
209 |
+
)
|
210 |
+
|
211 |
+
def _display_gantt_chart(self, schedule_items):
|
212 |
+
st.subheader("مخطط جانت")
|
213 |
+
df = pd.DataFrame(schedule_items)
|
214 |
+
fig = ff.create_gantt(
|
215 |
+
df,
|
216 |
+
colors={
|
217 |
+
'Complete': 'rgb(0, 255, 100)',
|
218 |
+
'Incomplete': 'rgb(160, 160, 160)'
|
219 |
+
},
|
220 |
+
index_col='Resource',
|
221 |
+
show_colorbar=True,
|
222 |
+
group_tasks=True,
|
223 |
+
showgrid_x=True,
|
224 |
+
showgrid_y=True
|
225 |
+
)
|
226 |
+
|
227 |
+
fig.update_layout(
|
228 |
+
title="مخطط جانت للمشروع",
|
229 |
+
xaxis_title="التاريخ",
|
230 |
+
yaxis_title="البنود",
|
231 |
+
height=600
|
232 |
+
)
|
233 |
+
|
234 |
+
st.plotly_chart(fig, use_container_width=True)
|
235 |
+
|
236 |
+
def _display_progress_report(self, schedule_items):
|
237 |
+
st.subheader("تقرير تقدم المشروع")
|
238 |
+
df = pd.DataFrame(schedule_items)
|
239 |
+
avg_progress = df['Progress'].mean()
|
240 |
+
|
241 |
+
col1, col2, col3 = st.columns(3)
|
242 |
+
with col1:
|
243 |
+
st.metric("متوسط نسبة الإنجاز", f"{avg_progress:.1f}%")
|
244 |
+
with col2:
|
245 |
+
completed_tasks = len(df[df['Progress'] == 100])
|
246 |
+
st.metric("البنود المكتملة", f"{completed_tasks} من {len(df)}")
|
247 |
+
with col3:
|
248 |
+
not_started = len(df[df['Progress'] == 0])
|
249 |
+
st.metric("ا��بنود غير المبدوءة", not_started)
|
modules/translation/translation_app.py
CHANGED
@@ -1,936 +1,936 @@
|
|
1 |
-
"""
|
2 |
-
وحدة الترجمة - نظام تحليل المناقصات
|
3 |
-
"""
|
4 |
-
|
5 |
-
import streamlit as st
|
6 |
-
import pandas as pd
|
7 |
-
import numpy as np
|
8 |
-
import os
|
9 |
-
import sys
|
10 |
-
from pathlib import Path
|
11 |
-
import re
|
12 |
-
import datetime
|
13 |
-
|
14 |
-
# إضافة مسار المشروع للنظام
|
15 |
-
sys.path.append(str(Path(__file__).parent.parent))
|
16 |
-
|
17 |
-
# استيراد محسن واجهة المستخدم
|
18 |
-
from styling.enhanced_ui import UIEnhancer
|
19 |
-
|
20 |
-
class TranslationApp:
|
21 |
-
"""تطبيق الترجمة"""
|
22 |
-
|
23 |
-
def __init__(self):
|
24 |
-
"""تهيئة تطبيق الترجمة"""
|
25 |
-
self.ui = UIEnhancer(page_title="الترجمة - نظام تحليل المناقصات", page_icon="🌐")
|
26 |
-
self.ui.apply_theme_colors()
|
27 |
-
|
28 |
-
# قائمة اللغات المدعومة
|
29 |
-
self.supported_languages = {
|
30 |
-
"ar": "العربية",
|
31 |
-
"en": "الإنجليزية",
|
32 |
-
"fr": "الفرنسية",
|
33 |
-
"de": "الألمانية",
|
34 |
-
"es": "الإسبانية",
|
35 |
-
"it": "الإيطالية",
|
36 |
-
"zh": "الصينية",
|
37 |
-
"ja": "اليابانية",
|
38 |
-
"ru": "الروسية",
|
39 |
-
"tr": "التركية"
|
40 |
-
}
|
41 |
-
|
42 |
-
# بيانات نموذجية للمصطلحات الفنية
|
43 |
-
self.technical_terms = [
|
44 |
-
{"ar": "كراسة الشروط", "en": "Terms and Conditions Document", "category": "مستندات"},
|
45 |
-
{"ar": "جدول الكميات", "en": "Bill of Quantities (BOQ)", "category": "مستندات"},
|
46 |
-
{"ar": "المواصفات الفنية", "en": "Technical Specifications", "category": "مستندات"},
|
47 |
-
{"ar": "ضمان ابتدائي", "en": "Bid Bond", "category": "ضمانات"},
|
48 |
-
{"ar": "ضمان حسن التنفيذ", "en": "Performance Bond", "category": "ضمانات"},
|
49 |
-
{"ar": "ضمان دفعة مقدمة", "en": "Advance Payment Guarantee", "category": "ضمانات"},
|
50 |
-
{"ar": "ضمان صيانة", "en": "Maintenance Bond", "category": "ضمانات"},
|
51 |
-
{"ar": "مناقصة عامة", "en": "Public Tender", "category": "أنواع المناقصات"},
|
52 |
-
{"ar": "مناقصة محدودة", "en": "Limited Tender", "category": "أنواع المناقصات"},
|
53 |
-
{"ar": "منافسة", "en": "Competition", "category": "أنواع المناقصات"},
|
54 |
-
{"ar": "أمر شراء", "en": "Purchase Order", "category": "عقود"},
|
55 |
-
{"ar": "عقد إطاري", "en": "Framework Agreement", "category": "عقود"},
|
56 |
-
{"ar": "عقد زمني", "en": "Time-based Contract", "category": "عقود"},
|
57 |
-
{"ar": "عقد تسليم مفتاح", "en": "Turnkey Contract", "category": "عقود"},
|
58 |
-
{"ar": "مقاول من الباطن", "en": "Subcontractor", "category": "أطراف"},
|
59 |
-
{"ar": "استشاري", "en": "Consultant", "category": "أطراف"},
|
60 |
-
{"ar": "مالك المشروع", "en": "Project Owner", "category": "أطراف"},
|
61 |
-
{"ar": "مدير المشروع", "en": "Project Manager", "category": "أطراف"},
|
62 |
-
{"ar": "مهندس الموقع", "en": "Site Engineer", "category": "أطراف"},
|
63 |
-
{"ar": "مراقب الجودة", "en": "Quality Control", "category": "أطراف"},
|
64 |
-
{"ar": "أعمال مدنية", "en": "Civil Works", "category": "أعمال"},
|
65 |
-
{"ar": "أعمال كهربائية", "en": "Electrical Works", "category": "أعمال"},
|
66 |
-
{"ar": "أعمال ميكانيكية", "en": "Mechanical Works", "category": "أعمال"},
|
67 |
-
{"ar": "أعمال معمارية", "en": "Architectural Works", "category": "أعمال"},
|
68 |
-
{"ar": "أعمال تشطيبات", "en": "Finishing Works", "category": "أعمال"},
|
69 |
-
{"ar": "غرامة تأخير", "en": "Delay Penalty", "category": "شروط"},
|
70 |
-
{"ar": "مدة التنفيذ", "en": "Execution Period", "category": "شروط"},
|
71 |
-
{"ar": "فترة الضمان", "en": "Warranty Period", "category": "شروط"},
|
72 |
-
{"ar": "شروط الدفع", "en": "Payment Terms", "category": "شروط"},
|
73 |
-
{"ar": "تسوية النزاعات", "en": "Dispute Resolution", "category": "شروط"}
|
74 |
-
]
|
75 |
-
|
76 |
-
# بيانات نموذجية للمستندات المترجمة
|
77 |
-
self.translated_documents = [
|
78 |
-
{
|
79 |
-
"id": "TD001",
|
80 |
-
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
81 |
-
"source_language": "ar",
|
82 |
-
"target_language": "en",
|
83 |
-
"original_file": "specs_v2.0_ar.pdf",
|
84 |
-
"translated_file": "specs_v2.0_en.pdf",
|
85 |
-
"translation_date": "2025-03-15",
|
86 |
-
"translated_by": "أحمد محمد",
|
87 |
-
"status": "مكتمل",
|
88 |
-
"pages": 52,
|
89 |
-
"related_entity": "T-2025-001"
|
90 |
-
},
|
91 |
-
{
|
92 |
-
"id": "TD002",
|
93 |
-
"name": "جدول
|
94 |
-
"source_language": "ar",
|
95 |
-
"target_language": "en",
|
96 |
-
"original_file": "boq_v1.1_ar.xlsx",
|
97 |
-
"translated_file": "boq_v1.1_en.xlsx",
|
98 |
-
"translation_date": "2025-02-25",
|
99 |
-
"translated_by": "سارة عبدالله",
|
100 |
-
"status": "مكتمل",
|
101 |
-
"pages": 22,
|
102 |
-
"related_entity": "T-2025-001"
|
103 |
-
},
|
104 |
-
{
|
105 |
-
"id": "TD003",
|
106 |
-
"name": "المخططات - مناقصة إنشاء مبنى إداري",
|
107 |
-
"source_language": "ar",
|
108 |
-
"target_language": "en",
|
109 |
-
"original_file": "drawings_v2.0_ar.pdf",
|
110 |
-
"translated_file": "drawings_v2.0_en.pdf",
|
111 |
-
"translation_date": "2025-03-20",
|
112 |
-
"translated_by": "محمد علي",
|
113 |
-
"status": "مكتمل",
|
114 |
-
"pages": 35,
|
115 |
-
"related_entity": "T-2025-001"
|
116 |
-
},
|
117 |
-
{
|
118 |
-
"id": "TD004",
|
119 |
-
"name": "كراسة الشروط - مناقصة صيانة طرق",
|
120 |
-
"source_language": "ar",
|
121 |
-
"target_language": "en",
|
122 |
-
"original_file": "specs_v1.1_ar.pdf",
|
123 |
-
"translated_file": "specs_v1.1_en.pdf",
|
124 |
-
"translation_date": "2025-03-25",
|
125 |
-
"translated_by": "فاطمة أحمد",
|
126 |
-
"status": "مكتمل",
|
127 |
-
"pages": 34,
|
128 |
-
"related_entity": "T-2025-002"
|
129 |
-
},
|
130 |
-
{
|
131 |
-
"id": "TD005",
|
132 |
-
"name": "جدول الكميات - مناقصة صيانة طرق",
|
133 |
-
"source_language": "ar",
|
134 |
-
"target_language": "en",
|
135 |
-
"original_file": "boq_v1.0_ar.xlsx",
|
136 |
-
"translated_file": "boq_v1.0_en.xlsx",
|
137 |
-
"translation_date": "2025-03-10",
|
138 |
-
"translated_by": "خالد عمر",
|
139 |
-
"status": "مكتمل",
|
140 |
-
"pages": 15,
|
141 |
-
"related_entity": "T-2025-002"
|
142 |
-
},
|
143 |
-
{
|
144 |
-
"id": "TD006",
|
145 |
-
"name": "كراسة الشروط - مناقصة توريد معدات",
|
146 |
-
"source_language": "en",
|
147 |
-
"target_language": "ar",
|
148 |
-
"original_file": "specs_v1.0_en.pdf",
|
149 |
-
"translated_file": "specs_v1.0_ar.pdf",
|
150 |
-
"translation_date": "2025-02-15",
|
151 |
-
"translated_by": "أحمد محمد",
|
152 |
-
"status": "مكتمل",
|
153 |
-
"pages": 28,
|
154 |
-
"related_entity": "T-2025-003"
|
155 |
-
},
|
156 |
-
{
|
157 |
-
"id": "TD007",
|
158 |
-
"name": "عقد توريد - مناقصة توريد معدات",
|
159 |
-
"source_language": "en",
|
160 |
-
"target_language": "ar",
|
161 |
-
"original_file": "contract_v1.0_en.pdf",
|
162 |
-
"translated_file": "contract_v1.0_ar.pdf",
|
163 |
-
"translation_date": "2025-03-05",
|
164 |
-
"translated_by": "سارة عبدالله",
|
165 |
-
"status": "مكتمل",
|
166 |
-
"pages": 20,
|
167 |
-
"related_entity": "T-2025-003"
|
168 |
-
},
|
169 |
-
{
|
170 |
-
"id": "TD008",
|
171 |
-
"name": "كراسة الشروط - مناقصة تجهيز مختبرات",
|
172 |
-
"source_language": "ar",
|
173 |
-
"target_language": "en",
|
174 |
-
"original_file": "specs_v1.0_ar.pdf",
|
175 |
-
"translated_file": "specs_v1.0_en.pdf",
|
176 |
-
"translation_date": "2025-03-28",
|
177 |
-
"translated_by": "محمد علي",
|
178 |
-
"status": "قيد التنفيذ",
|
179 |
-
"pages": 30,
|
180 |
-
"related_entity": "T-2025-004"
|
181 |
-
}
|
182 |
-
]
|
183 |
-
|
184 |
-
# بيانات نموذجية للنصوص المترجمة
|
185 |
-
self.sample_translations = {
|
186 |
-
"text1": {
|
187 |
-
"ar": """
|
188 |
-
# كراسة الشروط والمواصفات
|
189 |
-
## مناقصة إنشاء مبنى إداري
|
190 |
-
|
191 |
-
### 1. مقدمة
|
192 |
-
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
|
193 |
-
|
194 |
-
### 2. نطاق العمل
|
195 |
-
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
|
196 |
-
- أعمال الهيكل الإنشائي
|
197 |
-
- أعمال التشطيبات الداخلية والخارجية
|
198 |
-
- أعمال الكهرباء والميكانيكا
|
199 |
-
-
|
200 |
-
- أعمال أنظمة الأمن والسلامة
|
201 |
-
- أعمال أنظمة المباني الذكية
|
202 |
-
""",
|
203 |
-
|
204 |
-
"en": """
|
205 |
-
# Terms and Conditions Document
|
206 |
-
## Administrative Building Construction Tender
|
207 |
-
|
208 |
-
### 1. Introduction
|
209 |
-
Peninsula Contracting Company invites specialized companies to submit their offers for the implementation of an administrative building construction project in Riyadh.
|
210 |
-
|
211 |
-
### 2. Scope of Work
|
212 |
-
The scope of work includes the design and implementation of a 6-floor administrative building with a total area of 6000 square meters, including:
|
213 |
-
- Structural works
|
214 |
-
- Interior and exterior finishing works
|
215 |
-
- Electrical and mechanical works
|
216 |
-
- Site coordination works
|
217 |
-
- Security and safety systems works
|
218 |
-
- Smart building systems works
|
219 |
-
"""
|
220 |
-
},
|
221 |
-
|
222 |
-
"text2": {
|
223 |
-
"ar": """
|
224 |
-
### 3. المواصفات الفنية
|
225 |
-
#### 3.1 أعمال الخرسانة
|
226 |
-
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
|
227 |
-
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
228 |
-
- يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
|
229 |
-
|
230 |
-
#### 3.2 أعمال التشطيبات
|
231 |
-
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
232 |
-
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
233 |
-
- يجب استخدام زجاج عاكس للحرارة للواجهات
|
234 |
-
- يجب استخدام مواد صديقة للبيئة
|
235 |
-
""",
|
236 |
-
|
237 |
-
"en": """
|
238 |
-
### 3. Technical Specifications
|
239 |
-
#### 3.1 Concrete Works
|
240 |
-
- Reinforced concrete must have a strength of not less than 40 Newton/mm²
|
241 |
-
- Reinforcement steel must comply with Saudi specifications
|
242 |
-
- Concrete additives must be used to increase its resistance to weather conditions
|
243 |
-
|
244 |
-
#### 3.2 Finishing Works
|
245 |
-
- High-quality materials must be used for interior finishes
|
246 |
-
- Exterior facades must be weather-resistant
|
247 |
-
- Heat-reflective glass must be used for facades
|
248 |
-
- Environmentally friendly materials must be used
|
249 |
-
"""
|
250 |
-
}
|
251 |
-
}
|
252 |
-
|
253 |
-
def run(self):
|
254 |
-
"""تشغيل تطبيق الترجمة"""
|
255 |
-
# إنشاء قائمة العناصر
|
256 |
-
menu_items = [
|
257 |
-
{"name": "لوحة المعلومات", "icon": "house"},
|
258 |
-
{"name": "المناقصات والعقود", "icon": "file-text"},
|
259 |
-
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
260 |
-
{"name": "نظام التسعير", "icon": "calculator"},
|
261 |
-
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
262 |
-
{"name": "الموارد والتكاليف", "icon": "people"},
|
263 |
-
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
264 |
-
{"name": "إدارة المشاريع", "icon": "kanban"},
|
265 |
-
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
266 |
-
{"name": "الجدول الزمني", "icon": "calendar3"},
|
267 |
-
{"name": "الإشعارات", "icon": "bell"},
|
268 |
-
{"name": "مقارنة المستندات", "icon": "files"},
|
269 |
-
{"name": "الترجمة", "icon": "translate"},
|
270 |
-
{"name": "المساعد الذكي", "icon": "robot"},
|
271 |
-
{"name": "التقارير", "icon": "bar-chart"},
|
272 |
-
{"name": "الإعدادات", "icon": "gear"}
|
273 |
-
]
|
274 |
-
|
275 |
-
# إنشاء الشريط الجانبي
|
276 |
-
selected = self.ui.create_sidebar(menu_items)
|
277 |
-
|
278 |
-
# إنشاء ترويسة الصفحة
|
279 |
-
self.ui.create_header("الترجمة", "أدوات ترجمة المستندات والنصوص")
|
280 |
-
|
281 |
-
# إنشاء علامات تبويب للوظائف المختلفة
|
282 |
-
tabs = st.tabs(["ترجمة النصوص", "ترجمة المستندات", "قاموس المصطلحات", "المستندات المترجمة"])
|
283 |
-
|
284 |
-
# علامة تبويب ترجمة النصوص
|
285 |
-
with tabs[0]:
|
286 |
-
self.translate_text()
|
287 |
-
|
288 |
-
# علامة تبويب ترجمة المستندات
|
289 |
-
with tabs[1]:
|
290 |
-
self.translate_documents()
|
291 |
-
|
292 |
-
# علامة تبويب قاموس المصطلحات
|
293 |
-
with tabs[2]:
|
294 |
-
self.technical_terms_dictionary()
|
295 |
-
|
296 |
-
# علامة تبويب المستندات المترجمة
|
297 |
-
with tabs[3]:
|
298 |
-
self.show_translated_documents()
|
299 |
-
|
300 |
-
def translate_text(self):
|
301 |
-
"""ترجمة النصوص"""
|
302 |
-
st.markdown("### ترجمة النصوص")
|
303 |
-
|
304 |
-
# اختيار لغات الترجمة
|
305 |
-
col1, col2 = st.columns(2)
|
306 |
-
|
307 |
-
with col1:
|
308 |
-
source_language = st.selectbox(
|
309 |
-
"لغة المصدر",
|
310 |
-
options=list(self.supported_languages.keys()),
|
311 |
-
format_func=lambda x: self.supported_languages[x],
|
312 |
-
index=0 # العربية كلغة افتراضية
|
313 |
-
)
|
314 |
-
|
315 |
-
with col2:
|
316 |
-
# استبعاد لغة المصدر من خيارات لغة الهدف
|
317 |
-
target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
|
318 |
-
target_language = st.selectbox(
|
319 |
-
"لغة الهدف",
|
320 |
-
options=list(target_languages.keys()),
|
321 |
-
format_func=lambda x: self.supported_languages[x],
|
322 |
-
index=0 # أول لغة متاحة
|
323 |
-
)
|
324 |
-
|
325 |
-
# خيارات الترجمة
|
326 |
-
st.markdown("#### خيارات الترجمة")
|
327 |
-
|
328 |
-
col1, col2, col3 = st.columns(3)
|
329 |
-
|
330 |
-
with col1:
|
331 |
-
translation_engine = st.radio(
|
332 |
-
"محرك الترجمة",
|
333 |
-
options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"]
|
334 |
-
)
|
335 |
-
|
336 |
-
with col2:
|
337 |
-
use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True)
|
338 |
-
|
339 |
-
with col3:
|
340 |
-
preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True)
|
341 |
-
|
342 |
-
# إدخال النص المراد ترجمته
|
343 |
-
st.markdown("#### النص المراد ترجمته")
|
344 |
-
|
345 |
-
# إضافة أمثلة نصية
|
346 |
-
examples = st.expander("أمثلة نصية")
|
347 |
-
with examples:
|
348 |
-
if st.button("مثال 1: مقدمة كراسة الشروط"):
|
349 |
-
source_text = self.sample_translations["text1"][source_language] if source_language in self.sample_translations["text1"] else self.sample_translations["text1"]["ar"]
|
350 |
-
elif st.button("مثال 2: المواصفات الفنية"):
|
351 |
-
source_text = self.sample_translations["text2"][source_language] if source_language in self.sample_translations["text2"] else self.sample_translations["text2"]["ar"]
|
352 |
-
else:
|
353 |
-
source_text = ""
|
354 |
-
|
355 |
-
if "source_text" not in locals():
|
356 |
-
source_text = ""
|
357 |
-
|
358 |
-
source_text = st.text_area(
|
359 |
-
"أدخل النص المراد ترجمته",
|
360 |
-
value=source_text,
|
361 |
-
height=200
|
362 |
-
)
|
363 |
-
|
364 |
-
# زر الترجمة
|
365 |
-
if st.button("ترجمة النص", use_container_width=True):
|
366 |
-
if not source_text:
|
367 |
-
st.error("يرجى إدخال النص المراد ترجمته")
|
368 |
-
else:
|
369 |
-
# في تطبيق حقيقي، سيتم استدعاء واجهة برمجة التطبيقات للترجمة
|
370 |
-
# هنا نستخدم النصوص النموذجية المحددة مسبقاً للعرض
|
371 |
-
|
372 |
-
with st.spinner("جاري الترجمة..."):
|
373 |
-
# محاكاة تأخير الترجمة
|
374 |
-
import time
|
375 |
-
time.sleep(1)
|
376 |
-
|
377 |
-
# التحقق من وجود ترجمة نموذجية
|
378 |
-
if source_language == "ar" and target_language == "en" and source_text.strip() in [self.sample_translations["text1"]["ar"].strip(), self.sample_translations["text2"]["ar"].strip()]:
|
379 |
-
if source_text.strip() == self.sample_translations["text1"]["ar"].strip():
|
380 |
-
translated_text = self.sample_translations["text1"]["en"]
|
381 |
-
else:
|
382 |
-
translated_text = self.sample_translations["text2"]["en"]
|
383 |
-
elif source_language == "en" and target_language == "ar" and source_text.strip() in [self.sample_translations["text1"]["en"].strip(), self.sample_translations["text2"]["en"].strip()]:
|
384 |
-
if source_text.strip() == self.sample_translations["text1"]["en"].strip():
|
385 |
-
translated_text = self.sample_translations["text1"]["ar"]
|
386 |
-
else:
|
387 |
-
translated_text = self.sample_translations["text2"]["ar"]
|
388 |
-
else:
|
389 |
-
# ترجمة نموذجية
|
390 |
-
translated_text = f"[هذا نص مترجم نموذجي من {self.supported_languages[source_language]} إلى {self.supported_languages[target_language]}]\n\n{source_text}"
|
391 |
-
|
392 |
-
# عرض النص المترجم
|
393 |
-
st.markdown("#### النص المترجم")
|
394 |
-
st.text_area(
|
395 |
-
"النص المترجم",
|
396 |
-
value=translated_text,
|
397 |
-
height=200
|
398 |
-
)
|
399 |
-
|
400 |
-
# أزرار إضافية
|
401 |
-
col1, col2, col3 = st.columns(3)
|
402 |
-
|
403 |
-
with col1:
|
404 |
-
if st.button("نسخ النص المترجم", use_container_width=True):
|
405 |
-
st.success("تم نسخ النص المترجم إلى الحافظة")
|
406 |
-
|
407 |
-
with col2:
|
408 |
-
if st.button("حفظ الترجمة", use_container_width=True):
|
409 |
-
st.success("تم حفظ الترجمة بنجاح")
|
410 |
-
|
411 |
-
with col3:
|
412 |
-
if st.button("تصدير كملف", use_container_width=True):
|
413 |
-
st.success("تم تصدير الترجمة كملف بنجاح")
|
414 |
-
|
415 |
-
# عرض إحصائيات الترجمة
|
416 |
-
st.markdown("#### إحصائيات الترجمة")
|
417 |
-
|
418 |
-
col1, col2, col3, col4 = st.columns(4)
|
419 |
-
|
420 |
-
with col1:
|
421 |
-
self.ui.create_metric_card(
|
422 |
-
"عدد الكلمات",
|
423 |
-
str(len(source_text.split())),
|
424 |
-
None,
|
425 |
-
self.ui.COLORS['primary']
|
426 |
-
)
|
427 |
-
|
428 |
-
with col2:
|
429 |
-
self.ui.create_metric_card(
|
430 |
-
"عدد الأحرف",
|
431 |
-
str(len(source_text)),
|
432 |
-
None,
|
433 |
-
self.ui.COLORS['secondary']
|
434 |
-
)
|
435 |
-
|
436 |
-
with col3:
|
437 |
-
self.ui.create_metric_card(
|
438 |
-
"وقت الترجمة",
|
439 |
-
"1.2 ثانية",
|
440 |
-
None,
|
441 |
-
self.ui.COLORS['success']
|
442 |
-
)
|
443 |
-
|
444 |
-
with col4:
|
445 |
-
self.ui.create_metric_card(
|
446 |
-
"المصطلحات الفنية",
|
447 |
-
"5",
|
448 |
-
None,
|
449 |
-
self.ui.COLORS['accent']
|
450 |
-
)
|
451 |
-
|
452 |
-
def translate_documents(self):
|
453 |
-
"""ترجمة المستندات"""
|
454 |
-
st.markdown("### ترجمة المستندات")
|
455 |
-
|
456 |
-
# اختيار لغات الترجمة
|
457 |
-
col1, col2 = st.columns(2)
|
458 |
-
|
459 |
-
with col1:
|
460 |
-
source_language = st.selectbox(
|
461 |
-
"لغة المصدر",
|
462 |
-
options=list(self.supported_languages.keys()),
|
463 |
-
format_func=lambda x: self.supported_languages[x],
|
464 |
-
index=0, # العربية كلغة افتراضية
|
465 |
-
key="doc_source_lang"
|
466 |
-
)
|
467 |
-
|
468 |
-
with col2:
|
469 |
-
# استبعاد لغة المصدر من خيارات لغة الهدف
|
470 |
-
target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
|
471 |
-
target_language = st.selectbox(
|
472 |
-
"لغة الهدف",
|
473 |
-
options=list(target_languages.keys()),
|
474 |
-
format_func=lambda x: self.supported_languages[x],
|
475 |
-
index=0, # أول لغة متاحة
|
476 |
-
key="doc_target_lang"
|
477 |
-
)
|
478 |
-
|
479 |
-
# تحميل المستند
|
480 |
-
st.markdown("#### تحميل المستند")
|
481 |
-
|
482 |
-
uploaded_file = st.file_uploader("اختر المستند المراد ترجمته", type=["pdf", "docx", "xlsx", "txt"])
|
483 |
-
|
484 |
-
if uploaded_file is not None:
|
485 |
-
st.success(f"تم تحميل الملف: {uploaded_file.name}")
|
486 |
-
|
487 |
-
# عرض معلومات الملف
|
488 |
-
file_details = {
|
489 |
-
"اسم الملف": uploaded_file.name,
|
490 |
-
"نوع الملف": uploaded_file.type,
|
491 |
-
"حجم الملف": f"{uploaded_file.size / 1024:.1f} كيلوبايت"
|
492 |
-
}
|
493 |
-
|
494 |
-
st.json(file_details)
|
495 |
-
|
496 |
-
# خيارات الترجمة
|
497 |
-
st.markdown("#### خيارات الترجمة")
|
498 |
-
|
499 |
-
col1, col2 = st.columns(2)
|
500 |
-
|
501 |
-
with col1:
|
502 |
-
translation_engine = st.radio(
|
503 |
-
"محرك الترجمة",
|
504 |
-
options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"],
|
505 |
-
key="doc_engine"
|
506 |
-
)
|
507 |
-
|
508 |
-
use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True, key="doc_terms")
|
509 |
-
|
510 |
-
with col2:
|
511 |
-
preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True, key="doc_format")
|
512 |
-
|
513 |
-
translate_images = st.checkbox("ترجمة النصوص في الصور", value=False)
|
514 |
-
|
515 |
-
maintain_layout = st.checkbox("الحفاظ على تخطيط المستند", value=True)
|
516 |
-
|
517 |
-
# معلومات إضافية
|
518 |
-
st.markdown("#### معلومات إضافية")
|
519 |
-
|
520 |
-
col1, col2 = st.columns(2)
|
521 |
-
|
522 |
-
with col1:
|
523 |
-
document_name = st.text_input("اسم المستند")
|
524 |
-
|
525 |
-
with col2:
|
526 |
-
related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة أو المشروع)")
|
527 |
-
|
528 |
-
# زر بدء الترجمة
|
529 |
-
if st.button("بدء ترجمة المستند", use_container_width=True):
|
530 |
-
if uploaded_file is None:
|
531 |
-
st.error("يرجى تحميل المستند المراد ترجمته")
|
532 |
-
else:
|
533 |
-
# في تطبيق حقيقي، سيتم إرسال المستند إلى خدمة الترجمة
|
534 |
-
# هنا نعرض محاكاة لعملية الترجمة
|
535 |
-
|
536 |
-
progress_bar = st.progress(0)
|
537 |
-
status_text = st.empty()
|
538 |
-
|
539 |
-
# محاكاة تقدم الترجمة
|
540 |
-
import time
|
541 |
-
for i in range(101):
|
542 |
-
progress_bar.progress(i)
|
543 |
-
|
544 |
-
if i < 10:
|
545 |
-
status_text.text("جاري تحليل المستند...")
|
546 |
-
elif i < 30:
|
547 |
-
status_text.text("جاري استخراج النصوص...")
|
548 |
-
elif i < 70:
|
549 |
-
status_text.text("جاري ترجمة المحتوى...")
|
550 |
-
elif i < 90:
|
551 |
-
status_text.text("جاري إعادة بناء المستند...")
|
552 |
-
else:
|
553 |
-
status_text.text("جاري إنهاء الترجمة...")
|
554 |
-
|
555 |
-
time.sleep(0.05)
|
556 |
-
|
557 |
-
# عرض نتيجة الترجمة
|
558 |
-
st.success("تمت ترجمة المستند بنجاح!")
|
559 |
-
|
560 |
-
# إنشاء اسم الملف المترجم
|
561 |
-
file_name_parts = uploaded_file.name.split('.')
|
562 |
-
translated_file_name = f"{'.'.join(file_name_parts[:-1])}_{target_language}.{file_name_parts[-1]}"
|
563 |
-
|
564 |
-
# عرض معلومات الملف المترجم
|
565 |
-
st.markdown("#### معلومات الملف المترجم")
|
566 |
-
|
567 |
-
col1, col2 = st.columns(2)
|
568 |
-
|
569 |
-
with col1:
|
570 |
-
st.markdown(f"**اسم الملف:** {translated_file_name}")
|
571 |
-
st.markdown(f"**لغة المصدر:** {self.supported_languages[source_language]}")
|
572 |
-
st.markdown(f"**لغة الهدف:** {self.supported_languages[target_language]}")
|
573 |
-
|
574 |
-
with col2:
|
575 |
-
st.markdown(f"**محرك الترجمة:** {translation_engine}")
|
576 |
-
st.markdown(f"**تاريخ الترجمة:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
|
577 |
-
st.markdown(f"**حالة الترجمة:** مكتمل")
|
578 |
-
|
579 |
-
# أزرار إضافية
|
580 |
-
col1, col2, col3 = st.columns(3)
|
581 |
-
|
582 |
-
with col1:
|
583 |
-
if st.button("تنزيل الملف المترجم", use_container_width=True):
|
584 |
-
st.success("تم بدء تنزيل الملف المترجم")
|
585 |
-
|
586 |
-
with col2:
|
587 |
-
if st.button("حفظ في المستندات المترجمة", use_container_width=True):
|
588 |
-
st.success("تم حفظ الملف في المستندات المترجمة")
|
589 |
-
|
590 |
-
with col3:
|
591 |
-
if st.button("مشاركة الملف", use_container_width=True):
|
592 |
-
st.success("تم نسخ رابط مشاركة الملف")
|
593 |
-
|
594 |
-
# عرض إحصائيات الترجمة
|
595 |
-
st.markdown("#### إحصائيات الترجمة")
|
596 |
-
|
597 |
-
col1, col2, col3, col4 = st.columns(4)
|
598 |
-
|
599 |
-
with col1:
|
600 |
-
self.ui.create_metric_card(
|
601 |
-
"عدد الصفحات",
|
602 |
-
"12",
|
603 |
-
None,
|
604 |
-
self.ui.COLORS['primary']
|
605 |
-
)
|
606 |
-
|
607 |
-
with col2:
|
608 |
-
self.ui.create_metric_card(
|
609 |
-
"عدد الكلمات",
|
610 |
-
"2,450",
|
611 |
-
None,
|
612 |
-
self.ui.COLORS['secondary']
|
613 |
-
)
|
614 |
-
|
615 |
-
with col3:
|
616 |
-
self.ui.create_metric_card(
|
617 |
-
"وقت الترجمة",
|
618 |
-
"45 ثانية",
|
619 |
-
None,
|
620 |
-
self.ui.COLORS['success']
|
621 |
-
)
|
622 |
-
|
623 |
-
with col4:
|
624 |
-
self.ui.create_metric_card(
|
625 |
-
"المصطلحات الفنية",
|
626 |
-
"28",
|
627 |
-
None,
|
628 |
-
self.ui.COLORS['accent']
|
629 |
-
)
|
630 |
-
|
631 |
-
def technical_terms_dictionary(self):
|
632 |
-
"""قاموس المصطلحات الفنية"""
|
633 |
-
st.markdown("### قاموس المصطلحات الفنية")
|
634 |
-
|
635 |
-
# إضافة مصطلح جديد
|
636 |
-
with st.expander("إضافة مصطلح جديد"):
|
637 |
-
with st.form("add_term_form"):
|
638 |
-
col1, col2, col3 = st.columns(3)
|
639 |
-
|
640 |
-
with col1:
|
641 |
-
term_ar = st.text_input("المصطلح بالعربية")
|
642 |
-
|
643 |
-
with col2:
|
644 |
-
term_en = st.text_input("المصطلح بالإنجليزية")
|
645 |
-
|
646 |
-
with col3:
|
647 |
-
term_category = st.selectbox(
|
648 |
-
"الفئة",
|
649 |
-
options=["مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
|
650 |
-
)
|
651 |
-
|
652 |
-
# زر إضافة المصطلح
|
653 |
-
submit_button = st.form_submit_button("إضافة المصطلح")
|
654 |
-
|
655 |
-
if submit_button:
|
656 |
-
if not term_ar or not term_en:
|
657 |
-
st.error("يرجى ملء جميع الحقول المطلوبة")
|
658 |
-
else:
|
659 |
-
# في تطبيق حقيقي، سيتم إضافة المصطلح إلى قاعدة البيانات
|
660 |
-
st.success("تمت إضافة المصطلح بنجاح")
|
661 |
-
|
662 |
-
# البحث في المصطلحات
|
663 |
-
st.markdown("#### البحث في المصطلحات")
|
664 |
-
|
665 |
-
col1, col2, col3 = st.columns(3)
|
666 |
-
|
667 |
-
with col1:
|
668 |
-
search_term = st.text_input("البحث عن مصطلح")
|
669 |
-
|
670 |
-
with col2:
|
671 |
-
search_language = st.radio(
|
672 |
-
"لغة البحث",
|
673 |
-
options=["الكل", "العربية", "الإنجليزية"],
|
674 |
-
horizontal=True
|
675 |
-
)
|
676 |
-
|
677 |
-
with col3:
|
678 |
-
category_filter = st.selectbox(
|
679 |
-
"تصفية حسب الفئة",
|
680 |
-
options=["الكل", "مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
|
681 |
-
)
|
682 |
-
|
683 |
-
# تطبيق الفلاتر
|
684 |
-
filtered_terms = self.technical_terms
|
685 |
-
|
686 |
-
if search_term:
|
687 |
-
if search_language == "العربية":
|
688 |
-
filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower()]
|
689 |
-
elif search_language == "الإنجليزية":
|
690 |
-
filtered_terms = [term for term in filtered_terms if search_term.lower() in term["en"].lower()]
|
691 |
-
else:
|
692 |
-
filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower() or search_term.lower() in term["en"].lower()]
|
693 |
-
|
694 |
-
if category_filter != "الكل":
|
695 |
-
filtered_terms = [term for term in filtered_terms if term["category"] == category_filter]
|
696 |
-
|
697 |
-
# عرض المصطلحات
|
698 |
-
st.markdown("#### المصطلحات الفنية")
|
699 |
-
|
700 |
-
if not filtered_terms:
|
701 |
-
st.info("لا توجد مصطلحات تطابق معايير البحث")
|
702 |
-
else:
|
703 |
-
# تحويل البيانات إلى DataFrame
|
704 |
-
terms_df = pd.DataFrame(filtered_terms)
|
705 |
-
|
706 |
-
# إعادة تسمية الأعمدة
|
707 |
-
terms_df = terms_df.rename(columns={
|
708 |
-
"ar": "المصطلح بالعربية",
|
709 |
-
"en": "المصطلح بالإنجليزية",
|
710 |
-
"category": "الفئة"
|
711 |
-
})
|
712 |
-
|
713 |
-
# عرض الجدول
|
714 |
-
st.dataframe(
|
715 |
-
terms_df,
|
716 |
-
use_container_width=True,
|
717 |
-
hide_index=True
|
718 |
-
)
|
719 |
-
|
720 |
-
# أزرار إضافية
|
721 |
-
col1, col2 = st.columns([1, 5])
|
722 |
-
|
723 |
-
with col1:
|
724 |
-
if st.button("تصدير القاموس", use_container_width=True):
|
725 |
-
st.success("تم تصدير القاموس بنجاح")
|
726 |
-
|
727 |
-
# عرض إحصائيات القاموس
|
728 |
-
st.markdown("#### إحصائيات القاموس")
|
729 |
-
|
730 |
-
# حساب عدد المصطلحات في كل فئة
|
731 |
-
category_counts = {}
|
732 |
-
for term in self.technical_terms:
|
733 |
-
if term["category"] not in category_counts:
|
734 |
-
category_counts[term["category"]] = 0
|
735 |
-
category_counts[term["category"]] += 1
|
736 |
-
|
737 |
-
# عرض الإحصائيات
|
738 |
-
col1, col2 = st.columns(2)
|
739 |
-
|
740 |
-
with col1:
|
741 |
-
st.markdown("##### عدد المصطلحات حسب الفئة")
|
742 |
-
|
743 |
-
# تحويل البيانات إلى DataFrame
|
744 |
-
category_df = pd.DataFrame({
|
745 |
-
"الفئة": list(category_counts.keys()),
|
746 |
-
"العدد": list(category_counts.values())
|
747 |
-
})
|
748 |
-
|
749 |
-
# عرض الرسم البياني
|
750 |
-
st.bar_chart(category_df.set_index("الفئة"))
|
751 |
-
|
752 |
-
with col2:
|
753 |
-
st.markdown("##### إحصائيات عامة")
|
754 |
-
|
755 |
-
total_terms = len(self.technical_terms)
|
756 |
-
categories_count = len(category_counts)
|
757 |
-
|
758 |
-
st.markdown(f"**إجمالي المصطلحات:** {total_terms}")
|
759 |
-
st.markdown(f"**عدد الفئات:** {categories_count}")
|
760 |
-
st.markdown(f"**متوسط المصطلحات لكل فئة:** {total_terms / categories_count:.1f}")
|
761 |
-
st.markdown(f"**آخر تحديث للقاموس:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
|
762 |
-
|
763 |
-
def show_translated_documents(self):
|
764 |
-
"""عرض المستندات المترجمة"""
|
765 |
-
st.markdown("### المستندات المترجمة")
|
766 |
-
|
767 |
-
# إنشاء فلاتر للمستندات
|
768 |
-
col1, col2, col3 = st.columns(3)
|
769 |
-
|
770 |
-
with col1:
|
771 |
-
entity_filter = st.selectbox(
|
772 |
-
"تصفية حسب الكيان",
|
773 |
-
options=["الكل"] + list(set([doc["related_entity"] for doc in self.translated_documents]))
|
774 |
-
)
|
775 |
-
|
776 |
-
with col2:
|
777 |
-
language_pair_filter = st.selectbox(
|
778 |
-
"تصفية حسب زوج اللغات",
|
779 |
-
options=["الكل"] + list(set([f"{doc['source_language']} -> {doc['target_language']}" for doc in self.translated_documents]))
|
780 |
-
)
|
781 |
-
|
782 |
-
with col3:
|
783 |
-
status_filter = st.selectbox(
|
784 |
-
"تصفية حسب الحالة",
|
785 |
-
options=["الكل", "مكتمل", "قيد التنفيذ"]
|
786 |
-
)
|
787 |
-
|
788 |
-
# تطبيق الفلاتر
|
789 |
-
filtered_docs = self.translated_documents
|
790 |
-
|
791 |
-
if entity_filter != "الكل":
|
792 |
-
filtered_docs = [doc for doc in filtered_docs if doc["related_entity"] == entity_filter]
|
793 |
-
|
794 |
-
if language_pair_filter != "الكل":
|
795 |
-
source_lang, target_lang = language_pair_filter.split(" -> ")
|
796 |
-
filtered_docs = [doc for doc in filtered_docs if doc["source_language"] == source_lang and doc["target_language"] == target_lang]
|
797 |
-
|
798 |
-
if status_filter != "الكل":
|
799 |
-
filtered_docs = [doc for doc in filtered_docs if doc["status"] == status_filter]
|
800 |
-
|
801 |
-
# عرض المستندات المترجمة
|
802 |
-
if not filtered_docs:
|
803 |
-
st.info("لا توجد مستندات مترجمة تطابق معايير التصفية")
|
804 |
-
else:
|
805 |
-
# تحويل البيانات إلى DataFrame
|
806 |
-
docs_df = pd.DataFrame(filtered_docs)
|
807 |
-
|
808 |
-
# تحويل رموز اللغات إلى أسماء اللغات
|
809 |
-
docs_df["source_language"] = docs_df["source_language"].map(self.supported_languages)
|
810 |
-
docs_df["target_language"] = docs_df["target_language"].map(self.supported_languages)
|
811 |
-
|
812 |
-
# إعادة ترتيب الأعمدة وتغيير أسمائها
|
813 |
-
display_df = docs_df[[
|
814 |
-
"id", "name", "source_language", "target_language", "translation_date", "status", "pages", "related_entity"
|
815 |
-
]].rename(columns={
|
816 |
-
"id": "الرقم",
|
817 |
-
"name": "اسم المستند",
|
818 |
-
"source_language": "لغة المصدر",
|
819 |
-
"target_language": "لغة الهدف",
|
820 |
-
"translation_date": "تاريخ الترجمة",
|
821 |
-
"status": "الحالة",
|
822 |
-
"pages": "عدد الصفحات",
|
823 |
-
"related_entity": "الكيان المرتبط"
|
824 |
-
})
|
825 |
-
|
826 |
-
# عرض الجدول
|
827 |
-
st.dataframe(
|
828 |
-
display_df,
|
829 |
-
use_container_width=True,
|
830 |
-
hide_index=True
|
831 |
-
)
|
832 |
-
|
833 |
-
# عرض تفاصيل المستند المحدد
|
834 |
-
st.markdown("#### تفاصيل المستند المترجم")
|
835 |
-
|
836 |
-
selected_doc_id = st.selectbox(
|
837 |
-
"اختر مستنداً لعرض التفاصيل",
|
838 |
-
options=[doc["id"] for doc in filtered_docs],
|
839 |
-
format_func=lambda x: next((f"{doc['id']} - {doc['name']}" for doc in filtered_docs if doc["id"] == x), "")
|
840 |
-
)
|
841 |
-
|
842 |
-
# العثور على المستند المحدد
|
843 |
-
selected_doc = next((doc for doc in filtered_docs if doc["id"] == selected_doc_id), None)
|
844 |
-
|
845 |
-
if selected_doc:
|
846 |
-
col1, col2 = st.columns(2)
|
847 |
-
|
848 |
-
with col1:
|
849 |
-
st.markdown(f"**اسم المستند:** {selected_doc['name']}")
|
850 |
-
st.markdown(f"**لغة المصدر:** {self.supported_languages[selected_doc['source_language']]}")
|
851 |
-
st.markdown(f"**لغة الهدف:** {self.supported_languages[selected_doc['target_language']]}")
|
852 |
-
st.markdown(f"**تاريخ الترجمة:** {selected_doc['translation_date']}")
|
853 |
-
|
854 |
-
with col2:
|
855 |
-
st.markdown(f"**الملف الأصلي:** {selected_doc['original_file']}")
|
856 |
-
st.markdown(f"**الملف المترجم:** {selected_doc['translated_file']}")
|
857 |
-
st.markdown(f"**المترجم:** {selected_doc['translated_by']}")
|
858 |
-
st.markdown(f"**الحالة:** {selected_doc['status']}")
|
859 |
-
|
860 |
-
# أزرار الإجراءات
|
861 |
-
col1, col2, col3 = st.columns(3)
|
862 |
-
|
863 |
-
with col1:
|
864 |
-
if st.button("تنزيل الملف الأصلي", use_container_width=True):
|
865 |
-
st.success("تم بدء تنزيل الملف الأصلي")
|
866 |
-
|
867 |
-
with col2:
|
868 |
-
if st.button("تنزيل الملف المترجم", use_container_width=True):
|
869 |
-
st.success("تم بدء تنزيل الملف المترجم")
|
870 |
-
|
871 |
-
with col3:
|
872 |
-
if st.button("مشاركة الملف المترجم", use_container_width=True):
|
873 |
-
st.success("تم نسخ رابط مشاركة الملف المترجم")
|
874 |
-
|
875 |
-
# عرض إحصائيات الترجمة
|
876 |
-
st.markdown("#### إحصائيات الترجمة")
|
877 |
-
|
878 |
-
col1, col2, col3 = st.columns(3)
|
879 |
-
|
880 |
-
with col1:
|
881 |
-
# إحصائيات حسب زوج اللغات
|
882 |
-
language_pairs = {}
|
883 |
-
for doc in self.translated_documents:
|
884 |
-
pair = f"{self.supported_languages[doc['source_language']]} -> {self.supported_languages[doc['target_language']]}"
|
885 |
-
if pair not in language_pairs:
|
886 |
-
language_pairs[pair] = 0
|
887 |
-
language_pairs[pair] += 1
|
888 |
-
|
889 |
-
st.markdown("##### المستندات حسب زوج اللغات")
|
890 |
-
|
891 |
-
# تحويل البيانات إلى DataFrame
|
892 |
-
language_df = pd.DataFrame({
|
893 |
-
"زوج اللغات": list(language_pairs.keys()),
|
894 |
-
"العدد": list(language_pairs.values())
|
895 |
-
})
|
896 |
-
|
897 |
-
# عرض الرسم البياني
|
898 |
-
st.bar_chart(language_df.set_index("زوج اللغات"))
|
899 |
-
|
900 |
-
with col2:
|
901 |
-
# إحصائيات حسب الكيان المرتبط
|
902 |
-
entity_counts = {}
|
903 |
-
for doc in self.translated_documents:
|
904 |
-
if doc["related_entity"] not in entity_counts:
|
905 |
-
entity_counts[doc["related_entity"]] = 0
|
906 |
-
entity_counts[doc["related_entity"]] += 1
|
907 |
-
|
908 |
-
st.markdown("##### المستندات حسب الكيان المرتبط")
|
909 |
-
|
910 |
-
# تحويل البيانات إلى DataFrame
|
911 |
-
entity_df = pd.DataFrame({
|
912 |
-
"الكيان المرتبط": list(entity_counts.keys()),
|
913 |
-
"العدد": list(entity_counts.values())
|
914 |
-
})
|
915 |
-
|
916 |
-
# عرض الرسم البياني
|
917 |
-
st.bar_chart(entity_df.set_index("الكيان المرتبط"))
|
918 |
-
|
919 |
-
with col3:
|
920 |
-
# إحصائيات عامة
|
921 |
-
total_docs = len(self.translated_documents)
|
922 |
-
completed_docs = len([doc for doc in self.translated_documents if doc["status"] == "مكتمل"])
|
923 |
-
in_progress_docs = len([doc for doc in self.translated_documents if doc["status"] == "قيد التنفيذ"])
|
924 |
-
total_pages = sum([doc["pages"] for doc in self.translated_documents])
|
925 |
-
|
926 |
-
st.markdown("##### إحصائيات عامة")
|
927 |
-
st.markdown(f"**إجمالي المستندات المترجمة:** {total_docs}")
|
928 |
-
st.markdown(f"**المستندات المكتملة:** {completed_docs}")
|
929 |
-
st.markdown(f"**المستندات قيد التنفيذ:** {in_progress_docs}")
|
930 |
-
st.markdown(f"**إجمالي الصفحات المترجمة:** {total_pages}")
|
931 |
-
st.markdown(f"**متوسط الصفحات لكل مستند:** {total_pages / total_docs:.1f}")
|
932 |
-
|
933 |
-
# تشغيل التطبيق
|
934 |
-
if __name__ == "__main__":
|
935 |
-
translation_app = TranslationApp()
|
936 |
-
translation_app.run()
|
|
|
1 |
+
"""
|
2 |
+
وحدة الترجمة - نظام تحليل المناقصات
|
3 |
+
"""
|
4 |
+
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
import numpy as np
|
8 |
+
import os
|
9 |
+
import sys
|
10 |
+
from pathlib import Path
|
11 |
+
import re
|
12 |
+
import datetime
|
13 |
+
|
14 |
+
# إضافة مسار المشروع للنظام
|
15 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
16 |
+
|
17 |
+
# استيراد محسن واجهة المستخدم
|
18 |
+
from styling.enhanced_ui import UIEnhancer
|
19 |
+
|
20 |
+
class TranslationApp:
|
21 |
+
"""تطبيق الترجمة"""
|
22 |
+
|
23 |
+
def __init__(self):
|
24 |
+
"""تهيئة تطبيق الترجمة"""
|
25 |
+
self.ui = UIEnhancer(page_title="الترجمة - نظام تحليل المناقصات", page_icon="🌐")
|
26 |
+
self.ui.apply_theme_colors()
|
27 |
+
|
28 |
+
# قائمة اللغات المدعومة
|
29 |
+
self.supported_languages = {
|
30 |
+
"ar": "العربية",
|
31 |
+
"en": "الإنجليزية",
|
32 |
+
"fr": "الفرنسية",
|
33 |
+
"de": "الألمانية",
|
34 |
+
"es": "الإسبانية",
|
35 |
+
"it": "الإيطالية",
|
36 |
+
"zh": "الصينية",
|
37 |
+
"ja": "اليابانية",
|
38 |
+
"ru": "الروسية",
|
39 |
+
"tr": "التركية"
|
40 |
+
}
|
41 |
+
|
42 |
+
# بيانات نموذجية للمصطلحات الفنية
|
43 |
+
self.technical_terms = [
|
44 |
+
{"ar": "كراسة الشروط", "en": "Terms and Conditions Document", "category": "مستندات"},
|
45 |
+
{"ar": "جدول الكميات", "en": "Bill of Quantities (BOQ)", "category": "مستندات"},
|
46 |
+
{"ar": "المواصفات الفنية", "en": "Technical Specifications", "category": "مستندات"},
|
47 |
+
{"ar": "ضمان ابتدائي", "en": "Bid Bond", "category": "ضمانات"},
|
48 |
+
{"ar": "ضمان حسن التنفيذ", "en": "Performance Bond", "category": "ضمانات"},
|
49 |
+
{"ar": "ضمان دفعة مقدمة", "en": "Advance Payment Guarantee", "category": "ضمانات"},
|
50 |
+
{"ar": "ضمان صيانة", "en": "Maintenance Bond", "category": "ضمانات"},
|
51 |
+
{"ar": "مناقصة عامة", "en": "Public Tender", "category": "أنواع المناقصات"},
|
52 |
+
{"ar": "مناقصة محدودة", "en": "Limited Tender", "category": "أنواع المناقصات"},
|
53 |
+
{"ar": "منافسة", "en": "Competition", "category": "أنواع المناقصات"},
|
54 |
+
{"ar": "أمر شراء", "en": "Purchase Order", "category": "عقود"},
|
55 |
+
{"ar": "عقد إطاري", "en": "Framework Agreement", "category": "عقود"},
|
56 |
+
{"ar": "عقد زمني", "en": "Time-based Contract", "category": "عقود"},
|
57 |
+
{"ar": "عقد تسليم مفتاح", "en": "Turnkey Contract", "category": "عقود"},
|
58 |
+
{"ar": "مقاول من الباطن", "en": "Subcontractor", "category": "أطراف"},
|
59 |
+
{"ar": "استشاري", "en": "Consultant", "category": "أطراف"},
|
60 |
+
{"ar": "مالك المشروع", "en": "Project Owner", "category": "أطراف"},
|
61 |
+
{"ar": "مدير المشروع", "en": "Project Manager", "category": "أطراف"},
|
62 |
+
{"ar": "مهندس الموقع", "en": "Site Engineer", "category": "أطراف"},
|
63 |
+
{"ar": "مراقب الجودة", "en": "Quality Control", "category": "أطراف"},
|
64 |
+
{"ar": "أعمال مدنية", "en": "Civil Works", "category": "أعمال"},
|
65 |
+
{"ar": "أعمال كهربائية", "en": "Electrical Works", "category": "أعمال"},
|
66 |
+
{"ar": "أعمال ميكانيكية", "en": "Mechanical Works", "category": "أعمال"},
|
67 |
+
{"ar": "أعمال معمارية", "en": "Architectural Works", "category": "أعمال"},
|
68 |
+
{"ar": "أعمال تشطيبات", "en": "Finishing Works", "category": "أعمال"},
|
69 |
+
{"ar": "غرامة تأخير", "en": "Delay Penalty", "category": "شروط"},
|
70 |
+
{"ar": "مدة التنفيذ", "en": "Execution Period", "category": "شروط"},
|
71 |
+
{"ar": "فترة الضمان", "en": "Warranty Period", "category": "شروط"},
|
72 |
+
{"ar": "شروط الدفع", "en": "Payment Terms", "category": "شروط"},
|
73 |
+
{"ar": "تسوية النزاعات", "en": "Dispute Resolution", "category": "شروط"}
|
74 |
+
]
|
75 |
+
|
76 |
+
# بيانات نموذجية للمستندات المترجمة
|
77 |
+
self.translated_documents = [
|
78 |
+
{
|
79 |
+
"id": "TD001",
|
80 |
+
"name": "كراسة الشروط - مناقصة إنشاء مبنى إداري",
|
81 |
+
"source_language": "ar",
|
82 |
+
"target_language": "en",
|
83 |
+
"original_file": "specs_v2.0_ar.pdf",
|
84 |
+
"translated_file": "specs_v2.0_en.pdf",
|
85 |
+
"translation_date": "2025-03-15",
|
86 |
+
"translated_by": "أحمد محمد",
|
87 |
+
"status": "مكتمل",
|
88 |
+
"pages": 52,
|
89 |
+
"related_entity": "T-2025-001"
|
90 |
+
},
|
91 |
+
{
|
92 |
+
"id": "TD002",
|
93 |
+
"name": "جدول الك��يات - مناقصة إنشاء مبنى إداري",
|
94 |
+
"source_language": "ar",
|
95 |
+
"target_language": "en",
|
96 |
+
"original_file": "boq_v1.1_ar.xlsx",
|
97 |
+
"translated_file": "boq_v1.1_en.xlsx",
|
98 |
+
"translation_date": "2025-02-25",
|
99 |
+
"translated_by": "سارة عبدالله",
|
100 |
+
"status": "مكتمل",
|
101 |
+
"pages": 22,
|
102 |
+
"related_entity": "T-2025-001"
|
103 |
+
},
|
104 |
+
{
|
105 |
+
"id": "TD003",
|
106 |
+
"name": "المخططات - مناقصة إنشاء مبنى إداري",
|
107 |
+
"source_language": "ar",
|
108 |
+
"target_language": "en",
|
109 |
+
"original_file": "drawings_v2.0_ar.pdf",
|
110 |
+
"translated_file": "drawings_v2.0_en.pdf",
|
111 |
+
"translation_date": "2025-03-20",
|
112 |
+
"translated_by": "محمد علي",
|
113 |
+
"status": "مكتمل",
|
114 |
+
"pages": 35,
|
115 |
+
"related_entity": "T-2025-001"
|
116 |
+
},
|
117 |
+
{
|
118 |
+
"id": "TD004",
|
119 |
+
"name": "كراسة الشروط - مناقصة صيانة طرق",
|
120 |
+
"source_language": "ar",
|
121 |
+
"target_language": "en",
|
122 |
+
"original_file": "specs_v1.1_ar.pdf",
|
123 |
+
"translated_file": "specs_v1.1_en.pdf",
|
124 |
+
"translation_date": "2025-03-25",
|
125 |
+
"translated_by": "فاطمة أحمد",
|
126 |
+
"status": "مكتمل",
|
127 |
+
"pages": 34,
|
128 |
+
"related_entity": "T-2025-002"
|
129 |
+
},
|
130 |
+
{
|
131 |
+
"id": "TD005",
|
132 |
+
"name": "جدول الكميات - مناقصة صيانة طرق",
|
133 |
+
"source_language": "ar",
|
134 |
+
"target_language": "en",
|
135 |
+
"original_file": "boq_v1.0_ar.xlsx",
|
136 |
+
"translated_file": "boq_v1.0_en.xlsx",
|
137 |
+
"translation_date": "2025-03-10",
|
138 |
+
"translated_by": "خالد عمر",
|
139 |
+
"status": "مكتمل",
|
140 |
+
"pages": 15,
|
141 |
+
"related_entity": "T-2025-002"
|
142 |
+
},
|
143 |
+
{
|
144 |
+
"id": "TD006",
|
145 |
+
"name": "كراسة الشروط - مناقصة توريد معدات",
|
146 |
+
"source_language": "en",
|
147 |
+
"target_language": "ar",
|
148 |
+
"original_file": "specs_v1.0_en.pdf",
|
149 |
+
"translated_file": "specs_v1.0_ar.pdf",
|
150 |
+
"translation_date": "2025-02-15",
|
151 |
+
"translated_by": "أحمد محمد",
|
152 |
+
"status": "مكتمل",
|
153 |
+
"pages": 28,
|
154 |
+
"related_entity": "T-2025-003"
|
155 |
+
},
|
156 |
+
{
|
157 |
+
"id": "TD007",
|
158 |
+
"name": "عقد توريد - مناقصة توريد معدات",
|
159 |
+
"source_language": "en",
|
160 |
+
"target_language": "ar",
|
161 |
+
"original_file": "contract_v1.0_en.pdf",
|
162 |
+
"translated_file": "contract_v1.0_ar.pdf",
|
163 |
+
"translation_date": "2025-03-05",
|
164 |
+
"translated_by": "سارة عبدالله",
|
165 |
+
"status": "مكتمل",
|
166 |
+
"pages": 20,
|
167 |
+
"related_entity": "T-2025-003"
|
168 |
+
},
|
169 |
+
{
|
170 |
+
"id": "TD008",
|
171 |
+
"name": "كراسة الشروط - مناقصة تجهيز مختبرات",
|
172 |
+
"source_language": "ar",
|
173 |
+
"target_language": "en",
|
174 |
+
"original_file": "specs_v1.0_ar.pdf",
|
175 |
+
"translated_file": "specs_v1.0_en.pdf",
|
176 |
+
"translation_date": "2025-03-28",
|
177 |
+
"translated_by": "محمد علي",
|
178 |
+
"status": "قيد التنفيذ",
|
179 |
+
"pages": 30,
|
180 |
+
"related_entity": "T-2025-004"
|
181 |
+
}
|
182 |
+
]
|
183 |
+
|
184 |
+
# بيانات نموذجية للنصوص المترجمة
|
185 |
+
self.sample_translations = {
|
186 |
+
"text1": {
|
187 |
+
"ar": """
|
188 |
+
# كراسة الشروط والمواصفات
|
189 |
+
## مناقصة إنشاء مبنى إداري
|
190 |
+
|
191 |
+
### 1. مقدمة
|
192 |
+
تدعو شركة شبه الجزيرة للمقاولات الشركات المتخصصة للتقدم بعروضها لتنفيذ مشروع إنشاء مبنى إداري في مدينة الرياض.
|
193 |
+
|
194 |
+
### 2. نطاق العمل
|
195 |
+
يشمل نطاق العمل تصميم وتنفيذ مبنى إداري مكون من 6 طوابق بمساحة إجمالية 6000 متر مربع، ويشمل ذلك:
|
196 |
+
- أعمال الهيكل الإنشائي
|
197 |
+
- أعمال التشطيبات الداخلية والخارجية
|
198 |
+
- أعمال الكهرباء والميكانيكا
|
199 |
+
- ��عمال تنسيق الموقع
|
200 |
+
- أعمال أنظمة الأمن والسلامة
|
201 |
+
- أعمال أنظمة المباني الذكية
|
202 |
+
""",
|
203 |
+
|
204 |
+
"en": """
|
205 |
+
# Terms and Conditions Document
|
206 |
+
## Administrative Building Construction Tender
|
207 |
+
|
208 |
+
### 1. Introduction
|
209 |
+
Peninsula Contracting Company invites specialized companies to submit their offers for the implementation of an administrative building construction project in Riyadh.
|
210 |
+
|
211 |
+
### 2. Scope of Work
|
212 |
+
The scope of work includes the design and implementation of a 6-floor administrative building with a total area of 6000 square meters, including:
|
213 |
+
- Structural works
|
214 |
+
- Interior and exterior finishing works
|
215 |
+
- Electrical and mechanical works
|
216 |
+
- Site coordination works
|
217 |
+
- Security and safety systems works
|
218 |
+
- Smart building systems works
|
219 |
+
"""
|
220 |
+
},
|
221 |
+
|
222 |
+
"text2": {
|
223 |
+
"ar": """
|
224 |
+
### 3. المواصفات الفنية
|
225 |
+
#### 3.1 أعمال الخرسانة
|
226 |
+
- يجب أن تكون الخرسانة المسلحة بقوة لا تقل عن 40 نيوتن/مم²
|
227 |
+
- يجب استخدام حديد تسليح مطابق للمواصفات السعودية
|
228 |
+
- يجب استخدام إضافات للخرسانة لزيادة مقاومتها للعوامل الجوية
|
229 |
+
|
230 |
+
#### 3.2 أعمال التشطيبات
|
231 |
+
- يجب استخدام مواد عالية الجودة للتشطيبات الداخلية
|
232 |
+
- يجب أن تكون الواجهات الخارجية مقاومة للعوامل الجوية
|
233 |
+
- يجب استخدام زجاج عاكس للحرارة للواجهات
|
234 |
+
- يجب استخدام مواد صديقة للبيئة
|
235 |
+
""",
|
236 |
+
|
237 |
+
"en": """
|
238 |
+
### 3. Technical Specifications
|
239 |
+
#### 3.1 Concrete Works
|
240 |
+
- Reinforced concrete must have a strength of not less than 40 Newton/mm²
|
241 |
+
- Reinforcement steel must comply with Saudi specifications
|
242 |
+
- Concrete additives must be used to increase its resistance to weather conditions
|
243 |
+
|
244 |
+
#### 3.2 Finishing Works
|
245 |
+
- High-quality materials must be used for interior finishes
|
246 |
+
- Exterior facades must be weather-resistant
|
247 |
+
- Heat-reflective glass must be used for facades
|
248 |
+
- Environmentally friendly materials must be used
|
249 |
+
"""
|
250 |
+
}
|
251 |
+
}
|
252 |
+
|
253 |
+
def run(self):
|
254 |
+
"""تشغيل تطبيق الترجمة"""
|
255 |
+
# إنشاء قائمة العناصر
|
256 |
+
menu_items = [
|
257 |
+
{"name": "لوحة المعلومات", "icon": "house"},
|
258 |
+
{"name": "المناقصات والعقود", "icon": "file-text"},
|
259 |
+
{"name": "تحليل المستندات", "icon": "file-earmark-text"},
|
260 |
+
{"name": "نظام التسعير", "icon": "calculator"},
|
261 |
+
{"name": "حاسبة تكاليف البناء", "icon": "building"},
|
262 |
+
{"name": "الموارد والتكاليف", "icon": "people"},
|
263 |
+
{"name": "تحليل المخاطر", "icon": "exclamation-triangle"},
|
264 |
+
{"name": "إدارة المشاريع", "icon": "kanban"},
|
265 |
+
{"name": "الخرائط والمواقع", "icon": "geo-alt"},
|
266 |
+
{"name": "الجدول الزمني", "icon": "calendar3"},
|
267 |
+
{"name": "الإشعارات", "icon": "bell"},
|
268 |
+
{"name": "مقارنة المستندات", "icon": "files"},
|
269 |
+
{"name": "الترجمة", "icon": "translate"},
|
270 |
+
{"name": "المساعد الذكي", "icon": "robot"},
|
271 |
+
{"name": "التقارير", "icon": "bar-chart"},
|
272 |
+
{"name": "الإعدادات", "icon": "gear"}
|
273 |
+
]
|
274 |
+
|
275 |
+
# إنشاء الشريط الجانبي
|
276 |
+
selected = self.ui.create_sidebar(menu_items)
|
277 |
+
|
278 |
+
# إنشاء ترويسة الصفحة
|
279 |
+
self.ui.create_header("الترجمة", "أدوات ترجمة المستندات والنصوص")
|
280 |
+
|
281 |
+
# إنشاء علامات تبويب للوظائف المختلفة
|
282 |
+
tabs = st.tabs(["ترجمة النصوص", "ترجمة المستندات", "قاموس المصطلحات", "المستندات المترجمة"])
|
283 |
+
|
284 |
+
# علامة تبويب ترجمة النصوص
|
285 |
+
with tabs[0]:
|
286 |
+
self.translate_text()
|
287 |
+
|
288 |
+
# علامة تبويب ترجمة المستندات
|
289 |
+
with tabs[1]:
|
290 |
+
self.translate_documents()
|
291 |
+
|
292 |
+
# علامة تبويب قاموس المصطلحات
|
293 |
+
with tabs[2]:
|
294 |
+
self.technical_terms_dictionary()
|
295 |
+
|
296 |
+
# علامة تبويب المستندات المترجمة
|
297 |
+
with tabs[3]:
|
298 |
+
self.show_translated_documents()
|
299 |
+
|
300 |
+
def translate_text(self):
|
301 |
+
"""ترجمة النصوص"""
|
302 |
+
st.markdown("### ترجمة النصوص")
|
303 |
+
|
304 |
+
# اختيار لغات الترجمة
|
305 |
+
col1, col2 = st.columns(2)
|
306 |
+
|
307 |
+
with col1:
|
308 |
+
source_language = st.selectbox(
|
309 |
+
"لغة المصدر",
|
310 |
+
options=list(self.supported_languages.keys()),
|
311 |
+
format_func=lambda x: self.supported_languages[x],
|
312 |
+
index=0 # العربية كلغة افتراضية
|
313 |
+
)
|
314 |
+
|
315 |
+
with col2:
|
316 |
+
# استبعاد لغة المصدر من خيارات لغة الهدف
|
317 |
+
target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
|
318 |
+
target_language = st.selectbox(
|
319 |
+
"لغة الهدف",
|
320 |
+
options=list(target_languages.keys()),
|
321 |
+
format_func=lambda x: self.supported_languages[x],
|
322 |
+
index=0 # أول لغة متاحة
|
323 |
+
)
|
324 |
+
|
325 |
+
# خيارات الترجمة
|
326 |
+
st.markdown("#### خيارات الترجمة")
|
327 |
+
|
328 |
+
col1, col2, col3 = st.columns(3)
|
329 |
+
|
330 |
+
with col1:
|
331 |
+
translation_engine = st.radio(
|
332 |
+
"محرك الترجمة",
|
333 |
+
options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"]
|
334 |
+
)
|
335 |
+
|
336 |
+
with col2:
|
337 |
+
use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True)
|
338 |
+
|
339 |
+
with col3:
|
340 |
+
preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True)
|
341 |
+
|
342 |
+
# إدخال النص المراد ترجمته
|
343 |
+
st.markdown("#### النص المراد ترجمته")
|
344 |
+
|
345 |
+
# إضافة أمثلة نصية
|
346 |
+
examples = st.expander("أمثلة نصية")
|
347 |
+
with examples:
|
348 |
+
if st.button("مثال 1: مقدمة كراسة الشروط"):
|
349 |
+
source_text = self.sample_translations["text1"][source_language] if source_language in self.sample_translations["text1"] else self.sample_translations["text1"]["ar"]
|
350 |
+
elif st.button("مثال 2: المواصفات الفنية"):
|
351 |
+
source_text = self.sample_translations["text2"][source_language] if source_language in self.sample_translations["text2"] else self.sample_translations["text2"]["ar"]
|
352 |
+
else:
|
353 |
+
source_text = ""
|
354 |
+
|
355 |
+
if "source_text" not in locals():
|
356 |
+
source_text = ""
|
357 |
+
|
358 |
+
source_text = st.text_area(
|
359 |
+
"أدخل النص المراد ترجمته",
|
360 |
+
value=source_text,
|
361 |
+
height=200
|
362 |
+
)
|
363 |
+
|
364 |
+
# زر الترجمة
|
365 |
+
if st.button("ترجمة النص", use_container_width=True):
|
366 |
+
if not source_text:
|
367 |
+
st.error("يرجى إدخال النص المراد ترجمته")
|
368 |
+
else:
|
369 |
+
# في تطبيق حقيقي، سيتم استدعاء واجهة برمجة التطبيقات للترجمة
|
370 |
+
# هنا نستخدم النصوص النموذجية المحددة مسبقاً للعرض
|
371 |
+
|
372 |
+
with st.spinner("جاري الترجمة..."):
|
373 |
+
# محاكاة تأخير الترجمة
|
374 |
+
import time
|
375 |
+
time.sleep(1)
|
376 |
+
|
377 |
+
# التحقق من وجود ترجمة نموذجية
|
378 |
+
if source_language == "ar" and target_language == "en" and source_text.strip() in [self.sample_translations["text1"]["ar"].strip(), self.sample_translations["text2"]["ar"].strip()]:
|
379 |
+
if source_text.strip() == self.sample_translations["text1"]["ar"].strip():
|
380 |
+
translated_text = self.sample_translations["text1"]["en"]
|
381 |
+
else:
|
382 |
+
translated_text = self.sample_translations["text2"]["en"]
|
383 |
+
elif source_language == "en" and target_language == "ar" and source_text.strip() in [self.sample_translations["text1"]["en"].strip(), self.sample_translations["text2"]["en"].strip()]:
|
384 |
+
if source_text.strip() == self.sample_translations["text1"]["en"].strip():
|
385 |
+
translated_text = self.sample_translations["text1"]["ar"]
|
386 |
+
else:
|
387 |
+
translated_text = self.sample_translations["text2"]["ar"]
|
388 |
+
else:
|
389 |
+
# ترجمة نموذجية للعر�� فقط
|
390 |
+
translated_text = f"[هذا نص مترجم نموذجي من {self.supported_languages[source_language]} إلى {self.supported_languages[target_language]}]\n\n{source_text}"
|
391 |
+
|
392 |
+
# عرض النص المترجم
|
393 |
+
st.markdown("#### النص المترجم")
|
394 |
+
st.text_area(
|
395 |
+
"النص المترجم",
|
396 |
+
value=translated_text,
|
397 |
+
height=200
|
398 |
+
)
|
399 |
+
|
400 |
+
# أزرار إضافية
|
401 |
+
col1, col2, col3 = st.columns(3)
|
402 |
+
|
403 |
+
with col1:
|
404 |
+
if st.button("نسخ النص المترجم", use_container_width=True):
|
405 |
+
st.success("تم نسخ النص المترجم إلى الحافظة")
|
406 |
+
|
407 |
+
with col2:
|
408 |
+
if st.button("حفظ الترجمة", use_container_width=True):
|
409 |
+
st.success("تم حفظ الترجمة بنجاح")
|
410 |
+
|
411 |
+
with col3:
|
412 |
+
if st.button("تصدير كملف", use_container_width=True):
|
413 |
+
st.success("تم تصدير الترجمة كملف بنجاح")
|
414 |
+
|
415 |
+
# عرض إحصائيات الترجمة
|
416 |
+
st.markdown("#### إحصائيات الترجمة")
|
417 |
+
|
418 |
+
col1, col2, col3, col4 = st.columns(4)
|
419 |
+
|
420 |
+
with col1:
|
421 |
+
self.ui.create_metric_card(
|
422 |
+
"عدد الكلمات",
|
423 |
+
str(len(source_text.split())),
|
424 |
+
None,
|
425 |
+
self.ui.COLORS['primary']
|
426 |
+
)
|
427 |
+
|
428 |
+
with col2:
|
429 |
+
self.ui.create_metric_card(
|
430 |
+
"عدد الأحرف",
|
431 |
+
str(len(source_text)),
|
432 |
+
None,
|
433 |
+
self.ui.COLORS['secondary']
|
434 |
+
)
|
435 |
+
|
436 |
+
with col3:
|
437 |
+
self.ui.create_metric_card(
|
438 |
+
"وقت الترجمة",
|
439 |
+
"1.2 ثانية",
|
440 |
+
None,
|
441 |
+
self.ui.COLORS['success']
|
442 |
+
)
|
443 |
+
|
444 |
+
with col4:
|
445 |
+
self.ui.create_metric_card(
|
446 |
+
"المصطلحات الفنية",
|
447 |
+
"5",
|
448 |
+
None,
|
449 |
+
self.ui.COLORS['accent']
|
450 |
+
)
|
451 |
+
|
452 |
+
def translate_documents(self):
|
453 |
+
"""ترجمة المستندات"""
|
454 |
+
st.markdown("### ترجمة المستندات")
|
455 |
+
|
456 |
+
# اختيار لغات الترجمة
|
457 |
+
col1, col2 = st.columns(2)
|
458 |
+
|
459 |
+
with col1:
|
460 |
+
source_language = st.selectbox(
|
461 |
+
"لغة المصدر",
|
462 |
+
options=list(self.supported_languages.keys()),
|
463 |
+
format_func=lambda x: self.supported_languages[x],
|
464 |
+
index=0, # العربية كلغة افتراضية
|
465 |
+
key="doc_source_lang"
|
466 |
+
)
|
467 |
+
|
468 |
+
with col2:
|
469 |
+
# استبعاد لغة المصدر من خيارات لغة الهدف
|
470 |
+
target_languages = {k: v for k, v in self.supported_languages.items() if k != source_language}
|
471 |
+
target_language = st.selectbox(
|
472 |
+
"لغة الهدف",
|
473 |
+
options=list(target_languages.keys()),
|
474 |
+
format_func=lambda x: self.supported_languages[x],
|
475 |
+
index=0, # أول لغة متاحة
|
476 |
+
key="doc_target_lang"
|
477 |
+
)
|
478 |
+
|
479 |
+
# تحميل المستند
|
480 |
+
st.markdown("#### تحميل المستند")
|
481 |
+
|
482 |
+
uploaded_file = st.file_uploader("اختر المستند المراد ترجمته", type=["pdf", "docx", "xlsx", "txt"])
|
483 |
+
|
484 |
+
if uploaded_file is not None:
|
485 |
+
st.success(f"تم تحميل الملف: {uploaded_file.name}")
|
486 |
+
|
487 |
+
# عرض معلومات الملف
|
488 |
+
file_details = {
|
489 |
+
"اسم الملف": uploaded_file.name,
|
490 |
+
"نوع الملف": uploaded_file.type,
|
491 |
+
"حجم الملف": f"{uploaded_file.size / 1024:.1f} كيلوبايت"
|
492 |
+
}
|
493 |
+
|
494 |
+
st.json(file_details)
|
495 |
+
|
496 |
+
# خيارات الترجمة
|
497 |
+
st.markdown("#### خيارات الترجمة")
|
498 |
+
|
499 |
+
col1, col2 = st.columns(2)
|
500 |
+
|
501 |
+
with col1:
|
502 |
+
translation_engine = st.radio(
|
503 |
+
"محرك الترجمة",
|
504 |
+
options=["OpenAI", "Google Translate", "Microsoft Translator", "محلي"],
|
505 |
+
key="doc_engine"
|
506 |
+
)
|
507 |
+
|
508 |
+
use_technical_terms = st.checkbox("استخدام قاموس المصطلحات الفنية", value=True, key="doc_terms")
|
509 |
+
|
510 |
+
with col2:
|
511 |
+
preserve_formatting = st.checkbox("الحفاظ على التنسيق", value=True, key="doc_format")
|
512 |
+
|
513 |
+
translate_images = st.checkbox("ترجمة النصوص في الصور", value=False)
|
514 |
+
|
515 |
+
maintain_layout = st.checkbox("الحفاظ على تخطيط المستند", value=True)
|
516 |
+
|
517 |
+
# معلومات إضافية
|
518 |
+
st.markdown("#### معلومات إضافية")
|
519 |
+
|
520 |
+
col1, col2 = st.columns(2)
|
521 |
+
|
522 |
+
with col1:
|
523 |
+
document_name = st.text_input("اسم المستند")
|
524 |
+
|
525 |
+
with col2:
|
526 |
+
related_entity = st.text_input("الكيان المرتبط (مثل: رقم المناقصة أو المشروع)")
|
527 |
+
|
528 |
+
# زر بدء الترجمة
|
529 |
+
if st.button("بدء ترجمة المستند", use_container_width=True):
|
530 |
+
if uploaded_file is None:
|
531 |
+
st.error("يرجى تحميل المستند المراد ترجمته")
|
532 |
+
else:
|
533 |
+
# في تطبيق حقيقي، سيتم إرسال المستند إلى خدمة الترجمة
|
534 |
+
# هنا نعرض محاكاة لعملية الترجمة
|
535 |
+
|
536 |
+
progress_bar = st.progress(0)
|
537 |
+
status_text = st.empty()
|
538 |
+
|
539 |
+
# محاكاة تقدم الترجمة
|
540 |
+
import time
|
541 |
+
for i in range(101):
|
542 |
+
progress_bar.progress(i)
|
543 |
+
|
544 |
+
if i < 10:
|
545 |
+
status_text.text("جاري تحليل المستند...")
|
546 |
+
elif i < 30:
|
547 |
+
status_text.text("جاري استخراج النصوص...")
|
548 |
+
elif i < 70:
|
549 |
+
status_text.text("جاري ترجمة المحتوى...")
|
550 |
+
elif i < 90:
|
551 |
+
status_text.text("جاري إعادة بناء المستند...")
|
552 |
+
else:
|
553 |
+
status_text.text("جاري إنهاء الترجمة...")
|
554 |
+
|
555 |
+
time.sleep(0.05)
|
556 |
+
|
557 |
+
# عرض نتيجة الترجمة
|
558 |
+
st.success("تمت ترجمة المستند بنجاح!")
|
559 |
+
|
560 |
+
# إنشاء اسم الملف المترجم
|
561 |
+
file_name_parts = uploaded_file.name.split('.')
|
562 |
+
translated_file_name = f"{'.'.join(file_name_parts[:-1])}_{target_language}.{file_name_parts[-1]}"
|
563 |
+
|
564 |
+
# عرض معلومات الملف المترجم
|
565 |
+
st.markdown("#### معلومات الملف المترجم")
|
566 |
+
|
567 |
+
col1, col2 = st.columns(2)
|
568 |
+
|
569 |
+
with col1:
|
570 |
+
st.markdown(f"**اسم الملف:** {translated_file_name}")
|
571 |
+
st.markdown(f"**لغة المصدر:** {self.supported_languages[source_language]}")
|
572 |
+
st.markdown(f"**لغة الهدف:** {self.supported_languages[target_language]}")
|
573 |
+
|
574 |
+
with col2:
|
575 |
+
st.markdown(f"**محرك الترجمة:** {translation_engine}")
|
576 |
+
st.markdown(f"**تاريخ الترجمة:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
|
577 |
+
st.markdown(f"**حالة الترجمة:** مكتمل")
|
578 |
+
|
579 |
+
# أزرار إضافية
|
580 |
+
col1, col2, col3 = st.columns(3)
|
581 |
+
|
582 |
+
with col1:
|
583 |
+
if st.button("تنزيل الملف المترجم", use_container_width=True):
|
584 |
+
st.success("تم بدء تنزيل الملف المترجم")
|
585 |
+
|
586 |
+
with col2:
|
587 |
+
if st.button("حفظ في المستندات المترجمة", use_container_width=True):
|
588 |
+
st.success("تم حفظ الملف في المستندات المترجمة")
|
589 |
+
|
590 |
+
with col3:
|
591 |
+
if st.button("مشاركة الملف", use_container_width=True):
|
592 |
+
st.success("تم نسخ رابط مشاركة الملف")
|
593 |
+
|
594 |
+
# عرض إحصائيات الترجمة
|
595 |
+
st.markdown("#### إحصائيات الترجمة")
|
596 |
+
|
597 |
+
col1, col2, col3, col4 = st.columns(4)
|
598 |
+
|
599 |
+
with col1:
|
600 |
+
self.ui.create_metric_card(
|
601 |
+
"عدد الصفحات",
|
602 |
+
"12",
|
603 |
+
None,
|
604 |
+
self.ui.COLORS['primary']
|
605 |
+
)
|
606 |
+
|
607 |
+
with col2:
|
608 |
+
self.ui.create_metric_card(
|
609 |
+
"عدد الكلمات",
|
610 |
+
"2,450",
|
611 |
+
None,
|
612 |
+
self.ui.COLORS['secondary']
|
613 |
+
)
|
614 |
+
|
615 |
+
with col3:
|
616 |
+
self.ui.create_metric_card(
|
617 |
+
"وقت الترجمة",
|
618 |
+
"45 ثانية",
|
619 |
+
None,
|
620 |
+
self.ui.COLORS['success']
|
621 |
+
)
|
622 |
+
|
623 |
+
with col4:
|
624 |
+
self.ui.create_metric_card(
|
625 |
+
"المصطلحات الفنية",
|
626 |
+
"28",
|
627 |
+
None,
|
628 |
+
self.ui.COLORS['accent']
|
629 |
+
)
|
630 |
+
|
631 |
+
def technical_terms_dictionary(self):
|
632 |
+
"""قاموس المصطلحات الفنية"""
|
633 |
+
st.markdown("### قاموس المصطلحات الفنية")
|
634 |
+
|
635 |
+
# إضافة مصطلح جديد
|
636 |
+
with st.expander("إضافة مصطلح جديد"):
|
637 |
+
with st.form("add_term_form"):
|
638 |
+
col1, col2, col3 = st.columns(3)
|
639 |
+
|
640 |
+
with col1:
|
641 |
+
term_ar = st.text_input("المصطلح بالعربية")
|
642 |
+
|
643 |
+
with col2:
|
644 |
+
term_en = st.text_input("المصطلح بالإنجليزية")
|
645 |
+
|
646 |
+
with col3:
|
647 |
+
term_category = st.selectbox(
|
648 |
+
"الفئة",
|
649 |
+
options=["مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
|
650 |
+
)
|
651 |
+
|
652 |
+
# زر إضافة المصطلح
|
653 |
+
submit_button = st.form_submit_button("إضافة المصطلح")
|
654 |
+
|
655 |
+
if submit_button:
|
656 |
+
if not term_ar or not term_en:
|
657 |
+
st.error("يرجى ملء جميع الحقول المطلوبة")
|
658 |
+
else:
|
659 |
+
# في تطبيق حقيقي، سيتم إضافة المصطلح إلى قاعدة البيانات
|
660 |
+
st.success("تمت إضافة المصطلح بنجاح")
|
661 |
+
|
662 |
+
# البحث في المصطلحات
|
663 |
+
st.markdown("#### البحث في المصطلحات")
|
664 |
+
|
665 |
+
col1, col2, col3 = st.columns(3)
|
666 |
+
|
667 |
+
with col1:
|
668 |
+
search_term = st.text_input("البحث عن مصطلح")
|
669 |
+
|
670 |
+
with col2:
|
671 |
+
search_language = st.radio(
|
672 |
+
"لغة البحث",
|
673 |
+
options=["الكل", "العربية", "الإنجليزية"],
|
674 |
+
horizontal=True
|
675 |
+
)
|
676 |
+
|
677 |
+
with col3:
|
678 |
+
category_filter = st.selectbox(
|
679 |
+
"تصفية حسب الفئة",
|
680 |
+
options=["الكل", "مستندات", "ضمانات", "أنواع المناقصات", "عقود", "أطراف", "أعمال", "شروط", "أخرى"]
|
681 |
+
)
|
682 |
+
|
683 |
+
# تطبيق الفلاتر
|
684 |
+
filtered_terms = self.technical_terms
|
685 |
+
|
686 |
+
if search_term:
|
687 |
+
if search_language == "العربية":
|
688 |
+
filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower()]
|
689 |
+
elif search_language == "الإنجليزية":
|
690 |
+
filtered_terms = [term for term in filtered_terms if search_term.lower() in term["en"].lower()]
|
691 |
+
else:
|
692 |
+
filtered_terms = [term for term in filtered_terms if search_term.lower() in term["ar"].lower() or search_term.lower() in term["en"].lower()]
|
693 |
+
|
694 |
+
if category_filter != "الكل":
|
695 |
+
filtered_terms = [term for term in filtered_terms if term["category"] == category_filter]
|
696 |
+
|
697 |
+
# عرض المصطلحات
|
698 |
+
st.markdown("#### المصطلحات الفنية")
|
699 |
+
|
700 |
+
if not filtered_terms:
|
701 |
+
st.info("لا توجد مصطلحات تطابق معايير البحث")
|
702 |
+
else:
|
703 |
+
# تحويل البيانات إلى DataFrame
|
704 |
+
terms_df = pd.DataFrame(filtered_terms)
|
705 |
+
|
706 |
+
# إعادة تسمية الأعمدة
|
707 |
+
terms_df = terms_df.rename(columns={
|
708 |
+
"ar": "المصطلح بالعربية",
|
709 |
+
"en": "المصطلح بالإنجليزية",
|
710 |
+
"category": "الفئة"
|
711 |
+
})
|
712 |
+
|
713 |
+
# عرض الجدول
|
714 |
+
st.dataframe(
|
715 |
+
terms_df,
|
716 |
+
use_container_width=True,
|
717 |
+
hide_index=True
|
718 |
+
)
|
719 |
+
|
720 |
+
# أزرار إضافية
|
721 |
+
col1, col2 = st.columns([1, 5])
|
722 |
+
|
723 |
+
with col1:
|
724 |
+
if st.button("تصدير القاموس", use_container_width=True):
|
725 |
+
st.success("تم تصدير القاموس بنجاح")
|
726 |
+
|
727 |
+
# عرض إحصائيات القاموس
|
728 |
+
st.markdown("#### إحصائيات القاموس")
|
729 |
+
|
730 |
+
# حساب عدد المصطلحات في كل فئة
|
731 |
+
category_counts = {}
|
732 |
+
for term in self.technical_terms:
|
733 |
+
if term["category"] not in category_counts:
|
734 |
+
category_counts[term["category"]] = 0
|
735 |
+
category_counts[term["category"]] += 1
|
736 |
+
|
737 |
+
# عرض الإحصائيات
|
738 |
+
col1, col2 = st.columns(2)
|
739 |
+
|
740 |
+
with col1:
|
741 |
+
st.markdown("##### عدد المصطلحات حسب الفئة")
|
742 |
+
|
743 |
+
# تحويل البيانات إلى DataFrame
|
744 |
+
category_df = pd.DataFrame({
|
745 |
+
"الفئة": list(category_counts.keys()),
|
746 |
+
"العدد": list(category_counts.values())
|
747 |
+
})
|
748 |
+
|
749 |
+
# عرض الرسم البياني
|
750 |
+
st.bar_chart(category_df.set_index("الفئة"))
|
751 |
+
|
752 |
+
with col2:
|
753 |
+
st.markdown("##### إحصائيات عامة")
|
754 |
+
|
755 |
+
total_terms = len(self.technical_terms)
|
756 |
+
categories_count = len(category_counts)
|
757 |
+
|
758 |
+
st.markdown(f"**إجمالي المصطلحات:** {total_terms}")
|
759 |
+
st.markdown(f"**عدد الفئات:** {categories_count}")
|
760 |
+
st.markdown(f"**متوسط المصطلحات لكل فئة:** {total_terms / categories_count:.1f}")
|
761 |
+
st.markdown(f"**آخر تحديث للقاموس:** {datetime.datetime.now().strftime('%Y-%m-%d')}")
|
762 |
+
|
763 |
+
def show_translated_documents(self):
|
764 |
+
"""عرض المستندات المترجمة"""
|
765 |
+
st.markdown("### المستندات المترجمة")
|
766 |
+
|
767 |
+
# إنشاء فلاتر للمستندات
|
768 |
+
col1, col2, col3 = st.columns(3)
|
769 |
+
|
770 |
+
with col1:
|
771 |
+
entity_filter = st.selectbox(
|
772 |
+
"تصفية حسب الكيان",
|
773 |
+
options=["الكل"] + list(set([doc["related_entity"] for doc in self.translated_documents]))
|
774 |
+
)
|
775 |
+
|
776 |
+
with col2:
|
777 |
+
language_pair_filter = st.selectbox(
|
778 |
+
"تصفية حسب زوج اللغات",
|
779 |
+
options=["الكل"] + list(set([f"{doc['source_language']} -> {doc['target_language']}" for doc in self.translated_documents]))
|
780 |
+
)
|
781 |
+
|
782 |
+
with col3:
|
783 |
+
status_filter = st.selectbox(
|
784 |
+
"تصفية حسب الحالة",
|
785 |
+
options=["الكل", "مكتمل", "قيد التنفيذ"]
|
786 |
+
)
|
787 |
+
|
788 |
+
# تطبيق الفلاتر
|
789 |
+
filtered_docs = self.translated_documents
|
790 |
+
|
791 |
+
if entity_filter != "الكل":
|
792 |
+
filtered_docs = [doc for doc in filtered_docs if doc["related_entity"] == entity_filter]
|
793 |
+
|
794 |
+
if language_pair_filter != "الكل":
|
795 |
+
source_lang, target_lang = language_pair_filter.split(" -> ")
|
796 |
+
filtered_docs = [doc for doc in filtered_docs if doc["source_language"] == source_lang and doc["target_language"] == target_lang]
|
797 |
+
|
798 |
+
if status_filter != "الكل":
|
799 |
+
filtered_docs = [doc for doc in filtered_docs if doc["status"] == status_filter]
|
800 |
+
|
801 |
+
# عرض المستندات المترجمة
|
802 |
+
if not filtered_docs:
|
803 |
+
st.info("لا توجد مستندات مترجمة تطابق معايير التصفية")
|
804 |
+
else:
|
805 |
+
# تحويل البيانات إلى DataFrame
|
806 |
+
docs_df = pd.DataFrame(filtered_docs)
|
807 |
+
|
808 |
+
# تحويل رموز اللغات إلى أسماء اللغات
|
809 |
+
docs_df["source_language"] = docs_df["source_language"].map(self.supported_languages)
|
810 |
+
docs_df["target_language"] = docs_df["target_language"].map(self.supported_languages)
|
811 |
+
|
812 |
+
# إعادة ترتيب الأعمدة وتغيير أسمائها
|
813 |
+
display_df = docs_df[[
|
814 |
+
"id", "name", "source_language", "target_language", "translation_date", "status", "pages", "related_entity"
|
815 |
+
]].rename(columns={
|
816 |
+
"id": "الرقم",
|
817 |
+
"name": "اسم المستند",
|
818 |
+
"source_language": "لغة المصدر",
|
819 |
+
"target_language": "لغة الهدف",
|
820 |
+
"translation_date": "تاريخ الترجمة",
|
821 |
+
"status": "الحالة",
|
822 |
+
"pages": "عدد الصفحات",
|
823 |
+
"related_entity": "الكيان المرتبط"
|
824 |
+
})
|
825 |
+
|
826 |
+
# عرض الجدول
|
827 |
+
st.dataframe(
|
828 |
+
display_df,
|
829 |
+
use_container_width=True,
|
830 |
+
hide_index=True
|
831 |
+
)
|
832 |
+
|
833 |
+
# عرض تفاصيل المستند المحدد
|
834 |
+
st.markdown("#### تفاصيل المستند المترجم")
|
835 |
+
|
836 |
+
selected_doc_id = st.selectbox(
|
837 |
+
"اختر مستنداً لعرض التفاصيل",
|
838 |
+
options=[doc["id"] for doc in filtered_docs],
|
839 |
+
format_func=lambda x: next((f"{doc['id']} - {doc['name']}" for doc in filtered_docs if doc["id"] == x), "")
|
840 |
+
)
|
841 |
+
|
842 |
+
# العثور على المستند المحدد
|
843 |
+
selected_doc = next((doc for doc in filtered_docs if doc["id"] == selected_doc_id), None)
|
844 |
+
|
845 |
+
if selected_doc:
|
846 |
+
col1, col2 = st.columns(2)
|
847 |
+
|
848 |
+
with col1:
|
849 |
+
st.markdown(f"**اسم المستند:** {selected_doc['name']}")
|
850 |
+
st.markdown(f"**لغة المصدر:** {self.supported_languages[selected_doc['source_language']]}")
|
851 |
+
st.markdown(f"**لغة الهدف:** {self.supported_languages[selected_doc['target_language']]}")
|
852 |
+
st.markdown(f"**تاريخ الترجمة:** {selected_doc['translation_date']}")
|
853 |
+
|
854 |
+
with col2:
|
855 |
+
st.markdown(f"**الملف الأصلي:** {selected_doc['original_file']}")
|
856 |
+
st.markdown(f"**الملف المترجم:** {selected_doc['translated_file']}")
|
857 |
+
st.markdown(f"**المترجم:** {selected_doc['translated_by']}")
|
858 |
+
st.markdown(f"**الحالة:** {selected_doc['status']}")
|
859 |
+
|
860 |
+
# أزرار الإجراءات
|
861 |
+
col1, col2, col3 = st.columns(3)
|
862 |
+
|
863 |
+
with col1:
|
864 |
+
if st.button("تنزيل الملف الأصلي", use_container_width=True):
|
865 |
+
st.success("تم بدء تنزيل الملف الأصلي")
|
866 |
+
|
867 |
+
with col2:
|
868 |
+
if st.button("تنزيل الملف المترجم", use_container_width=True):
|
869 |
+
st.success("تم بدء تنزيل الملف المترجم")
|
870 |
+
|
871 |
+
with col3:
|
872 |
+
if st.button("مشاركة الملف المترجم", use_container_width=True):
|
873 |
+
st.success("تم نسخ رابط مشاركة الملف المترجم")
|
874 |
+
|
875 |
+
# عرض إحصائيات الترجمة
|
876 |
+
st.markdown("#### إحصائيات الترجمة")
|
877 |
+
|
878 |
+
col1, col2, col3 = st.columns(3)
|
879 |
+
|
880 |
+
with col1:
|
881 |
+
# إحصائيات حسب زوج اللغات
|
882 |
+
language_pairs = {}
|
883 |
+
for doc in self.translated_documents:
|
884 |
+
pair = f"{self.supported_languages[doc['source_language']]} -> {self.supported_languages[doc['target_language']]}"
|
885 |
+
if pair not in language_pairs:
|
886 |
+
language_pairs[pair] = 0
|
887 |
+
language_pairs[pair] += 1
|
888 |
+
|
889 |
+
st.markdown("##### المستندات حسب زوج اللغات")
|
890 |
+
|
891 |
+
# تحويل البيانات إلى DataFrame
|
892 |
+
language_df = pd.DataFrame({
|
893 |
+
"زوج اللغات": list(language_pairs.keys()),
|
894 |
+
"العدد": list(language_pairs.values())
|
895 |
+
})
|
896 |
+
|
897 |
+
# عرض الرسم البياني
|
898 |
+
st.bar_chart(language_df.set_index("زوج اللغات"))
|
899 |
+
|
900 |
+
with col2:
|
901 |
+
# إحصائيات حسب الكيان المرتبط
|
902 |
+
entity_counts = {}
|
903 |
+
for doc in self.translated_documents:
|
904 |
+
if doc["related_entity"] not in entity_counts:
|
905 |
+
entity_counts[doc["related_entity"]] = 0
|
906 |
+
entity_counts[doc["related_entity"]] += 1
|
907 |
+
|
908 |
+
st.markdown("##### المستندات حسب الكيان المرتبط")
|
909 |
+
|
910 |
+
# تحويل البيانات إلى DataFrame
|
911 |
+
entity_df = pd.DataFrame({
|
912 |
+
"الكيان المرتبط": list(entity_counts.keys()),
|
913 |
+
"العدد": list(entity_counts.values())
|
914 |
+
})
|
915 |
+
|
916 |
+
# عرض الرسم البياني
|
917 |
+
st.bar_chart(entity_df.set_index("الكيان المرتبط"))
|
918 |
+
|
919 |
+
with col3:
|
920 |
+
# إحصائيات عامة
|
921 |
+
total_docs = len(self.translated_documents)
|
922 |
+
completed_docs = len([doc for doc in self.translated_documents if doc["status"] == "مكتمل"])
|
923 |
+
in_progress_docs = len([doc for doc in self.translated_documents if doc["status"] == "قيد التنفيذ"])
|
924 |
+
total_pages = sum([doc["pages"] for doc in self.translated_documents])
|
925 |
+
|
926 |
+
st.markdown("##### إحصائيات عامة")
|
927 |
+
st.markdown(f"**إجمالي المستندات المترجمة:** {total_docs}")
|
928 |
+
st.markdown(f"**المستندات المكتملة:** {completed_docs}")
|
929 |
+
st.markdown(f"**المستندات قيد التنفيذ:** {in_progress_docs}")
|
930 |
+
st.markdown(f"**إجمالي الصفحات المترجمة:** {total_pages}")
|
931 |
+
st.markdown(f"**متوسط الصفحات لكل مستند:** {total_pages / total_docs:.1f}")
|
932 |
+
|
933 |
+
# تشغيل التطبيق
|
934 |
+
if __name__ == "__main__":
|
935 |
+
translation_app = TranslationApp()
|
936 |
+
translation_app.run()
|
pricing_system/docs/user_guide.md
CHANGED
@@ -1,235 +1,235 @@
|
|
1 |
-
# نظام تحليل المناقصات وتسعير المشاريع - دليل التثبيت والاستخدام
|
2 |
-
|
3 |
-
## مقدمة
|
4 |
-
|
5 |
-
نظام تحليل المناقصات وتسعير المشاريع هو نظام متكامل يساعد المهندسين في تسعير المناقصات والمشاريع بطريقة احترافية ودقيقة. يوفر النظام مجموعة شاملة من الأدوات والوظائف التي تغطي جميع جوانب عملية التسعير، بدءًا من إدارة جداول الكميات وحتى تحليل المحتوى المحلي وإصدار التقارير.
|
6 |
-
|
7 |
-
## متطلبات النظام
|
8 |
-
|
9 |
-
- Python 3.8 أو أحدث
|
10 |
-
- Streamlit 1.10.0 أو أحدث
|
11 |
-
- Pandas 1.3.0 أو أحدث
|
12 |
-
- NumPy 1.20.0 أو أحدث
|
13 |
-
- Matplotlib 3.4.0 أو أحدث
|
14 |
-
- Seaborn 0.11.0 أو أحدث
|
15 |
-
- Plotly 5.3.0 أو أحدث
|
16 |
-
- OpenPyXL 3.0.0 أو أحدث
|
17 |
-
- XlsxWriter 3.0.0 أو أحدث
|
18 |
-
|
19 |
-
## التثبيت
|
20 |
-
|
21 |
-
1. قم بتثبيت Python من الموقع الرسمي: https://www.python.org/downloads/
|
22 |
-
2. قم بتثبيت المكتبات المطلوبة باستخدام الأمر التالي:
|
23 |
-
|
24 |
-
```bash
|
25 |
-
pip install streamlit pandas numpy matplotlib seaborn plotly openpyxl xlsxwriter
|
26 |
-
```
|
27 |
-
|
28 |
-
3. قم بتنزيل ملفات النظام وفك ضغطها في المجلد المطلوب.
|
29 |
-
4. انتقل إلى مجلد النظام واستخدم الأمر التالي لتشغيل النظام:
|
30 |
-
|
31 |
-
```bash
|
32 |
-
streamlit run app.py
|
33 |
-
```
|
34 |
-
|
35 |
-
## هيكل النظام
|
36 |
-
|
37 |
-
يتكون النظام من الوحدات الرئيسية التالية:
|
38 |
-
|
39 |
-
1. **وحدة التسعير (PricingApp)**:
|
40 |
-
- إدارة جداول الكميات (BOQ)
|
41 |
-
- تحليل التكاليف
|
42 |
-
- سيناريوهات التسعير
|
43 |
-
- التحليل التنافسي
|
44 |
-
- التقارير
|
45 |
-
|
46 |
-
2. **وحدة الموارد (ResourcesApp)**:
|
47 |
-
- إدارة المعدات
|
48 |
-
- إدارة المواد
|
49 |
-
- إدارة العمالة
|
50 |
-
- إدارة مقاولي الباطن
|
51 |
-
|
52 |
-
3. **وحدة الكتالوجات**:
|
53 |
-
- كتالوج المعدات
|
54 |
-
- كتالوج المواد
|
55 |
-
- كتالوج العمالة
|
56 |
-
- كتالوج مقاولي الباطن
|
57 |
-
|
58 |
-
4. **وحدة التحليل الذكي للأسعار**:
|
59 |
-
- تحليل تكاليف البنود
|
60 |
-
- تحليل المواد والمعدات والعمالة
|
61 |
-
- تحليل التكاليف غير المباشرة
|
62 |
-
- تحليل هامش الربح
|
63 |
-
|
64 |
-
5. **وحدة الإدارات المساندة**:
|
65 |
-
- إدارة تكاليف الإدارات المساندة
|
66 |
-
- توزيع التكاليف غير المباشرة
|
67 |
-
|
68 |
-
6. **وحدة استراتيجيات التسعير**:
|
69 |
-
- التسعير القياسي
|
70 |
-
- التسعير المتزن
|
71 |
-
- التسعير غير المتزن
|
72 |
-
- التسعير الموجه للربحية
|
73 |
-
- التسعير بالتجميع
|
74 |
-
- التسعير بالمحتوى المحلي
|
75 |
-
|
76 |
-
7. **وحدة تحليل المحتوى المحلي**:
|
77 |
-
- حساب نسبة المحتوى المحلي
|
78 |
-
- تحليل المحتوى المحلي حسب نوع الموارد
|
79 |
-
- توصيات لتحسين نسبة المحتوى المحلي
|
80 |
-
|
81 |
-
## دليل الاستخدام
|
82 |
-
|
83 |
-
### الشاشة الرئيسية
|
84 |
-
|
85 |
-
عند تشغيل النظام، ستظهر الشاشة الرئيسية التي تحتوي على قائمة بالوحدات الرئيسية:
|
86 |
-
|
87 |
-
- التسعير
|
88 |
-
- الموارد
|
89 |
-
- المشاريع
|
90 |
-
- التقارير
|
91 |
-
- الإعدادات
|
92 |
-
|
93 |
-
اختر الوحدة المطلوبة للانتقال إليها.
|
94 |
-
|
95 |
-
### وحدة التسعير
|
96 |
-
|
97 |
-
تتضمن وحدة التسعير العديد من علامات التبويب:
|
98 |
-
|
99 |
-
#### جدول الكميات (BOQ)
|
100 |
-
|
101 |
-
- **استيراد جدول الكميات**: يمكنك استيراد جدول الكميات من ملف Excel.
|
102 |
-
- **إضافة بنود يدويًا**: يمكنك إضافة بنود جديدة يدويًا.
|
103 |
-
- **تحرير البنود**: يمكنك تحرير البنود الموجودة.
|
104 |
-
- **التحليل الذكي للبنود**: يمكنك تحليل كل بند إلى مكوناته (مواد، معدات، عمالة، تكاليف غير مباشرة).
|
105 |
-
|
106 |
-
#### تحليل التكاليف
|
107 |
-
|
108 |
-
- **تحليل التكاليف الإجمالية**: عرض تحليل التكاليف الإجمالية للمشروع.
|
109 |
-
- **تحليل التكاليف حسب البنود**: عرض تحليل التكاليف لكل بند.
|
110 |
-
- **تحليل المواد الأكثر تكلفة**: عرض المواد الأكثر تكلفة في المشروع.
|
111 |
-
- **تحليل المعدات الأكثر تكلفة**: عرض المعدات الأكثر تكلفة في المشروع.
|
112 |
-
|
113 |
-
#### سيناريوهات التسعير
|
114 |
-
|
115 |
-
- **استراتيجيات التسعير**: يمكنك اختيار استراتيجية التسعير المناسبة وتطبيقها.
|
116 |
-
- **مقارنة استراتيجيات التسعير**: يمكنك مقارنة نتائج استراتيجيات التسعير
|
117 |
-
|
118 |
-
#### كتالوجات الموارد
|
119 |
-
|
120 |
-
- **كتالوج المعدات**: إدارة كتالوج المعدات واستيراده من Excel.
|
121 |
-
- **كتالوج المواد**: إدارة كتالوج المواد واستيراده من Excel.
|
122 |
-
- **كتالوج العمالة**: إدارة كتالوج العمالة واستيراده من Excel.
|
123 |
-
- **كتالوج مقاولي الباطن**: إدارة كتالوج مقاولي الباطن واستيراده من Excel.
|
124 |
-
|
125 |
-
#### الإدارات المساندة
|
126 |
-
|
127 |
-
- **إدارة الإدارات المساندة**: إضافة وتحرير الإدارات المساندة.
|
128 |
-
- **تحليل تكاليف الإدارات المساندة**: عرض تحليل تكاليف الإدارات المساندة.
|
129 |
-
- **توزيع التكاليف**: توزيع تكاليف الإدارات المساندة على بنود المشروع.
|
130 |
-
|
131 |
-
#### المحتوى المحلي
|
132 |
-
|
133 |
-
- **تحليل المحتوى المحلي**: عرض تحليل المحتوى المحلي للمشروع.
|
134 |
-
- **تحليل المحتوى المحلي حسب نوع الموارد**: عرض تحليل المحتوى المحلي حسب نوع الموارد.
|
135 |
-
- **توصيات لتحسين نسبة المحتوى المحلي**: عرض توصيات لتحسين نسبة المحتوى المحلي.
|
136 |
-
|
137 |
-
### وحدة الموارد
|
138 |
-
|
139 |
-
تتضمن وحدة الموارد إدارة الموارد المختلفة:
|
140 |
-
|
141 |
-
- **إدارة المعدات**: إضافة وتحرير المعدات.
|
142 |
-
- **إدارة المواد**: إضافة وتحرير المواد.
|
143 |
-
- **إدارة العمالة**: إضافة وتحرير العمالة.
|
144 |
-
- **إدارة مقاولي الباطن**: إضافة وتحرير مقاولي الباطن.
|
145 |
-
|
146 |
-
### وحدة المشاريع
|
147 |
-
|
148 |
-
تتضمن وحدة المشاريع إدارة المشاريع:
|
149 |
-
|
150 |
-
- **قائمة المشاريع**: عرض قائمة المشاريع.
|
151 |
-
- **إضافة مشروع جديد**: إضافة مشروع جديد.
|
152 |
-
- **تفاصيل المشروع**: عرض تفاصيل المشروع.
|
153 |
-
|
154 |
-
### وحدة التقارير
|
155 |
-
|
156 |
-
تتضمن وحدة التقارير إنشاء وعرض التقارير المختلفة:
|
157 |
-
|
158 |
-
- **تقرير ملخص المشروع**: إنشاء تقرير ملخص للمشروع.
|
159 |
-
- **تقرير تحليل التكاليف**: إنشاء تقرير تحليل التكاليف.
|
160 |
-
- **تقرير سيناريوهات التسعير**: إنشاء تقرير سيناريوهات التسعير.
|
161 |
-
- **تقرير المحتوى المحلي**: إنشاء تقرير المحتوى المحلي.
|
162 |
-
- **تقرير الإدارات المساندة**: إنشاء تقرير الإدارات المساندة.
|
163 |
-
- **تقرير المقارنة التنافسية**: إنشاء تقرير المقارنة التنافسية.
|
164 |
-
- **تقرير الموارد المستخدمة**: إنشاء تقرير الموارد المستخدمة.
|
165 |
-
|
166 |
-
### وحدة الإعدادات
|
167 |
-
|
168 |
-
تتضمن وحدة الإعدادات تخصيص إعدادات النظام:
|
169 |
-
|
170 |
-
- **إعدادات عامة**: تخصيص الإعدادات
|
171 |
-
- **إعدادات المستخدم**: تخصيص إعدادات المستخدم.
|
172 |
-
- **إعدادات النظام**: تخصيص إعدادات النظام.
|
173 |
-
- **النسخ الاحتياطي**: إنشاء واستعادة النسخ الاحتياطية.
|
174 |
-
|
175 |
-
## الميزات الرئيسية
|
176 |
-
|
177 |
-
### 1. إدارة البنود (BOQ)
|
178 |
-
|
179 |
-
- استيراد جداول الكميات من Excel
|
180 |
-
- إدخال البنود يدويًا وتحريرها
|
181 |
-
- تحليل كل بند إلى مكوناته
|
182 |
-
|
183 |
-
### 2. التحليل الذكي للأسعار
|
184 |
-
|
185 |
-
- تحليل تفصيلي لتكاليف المواد والمعدات والعمالة
|
186 |
-
- حساب المصاريف غير المباشرة وهامش الربح
|
187 |
-
- تحليل التكاليف الإجمالية للمشروع
|
188 |
-
|
189 |
-
### 3. كتالوجات الموارد
|
190 |
-
|
191 |
-
- كتالوج شامل للمعدات المستخدمة في مشاريع البنية التحتية والصرف الصحي والطرق والسيول والكباري
|
192 |
-
- كتالوج شامل للمواد المستخدمة في هذه المشاريع مع الأسعار التقريبية للسوق السعودي
|
193 |
-
- كتالوج شامل للعمالة والمهندسين مع الأسعار بالساعة واليوم والأسبوع والشهر
|
194 |
-
- كتالوج شامل لمقاولي الباطن المتخصصين في أعمال اليوتيلتيز والكهرباء وأنظمة ITC وCCTV وأنظمة التحكم وشبكات الري
|
195 |
-
|
196 |
-
### 4. إدارة الإدارات المساندة
|
197 |
-
|
198 |
-
- إدارة تكاليف الإدارات المساندة المختلفة
|
199 |
-
- توزيع التكاليف غير المباشرة على بنود المشروع
|
200 |
-
|
201 |
-
### 5. استراتيجيات التسعير المتقدمة
|
202 |
-
|
203 |
-
- التسعير القياسي: تحديد سعر كل بند بناءً على تكلفته الفعلية مضافاً إليها نسبة ربح
|
204 |
-
- التسعير المتزن: توزيع هامش الربح بشكل متوازن على جميع بنود المشروع مع مراعاة المخاطر
|
205 |
-
- التسعير غير المتزن: زيادة أسعار البنود المبكرة في المشروع وتخفيض أسعار البنود المتأخرة
|
206 |
-
- التسعير الموجه للربحية: زيادة أسعار البنود ذات التكلفة المنخفضة والكميات الكبيرة
|
207 |
-
- التسعير بالتجميع: تجميع البنود المتشابهة وتسعيرها كمجموعة واحدة
|
208 |
-
- التسعير بالمحتوى المحلي: زيادة نسبة المحتوى المحلي في المشروع لتحقيق متطلبات الجهات المالكة
|
209 |
-
|
210 |
-
### 6. تحليل المحتوى المحلي
|
211 |
-
|
212 |
-
- حساب نسبة المحتوى المحلي في المشروع
|
213 |
-
- تحليل المحتوى المحلي حسب نوع الموارد
|
214 |
-
- توصيات لتحسين نسبة المحتوى المحلي
|
215 |
-
|
216 |
-
### 7. التقارير المتقدمة
|
217 |
-
|
218 |
-
- تقارير تفصيلية وإجمالية للمشروع
|
219 |
-
- تقارير تحليل التكاليف
|
220 |
-
- تقارير سيناريوهات التسعير
|
221 |
-
- تقارير المحتوى المحلي
|
222 |
-
- تقارير الإدارات المساندة
|
223 |
-
- تقارير المقارنة التنافسية
|
224 |
-
- تقارير الموارد المستخدمة
|
225 |
-
|
226 |
-
## الدعم الفني
|
227 |
-
|
228 |
-
للحصول على الدعم الفني، يرجى التواصل معنا عبر:
|
229 |
-
|
230 |
-
- البريد الإلكتروني: [email protected]
|
231 |
-
- الهاتف: +966 12 345 6789
|
232 |
-
|
233 |
-
## حقوق الملكية
|
234 |
-
|
235 |
-
جميع حقوق الملكية محفوظة © 2025 نظام تحليل المناقصات وتسعير المشاريع.
|
|
|
1 |
+
# نظام تحليل المناقصات وتسعير المشاريع - دليل التثبيت والاستخدام
|
2 |
+
|
3 |
+
## مقدمة
|
4 |
+
|
5 |
+
نظام تحليل المناقصات وتسعير المشاريع هو نظام متكامل يساعد المهندسين في تسعير المناقصات والمشاريع بطريقة احترافية ودقيقة. يوفر النظام مجموعة شاملة من الأدوات والوظائف التي تغطي جميع جوانب عملية التسعير، بدءًا من إدارة جداول الكميات وحتى تحليل المحتوى المحلي وإصدار التقارير.
|
6 |
+
|
7 |
+
## متطلبات النظام
|
8 |
+
|
9 |
+
- Python 3.8 أو أحدث
|
10 |
+
- Streamlit 1.10.0 أو أحدث
|
11 |
+
- Pandas 1.3.0 أو أحدث
|
12 |
+
- NumPy 1.20.0 أو أحدث
|
13 |
+
- Matplotlib 3.4.0 أو أحدث
|
14 |
+
- Seaborn 0.11.0 أو أحدث
|
15 |
+
- Plotly 5.3.0 أو أحدث
|
16 |
+
- OpenPyXL 3.0.0 أو أحدث
|
17 |
+
- XlsxWriter 3.0.0 أو أحدث
|
18 |
+
|
19 |
+
## التثبيت
|
20 |
+
|
21 |
+
1. قم بتثبيت Python من الموقع الرسمي: https://www.python.org/downloads/
|
22 |
+
2. قم بتثبيت المكتبات المطلوبة باستخدام الأمر التالي:
|
23 |
+
|
24 |
+
```bash
|
25 |
+
pip install streamlit pandas numpy matplotlib seaborn plotly openpyxl xlsxwriter
|
26 |
+
```
|
27 |
+
|
28 |
+
3. قم بتنزيل ملفات النظام وفك ضغطها في المجلد المطلوب.
|
29 |
+
4. انتقل إلى مجلد النظام واستخدم الأمر التالي لتشغيل النظام:
|
30 |
+
|
31 |
+
```bash
|
32 |
+
streamlit run app.py
|
33 |
+
```
|
34 |
+
|
35 |
+
## هيكل النظام
|
36 |
+
|
37 |
+
يتكون النظام من الوحدات الرئيسية التالية:
|
38 |
+
|
39 |
+
1. **وحدة التسعير (PricingApp)**:
|
40 |
+
- إدارة جداول الكميات (BOQ)
|
41 |
+
- تحليل التكاليف
|
42 |
+
- سيناريوهات التسعير
|
43 |
+
- التحليل التنافسي
|
44 |
+
- التقارير
|
45 |
+
|
46 |
+
2. **وحدة الموارد (ResourcesApp)**:
|
47 |
+
- إدارة المعدات
|
48 |
+
- إدارة المواد
|
49 |
+
- إدارة العمالة
|
50 |
+
- إدارة مقاولي الباطن
|
51 |
+
|
52 |
+
3. **وحدة الكتالوجات**:
|
53 |
+
- كتالوج المعدات
|
54 |
+
- كتالوج المواد
|
55 |
+
- كتالوج العمالة
|
56 |
+
- كتالوج مقاولي الباطن
|
57 |
+
|
58 |
+
4. **وحدة التحليل الذكي للأسعار**:
|
59 |
+
- تحليل تكاليف البنود
|
60 |
+
- تحليل المواد والمعدات والعمالة
|
61 |
+
- تحليل التكاليف غير المباشرة
|
62 |
+
- تحليل هامش الربح
|
63 |
+
|
64 |
+
5. **وحدة الإدارات المساندة**:
|
65 |
+
- إدارة تكاليف الإدارات المساندة
|
66 |
+
- توزيع التكاليف غير المباشرة
|
67 |
+
|
68 |
+
6. **وحدة استراتيجيات التسعير**:
|
69 |
+
- التسعير القياسي
|
70 |
+
- التسعير المتزن
|
71 |
+
- التسعير غير المتزن
|
72 |
+
- التسعير الموجه للربحية
|
73 |
+
- التسعير بالتجميع
|
74 |
+
- التسعير بالمحتوى المحلي
|
75 |
+
|
76 |
+
7. **وحدة تحليل المحتوى المحلي**:
|
77 |
+
- حساب نسبة المحتوى المحلي
|
78 |
+
- تحليل المحتوى المحلي حسب نوع الموارد
|
79 |
+
- توصيات لتحسين نسبة المحتوى المحلي
|
80 |
+
|
81 |
+
## دليل الاستخدام
|
82 |
+
|
83 |
+
### الشاشة الرئيسية
|
84 |
+
|
85 |
+
عند تشغيل النظام، ستظهر الشاشة الرئيسية التي تحتوي على قائمة بالوحدات الرئيسية:
|
86 |
+
|
87 |
+
- التسعير
|
88 |
+
- الموارد
|
89 |
+
- المشاريع
|
90 |
+
- التقارير
|
91 |
+
- الإعدادات
|
92 |
+
|
93 |
+
اختر الوحدة المطلوبة للانتقال إليها.
|
94 |
+
|
95 |
+
### وحدة التسعير
|
96 |
+
|
97 |
+
تتضمن وحدة التسعير العديد من علامات التبويب:
|
98 |
+
|
99 |
+
#### جدول الكميات (BOQ)
|
100 |
+
|
101 |
+
- **استيراد جدول الكميات**: يمكنك استيراد جدول الكميات من ملف Excel.
|
102 |
+
- **إضافة بنود يدويًا**: يمكنك إضافة بنود جديدة يدويًا.
|
103 |
+
- **تحرير البنود**: يمكنك تحرير البنود الموجودة.
|
104 |
+
- **التحليل الذكي للبنود**: يمكنك تحليل كل بند إلى مكوناته (مواد، معدات، عمالة، تكاليف غير مباشرة).
|
105 |
+
|
106 |
+
#### تحليل التكاليف
|
107 |
+
|
108 |
+
- **تحليل التكاليف الإجمالية**: عرض تحليل التكاليف الإجمالية للمشروع.
|
109 |
+
- **تحليل التكاليف حسب البنود**: عرض تحليل التكاليف لكل بند.
|
110 |
+
- **تحليل المواد الأكثر تكلفة**: عرض المواد الأكثر تكلفة في المشروع.
|
111 |
+
- **تحليل المعدات الأكثر تكلفة**: عرض المعدات الأكثر تكلفة في المشروع.
|
112 |
+
|
113 |
+
#### سيناريوهات التسعير
|
114 |
+
|
115 |
+
- **استراتيجيات التسعير**: يمكنك اختيار استراتيجية التسعير المناسبة وتطبيقها.
|
116 |
+
- **مقارنة استراتيجيات التسعير**: يمكنك مقارنة نتائج استراتيجيات التسعير المختلفة.
|
117 |
+
|
118 |
+
#### كتالوجات الموارد
|
119 |
+
|
120 |
+
- **كتالوج المعدات**: إدارة كتالوج المعدات واستيراده من Excel.
|
121 |
+
- **كتالوج المواد**: إدارة كتالوج المواد واستيراده من Excel.
|
122 |
+
- **كتالوج العمالة**: إدارة كتالوج العمالة واستيراده من Excel.
|
123 |
+
- **كتالوج مقاولي الباطن**: إدارة كتالوج مقاولي الباطن واستيراده من Excel.
|
124 |
+
|
125 |
+
#### الإدارات المساندة
|
126 |
+
|
127 |
+
- **إدارة الإدارات المساندة**: إضافة وتحرير الإدارات المساندة.
|
128 |
+
- **تحليل تكاليف الإدارات المساندة**: عرض تحليل تكاليف الإدارات المساندة.
|
129 |
+
- **توزيع التكاليف**: توزيع تكاليف الإدارات المساندة على بنود المشروع.
|
130 |
+
|
131 |
+
#### المحتوى المحلي
|
132 |
+
|
133 |
+
- **تحليل المحتوى المحلي**: عرض تحليل المحتوى المحلي للمشروع.
|
134 |
+
- **تحليل المحتوى المحلي حسب نوع الموارد**: عرض تحليل المحتوى المحلي حسب نوع الموارد.
|
135 |
+
- **توصيات لتحسين نسبة المحتوى المحلي**: عرض توصيات لتحسين نسبة المحتوى المحلي.
|
136 |
+
|
137 |
+
### وحدة الموارد
|
138 |
+
|
139 |
+
تتضمن وحدة الموارد إدارة الموارد المختلفة:
|
140 |
+
|
141 |
+
- **إدارة المعدات**: إضافة وتحرير المعدات.
|
142 |
+
- **إدارة المواد**: إضافة وتحرير المواد.
|
143 |
+
- **إدارة العمالة**: إضافة وتحرير العمالة.
|
144 |
+
- **إدارة مقاولي الباطن**: إضافة وتحرير مقاولي الباطن.
|
145 |
+
|
146 |
+
### وحدة المشاريع
|
147 |
+
|
148 |
+
تتضمن وحدة المشاريع إدارة المشاريع:
|
149 |
+
|
150 |
+
- **قائمة المشاريع**: عرض قائمة المشاريع.
|
151 |
+
- **إضافة مشروع جديد**: إضافة مشروع جديد.
|
152 |
+
- **تفاصيل المشروع**: عرض تفاصيل المشروع.
|
153 |
+
|
154 |
+
### وحدة التقارير
|
155 |
+
|
156 |
+
تتضمن وحدة التقارير إنشاء وعرض التقارير المختلفة:
|
157 |
+
|
158 |
+
- **تقرير ملخص المشروع**: إنشاء تقرير ملخص للمشروع.
|
159 |
+
- **تقرير تحليل التكاليف**: إنشاء تقرير تحليل التكاليف.
|
160 |
+
- **تقرير سيناريوهات التسعير**: إنشاء تقرير سيناريوهات التسعير.
|
161 |
+
- **تقرير المحتوى المحلي**: إنشاء تقرير المحتوى المحلي.
|
162 |
+
- **تقرير الإدارات المساندة**: إنشاء تقرير الإدارات المساندة.
|
163 |
+
- **تقرير المقارنة التنافسية**: إنشاء تقرير المقارنة التنافسية.
|
164 |
+
- **تقرير الموارد المستخدمة**: إنشاء تقرير الموارد المستخدمة.
|
165 |
+
|
166 |
+
### وحدة الإعدادات
|
167 |
+
|
168 |
+
تتضمن وحدة الإعدادات تخصيص إعدادات النظام:
|
169 |
+
|
170 |
+
- **إعدادات عامة**: تخصيص الإعدادات ال��امة مثل اللغة والسمة.
|
171 |
+
- **إعدادات المستخدم**: تخصيص إعدادات المستخدم.
|
172 |
+
- **إعدادات النظام**: تخصيص إعدادات النظام.
|
173 |
+
- **النسخ الاحتياطي**: إنشاء واستعادة النسخ الاحتياطية.
|
174 |
+
|
175 |
+
## الميزات الرئيسية
|
176 |
+
|
177 |
+
### 1. إدارة البنود (BOQ)
|
178 |
+
|
179 |
+
- استيراد جداول الكميات من Excel
|
180 |
+
- إدخال البنود يدويًا وتحريرها
|
181 |
+
- تحليل كل بند إلى مكوناته
|
182 |
+
|
183 |
+
### 2. التحليل الذكي للأسعار
|
184 |
+
|
185 |
+
- تحليل تفصيلي لتكاليف المواد والمعدات والعمالة
|
186 |
+
- حساب المصاريف غير المباشرة وهامش الربح
|
187 |
+
- تحليل التكاليف الإجمالية للمشروع
|
188 |
+
|
189 |
+
### 3. كتالوجات الموارد
|
190 |
+
|
191 |
+
- كتالوج شامل للمعدات المستخدمة في مشاريع البنية التحتية والصرف الصحي والطرق والسيول والكباري
|
192 |
+
- كتالوج شامل للمواد المستخدمة في هذه المشاريع مع الأسعار التقريبية للسوق السعودي
|
193 |
+
- كتالوج شامل للعمالة والمهندسين مع الأسعار بالساعة واليوم والأسبوع والشهر
|
194 |
+
- كتالوج شامل لمقاولي الباطن المتخصصين في أعمال اليوتيلتيز والكهرباء وأنظمة ITC وCCTV وأنظمة التحكم وشبكات الري
|
195 |
+
|
196 |
+
### 4. إدارة الإدارات المساندة
|
197 |
+
|
198 |
+
- إدارة تكاليف الإدارات المساندة المختلفة
|
199 |
+
- توزيع التكاليف غير المباشرة على بنود المشروع
|
200 |
+
|
201 |
+
### 5. استراتيجيات التسعير المتقدمة
|
202 |
+
|
203 |
+
- التسعير القياسي: تحديد سعر كل بند بناءً على تكلفته الفعلية مضافاً إليها نسبة ربح ثابتة
|
204 |
+
- التسعير المتزن: توزيع هامش الربح بشكل متوازن على جميع بنود المشروع مع مراعاة المخاطر
|
205 |
+
- التسعير غير المتزن: زيادة أسعار البنود المبكرة في المشروع وتخفيض أسعار البنود المتأخرة
|
206 |
+
- التسعير الموجه للربحية: زيادة أسعار البنود ذات التكلفة المنخفضة والكميات الكبيرة
|
207 |
+
- التسعير بالتجميع: تجميع البنود المتشابهة وتسعيرها كمجموعة واحدة
|
208 |
+
- التسعير بالمحتوى المحلي: زيادة نسبة المحتوى المحلي في المشروع لتحقيق متطلبات الجهات المالكة
|
209 |
+
|
210 |
+
### 6. تحليل المحتوى المحلي
|
211 |
+
|
212 |
+
- حساب نسبة المحتوى المحلي في المشروع
|
213 |
+
- تحليل المحتوى المحلي حسب نوع الموارد
|
214 |
+
- توصيات لتحسين نسبة المحتوى المحلي
|
215 |
+
|
216 |
+
### 7. التقارير المتقدمة
|
217 |
+
|
218 |
+
- تقارير تفصيلية وإجمالية للمشروع
|
219 |
+
- تقارير تحليل التكاليف
|
220 |
+
- تقارير سيناريوهات التسعير
|
221 |
+
- تقارير المحتوى المحلي
|
222 |
+
- تقارير الإدارات المساندة
|
223 |
+
- تقارير المقارنة التنافسية
|
224 |
+
- تقارير الموارد المستخدمة
|
225 |
+
|
226 |
+
## الدعم الفني
|
227 |
+
|
228 |
+
للحصول على الدعم الفني، يرجى التواصل معنا عبر:
|
229 |
+
|
230 |
+
- البريد الإلكتروني: [email protected]
|
231 |
+
- الهاتف: +966 12 345 6789
|
232 |
+
|
233 |
+
## حقوق الملكية
|
234 |
+
|
235 |
+
جميع حقوق الملكية محفوظة © 2025 نظام تحليل المناقصات وتسعير المشاريع.
|
pricing_system/integrated_app.py
CHANGED
@@ -1,334 +1,1015 @@
|
|
1 |
import streamlit as st
|
2 |
-
import
|
3 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
-
|
6 |
-
|
|
|
|
|
|
|
7 |
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
10 |
|
11 |
-
# استيراد الوحدات الأصلية
|
12 |
-
from modules.pricing.pricing_app import PricingApp
|
13 |
-
from modules.resources.resources_app import ResourcesApp
|
14 |
|
15 |
class IntegratedApp:
|
16 |
-
"""
|
17 |
-
التطبيق المتكامل الذي يجمع بين النظام القديم والنظام الجديد
|
18 |
-
"""
|
19 |
-
|
20 |
def __init__(self):
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
def run(self):
|
41 |
-
"""
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
self.
|
71 |
-
elif st.session_state.
|
72 |
-
self.
|
73 |
-
elif st.session_state.
|
74 |
-
self.
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
st.
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
]
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
with col1:
|
98 |
-
st.
|
99 |
with col2:
|
100 |
-
st.
|
101 |
with col3:
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
col1, col2 = st.columns(2)
|
|
|
122 |
with col1:
|
123 |
-
|
|
|
|
|
|
|
124 |
with col2:
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
if submit_button:
|
132 |
-
if project_name and project_client:
|
133 |
-
st.success(f"تمت إضافة مشروع {project_name} بنجاح")
|
134 |
else:
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
"
|
149 |
-
|
150 |
-
"
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
col1, col2 = st.columns(2)
|
172 |
-
|
173 |
with col1:
|
174 |
-
|
175 |
-
|
176 |
-
|
|
|
177 |
with col2:
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
)
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
if st.
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
from pricing_system.modules.stages.project_entry import render_project_entry
|
4 |
+
from pricing_system.modules.pricing_strategies import balanced_pricing
|
5 |
+
from pricing_system.modules.indirect_support import overheads
|
6 |
+
from pricing_system.modules.analysis.smart_price_analysis import SmartPriceAnalysis
|
7 |
+
from pricing_system.modules.analysis.market_analysis import MarketAnalysis
|
8 |
+
|
9 |
+
import openpyxl
|
10 |
|
11 |
+
from pricing_system.modules.risk_analysis.risk_analyzer import RiskAnalyzer
|
12 |
+
from pricing_system.modules.reference_guides.pricing_guidelines import PricingGuidelines
|
13 |
+
import os
|
14 |
+
from datetime import datetime
|
15 |
+
import pdfkit
|
16 |
|
17 |
+
class ReferenceGuides:
|
18 |
+
def render(self):
|
19 |
+
st.title("المراجع والأدلة")
|
20 |
+
st.markdown("## دليل تحليل أسعار بنود الإنشاءات")
|
21 |
+
st.markdown("**المرجع الأول:** [رابط للمرجع الأول](link_to_reference_1)")
|
22 |
+
st.markdown("**المرجع الثاني:** [رابط للمرجع الثاني](link_to_reference_2)")
|
23 |
|
|
|
|
|
|
|
24 |
|
25 |
class IntegratedApp:
|
|
|
|
|
|
|
|
|
26 |
def __init__(self):
|
27 |
+
from config_manager import ConfigManager
|
28 |
+
config_manager = ConfigManager()
|
29 |
+
config_manager.set_page_config_if_needed(
|
30 |
+
page_title="نظام التسعير المتكامل",
|
31 |
+
page_icon="💰",
|
32 |
+
layout="wide",
|
33 |
+
initial_sidebar_state="expanded"
|
34 |
+
)
|
35 |
+
|
36 |
+
if 'pricing_stage' not in st.session_state:
|
37 |
+
st.session_state.pricing_stage = 1
|
38 |
+
|
39 |
+
if 'current_project' not in st.session_state:
|
40 |
+
st.session_state.current_project = {
|
41 |
+
'name': '',
|
42 |
+
'code': '',
|
43 |
+
'boq_items': [],
|
44 |
+
'indirect_costs': {
|
45 |
+
'overhead': 0.15,
|
46 |
+
'profit': 0.10,
|
47 |
+
'risk': 0.05
|
48 |
+
}
|
49 |
+
}
|
50 |
+
elif 'boq_items' not in st.session_state.current_project:
|
51 |
+
st.session_state.current_project['boq_items'] = []
|
52 |
+
|
53 |
+
self.smart_analysis = SmartPriceAnalysis()
|
54 |
+
self.market_analysis = MarketAnalysis()
|
55 |
+
self.risk_analyzer = RiskAnalyzer()
|
56 |
+
self.reference_guides = ReferenceGuides()
|
57 |
+
|
58 |
def run(self):
|
59 |
+
st.markdown("""
|
60 |
+
<style>
|
61 |
+
.main-title {
|
62 |
+
color: #1f77b4;
|
63 |
+
font-size: 2rem;
|
64 |
+
text-align: center;
|
65 |
+
margin-bottom: 2rem;
|
66 |
+
}
|
67 |
+
.sidebar-title {
|
68 |
+
font-size: 1.2rem;
|
69 |
+
font-weight: bold;
|
70 |
+
margin-bottom: 1rem;
|
71 |
+
}
|
72 |
+
.stage-number {
|
73 |
+
background-color: #1f77b4;
|
74 |
+
color: white;
|
75 |
+
padding: 0.2rem 0.5rem;
|
76 |
+
border-radius: 50%;
|
77 |
+
margin-right: 0.5rem;
|
78 |
+
}
|
79 |
+
</style>
|
80 |
+
""", unsafe_allow_html=True)
|
81 |
+
|
82 |
+
st.markdown('<h1 class="main-title">نظام التسعير المتكامل</h1>', unsafe_allow_html=True)
|
83 |
+
st.sidebar.markdown('<div class="sidebar-title">مراحل التسعير</div>', unsafe_allow_html=True)
|
84 |
+
|
85 |
+
self._render_sidebar_stages()
|
86 |
+
|
87 |
+
if st.session_state.pricing_stage == 1:
|
88 |
+
self._render_project_entry()
|
89 |
+
elif st.session_state.pricing_stage == 2:
|
90 |
+
self._render_boq_items()
|
91 |
+
elif st.session_state.pricing_stage == 3:
|
92 |
+
self._render_price_analysis()
|
93 |
+
elif st.session_state.pricing_stage == 4:
|
94 |
+
self._render_risk_analysis()
|
95 |
+
elif st.session_state.pricing_stage == 5:
|
96 |
+
self._render_pricing_strategies()
|
97 |
+
elif st.session_state.pricing_stage == 6:
|
98 |
+
self._render_local_content()
|
99 |
+
elif st.session_state.pricing_stage == 7:
|
100 |
+
self._render_final_boq()
|
101 |
+
elif st.session_state.pricing_stage == 8:
|
102 |
+
self._render_reference_guides()
|
103 |
+
|
104 |
+
self._render_navigation()
|
105 |
+
|
106 |
+
def _render_sidebar_stages(self):
|
107 |
+
st.sidebar.markdown("### مراحل التسعير")
|
108 |
+
stages = [
|
109 |
+
"بيانات المشروع",
|
110 |
+
"جدول الكميات",
|
111 |
+
"تحليل الأسعار",
|
112 |
+
"تحليل المخاطر",
|
113 |
+
"استراتيجيات التسعير",
|
114 |
+
"المحتوى المحلي",
|
115 |
+
"الجدول النهائي",
|
116 |
+
"المراجع والأدلة"
|
117 |
]
|
118 |
+
for i, stage in enumerate(stages, 1):
|
119 |
+
if st.session_state.pricing_stage > i:
|
120 |
+
status = "✓"
|
121 |
+
elif st.session_state.pricing_stage == i:
|
122 |
+
status = "🔄"
|
123 |
+
else:
|
124 |
+
status = ""
|
125 |
+
st.sidebar.markdown(f"{i}. {stage} {status}")
|
126 |
+
|
127 |
+
def _render_project_entry(self):
|
128 |
+
st.title("بيانات المشروع")
|
129 |
+
|
130 |
+
if 'current_project' not in st.session_state:
|
131 |
+
st.session_state.current_project = {}
|
132 |
+
st.session_state.show_entry_form = True
|
133 |
+
|
134 |
+
if st.session_state.get('show_entry_form', True):
|
135 |
+
st.subheader("إدخال بيانات المشروع")
|
136 |
+
with st.form("project_entry_form"):
|
137 |
+
col1, col2 = st.columns(2)
|
138 |
+
|
139 |
+
with col1:
|
140 |
+
name = st.text_input("اسم المشروع")
|
141 |
+
code = st.text_input("رقم المشروع")
|
142 |
+
location = st.text_input("الموقع")
|
143 |
+
|
144 |
+
with col2:
|
145 |
+
start_date = st.date_input("تاريخ البدء")
|
146 |
+
duration = st.number_input("مدة المشروع (يوم)", min_value=1, value=180)
|
147 |
+
budget = st.number_input("الميزانية (ريال)", min_value=0.0, step=1000.0)
|
148 |
+
|
149 |
+
description = st.text_area("وصف المشروع")
|
150 |
+
|
151 |
+
if st.form_submit_button("حفظ البيانات"):
|
152 |
+
st.session_state.current_project.update({
|
153 |
+
'name': name,
|
154 |
+
'code': code,
|
155 |
+
'location': location,
|
156 |
+
'start_date': start_date,
|
157 |
+
'duration': duration,
|
158 |
+
'budget': budget,
|
159 |
+
'description': description
|
160 |
+
})
|
161 |
+
st.session_state.show_entry_form = False
|
162 |
+
st.success("تم حفظ بيانات المشروع بنجاح!")
|
163 |
+
st.rerun()
|
164 |
+
|
165 |
+
else:
|
166 |
+
st.subheader("بيانات المشروع المحفوظة")
|
167 |
+
col1, col2 = st.columns(2)
|
168 |
+
|
169 |
+
with col1:
|
170 |
+
st.markdown(f"**اسم المشروع:** {st.session_state.current_project.get('name', '')}")
|
171 |
+
st.markdown(f"**رقم المشروع:** {st.session_state.current_project.get('code', '')}")
|
172 |
+
st.markdown(f"**الموقع:** {st.session_state.current_project.get('location', '')}")
|
173 |
+
|
174 |
+
with col2:
|
175 |
+
st.markdown(f"**تاريخ البدء:** {st.session_state.current_project.get('start_date', '')}")
|
176 |
+
st.markdown(f"**مدة المشروع:** {st.session_state.current_project.get('duration', '')} يوم")
|
177 |
+
st.markdown(f"**الميزانية:** {st.session_state.current_project.get('budget', '')} ريال")
|
178 |
+
|
179 |
+
st.markdown(f"**الوصف:** {st.session_state.current_project.get('description', '')}")
|
180 |
+
|
181 |
+
if st.button("تعديل البيانات"):
|
182 |
+
st.session_state.show_entry_form = True
|
183 |
+
st.rerun()
|
184 |
+
|
185 |
+
def _render_boq_items(self):
|
186 |
+
st.title("جدول الكميات")
|
187 |
+
tabs = st.tabs(["إدخال البنود", "استيراد/تصدير جدول الكميات", "تحليل البنود"])
|
188 |
+
|
189 |
+
with tabs[0]:
|
190 |
+
st.subheader("إضافة بند جديد")
|
191 |
+
st.markdown("### إضافة بند جديد")
|
192 |
+
new_item_type = st.selectbox("نوع البند", ["عمالة", "معدات", "مواد"])
|
193 |
+
|
194 |
+
col1, col2, col3 = st.columns(3)
|
195 |
with col1:
|
196 |
+
item_name = st.text_input("اسم البند")
|
197 |
with col2:
|
198 |
+
item_quantity = st.number_input("الكمية", min_value=0.0, step=0.1)
|
199 |
with col3:
|
200 |
+
item_price = st.number_input("السعر", min_value=0.0, step=0.1)
|
201 |
+
|
202 |
+
if st.button("إضافة البند"):
|
203 |
+
new_item = {
|
204 |
+
"type": new_item_type,
|
205 |
+
"name": item_name,
|
206 |
+
"quantity": item_quantity,
|
207 |
+
"price": item_price,
|
208 |
+
"total": item_quantity * item_price
|
209 |
+
}
|
210 |
+
|
211 |
+
if new_item_type == "عمالة":
|
212 |
+
if 'labor' not in st.session_state.current_project:
|
213 |
+
st.session_state.current_project['labor'] = []
|
214 |
+
st.session_state.current_project['labor'].append(new_item)
|
215 |
+
elif new_item_type == "معدات":
|
216 |
+
if 'equipment' not in st.session_state.current_project:
|
217 |
+
st.session_state.current_project['equipment'] = []
|
218 |
+
st.session_state.current_project['equipment'].append(new_item)
|
219 |
+
else:
|
220 |
+
if 'materials' not in st.session_state.current_project:
|
221 |
+
st.session_state.current_project['materials'] = []
|
222 |
+
st.session_state.current_project['materials'].append(new_item)
|
223 |
+
|
224 |
+
st.success(f"تم إضافة {item_name} بنجاح")
|
225 |
+
st.rerun()
|
226 |
+
|
227 |
+
|
228 |
+
with st.form("boq_item_form"):
|
229 |
col1, col2 = st.columns(2)
|
230 |
+
|
231 |
with col1:
|
232 |
+
item_code = st.text_input("كود البند")
|
233 |
+
item_desc = st.text_area("وصف البند")
|
234 |
+
quantity = st.number_input("الكمية", min_value=0.0)
|
235 |
+
|
236 |
with col2:
|
237 |
+
unit = st.selectbox("الوحدة", ["متر مربع", "متر مكعب", "متر طولي", "عدد", "طن", "كجم"])
|
238 |
+
unit_price = st.number_input("سعر الوحدة", min_value=0.0)
|
239 |
+
|
240 |
+
if st.form_submit_button("إضافة البند"):
|
241 |
+
if not item_code or not item_desc or quantity <= 0 or unit_price <= 0:
|
242 |
+
st.error("يرجى إدخال جميع البيانات المطلوبة")
|
|
|
|
|
|
|
243 |
else:
|
244 |
+
new_item = {
|
245 |
+
'code': item_code,
|
246 |
+
'description': item_desc,
|
247 |
+
'unit': unit,
|
248 |
+
'quantity': quantity,
|
249 |
+
'unit_price': unit_price,
|
250 |
+
'total_price': quantity * unit_price
|
251 |
+
}
|
252 |
+
st.session_state.current_project['boq_items'].append(new_item)
|
253 |
+
st.success("تم إضافة البند بنجاح")
|
254 |
+
st.rerun()
|
255 |
+
|
256 |
+
with tabs[1]:
|
257 |
+
st.subheader("استيراد/تصدير جدول الكميات")
|
258 |
+
|
259 |
+
uploaded_file = st.file_uploader("رفع ملف جدول كميات", type=['xlsx', 'xls'], key="boq_upload")
|
260 |
+
if uploaded_file:
|
261 |
+
try:
|
262 |
+
df = pd.read_excel(uploaded_file)
|
263 |
+
st.write("معاينة البيانات:")
|
264 |
+
st.dataframe(df)
|
265 |
+
|
266 |
+
if st.button("تأكيد استيراد البيانات", key="confirm_import"):
|
267 |
+
for _, row in df.iterrows():
|
268 |
+
new_item = {
|
269 |
+
'code': str(row.get('كود البند', '')),
|
270 |
+
'description': str(row.get('وصف البند', '')),
|
271 |
+
'unit': str(row.get('الوحدة', '')),
|
272 |
+
'quantity': float(row.get('الكمية', 0)),
|
273 |
+
'unit_price': float(row.get('سعر الوحدة', 0)),
|
274 |
+
'total_price': float(row.get('السعر الإجمالي', 0))
|
275 |
+
}
|
276 |
+
st.session_state.current_project['boq_items'].append(new_item)
|
277 |
+
st.success("تم استيراد البيانات بنجاح")
|
278 |
+
st.rerun()
|
279 |
+
except Exception as e:
|
280 |
+
st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}")
|
281 |
+
|
282 |
+
st.divider()
|
283 |
+
|
284 |
+
if st.session_state.current_project.get('boq_items'):
|
285 |
+
if st.button("تصدير جدول الكميات الحالي", key="export_current_boq"):
|
286 |
+
try:
|
287 |
+
df = pd.DataFrame(st.session_state.current_project['boq_items'])
|
288 |
+
# Rename columns to Arabic
|
289 |
+
df = df.rename(columns={
|
290 |
+
'code': 'كود البند',
|
291 |
+
'description': 'وصف البند',
|
292 |
+
'unit': 'الوحدة',
|
293 |
+
'quantity': 'الكمية',
|
294 |
+
'unit_price': 'سعر الوحدة',
|
295 |
+
'total_price': 'السعر الإجمالي'
|
296 |
+
})
|
297 |
+
|
298 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
299 |
+
excel_file = f"data/exports/boq_{timestamp}.xlsx"
|
300 |
+
os.makedirs("data/exports", exist_ok=True)
|
301 |
+
df.to_excel(excel_file, index=False)
|
302 |
+
|
303 |
+
with open(excel_file, 'rb') as f:
|
304 |
+
st.download_button(
|
305 |
+
label="تحميل الملف",
|
306 |
+
data=f,
|
307 |
+
file_name=f"boq_{timestamp}.xlsx",
|
308 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
309 |
+
key="download_boq"
|
310 |
+
)
|
311 |
+
except Exception as e:
|
312 |
+
st.error(f"حدث خطأ أثناء تصدير الملف: {str(e)}")
|
313 |
+
|
314 |
+
with tabs[2]:
|
315 |
+
st.subheader("تحليل البنود")
|
316 |
+
if st.session_state.current_project.get('boq_items'):
|
317 |
+
df = pd.DataFrame(st.session_state.current_project['boq_items'])
|
318 |
+
|
319 |
+
categories = ["أعمال ترابية", "أعمال خرسانية", "أعمال حديد", "أعمال بناء", "أعمال تشطيبات"]
|
320 |
+
selected_category = st.selectbox("اختر فئة البند", categories)
|
321 |
+
|
322 |
+
if 'category' in df.columns:
|
323 |
+
category_items = df[df['category'] == selected_category]
|
324 |
+
if not category_items.empty:
|
325 |
+
st.write("### البنود المتاحة في هذه الفئة:")
|
326 |
+
for _, item in category_items.iterrows():
|
327 |
+
st.write(f"- {item['description']}")
|
328 |
+
else:
|
329 |
+
st.info("لا توجد بنود في هذه الفئة")
|
330 |
+
|
331 |
+
total_cost = df['total_price'].sum()
|
332 |
+
st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
|
333 |
+
|
334 |
+
st.subheader("توزيع التكاليف حسب الوحدات")
|
335 |
+
unit_costs = df.groupby('unit')['total_price'].sum()
|
336 |
+
st.bar_chart(unit_costs)
|
337 |
+
|
338 |
+
st.subheader("البنود الأعلى تكلفة")
|
339 |
+
top_items = df.nlargest(5, 'total_price')[['code', 'description', 'total_price']]
|
340 |
+
st.dataframe(top_items)
|
341 |
+
|
342 |
+
st.subheader("تحليل تفصيلي للبند")
|
343 |
+
selected_item = st.selectbox("اختر بند للتحليل", df['code'].tolist())
|
344 |
+
if selected_item:
|
345 |
+
item = df[df['code'] == selected_item].iloc[0]
|
346 |
+
col1, col2 = st.columns(2)
|
347 |
+
with col1:
|
348 |
+
st.write("**الوصف:**", item['description'])
|
349 |
+
st.write("**الكمية:**", item['quantity'])
|
350 |
+
with col2:
|
351 |
+
st.write("**سعر الوحدة:**", f"{item['unit_price']:,.2f} ريال")
|
352 |
+
st.write("**الإجمالي:**", f"{item['total_price']:,.2f} ريال")
|
353 |
+
else:
|
354 |
+
st.warning("لا توجد بنود للتحليل. الرجاء إضافة بنود أولاً.")
|
355 |
+
|
356 |
+
if st.session_state.current_project['boq_items']:
|
357 |
+
st.markdown("### البنود المضافة")
|
358 |
+
|
359 |
+
for idx, item in enumerate(st.session_state.current_project['boq_items']):
|
360 |
+
st.markdown(f"### البند {idx+1}: {item['description'][:50]}...")
|
361 |
+
col1, col2, col3 = st.columns([2,2,1])
|
362 |
+
|
363 |
+
with col1:
|
364 |
+
st.text_input("كود البند", value=item['code'], key=f"code_{idx}")
|
365 |
+
st.text_area("وصف البند", value=item['description'], key=f"desc_{idx}")
|
366 |
+
|
367 |
+
if st.checkbox(f"عرض تحليل مكونات البند {idx+1}", key=f"show_analysis_{idx}"):
|
368 |
+
with st.expander("تحليل مكونات البند"):
|
369 |
+
# المواد
|
370 |
+
st.subheader("المواد")
|
371 |
+
materials_container = st.container()
|
372 |
+
with materials_container:
|
373 |
+
if 'materials' not in st.session_state:
|
374 |
+
st.session_state.materials = []
|
375 |
+
materials = st.session_state.materials
|
376 |
+
|
377 |
+
for i in range(len(materials)):
|
378 |
+
cols = st.columns([3, 2, 2, 1])
|
379 |
+
with cols[0]:
|
380 |
+
materials[i]['name'] = st.selectbox(f"المادة {i+1}", ["اسمنت", "رمل", "حصى", "حديد"], key=f"mat_{idx}_{i}", index = 0 if materials[i]['name'] == "" else ["اسمنت", "رمل", "حصى", "حديد"].index(materials[i]['name']))
|
381 |
+
with cols[1]:
|
382 |
+
unit_col1, unit_col2 = st.columns(2)
|
383 |
+
with unit_col1:
|
384 |
+
materials[i]['unit'] = st.selectbox(
|
385 |
+
"وحدة القياس",
|
386 |
+
["متر مربع", "متر مكعب", "متر طولي", "رول", "بوكس", "كجم", "طن", "عدد", "لتر"],
|
387 |
+
key=f"mat_unit_{idx}_{i}"
|
388 |
+
)
|
389 |
+
with unit_col2:
|
390 |
+
try:
|
391 |
+
current_qty = float(materials[i]['quantity'])
|
392 |
+
except (ValueError, TypeError):
|
393 |
+
current_qty = 0.0
|
394 |
+
materials[i]['quantity'] = st.number_input(
|
395 |
+
"الكمية",
|
396 |
+
min_value=0.0,
|
397 |
+
value=current_qty,
|
398 |
+
key=f"mat_qty_{idx}_{i}"
|
399 |
+
)
|
400 |
+
with cols[2]:
|
401 |
+
try:
|
402 |
+
current_price = float(materials[i]['price'])
|
403 |
+
except (ValueError, TypeError):
|
404 |
+
current_price = 0.0
|
405 |
+
materials[i]['price'] = st.number_input("السعر", min_value=0.0, value=current_price, key=f"mat_price_{idx}_{i}")
|
406 |
+
with cols[3]:
|
407 |
+
materials[i]['total'] = materials[i]['quantity'] * materials[i]['price']
|
408 |
+
st.text(f"{materials[i]['total']:.2f}")
|
409 |
+
if st.button("🗑️ حذف", key=f"delete_mat_{idx}_{i}"):
|
410 |
+
if len(materials) > i:
|
411 |
+
materials.pop(i)
|
412 |
+
st.rerun()
|
413 |
+
|
414 |
+
if st.button("➕ إضافة مادة جديدة", key=f"add_mat_{idx}"):
|
415 |
+
materials.append({
|
416 |
+
"name": "",
|
417 |
+
"quantity": 0,
|
418 |
+
"price": 0,
|
419 |
+
"total": 0
|
420 |
+
})
|
421 |
+
st.rerun()
|
422 |
+
|
423 |
+
# العمالة
|
424 |
+
st.subheader("العمالة")
|
425 |
+
with st.container():
|
426 |
+
labor_container = st.container()
|
427 |
+
with labor_container:
|
428 |
+
if 'labor' not in st.session_state:
|
429 |
+
st.session_state.labor = []
|
430 |
+
labor = st.session_state.labor
|
431 |
+
|
432 |
+
for i in range(len(labor)):
|
433 |
+
cols = st.columns([3, 2, 2, 1])
|
434 |
+
with cols[0]:
|
435 |
+
labor[i]['name'] = st.selectbox(f"العامل {i+1}", ["نجار", "حداد", "عامل", "فني"], key=f"labor_{idx}_{i}")
|
436 |
+
with cols[1]:
|
437 |
+
labor[i]['quantity'] = st.number_input("العدد", min_value=0, value=labor[i].get('quantity', 0), key=f"labor_qty_{idx}_{i}")
|
438 |
+
with cols[2]:
|
439 |
+
current_price = float(labor[i].get('price', 0.0))
|
440 |
+
labor[i]['price'] = st.number_input("الأجر اليومي", min_value=0.0, value=current_price, key=f"labor_price_{idx}_{i}")
|
441 |
+
with cols[3]:
|
442 |
+
labor[i]['total'] = labor[i]['quantity'] * labor[i]['price']
|
443 |
+
st.text(f"{labor[i]['total']:.2f}")
|
444 |
+
if st.button("🗑️ حذف", key=f"delete_labor_{idx}_{i}"):
|
445 |
+
if len(labor) > i:
|
446 |
+
labor.pop(i)
|
447 |
+
st.rerun()
|
448 |
+
|
449 |
+
if st.button("➕ إضافة عامل جديد", key=f"add_labor_{idx}"):
|
450 |
+
labor.append({
|
451 |
+
"name": "",
|
452 |
+
"quantity": 0,
|
453 |
+
"price": 0,
|
454 |
+
"total": 0
|
455 |
+
})
|
456 |
+
st.rerun()
|
457 |
+
|
458 |
+
# المعدات
|
459 |
+
st.subheader("المعدات")
|
460 |
+
equipment_container = st.container()
|
461 |
+
with equipment_container:
|
462 |
+
if 'equipment' not in st.session_state:
|
463 |
+
st.session_state.equipment = []
|
464 |
+
equipment = st.session_state.equipment
|
465 |
+
|
466 |
+
for i in range(len(equipment)):
|
467 |
+
cols = st.columns([3, 2, 2, 1])
|
468 |
+
with cols[0]:
|
469 |
+
equipment[i]['name'] = st.selectbox(f"المعدة {i+1}", ["خلاطة", "هزاز", "ونش", "مضخة"], key=f"equip_{idx}_{i}")
|
470 |
+
with cols[1]:
|
471 |
+
equipment[i]['quantity'] = st.number_input("العدد", min_value=0, value=int(equipment[i].get('quantity', 0)), key=f"equip_qty_{idx}_{i}")
|
472 |
+
with cols[2]:
|
473 |
+
current_price = float(equipment[i].get('price', 0.0))
|
474 |
+
equipment[i]['price'] = st.number_input("السعر اليومي", min_value=0.0, value=current_price, key=f"equip_price_{idx}_{i}")
|
475 |
+
with cols[3]:
|
476 |
+
equipment[i]['total'] = equipment[i]['quantity'] * equipment[i]['price']
|
477 |
+
st.text(f"{equipment[i]['total']:.2f}")
|
478 |
+
if st.button("🗑️ حذف", key=f"delete_equip_{idx}_{i}"):
|
479 |
+
if len(equipment) > i:
|
480 |
+
equipment.pop(i)
|
481 |
+
st.rerun()
|
482 |
+
|
483 |
+
if st.button("➕ إضافة معدة جديدة", key=f"add_equip_{idx}"):
|
484 |
+
equipment.append({
|
485 |
+
"name": "",
|
486 |
+
"quantity": 0,
|
487 |
+
"price": 0,
|
488 |
+
"total": 0
|
489 |
+
})
|
490 |
+
st.rerun()
|
491 |
+
col1, col2 = st.columns(2)
|
492 |
+
with col1:
|
493 |
+
if st.button("➕ إضافة بند جديد", key=f"add_item_{idx}"):
|
494 |
+
if 'materials' not in st.session_state:
|
495 |
+
st.session_state.materials = []
|
496 |
+
st.session_state.materials.append({
|
497 |
+
"name": "",
|
498 |
+
"quantity": 0,
|
499 |
+
"price": 0,
|
500 |
+
"total": 0
|
501 |
+
})
|
502 |
+
st.rerun()
|
503 |
+
with col2:
|
504 |
+
if st.button("❌ حذف البند", key=f"delete_item_{idx}"):
|
505 |
+
if len(st.session_state.materials) > 0:
|
506 |
+
st.session_state.materials.pop()
|
507 |
+
st.rerun()
|
508 |
+
|
509 |
+
total_materials = sum(m["total"] for m in materials)
|
510 |
+
total_labor = sum(l["total"] for l in labor)
|
511 |
+
total_equipment = sum(e["total"] for e in equipment)
|
512 |
+
total_cost = total_materials + total_labor + total_equipment
|
513 |
+
|
514 |
+
st.markdown("---")
|
515 |
+
st.subheader("ملخص التكاليف والحاسبة")
|
516 |
+
|
517 |
+
col1, col2, col3, col4 = st.columns(4)
|
518 |
+
with col1:
|
519 |
+
st.metric("تكلفة المواد", f"{total_materials:.2f}")
|
520 |
+
with col2:
|
521 |
+
st.metric("تكلفة العمالة", f"{total_labor:.2f}")
|
522 |
+
with col3:
|
523 |
+
st.metric("تكلفة المعدات", f"{total_equipment:.2f}")
|
524 |
+
with col4:
|
525 |
+
st.metric("التكلفة الإجمالية", f"{total_cost:.2f}")
|
526 |
+
|
527 |
+
st.markdown("### الحاسبة")
|
528 |
+
calc_col1, calc_col2, calc_col3 = st.columns([2,1,2])
|
529 |
+
|
530 |
+
with calc_col1:
|
531 |
+
num1 = st.number_input("الرقم الأول", value=0.0, format="%.2f")
|
532 |
+
num2 = st.number_input("الرقم الثاني", value=0.0, format="%.2f")
|
533 |
+
|
534 |
+
with calc_col2:
|
535 |
+
operation = st.selectbox("العملية", ['+', '-', '×', '÷'])
|
536 |
+
|
537 |
+
with calc_col3:
|
538 |
+
if operation == '+':
|
539 |
+
result = num1 + num2
|
540 |
+
elif operation == '-':
|
541 |
+
result = num1 - num2
|
542 |
+
elif operation == '×':
|
543 |
+
result = num1 * num2
|
544 |
+
elif operation == '÷':
|
545 |
+
result = num1 / num2 if num2 != 0 else 0
|
546 |
+
|
547 |
+
st.metric("النتيجة", f"{result:.2f}")
|
548 |
+
|
549 |
+
unit_cost = total_cost / quantity if quantity > 0 else 0
|
550 |
+
st.success(f"تكلفة الوحدة: {unit_cost:.2f}")
|
551 |
+
|
552 |
+
if st.button("تطبيق السعر", key=f"apply_price_{idx}"):
|
553 |
+
item['unit_price'] = unit_cost
|
554 |
+
item['total_price'] = unit_cost * quantity
|
555 |
+
st.success("تم تحديث سعر البند")
|
556 |
+
st.rerun()
|
557 |
+
|
558 |
+
with col2:
|
559 |
+
units_list = ["م3", "م2", "متر طولي", "عدد", "متر مربع", "طن", "كجم"]
|
560 |
+
try:
|
561 |
+
default_index = units_list.index(item['unit'])
|
562 |
+
except ValueError:
|
563 |
+
default_index = 0
|
564 |
+
unit = st.selectbox("الوحدة", units_list, key=f"unit_{idx}", index=default_index)
|
565 |
+
quantity = st.number_input("الكمية", value=float(item['quantity']), key=f"quantity_{idx}")
|
566 |
+
|
567 |
+
|
568 |
+
with col3:
|
569 |
+
unit_price = st.number_input("سعر الوحدة", value=float(item['unit_price']), key=f"price_{idx}")
|
570 |
+
if st.button("تحديث البند", key=f"update_{idx}"):
|
571 |
+
st.session_state.current_project['boq_items'][idx].update({
|
572 |
+
'code': st.session_state[f"code_{idx}"],
|
573 |
+
'description': st.session_state[f"desc_{idx}"],
|
574 |
+
'unit': st.session_state[f"unit_{idx}"],
|
575 |
+
'quantity': st.session_state[f"quantity_{idx}"],
|
576 |
+
'unit_price': st.session_state[f"price_{idx}"],
|
577 |
+
'total_price': st.session_state[f"quantity_{idx}"] * st.session_state[f"price_{idx}"]
|
578 |
+
})
|
579 |
+
st.success("تم تحديث البند بنجاح")
|
580 |
+
st.rerun()
|
581 |
+
|
582 |
+
if st.button("حذف البند", key=f"delete_{idx}"):
|
583 |
+
st.session_state.current_project['boq_items'].pop(idx)
|
584 |
+
st.success("تم حذف البند بنجاح")
|
585 |
+
st.rerun()
|
586 |
+
|
587 |
+
df = pd.DataFrame(st.session_state.current_project['boq_items'])
|
588 |
+
column_names = {
|
589 |
+
'code': 'كود البند',
|
590 |
+
'description': 'وصف البند',
|
591 |
+
'unit': 'الوحدة',
|
592 |
+
'quantity': 'الكمية',
|
593 |
+
'unit_price': 'سعر الوحدة',
|
594 |
+
'total_price': 'السعر الإجمالي'
|
595 |
+
}
|
596 |
+
df = df.rename(columns=column_names)
|
597 |
+
st.markdown("### ملخص البنود")
|
598 |
+
st.dataframe(df, use_container_width=True)
|
599 |
+
|
600 |
+
def _render_price_analysis(self):
|
601 |
+
st.title("تحليل الأسعار")
|
602 |
+
tabs = st.tabs(["تحليل البنود", "تحليل التكاليف", "تحليل السوق", "التحليل الذكي"])
|
603 |
+
|
604 |
+
with tabs[0]:
|
605 |
+
if 'current_project' in st.session_state and 'boq_items' in st.session_state.current_project:
|
606 |
+
df = pd.DataFrame(st.session_state.current_project['boq_items'])
|
607 |
+
if not df.empty:
|
608 |
+
# تغيير أسماء الأعمدة إلى العربية
|
609 |
+
df = df.rename(columns={
|
610 |
+
'code': 'كود البند',
|
611 |
+
'description': 'وصف البند',
|
612 |
+
'unit': 'الوحدة',
|
613 |
+
'quantity': 'الكمية',
|
614 |
+
'unit_price': 'سعر الوحدة',
|
615 |
+
'total_price': 'السعر الإجمالي'
|
616 |
+
})
|
617 |
+
st.dataframe(df)
|
618 |
+
|
619 |
+
selected_item = st.selectbox("اختر بند للتحليل", df['كود البند'].tolist())
|
620 |
+
if selected_item:
|
621 |
+
item = df[df['كود البند'] == selected_item].iloc[0]
|
622 |
+
st.write("### تفاصيل البند")
|
623 |
+
st.write(f"الوصف: {item['وصف البند']}")
|
624 |
+
st.write(f"الكمية: {item['الكمية']}")
|
625 |
+
st.write(f"سعر الوحدة: {item['سعر الوحدة']}")
|
626 |
+
st.write(f"الإجمالي: {item['السعر الإجمالي']}")
|
627 |
+
else:
|
628 |
+
st.warning("لا توجد بنود مضافة بعد")
|
629 |
+
else:
|
630 |
+
st.warning("الرجاء إضافة بنود للمشروع أولاً")
|
631 |
+
|
632 |
+
with tabs[1]:
|
633 |
+
self._render_cost_analysis()
|
634 |
+
with tabs[2]:
|
635 |
+
self.market_analysis.render()
|
636 |
+
with tabs[3]:
|
637 |
+
self.smart_analysis.render()
|
638 |
+
|
639 |
+
def _render_cost_analysis(self):
|
640 |
+
if not st.session_state.current_project['boq_items']:
|
641 |
+
st.warning("لا توجد بنود لتحليلها")
|
642 |
+
return
|
643 |
+
|
644 |
+
total_direct_cost = sum(item['total_price'] for item in st.session_state.current_project['boq_items'])
|
645 |
+
indirect_costs = st.session_state.current_project['indirect_costs']
|
646 |
+
|
647 |
+
st.markdown("### ملخص التكاليف")
|
648 |
col1, col2 = st.columns(2)
|
649 |
+
|
650 |
with col1:
|
651 |
+
st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.2f} ريال")
|
652 |
+
st.metric("المصاريف العامة", f"{total_direct_cost * indirect_costs['overhead']:,.2f} ريال")
|
653 |
+
st.metric("هامش الربح", f"{total_direct_cost * indirect_costs['profit']:,.2f} ريال")
|
654 |
+
|
655 |
with col2:
|
656 |
+
st.metric("احتياطي المخاطر", f"{total_direct_cost * indirect_costs['risk']:,.2f} ريال")
|
657 |
+
total_cost = total_direct_cost * (1 + sum(indirect_costs.values()))
|
658 |
+
st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال")
|
659 |
+
|
660 |
+
def _render_risk_analysis(self):
|
661 |
+
st.title("تحليل المخاطر")
|
662 |
+
self.risk_analyzer.render()
|
663 |
+
|
664 |
+
def _render_pricing_strategies(self):
|
665 |
+
st.title("استراتيجيات التسعير")
|
666 |
+
balanced_pricing.render_balanced_strategy()
|
667 |
+
|
668 |
+
def _render_local_content(self):
|
669 |
+
st.title("المحتوى المحلي")
|
670 |
+
st.subheader("نسب المكونات المحلية")
|
671 |
+
|
672 |
+
col1, col2 = st.columns(2)
|
673 |
+
with col1:
|
674 |
+
materials_percentage = st.number_input("نسبة المواد المحلية (%)", 0, 100, 40)
|
675 |
+
equipment_percentage = st.number_input("نسبة المعدات المحلية (%)", 0, 100, 30)
|
676 |
+
|
677 |
+
with col2:
|
678 |
+
labor_percentage = st.number_input("نسبة العمالة المحلية (%)", 0, 100, 80)
|
679 |
+
subcontractors_percentage = st.number_input("نسبة المقاولين المحليين (%)", 0, 100, 50)
|
680 |
+
|
681 |
+
total_local_content = (
|
682 |
+
materials_percentage * 0.4 +
|
683 |
+
equipment_percentage * 0.2 +
|
684 |
+
labor_percentage * 0.3 +
|
685 |
+
subcontractors_percentage * 0.1
|
686 |
+
)
|
687 |
+
|
688 |
+
st.markdown("### نتيجة تحليل المحتوى المحلي")
|
689 |
+
st.metric("نسبة المحتوى المحلي الإجمالية", f"{total_local_content:.1f}%")
|
690 |
+
|
691 |
+
if st.checkbox("عرض التحليل التفصيلي"):
|
692 |
+
data = {
|
693 |
+
'المكون': ['المواد', 'المعدات', 'العمالة', 'المقاولين'],
|
694 |
+
'النسبة المحلية': [materials_percentage, equipment_percentage,
|
695 |
+
labor_percentage, subcontractors_percentage]
|
696 |
+
}
|
697 |
+
df = pd.DataFrame(data)
|
698 |
+
st.bar_chart(df.set_index('المكون'))
|
699 |
+
|
700 |
+
def _render_final_boq(self):
|
701 |
+
st.title("جدول الكميات النهائي")
|
702 |
+
|
703 |
+
if not st.session_state.current_project['boq_items']:
|
704 |
+
st.warning("لا توجد بنود في جدول الكميات")
|
705 |
+
return
|
706 |
+
|
707 |
+
# Create fresh DataFrame with numeric values
|
708 |
+
df = pd.DataFrame(st.session_state.current_project['boq_items'])
|
709 |
+
|
710 |
+
# Convert quantity and prices to float
|
711 |
+
df['quantity'] = pd.to_numeric(df['quantity'], errors='coerce')
|
712 |
+
df['unit_price'] = pd.to_numeric(df['unit_price'], errors='coerce')
|
713 |
+
|
714 |
+
# Calculate total price
|
715 |
+
df['total_price'] = df['quantity'] * df['unit_price']
|
716 |
+
|
717 |
+
# Calculate grand total
|
718 |
+
total = df['total_price'].sum()
|
719 |
+
|
720 |
+
# Format numbers for display
|
721 |
+
df['quantity'] = df['quantity'].apply(lambda x: '{:.2f}'.format(x))
|
722 |
+
df['unit_price'] = df['unit_price'].apply(lambda x: '{:.2f}'.format(x))
|
723 |
+
df['total_price'] = df['total_price'].apply(lambda x: '{:.2f}'.format(x))
|
724 |
+
|
725 |
+
# Rename columns to Arabic
|
726 |
+
# Get local content percentage if available
|
727 |
+
local_content = 0
|
728 |
+
if hasattr(st.session_state, 'local_content'):
|
729 |
+
materials_percentage = st.session_state.local_content.get('materials_local', 0.4) * 100
|
730 |
+
equipment_percentage = st.session_state.local_content.get('equipment_local', 0.3) * 100
|
731 |
+
labor_percentage = st.session_state.local_content.get('labor_local', 0.8) * 100
|
732 |
+
subcontractors_percentage = st.session_state.local_content.get('subcontractors_local', 0.5) * 100
|
733 |
+
|
734 |
+
local_content = (
|
735 |
+
materials_percentage * 0.4 +
|
736 |
+
equipment_percentage * 0.2 +
|
737 |
+
labor_percentage * 0.3 +
|
738 |
+
subcontractors_percentage * 0.1
|
739 |
)
|
740 |
+
|
741 |
+
# Add local content column
|
742 |
+
df['نسبة المحتوى المحلي'] = local_content
|
743 |
+
|
744 |
+
df = df.rename(columns={
|
745 |
+
'code': 'الكود',
|
746 |
+
'description': 'الوصف',
|
747 |
+
'unit': 'الوحدة',
|
748 |
+
'quantity': 'الكمية',
|
749 |
+
'unit_price': 'سعر الوحدة',
|
750 |
+
'total_price': 'السعر الإجمالي'
|
751 |
+
})
|
752 |
+
|
753 |
+
# Add total row to display dataframe
|
754 |
+
total_row = pd.DataFrame([{
|
755 |
+
'الكود': 'الإجمالي',
|
756 |
+
'الوصف': '',
|
757 |
+
'الوحدة': '',
|
758 |
+
'الكمية': '',
|
759 |
+
'سعر الوحدة': '',
|
760 |
+
'السعر الإجمالي': f"{total:,.2f}"
|
761 |
+
}])
|
762 |
+
|
763 |
+
# Combine original dataframe with total row
|
764 |
+
df_with_total = pd.concat([df, total_row], ignore_index=True)
|
765 |
+
|
766 |
+
st.dataframe(
|
767 |
+
df_with_total,
|
768 |
+
use_container_width=True,
|
769 |
+
hide_index=True,
|
770 |
+
column_config={
|
771 |
+
"الكود": st.column_config.TextColumn("الكود", width="small", help="كود البند"),
|
772 |
+
"الوصف": st.column_config.TextColumn("الوصف", width="medium", help="وصف البند"),
|
773 |
+
"الوحدة": st.column_config.TextColumn("الوحدة", width="small", help="وحدة القياس"),
|
774 |
+
"الكمية": st.column_config.NumberColumn("الكمية", width="small", help="كمية البند", format="%.2f"),
|
775 |
+
"سعر الوحدة": st.column_config.NumberColumn("سعر الوحدة", width="small", help="سعر الوحدة", format="%.2f"),
|
776 |
+
"السعر الإجمالي": st.column_config.NumberColumn("السعر الإجمالي", width="small", help="السعر الإجمالي", format="%.2f"),
|
777 |
+
"نسبة المحتوى المحلي": st.column_config.NumberColumn("نسبة المحتوى المحلي", width="small", help="نسبة المحتوى المحلي", format="%.1f%%")
|
778 |
+
}
|
779 |
+
)
|
780 |
+
st.metric("إجمالي جدول الكميات", f"{total:,.2f} ريال")
|
781 |
+
|
782 |
+
# Show saved pricing history with export option
|
783 |
+
if st.button("عرض التسعيرات المحفوظة", key="show_saved_pricing"):
|
784 |
+
if 'saved_pricing' in st.session_state and st.session_state.saved_pricing:
|
785 |
+
st.subheader("التسعيرات المحفوظة")
|
786 |
+
|
787 |
+
# Create selection for saved pricing
|
788 |
+
pricing_options = [f"{p['project_name']} - {p['timestamp']}" for p in st.session_state.saved_pricing]
|
789 |
+
selected_pricing = st.selectbox("اختر التسعير", pricing_options, key="pricing_select")
|
790 |
+
|
791 |
+
if selected_pricing:
|
792 |
+
selected_idx = pricing_options.index(selected_pricing)
|
793 |
+
pricing = st.session_state.saved_pricing[selected_idx]
|
794 |
+
|
795 |
+
st.write(f"إجمالي السعر: {pricing['total_price']:,.2f} ريال")
|
796 |
+
st.write(f"نسبة المحتوى المحلي: {pricing['local_content']:.1f}%")
|
797 |
+
df = pd.DataFrame(pricing['items'])
|
798 |
+
st.dataframe(df)
|
799 |
+
|
800 |
+
# Export selected pricing to Excel
|
801 |
+
if st.button("تصدير التسعير المحدد إلى Excel", key="export_selected_pricing"):
|
802 |
+
try:
|
803 |
+
export_path = "data/exports"
|
804 |
+
os.makedirs(export_path, exist_ok=True)
|
805 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
806 |
+
excel_file = f"{export_path}/saved_pricing_{timestamp}.xlsx"
|
807 |
+
|
808 |
+
# Create Excel writer
|
809 |
+
with pd.ExcelWriter(excel_file, engine='openpyxl') as writer:
|
810 |
+
df.to_excel(writer, index=False, sheet_name='التسعير المحفوظ')
|
811 |
+
worksheet = writer.sheets['التسعير المحفوظ']
|
812 |
+
|
813 |
+
# Add summary information
|
814 |
+
worksheet['A1'] = f"اسم المشروع: {pricing['project_name']}"
|
815 |
+
worksheet['A2'] = f"التاريخ: {pricing['timestamp']}"
|
816 |
+
worksheet['A3'] = f"إجمالي السعر: {pricing['total_price']:,.2f} ريال"
|
817 |
+
worksheet['A4'] = f"نسبة المحتوى المحلي: {pricing['local_content']:.1f}%"
|
818 |
+
|
819 |
+
# Provide download button
|
820 |
+
with open(excel_file, 'rb') as f:
|
821 |
+
excel_data = f.read()
|
822 |
+
st.download_button(
|
823 |
+
label="تحميل ملف Excel",
|
824 |
+
data=excel_data,
|
825 |
+
file_name=f"saved_pricing_{timestamp}.xlsx",
|
826 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
827 |
+
)
|
828 |
+
st.success("تم تصدير التسعير المحدد بنجاح!")
|
829 |
+
except Exception as e:
|
830 |
+
st.error(f"حدث خطأ أثناء التصدير: {str(e)}")
|
831 |
+
else:
|
832 |
+
st.info("لا توجد تسعيرات محفوظة")
|
833 |
+
|
834 |
+
col1, col2, col3, col4 = st.columns(4)
|
835 |
+
|
836 |
+
# Add save button
|
837 |
+
with col1:
|
838 |
+
if st.button("💾 حفظ التسعير", key="save_pricing_btn"):
|
839 |
+
try:
|
840 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
841 |
+
pricing_data = {
|
842 |
+
'timestamp': timestamp,
|
843 |
+
'project_name': st.session_state.current_project.get('name', 'مشروع جديد'),
|
844 |
+
'total_price': total,
|
845 |
+
'items': df.to_dict('records'),
|
846 |
+
'local_content': local_content
|
847 |
+
}
|
848 |
+
|
849 |
+
# Save to session state database
|
850 |
+
if 'saved_pricing' not in st.session_state:
|
851 |
+
st.session_state.saved_pricing = []
|
852 |
+
|
853 |
+
# Check for duplicates before saving
|
854 |
+
is_duplicate = False
|
855 |
+
for saved_pricing in st.session_state.saved_pricing:
|
856 |
+
if (saved_pricing['project_name'] == pricing_data['project_name'] and
|
857 |
+
saved_pricing['total_price'] == pricing_data['total_price'] and
|
858 |
+
saved_pricing['timestamp'] == pricing_data['timestamp']):
|
859 |
+
is_duplicate = True
|
860 |
+
break
|
861 |
+
|
862 |
+
if not is_duplicate:
|
863 |
+
st.session_state.saved_pricing.append(pricing_data)
|
864 |
+
st.success("تم حفظ التسعير بنجاح!")
|
865 |
+
else:
|
866 |
+
st.warning("هذا التسعير موجود بالفعل!")
|
867 |
+
except Exception as e:
|
868 |
+
st.error(f"حدث خطأ أثناء الحفظ: {str(e)}")
|
869 |
+
|
870 |
+
# Export button
|
871 |
+
with col2:
|
872 |
+
st.empty() # Placeholder for future functionality
|
873 |
+
|
874 |
+
with col3:
|
875 |
+
if st.button("تصدير إلى Excel"):
|
876 |
+
try:
|
877 |
+
export_path = "data/exports"
|
878 |
+
os.makedirs(export_path, exist_ok=True)
|
879 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
880 |
+
excel_file = f"{export_path}/boq_{timestamp}.xlsx"
|
881 |
+
|
882 |
+
# Add a total row
|
883 |
+
total_row = pd.DataFrame([{
|
884 |
+
'الكود': 'الإجمالي',
|
885 |
+
'الوصف': '',
|
886 |
+
'الوحدة': '',
|
887 |
+
'الكمية': '',
|
888 |
+
'سعر الوحدة': '',
|
889 |
+
'السعر الإجمالي': f"{total:,.2f}"
|
890 |
+
}])
|
891 |
+
|
892 |
+
# Combine original dataframe with total row
|
893 |
+
df_with_total = pd.concat([df, total_row], ignore_index=True)
|
894 |
+
|
895 |
+
# Write to Excel with styling
|
896 |
+
with pd.ExcelWriter(excel_file, engine='openpyxl') as writer:
|
897 |
+
df_with_total.to_excel(writer, index=False, sheet_name='جدول الكميات')
|
898 |
+
worksheet = writer.sheets['جدول الكميات']
|
899 |
+
# Style the total row
|
900 |
+
for col in range(1, worksheet.max_column + 1):
|
901 |
+
cell = worksheet.cell(row=len(df_with_total), column=col)
|
902 |
+
cell.font = openpyxl.styles.Font(bold=True)
|
903 |
+
|
904 |
+
with open(excel_file, 'rb') as f:
|
905 |
+
excel_data = f.read()
|
906 |
+
st.download_button(
|
907 |
+
label="تحميل ملف Excel",
|
908 |
+
data=excel_data,
|
909 |
+
file_name=f"boq_{timestamp}.xlsx",
|
910 |
+
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
911 |
+
)
|
912 |
+
st.success("تم تصدير الملف بنجاح!")
|
913 |
+
except Exception as e:
|
914 |
+
st.error(f"حدث خطأ أثناء التصدير: {str(e)}")
|
915 |
+
|
916 |
+
with col2:
|
917 |
+
if st.button("تصدير إلى PDF"):
|
918 |
+
try:
|
919 |
+
export_path = "data/exports"
|
920 |
+
os.makedirs(export_path, exist_ok=True)
|
921 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
922 |
+
pdf_file = f"{export_path}/boq_{timestamp}.pdf"
|
923 |
+
|
924 |
+
# Create HTML with Arabic support and styling
|
925 |
+
html = f"""
|
926 |
+
<html dir="rtl">
|
927 |
+
<head><meta charset="UTF-8">
|
928 |
+
<style>
|
929 |
+
body {{ font-family: Arial, sans-serif; }}
|
930 |
+
table {{ border-collapse: collapse; width: 100%; direction: rtl; }}
|
931 |
+
th, td {{ border: 1px solid black; padding: 8px; text-align: center; }}
|
932 |
+
th {{ background-color: #f2f2f2; }}
|
933 |
+
</style>
|
934 |
+
</head>
|
935 |
+
<body>
|
936 |
+
<h2>جدول الكميات</h2>
|
937 |
+
{df.to_html(index=False)}
|
938 |
+
<p>إجمالي جدول الكميات: {total:,.2f} ريال</p>
|
939 |
+
</body>
|
940 |
+
</html>
|
941 |
+
"""
|
942 |
+
|
943 |
+
# Configure PDF options
|
944 |
+
options = {
|
945 |
+
'page-size': 'A4',
|
946 |
+
'margin-top': '1.0in',
|
947 |
+
'margin-right': '0.75in',
|
948 |
+
'margin-bottom': '1.0in',
|
949 |
+
'margin-left': '0.75in',
|
950 |
+
'encoding': 'UTF-8',
|
951 |
+
'enable-local-file-access': None
|
952 |
+
}
|
953 |
+
|
954 |
+
# Generate PDF
|
955 |
+
pdfkit.from_string(html, pdf_file, options=options)
|
956 |
+
|
957 |
+
# Provide download button
|
958 |
+
with open(pdf_file, 'rb') as f:
|
959 |
+
pdf_data = f.read()
|
960 |
+
st.download_button(
|
961 |
+
label="تحميل ملف PDF",
|
962 |
+
data=pdf_data,
|
963 |
+
file_name=f"boq_{timestamp}.pdf",
|
964 |
+
mime="application/pdf"
|
965 |
+
)
|
966 |
+
st.success("تم تصدير الملف بنجاح!")
|
967 |
+
except Exception as e:
|
968 |
+
st.error(f"حدث خطأ أثناء التصدير: {str(e)}")
|
969 |
+
|
970 |
+
|
971 |
+
def _render_navigation(self):
|
972 |
+
can_proceed = self._validate_current_stage()
|
973 |
+
st.markdown("---")
|
974 |
+
st.markdown("""
|
975 |
+
<style>
|
976 |
+
.nav-button {
|
977 |
+
width: 120px;
|
978 |
+
height: 40px;
|
979 |
+
margin: 10px;
|
980 |
+
}
|
981 |
+
</style>
|
982 |
+
""", unsafe_allow_html=True)
|
983 |
+
|
984 |
+
nav_col1, nav_col2, nav_col3 = st.columns([2, 4, 2])
|
985 |
+
|
986 |
+
with nav_col1:
|
987 |
+
if st.session_state.pricing_stage > 1:
|
988 |
+
st.button("⮕ السابق", key="prev_button", help="العودة للمرحلة السابقة", on_click=lambda: setattr(st.session_state, 'pricing_stage', st.session_state.pricing_stage - 1))
|
989 |
+
|
990 |
+
with nav_col2:
|
991 |
+
st.markdown(f"<h4 style='text-align: center;'>المرحلة {st.session_state.pricing_stage} من 8</h4>", unsafe_allow_html=True)
|
992 |
+
|
993 |
+
with nav_col3:
|
994 |
+
if st.session_state.pricing_stage < 8:
|
995 |
+
st.button("التالي ⬅️", key="next_button", help="الانتقال للمرحلة التالية", disabled=not can_proceed, on_click=lambda: setattr(st.session_state, 'pricing_stage', st.session_state.pricing_stage + 1))
|
996 |
+
|
997 |
+
progress = (st.session_state.pricing_stage - 1) / 7
|
998 |
+
st.progress(progress, text=f"اكتمال {int(progress * 100)}% من مراحل التسعير")
|
999 |
+
|
1000 |
+
progress = (st.session_state.pricing_stage - 1) / 7
|
1001 |
+
st.progress(progress, text=f"اكتمال {int(progress * 100)}% من مراحل التسعير")
|
1002 |
+
|
1003 |
+
def _validate_current_stage(self):
|
1004 |
+
if st.session_state.pricing_stage == 1:
|
1005 |
+
return True
|
1006 |
+
elif st.session_state.pricing_stage == 2:
|
1007 |
+
return len(st.session_state.current_project.get('boq_items', [])) > 0
|
1008 |
+
elif st.session_state.pricing_stage == 3:
|
1009 |
+
return True
|
1010 |
+
elif st.session_state.pricing_stage == 4:
|
1011 |
+
return True
|
1012 |
+
return True
|
1013 |
+
|
1014 |
+
def _render_reference_guides(self):
|
1015 |
+
self.reference_guides.render()
|
pricing_system/integration_framework.py
CHANGED
@@ -17,7 +17,7 @@ from modules.catalogs.materials_catalog import MaterialsCatalog
|
|
17 |
from modules.catalogs.labor_catalog import LaborCatalog
|
18 |
from modules.catalogs.subcontractors_catalog import SubcontractorsCatalog
|
19 |
from modules.analysis.smart_price_analysis import SmartPriceAnalysis
|
20 |
-
from modules.indirect_support.
|
21 |
from modules.pricing_strategies.pricing_strategies import PricingStrategies
|
22 |
|
23 |
class IntegrationFramework:
|
@@ -43,7 +43,8 @@ class IntegrationFramework:
|
|
43 |
# تهيئة وحدات التحليل والتسعير
|
44 |
st.session_state.smart_price_analysis = SmartPriceAnalysis()
|
45 |
st.session_state.indirect_support = IndirectSupportManagement()
|
46 |
-
st.session_state.pricing_strategies
|
|
|
47 |
|
48 |
# تهيئة بيانات المشروع
|
49 |
st.session_state.project_data = {
|
@@ -1381,3 +1382,49 @@ class IntegrationFramework:
|
|
1381 |
st.error(f"حدث خطأ أثناء تحليل المحتوى المحلي: {local_content_analysis['message']}")
|
1382 |
else:
|
1383 |
st.warning("لا توجد بنود في المشروع")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
from modules.catalogs.labor_catalog import LaborCatalog
|
18 |
from modules.catalogs.subcontractors_catalog import SubcontractorsCatalog
|
19 |
from modules.analysis.smart_price_analysis import SmartPriceAnalysis
|
20 |
+
from pricing_system.modules.indirect_support.overheads import IndirectSupportManagement
|
21 |
from modules.pricing_strategies.pricing_strategies import PricingStrategies
|
22 |
|
23 |
class IntegrationFramework:
|
|
|
43 |
# تهيئة وحدات التحليل والتسعير
|
44 |
st.session_state.smart_price_analysis = SmartPriceAnalysis()
|
45 |
st.session_state.indirect_support = IndirectSupportManagement()
|
46 |
+
if 'pricing_strategies' not in st.session_state or not isinstance(st.session_state.pricing_strategies, PricingStrategies):
|
47 |
+
st.session_state.pricing_strategies = PricingStrategies()
|
48 |
|
49 |
# تهيئة بيانات المشروع
|
50 |
st.session_state.project_data = {
|
|
|
1382 |
st.error(f"حدث خطأ أثناء تحليل المحتوى المحلي: {local_content_analysis['message']}")
|
1383 |
else:
|
1384 |
st.warning("لا توجد بنود في المشروع")
|
1385 |
+
from pricing_system.modules.analysis.market_analysis import MarketAnalysis
|
1386 |
+
from pricing_system.modules.analysis.smart_price_analysis import SmartPriceAnalysis
|
1387 |
+
from modules.risk_analysis.risk_analyzer import RiskAnalyzer
|
1388 |
+
|
1389 |
+
class IntegratedPricingSystem:
|
1390 |
+
def __init__(self):
|
1391 |
+
self.market_analysis = MarketAnalysis()
|
1392 |
+
self.smart_analysis = SmartPriceAnalysis()
|
1393 |
+
self.risk_analyzer = RiskAnalyzer()
|
1394 |
+
|
1395 |
+
if 'integrated_data' not in st.session_state:
|
1396 |
+
self._initialize_integrated_data()
|
1397 |
+
|
1398 |
+
def _initialize_integrated_data(self):
|
1399 |
+
st.session_state.integrated_data = {
|
1400 |
+
'market_trends': {},
|
1401 |
+
'risk_analysis': {},
|
1402 |
+
'price_analysis': {},
|
1403 |
+
'local_content': {}
|
1404 |
+
}
|
1405 |
+
|
1406 |
+
def render(self):
|
1407 |
+
st.title("نظام التسعير المتكامل")
|
1408 |
+
|
1409 |
+
tabs = st.tabs([
|
1410 |
+
"تحليل السوق",
|
1411 |
+
"تحليل المخاطر",
|
1412 |
+
"التحليل الذكي للأسعار",
|
1413 |
+
"المحتوى المحلي"
|
1414 |
+
])
|
1415 |
+
|
1416 |
+
with tabs[0]:
|
1417 |
+
self.market_analysis.render()
|
1418 |
+
|
1419 |
+
with tabs[1]:
|
1420 |
+
self.risk_analyzer.render_risk_analysis(st.session_state.project_data)
|
1421 |
+
|
1422 |
+
with tabs[2]:
|
1423 |
+
self.smart_analysis.render()
|
1424 |
+
|
1425 |
+
with tabs[3]:
|
1426 |
+
self._render_local_content()
|
1427 |
+
|
1428 |
+
def _render_local_content(self):
|
1429 |
+
st.header("تحليل المحتوى المحلي")
|
1430 |
+
# إضافة تحليل المحتوى المحلي هنا
|
pricing_system/modules/analysis/market_analysis.py
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
وحدة تحليل السوق والأسعار التاريخية
|
3 |
+
"""
|
4 |
+
import streamlit as st
|
5 |
+
import pandas as pd
|
6 |
+
import numpy as np
|
7 |
+
import plotly.express as px
|
8 |
+
from datetime import datetime, timedelta
|
9 |
+
|
10 |
+
class MarketAnalysis:
|
11 |
+
def __init__(self):
|
12 |
+
if 'market_data' not in st.session_state:
|
13 |
+
self._initialize_market_data()
|
14 |
+
|
15 |
+
def _initialize_market_data(self):
|
16 |
+
st.session_state.market_data = {
|
17 |
+
'price_indices': {},
|
18 |
+
'historical_prices': {},
|
19 |
+
'market_trends': {}
|
20 |
+
}
|
21 |
+
|
22 |
+
def render(self):
|
23 |
+
st.header("تحليل السوق والأسعار")
|
24 |
+
|
25 |
+
tabs = st.tabs([
|
26 |
+
"مؤشرات الأسعار",
|
27 |
+
"التحليل التاريخي",
|
28 |
+
"اتجاهات السوق"
|
29 |
+
])
|
30 |
+
|
31 |
+
with tabs[0]:
|
32 |
+
self._render_price_indices()
|
33 |
+
|
34 |
+
with tabs[1]:
|
35 |
+
self._render_historical_analysis()
|
36 |
+
|
37 |
+
with tabs[2]:
|
38 |
+
self._render_market_trends()
|
39 |
+
|
40 |
+
def _render_price_indices(self):
|
41 |
+
st.subheader("مؤشرات الأسعار الرئيسية")
|
42 |
+
|
43 |
+
# عرض مؤشرات المواد الرئيسية
|
44 |
+
materials = {
|
45 |
+
'الحديد': {'current': 3200, 'change': 5.2},
|
46 |
+
'الأسمنت': {'current': 400, 'change': -2.1},
|
47 |
+
'الخرسانة': {'current': 250, 'change': 1.5},
|
48 |
+
'الأسفلت': {'current': 2800, 'change': 3.8}
|
49 |
+
}
|
50 |
+
|
51 |
+
cols = st.columns(4)
|
52 |
+
for i, (material, data) in enumerate(materials.items()):
|
53 |
+
with cols[i]:
|
54 |
+
st.metric(
|
55 |
+
material,
|
56 |
+
f"{data['current']} ريال",
|
57 |
+
f"{data['change']}%"
|
58 |
+
)
|
59 |
+
|
60 |
+
def _render_historical_analysis(self):
|
61 |
+
st.subheader("تحليل الأسعار التاريخي")
|
62 |
+
|
63 |
+
# إنشاء بيانات تاريخية افتراضية
|
64 |
+
dates = pd.date_range(start='2023-01-01', end='2023-12-31', freq='M')
|
65 |
+
materials = ['الحديد', 'الأسمنت', 'الخرسانة', 'الأسفلت']
|
66 |
+
|
67 |
+
data = []
|
68 |
+
for material in materials:
|
69 |
+
base_price = 1000 if material == 'الحديد' else 500
|
70 |
+
for date in dates:
|
71 |
+
data.append({
|
72 |
+
'التاريخ': date,
|
73 |
+
'المادة': material,
|
74 |
+
'السعر': base_price * (1 + 0.1 * np.random.randn())
|
75 |
+
})
|
76 |
+
|
77 |
+
df = pd.DataFrame(data)
|
78 |
+
|
79 |
+
# رسم بياني للأسعار التاريخية
|
80 |
+
fig = px.line(
|
81 |
+
df,
|
82 |
+
x='التاريخ',
|
83 |
+
y='السعر',
|
84 |
+
color='المادة',
|
85 |
+
title='تطور الأسعار خلال العام'
|
86 |
+
)
|
87 |
+
st.plotly_chart(fig)
|
88 |
+
|
89 |
+
def _render_market_trends(self):
|
90 |
+
st.subheader("اتجاهات السوق والتوقعات")
|
91 |
+
|
92 |
+
# تحليل الاتجاهات
|
93 |
+
trends = {
|
94 |
+
'قصير المدى': {
|
95 |
+
'الحديد': 'صعود',
|
96 |
+
'الأسمنت': 'هبوط',
|
97 |
+
'الخرسانة': 'ثبات',
|
98 |
+
'الأسفلت': 'صعود'
|
99 |
+
},
|
100 |
+
'متوسط المدى': {
|
101 |
+
'الحديد': 'ثبات',
|
102 |
+
'الأسمنت': 'صعود',
|
103 |
+
'الخرسانة': 'صعود',
|
104 |
+
'الأسفلت': 'ثبات'
|
105 |
+
},
|
106 |
+
'طويل المدى': {
|
107 |
+
'الحديد': 'صعود',
|
108 |
+
'الأسمنت': 'صعود',
|
109 |
+
'الخرسانة': 'صعود',
|
110 |
+
'الأسفلت': 'صعود'
|
111 |
+
}
|
112 |
+
}
|
113 |
+
|
114 |
+
st.dataframe(pd.DataFrame(trends))
|
pricing_system/modules/analysis/smart_price_analysis.py
CHANGED
@@ -15,23 +15,24 @@ import math
|
|
15 |
|
16 |
class SmartPriceAnalysis:
|
17 |
"""فئة التحليل الذكي للأسعار"""
|
18 |
-
|
19 |
def __init__(self):
|
20 |
"""تهيئة وحدة التحليل الذكي للأسعار"""
|
21 |
-
|
22 |
# تهيئة حالة الجلسة للتحليل الذكي للأسعار
|
23 |
if 'smart_price_analysis' not in st.session_state:
|
24 |
self._initialize_smart_price_analysis()
|
25 |
-
|
26 |
# الوصول إلى كتالوجات الموارد
|
27 |
self.equipment_catalog = self._get_equipment_catalog()
|
28 |
self.materials_catalog = self._get_materials_catalog()
|
29 |
self.labor_catalog = self._get_labor_catalog()
|
30 |
self.subcontractors_catalog = self._get_subcontractors_catalog()
|
31 |
-
|
|
|
32 |
def _initialize_smart_price_analysis(self):
|
33 |
"""تهيئة بيانات التحليل الذكي للأسعار"""
|
34 |
-
|
35 |
# إنشاء بيانات افتراضية للتحليل الذكي للأسعار
|
36 |
st.session_state.smart_price_analysis = {
|
37 |
"price_components": {
|
@@ -64,14 +65,14 @@ class SmartPriceAnalysis:
|
|
64 |
"analysis_history": [], # سجل تحليلات الأسعار
|
65 |
"current_item": None # البند الحالي قيد التحليل
|
66 |
}
|
67 |
-
|
68 |
# إنشاء بيانات افتراضية لبنود جدول الكميات
|
69 |
if 'boq_items' not in st.session_state:
|
70 |
self._initialize_boq_items()
|
71 |
-
|
72 |
def _initialize_boq_items(self):
|
73 |
"""تهيئة بيانات بنود جدول الكميات"""
|
74 |
-
|
75 |
# إنشاء بيانات افتراضية لبنود جدول الكميات
|
76 |
boq_items = [
|
77 |
{
|
@@ -195,75 +196,75 @@ class SmartPriceAnalysis:
|
|
195 |
"components": {}
|
196 |
}
|
197 |
]
|
198 |
-
|
199 |
# تخزين البيانات في حالة الجلسة
|
200 |
st.session_state.boq_items = pd.DataFrame(boq_items)
|
201 |
-
|
202 |
def _get_equipment_catalog(self):
|
203 |
"""الحصول على كتالوج المعدات"""
|
204 |
-
|
205 |
# التحقق من وجود كتالوج المعدات في حالة الجلسة
|
206 |
if 'equipment_catalog' in st.session_state:
|
207 |
return st.session_state.equipment_catalog
|
208 |
-
|
209 |
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
|
210 |
equipment_data = []
|
211 |
-
|
212 |
# تخزين البيانات في حالة الجلسة
|
213 |
st.session_state.equipment_catalog = pd.DataFrame(equipment_data)
|
214 |
-
|
215 |
return st.session_state.equipment_catalog
|
216 |
-
|
217 |
def _get_materials_catalog(self):
|
218 |
"""الحصول على كتالوج المواد"""
|
219 |
-
|
220 |
# التحقق من وجود كتالوج المواد في حالة الجلسة
|
221 |
if 'materials_catalog' in st.session_state:
|
222 |
return st.session_state.materials_catalog
|
223 |
-
|
224 |
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
|
225 |
materials_data = []
|
226 |
-
|
227 |
# تخزين البيانات في حالة الجلسة
|
228 |
st.session_state.materials_catalog = pd.DataFrame(materials_data)
|
229 |
-
|
230 |
return st.session_state.materials_catalog
|
231 |
-
|
232 |
def _get_labor_catalog(self):
|
233 |
"""الحصول على كتالوج العمالة"""
|
234 |
-
|
235 |
# التحقق من وجود كتالوج العمالة في حالة الجلسة
|
236 |
if 'labor_catalog' in st.session_state:
|
237 |
return st.session_state.labor_catalog
|
238 |
-
|
239 |
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
|
240 |
labor_data = []
|
241 |
-
|
242 |
# تخزين البيانات في حالة الجلسة
|
243 |
st.session_state.labor_catalog = pd.DataFrame(labor_data)
|
244 |
-
|
245 |
return st.session_state.labor_catalog
|
246 |
-
|
247 |
def _get_subcontractors_catalog(self):
|
248 |
"""الحصول على كتالوج مقاولي الباطن"""
|
249 |
-
|
250 |
# التحقق من وجود كتالوج مقاولي الباطن في حالة الجلسة
|
251 |
if 'subcontractors_catalog' in st.session_state:
|
252 |
return st.session_state.subcontractors_catalog
|
253 |
-
|
254 |
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
|
255 |
subcontractors_data = []
|
256 |
-
|
257 |
# تخزين البيانات في حالة الجلسة
|
258 |
st.session_state.subcontractors_catalog = pd.DataFrame(subcontractors_data)
|
259 |
-
|
260 |
return st.session_state.subcontractors_catalog
|
261 |
-
|
262 |
def render(self):
|
263 |
"""عرض واجهة التحليل الذكي للأسعار"""
|
264 |
-
|
265 |
st.markdown("## التحليل الذكي للأسعار")
|
266 |
-
|
267 |
# إنشاء تبويبات لعرض التحليل الذكي للأسعار
|
268 |
tabs = st.tabs([
|
269 |
"تحليل البنود",
|
@@ -271,152 +272,152 @@ class SmartPriceAnalysis:
|
|
271 |
"تقارير التحليل",
|
272 |
"المحتوى المحلي"
|
273 |
])
|
274 |
-
|
275 |
with tabs[0]:
|
276 |
self._render_item_analysis_tab()
|
277 |
-
|
278 |
with tabs[1]:
|
279 |
self._render_analysis_settings_tab()
|
280 |
-
|
281 |
with tabs[2]:
|
282 |
self._render_analysis_reports_tab()
|
283 |
-
|
284 |
with tabs[3]:
|
285 |
self._render_local_content_tab()
|
286 |
-
|
287 |
def _render_item_analysis_tab(self):
|
288 |
"""عرض تبويب تحليل البنود"""
|
289 |
-
|
290 |
st.markdown("### تحليل بنود جدول الكميات")
|
291 |
-
|
292 |
# استخراج البيانات
|
293 |
boq_items = st.session_state.boq_items
|
294 |
-
|
295 |
# إنشاء فلاتر للعرض
|
296 |
col1, col2, col3 = st.columns(3)
|
297 |
-
|
298 |
with col1:
|
299 |
# فلتر حسب الفئة
|
300 |
categories = ["الكل"] + sorted(boq_items["category"].unique().tolist())
|
301 |
selected_category = st.selectbox("اختر فئة البند", categories, key="item_analysis_category")
|
302 |
-
|
303 |
with col2:
|
304 |
# فلتر حسب الفئة الفرعية
|
305 |
if selected_category != "الكل":
|
306 |
subcategories = ["الكل"] + sorted(boq_items[boq_items["category"] == selected_category]["subcategory"].unique().tolist())
|
307 |
else:
|
308 |
subcategories = ["الكل"] + sorted(boq_items["subcategory"].unique().tolist())
|
309 |
-
|
310 |
selected_subcategory = st.selectbox("اختر التخصص", subcategories, key="item_analysis_subcategory")
|
311 |
-
|
312 |
with col3:
|
313 |
# فلتر حسب حالة التحليل
|
314 |
analysis_status = ["الكل", "تم التحليل", "لم يتم التحليل"]
|
315 |
selected_status = st.selectbox("اختر حالة التحليل", analysis_status, key="item_analysis_status")
|
316 |
-
|
317 |
# تطبيق الفلاتر
|
318 |
filtered_df = boq_items.copy()
|
319 |
-
|
320 |
if selected_category != "الكل":
|
321 |
filtered_df = filtered_df[filtered_df["category"] == selected_category]
|
322 |
-
|
323 |
if selected_subcategory != "الكل":
|
324 |
filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
|
325 |
-
|
326 |
if selected_status != "الكل":
|
327 |
if selected_status == "تم التحليل":
|
328 |
filtered_df = filtered_df[filtered_df["analyzed"] == True]
|
329 |
else:
|
330 |
filtered_df = filtered_df[filtered_df["analyzed"] == False]
|
331 |
-
|
332 |
# عرض البيانات
|
333 |
if not filtered_df.empty:
|
334 |
# عرض عدد النتائج
|
335 |
st.info(f"تم العثور على {len(filtered_df)} بند")
|
336 |
-
|
337 |
# إنشاء جدول للعرض
|
338 |
display_df = filtered_df[["id", "description", "unit", "quantity", "unit_price", "total_price", "analyzed"]].copy()
|
339 |
display_df.columns = ["الكود", "الوصف", "الوحدة", "الكمية", "سعر الوحدة", "الإجمالي", "تم التحليل"]
|
340 |
display_df["تم التحليل"] = display_df["تم التحليل"].map({True: "✅", False: "❌"})
|
341 |
-
|
342 |
# عرض الجدول
|
343 |
st.dataframe(display_df, use_container_width=True)
|
344 |
-
|
345 |
# اختيار بند للتحليل
|
346 |
st.markdown("#### اختر بند للتحليل")
|
347 |
-
|
348 |
selected_item_id = st.selectbox("اختر كود البند", filtered_df["id"].tolist(), key="item_analysis_selected_id")
|
349 |
-
|
350 |
# استخراج البند المختار
|
351 |
selected_item = filtered_df[filtered_df["id"] == selected_item_id].iloc[0]
|
352 |
-
|
353 |
# عرض تفاصيل البند
|
354 |
st.markdown(f"**البند:** {selected_item['description']}")
|
355 |
st.markdown(f"**الوحدة:** {selected_item['unit']} | **الكمية:** {selected_item['quantity']} | **سعر الوحدة:** {selected_item['unit_price']} ريال | **الإجمالي:** {selected_item['total_price']} ريال")
|
356 |
-
|
357 |
# تحليل البند
|
358 |
st.markdown("#### تحليل البند")
|
359 |
-
|
360 |
# التحقق من حالة التحليل
|
361 |
if selected_item["analyzed"]:
|
362 |
# عرض نتائج التحليل السابق
|
363 |
st.success("تم تحليل هذا البند مسبقاً")
|
364 |
-
|
365 |
# استخراج مكونات البند
|
366 |
components = selected_item["components"]
|
367 |
-
|
368 |
# عرض مكونات البند
|
369 |
self._display_item_components(selected_item)
|
370 |
-
|
371 |
# زر إعادة التحليل
|
372 |
if st.button("إعادة تحليل البند", key="reanalyze_button"):
|
373 |
# تعيين البند الحالي
|
374 |
st.session_state.smart_price_analysis["current_item"] = selected_item.to_dict()
|
375 |
-
|
376 |
# إعادة توجيه إلى صفحة التحليل
|
377 |
-
st.
|
378 |
else:
|
379 |
# تحليل البند لأول مرة
|
380 |
if st.button("تحليل البند", key="analyze_button"):
|
381 |
# تعيين البند الحالي
|
382 |
st.session_state.smart_price_analysis["current_item"] = selected_item.to_dict()
|
383 |
-
|
384 |
# إعادة توجيه إلى صفحة التحليل
|
385 |
-
st.
|
386 |
-
|
387 |
# التحقق من وجود بند حالي قيد التحليل
|
388 |
current_item = st.session_state.smart_price_analysis["current_item"]
|
389 |
-
|
390 |
if current_item and current_item["id"] == selected_item_id:
|
391 |
# عرض نموذج التحليل
|
392 |
self._render_analysis_form(current_item)
|
393 |
else:
|
394 |
st.warning("لا يوجد بنود تطابق معايير البحث")
|
395 |
-
|
396 |
def _render_analysis_form(self, item):
|
397 |
"""عرض نموذج تحليل البند"""
|
398 |
-
|
399 |
st.markdown("### تحليل البند")
|
400 |
st.markdown(f"**البند:** {item['description']}")
|
401 |
st.markdown(f"**الوحدة:** {item['unit']} | **الكمية:** {item['quantity']} | **سعر الوحدة:** {item['unit_price']} ريال | **الإجمالي:** {item['total_price']} ريال")
|
402 |
-
|
403 |
# استخراج نسب المكونات
|
404 |
price_components = st.session_state.smart_price_analysis["price_components"]
|
405 |
-
|
406 |
# حساب قيم المكونات
|
407 |
materials_value = item["unit_price"] * price_components["materials"]
|
408 |
equipment_value = item["unit_price"] * price_components["equipment"]
|
409 |
labor_value = item["unit_price"] * price_components["labor"]
|
410 |
subcontractors_value = item["unit_price"] * price_components["subcontractors"]
|
411 |
-
|
412 |
# إنشاء نموذج التحليل
|
413 |
with st.form("analysis_form"):
|
414 |
st.markdown("#### تحليل سعر الوحدة")
|
415 |
-
|
416 |
# المواد
|
417 |
st.markdown("##### المواد")
|
418 |
materials_col1, materials_col2 = st.columns(2)
|
419 |
-
|
420 |
with materials_col1:
|
421 |
materials_percentage = st.slider(
|
422 |
"نسبة المواد من سعر الوحدة",
|
@@ -427,7 +428,7 @@ class SmartPriceAnalysis:
|
|
427 |
format="%g%%",
|
428 |
key="materials_percentage"
|
429 |
) * 100
|
430 |
-
|
431 |
with materials_col2:
|
432 |
materials_amount = st.number_input(
|
433 |
"قيمة المواد (ريال)",
|
@@ -436,27 +437,27 @@ class SmartPriceAnalysis:
|
|
436 |
step=10.0,
|
437 |
key="materials_amount"
|
438 |
)
|
439 |
-
|
440 |
# إضافة المواد
|
441 |
materials_items = []
|
442 |
-
|
443 |
st.markdown("إضافة المواد")
|
444 |
-
|
445 |
for i in range(3): # السماح بإضافة 3 مواد كحد أقصى
|
446 |
material_col1, material_col2, material_col3, material_col4 = st.columns([3, 1, 1, 1])
|
447 |
-
|
448 |
with material_col1:
|
449 |
material_name = st.text_input(
|
450 |
"اسم المادة",
|
451 |
key=f"material_name_{i}"
|
452 |
)
|
453 |
-
|
454 |
with material_col2:
|
455 |
material_unit = st.text_input(
|
456 |
"الوحدة",
|
457 |
key=f"material_unit_{i}"
|
458 |
)
|
459 |
-
|
460 |
with material_col3:
|
461 |
material_quantity = st.number_input(
|
462 |
"الكمية",
|
@@ -464,7 +465,7 @@ class SmartPriceAnalysis:
|
|
464 |
step=0.1,
|
465 |
key=f"material_quantity_{i}"
|
466 |
)
|
467 |
-
|
468 |
with material_col4:
|
469 |
material_price = st.number_input(
|
470 |
"السعر",
|
@@ -472,7 +473,7 @@ class SmartPriceAnalysis:
|
|
472 |
step=10.0,
|
473 |
key=f"material_price_{i}"
|
474 |
)
|
475 |
-
|
476 |
if material_name and material_unit and material_quantity > 0 and material_price > 0:
|
477 |
materials_items.append({
|
478 |
"name": material_name,
|
@@ -481,11 +482,11 @@ class SmartPriceAnalysis:
|
|
481 |
"price": material_price,
|
482 |
"total": material_quantity * material_price
|
483 |
})
|
484 |
-
|
485 |
# المعدات
|
486 |
st.markdown("##### المعدات")
|
487 |
equipment_col1, equipment_col2 = st.columns(2)
|
488 |
-
|
489 |
with equipment_col1:
|
490 |
equipment_percentage = st.slider(
|
491 |
"نسبة المعدات من سعر الوحدة",
|
@@ -496,7 +497,7 @@ class SmartPriceAnalysis:
|
|
496 |
format="%g%%",
|
497 |
key="equipment_percentage"
|
498 |
) * 100
|
499 |
-
|
500 |
with equipment_col2:
|
501 |
equipment_amount = st.number_input(
|
502 |
"قيمة المعدات (ريال)",
|
@@ -505,27 +506,27 @@ class SmartPriceAnalysis:
|
|
505 |
step=10.0,
|
506 |
key="equipment_amount"
|
507 |
)
|
508 |
-
|
509 |
# إضافة المعدات
|
510 |
equipment_items = []
|
511 |
-
|
512 |
st.markdown("إضافة المعدات")
|
513 |
-
|
514 |
for i in range(3): # السماح بإضافة 3 معدات كحد أقصى
|
515 |
equipment_col1, equipment_col2, equipment_col3, equipment_col4 = st.columns([3, 1, 1, 1])
|
516 |
-
|
517 |
with equipment_col1:
|
518 |
equipment_name = st.text_input(
|
519 |
"اسم المعدة",
|
520 |
key=f"equipment_name_{i}"
|
521 |
)
|
522 |
-
|
523 |
with equipment_col2:
|
524 |
equipment_unit = st.text_input(
|
525 |
"الوحدة",
|
526 |
key=f"equipment_unit_{i}"
|
527 |
)
|
528 |
-
|
529 |
with equipment_col3:
|
530 |
equipment_quantity = st.number_input(
|
531 |
"الكمية",
|
@@ -533,7 +534,7 @@ class SmartPriceAnalysis:
|
|
533 |
step=0.1,
|
534 |
key=f"equipment_quantity_{i}"
|
535 |
)
|
536 |
-
|
537 |
with equipment_col4:
|
538 |
equipment_price = st.number_input(
|
539 |
"السعر",
|
@@ -541,7 +542,7 @@ class SmartPriceAnalysis:
|
|
541 |
step=10.0,
|
542 |
key=f"equipment_price_{i}"
|
543 |
)
|
544 |
-
|
545 |
if equipment_name and equipment_unit and equipment_quantity > 0 and equipment_price > 0:
|
546 |
equipment_items.append({
|
547 |
"name": equipment_name,
|
@@ -550,11 +551,11 @@ class SmartPriceAnalysis:
|
|
550 |
"price": equipment_price,
|
551 |
"total": equipment_quantity * equipment_price
|
552 |
})
|
553 |
-
|
554 |
# العمالة
|
555 |
st.markdown("##### العمالة")
|
556 |
labor_col1, labor_col2 = st.columns(2)
|
557 |
-
|
558 |
with labor_col1:
|
559 |
labor_percentage = st.slider(
|
560 |
"نسبة العمالة من سعر الوحدة",
|
@@ -565,7 +566,7 @@ class SmartPriceAnalysis:
|
|
565 |
format="%g%%",
|
566 |
key="labor_percentage"
|
567 |
) * 100
|
568 |
-
|
569 |
with labor_col2:
|
570 |
labor_amount = st.number_input(
|
571 |
"قيمة العمالة (ريال)",
|
@@ -574,27 +575,27 @@ class SmartPriceAnalysis:
|
|
574 |
step=10.0,
|
575 |
key="labor_amount"
|
576 |
)
|
577 |
-
|
578 |
# إضافة العمالة
|
579 |
labor_items = []
|
580 |
-
|
581 |
st.markdown("إضافة العمالة")
|
582 |
-
|
583 |
for i in range(3): # السماح بإضافة 3 عمال كحد أقصى
|
584 |
labor_col1, labor_col2, labor_col3, labor_col4 = st.columns([3, 1, 1, 1])
|
585 |
-
|
586 |
with labor_col1:
|
587 |
labor_name = st.text_input(
|
588 |
"المسمى الوظيفي",
|
589 |
key=f"labor_name_{i}"
|
590 |
)
|
591 |
-
|
592 |
with labor_col2:
|
593 |
labor_unit = st.text_input(
|
594 |
"الوحدة",
|
595 |
key=f"labor_unit_{i}"
|
596 |
)
|
597 |
-
|
598 |
with labor_col3:
|
599 |
labor_quantity = st.number_input(
|
600 |
"الكمية",
|
@@ -602,7 +603,7 @@ class SmartPriceAnalysis:
|
|
602 |
step=0.1,
|
603 |
key=f"labor_quantity_{i}"
|
604 |
)
|
605 |
-
|
606 |
with labor_col4:
|
607 |
labor_price = st.number_input(
|
608 |
"السعر",
|
@@ -610,7 +611,7 @@ class SmartPriceAnalysis:
|
|
610 |
step=10.0,
|
611 |
key=f"labor_price_{i}"
|
612 |
)
|
613 |
-
|
614 |
if labor_name and labor_unit and labor_quantity > 0 and labor_price > 0:
|
615 |
labor_items.append({
|
616 |
"name": labor_name,
|
@@ -619,11 +620,11 @@ class SmartPriceAnalysis:
|
|
619 |
"price": labor_price,
|
620 |
"total": labor_quantity * labor_price
|
621 |
})
|
622 |
-
|
623 |
# مقاولي الباطن
|
624 |
st.markdown("##### مقاولي الباطن")
|
625 |
subcontractors_col1, subcontractors_col2 = st.columns(2)
|
626 |
-
|
627 |
with subcontractors_col1:
|
628 |
subcontractors_percentage = st.slider(
|
629 |
"نسبة مقاولي الباطن من سعر الوحدة",
|
@@ -634,7 +635,7 @@ class SmartPriceAnalysis:
|
|
634 |
format="%g%%",
|
635 |
key="subcontractors_percentage"
|
636 |
) * 100
|
637 |
-
|
638 |
with subcontractors_col2:
|
639 |
subcontractors_amount = st.number_input(
|
640 |
"قيمة مقاولي الباطن (ريال)",
|
@@ -643,27 +644,27 @@ class SmartPriceAnalysis:
|
|
643 |
step=10.0,
|
644 |
key="subcontractors_amount"
|
645 |
)
|
646 |
-
|
647 |
# إضافة مقاولي الباطن
|
648 |
subcontractors_items = []
|
649 |
-
|
650 |
st.markdown("إضافة مقاولي الباطن")
|
651 |
-
|
652 |
for i in range(2): # السماح بإضافة 2 مقاول باطن كحد أقصى
|
653 |
subcontractor_col1, subcontractor_col2, subcontractor_col3 = st.columns([4, 1, 1])
|
654 |
-
|
655 |
with subcontractor_col1:
|
656 |
subcontractor_name = st.text_input(
|
657 |
"اسم مقاول الباطن",
|
658 |
key=f"subcontractor_name_{i}"
|
659 |
)
|
660 |
-
|
661 |
with subcontractor_col2:
|
662 |
subcontractor_work = st.text_input(
|
663 |
"نوع العمل",
|
664 |
key=f"subcontractor_work_{i}"
|
665 |
)
|
666 |
-
|
667 |
with subcontractor_col3:
|
668 |
subcontractor_price = st.number_input(
|
669 |
"السعر",
|
@@ -671,22 +672,22 @@ class SmartPriceAnalysis:
|
|
671 |
step=10.0,
|
672 |
key=f"subcontractor_price_{i}"
|
673 |
)
|
674 |
-
|
675 |
if subcontractor_name and subcontractor_work and subcontractor_price > 0:
|
676 |
subcontractors_items.append({
|
677 |
"name": subcontractor_name,
|
678 |
"work": subcontractor_work,
|
679 |
"price": subcontractor_price
|
680 |
})
|
681 |
-
|
682 |
# التكاليف غير المباشرة
|
683 |
st.markdown("##### التكاليف غير المباشرة")
|
684 |
-
|
685 |
# استخراج نسب التكاليف غير المباشرة
|
686 |
indirect_costs = st.session_state.smart_price_analysis["indirect_costs"]
|
687 |
-
|
688 |
indirect_col1, indirect_col2, indirect_col3 = st.columns(3)
|
689 |
-
|
690 |
with indirect_col1:
|
691 |
overhead_percentage = st.slider(
|
692 |
"نسبة المصاريف العمومية والإدارية",
|
@@ -697,7 +698,7 @@ class SmartPriceAnalysis:
|
|
697 |
format="%g%%",
|
698 |
key="overhead_percentage"
|
699 |
) * 100
|
700 |
-
|
701 |
with indirect_col2:
|
702 |
profit_percentage = st.slider(
|
703 |
"نسبة الربح",
|
@@ -708,7 +709,7 @@ class SmartPriceAnalysis:
|
|
708 |
format="%g%%",
|
709 |
key="profit_percentage"
|
710 |
) * 100
|
711 |
-
|
712 |
with indirect_col3:
|
713 |
contingency_percentage = st.slider(
|
714 |
"نسبة الطوارئ",
|
@@ -719,14 +720,14 @@ class SmartPriceAnalysis:
|
|
719 |
format="%g%%",
|
720 |
key="contingency_percentage"
|
721 |
) * 100
|
722 |
-
|
723 |
# زر حفظ التحليل
|
724 |
submit_button = st.form_submit_button("حفظ التحليل")
|
725 |
-
|
726 |
if submit_button:
|
727 |
# التحقق من صحة البيانات
|
728 |
total_percentage = (materials_percentage + equipment_percentage + labor_percentage + subcontractors_percentage) / 100
|
729 |
-
|
730 |
if abs(total_percentage - 1.0) > 0.01:
|
731 |
st.error("مجموع نسب المكونات يجب أن يساوي 100%")
|
732 |
else:
|
@@ -735,18 +736,18 @@ class SmartPriceAnalysis:
|
|
735 |
equipment_total = sum([item["total"] for item in equipment_items]) if equipment_items else equipment_amount
|
736 |
labor_total = sum([item["total"] for item in labor_items]) if labor_items else labor_amount
|
737 |
subcontractors_total = sum([item["price"] for item in subcontractors_items]) if subcontractors_items else subcontractors_amount
|
738 |
-
|
739 |
# حساب التكاليف المباشرة
|
740 |
direct_cost = materials_total + equipment_total + labor_total + subcontractors_total
|
741 |
-
|
742 |
# حساب التكاليف غير المباشرة
|
743 |
overhead_amount = direct_cost * (overhead_percentage / 100)
|
744 |
profit_amount = direct_cost * (profit_percentage / 100)
|
745 |
contingency_amount = direct_cost * (contingency_percentage / 100)
|
746 |
-
|
747 |
# حساب إجمالي التكاليف
|
748 |
total_cost = direct_cost + overhead_amount + profit_amount + contingency_amount
|
749 |
-
|
750 |
# إنشاء مكونات البند
|
751 |
components = {
|
752 |
"materials": {
|
@@ -787,17 +788,17 @@ class SmartPriceAnalysis:
|
|
787 |
"total_cost": total_cost,
|
788 |
"analysis_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
789 |
}
|
790 |
-
|
791 |
# تحديث البند في جدول الكميات
|
792 |
boq_items = st.session_state.boq_items
|
793 |
item_index = boq_items[boq_items["id"] == item["id"]].index[0]
|
794 |
-
|
795 |
boq_items.at[item_index, "analyzed"] = True
|
796 |
boq_items.at[item_index, "components"] = components
|
797 |
-
|
798 |
# تحديث حالة الجلسة
|
799 |
st.session_state.boq_items = boq_items
|
800 |
-
|
801 |
# إضافة التحليل إلى سجل التحليلات
|
802 |
analysis_history = st.session_state.smart_price_analysis["analysis_history"]
|
803 |
analysis_history.append({
|
@@ -807,48 +808,48 @@ class SmartPriceAnalysis:
|
|
807 |
"components": components,
|
808 |
"analysis_date": components["analysis_date"]
|
809 |
})
|
810 |
-
|
811 |
st.session_state.smart_price_analysis["analysis_history"] = analysis_history
|
812 |
-
|
813 |
# إعادة تعيين البند الحالي
|
814 |
st.session_state.smart_price_analysis["current_item"] = None
|
815 |
-
|
816 |
# عرض رسالة نجاح
|
817 |
st.success(f"تم تحليل البند {item['id']} بنجاح!")
|
818 |
-
|
819 |
# إعادة توجيه إلى صفحة التحليل
|
820 |
-
st.
|
821 |
-
|
822 |
def _display_item_components(self, item):
|
823 |
"""عرض مكونات البند"""
|
824 |
-
|
825 |
# استخراج مكونات البند
|
826 |
components = item["components"]
|
827 |
-
|
828 |
if not components:
|
829 |
st.warning("لم يتم تحليل هذا البند بعد")
|
830 |
return
|
831 |
-
|
832 |
# عرض ملخص التحليل
|
833 |
st.markdown("#### ملخص التحليل")
|
834 |
-
|
835 |
# عرض تاريخ التحليل
|
836 |
st.markdown(f"**تاريخ التحليل:** {components['analysis_date']}")
|
837 |
-
|
838 |
# عرض التكاليف المباشرة وغير المباشرة
|
839 |
col1, col2 = st.columns(2)
|
840 |
-
|
841 |
with col1:
|
842 |
-
st.markdown(f"
|
843 |
st.markdown(f"**التكاليف غير المباشرة:** {(components['total_cost'] - components['direct_cost']):.2f} ريال")
|
844 |
-
|
845 |
with col2:
|
846 |
st.markdown(f"**إجمالي التكاليف:** {components['total_cost']:.2f} ريال")
|
847 |
st.markdown(f"**سعر الوحدة:** {item['unit_price']:.2f} ريال")
|
848 |
-
|
849 |
# عرض نسب المكونات
|
850 |
st.markdown("#### نسب المكونات")
|
851 |
-
|
852 |
# إنشاء بيانات الرسم البياني
|
853 |
components_data = {
|
854 |
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
|
@@ -865,7 +866,7 @@ class SmartPriceAnalysis:
|
|
865 |
components["subcontractors"]["amount"]
|
866 |
]
|
867 |
}
|
868 |
-
|
869 |
# إنشاء رسم بياني دائري
|
870 |
fig = px.pie(
|
871 |
components_data,
|
@@ -875,82 +876,82 @@ class SmartPriceAnalysis:
|
|
875 |
color="المكون",
|
876 |
hover_data=["القيمة"]
|
877 |
)
|
878 |
-
|
879 |
st.plotly_chart(fig, use_container_width=True)
|
880 |
-
|
881 |
# عرض تفاصيل المكونات
|
882 |
st.markdown("#### تفاصيل المكونات")
|
883 |
-
|
884 |
# إنشاء تبويبات لعرض تفاصيل المكونات
|
885 |
component_tabs = st.tabs(["المواد", "المعدات", "العمالة", "مقاولي الباطن", "التكاليف غير المباشرة"])
|
886 |
-
|
887 |
with component_tabs[0]:
|
888 |
# عرض تفاصيل المواد
|
889 |
st.markdown("##### المواد")
|
890 |
st.markdown(f"**نسبة المواد:** {components['materials']['percentage'] * 100:.2f}%")
|
891 |
st.markdown(f"**قيمة المواد:** {components['materials']['amount']:.2f} ريال")
|
892 |
-
|
893 |
# عرض قائمة المواد
|
894 |
if components["materials"]["items"]:
|
895 |
materials_df = pd.DataFrame(components["materials"]["items"])
|
896 |
materials_df.columns = ["اسم المادة", "الوحدة", "الكمية", "السعر", "الإجمالي"]
|
897 |
-
|
898 |
st.dataframe(materials_df, use_container_width=True)
|
899 |
else:
|
900 |
st.info("لم يتم إضافة مواد محددة")
|
901 |
-
|
902 |
with component_tabs[1]:
|
903 |
# عرض تفاصيل المعدات
|
904 |
st.markdown("##### المعدات")
|
905 |
st.markdown(f"**نسبة المعدات:** {components['equipment']['percentage'] * 100:.2f}%")
|
906 |
st.markdown(f"**قيمة المعدات:** {components['equipment']['amount']:.2f} ريال")
|
907 |
-
|
908 |
# عرض قائمة المعدات
|
909 |
if components["equipment"]["items"]:
|
910 |
equipment_df = pd.DataFrame(components["equipment"]["items"])
|
911 |
equipment_df.columns = ["اسم المعدة", "الوحدة", "الكمية", "السعر", "الإجمالي"]
|
912 |
-
|
913 |
st.dataframe(equipment_df, use_container_width=True)
|
914 |
else:
|
915 |
st.info("لم يتم إضافة معدات محددة")
|
916 |
-
|
917 |
with component_tabs[2]:
|
918 |
# عرض تفاصيل العمالة
|
919 |
st.markdown("##### العمالة")
|
920 |
st.markdown(f"**نسبة العمالة:** {components['labor']['percentage'] * 100:.2f}%")
|
921 |
st.markdown(f"**قيمة العمالة:** {components['labor']['amount']:.2f} ريال")
|
922 |
-
|
923 |
# عرض قائمة العمالة
|
924 |
if components["labor"]["items"]:
|
925 |
labor_df = pd.DataFrame(components["labor"]["items"])
|
926 |
labor_df.columns = ["المسمى الوظيفي", "الوحدة", "الكمية", "السعر", "الإجمالي"]
|
927 |
-
|
928 |
st.dataframe(labor_df, use_container_width=True)
|
929 |
else:
|
930 |
st.info("لم يتم إضافة عمالة محددة")
|
931 |
-
|
932 |
with component_tabs[3]:
|
933 |
# عرض تفاصيل مقاولي الباطن
|
934 |
st.markdown("##### مقاولي الباطن")
|
935 |
st.markdown(f"**نسبة مقاولي الباطن:** {components['subcontractors']['percentage'] * 100:.2f}%")
|
936 |
st.markdown(f"**قيمة مقاولي الباطن:** {components['subcontractors']['amount']:.2f} ريال")
|
937 |
-
|
938 |
# عرض قائمة مقاولي الباطن
|
939 |
if components["subcontractors"]["items"]:
|
940 |
subcontractors_df = pd.DataFrame(components["subcontractors"]["items"])
|
941 |
subcontractors_df.columns = ["اسم مقاول الباطن", "نوع العمل", "السعر"]
|
942 |
-
|
943 |
st.dataframe(subcontractors_df, use_container_width=True)
|
944 |
else:
|
945 |
st.info("لم يتم إضافة مقاولي باطن محددين")
|
946 |
-
|
947 |
with component_tabs[4]:
|
948 |
# عرض تفاصيل التكاليف غير المباشرة
|
949 |
st.markdown("##### التكاليف غير المباشرة")
|
950 |
-
|
951 |
# إنشاء بيانات التكاليف غير المباشرة
|
952 |
indirect_costs = components["indirect_costs"]
|
953 |
-
|
954 |
indirect_data = {
|
955 |
"البند": ["المصاريف العمومية والإدارية", "الربح", "الطوارئ"],
|
956 |
"النسبة": [
|
@@ -964,12 +965,12 @@ class SmartPriceAnalysis:
|
|
964 |
indirect_costs["contingency"]["amount"]
|
965 |
]
|
966 |
}
|
967 |
-
|
968 |
# إنشاء جدول للعرض
|
969 |
indirect_df = pd.DataFrame(indirect_data)
|
970 |
-
|
971 |
st.dataframe(indirect_df, use_container_width=True)
|
972 |
-
|
973 |
# إنشاء رسم بياني للتكاليف غير المباشرة
|
974 |
fig = px.bar(
|
975 |
indirect_df,
|
@@ -979,26 +980,26 @@ class SmartPriceAnalysis:
|
|
979 |
color="البند",
|
980 |
text_auto=True
|
981 |
)
|
982 |
-
|
983 |
st.plotly_chart(fig, use_container_width=True)
|
984 |
-
|
985 |
def _render_analysis_settings_tab(self):
|
986 |
"""عرض تبويب إعدادات التحليل"""
|
987 |
-
|
988 |
st.markdown("### إعدادات التحليل الذكي للأسعار")
|
989 |
-
|
990 |
# استخراج إعدادات التحليل
|
991 |
price_components = st.session_state.smart_price_analysis["price_components"]
|
992 |
indirect_costs = st.session_state.smart_price_analysis["indirect_costs"]
|
993 |
productivity_factors = st.session_state.smart_price_analysis["productivity_factors"]
|
994 |
-
|
995 |
# إنشاء نموذج إعدادات التحليل
|
996 |
with st.form("analysis_settings_form"):
|
997 |
st.markdown("#### نسب مكونات السعر الافتراضية")
|
998 |
-
|
999 |
# نسب مكونات السعر
|
1000 |
components_col1, components_col2 = st.columns(2)
|
1001 |
-
|
1002 |
with components_col1:
|
1003 |
materials_percentage = st.slider(
|
1004 |
"نسبة المواد من سعر الوحدة",
|
@@ -1009,7 +1010,7 @@ class SmartPriceAnalysis:
|
|
1009 |
format="%g%%",
|
1010 |
key="settings_materials_percentage"
|
1011 |
) * 100
|
1012 |
-
|
1013 |
equipment_percentage = st.slider(
|
1014 |
"نسبة المعدات من سعر الوحدة",
|
1015 |
min_value=0.0,
|
@@ -1019,7 +1020,7 @@ class SmartPriceAnalysis:
|
|
1019 |
format="%g%%",
|
1020 |
key="settings_equipment_percentage"
|
1021 |
) * 100
|
1022 |
-
|
1023 |
with components_col2:
|
1024 |
labor_percentage = st.slider(
|
1025 |
"نسبة العمالة من سعر الوحدة",
|
@@ -1030,7 +1031,7 @@ class SmartPriceAnalysis:
|
|
1030 |
format="%g%%",
|
1031 |
key="settings_labor_percentage"
|
1032 |
) * 100
|
1033 |
-
|
1034 |
subcontractors_percentage = st.slider(
|
1035 |
"نسبة مقاولي الباطن من سعر الوحدة",
|
1036 |
min_value=0.0,
|
@@ -1040,12 +1041,12 @@ class SmartPriceAnalysis:
|
|
1040 |
format="%g%%",
|
1041 |
key="settings_subcontractors_percentage"
|
1042 |
) * 100
|
1043 |
-
|
1044 |
# التكاليف غير المباشرة
|
1045 |
st.markdown("#### نسب التكاليف غير المباشرة الافتراضية")
|
1046 |
-
|
1047 |
indirect_col1, indirect_col2, indirect_col3 = st.columns(3)
|
1048 |
-
|
1049 |
with indirect_col1:
|
1050 |
overhead_percentage = st.slider(
|
1051 |
"نسبة المصاريف العمومية والإدارية",
|
@@ -1056,7 +1057,7 @@ class SmartPriceAnalysis:
|
|
1056 |
format="%g%%",
|
1057 |
key="settings_overhead_percentage"
|
1058 |
) * 100
|
1059 |
-
|
1060 |
with indirect_col2:
|
1061 |
profit_percentage = st.slider(
|
1062 |
"نسبة الربح",
|
@@ -1067,7 +1068,7 @@ class SmartPriceAnalysis:
|
|
1067 |
format="%g%%",
|
1068 |
key="settings_profit_percentage"
|
1069 |
) * 100
|
1070 |
-
|
1071 |
with indirect_col3:
|
1072 |
contingency_percentage = st.slider(
|
1073 |
"نسبة الطوارئ",
|
@@ -1078,12 +1079,12 @@ class SmartPriceAnalysis:
|
|
1078 |
format="%g%%",
|
1079 |
key="settings_contingency_percentage"
|
1080 |
) * 100
|
1081 |
-
|
1082 |
# عوامل الإنتاجية
|
1083 |
st.markdown("#### عوامل الإنتاجية")
|
1084 |
-
|
1085 |
productivity_col1, productivity_col2 = st.columns(2)
|
1086 |
-
|
1087 |
with productivity_col1:
|
1088 |
weather_factor = st.slider(
|
1089 |
"عامل الطقس",
|
@@ -1093,7 +1094,7 @@ class SmartPriceAnalysis:
|
|
1093 |
step=0.1,
|
1094 |
key="settings_weather_factor"
|
1095 |
)
|
1096 |
-
|
1097 |
location_factor = st.slider(
|
1098 |
"عامل الموقع",
|
1099 |
min_value=0.5,
|
@@ -1102,7 +1103,7 @@ class SmartPriceAnalysis:
|
|
1102 |
step=0.1,
|
1103 |
key="settings_location_factor"
|
1104 |
)
|
1105 |
-
|
1106 |
complexity_factor = st.slider(
|
1107 |
"عامل التعقيد",
|
1108 |
min_value=0.5,
|
@@ -1111,7 +1112,7 @@ class SmartPriceAnalysis:
|
|
1111 |
step=0.1,
|
1112 |
key="settings_complexity_factor"
|
1113 |
)
|
1114 |
-
|
1115 |
with productivity_col2:
|
1116 |
schedule_factor = st.slider(
|
1117 |
"عامل الجدول الزمني",
|
@@ -1121,7 +1122,7 @@ class SmartPriceAnalysis:
|
|
1121 |
step=0.1,
|
1122 |
key="settings_schedule_factor"
|
1123 |
)
|
1124 |
-
|
1125 |
resources_factor = st.slider(
|
1126 |
"عامل الموارد",
|
1127 |
min_value=0.5,
|
@@ -1130,14 +1131,14 @@ class SmartPriceAnalysis:
|
|
1130 |
step=0.1,
|
1131 |
key="settings_resources_factor"
|
1132 |
)
|
1133 |
-
|
1134 |
# زر حفظ الإعدادات
|
1135 |
submit_button = st.form_submit_button("حفظ الإعدادات")
|
1136 |
-
|
1137 |
if submit_button:
|
1138 |
# التحقق من صحة البيانات
|
1139 |
total_percentage = (materials_percentage + equipment_percentage + labor_percentage + subcontractors_percentage) / 100
|
1140 |
-
|
1141 |
if abs(total_percentage - 1.0) > 0.01:
|
1142 |
st.error("مجموع نسب المكونات يجب أن يساوي 100%")
|
1143 |
else:
|
@@ -1146,33 +1147,33 @@ class SmartPriceAnalysis:
|
|
1146 |
price_components["equipment"] = equipment_percentage / 100
|
1147 |
price_components["labor"] = labor_percentage / 100
|
1148 |
price_components["subcontractors"] = subcontractors_percentage / 100
|
1149 |
-
|
1150 |
# تحديث نسب التكاليف غير المباشرة
|
1151 |
indirect_costs["overhead"] = overhead_percentage / 100
|
1152 |
indirect_costs["profit"] = profit_percentage / 100
|
1153 |
indirect_costs["contingency"] = contingency_percentage / 100
|
1154 |
-
|
1155 |
# تحديث عوامل الإنتاجية
|
1156 |
productivity_factors["weather"] = weather_factor
|
1157 |
productivity_factors["location"] = location_factor
|
1158 |
productivity_factors["complexity"] = complexity_factor
|
1159 |
productivity_factors["schedule"] = schedule_factor
|
1160 |
productivity_factors["resources"] = resources_factor
|
1161 |
-
|
1162 |
# تحديث حالة الجلسة
|
1163 |
st.session_state.smart_price_analysis["price_components"] = price_components
|
1164 |
st.session_state.smart_price_analysis["indirect_costs"] = indirect_costs
|
1165 |
st.session_state.smart_price_analysis["productivity_factors"] = productivity_factors
|
1166 |
-
|
1167 |
# عرض رسالة نجاح
|
1168 |
st.success("تم حفظ إعدادات التحليل بنجاح!")
|
1169 |
-
|
1170 |
# عرض الإعدادات الحالية
|
1171 |
st.markdown("### الإعدادات الحالية")
|
1172 |
-
|
1173 |
# عرض نسب مكونات السعر
|
1174 |
st.markdown("#### نسب مكونات السعر")
|
1175 |
-
|
1176 |
# إنشاء بيانات الرسم البياني
|
1177 |
components_data = {
|
1178 |
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
|
@@ -1183,7 +1184,7 @@ class SmartPriceAnalysis:
|
|
1183 |
price_components["subcontractors"] * 100
|
1184 |
]
|
1185 |
}
|
1186 |
-
|
1187 |
# إنشاء رسم بياني دائري
|
1188 |
fig = px.pie(
|
1189 |
components_data,
|
@@ -1192,12 +1193,12 @@ class SmartPriceAnalysis:
|
|
1192 |
title="توزيع مكونات سعر الوحدة",
|
1193 |
color="المكون"
|
1194 |
)
|
1195 |
-
|
1196 |
st.plotly_chart(fig, use_container_width=True)
|
1197 |
-
|
1198 |
# عرض نسب التكاليف غير المباشرة
|
1199 |
st.markdown("#### نسب التكاليف غير المباشرة")
|
1200 |
-
|
1201 |
# إنشاء بيانات الرسم البياني
|
1202 |
indirect_data = {
|
1203 |
"البند": ["المصاريف العمومية والإدارية", "الربح", "الطوارئ"],
|
@@ -1207,7 +1208,7 @@ class SmartPriceAnalysis:
|
|
1207 |
indirect_costs["contingency"] * 100
|
1208 |
]
|
1209 |
}
|
1210 |
-
|
1211 |
# إنشاء رسم بياني شريطي
|
1212 |
fig = px.bar(
|
1213 |
indirect_data,
|
@@ -1217,12 +1218,12 @@ class SmartPriceAnalysis:
|
|
1217 |
color="البند",
|
1218 |
text_auto=True
|
1219 |
)
|
1220 |
-
|
1221 |
st.plotly_chart(fig, use_container_width=True)
|
1222 |
-
|
1223 |
# عرض عوامل الإنتاجية
|
1224 |
st.markdown("#### عوامل الإنتاجية")
|
1225 |
-
|
1226 |
# إنشاء بيانات الرسم البياني
|
1227 |
productivity_data = {
|
1228 |
"العامل": ["الطقس", "الموقع", "التعقيد", "الجدول الزمني", "الموارد"],
|
@@ -1234,7 +1235,7 @@ class SmartPriceAnalysis:
|
|
1234 |
productivity_factors["resources"]
|
1235 |
]
|
1236 |
}
|
1237 |
-
|
1238 |
# إنشاء رسم بياني شريطي
|
1239 |
fig = px.bar(
|
1240 |
productivity_data,
|
@@ -1244,7 +1245,7 @@ class SmartPriceAnalysis:
|
|
1244 |
color="العامل",
|
1245 |
text_auto=True
|
1246 |
)
|
1247 |
-
|
1248 |
# إضافة خط أفقي عند القيمة 1.0
|
1249 |
fig.add_shape(
|
1250 |
type="line",
|
@@ -1258,42 +1259,42 @@ class SmartPriceAnalysis:
|
|
1258 |
dash="dash"
|
1259 |
)
|
1260 |
)
|
1261 |
-
|
1262 |
st.plotly_chart(fig, use_container_width=True)
|
1263 |
-
|
1264 |
def _render_analysis_reports_tab(self):
|
1265 |
"""عرض تبويب تقارير التحليل"""
|
1266 |
-
|
1267 |
st.markdown("### تقارير التحليل الذكي للأسعار")
|
1268 |
-
|
1269 |
# استخراج البيانات
|
1270 |
boq_items = st.session_state.boq_items
|
1271 |
analysis_history = st.session_state.smart_price_analysis["analysis_history"]
|
1272 |
-
|
1273 |
# عرض ملخص التحليل
|
1274 |
st.markdown("#### ملخص التحليل")
|
1275 |
-
|
1276 |
# حساب عدد البنود المحللة وغير المحللة
|
1277 |
analyzed_count = len(boq_items[boq_items["analyzed"] == True])
|
1278 |
not_analyzed_count = len(boq_items[boq_items["analyzed"] == False])
|
1279 |
-
|
1280 |
# عرض نسبة التحليل
|
1281 |
analysis_percentage = analyzed_count / len(boq_items) * 100 if len(boq_items) > 0 else 0
|
1282 |
-
|
1283 |
st.markdown(f"**عدد البنود المحللة:** {analyzed_count} من أصل {len(boq_items)} ({analysis_percentage:.2f}%)")
|
1284 |
-
|
1285 |
# إنشاء مؤشر التقدم
|
1286 |
st.progress(analysis_percentage / 100)
|
1287 |
-
|
1288 |
# عرض توزيع البنود المحللة حسب الفئة
|
1289 |
st.markdown("#### توزيع البنود المحللة حسب الفئة")
|
1290 |
-
|
1291 |
# حساب عدد البنود المحللة لكل فئة
|
1292 |
category_analysis = boq_items.groupby(["category", "analyzed"]).size().unstack(fill_value=0).reset_index()
|
1293 |
-
|
1294 |
if True in category_analysis.columns:
|
1295 |
category_analysis.columns = ["الفئة", "غير محلل", "محلل"]
|
1296 |
-
|
1297 |
# إنشاء رسم بياني شريطي
|
1298 |
fig = px.bar(
|
1299 |
category_analysis,
|
@@ -1303,26 +1304,26 @@ class SmartPriceAnalysis:
|
|
1303 |
barmode="stack",
|
1304 |
color_discrete_map={"محلل": "green", "غير محلل": "red"}
|
1305 |
)
|
1306 |
-
|
1307 |
st.plotly_chart(fig, use_container_width=True)
|
1308 |
else:
|
1309 |
st.info("لا يوجد بنود محللة بعد")
|
1310 |
-
|
1311 |
# عرض توزيع مكونات الأسعار
|
1312 |
st.markdown("#### توزيع مكونات الأسعار")
|
1313 |
-
|
1314 |
# التحقق من وجود بنود محللة
|
1315 |
if analyzed_count > 0:
|
1316 |
# استخراج البنود المحللة
|
1317 |
analyzed_items = boq_items[boq_items["analyzed"] == True]
|
1318 |
-
|
1319 |
# إنشاء قائمة لتخزين بيانات المكونات
|
1320 |
components_data = []
|
1321 |
-
|
1322 |
# استخراج بيانات المكونات
|
1323 |
for _, item in analyzed_items.iterrows():
|
1324 |
components = item["components"]
|
1325 |
-
|
1326 |
components_data.append({
|
1327 |
"id": item["id"],
|
1328 |
"description": item["description"],
|
@@ -1336,19 +1337,19 @@ class SmartPriceAnalysis:
|
|
1336 |
"labor_amount": components["labor"]["amount"],
|
1337 |
"subcontractors_amount": components["subcontractors"]["amount"]
|
1338 |
})
|
1339 |
-
|
1340 |
# إنشاء DataFrame
|
1341 |
components_df = pd.DataFrame(components_data)
|
1342 |
-
|
1343 |
# حساب متوسط النسب
|
1344 |
avg_materials_percentage = components_df["materials_percentage"].mean() * 100
|
1345 |
avg_equipment_percentage = components_df["equipment_percentage"].mean() * 100
|
1346 |
avg_labor_percentage = components_df["labor_percentage"].mean() * 100
|
1347 |
avg_subcontractors_percentage = components_df["subcontractors_percentage"].mean() * 100
|
1348 |
-
|
1349 |
# عرض متوسط النسب
|
1350 |
st.markdown("##### متوسط نسب المكونات")
|
1351 |
-
|
1352 |
avg_components_data = {
|
1353 |
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
|
1354 |
"النسبة": [
|
@@ -1358,7 +1359,7 @@ class SmartPriceAnalysis:
|
|
1358 |
avg_subcontractors_percentage
|
1359 |
]
|
1360 |
}
|
1361 |
-
|
1362 |
# إنشاء رسم بياني دائري
|
1363 |
fig = px.pie(
|
1364 |
avg_components_data,
|
@@ -1367,15 +1368,15 @@ class SmartPriceAnalysis:
|
|
1367 |
title="متوسط نسب مكونات الأسعار",
|
1368 |
color="المكون"
|
1369 |
)
|
1370 |
-
|
1371 |
st.plotly_chart(fig, use_container_width=True)
|
1372 |
-
|
1373 |
# عرض توزيع النسب حسب البند
|
1374 |
st.markdown("##### توزيع نسب المكونات حسب البند")
|
1375 |
-
|
1376 |
# إنشاء بيانات للرسم البياني
|
1377 |
item_components_data = []
|
1378 |
-
|
1379 |
for _, row in components_df.iterrows():
|
1380 |
item_components_data.extend([
|
1381 |
{"البند": row["id"], "المكون": "المواد", "النسبة": row["materials_percentage"] * 100},
|
@@ -1383,10 +1384,10 @@ class SmartPriceAnalysis:
|
|
1383 |
{"البند": row["id"], "المكون": "العمالة", "النسبة": row["labor_percentage"] * 100},
|
1384 |
{"البند": row["id"], "المكون": "مقاولي الباطن", "النسبة": row["subcontractors_percentage"] * 100}
|
1385 |
])
|
1386 |
-
|
1387 |
# إنشاء DataFrame
|
1388 |
item_components_df = pd.DataFrame(item_components_data)
|
1389 |
-
|
1390 |
# إنشاء رسم بياني شريطي
|
1391 |
fig = px.bar(
|
1392 |
item_components_df,
|
@@ -1396,15 +1397,15 @@ class SmartPriceAnalysis:
|
|
1396 |
title="توزيع نسب المكونات ح��ب البند",
|
1397 |
barmode="stack"
|
1398 |
)
|
1399 |
-
|
1400 |
st.plotly_chart(fig, use_container_width=True)
|
1401 |
-
|
1402 |
# عرض مقارنة أسعار الوحدة
|
1403 |
st.markdown("##### مقارنة أسعار الوحدة")
|
1404 |
-
|
1405 |
# إنشاء بيانات للرسم البياني
|
1406 |
unit_price_data = []
|
1407 |
-
|
1408 |
for _, row in components_df.iterrows():
|
1409 |
unit_price_data.extend([
|
1410 |
{"البند": row["id"], "المكون": "المواد", "القيمة": row["materials_amount"]},
|
@@ -1412,10 +1413,10 @@ class SmartPriceAnalysis:
|
|
1412 |
{"البند": row["id"], "المكون": "العمالة", "القيمة": row["labor_amount"]},
|
1413 |
{"البند": row["id"], "المكون": "مقاولي الباطن", "القيمة": row["subcontractors_amount"]}
|
1414 |
])
|
1415 |
-
|
1416 |
# إنشاء DataFrame
|
1417 |
unit_price_df = pd.DataFrame(unit_price_data)
|
1418 |
-
|
1419 |
# إنشاء رسم بياني شريطي
|
1420 |
fig = px.bar(
|
1421 |
unit_price_df,
|
@@ -1425,7 +1426,7 @@ class SmartPriceAnalysis:
|
|
1425 |
title="مقارنة مكونات أسعار الوحدة",
|
1426 |
barmode="stack"
|
1427 |
)
|
1428 |
-
|
1429 |
# إضافة خط لسعر الوحدة
|
1430 |
for i, row in components_df.iterrows():
|
1431 |
fig.add_shape(
|
@@ -1440,21 +1441,22 @@ class SmartPriceAnalysis:
|
|
1440 |
dash="dash"
|
1441 |
)
|
1442 |
)
|
1443 |
-
|
1444 |
st.plotly_chart(fig, use_container_width=True)
|
|
|
1445 |
else:
|
1446 |
st.info("لا يوجد بنود محللة بعد")
|
1447 |
-
|
1448 |
# عرض سجل التحليلات
|
1449 |
st.markdown("#### سجل التحليلات")
|
1450 |
-
|
1451 |
if analysis_history:
|
1452 |
# عرض عدد التحليلات
|
1453 |
st.markdown(f"**عدد التحليلات:** {len(analysis_history)}")
|
1454 |
-
|
1455 |
# عرض آخر 5 تحليلات
|
1456 |
st.markdown("##### آخر 5 تحليلات")
|
1457 |
-
|
1458 |
for i, analysis in enumerate(analysis_history[-5:]):
|
1459 |
st.markdown(f"**{i+1}. البند:** {analysis['item_id']} - {analysis['item_description']}")
|
1460 |
st.markdown(f"**تاريخ التحليل:** {analysis['analysis_date']}")
|
@@ -1462,19 +1464,19 @@ class SmartPriceAnalysis:
|
|
1462 |
st.markdown("---")
|
1463 |
else:
|
1464 |
st.info("لا يوجد سجل تحليلات بعد")
|
1465 |
-
|
1466 |
def _render_local_content_tab(self):
|
1467 |
"""عرض تبويب المحتوى المحلي"""
|
1468 |
-
|
1469 |
st.markdown("### تحليل المحتوى المحلي")
|
1470 |
-
|
1471 |
# استخراج بيانات المحتوى المحلي
|
1472 |
local_content = st.session_state.smart_price_analysis["local_content"]
|
1473 |
-
|
1474 |
# إنشاء نموذج إعدادات المحتوى المحلي
|
1475 |
with st.form("local_content_form"):
|
1476 |
st.markdown("#### إعدادات المحتوى المحلي")
|
1477 |
-
|
1478 |
# النسبة المستهدفة للمحتوى المحلي
|
1479 |
target_percentage = st.slider(
|
1480 |
"النسبة المستهدفة للمحتوى المحلي",
|
@@ -1485,12 +1487,12 @@ class SmartPriceAnalysis:
|
|
1485 |
format="%g%%",
|
1486 |
key="local_content_target"
|
1487 |
) * 100
|
1488 |
-
|
1489 |
# نسب المحتوى المحلي لكل مكون
|
1490 |
st.markdown("#### نسب المحتوى المحلي لكل مكون")
|
1491 |
-
|
1492 |
local_col1, local_col2 = st.columns(2)
|
1493 |
-
|
1494 |
with local_col1:
|
1495 |
materials_local = st.slider(
|
1496 |
"نسبة المواد المحلية",
|
@@ -1501,7 +1503,7 @@ class SmartPriceAnalysis:
|
|
1501 |
format="%g%%",
|
1502 |
key="materials_local"
|
1503 |
) * 100
|
1504 |
-
|
1505 |
equipment_local = st.slider(
|
1506 |
"نسبة المعدات المحلية",
|
1507 |
min_value=0.0,
|
@@ -1511,7 +1513,7 @@ class SmartPriceAnalysis:
|
|
1511 |
format="%g%%",
|
1512 |
key="equipment_local"
|
1513 |
) * 100
|
1514 |
-
|
1515 |
with local_col2:
|
1516 |
labor_local = st.slider(
|
1517 |
"نسبة العمالة المحلية",
|
@@ -1522,7 +1524,7 @@ class SmartPriceAnalysis:
|
|
1522 |
format="%g%%",
|
1523 |
key="labor_local"
|
1524 |
) * 100
|
1525 |
-
|
1526 |
subcontractors_local = st.slider(
|
1527 |
"نسبة مقاولي الباطن المحليين",
|
1528 |
min_value=0.0,
|
@@ -1532,10 +1534,10 @@ class SmartPriceAnalysis:
|
|
1532 |
format="%g%%",
|
1533 |
key="subcontractors_local"
|
1534 |
) * 100
|
1535 |
-
|
1536 |
# زر حفظ الإعدادات
|
1537 |
submit_button = st.form_submit_button("حفظ إعدادات المحتوى المحلي")
|
1538 |
-
|
1539 |
if submit_button:
|
1540 |
# تحديث إعدادات المحتوى المحلي
|
1541 |
local_content["target"] = target_percentage / 100
|
@@ -1543,19 +1545,19 @@ class SmartPriceAnalysis:
|
|
1543 |
local_content["equipment_local"] = equipment_local / 100
|
1544 |
local_content["labor_local"] = labor_local / 100
|
1545 |
local_content["subcontractors_local"] = subcontractors_local / 100
|
1546 |
-
|
1547 |
# تحديث حالة الجلسة
|
1548 |
st.session_state.smart_price_analysis["local_content"] = local_content
|
1549 |
-
|
1550 |
# عرض رسالة نجاح
|
1551 |
st.success("تم حفظ إعدادات المحتوى المحلي بنجاح!")
|
1552 |
-
|
1553 |
# حساب نسبة المحتوى المحلي الفعلية
|
1554 |
st.markdown("#### حساب نسبة المحتوى المحلي الفعلية")
|
1555 |
-
|
1556 |
# استخراج نسب مكونات السعر
|
1557 |
price_components = st.session_state.smart_price_analysis["price_components"]
|
1558 |
-
|
1559 |
# حساب نسبة المحتوى المحلي الفعلية
|
1560 |
actual_local_content = (
|
1561 |
price_components["materials"] * local_content["materials_local"] +
|
@@ -1563,31 +1565,31 @@ class SmartPriceAnalysis:
|
|
1563 |
price_components["labor"] * local_content["labor_local"] +
|
1564 |
price_components["subcontractors"] * local_content["subcontractors_local"]
|
1565 |
)
|
1566 |
-
|
1567 |
# عرض نسبة المحتوى المحلي الفعلية
|
1568 |
st.markdown(f"**نسبة المحتوى المحلي الفعلية:** {actual_local_content * 100:.2f}%")
|
1569 |
st.markdown(f"**النسبة المستهدفة للمحتوى المحلي:** {local_content['target'] * 100:.2f}%")
|
1570 |
-
|
1571 |
# عرض مؤشر التقدم
|
1572 |
progress_percentage = min(actual_local_content / local_content["target"], 1.0) if local_content["target"] > 0 else 0
|
1573 |
-
|
1574 |
st.progress(progress_percentage)
|
1575 |
-
|
1576 |
# عرض حالة المحتوى المحلي
|
1577 |
if actual_local_content >= local_content["target"]:
|
1578 |
st.success("تم تحقيق النسبة المستهدفة للمحتوى المحلي")
|
1579 |
else:
|
1580 |
st.warning("لم يتم تحقيق النسبة المستهدفة للمحتوى المحلي")
|
1581 |
-
|
1582 |
# عرض مساهمة كل مكون في المحتوى المحلي
|
1583 |
st.markdown("#### مساهمة كل مكون في المحتوى المحلي")
|
1584 |
-
|
1585 |
# حساب مساهمة كل مكون
|
1586 |
materials_contribution = price_components["materials"] * local_content["materials_local"]
|
1587 |
equipment_contribution = price_components["equipment"] * local_content["equipment_local"]
|
1588 |
labor_contribution = price_components["labor"] * local_content["labor_local"]
|
1589 |
subcontractors_contribution = price_components["subcontractors"] * local_content["subcontractors_local"]
|
1590 |
-
|
1591 |
# إنشاء بيانات الرسم البياني
|
1592 |
contribution_data = {
|
1593 |
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
|
@@ -1598,7 +1600,7 @@ class SmartPriceAnalysis:
|
|
1598 |
subcontractors_contribution * 100
|
1599 |
]
|
1600 |
}
|
1601 |
-
|
1602 |
# إنشاء رسم بياني شريطي
|
1603 |
fig = px.bar(
|
1604 |
contribution_data,
|
@@ -1608,21 +1610,21 @@ class SmartPriceAnalysis:
|
|
1608 |
color="المكون",
|
1609 |
text_auto=True
|
1610 |
)
|
1611 |
-
|
1612 |
st.plotly_chart(fig, use_container_width=True)
|
1613 |
-
|
1614 |
# عرض توصيات لتحسين نسبة المحتوى المحلي
|
1615 |
st.markdown("#### توصيات لتحسين نسبة المحتوى المحلي")
|
1616 |
-
|
1617 |
if actual_local_content < local_content["target"]:
|
1618 |
# حساب الفجوة
|
1619 |
gap = local_content["target"] - actual_local_content
|
1620 |
-
|
1621 |
st.markdown(f"**الفجوة الحالية:** {gap * 100:.2f}%")
|
1622 |
-
|
1623 |
# تحديد المكونات التي يمكن تحسينها
|
1624 |
components_to_improve = []
|
1625 |
-
|
1626 |
if local_content["materials_local"] < 1.0:
|
1627 |
components_to_improve.append({
|
1628 |
"name": "المواد",
|
@@ -1630,7 +1632,7 @@ class SmartPriceAnalysis:
|
|
1630 |
"weight": price_components["materials"],
|
1631 |
"potential": price_components["materials"] * (1.0 - local_content["materials_local"])
|
1632 |
})
|
1633 |
-
|
1634 |
if local_content["equipment_local"] < 1.0:
|
1635 |
components_to_improve.append({
|
1636 |
"name": "المعدات",
|
@@ -1638,7 +1640,7 @@ class SmartPriceAnalysis:
|
|
1638 |
"weight": price_components["equipment"],
|
1639 |
"potential": price_components["equipment"] * (1.0 - local_content["equipment_local"])
|
1640 |
})
|
1641 |
-
|
1642 |
if local_content["labor_local"] < 1.0:
|
1643 |
components_to_improve.append({
|
1644 |
"name": "العمالة",
|
@@ -1646,7 +1648,7 @@ class SmartPriceAnalysis:
|
|
1646 |
"weight": price_components["labor"],
|
1647 |
"potential": price_components["labor"] * (1.0 - local_content["labor_local"])
|
1648 |
})
|
1649 |
-
|
1650 |
if local_content["subcontractors_local"] < 1.0:
|
1651 |
components_to_improve.append({
|
1652 |
"name": "مقاولي الباطن",
|
@@ -1654,90 +1656,133 @@ class SmartPriceAnalysis:
|
|
1654 |
"weight": price_components["subcontractors"],
|
1655 |
"potential": price_components["subcontractors"] * (1.0 - local_content["subcontractors_local"])
|
1656 |
})
|
1657 |
-
|
1658 |
# ترتيب المكونات حسب إمكانية التحسين
|
1659 |
components_to_improve.sort(key=lambda x: x["potential"], reverse=True)
|
1660 |
-
|
1661 |
# عرض التوصيات
|
1662 |
for component in components_to_improve:
|
1663 |
st.markdown(f"**{component['name']}:** زيادة نسبة {component['name']} المحلية من {component['current'] * 100:.2f}% إلى {min(component['current'] + gap / component['weight'], 1.0) * 100:.2f}%")
|
1664 |
else:
|
1665 |
st.success("تم تحقيق النسبة المستهدفة للمحتوى المحلي")
|
1666 |
-
|
1667 |
def calculate_item_price(self, item_data):
|
1668 |
"""حساب سعر البند بناءً على مكوناته"""
|
1669 |
-
|
1670 |
# استخراج مكونات البند
|
1671 |
materials = item_data.get("materials", [])
|
1672 |
equipment = item_data.get("equipment", [])
|
1673 |
labor = item_data.get("labor", [])
|
1674 |
subcontractors = item_data.get("subcontractors", [])
|
1675 |
-
|
1676 |
# حساب تكلفة المواد
|
1677 |
materials_cost = sum([material["quantity"] * material["price"] for material in materials])
|
1678 |
-
|
1679 |
# حساب تكلفة المعدات
|
1680 |
equipment_cost = sum([equipment_item["quantity"] * equipment_item["price"] for equipment_item in equipment])
|
1681 |
-
|
1682 |
# حساب تكلفة العمالة
|
1683 |
labor_cost = sum([labor_item["quantity"] * labor_item["price"] for labor_item in labor])
|
1684 |
-
|
1685 |
# حساب تكلفة مقاولي الباطن
|
1686 |
subcontractors_cost = sum([subcontractor["price"] for subcontractor in subcontractors])
|
1687 |
-
|
1688 |
# حساب التكاليف المباشرة
|
1689 |
direct_cost = materials_cost + equipment_cost + labor_cost + subcontractors_cost
|
1690 |
-
|
1691 |
# استخراج نسب التكاليف غير المباشرة
|
1692 |
indirect_costs = st.session_state.smart_price_analysis["indirect_costs"]
|
1693 |
-
|
1694 |
# حساب التكاليف غير المباشرة
|
1695 |
overhead_amount = direct_cost * indirect_costs["overhead"]
|
1696 |
profit_amount = direct_cost * indirect_costs["profit"]
|
1697 |
contingency_amount = direct_cost * indirect_costs["contingency"]
|
1698 |
-
|
1699 |
# حساب إجمالي التكاليف
|
1700 |
total_cost = direct_cost + overhead_amount + profit_amount + contingency_amount
|
1701 |
-
|
1702 |
return total_cost
|
1703 |
-
|
1704 |
def calculate_local_content(self, item_data):
|
1705 |
"""حساب نسبة المحتوى المحلي للبند"""
|
1706 |
-
|
1707 |
# استخراج مكونات البند
|
1708 |
materials = item_data.get("materials", [])
|
1709 |
equipment = item_data.get("equipment", [])
|
1710 |
labor = item_data.get("labor", [])
|
1711 |
subcontractors = item_data.get("subcontractors", [])
|
1712 |
-
|
1713 |
# استخراج نسب المحتوى المحلي
|
1714 |
local_content = st.session_state.smart_price_analysis["local_content"]
|
1715 |
-
|
1716 |
# حساب تكلفة المواد
|
1717 |
materials_cost = sum([material["quantity"] * material["price"] for material in materials])
|
1718 |
-
|
1719 |
# حساب تكلفة المعدات
|
1720 |
equipment_cost = sum([equipment_item["quantity"] * equipment_item["price"] for equipment_item in equipment])
|
1721 |
-
|
1722 |
# حساب تكلفة العمالة
|
1723 |
labor_cost = sum([labor_item["quantity"] * labor_item["price"] for labor_item in labor])
|
1724 |
-
|
1725 |
# حساب تكلفة مقاولي الباطن
|
1726 |
subcontractors_cost = sum([subcontractor["price"] for subcontractor in subcontractors])
|
1727 |
-
|
1728 |
# حساب التكاليف المباشرة
|
1729 |
direct_cost = materials_cost + equipment_cost + labor_cost + subcontractors_cost
|
1730 |
-
|
1731 |
# حساب المحتوى المحلي
|
1732 |
local_materials = materials_cost * local_content["materials_local"]
|
1733 |
local_equipment = equipment_cost * local_content["equipment_local"]
|
1734 |
local_labor = labor_cost * local_content["labor_local"]
|
1735 |
local_subcontractors = subcontractors_cost * local_content["subcontractors_local"]
|
1736 |
-
|
1737 |
# حساب إجمالي المحتوى المحلي
|
1738 |
total_local_content = local_materials + local_equipment + local_labor + local_subcontractors
|
1739 |
-
|
1740 |
# حساب نسبة المحتوى المحلي
|
1741 |
local_content_percentage = total_local_content / direct_cost if direct_cost > 0 else 0
|
1742 |
-
|
1743 |
return local_content_percentage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
class SmartPriceAnalysis:
|
17 |
"""فئة التحليل الذكي للأسعار"""
|
18 |
+
|
19 |
def __init__(self):
|
20 |
"""تهيئة وحدة التحليل الذكي للأسعار"""
|
21 |
+
|
22 |
# تهيئة حالة الجلسة للتحليل الذكي للأسعار
|
23 |
if 'smart_price_analysis' not in st.session_state:
|
24 |
self._initialize_smart_price_analysis()
|
25 |
+
|
26 |
# الوصول إلى كتالوجات الموارد
|
27 |
self.equipment_catalog = self._get_equipment_catalog()
|
28 |
self.materials_catalog = self._get_materials_catalog()
|
29 |
self.labor_catalog = self._get_labor_catalog()
|
30 |
self.subcontractors_catalog = self._get_subcontractors_catalog()
|
31 |
+
self.cost_breakdown = {} #Added this line
|
32 |
+
|
33 |
def _initialize_smart_price_analysis(self):
|
34 |
"""تهيئة بيانات التحليل الذكي للأسعار"""
|
35 |
+
|
36 |
# إنشاء بيانات افتراضية للتحليل الذكي للأسعار
|
37 |
st.session_state.smart_price_analysis = {
|
38 |
"price_components": {
|
|
|
65 |
"analysis_history": [], # سجل تحليلات الأسعار
|
66 |
"current_item": None # البند الحالي قيد التحليل
|
67 |
}
|
68 |
+
|
69 |
# إنشاء بيانات افتراضية لبنود جدول الكميات
|
70 |
if 'boq_items' not in st.session_state:
|
71 |
self._initialize_boq_items()
|
72 |
+
|
73 |
def _initialize_boq_items(self):
|
74 |
"""تهيئة بيانات بنود جدول الكميات"""
|
75 |
+
|
76 |
# إنشاء بيانات افتراضية لبنود جدول الكميات
|
77 |
boq_items = [
|
78 |
{
|
|
|
196 |
"components": {}
|
197 |
}
|
198 |
]
|
199 |
+
|
200 |
# تخزين البيانات في حالة الجلسة
|
201 |
st.session_state.boq_items = pd.DataFrame(boq_items)
|
202 |
+
|
203 |
def _get_equipment_catalog(self):
|
204 |
"""الحصول على كتالوج المعدات"""
|
205 |
+
|
206 |
# التحقق من وجود كتالوج المعدات في حالة الجلسة
|
207 |
if 'equipment_catalog' in st.session_state:
|
208 |
return st.session_state.equipment_catalog
|
209 |
+
|
210 |
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
|
211 |
equipment_data = []
|
212 |
+
|
213 |
# تخزين البيانات في حالة الجلسة
|
214 |
st.session_state.equipment_catalog = pd.DataFrame(equipment_data)
|
215 |
+
|
216 |
return st.session_state.equipment_catalog
|
217 |
+
|
218 |
def _get_materials_catalog(self):
|
219 |
"""الحصول على كتالوج المواد"""
|
220 |
+
|
221 |
# التحقق من وجود كتالوج المواد في حالة الجلسة
|
222 |
if 'materials_catalog' in st.session_state:
|
223 |
return st.session_state.materials_catalog
|
224 |
+
|
225 |
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
|
226 |
materials_data = []
|
227 |
+
|
228 |
# تخزين البيانات في حالة الجلسة
|
229 |
st.session_state.materials_catalog = pd.DataFrame(materials_data)
|
230 |
+
|
231 |
return st.session_state.materials_catalog
|
232 |
+
|
233 |
def _get_labor_catalog(self):
|
234 |
"""الحصول على كتالوج العمالة"""
|
235 |
+
|
236 |
# التحقق من وجود كتالوج العمالة في حالة الجلسة
|
237 |
if 'labor_catalog' in st.session_state:
|
238 |
return st.session_state.labor_catalog
|
239 |
+
|
240 |
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
|
241 |
labor_data = []
|
242 |
+
|
243 |
# تخزين البيانات في حالة الجلسة
|
244 |
st.session_state.labor_catalog = pd.DataFrame(labor_data)
|
245 |
+
|
246 |
return st.session_state.labor_catalog
|
247 |
+
|
248 |
def _get_subcontractors_catalog(self):
|
249 |
"""الحصول على كتالوج مقاولي الباطن"""
|
250 |
+
|
251 |
# التحقق من وجود كتالوج مقاولي الباطن في حالة الجلسة
|
252 |
if 'subcontractors_catalog' in st.session_state:
|
253 |
return st.session_state.subcontractors_catalog
|
254 |
+
|
255 |
# إذا لم يكن موجوداً، إنشاء كتالوج افتراضي
|
256 |
subcontractors_data = []
|
257 |
+
|
258 |
# تخزين البيانات في حالة الجلسة
|
259 |
st.session_state.subcontractors_catalog = pd.DataFrame(subcontractors_data)
|
260 |
+
|
261 |
return st.session_state.subcontractors_catalog
|
262 |
+
|
263 |
def render(self):
|
264 |
"""عرض واجهة التحليل الذكي للأسعار"""
|
265 |
+
|
266 |
st.markdown("## التحليل الذكي للأسعار")
|
267 |
+
|
268 |
# إنشاء تبويبات لعرض التحليل الذكي للأسعار
|
269 |
tabs = st.tabs([
|
270 |
"تحليل البنود",
|
|
|
272 |
"تقارير التحليل",
|
273 |
"المحتوى المحلي"
|
274 |
])
|
275 |
+
|
276 |
with tabs[0]:
|
277 |
self._render_item_analysis_tab()
|
278 |
+
|
279 |
with tabs[1]:
|
280 |
self._render_analysis_settings_tab()
|
281 |
+
|
282 |
with tabs[2]:
|
283 |
self._render_analysis_reports_tab()
|
284 |
+
|
285 |
with tabs[3]:
|
286 |
self._render_local_content_tab()
|
287 |
+
|
288 |
def _render_item_analysis_tab(self):
|
289 |
"""عرض تبويب تحليل البنود"""
|
290 |
+
|
291 |
st.markdown("### تحليل بنود جدول الكميات")
|
292 |
+
|
293 |
# استخراج البيانات
|
294 |
boq_items = st.session_state.boq_items
|
295 |
+
|
296 |
# إنشاء فلاتر للعرض
|
297 |
col1, col2, col3 = st.columns(3)
|
298 |
+
|
299 |
with col1:
|
300 |
# فلتر حسب الفئة
|
301 |
categories = ["الكل"] + sorted(boq_items["category"].unique().tolist())
|
302 |
selected_category = st.selectbox("اختر فئة البند", categories, key="item_analysis_category")
|
303 |
+
|
304 |
with col2:
|
305 |
# فلتر حسب الفئة الفرعية
|
306 |
if selected_category != "الكل":
|
307 |
subcategories = ["الكل"] + sorted(boq_items[boq_items["category"] == selected_category]["subcategory"].unique().tolist())
|
308 |
else:
|
309 |
subcategories = ["الكل"] + sorted(boq_items["subcategory"].unique().tolist())
|
310 |
+
|
311 |
selected_subcategory = st.selectbox("اختر التخصص", subcategories, key="item_analysis_subcategory")
|
312 |
+
|
313 |
with col3:
|
314 |
# فلتر حسب حالة التحليل
|
315 |
analysis_status = ["الكل", "تم التحليل", "لم يتم التحليل"]
|
316 |
selected_status = st.selectbox("اختر حالة التحليل", analysis_status, key="item_analysis_status")
|
317 |
+
|
318 |
# تطبيق الفلاتر
|
319 |
filtered_df = boq_items.copy()
|
320 |
+
|
321 |
if selected_category != "الكل":
|
322 |
filtered_df = filtered_df[filtered_df["category"] == selected_category]
|
323 |
+
|
324 |
if selected_subcategory != "الكل":
|
325 |
filtered_df = filtered_df[filtered_df["subcategory"] == selected_subcategory]
|
326 |
+
|
327 |
if selected_status != "الكل":
|
328 |
if selected_status == "تم التحليل":
|
329 |
filtered_df = filtered_df[filtered_df["analyzed"] == True]
|
330 |
else:
|
331 |
filtered_df = filtered_df[filtered_df["analyzed"] == False]
|
332 |
+
|
333 |
# عرض البيانات
|
334 |
if not filtered_df.empty:
|
335 |
# عرض عدد النتائج
|
336 |
st.info(f"تم العثور على {len(filtered_df)} بند")
|
337 |
+
|
338 |
# إنشاء جدول للعرض
|
339 |
display_df = filtered_df[["id", "description", "unit", "quantity", "unit_price", "total_price", "analyzed"]].copy()
|
340 |
display_df.columns = ["الكود", "الوصف", "الوحدة", "الكمية", "سعر الوحدة", "الإجمالي", "تم التحليل"]
|
341 |
display_df["تم التحليل"] = display_df["تم التحليل"].map({True: "✅", False: "❌"})
|
342 |
+
|
343 |
# عرض الجدول
|
344 |
st.dataframe(display_df, use_container_width=True)
|
345 |
+
|
346 |
# اختيار بند للتحليل
|
347 |
st.markdown("#### اختر بند للتحليل")
|
348 |
+
|
349 |
selected_item_id = st.selectbox("اختر كود البند", filtered_df["id"].tolist(), key="item_analysis_selected_id")
|
350 |
+
|
351 |
# استخراج البند المختار
|
352 |
selected_item = filtered_df[filtered_df["id"] == selected_item_id].iloc[0]
|
353 |
+
|
354 |
# عرض تفاصيل البند
|
355 |
st.markdown(f"**البند:** {selected_item['description']}")
|
356 |
st.markdown(f"**الوحدة:** {selected_item['unit']} | **الكمية:** {selected_item['quantity']} | **سعر الوحدة:** {selected_item['unit_price']} ريال | **الإجمالي:** {selected_item['total_price']} ريال")
|
357 |
+
|
358 |
# تحليل البند
|
359 |
st.markdown("#### تحليل البند")
|
360 |
+
|
361 |
# التحقق من حالة التحليل
|
362 |
if selected_item["analyzed"]:
|
363 |
# عرض نتائج التحليل السابق
|
364 |
st.success("تم تحليل هذا البند مسبقاً")
|
365 |
+
|
366 |
# استخراج مكونات البند
|
367 |
components = selected_item["components"]
|
368 |
+
|
369 |
# عرض مكونات البند
|
370 |
self._display_item_components(selected_item)
|
371 |
+
|
372 |
# زر إعادة التحليل
|
373 |
if st.button("إعادة تحليل البند", key="reanalyze_button"):
|
374 |
# تعيين البند الحالي
|
375 |
st.session_state.smart_price_analysis["current_item"] = selected_item.to_dict()
|
376 |
+
|
377 |
# إعادة توجيه إلى صفحة التحليل
|
378 |
+
st.rerun()
|
379 |
else:
|
380 |
# تحليل البند لأول مرة
|
381 |
if st.button("تحليل البند", key="analyze_button"):
|
382 |
# تعيين البند الحالي
|
383 |
st.session_state.smart_price_analysis["current_item"] = selected_item.to_dict()
|
384 |
+
|
385 |
# إعادة توجيه إلى صفحة التحليل
|
386 |
+
st.rerun()
|
387 |
+
|
388 |
# التحقق من وجود بند حالي قيد التحليل
|
389 |
current_item = st.session_state.smart_price_analysis["current_item"]
|
390 |
+
|
391 |
if current_item and current_item["id"] == selected_item_id:
|
392 |
# عرض نموذج التحليل
|
393 |
self._render_analysis_form(current_item)
|
394 |
else:
|
395 |
st.warning("لا يوجد بنود تطابق معايير البحث")
|
396 |
+
|
397 |
def _render_analysis_form(self, item):
|
398 |
"""عرض نموذج تحليل البند"""
|
399 |
+
|
400 |
st.markdown("### تحليل البند")
|
401 |
st.markdown(f"**البند:** {item['description']}")
|
402 |
st.markdown(f"**الوحدة:** {item['unit']} | **الكمية:** {item['quantity']} | **سعر الوحدة:** {item['unit_price']} ريال | **الإجمالي:** {item['total_price']} ريال")
|
403 |
+
|
404 |
# استخراج نسب المكونات
|
405 |
price_components = st.session_state.smart_price_analysis["price_components"]
|
406 |
+
|
407 |
# حساب قيم المكونات
|
408 |
materials_value = item["unit_price"] * price_components["materials"]
|
409 |
equipment_value = item["unit_price"] * price_components["equipment"]
|
410 |
labor_value = item["unit_price"] * price_components["labor"]
|
411 |
subcontractors_value = item["unit_price"] * price_components["subcontractors"]
|
412 |
+
|
413 |
# إنشاء نموذج التحليل
|
414 |
with st.form("analysis_form"):
|
415 |
st.markdown("#### تحليل سعر الوحدة")
|
416 |
+
|
417 |
# المواد
|
418 |
st.markdown("##### المواد")
|
419 |
materials_col1, materials_col2 = st.columns(2)
|
420 |
+
|
421 |
with materials_col1:
|
422 |
materials_percentage = st.slider(
|
423 |
"نسبة المواد من سعر الوحدة",
|
|
|
428 |
format="%g%%",
|
429 |
key="materials_percentage"
|
430 |
) * 100
|
431 |
+
|
432 |
with materials_col2:
|
433 |
materials_amount = st.number_input(
|
434 |
"قيمة المواد (ريال)",
|
|
|
437 |
step=10.0,
|
438 |
key="materials_amount"
|
439 |
)
|
440 |
+
|
441 |
# إضافة المواد
|
442 |
materials_items = []
|
443 |
+
|
444 |
st.markdown("إضافة المواد")
|
445 |
+
|
446 |
for i in range(3): # السماح بإضافة 3 مواد كحد أقصى
|
447 |
material_col1, material_col2, material_col3, material_col4 = st.columns([3, 1, 1, 1])
|
448 |
+
|
449 |
with material_col1:
|
450 |
material_name = st.text_input(
|
451 |
"اسم المادة",
|
452 |
key=f"material_name_{i}"
|
453 |
)
|
454 |
+
|
455 |
with material_col2:
|
456 |
material_unit = st.text_input(
|
457 |
"الوحدة",
|
458 |
key=f"material_unit_{i}"
|
459 |
)
|
460 |
+
|
461 |
with material_col3:
|
462 |
material_quantity = st.number_input(
|
463 |
"الكمية",
|
|
|
465 |
step=0.1,
|
466 |
key=f"material_quantity_{i}"
|
467 |
)
|
468 |
+
|
469 |
with material_col4:
|
470 |
material_price = st.number_input(
|
471 |
"السعر",
|
|
|
473 |
step=10.0,
|
474 |
key=f"material_price_{i}"
|
475 |
)
|
476 |
+
|
477 |
if material_name and material_unit and material_quantity > 0 and material_price > 0:
|
478 |
materials_items.append({
|
479 |
"name": material_name,
|
|
|
482 |
"price": material_price,
|
483 |
"total": material_quantity * material_price
|
484 |
})
|
485 |
+
|
486 |
# المعدات
|
487 |
st.markdown("##### المعدات")
|
488 |
equipment_col1, equipment_col2 = st.columns(2)
|
489 |
+
|
490 |
with equipment_col1:
|
491 |
equipment_percentage = st.slider(
|
492 |
"نسبة المعدات من سعر الوحدة",
|
|
|
497 |
format="%g%%",
|
498 |
key="equipment_percentage"
|
499 |
) * 100
|
500 |
+
|
501 |
with equipment_col2:
|
502 |
equipment_amount = st.number_input(
|
503 |
"قيمة المعدات (ريال)",
|
|
|
506 |
step=10.0,
|
507 |
key="equipment_amount"
|
508 |
)
|
509 |
+
|
510 |
# إضافة المعدات
|
511 |
equipment_items = []
|
512 |
+
|
513 |
st.markdown("إضافة المعدات")
|
514 |
+
|
515 |
for i in range(3): # السماح بإضافة 3 معدات كحد أقصى
|
516 |
equipment_col1, equipment_col2, equipment_col3, equipment_col4 = st.columns([3, 1, 1, 1])
|
517 |
+
|
518 |
with equipment_col1:
|
519 |
equipment_name = st.text_input(
|
520 |
"اسم المعدة",
|
521 |
key=f"equipment_name_{i}"
|
522 |
)
|
523 |
+
|
524 |
with equipment_col2:
|
525 |
equipment_unit = st.text_input(
|
526 |
"الوحدة",
|
527 |
key=f"equipment_unit_{i}"
|
528 |
)
|
529 |
+
|
530 |
with equipment_col3:
|
531 |
equipment_quantity = st.number_input(
|
532 |
"الكمية",
|
|
|
534 |
step=0.1,
|
535 |
key=f"equipment_quantity_{i}"
|
536 |
)
|
537 |
+
|
538 |
with equipment_col4:
|
539 |
equipment_price = st.number_input(
|
540 |
"السعر",
|
|
|
542 |
step=10.0,
|
543 |
key=f"equipment_price_{i}"
|
544 |
)
|
545 |
+
|
546 |
if equipment_name and equipment_unit and equipment_quantity > 0 and equipment_price > 0:
|
547 |
equipment_items.append({
|
548 |
"name": equipment_name,
|
|
|
551 |
"price": equipment_price,
|
552 |
"total": equipment_quantity * equipment_price
|
553 |
})
|
554 |
+
|
555 |
# العمالة
|
556 |
st.markdown("##### العمالة")
|
557 |
labor_col1, labor_col2 = st.columns(2)
|
558 |
+
|
559 |
with labor_col1:
|
560 |
labor_percentage = st.slider(
|
561 |
"نسبة العمالة من سعر الوحدة",
|
|
|
566 |
format="%g%%",
|
567 |
key="labor_percentage"
|
568 |
) * 100
|
569 |
+
|
570 |
with labor_col2:
|
571 |
labor_amount = st.number_input(
|
572 |
"قيمة العمالة (ريال)",
|
|
|
575 |
step=10.0,
|
576 |
key="labor_amount"
|
577 |
)
|
578 |
+
|
579 |
# إضافة العمالة
|
580 |
labor_items = []
|
581 |
+
|
582 |
st.markdown("إضافة العمالة")
|
583 |
+
|
584 |
for i in range(3): # السماح بإضافة 3 عمال كحد أقصى
|
585 |
labor_col1, labor_col2, labor_col3, labor_col4 = st.columns([3, 1, 1, 1])
|
586 |
+
|
587 |
with labor_col1:
|
588 |
labor_name = st.text_input(
|
589 |
"المسمى الوظيفي",
|
590 |
key=f"labor_name_{i}"
|
591 |
)
|
592 |
+
|
593 |
with labor_col2:
|
594 |
labor_unit = st.text_input(
|
595 |
"الوحدة",
|
596 |
key=f"labor_unit_{i}"
|
597 |
)
|
598 |
+
|
599 |
with labor_col3:
|
600 |
labor_quantity = st.number_input(
|
601 |
"الكمية",
|
|
|
603 |
step=0.1,
|
604 |
key=f"labor_quantity_{i}"
|
605 |
)
|
606 |
+
|
607 |
with labor_col4:
|
608 |
labor_price = st.number_input(
|
609 |
"السعر",
|
|
|
611 |
step=10.0,
|
612 |
key=f"labor_price_{i}"
|
613 |
)
|
614 |
+
|
615 |
if labor_name and labor_unit and labor_quantity > 0 and labor_price > 0:
|
616 |
labor_items.append({
|
617 |
"name": labor_name,
|
|
|
620 |
"price": labor_price,
|
621 |
"total": labor_quantity * labor_price
|
622 |
})
|
623 |
+
|
624 |
# مقاولي الباطن
|
625 |
st.markdown("##### مقاولي الباطن")
|
626 |
subcontractors_col1, subcontractors_col2 = st.columns(2)
|
627 |
+
|
628 |
with subcontractors_col1:
|
629 |
subcontractors_percentage = st.slider(
|
630 |
"نسبة مقاولي الباطن من سعر الوحدة",
|
|
|
635 |
format="%g%%",
|
636 |
key="subcontractors_percentage"
|
637 |
) * 100
|
638 |
+
|
639 |
with subcontractors_col2:
|
640 |
subcontractors_amount = st.number_input(
|
641 |
"قيمة مقاولي الباطن (ريال)",
|
|
|
644 |
step=10.0,
|
645 |
key="subcontractors_amount"
|
646 |
)
|
647 |
+
|
648 |
# إضافة مقاولي الباطن
|
649 |
subcontractors_items = []
|
650 |
+
|
651 |
st.markdown("إضافة مقاولي الباطن")
|
652 |
+
|
653 |
for i in range(2): # السماح بإضافة 2 مقاول باطن كحد أقصى
|
654 |
subcontractor_col1, subcontractor_col2, subcontractor_col3 = st.columns([4, 1, 1])
|
655 |
+
|
656 |
with subcontractor_col1:
|
657 |
subcontractor_name = st.text_input(
|
658 |
"اسم مقاول الباطن",
|
659 |
key=f"subcontractor_name_{i}"
|
660 |
)
|
661 |
+
|
662 |
with subcontractor_col2:
|
663 |
subcontractor_work = st.text_input(
|
664 |
"نوع العمل",
|
665 |
key=f"subcontractor_work_{i}"
|
666 |
)
|
667 |
+
|
668 |
with subcontractor_col3:
|
669 |
subcontractor_price = st.number_input(
|
670 |
"السعر",
|
|
|
672 |
step=10.0,
|
673 |
key=f"subcontractor_price_{i}"
|
674 |
)
|
675 |
+
|
676 |
if subcontractor_name and subcontractor_work and subcontractor_price > 0:
|
677 |
subcontractors_items.append({
|
678 |
"name": subcontractor_name,
|
679 |
"work": subcontractor_work,
|
680 |
"price": subcontractor_price
|
681 |
})
|
682 |
+
|
683 |
# التكاليف غير المباشرة
|
684 |
st.markdown("##### التكاليف غير المباشرة")
|
685 |
+
|
686 |
# استخراج نسب التكاليف غير المباشرة
|
687 |
indirect_costs = st.session_state.smart_price_analysis["indirect_costs"]
|
688 |
+
|
689 |
indirect_col1, indirect_col2, indirect_col3 = st.columns(3)
|
690 |
+
|
691 |
with indirect_col1:
|
692 |
overhead_percentage = st.slider(
|
693 |
"نسبة المصاريف العمومية والإدارية",
|
|
|
698 |
format="%g%%",
|
699 |
key="overhead_percentage"
|
700 |
) * 100
|
701 |
+
|
702 |
with indirect_col2:
|
703 |
profit_percentage = st.slider(
|
704 |
"نسبة الربح",
|
|
|
709 |
format="%g%%",
|
710 |
key="profit_percentage"
|
711 |
) * 100
|
712 |
+
|
713 |
with indirect_col3:
|
714 |
contingency_percentage = st.slider(
|
715 |
"نسبة الطوارئ",
|
|
|
720 |
format="%g%%",
|
721 |
key="contingency_percentage"
|
722 |
) * 100
|
723 |
+
|
724 |
# زر حفظ التحليل
|
725 |
submit_button = st.form_submit_button("حفظ التحليل")
|
726 |
+
|
727 |
if submit_button:
|
728 |
# التحقق من صحة البيانات
|
729 |
total_percentage = (materials_percentage + equipment_percentage + labor_percentage + subcontractors_percentage) / 100
|
730 |
+
|
731 |
if abs(total_percentage - 1.0) > 0.01:
|
732 |
st.error("مجموع نسب المكونات يجب أن يساوي 100%")
|
733 |
else:
|
|
|
736 |
equipment_total = sum([item["total"] for item in equipment_items]) if equipment_items else equipment_amount
|
737 |
labor_total = sum([item["total"] for item in labor_items]) if labor_items else labor_amount
|
738 |
subcontractors_total = sum([item["price"] for item in subcontractors_items]) if subcontractors_items else subcontractors_amount
|
739 |
+
|
740 |
# حساب التكاليف المباشرة
|
741 |
direct_cost = materials_total + equipment_total + labor_total + subcontractors_total
|
742 |
+
|
743 |
# حساب التكاليف غير المباشرة
|
744 |
overhead_amount = direct_cost * (overhead_percentage / 100)
|
745 |
profit_amount = direct_cost * (profit_percentage / 100)
|
746 |
contingency_amount = direct_cost * (contingency_percentage / 100)
|
747 |
+
|
748 |
# حساب إجمالي التكاليف
|
749 |
total_cost = direct_cost + overhead_amount + profit_amount + contingency_amount
|
750 |
+
|
751 |
# إنشاء مكونات البند
|
752 |
components = {
|
753 |
"materials": {
|
|
|
788 |
"total_cost": total_cost,
|
789 |
"analysis_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
790 |
}
|
791 |
+
|
792 |
# تحديث البند في جدول الكميات
|
793 |
boq_items = st.session_state.boq_items
|
794 |
item_index = boq_items[boq_items["id"] == item["id"]].index[0]
|
795 |
+
|
796 |
boq_items.at[item_index, "analyzed"] = True
|
797 |
boq_items.at[item_index, "components"] = components
|
798 |
+
|
799 |
# تحديث حالة الجلسة
|
800 |
st.session_state.boq_items = boq_items
|
801 |
+
|
802 |
# إضافة التحليل إلى سجل التحليلات
|
803 |
analysis_history = st.session_state.smart_price_analysis["analysis_history"]
|
804 |
analysis_history.append({
|
|
|
808 |
"components": components,
|
809 |
"analysis_date": components["analysis_date"]
|
810 |
})
|
811 |
+
|
812 |
st.session_state.smart_price_analysis["analysis_history"] = analysis_history
|
813 |
+
|
814 |
# إعادة تعيين البند الحالي
|
815 |
st.session_state.smart_price_analysis["current_item"] = None
|
816 |
+
|
817 |
# عرض رسالة نجاح
|
818 |
st.success(f"تم تحليل البند {item['id']} بنجاح!")
|
819 |
+
|
820 |
# إعادة توجيه إلى صفحة التحليل
|
821 |
+
st.rerun()
|
822 |
+
|
823 |
def _display_item_components(self, item):
|
824 |
"""عرض مكونات البند"""
|
825 |
+
|
826 |
# استخراج مكونات البند
|
827 |
components = item["components"]
|
828 |
+
|
829 |
if not components:
|
830 |
st.warning("لم يتم تحليل هذا البند بعد")
|
831 |
return
|
832 |
+
|
833 |
# عرض ملخص التحليل
|
834 |
st.markdown("#### ملخص التحليل")
|
835 |
+
|
836 |
# عرض تاريخ التحليل
|
837 |
st.markdown(f"**تاريخ التحليل:** {components['analysis_date']}")
|
838 |
+
|
839 |
# عرض التكاليف المباشرة وغير المباشرة
|
840 |
col1, col2 = st.columns(2)
|
841 |
+
|
842 |
with col1:
|
843 |
+
st.markdown(f"**التكاليفالمباشرة:** {components['direct_cost']:.2f} ريال")
|
844 |
st.markdown(f"**التكاليف غير المباشرة:** {(components['total_cost'] - components['direct_cost']):.2f} ريال")
|
845 |
+
|
846 |
with col2:
|
847 |
st.markdown(f"**إجمالي التكاليف:** {components['total_cost']:.2f} ريال")
|
848 |
st.markdown(f"**سعر الوحدة:** {item['unit_price']:.2f} ريال")
|
849 |
+
|
850 |
# عرض نسب المكونات
|
851 |
st.markdown("#### نسب المكونات")
|
852 |
+
|
853 |
# إنشاء بيانات الرسم البياني
|
854 |
components_data = {
|
855 |
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
|
|
|
866 |
components["subcontractors"]["amount"]
|
867 |
]
|
868 |
}
|
869 |
+
|
870 |
# إنشاء رسم بياني دائري
|
871 |
fig = px.pie(
|
872 |
components_data,
|
|
|
876 |
color="المكون",
|
877 |
hover_data=["القيمة"]
|
878 |
)
|
879 |
+
|
880 |
st.plotly_chart(fig, use_container_width=True)
|
881 |
+
|
882 |
# عرض تفاصيل المكونات
|
883 |
st.markdown("#### تفاصيل المكونات")
|
884 |
+
|
885 |
# إنشاء تبويبات لعرض تفاصيل المكونات
|
886 |
component_tabs = st.tabs(["المواد", "المعدات", "العمالة", "مقاولي الباطن", "التكاليف غير المباشرة"])
|
887 |
+
|
888 |
with component_tabs[0]:
|
889 |
# عرض تفاصيل المواد
|
890 |
st.markdown("##### المواد")
|
891 |
st.markdown(f"**نسبة المواد:** {components['materials']['percentage'] * 100:.2f}%")
|
892 |
st.markdown(f"**قيمة المواد:** {components['materials']['amount']:.2f} ريال")
|
893 |
+
|
894 |
# عرض قائمة المواد
|
895 |
if components["materials"]["items"]:
|
896 |
materials_df = pd.DataFrame(components["materials"]["items"])
|
897 |
materials_df.columns = ["اسم المادة", "الوحدة", "الكمية", "السعر", "الإجمالي"]
|
898 |
+
|
899 |
st.dataframe(materials_df, use_container_width=True)
|
900 |
else:
|
901 |
st.info("لم يتم إضافة مواد محددة")
|
902 |
+
|
903 |
with component_tabs[1]:
|
904 |
# عرض تفاصيل المعدات
|
905 |
st.markdown("##### المعدات")
|
906 |
st.markdown(f"**نسبة المعدات:** {components['equipment']['percentage'] * 100:.2f}%")
|
907 |
st.markdown(f"**قيمة المعدات:** {components['equipment']['amount']:.2f} ريال")
|
908 |
+
|
909 |
# عرض قائمة المعدات
|
910 |
if components["equipment"]["items"]:
|
911 |
equipment_df = pd.DataFrame(components["equipment"]["items"])
|
912 |
equipment_df.columns = ["اسم المعدة", "الوحدة", "الكمية", "السعر", "الإجمالي"]
|
913 |
+
|
914 |
st.dataframe(equipment_df, use_container_width=True)
|
915 |
else:
|
916 |
st.info("لم يتم إضافة معدات محددة")
|
917 |
+
|
918 |
with component_tabs[2]:
|
919 |
# عرض تفاصيل العمالة
|
920 |
st.markdown("##### العمالة")
|
921 |
st.markdown(f"**نسبة العمالة:** {components['labor']['percentage'] * 100:.2f}%")
|
922 |
st.markdown(f"**قيمة العمالة:** {components['labor']['amount']:.2f} ريال")
|
923 |
+
|
924 |
# عرض قائمة العمالة
|
925 |
if components["labor"]["items"]:
|
926 |
labor_df = pd.DataFrame(components["labor"]["items"])
|
927 |
labor_df.columns = ["المسمى الوظيفي", "الوحدة", "الكمية", "السعر", "الإجمالي"]
|
928 |
+
|
929 |
st.dataframe(labor_df, use_container_width=True)
|
930 |
else:
|
931 |
st.info("لم يتم إضافة عمالة محددة")
|
932 |
+
|
933 |
with component_tabs[3]:
|
934 |
# عرض تفاصيل مقاولي الباطن
|
935 |
st.markdown("##### مقاولي الباطن")
|
936 |
st.markdown(f"**نسبة مقاولي الباطن:** {components['subcontractors']['percentage'] * 100:.2f}%")
|
937 |
st.markdown(f"**قيمة مقاولي الباطن:** {components['subcontractors']['amount']:.2f} ريال")
|
938 |
+
|
939 |
# عرض قائمة مقاولي الباطن
|
940 |
if components["subcontractors"]["items"]:
|
941 |
subcontractors_df = pd.DataFrame(components["subcontractors"]["items"])
|
942 |
subcontractors_df.columns = ["اسم مقاول الباطن", "نوع العمل", "السعر"]
|
943 |
+
|
944 |
st.dataframe(subcontractors_df, use_container_width=True)
|
945 |
else:
|
946 |
st.info("لم يتم إضافة مقاولي باطن محددين")
|
947 |
+
|
948 |
with component_tabs[4]:
|
949 |
# عرض تفاصيل التكاليف غير المباشرة
|
950 |
st.markdown("##### التكاليف غير المباشرة")
|
951 |
+
|
952 |
# إنشاء بيانات التكاليف غير المباشرة
|
953 |
indirect_costs = components["indirect_costs"]
|
954 |
+
|
955 |
indirect_data = {
|
956 |
"البند": ["المصاريف العمومية والإدارية", "الربح", "الطوارئ"],
|
957 |
"النسبة": [
|
|
|
965 |
indirect_costs["contingency"]["amount"]
|
966 |
]
|
967 |
}
|
968 |
+
|
969 |
# إنشاء جدول للعرض
|
970 |
indirect_df = pd.DataFrame(indirect_data)
|
971 |
+
|
972 |
st.dataframe(indirect_df, use_container_width=True)
|
973 |
+
|
974 |
# إنشاء رسم بياني للتكاليف غير المباشرة
|
975 |
fig = px.bar(
|
976 |
indirect_df,
|
|
|
980 |
color="البند",
|
981 |
text_auto=True
|
982 |
)
|
983 |
+
|
984 |
st.plotly_chart(fig, use_container_width=True)
|
985 |
+
|
986 |
def _render_analysis_settings_tab(self):
|
987 |
"""عرض تبويب إعدادات التحليل"""
|
988 |
+
|
989 |
st.markdown("### إعدادات التحليل الذكي للأسعار")
|
990 |
+
|
991 |
# استخراج إعدادات التحليل
|
992 |
price_components = st.session_state.smart_price_analysis["price_components"]
|
993 |
indirect_costs = st.session_state.smart_price_analysis["indirect_costs"]
|
994 |
productivity_factors = st.session_state.smart_price_analysis["productivity_factors"]
|
995 |
+
|
996 |
# إنشاء نموذج إعدادات التحليل
|
997 |
with st.form("analysis_settings_form"):
|
998 |
st.markdown("#### نسب مكونات السعر الافتراضية")
|
999 |
+
|
1000 |
# نسب مكونات السعر
|
1001 |
components_col1, components_col2 = st.columns(2)
|
1002 |
+
|
1003 |
with components_col1:
|
1004 |
materials_percentage = st.slider(
|
1005 |
"نسبة المواد من سعر الوحدة",
|
|
|
1010 |
format="%g%%",
|
1011 |
key="settings_materials_percentage"
|
1012 |
) * 100
|
1013 |
+
|
1014 |
equipment_percentage = st.slider(
|
1015 |
"نسبة المعدات من سعر الوحدة",
|
1016 |
min_value=0.0,
|
|
|
1020 |
format="%g%%",
|
1021 |
key="settings_equipment_percentage"
|
1022 |
) * 100
|
1023 |
+
|
1024 |
with components_col2:
|
1025 |
labor_percentage = st.slider(
|
1026 |
"نسبة العمالة من سعر الوحدة",
|
|
|
1031 |
format="%g%%",
|
1032 |
key="settings_labor_percentage"
|
1033 |
) * 100
|
1034 |
+
|
1035 |
subcontractors_percentage = st.slider(
|
1036 |
"نسبة مقاولي الباطن من سعر الوحدة",
|
1037 |
min_value=0.0,
|
|
|
1041 |
format="%g%%",
|
1042 |
key="settings_subcontractors_percentage"
|
1043 |
) * 100
|
1044 |
+
|
1045 |
# التكاليف غير المباشرة
|
1046 |
st.markdown("#### نسب التكاليف غير المباشرة الافتراضية")
|
1047 |
+
|
1048 |
indirect_col1, indirect_col2, indirect_col3 = st.columns(3)
|
1049 |
+
|
1050 |
with indirect_col1:
|
1051 |
overhead_percentage = st.slider(
|
1052 |
"نسبة المصاريف العمومية والإدارية",
|
|
|
1057 |
format="%g%%",
|
1058 |
key="settings_overhead_percentage"
|
1059 |
) * 100
|
1060 |
+
|
1061 |
with indirect_col2:
|
1062 |
profit_percentage = st.slider(
|
1063 |
"نسبة الربح",
|
|
|
1068 |
format="%g%%",
|
1069 |
key="settings_profit_percentage"
|
1070 |
) * 100
|
1071 |
+
|
1072 |
with indirect_col3:
|
1073 |
contingency_percentage = st.slider(
|
1074 |
"نسبة الطوارئ",
|
|
|
1079 |
format="%g%%",
|
1080 |
key="settings_contingency_percentage"
|
1081 |
) * 100
|
1082 |
+
|
1083 |
# عوامل الإنتاجية
|
1084 |
st.markdown("#### عوامل الإنتاجية")
|
1085 |
+
|
1086 |
productivity_col1, productivity_col2 = st.columns(2)
|
1087 |
+
|
1088 |
with productivity_col1:
|
1089 |
weather_factor = st.slider(
|
1090 |
"عامل الطقس",
|
|
|
1094 |
step=0.1,
|
1095 |
key="settings_weather_factor"
|
1096 |
)
|
1097 |
+
|
1098 |
location_factor = st.slider(
|
1099 |
"عامل الموقع",
|
1100 |
min_value=0.5,
|
|
|
1103 |
step=0.1,
|
1104 |
key="settings_location_factor"
|
1105 |
)
|
1106 |
+
|
1107 |
complexity_factor = st.slider(
|
1108 |
"عامل التعقيد",
|
1109 |
min_value=0.5,
|
|
|
1112 |
step=0.1,
|
1113 |
key="settings_complexity_factor"
|
1114 |
)
|
1115 |
+
|
1116 |
with productivity_col2:
|
1117 |
schedule_factor = st.slider(
|
1118 |
"عامل الجدول الزمني",
|
|
|
1122 |
step=0.1,
|
1123 |
key="settings_schedule_factor"
|
1124 |
)
|
1125 |
+
|
1126 |
resources_factor = st.slider(
|
1127 |
"عامل الموارد",
|
1128 |
min_value=0.5,
|
|
|
1131 |
step=0.1,
|
1132 |
key="settings_resources_factor"
|
1133 |
)
|
1134 |
+
|
1135 |
# زر حفظ الإعدادات
|
1136 |
submit_button = st.form_submit_button("حفظ الإعدادات")
|
1137 |
+
|
1138 |
if submit_button:
|
1139 |
# التحقق من صحة البيانات
|
1140 |
total_percentage = (materials_percentage + equipment_percentage + labor_percentage + subcontractors_percentage) / 100
|
1141 |
+
|
1142 |
if abs(total_percentage - 1.0) > 0.01:
|
1143 |
st.error("مجموع نسب المكونات يجب أن يساوي 100%")
|
1144 |
else:
|
|
|
1147 |
price_components["equipment"] = equipment_percentage / 100
|
1148 |
price_components["labor"] = labor_percentage / 100
|
1149 |
price_components["subcontractors"] = subcontractors_percentage / 100
|
1150 |
+
|
1151 |
# تحديث نسب التكاليف غير المباشرة
|
1152 |
indirect_costs["overhead"] = overhead_percentage / 100
|
1153 |
indirect_costs["profit"] = profit_percentage / 100
|
1154 |
indirect_costs["contingency"] = contingency_percentage / 100
|
1155 |
+
|
1156 |
# تحديث عوامل الإنتاجية
|
1157 |
productivity_factors["weather"] = weather_factor
|
1158 |
productivity_factors["location"] = location_factor
|
1159 |
productivity_factors["complexity"] = complexity_factor
|
1160 |
productivity_factors["schedule"] = schedule_factor
|
1161 |
productivity_factors["resources"] = resources_factor
|
1162 |
+
|
1163 |
# تحديث حالة الجلسة
|
1164 |
st.session_state.smart_price_analysis["price_components"] = price_components
|
1165 |
st.session_state.smart_price_analysis["indirect_costs"] = indirect_costs
|
1166 |
st.session_state.smart_price_analysis["productivity_factors"] = productivity_factors
|
1167 |
+
|
1168 |
# عرض رسالة نجاح
|
1169 |
st.success("تم حفظ إعدادات التحليل بنجاح!")
|
1170 |
+
|
1171 |
# عرض الإعدادات الحالية
|
1172 |
st.markdown("### الإعدادات الحالية")
|
1173 |
+
|
1174 |
# عرض نسب مكونات السعر
|
1175 |
st.markdown("#### نسب مكونات السعر")
|
1176 |
+
|
1177 |
# إنشاء بيانات الرسم البياني
|
1178 |
components_data = {
|
1179 |
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
|
|
|
1184 |
price_components["subcontractors"] * 100
|
1185 |
]
|
1186 |
}
|
1187 |
+
|
1188 |
# إنشاء رسم بياني دائري
|
1189 |
fig = px.pie(
|
1190 |
components_data,
|
|
|
1193 |
title="توزيع مكونات سعر الوحدة",
|
1194 |
color="المكون"
|
1195 |
)
|
1196 |
+
|
1197 |
st.plotly_chart(fig, use_container_width=True)
|
1198 |
+
|
1199 |
# عرض نسب التكاليف غير المباشرة
|
1200 |
st.markdown("#### نسب التكاليف غير المباشرة")
|
1201 |
+
|
1202 |
# إنشاء بيانات الرسم البياني
|
1203 |
indirect_data = {
|
1204 |
"البند": ["المصاريف العمومية والإدارية", "الربح", "الطوارئ"],
|
|
|
1208 |
indirect_costs["contingency"] * 100
|
1209 |
]
|
1210 |
}
|
1211 |
+
|
1212 |
# إنشاء رسم بياني شريطي
|
1213 |
fig = px.bar(
|
1214 |
indirect_data,
|
|
|
1218 |
color="البند",
|
1219 |
text_auto=True
|
1220 |
)
|
1221 |
+
|
1222 |
st.plotly_chart(fig, use_container_width=True)
|
1223 |
+
|
1224 |
# عرض عوامل الإنتاجية
|
1225 |
st.markdown("#### عوامل الإنتاجية")
|
1226 |
+
|
1227 |
# إنشاء بيانات الرسم البياني
|
1228 |
productivity_data = {
|
1229 |
"العامل": ["الطقس", "الموقع", "التعقيد", "الجدول الزمني", "الموارد"],
|
|
|
1235 |
productivity_factors["resources"]
|
1236 |
]
|
1237 |
}
|
1238 |
+
|
1239 |
# إنشاء رسم بياني شريطي
|
1240 |
fig = px.bar(
|
1241 |
productivity_data,
|
|
|
1245 |
color="العامل",
|
1246 |
text_auto=True
|
1247 |
)
|
1248 |
+
|
1249 |
# إضافة خط أفقي عند القيمة 1.0
|
1250 |
fig.add_shape(
|
1251 |
type="line",
|
|
|
1259 |
dash="dash"
|
1260 |
)
|
1261 |
)
|
1262 |
+
|
1263 |
st.plotly_chart(fig, use_container_width=True)
|
1264 |
+
|
1265 |
def _render_analysis_reports_tab(self):
|
1266 |
"""عرض تبويب تقارير التحليل"""
|
1267 |
+
|
1268 |
st.markdown("### تقارير التحليل الذكي للأسعار")
|
1269 |
+
|
1270 |
# استخراج البيانات
|
1271 |
boq_items = st.session_state.boq_items
|
1272 |
analysis_history = st.session_state.smart_price_analysis["analysis_history"]
|
1273 |
+
|
1274 |
# عرض ملخص التحليل
|
1275 |
st.markdown("#### ملخص التحليل")
|
1276 |
+
|
1277 |
# حساب عدد البنود المحللة وغير المحللة
|
1278 |
analyzed_count = len(boq_items[boq_items["analyzed"] == True])
|
1279 |
not_analyzed_count = len(boq_items[boq_items["analyzed"] == False])
|
1280 |
+
|
1281 |
# عرض نسبة التحليل
|
1282 |
analysis_percentage = analyzed_count / len(boq_items) * 100 if len(boq_items) > 0 else 0
|
1283 |
+
|
1284 |
st.markdown(f"**عدد البنود المحللة:** {analyzed_count} من أصل {len(boq_items)} ({analysis_percentage:.2f}%)")
|
1285 |
+
|
1286 |
# إنشاء مؤشر التقدم
|
1287 |
st.progress(analysis_percentage / 100)
|
1288 |
+
|
1289 |
# عرض توزيع البنود المحللة حسب الفئة
|
1290 |
st.markdown("#### توزيع البنود المحللة حسب الفئة")
|
1291 |
+
|
1292 |
# حساب عدد البنود المحللة لكل فئة
|
1293 |
category_analysis = boq_items.groupby(["category", "analyzed"]).size().unstack(fill_value=0).reset_index()
|
1294 |
+
|
1295 |
if True in category_analysis.columns:
|
1296 |
category_analysis.columns = ["الفئة", "غير محلل", "محلل"]
|
1297 |
+
|
1298 |
# إنشاء رسم بياني شريطي
|
1299 |
fig = px.bar(
|
1300 |
category_analysis,
|
|
|
1304 |
barmode="stack",
|
1305 |
color_discrete_map={"محلل": "green", "غير محلل": "red"}
|
1306 |
)
|
1307 |
+
|
1308 |
st.plotly_chart(fig, use_container_width=True)
|
1309 |
else:
|
1310 |
st.info("لا يوجد بنود محللة بعد")
|
1311 |
+
|
1312 |
# عرض توزيع مكونات الأسعار
|
1313 |
st.markdown("#### توزيع مكونات الأسعار")
|
1314 |
+
|
1315 |
# التحقق من وجود بنود محللة
|
1316 |
if analyzed_count > 0:
|
1317 |
# استخراج البنود المحللة
|
1318 |
analyzed_items = boq_items[boq_items["analyzed"] == True]
|
1319 |
+
|
1320 |
# إنشاء قائمة لتخزين بيانات المكونات
|
1321 |
components_data = []
|
1322 |
+
|
1323 |
# استخراج بيانات المكونات
|
1324 |
for _, item in analyzed_items.iterrows():
|
1325 |
components = item["components"]
|
1326 |
+
|
1327 |
components_data.append({
|
1328 |
"id": item["id"],
|
1329 |
"description": item["description"],
|
|
|
1337 |
"labor_amount": components["labor"]["amount"],
|
1338 |
"subcontractors_amount": components["subcontractors"]["amount"]
|
1339 |
})
|
1340 |
+
|
1341 |
# إنشاء DataFrame
|
1342 |
components_df = pd.DataFrame(components_data)
|
1343 |
+
|
1344 |
# حساب متوسط النسب
|
1345 |
avg_materials_percentage = components_df["materials_percentage"].mean() * 100
|
1346 |
avg_equipment_percentage = components_df["equipment_percentage"].mean() * 100
|
1347 |
avg_labor_percentage = components_df["labor_percentage"].mean() * 100
|
1348 |
avg_subcontractors_percentage = components_df["subcontractors_percentage"].mean() * 100
|
1349 |
+
|
1350 |
# عرض متوسط النسب
|
1351 |
st.markdown("##### متوسط نسب المكونات")
|
1352 |
+
|
1353 |
avg_components_data = {
|
1354 |
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
|
1355 |
"النسبة": [
|
|
|
1359 |
avg_subcontractors_percentage
|
1360 |
]
|
1361 |
}
|
1362 |
+
|
1363 |
# إنشاء رسم بياني دائري
|
1364 |
fig = px.pie(
|
1365 |
avg_components_data,
|
|
|
1368 |
title="متوسط نسب مكونات الأسعار",
|
1369 |
color="المكون"
|
1370 |
)
|
1371 |
+
|
1372 |
st.plotly_chart(fig, use_container_width=True)
|
1373 |
+
|
1374 |
# عرض توزيع النسب حسب البند
|
1375 |
st.markdown("##### توزيع نسب المكونات حسب البند")
|
1376 |
+
|
1377 |
# إنشاء بيانات للرسم البياني
|
1378 |
item_components_data = []
|
1379 |
+
|
1380 |
for _, row in components_df.iterrows():
|
1381 |
item_components_data.extend([
|
1382 |
{"البند": row["id"], "المكون": "المواد", "النسبة": row["materials_percentage"] * 100},
|
|
|
1384 |
{"البند": row["id"], "المكون": "العمالة", "النسبة": row["labor_percentage"] * 100},
|
1385 |
{"البند": row["id"], "المكون": "مقاولي الباطن", "النسبة": row["subcontractors_percentage"] * 100}
|
1386 |
])
|
1387 |
+
|
1388 |
# إنشاء DataFrame
|
1389 |
item_components_df = pd.DataFrame(item_components_data)
|
1390 |
+
|
1391 |
# إنشاء رسم بياني شريطي
|
1392 |
fig = px.bar(
|
1393 |
item_components_df,
|
|
|
1397 |
title="توزيع نسب المكونات ح��ب البند",
|
1398 |
barmode="stack"
|
1399 |
)
|
1400 |
+
|
1401 |
st.plotly_chart(fig, use_container_width=True)
|
1402 |
+
|
1403 |
# عرض مقارنة أسعار الوحدة
|
1404 |
st.markdown("##### مقارنة أسعار الوحدة")
|
1405 |
+
|
1406 |
# إنشاء بيانات للرسم البياني
|
1407 |
unit_price_data = []
|
1408 |
+
|
1409 |
for _, row in components_df.iterrows():
|
1410 |
unit_price_data.extend([
|
1411 |
{"البند": row["id"], "المكون": "المواد", "القيمة": row["materials_amount"]},
|
|
|
1413 |
{"البند": row["id"], "المكون": "العمالة", "القيمة": row["labor_amount"]},
|
1414 |
{"البند": row["id"], "المكون": "مقاولي الباطن", "القيمة": row["subcontractors_amount"]}
|
1415 |
])
|
1416 |
+
|
1417 |
# إنشاء DataFrame
|
1418 |
unit_price_df = pd.DataFrame(unit_price_data)
|
1419 |
+
|
1420 |
# إنشاء رسم بياني شريطي
|
1421 |
fig = px.bar(
|
1422 |
unit_price_df,
|
|
|
1426 |
title="مقارنة مكونات أسعار الوحدة",
|
1427 |
barmode="stack"
|
1428 |
)
|
1429 |
+
|
1430 |
# إضافة خط لسعر الوحدة
|
1431 |
for i, row in components_df.iterrows():
|
1432 |
fig.add_shape(
|
|
|
1441 |
dash="dash"
|
1442 |
)
|
1443 |
)
|
1444 |
+
|
1445 |
st.plotly_chart(fig, use_container_width=True)
|
1446 |
+
self.render_cost_breakdown() #Added this line
|
1447 |
else:
|
1448 |
st.info("لا يوجد بنود محللة بعد")
|
1449 |
+
|
1450 |
# عرض سجل التحليلات
|
1451 |
st.markdown("#### سجل التحليلات")
|
1452 |
+
|
1453 |
if analysis_history:
|
1454 |
# عرض عدد التحليلات
|
1455 |
st.markdown(f"**عدد التحليلات:** {len(analysis_history)}")
|
1456 |
+
|
1457 |
# عرض آخر 5 تحليلات
|
1458 |
st.markdown("##### آخر 5 تحليلات")
|
1459 |
+
|
1460 |
for i, analysis in enumerate(analysis_history[-5:]):
|
1461 |
st.markdown(f"**{i+1}. البند:** {analysis['item_id']} - {analysis['item_description']}")
|
1462 |
st.markdown(f"**تاريخ التحليل:** {analysis['analysis_date']}")
|
|
|
1464 |
st.markdown("---")
|
1465 |
else:
|
1466 |
st.info("لا يوجد سجل تحليلات بعد")
|
1467 |
+
|
1468 |
def _render_local_content_tab(self):
|
1469 |
"""عرض تبويب المحتوى المحلي"""
|
1470 |
+
|
1471 |
st.markdown("### تحليل المحتوى المحلي")
|
1472 |
+
|
1473 |
# استخراج بيانات المحتوى المحلي
|
1474 |
local_content = st.session_state.smart_price_analysis["local_content"]
|
1475 |
+
|
1476 |
# إنشاء نموذج إعدادات المحتوى المحلي
|
1477 |
with st.form("local_content_form"):
|
1478 |
st.markdown("#### إعدادات المحتوى المحلي")
|
1479 |
+
|
1480 |
# النسبة المستهدفة للمحتوى المحلي
|
1481 |
target_percentage = st.slider(
|
1482 |
"النسبة المستهدفة للمحتوى المحلي",
|
|
|
1487 |
format="%g%%",
|
1488 |
key="local_content_target"
|
1489 |
) * 100
|
1490 |
+
|
1491 |
# نسب المحتوى المحلي لكل مكون
|
1492 |
st.markdown("#### نسب المحتوى المحلي لكل مكون")
|
1493 |
+
|
1494 |
local_col1, local_col2 = st.columns(2)
|
1495 |
+
|
1496 |
with local_col1:
|
1497 |
materials_local = st.slider(
|
1498 |
"نسبة المواد المحلية",
|
|
|
1503 |
format="%g%%",
|
1504 |
key="materials_local"
|
1505 |
) * 100
|
1506 |
+
|
1507 |
equipment_local = st.slider(
|
1508 |
"نسبة المعدات المحلية",
|
1509 |
min_value=0.0,
|
|
|
1513 |
format="%g%%",
|
1514 |
key="equipment_local"
|
1515 |
) * 100
|
1516 |
+
|
1517 |
with local_col2:
|
1518 |
labor_local = st.slider(
|
1519 |
"نسبة العمالة المحلية",
|
|
|
1524 |
format="%g%%",
|
1525 |
key="labor_local"
|
1526 |
) * 100
|
1527 |
+
|
1528 |
subcontractors_local = st.slider(
|
1529 |
"نسبة مقاولي الباطن المحليين",
|
1530 |
min_value=0.0,
|
|
|
1534 |
format="%g%%",
|
1535 |
key="subcontractors_local"
|
1536 |
) * 100
|
1537 |
+
|
1538 |
# زر حفظ الإعدادات
|
1539 |
submit_button = st.form_submit_button("حفظ إعدادات المحتوى المحلي")
|
1540 |
+
|
1541 |
if submit_button:
|
1542 |
# تحديث إعدادات المحتوى المحلي
|
1543 |
local_content["target"] = target_percentage / 100
|
|
|
1545 |
local_content["equipment_local"] = equipment_local / 100
|
1546 |
local_content["labor_local"] = labor_local / 100
|
1547 |
local_content["subcontractors_local"] = subcontractors_local / 100
|
1548 |
+
|
1549 |
# تحديث حالة الجلسة
|
1550 |
st.session_state.smart_price_analysis["local_content"] = local_content
|
1551 |
+
|
1552 |
# عرض رسالة نجاح
|
1553 |
st.success("تم حفظ إعدادات المحتوى المحلي بنجاح!")
|
1554 |
+
|
1555 |
# حساب نسبة المحتوى المحلي الفعلية
|
1556 |
st.markdown("#### حساب نسبة المحتوى المحلي الفعلية")
|
1557 |
+
|
1558 |
# استخراج نسب مكونات السعر
|
1559 |
price_components = st.session_state.smart_price_analysis["price_components"]
|
1560 |
+
|
1561 |
# حساب نسبة المحتوى المحلي الفعلية
|
1562 |
actual_local_content = (
|
1563 |
price_components["materials"] * local_content["materials_local"] +
|
|
|
1565 |
price_components["labor"] * local_content["labor_local"] +
|
1566 |
price_components["subcontractors"] * local_content["subcontractors_local"]
|
1567 |
)
|
1568 |
+
|
1569 |
# عرض نسبة المحتوى المحلي الفعلية
|
1570 |
st.markdown(f"**نسبة المحتوى المحلي الفعلية:** {actual_local_content * 100:.2f}%")
|
1571 |
st.markdown(f"**النسبة المستهدفة للمحتوى المحلي:** {local_content['target'] * 100:.2f}%")
|
1572 |
+
|
1573 |
# عرض مؤشر التقدم
|
1574 |
progress_percentage = min(actual_local_content / local_content["target"], 1.0) if local_content["target"] > 0 else 0
|
1575 |
+
|
1576 |
st.progress(progress_percentage)
|
1577 |
+
|
1578 |
# عرض حالة المحتوى المحلي
|
1579 |
if actual_local_content >= local_content["target"]:
|
1580 |
st.success("تم تحقيق النسبة المستهدفة للمحتوى المحلي")
|
1581 |
else:
|
1582 |
st.warning("لم يتم تحقيق النسبة المستهدفة للمحتوى المحلي")
|
1583 |
+
|
1584 |
# عرض مساهمة كل مكون في المحتوى المحلي
|
1585 |
st.markdown("#### مساهمة كل مكون في المحتوى المحلي")
|
1586 |
+
|
1587 |
# حساب مساهمة كل مكون
|
1588 |
materials_contribution = price_components["materials"] * local_content["materials_local"]
|
1589 |
equipment_contribution = price_components["equipment"] * local_content["equipment_local"]
|
1590 |
labor_contribution = price_components["labor"] * local_content["labor_local"]
|
1591 |
subcontractors_contribution = price_components["subcontractors"] * local_content["subcontractors_local"]
|
1592 |
+
|
1593 |
# إنشاء بيانات الرسم البياني
|
1594 |
contribution_data = {
|
1595 |
"المكون": ["المواد", "المعدات", "العمالة", "مقاولي الباطن"],
|
|
|
1600 |
subcontractors_contribution * 100
|
1601 |
]
|
1602 |
}
|
1603 |
+
|
1604 |
# إنشاء رسم بياني شريطي
|
1605 |
fig = px.bar(
|
1606 |
contribution_data,
|
|
|
1610 |
color="المكون",
|
1611 |
text_auto=True
|
1612 |
)
|
1613 |
+
|
1614 |
st.plotly_chart(fig, use_container_width=True)
|
1615 |
+
|
1616 |
# عرض توصيات لتحسين نسبة المحتوى المحلي
|
1617 |
st.markdown("#### توصيات لتحسين نسبة المحتوى المحلي")
|
1618 |
+
|
1619 |
if actual_local_content < local_content["target"]:
|
1620 |
# حساب الفجوة
|
1621 |
gap = local_content["target"] - actual_local_content
|
1622 |
+
|
1623 |
st.markdown(f"**الفجوة الحالية:** {gap * 100:.2f}%")
|
1624 |
+
|
1625 |
# تحديد المكونات التي يمكن تحسينها
|
1626 |
components_to_improve = []
|
1627 |
+
|
1628 |
if local_content["materials_local"] < 1.0:
|
1629 |
components_to_improve.append({
|
1630 |
"name": "المواد",
|
|
|
1632 |
"weight": price_components["materials"],
|
1633 |
"potential": price_components["materials"] * (1.0 - local_content["materials_local"])
|
1634 |
})
|
1635 |
+
|
1636 |
if local_content["equipment_local"] < 1.0:
|
1637 |
components_to_improve.append({
|
1638 |
"name": "المعدات",
|
|
|
1640 |
"weight": price_components["equipment"],
|
1641 |
"potential": price_components["equipment"] * (1.0 - local_content["equipment_local"])
|
1642 |
})
|
1643 |
+
|
1644 |
if local_content["labor_local"] < 1.0:
|
1645 |
components_to_improve.append({
|
1646 |
"name": "العمالة",
|
|
|
1648 |
"weight": price_components["labor"],
|
1649 |
"potential": price_components["labor"] * (1.0 - local_content["labor_local"])
|
1650 |
})
|
1651 |
+
|
1652 |
if local_content["subcontractors_local"] < 1.0:
|
1653 |
components_to_improve.append({
|
1654 |
"name": "مقاولي الباطن",
|
|
|
1656 |
"weight": price_components["subcontractors"],
|
1657 |
"potential": price_components["subcontractors"] * (1.0 - local_content["subcontractors_local"])
|
1658 |
})
|
1659 |
+
|
1660 |
# ترتيب المكونات حسب إمكانية التحسين
|
1661 |
components_to_improve.sort(key=lambda x: x["potential"], reverse=True)
|
1662 |
+
|
1663 |
# عرض التوصيات
|
1664 |
for component in components_to_improve:
|
1665 |
st.markdown(f"**{component['name']}:** زيادة نسبة {component['name']} المحلية من {component['current'] * 100:.2f}% إلى {min(component['current'] + gap / component['weight'], 1.0) * 100:.2f}%")
|
1666 |
else:
|
1667 |
st.success("تم تحقيق النسبة المستهدفة للمحتوى المحلي")
|
1668 |
+
|
1669 |
def calculate_item_price(self, item_data):
|
1670 |
"""حساب سعر البند بناءً على مكوناته"""
|
1671 |
+
|
1672 |
# استخراج مكونات البند
|
1673 |
materials = item_data.get("materials", [])
|
1674 |
equipment = item_data.get("equipment", [])
|
1675 |
labor = item_data.get("labor", [])
|
1676 |
subcontractors = item_data.get("subcontractors", [])
|
1677 |
+
|
1678 |
# حساب تكلفة المواد
|
1679 |
materials_cost = sum([material["quantity"] * material["price"] for material in materials])
|
1680 |
+
|
1681 |
# حساب تكلفة المعدات
|
1682 |
equipment_cost = sum([equipment_item["quantity"] * equipment_item["price"] for equipment_item in equipment])
|
1683 |
+
|
1684 |
# حساب تكلفة العمالة
|
1685 |
labor_cost = sum([labor_item["quantity"] * labor_item["price"] for labor_item in labor])
|
1686 |
+
|
1687 |
# حساب تكلفة مقاولي الباطن
|
1688 |
subcontractors_cost = sum([subcontractor["price"] for subcontractor in subcontractors])
|
1689 |
+
|
1690 |
# حساب التكاليف المباشرة
|
1691 |
direct_cost = materials_cost + equipment_cost + labor_cost + subcontractors_cost
|
1692 |
+
|
1693 |
# استخراج نسب التكاليف غير المباشرة
|
1694 |
indirect_costs = st.session_state.smart_price_analysis["indirect_costs"]
|
1695 |
+
|
1696 |
# حساب التكاليف غير المباشرة
|
1697 |
overhead_amount = direct_cost * indirect_costs["overhead"]
|
1698 |
profit_amount = direct_cost * indirect_costs["profit"]
|
1699 |
contingency_amount = direct_cost * indirect_costs["contingency"]
|
1700 |
+
|
1701 |
# حساب إجمالي التكاليف
|
1702 |
total_cost = direct_cost + overhead_amount + profit_amount + contingency_amount
|
1703 |
+
|
1704 |
return total_cost
|
1705 |
+
|
1706 |
def calculate_local_content(self, item_data):
|
1707 |
"""حساب نسبة المحتوى المحلي للبند"""
|
1708 |
+
|
1709 |
# استخراج مكونات البند
|
1710 |
materials = item_data.get("materials", [])
|
1711 |
equipment = item_data.get("equipment", [])
|
1712 |
labor = item_data.get("labor", [])
|
1713 |
subcontractors = item_data.get("subcontractors", [])
|
1714 |
+
|
1715 |
# استخراج نسب المحتوى المحلي
|
1716 |
local_content = st.session_state.smart_price_analysis["local_content"]
|
1717 |
+
|
1718 |
# حساب تكلفة المواد
|
1719 |
materials_cost = sum([material["quantity"] * material["price"] for material in materials])
|
1720 |
+
|
1721 |
# حساب تكلفة المعدات
|
1722 |
equipment_cost = sum([equipment_item["quantity"] * equipment_item["price"] for equipment_item in equipment])
|
1723 |
+
|
1724 |
# حساب تكلفة العمالة
|
1725 |
labor_cost = sum([labor_item["quantity"] * labor_item["price"] for labor_item in labor])
|
1726 |
+
|
1727 |
# حساب تكلفة مقاولي الباطن
|
1728 |
subcontractors_cost = sum([subcontractor["price"] for subcontractor in subcontractors])
|
1729 |
+
|
1730 |
# حساب التكاليف المباشرة
|
1731 |
direct_cost = materials_cost + equipment_cost + labor_cost + subcontractors_cost
|
1732 |
+
|
1733 |
# حساب المحتوى المحلي
|
1734 |
local_materials = materials_cost * local_content["materials_local"]
|
1735 |
local_equipment = equipment_cost * local_content["equipment_local"]
|
1736 |
local_labor = labor_cost * local_content["labor_local"]
|
1737 |
local_subcontractors = subcontractors_cost * local_content["subcontractors_local"]
|
1738 |
+
|
1739 |
# حساب إجمالي المحتوى المحلي
|
1740 |
total_local_content = local_materials + local_equipment + local_labor + local_subcontractors
|
1741 |
+
|
1742 |
# حساب نسبة المحتوى المحلي
|
1743 |
local_content_percentage = total_local_content / direct_cost if direct_cost > 0 else 0
|
1744 |
+
|
1745 |
return local_content_percentage
|
1746 |
+
|
1747 |
+
def analyze_costs(self, items):
|
1748 |
+
"""تحليل التكاليف لبنود المشروع"""
|
1749 |
+
total_cost = sum(item['total_price'] for item in items)
|
1750 |
+
categories = {}
|
1751 |
+
|
1752 |
+
for item in items:
|
1753 |
+
if item['category'] not in categories:
|
1754 |
+
categories[item['category']] = 0
|
1755 |
+
categories[item['category']] += item['total_price']
|
1756 |
+
|
1757 |
+
return {
|
1758 |
+
'total_cost': total_cost,
|
1759 |
+
'categories': categories
|
1760 |
+
}
|
1761 |
+
|
1762 |
+
def render_cost_breakdown(self): #Added this function
|
1763 |
+
"""عرض تحليل التكاليف"""
|
1764 |
+
if 'bill_of_quantities' not in st.session_state:
|
1765 |
+
st.session_state.bill_of_quantities = []
|
1766 |
+
|
1767 |
+
if len(st.session_state.bill_of_quantities) > 0:
|
1768 |
+
analysis = self.analyze_costs(st.session_state.bill_of_quantities)
|
1769 |
+
|
1770 |
+
st.metric("إجمالي التكاليف", f"{analysis['total_cost']:,.2f} ريال")
|
1771 |
+
|
1772 |
+
# عرض التكاليف حسب الفئة
|
1773 |
+
st.subheader("التكاليف حسب الفئة")
|
1774 |
+
categories_df = pd.DataFrame([
|
1775 |
+
{"الفئة": cat, "التكلفة": cost}
|
1776 |
+
for cat, cost in analysis['categories'].items()
|
1777 |
+
])
|
1778 |
+
|
1779 |
+
if not categories_df.empty:
|
1780 |
+
fig = px.pie(
|
1781 |
+
categories_df,
|
1782 |
+
values="التكلفة",
|
1783 |
+
names="الفئة",
|
1784 |
+
title="توزيع التكاليف حسب الفئة"
|
1785 |
+
)
|
1786 |
+
st.plotly_chart(fig)
|
1787 |
+
else:
|
1788 |
+
st.warning("لا توجد بنود في جدول الكميات")
|
pricing_system/modules/catalogs/equipment_catalog.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
pricing_system/modules/catalogs/labor_catalog.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
pricing_system/modules/catalogs/materials_catalog.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
pricing_system/modules/catalogs/subcontractors_catalog.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
pricing_system/modules/indirect_support/overheads.py
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
pricing_system/modules/pricing_strategies/__init__.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
from .balanced_pricing import render_balanced_strategy, calculate_balanced_price
|
3 |
+
from .profit_oriented import render_profit_driven_strategy, calculate_profit_oriented_price
|
4 |
+
|
5 |
+
__all__ = [
|
6 |
+
'render_balanced_strategy',
|
7 |
+
'calculate_balanced_price',
|
8 |
+
'render_profit_driven_strategy',
|
9 |
+
'calculate_profit_oriented_price'
|
10 |
+
]
|
pricing_system/modules/pricing_strategies/balanced_pricing.py
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
"""
|
3 |
+
استراتيجية التسعير المتوازن
|
4 |
+
"""
|
5 |
+
import streamlit as st
|
6 |
+
import pandas as pd
|
7 |
+
|
8 |
+
def render_balanced_strategy():
|
9 |
+
st.markdown("### التسعير المتوازن")
|
10 |
+
|
11 |
+
strategies = [
|
12 |
+
"التسعير القياسي",
|
13 |
+
"التسعير المتزن",
|
14 |
+
"التسعير غير المتزن",
|
15 |
+
"التسعير الموجه للربحية",
|
16 |
+
"التسعير بالتجميع",
|
17 |
+
"التسعير بالمحتوى المحلي"
|
18 |
+
]
|
19 |
+
|
20 |
+
selected_strategy = st.selectbox(
|
21 |
+
"اختر استراتيجية التسعير",
|
22 |
+
strategies
|
23 |
+
)
|
24 |
+
|
25 |
+
# قراءة البنود من المشروع الحالي
|
26 |
+
if 'current_project' not in st.session_state:
|
27 |
+
st.warning("يرجى اختيار مشروع أولاً")
|
28 |
+
return
|
29 |
+
|
30 |
+
project = st.session_state.current_project
|
31 |
+
if not project:
|
32 |
+
st.info("لم يتم اختيار مشروع بعد. يرجى إدخال بيانات المشروع أولاً.")
|
33 |
+
return
|
34 |
+
|
35 |
+
boq_items = project.get('boq_items', [])
|
36 |
+
|
37 |
+
if not boq_items:
|
38 |
+
st.info("لا توجد بنود مضافة للمشروع بعد. يرجى إضافة البنود أولاً.")
|
39 |
+
return
|
40 |
+
|
41 |
+
# عرض تحليل البنود
|
42 |
+
st.markdown("#### تحليل بنود المشروع")
|
43 |
+
|
44 |
+
for i, item in enumerate(boq_items):
|
45 |
+
with st.expander(f"البند {i+1}: {item['description']}"):
|
46 |
+
col1, col2 = st.columns(2)
|
47 |
+
|
48 |
+
with col1:
|
49 |
+
st.write("معلومات البند:")
|
50 |
+
st.write(f"- الكود: {item['code']}")
|
51 |
+
st.write(f"- الوحدة: {item['unit']}")
|
52 |
+
st.write(f"- الكمية: {item['quantity']}")
|
53 |
+
st.write(f"- سعر الوحدة: {item['unit_price']} ريال")
|
54 |
+
|
55 |
+
with col2:
|
56 |
+
st.write("تحليل التكاليف:")
|
57 |
+
|
58 |
+
# تعديل سعر الوحدة
|
59 |
+
new_unit_price = st.number_input(
|
60 |
+
"سعر الوحدة الجديد",
|
61 |
+
min_value=0.0,
|
62 |
+
value=float(item['unit_price']),
|
63 |
+
key=f"unit_price_{selected_strategy}_{i}"
|
64 |
+
)
|
65 |
+
|
66 |
+
# تعديل الكمية
|
67 |
+
new_quantity = st.number_input(
|
68 |
+
"الكمية الجديدة",
|
69 |
+
min_value=0.0,
|
70 |
+
value=float(item['quantity']),
|
71 |
+
key=f"quantity_{selected_strategy}_{i}"
|
72 |
+
)
|
73 |
+
|
74 |
+
# زر تحديث البند
|
75 |
+
if st.button("تحديث البند", key=f"update_{selected_strategy}_{i}"):
|
76 |
+
item['unit_price'] = new_unit_price
|
77 |
+
item['quantity'] = new_quantity
|
78 |
+
item['total_price'] = new_unit_price * new_quantity
|
79 |
+
st.success("تم تحديث البند بنجاح")
|
80 |
+
st.rerun()
|
81 |
+
|
82 |
+
# زر حذف البند
|
83 |
+
if st.button("حذف البند", key=f"delete_{selected_strategy}_{i}"):
|
84 |
+
st.session_state.current_project['boq_items'].pop(i)
|
85 |
+
st.success("تم حذف البند بنجاح")
|
86 |
+
st.rerun()
|
87 |
+
|
88 |
+
def calculate_balanced_price(base_cost, overhead_ratio=0.15, profit_ratio=0.10):
|
89 |
+
"""حساب السعر المتوازن"""
|
90 |
+
overhead = base_cost * overhead_ratio
|
91 |
+
profit = base_cost * profit_ratio
|
92 |
+
return base_cost + overhead + profit
|