EGYADMIN commited on
Commit
fb20480
·
verified ·
1 Parent(s): edb2a3c

Upload 75 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +216 -13
  2. app.py +139 -0
  3. config.py +62 -0
  4. database/db_connector.py +180 -0
  5. database/models.py +279 -0
  6. demo_pricing.py +453 -0
  7. docs/technical_docs.md +165 -0
  8. docs/user_manual.md +373 -0
  9. huggingface_app.py +86 -0
  10. improved_app.py +144 -0
  11. models/README.md +52 -0
  12. models/datasets/README.md +61 -0
  13. models/trained/README.md +27 -0
  14. modules/ai_assistant/__init__.py +5 -0
  15. modules/ai_assistant/ai_assistant_app.py +0 -0
  16. modules/ai_models/ai_models_app.py +817 -0
  17. modules/document_analysis/document_analysis_app.py +1114 -0
  18. modules/document_analysis/services/__init__.py +22 -0
  19. modules/document_analysis/services/document_parser.py +219 -0
  20. modules/document_analysis/services/item_extractor.py +131 -0
  21. modules/document_analysis/services/text_extractor.py +105 -0
  22. modules/file_comparison/file_comparison_app.py +452 -0
  23. modules/maps/maps_app.py +249 -0
  24. modules/notifications/notifications_app.py +295 -0
  25. modules/pricing/constants.py +113 -0
  26. modules/pricing/exceptions.py +42 -0
  27. modules/pricing/price_analysis_component.py +932 -0
  28. modules/pricing/pricing_app.py +1312 -0
  29. modules/pricing/pricing_app.py.backup +1242 -0
  30. modules/pricing/services/construction_cost_calculator.py +1006 -0
  31. modules/pricing/services/construction_templates.py +748 -0
  32. modules/pricing/services/local_content_calculator.py +577 -0
  33. modules/pricing/services/price_prediction.py +444 -0
  34. modules/pricing/services/standard_pricing.py +232 -0
  35. modules/pricing/services/unbalanced_pricing.py +213 -0
  36. modules/pricing/specs_analyzer.py +527 -0
  37. modules/projects/projects_app.py +630 -0
  38. modules/reports/reports_app.py +88 -0
  39. modules/resources/__init__.py +5 -0
  40. modules/resources/resources_app.py +1410 -0
  41. modules/risk_analysis/risk_analysis_app.py +751 -0
  42. modules/services/item_extractor.py +124 -0
  43. modules/services/quantity_extractor.py +182 -0
  44. modules/services/risk_analyzer.py +219 -0
  45. modules/services/specs_analyzer.py +364 -0
  46. modules/services/text_extractor.py +90 -0
  47. modules/translation/translation_app.py +463 -0
  48. packages.txt +7 -0
  49. pyproject.toml +98 -0
  50. requirements.txt +60 -0
README.md CHANGED
@@ -1,13 +1,216 @@
1
- ---
2
- title: SA SAJCOAI
3
- emoji: 🐢
4
- colorFrom: yellow
5
- colorTo: indigo
6
- sdk: streamlit
7
- sdk_version: 1.44.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ title: نظام تحليل العقود والمناقصات بالذكاء الاصطناعي الاصدار 1
4
+ sdk: streamlit
5
+ emoji: 📊
6
+ colorFrom: green
7
+ colorTo: green
8
+ sdk_version: 1.44.0
9
+ ---
10
+ # نظام تحليل العقود والمناقصات بالذكاء الاصطناعي
11
+ ## شركة شبه الجزيرة للمقاولات
12
+
13
+ <p align="center">
14
+ <img src="https://imgg.io/images/2025/03/23/96dc2ab758cdd5e3267ff8221c2becc2.jpg" alt="شعار النظام" width="200"/>
15
+ <br>
16
+ <em>تحليل متقدم للمناقصات والعقود باستخدام الذكاء الاصطناعي</em>
17
+ </p>
18
+
19
+ ![إصدار النظام](https://img.shields.io/badge/الإصدار-1.0.0-blue)
20
+ ![حالة البناء](https://img.shields.io/badge/حالة_البناء-ناجح-success)
21
+ ![التوافق](https://img.shields.io/badge/التوافق-Hybrid_Face-orange)
22
+ ![الترخيص](https://img.shields.io/badge/الترخيص-خاص-red)
23
+
24
+ ## نبذة عن النظام
25
+
26
+ نظام متكامل يعتمد على تقنيات الذكاء الاصطناعي ومعالجة اللغة العربية الطبيعية لتحليل وتسعير المناقصات والعقود في قطاع المقاولات. تم تطويره خصيصاً لشركة شبه الجزيرة للمقاولات بهدف تحسين دقة التسعير وتعزيز القدرة التنافسية في المناقصات.
27
+
28
+ ### فريق التطوير
29
+
30
+ - **مطور النظام**: م. تامر الجوهري
31
+ - **مدير التطوير**: م. بدر وهبي
32
+ - **المستشار الفني**: م. إسلام عيسى - مدير المكتب الفني (القصيم)
33
+ - **مدير تكنولوجيا المعلومات**: م. إرشاد يعقوب
34
+
35
+ ## المتطلبات التقنية
36
+
37
+ ### متطلبات النظام
38
+
39
+ - **نظام التشغيل**: Windows 10/11، MacOS 12+، Linux Ubuntu 20.04+
40
+ - **المعالج**: Intel Core i5 أو ما يعادله (8 أنوية على الأقل)
41
+ - **الذاكرة**: 16GB RAM (يوصى بـ 32GB للمشاريع الكبيرة)
42
+ - **مساحة التخزين**: 10GB للنظام + مساحة إضافية للمستندات والبيانات
43
+ - **الشاشة**: دقة 1920×1080 أو أعلى
44
+
45
+ ### متطلبات البرمجيات
46
+
47
+ - **Python**: الإصدار 3.9 أو أحدث
48
+ - **بيئة Hybrid Face**: الإصدار 2.5 أو أحدث
49
+ - **قاعدة البيانات**: SQLite (للنشر المحلي) أو MySQL 8.0+ (للنشر المؤسسي)
50
+ - **متصفح**: Chrome 90+، Firefox 88+، Edge 90+
51
+
52
+ ## التثبيت والإعداد
53
+
54
+ ### 1. تثبيت بيئة Hybrid Face
55
+
56
+ ```bash
57
+ # تثبيت بيئة Hybrid Face
58
+ curl -sSL https://get.hybridface.io/install.sh | bash
59
+
60
+ # تفعيل البيئة
61
+ source ~/.hybridface/bin/activate
62
+ ```
63
+
64
+ ### 2. استنساخ المستودع
65
+
66
+ ```bash
67
+ # استنساخ المستودع
68
+ git clone https://gitlab.peninsula-contracting.com/ai-systems/tender-analysis-system.git
69
+ cd tender-analysis-system
70
+ ```
71
+
72
+ ### 3. تثبيت المتطلبات
73
+
74
+ ```bash
75
+ # تثبيت حزم Python المطلوبة
76
+ pip install -r requirements.txt
77
+
78
+ # تثبيت حزم لدعم اللغة العربية
79
+ pip install -r arabic_support_requirements.txt
80
+ ```
81
+
82
+ ### 4. إعداد قاعدة البيانات
83
+
84
+ ```bash
85
+ # إعداد قاعدة البيانات المحلية
86
+ python setup_db.py --mode=local
87
+
88
+ # أو إعداد قاعدة بيانات MySQL للمؤسسة
89
+ python setup_db.py --mode=enterprise --db-host=DB_HOST --db-user=DB_USER --db-pass=DB_PASS
90
+ ```
91
+
92
+ ### 5. تحميل نماذج اللغة والذكاء الاصطناعي
93
+
94
+ ```bash
95
+ # تحميل نماذج معالجة اللغة العربية
96
+ python download_models.py --models=nlp,arabic
97
+
98
+ # تحميل نماذج التعلم الآلي
99
+ python download_models.py --models=ml,pricing
100
+ ```
101
+
102
+ ### 6. تشغيل النظام
103
+
104
+ ```bash
105
+ # تشغيل النظام على الخادم المحلي
106
+ hface run --app=tender_system --port=8501
107
+ ```
108
+
109
+ ## هيكل النظام
110
+
111
+ ```
112
+ tender_system/
113
+ ├── app.py # نقطة دخول التطبيق الرئيسية
114
+ ├── config.py # إعدادات التطبيق
115
+ ├── requirements.txt # متطلبات المكتبات
116
+
117
+ ├── database/ # وحدة قاعدة البيانات
118
+ │ ├── db_connector.py # اتصال قاعدة البيانات
119
+ │ ├── models.py # نماذج البيانات
120
+ │ └── migrations/ # تغييرات قاعدة البيانات
121
+
122
+ ├── modules/ # وحدات النظام
123
+ │ ├── document_analysis/ # وحدة تحليل المستندات
124
+ │ ├── pricing/ # وحدة التسعير
125
+ │ ├── resources/ # وحدة الموارد
126
+ │ ├── risk_analysis/ # وحدة تحليل المخاطر
127
+ │ ├── projects/ # وحدة إدارة المشاريع
128
+ │ ├── reports/ # وحدة التقارير
129
+ │ └── ai_assistant/ # وحدة الذكاء الاصطناعي
130
+
131
+ ├── models/ # نماذج التعلم الآلي
132
+ │ ├── trained/ # النماذج المدربة
133
+ │ └── datasets/ # مجموعات البيانات
134
+
135
+ ├── static/ # ملفات ثابتة
136
+ │ ├── css/ # أنماط CSS
137
+ │ ├── js/ # سكربتات JavaScript
138
+ │ └── images/ # الصور والشعارات
139
+
140
+ └── docs/ # التوثيق
141
+ ├── user_manual.md # دليل المستخدم
142
+ ├── api_reference.md # مرجع API
143
+ └── technical_docs.md # التوثيق التقني
144
+ ```
145
+
146
+ ## الوحدات الرئيسية
147
+
148
+ | الوحدة | الوصف |
149
+ |--------|-------|
150
+ | **تحليل المستندات** | تحليل كراسات الشروط والعقود باستخدام معالجة اللغة العربية الطبيعية |
151
+ | **التسعير المتكامل** | نموذج شامل للتسعير يربط بين مختلف وحدات النظام |
152
+ | **الموارد والتكاليف** | إدارة بيانات المواد والمعدات والعمالة وتكاليفها |
153
+ | **تحليل المخاطر** | تقييم المخاطر التعاقدية والمالية والفنية |
154
+ | **إدارة المشاريع** | متابعة المناقصات والمشاريع المرساة |
155
+ | **التقارير والتحليلات** | تقارير وتحليلات متقدمة لدعم اتخاذ القرار |
156
+ | **الذكاء الاصطناعي** | محرك الذكاء الاصطناعي لمختلف وظائف النظام |
157
+
158
+ ## ميزات بيئة Hybrid Face
159
+
160
+ النظام مصمم للعمل على بيئة Hybrid Face، التي توفر المزايا التالية:
161
+
162
+ - **وضع عمل هجين**: يعمل محلياً وعبر السحابة
163
+ - **ترجمة فورية للواجهة**: دعم متعدد اللغات بالتركيز على العربية والإنجليزية
164
+ - **تكامل البيانات**: مزامنة البيانات بين النسخ المحلية ونسخة السحابة
165
+ - **وضع العمل دون اتصال**: استمرار العمل حتى عند انقطاع الاتصال بالإنترنت
166
+ - **أمان متقدم**: تشفير البيانات والمصادقة متعددة العوامل
167
+ - **قابلية للتوسع**: دعم المستخدمين المتعددين والمشاريع الكبيرة
168
+
169
+ ## استخدام النظام
170
+
171
+ ### 1. تسجيل الدخول
172
+
173
+ ```bash
174
+ # تسجيل الدخول إلى النظام
175
+ hface login --username=USER --password=PASS
176
+ ```
177
+
178
+ ### 2. إدارة المشاريع
179
+
180
+ ```bash
181
+ # إنشاء مشروع جديد
182
+ hface project create --name="اسم المشروع" --client="اسم العميل"
183
+
184
+ # استيراد كراسة شروط
185
+ hface document import --project-id=123 --type=tender --file=path/to/document.pdf
186
+ ```
187
+
188
+ ### 3. تشغيل التحليل والتسعير
189
+
190
+ ```bash
191
+ # تحليل مستندات المناقصة
192
+ hface analyze --project-id=123 --mode=full
193
+
194
+ # تشغيل التسعير الشامل
195
+ hface pricing --project-id=123 --strategy=comprehensive
196
+ ```
197
+
198
+ ## التوثيق
199
+
200
+ يمكن الوصول إلى التوثيق الكامل عبر:
201
+
202
+ - **دليل المستخدم**: `/docs/user_manual.md`
203
+ - **التوثيق التقني**: `/docs/technical_docs.md`
204
+ - **واجهة المساعدة التفاعلية**: متاحة داخل التطبيق عبر زر "المساعدة"
205
+
206
+ ## الدعم الفني
207
+
208
+ للحصول على الدعم الفني، يرجى التواصل مع:
209
+
210
+ - البريد الإلكتروني: [email protected]
211
+ - رقم الهاتف: ⁦+966 55 406 3339⁩
212
+ - نظام التذاكر: https://[email protected]
213
+
214
+ ## الترخيص
215
+
216
+ هذا المشروع ملكية خاصة لشركة شبه الجزيرة للمقاولات. جميع الحقوق محفوظة © 2025.
app.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ # هذا هو المكان الوحيد المسموح بوضع إعدادات الصفحة فيه
4
+ # Streamlit يتطلب أن يكون هذا الأمر في بداية التطبيق وفي ملف واحد فقط
5
+ st.set_page_config(
6
+ page_title="نظام تحليل العقود والمناقصات",
7
+ page_icon="📋",
8
+ layout="wide",
9
+ initial_sidebar_state="expanded",
10
+ menu_items={
11
+ 'About': "تطبيق تحليل العقود والمناقصات بالذكاء الاصطناعي - إصدار 2.0",
12
+ 'Get help': "https://www.wahbi-ai.com/help",
13
+ 'Report a bug': "https://www.wahbi-ai.com/report-bug"
14
+ }
15
+ )
16
+
17
+ # باقي الاستيرادات عادي
18
+ import os
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ # إضافة مسار المشروع الرئيسي إلى Python path
23
+ ROOT_DIR = Path(__file__).parent
24
+ sys.path.append(str(ROOT_DIR))
25
+
26
+ # استيراد الإعدادات
27
+ import config
28
+
29
+ # استيراد الوحدات
30
+ from modules.projects.projects_app import ProjectsApp
31
+ from modules.pricing.pricing_app import PricingApp
32
+ from modules.resources.resources_app import ResourcesApp
33
+ from modules.document_analysis.document_analysis_app import DocumentAnalysisApp
34
+ from modules.risk_analysis.risk_analysis_app import RiskAnalysisApp
35
+ from modules.reports.reports_app import ReportsApp
36
+ from modules.ai_assistant.ai_assistant_app import AIAssistantApp
37
+
38
+ # استيراد المكونات المشتركة
39
+ from utils.components.sidebar import render_sidebar
40
+ from utils.components.header import render_header
41
+ from utils.session_state import initialize_session_state
42
+
43
+
44
+ # تضمين CSS المخصص
45
+ with open(os.path.join(ROOT_DIR, 'static', 'css', 'styles.css')) as f:
46
+ st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
47
+
48
+ # تهيئة حالة الجلسة
49
+ initialize_session_state()
50
+
51
+ # عرض ترويسة الصفحة
52
+ render_header()
53
+
54
+ # عرض الشريط الجانبي
55
+ selected_module = render_sidebar()
56
+
57
+ # تهيئة وحدات النظام
58
+ modules = {
59
+ "الرئيسية": None, # سيتم التعامل معها بشكل خاص
60
+ "إدارة المشاريع": ProjectsApp(),
61
+ "التسعير المتكاملة": PricingApp(),
62
+ "الموارد والتكاليف": ResourcesApp(),
63
+ "تحليل المستندات": DocumentAnalysisApp(),
64
+ "تحليل المخاطر": RiskAnalysisApp(),
65
+ "التقارير والتحليلات": ReportsApp(),
66
+ "المساعد الذكي": AIAssistantApp()
67
+ }
68
+
69
+ # عرض الوحدة المختارة
70
+ if selected_module == "الرئيسية":
71
+ # عرض الصفحة الرئيسية
72
+ st.markdown("<h1 class='main-title'>النظام الشامل لتحليل العقود والمناقصات بالذكاء الاصطناعي</h1>", unsafe_allow_html=True)
73
+
74
+ # عرض لوحة معلومات عامة
75
+ col1, col2, col3 = st.columns(3)
76
+
77
+ with col1:
78
+ st.info("#### المناقصات النشطة\n\n**15** مناقصة", icon="📝")
79
+
80
+ with col2:
81
+ st.success("#### المشاريع المرساة\n\n**8** مشاريع", icon="✅")
82
+
83
+ with col3:
84
+ st.warning("#### مناقصات قيد التسعير\n\n**5** مناقصات", icon="⏳")
85
+
86
+ # عرض الابتكارات النظامية
87
+ st.markdown("## الابتكارات النظامية")
88
+
89
+ from utils.components.system_innovation import display_innovations
90
+ display_innovations()
91
+
92
+ # عرض المخطط العام للنظام
93
+ st.markdown("## هيكل النظام")
94
+
95
+ st.markdown("""
96
+ ```mermaid
97
+ graph TD
98
+ MAIN[النظام الشامل لتحليل العقود والمناقصات بالذكاء الاصطناعي] --> A
99
+ MAIN --> B
100
+ MAIN --> C
101
+ MAIN --> D
102
+ MAIN --> E
103
+ MAIN --> F
104
+
105
+ A[وحدة تحليل المستندات]
106
+ B[وحدة التسعير المتكاملة]
107
+ C[وحدة الموارد والتكاليف]
108
+ D[وحدة تحليل المخاطر]
109
+ E[وحدة إدارة المشاريع]
110
+ F[وحدة التقارير والتحليلات]
111
+
112
+ DB[(قاعدة البيانات المركزية)] --> A
113
+ DB --> B
114
+ DB --> C
115
+ DB --> D
116
+ DB --> E
117
+ DB --> F
118
+
119
+ AI{وحدة الذكاء الاصطناعي} --> A
120
+ AI --> B
121
+ AI --> F
122
+ ```
123
+ """)
124
+
125
+ # عرض معلومات الفريق
126
+ st.markdown("## فريق التطوير")
127
+
128
+ from utils.components.credits import display_credits
129
+ display_credits()
130
+
131
+ else:
132
+ # عرض الوحدة المختارة
133
+ module = modules.get(selected_module)
134
+ if module:
135
+ module.render()
136
+
137
+ # إضافة تذييل الصفحة
138
+ st.markdown("---")
139
+ st.markdown("<div class='footer'>© 2025 شركة شبه الجزيرة للمقاولات - جميع الحقوق محفوظة</div>", unsafe_allow_html=True)
config.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ملف إعدادات النظام
3
+ """
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ # مسارات النظام
9
+ ROOT_DIR = Path(__file__).parent
10
+ STATIC_DIR = os.path.join(ROOT_DIR, 'static')
11
+ MODELS_DIR = os.path.join(ROOT_DIR, 'models')
12
+ DATA_DIR = os.path.join(ROOT_DIR, 'database', 'data')
13
+
14
+ # عنوان التطبيق
15
+ APP_TITLE = "النظام الشامل لتحليل العقود والمناقصات - شركة شبه الجزيرة للمقاولات"
16
+ APP_ICON = "📋"
17
+
18
+ # إعدادات قاعدة البيانات
19
+ DB_TYPE = "sqlite" # يمكن استبدالها بـ 'mysql' أو 'postgresql'
20
+ DB_PATH = os.path.join(DATA_DIR, "tender_db.sqlite")
21
+
22
+ # إعدادات أخرى
23
+ DEBUG_MODE = True
24
+ LOG_LEVEL = "INFO"
25
+ LOCALE = "ar_SA"
26
+
27
+ # مسارات النماذج المدربة
28
+ NLP_ARABIC_MODEL = os.path.join(MODELS_DIR, "trained", "arabic_nlp_model.h5")
29
+ RISK_ANALYSIS_MODEL = os.path.join(MODELS_DIR, "trained", "risk_analysis_model.pkl")
30
+ PRICE_PREDICTION_MODEL = os.path.join(MODELS_DIR, "trained", "price_prediction_model.pkl")
31
+
32
+ # تكوين واجهة المستخدم
33
+ UI_THEME = "light" # 'light' أو 'dark'
34
+ ENABLE_ANIMATIONS = True
35
+ DEFAULT_MODULE = "الرئيسية"
36
+
37
+ # تكوين المحتوى المحلي
38
+ LOCAL_CONTENT_CATEGORIES = ["القوى العاملة", "المنتجات", "الخدمات"]
39
+ LOCAL_CONTENT_TARGETS = {
40
+ "القوى العاملة": 0.8, # 80%
41
+ "المنتجات": 0.7, # 70%
42
+ "الخدمات": 0.6 # 60%
43
+ }
44
+
45
+ # تكوين التسعير
46
+ PRICING_METHODS = [
47
+ "التسعير القياسي",
48
+ "التسعير غير المتزن",
49
+ "التسعير التنافسي",
50
+ "التسعير الموجه بالربحية"
51
+ ]
52
+
53
+ DEFAULT_OVERHEAD_PERCENTAGE = 15 # النسبة الافتراضية للمصاريف العامة والأرباح
54
+
55
+ # إعدادات تحليل المستندات
56
+ SUPPORTED_DOCUMENT_TYPES = ["pdf", "docx", "xlsx", "dwg", "jpg", "png"]
57
+ MAX_UPLOAD_SIZE_MB = 20
58
+
59
+ # إعدادات API الذكاء الاصطناعي
60
+ AI_API_ENABLED = True
61
+ AI_API_ENDPOINT = "http://localhost:8000/api/v1"
62
+ AI_API_KEY = "YOUR_API_KEY_HERE" # يجب استبدالها في بيئة الإنتاج
database/db_connector.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ وحدة الاتصال بقاعدة البيانات
3
+ """
4
+
5
+ import os
6
+ import sqlite3
7
+ from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Float, DateTime, ForeignKey, Boolean, Text
8
+ from sqlalchemy.ext.declarative import declarative_base
9
+ from sqlalchemy.orm import sessionmaker, relationship
10
+ from contextlib import contextmanager
11
+ from datetime import datetime
12
+ import config
13
+
14
+
15
+ # إنشاء قاعدة البيانات الأساسية
16
+ Base = declarative_base()
17
+
18
+ class DatabaseConnector:
19
+ """الفئة المسؤولة عن إدارة الاتصال بقاعدة البيانات"""
20
+
21
+ def __init__(self):
22
+ """تهيئة موصل قاعدة البيانات"""
23
+ db_path = config.DB_PATH
24
+ db_dir = os.path.dirname(db_path)
25
+
26
+ # التأكد من وجود المجلد
27
+ if not os.path.exists(db_dir):
28
+ os.makedirs(db_dir)
29
+
30
+ # إنشاء محرك قاعدة البيانات بناءاً على نوع قاعدة البيانات
31
+ if config.DB_TYPE == "sqlite":
32
+ self.engine = create_engine(f'sqlite:///{db_path}', echo=config.DEBUG_MODE)
33
+ elif config.DB_TYPE == "mysql":
34
+ # في حالة استخدام MySQL (يتطلب إضافة تفاصيل الاتصال في ملف config.py)
35
+ self.engine = create_engine(
36
+ f'mysql+pymysql://{config.DB_USER}:{config.DB_PASSWORD}@{config.DB_HOST}:{config.DB_PORT}/{config.DB_NAME}',
37
+ echo=config.DEBUG_MODE
38
+ )
39
+ elif config.DB_TYPE == "postgresql":
40
+ # في حالة استخدام PostgreSQL (يتطلب إضافة تفاصيل الاتصال في ملف config.py)
41
+ self.engine = create_engine(
42
+ f'postgresql+psycopg2://{config.DB_USER}:{config.DB_PASSWORD}@{config.DB_HOST}:{config.DB_PORT}/{config.DB_NAME}',
43
+ echo=config.DEBUG_MODE
44
+ )
45
+ else:
46
+ raise ValueError(f"نوع قاعدة البيانات غير مدعوم: {config.DB_TYPE}")
47
+
48
+ # إنشاء جلسة للتعامل مع قاعدة البيانات
49
+ self.Session = sessionmaker(bind=self.engine)
50
+
51
+ def create_tables(self):
52
+ """إنشاء جداول قاعدة البيانات"""
53
+ Base.metadata.create_all(self.engine)
54
+
55
+ @contextmanager
56
+ def session_scope(self):
57
+ """
58
+ توفير نطاق جلسة للتعامل مع قاعدة البيانات
59
+ يتم استخدامه مع 'with' لضمان إغلاق الجلسة بشكل صحيح
60
+ """
61
+ session = self.Session()
62
+ try:
63
+ yield session
64
+ session.commit()
65
+ except Exception as e:
66
+ session.rollback()
67
+ raise e
68
+ finally:
69
+ session.close()
70
+
71
+ def execute_query(self, query):
72
+ """
73
+ تنفيذ استعلام SQL مباشر
74
+
75
+ المعلمات:
76
+ query: استعلام SQL
77
+
78
+ الإرجاع:
79
+ نتائج الاستعلام
80
+ """
81
+ with self.engine.connect() as connection:
82
+ result = connection.execute(query)
83
+ return result.fetchall()
84
+
85
+ def backup_database(self, backup_path=None):
86
+ """
87
+ إنشاء نسخة احتياطية من قاعدة البيانات
88
+
89
+ المعلمات:
90
+ backup_path: مسار النسخة الاحتياطية (اختياري)
91
+
92
+ الإرجاع:
93
+ مسار ملف النسخة الاحتياطية
94
+ """
95
+ if config.DB_TYPE != "sqlite":
96
+ raise NotImplementedError("النسخ الاحتياطي مدعوم فقط لقواعد بيانات SQLite حاليًا")
97
+
98
+ # تحديد مسار النسخة الاحتياطية إذا لم يتم تحديده
99
+ if backup_path is None:
100
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
101
+ backup_path = os.path.join(
102
+ os.path.dirname(config.DB_PATH),
103
+ f"backup_{timestamp}.sqlite"
104
+ )
105
+
106
+ # إنشاء النسخة الاحتياطية
107
+ try:
108
+ # فتح اتصال مع قاعدة البيانات الأصلية
109
+ source_conn = sqlite3.connect(config.DB_PATH)
110
+ # فتح اتصال مع قاعدة البيانات الاحتياطية
111
+ dest_conn = sqlite3.connect(backup_path)
112
+
113
+ # نسخ البيانات
114
+ source_conn.backup(dest_conn)
115
+
116
+ # إغلاق الاتصالات
117
+ source_conn.close()
118
+ dest_conn.close()
119
+
120
+ return backup_path
121
+ except Exception as e:
122
+ raise RuntimeError(f"فشل في إنشاء النسخة الاحتياطية: {str(e)}")
123
+
124
+ def restore_database(self, backup_path):
125
+ """
126
+ استعادة قاعدة البيانات من نسخة احتياطية
127
+
128
+ المعلمات:
129
+ backup_path: مسار ملف النسخة الاحتياطية
130
+
131
+ الإرجاع:
132
+ True في حالة نجاح الاستعادة
133
+ """
134
+ if config.DB_TYPE != "sqlite":
135
+ raise NotImplementedError("استعادة النسخ الاحتياطي مدعومة فقط لقواعد بيانات SQLite حاليًا")
136
+
137
+ if not os.path.exists(backup_path):
138
+ raise FileNotFoundError(f"ملف النسخة الاحتياطية غير موجود: {backup_path}")
139
+
140
+ try:
141
+ # فتح اتصال مع قاعدة البيانات الاحتياطية
142
+ source_conn = sqlite3.connect(backup_path)
143
+ # فتح اتصال مع قاعدة البيانات الهدف
144
+ dest_conn = sqlite3.connect(config.DB_PATH)
145
+
146
+ # استعادة البيانات
147
+ source_conn.backup(dest_conn)
148
+
149
+ # إغلاق الاتصالات
150
+ source_conn.close()
151
+ dest_conn.close()
152
+
153
+ return True
154
+ except Exception as e:
155
+ raise RuntimeError(f"فشل في استعادة قاعدة البيانات: {str(e)}")
156
+
157
+
158
+ # إنشاء نموذج الاتصال بقاعدة البيانات
159
+ db_connector = DatabaseConnector()
160
+
161
+ # الحصول على جلسة للتعامل مع قاعدة البيانات
162
+ def get_db_session():
163
+ """
164
+ الحصول على جلسة للتعامل مع قاعدة البيانات
165
+
166
+ الإرجاع:
167
+ كائن الجلسة
168
+ """
169
+ return db_connector.Session()
170
+
171
+ # سياق الجلسة للتعامل مع قاعدة البيانات
172
+ def session_scope():
173
+ """
174
+ توفير نطاق جلسة للتعامل مع قاعدة البيانات
175
+
176
+ استخدام:
177
+ with session_scope() as session:
178
+ # عمليات قاعدة البيانات
179
+ """
180
+ return db_connector.session_scope()
database/models.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ نماذج بيانات النظام
3
+ """
4
+
5
+ from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Boolean, Text, Table, Enum
6
+ from sqlalchemy.orm import relationship
7
+ from datetime import datetime
8
+ import enum
9
+
10
+ from database.db_connector import Base
11
+
12
+
13
+ # جدول العلاقة متعددة القيم بين المشاريع والملفات
14
+ project_files = Table(
15
+ 'project_files',
16
+ Base.metadata,
17
+ Column('project_id', Integer, ForeignKey('projects.id'), primary_key=True),
18
+ Column('file_id', Integer, ForeignKey('files.id'), primary_key=True)
19
+ )
20
+
21
+ # تعريف الأنواع المدرجة
22
+ class ProjectStatus(enum.Enum):
23
+ """حالة المشروع"""
24
+ NEW = "جديد"
25
+ PRICING = "قيد التسعير"
26
+ SUBMITTED = "تم التقديم"
27
+ AWARDED = "تمت الترسية"
28
+ EXECUTION = "قيد التنفيذ"
29
+ COMPLETED = "منتهي"
30
+ CANCELLED = "ملغي"
31
+
32
+ class TenderType(enum.Enum):
33
+ """نوع المناقصة"""
34
+ PUBLIC = "عامة"
35
+ PRIVATE = "خاصة"
36
+ DIRECT = "أمر مباشر"
37
+
38
+ class PricingMethod(enum.Enum):
39
+ """طريقة التسعير"""
40
+ STANDARD = "قياسي"
41
+ UNBALANCED = "غير متزن"
42
+ COMPETITIVE = "تنافسي"
43
+ PROFITABILITY = "موجه بالربحية"
44
+
45
+ # نموذج المستخدم
46
+ class User(Base):
47
+ """نموذج بيانات المستخدم"""
48
+ __tablename__ = 'users'
49
+
50
+ id = Column(Integer, primary_key=True)
51
+ username = Column(String(50), unique=True, nullable=False)
52
+ password_hash = Column(String(128), nullable=False)
53
+ full_name = Column(String(100), nullable=False)
54
+ email = Column(String(100), unique=True, nullable=False)
55
+ phone = Column(String(20))
56
+ role = Column(String(20), nullable=False)
57
+ department = Column(String(50))
58
+ is_active = Column(Boolean, default=True)
59
+ created_at = Column(DateTime, default=datetime.now)
60
+ last_login = Column(DateTime)
61
+
62
+ # العلاقات
63
+ projects = relationship("Project", back_populates="created_by")
64
+ pricing_items = relationship("PricingItem", back_populates="created_by")
65
+
66
+ def __repr__(self):
67
+ return f"<User {self.username}>"
68
+
69
+ # نموذج المشروع
70
+ class Project(Base):
71
+ """نموذج بيانات المشروع"""
72
+ __tablename__ = 'projects'
73
+
74
+ id = Column(Integer, primary_key=True)
75
+ name = Column(String(100), nullable=False)
76
+ tender_number = Column(String(50))
77
+ client = Column(String(100), nullable=False)
78
+ location = Column(String(100))
79
+ description = Column(Text)
80
+ status = Column(Enum(ProjectStatus), default=ProjectStatus.NEW)
81
+ tender_type = Column(Enum(TenderType), default=TenderType.PUBLIC)
82
+ pricing_method = Column(Enum(PricingMethod), default=PricingMethod.STANDARD)
83
+ submission_date = Column(DateTime)
84
+ created_at = Column(DateTime, default=datetime.now)
85
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
86
+ created_by_id = Column(Integer, ForeignKey('users.id'))
87
+
88
+ # العلاقات
89
+ created_by = relationship("User", back_populates="projects")
90
+ pricing_sections = relationship("PricingSection", back_populates="project", cascade="all, delete-orphan")
91
+ pricing_items = relationship("PricingItem", back_populates="project", cascade="all, delete-orphan")
92
+ local_content_items = relationship("LocalContentItem", back_populates="project", cascade="all, delete-orphan")
93
+ risk_items = relationship("RiskItem", back_populates="project", cascade="all, delete-orphan")
94
+ files = relationship("File", secondary=project_files, back_populates="projects")
95
+
96
+ def __repr__(self):
97
+ return f"<Project {self.name}>"
98
+
99
+ # نموذج قسم التسعير
100
+ class PricingSection(Base):
101
+ """نموذج بيانات قسم التسعير"""
102
+ __tablename__ = 'pricing_sections'
103
+
104
+ id = Column(Integer, primary_key=True)
105
+ project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
106
+ name = Column(String(100), nullable=False)
107
+ description = Column(Text)
108
+ section_order = Column(Integer, default=0)
109
+ created_at = Column(DateTime, default=datetime.now)
110
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
111
+
112
+ # العلاقات
113
+ project = relationship("Project", back_populates="pricing_sections")
114
+ pricing_items = relationship("PricingItem", back_populates="section", cascade="all, delete-orphan")
115
+
116
+ def __repr__(self):
117
+ return f"<PricingSection {self.name}>"
118
+
119
+ # نموذج بند التسعير
120
+ class PricingItem(Base):
121
+ """نموذج بيانات بند التسعير"""
122
+ __tablename__ = 'pricing_items'
123
+
124
+ id = Column(Integer, primary_key=True)
125
+ project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
126
+ section_id = Column(Integer, ForeignKey('pricing_sections.id'))
127
+ item_code = Column(String(20))
128
+ description = Column(Text, nullable=False)
129
+ unit = Column(String(20), nullable=False)
130
+ quantity = Column(Float, nullable=False)
131
+ unit_price = Column(Float, default=0)
132
+ unbalanced_price = Column(Float)
133
+ final_price = Column(Float)
134
+ pricing_strategy = Column(String(20), default="متوازن")
135
+ notes = Column(Text)
136
+ created_by_id = Column(Integer, ForeignKey('users.id'))
137
+ created_at = Column(DateTime, default=datetime.now)
138
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
139
+
140
+ # العلاقات
141
+ project = relationship("Project", back_populates="pricing_items")
142
+ section = relationship("PricingSection", back_populates="pricing_items")
143
+ created_by = relationship("User", back_populates="pricing_items")
144
+ resource_usages = relationship("ResourceUsage", back_populates="pricing_item")
145
+
146
+ def __repr__(self):
147
+ return f"<PricingItem {self.item_code}: {self.description[:30]}>"
148
+
149
+ # نموذج بند المحتوى المحلي
150
+ class LocalContentItem(Base):
151
+ """نموذج بيانات بند المحتوى المحلي"""
152
+ __tablename__ = 'local_content_items'
153
+
154
+ id = Column(Integer, primary_key=True)
155
+ project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
156
+ category = Column(String(50), nullable=False)
157
+ item_name = Column(String(100), nullable=False)
158
+ supplier_id = Column(Integer, ForeignKey('suppliers.id'))
159
+ total_cost = Column(Float, default=0)
160
+ local_percentage = Column(Float, default=0)
161
+ notes = Column(Text)
162
+ created_at = Column(DateTime, default=datetime.now)
163
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
164
+
165
+ # العلاقات
166
+ project = relationship("Project", back_populates="local_content_items")
167
+ supplier = relationship("Supplier", back_populates="local_content_items")
168
+
169
+ def __repr__(self):
170
+ return f"<LocalContentItem {self.item_name}>"
171
+
172
+ # نموذج المورد
173
+ class Supplier(Base):
174
+ """نموذج بيانات المورد"""
175
+ __tablename__ = 'suppliers'
176
+
177
+ id = Column(Integer, primary_key=True)
178
+ name = Column(String(100), nullable=False)
179
+ contact_person = Column(String(100))
180
+ phone = Column(String(20))
181
+ email = Column(String(100))
182
+ address = Column(String(200))
183
+ category = Column(String(50))
184
+ is_local = Column(Boolean, default=False)
185
+ local_content_percentage = Column(Float, default=0)
186
+ created_at = Column(DateTime, default=datetime.now)
187
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
188
+
189
+ # العلاقات
190
+ local_content_items = relationship("LocalContentItem", back_populates="supplier")
191
+ resources = relationship("Resource", back_populates="supplier")
192
+
193
+ def __repr__(self):
194
+ return f"<Supplier {self.name}>"
195
+
196
+ # نموذج المخاطرة
197
+ class RiskItem(Base):
198
+ """نموذج بيانات المخاطرة"""
199
+ __tablename__ = 'risk_items'
200
+
201
+ id = Column(Integer, primary_key=True)
202
+ project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
203
+ risk_code = Column(String(20))
204
+ description = Column(Text, nullable=False)
205
+ category = Column(String(50), nullable=False)
206
+ impact = Column(String(20), nullable=False)
207
+ probability = Column(String(20), nullable=False)
208
+ mitigation_strategy = Column(Text)
209
+ created_at = Column(DateTime, default=datetime.now)
210
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
211
+
212
+ # العلاقات
213
+ project = relationship("Project", back_populates="risk_items")
214
+
215
+ def __repr__(self):
216
+ return f"<RiskItem {self.risk_code}: {self.description[:30]}>"
217
+
218
+ # نموذج المورد
219
+ class Resource(Base):
220
+ """نموذج بيانات المورد"""
221
+ __tablename__ = 'resources'
222
+
223
+ id = Column(Integer, primary_key=True)
224
+ code = Column(String(20), unique=True)
225
+ name = Column(String(100), nullable=False)
226
+ description = Column(Text)
227
+ category = Column(String(50), nullable=False)
228
+ unit = Column(String(20), nullable=False)
229
+ unit_price = Column(Float, default=0)
230
+ supplier_id = Column(Integer, ForeignKey('suppliers.id'))
231
+ is_local = Column(Boolean, default=False)
232
+ local_content_percentage = Column(Float, default=0)
233
+ created_at = Column(DateTime, default=datetime.now)
234
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
235
+
236
+ # العلاقات
237
+ supplier = relationship("Supplier", back_populates="resources")
238
+ resource_usages = relationship("ResourceUsage", back_populates="resource")
239
+
240
+ def __repr__(self):
241
+ return f"<Resource {self.code}: {self.name}>"
242
+
243
+ # نموذج استخدام المورد
244
+ class ResourceUsage(Base):
245
+ """نموذج بيانات استخدام المورد"""
246
+ __tablename__ = 'resource_usages'
247
+
248
+ id = Column(Integer, primary_key=True)
249
+ pricing_item_id = Column(Integer, ForeignKey('pricing_items.id'), nullable=False)
250
+ resource_id = Column(Integer, ForeignKey('resources.id'), nullable=False)
251
+ quantity = Column(Float, nullable=False)
252
+ created_at = Column(DateTime, default=datetime.now)
253
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
254
+
255
+ # العلاقات
256
+ pricing_item = relationship("PricingItem", back_populates="resource_usages")
257
+ resource = relationship("Resource", back_populates="resource_usages")
258
+
259
+ def __repr__(self):
260
+ return f"<ResourceUsage {self.pricing_item_id} - {self.resource_id}>"
261
+
262
+ # نموذج الملف
263
+ class File(Base):
264
+ """نموذج بيانات الملف"""
265
+ __tablename__ = 'files'
266
+
267
+ id = Column(Integer, primary_key=True)
268
+ filename = Column(String(100), nullable=False)
269
+ original_filename = Column(String(100), nullable=False)
270
+ file_type = Column(String(20), nullable=False)
271
+ file_size = Column(Integer, nullable=False)
272
+ file_path = Column(String(255), nullable=False)
273
+ upload_date = Column(DateTime, default=datetime.now)
274
+
275
+ # العلاقات
276
+ projects = relationship("Project", secondary=project_files, back_populates="files")
277
+
278
+ def __repr__(self):
279
+ return f"<File {self.original_filename}>"
demo_pricing.py ADDED
@@ -0,0 +1,453 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
8
+ # إعداد الصفحة
9
+ st.set_page_config(
10
+ page_title="عرض التسعير غير المتوازن",
11
+ page_icon="📊",
12
+ layout="wide"
13
+ )
14
+
15
+ st.title("عرض تحسينات واجهة المستخدم")
16
+
17
+ # بيانات تجريبية للعرض
18
+ @st.cache_data
19
+ def get_sample_data():
20
+ items = pd.DataFrame({
21
+ 'رقم البند': ['UB1', 'UB2', 'UB3', 'UB4', 'UB5'],
22
+ 'وصف البند': ['حفر أساسات', 'صب خرسانة مسلحة', 'أعمال طوب', 'أعمال تشطيبات', 'أعمال كهرباء'],
23
+ 'الوحدة': ['م3', 'م3', 'م2', 'م2', 'نقطة'],
24
+ 'الكمية': [350.0, 120.0, 500.0, 800.0, 150.0],
25
+ 'سعر الوحدة': [80.0, 950.0, 45.0, 120.0, 90.0],
26
+ 'الإجمالي': [28000.0, 114000.0, 22500.0, 96000.0, 13500.0],
27
+ 'إستراتيجية التسعير': ['نقص', 'زيادة', 'متوازن', 'زيادة', 'نقص']
28
+ })
29
+ return items
30
+
31
+ items = get_sample_data()
32
+
33
+ # 1. عرض الجدول مع تنسيق محسن
34
+ st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>بنود التسعير غير المتوازن</h3>", unsafe_allow_html=True)
35
+
36
+ # تعيين ألوان للإستراتيجيات وتنسيق الجدول بشكل متقدم
37
+ def highlight_row(row):
38
+ strategy = row['إستراتيجية التسعير']
39
+ styles = [''] * len(row)
40
+
41
+ # تطبيق لون خلفية لكل صف حسب الإستراتيجية
42
+ if strategy == 'زيادة':
43
+ background = 'linear-gradient(90deg, rgba(168, 230, 207, 0.3), rgba(168, 230, 207, 0.1))'
44
+ text_color = '#1F7A8C'
45
+ elif strategy == 'نقص':
46
+ background = 'linear-gradient(90deg, rgba(255, 154, 162, 0.3), rgba(255, 154, 162, 0.1))'
47
+ text_color = '#9D2A45'
48
+ else:
49
+ background = 'linear-gradient(90deg, rgba(220, 237, 255, 0.3), rgba(220, 237, 255, 0.1))'
50
+ text_color = '#555555'
51
+
52
+ # تطبيق النمط على جميع الخلايا في الصف
53
+ for i in range(len(styles)):
54
+ styles[i] = f'background: {background}; color: {text_color}; border-bottom: 1px solid #ddd;'
55
+
56
+ # تطبيق نمط خاص على خلية الإستراتيجية
57
+ if strategy == 'زيادة':
58
+ styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #a8e6cf; color: #007263; font-weight: bold; border-radius: 5px; text-align: center;'
59
+ elif strategy == 'نقص':
60
+ styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #ff9aa2; color: #9D2A45; font-weight: bold; border-radius: 5px; text-align: center;'
61
+ else:
62
+ styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #dceeff; color: #555555; font-weight: bold; border-radius: 5px; text-align: center;'
63
+
64
+ # تنسيق عمود السعر
65
+ price_idx = list(row.index).index('سعر الوحدة')
66
+ styles[price_idx] = styles[price_idx] + 'font-weight: bold;'
67
+
68
+ # تنسيق عمود الإجمالي
69
+ total_idx = list(row.index).index('الإجمالي')
70
+ styles[total_idx] = styles[total_idx] + 'font-weight: bold;'
71
+
72
+ return styles
73
+
74
+ # تطبيق التنسيق على الجدول
75
+ styled_items = items.style.apply(highlight_row, axis=1)
76
+
77
+ # تنسيق تنسيق الأرقام
78
+ styled_items = styled_items.format({
79
+ 'الكمية': '{:,.2f}',
80
+ 'سعر الوحدة': '{:,.2f}',
81
+ 'الإجمالي': '{:,.2f}'
82
+ })
83
+
84
+ st.dataframe(styled_items, use_container_width=True, height=None)
85
+
86
+ # 2. عرض المقارنة مع تصميم محسن
87
+ st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>مقارنة التسعير المتوازن وغير المتوازن</h3>", unsafe_allow_html=True)
88
+
89
+ # بيانات المقارنة
90
+ original_items = items.copy()
91
+ original_items['سعر الوحدة'] = [70.0, 820.0, 45.0, 100.0, 110.0]
92
+ original_items['الإجمالي'] = original_items['الكمية'] * original_items['سعر الوحدة']
93
+
94
+ original_total = original_items['الإجمالي'].sum()
95
+ unbalanced_total = items['الإجمالي'].sum()
96
+
97
+ # عرض بطاقات المقارنة بتصميم متقدم
98
+ st.markdown("""
99
+ <style>
100
+ .metric-container {
101
+ background: linear-gradient(to right, #f1f8ff, #ffffff);
102
+ border-radius: 10px;
103
+ padding: 15px;
104
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
105
+ text-align: center;
106
+ border: 1px solid #e6f2ff;
107
+ }
108
+ .metric-title {
109
+ color: #555;
110
+ font-size: 0.9em;
111
+ margin-bottom: 5px;
112
+ }
113
+ .metric-value {
114
+ color: #1F7A8C;
115
+ font-size: 1.8em;
116
+ font-weight: bold;
117
+ margin: 5px 0;
118
+ }
119
+ .metric-delta {
120
+ font-size: 0.9em;
121
+ font-weight: bold;
122
+ padding: 3px 8px;
123
+ border-radius: 10px;
124
+ display: inline-block;
125
+ margin-top: 5px;
126
+ }
127
+ .positive-delta {
128
+ background-color: rgba(40, 167, 69, 0.1);
129
+ color: #28a745;
130
+ }
131
+ .negative-delta {
132
+ background-color: rgba(220, 53, 69, 0.1);
133
+ color: #dc3545;
134
+ }
135
+ .neutral-delta {
136
+ background-color: rgba(108, 117, 125, 0.1);
137
+ color: #6c757d;
138
+ }
139
+ </style>
140
+ """, unsafe_allow_html=True)
141
+
142
+ col1, col2, col3 = st.columns(3)
143
+
144
+ with col1:
145
+ st.markdown("""
146
+ <div class="metric-container">
147
+ <div class="metric-title">إجمالي التسعير المتوازن</div>
148
+ <div class="metric-value">{:,.2f} ريال</div>
149
+ <div class="metric-delta neutral-delta">التسعير الأصلي</div>
150
+ </div>
151
+ """.format(original_total), unsafe_allow_html=True)
152
+
153
+ with col2:
154
+ st.markdown("""
155
+ <div class="metric-container">
156
+ <div class="metric-title">إجمالي التسعير غير المتوازن</div>
157
+ <div class="metric-value">{:,.2f} ريال</div>
158
+ <div class="metric-delta {}">بعد إعادة توزيع الأسعار</div>
159
+ </div>
160
+ """.format(
161
+ unbalanced_total,
162
+ "positive-delta" if unbalanced_total > original_total else "negative-delta" if unbalanced_total < original_total else "neutral-delta"
163
+ ), unsafe_allow_html=True)
164
+
165
+ with col3:
166
+ diff = unbalanced_total - original_total
167
+ delta_percent = diff/original_total*100 if original_total > 0 else 0
168
+
169
+ st.markdown("""
170
+ <div class="metric-container">
171
+ <div class="metric-title">الفرق بين التسعيرين</div>
172
+ <div class="metric-value">{:,.2f} ريال</div>
173
+ <div class="metric-delta {}">نسبة الفرق: {:+.1f}%</div>
174
+ </div>
175
+ """.format(
176
+ diff,
177
+ "positive-delta" if diff > 0 else "negative-delta" if diff < 0 else "neutral-delta",
178
+ delta_percent
179
+ ), unsafe_allow_html=True)
180
+
181
+ # 3. رسم بياني للمقارنة
182
+ st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>تحليل بصري للتسعير غير المتوازن</h3>", unsafe_allow_html=True)
183
+
184
+ # إعداد البيانات للرسم البياني
185
+ chart_data = pd.DataFrame({
186
+ 'وصف البند': original_items['وصف البند'],
187
+ 'التسعير المتوازن': original_items['الإجمالي'],
188
+ 'التسعير غير المتوازن': items['الإجمالي']
189
+ })
190
+
191
+ # إضافة عمود للنسبة المئوية للتغيير
192
+ chart_data['نسبة التغيير'] = (chart_data['التسعير غير المتوازن'] - chart_data['التسعير المتوازن']) / chart_data['التسعير المتوازن'] * 100
193
+
194
+ # تحديد لون الأعمدة بناءً على نسبة التغيير
195
+ bar_colors = []
196
+ for change in chart_data['نسبة التغيير']:
197
+ if change > 5: # زيادة كبيرة
198
+ bar_colors.append('#1F7A8C') # أزرق مخضر
199
+ elif change > 0: # زيادة صغيرة
200
+ bar_colors.append('#81B29A') # أخضر فاتح
201
+ elif change > -5: # نقص صغير
202
+ bar_colors.append('#F2CC8F') # أصفر
203
+ else: # نقص كبير
204
+ bar_colors.append('#E07A5F') # أحمر
205
+
206
+ # التبويب بين مخططات مختلفة للمقارنة
207
+ chart_tabs = st.tabs(["مخطط شريطي", "مخطط مقارنة", "مخطط نسبة التغيير"])
208
+
209
+ with chart_tabs[0]: # رسم بياني شريطي
210
+ # رسم بياني شريطي للمقارنة
211
+ fig = go.Figure()
212
+
213
+ fig.add_trace(go.Bar(
214
+ x=chart_data['وصف البند'],
215
+ y=chart_data['التسعير المتوازن'],
216
+ name='التسعير المتوازن',
217
+ marker_color='rgba(55, 83, 109, 0.7)'
218
+ ))
219
+
220
+ fig.add_trace(go.Bar(
221
+ x=chart_data['وصف البند'],
222
+ y=chart_data['التسعير غير المتوازن'],
223
+ name='التسعير غير المتوازن',
224
+ marker_color=bar_colors
225
+ ))
226
+
227
+ fig.update_layout(
228
+ title='مقارنة بين التسعير المتوازن وغير المتوازن',
229
+ xaxis_tickfont_size=14,
230
+ yaxis=dict(
231
+ title='الإجمالي (ريال)',
232
+ titlefont_size=16,
233
+ tickfont_size=14,
234
+ ),
235
+ legend=dict(
236
+ x=0.01,
237
+ y=0.99,
238
+ bgcolor='rgba(255, 255, 255, 0.8)',
239
+ bordercolor='rgba(0, 0, 0, 0.1)',
240
+ borderwidth=1
241
+ ),
242
+ barmode='group',
243
+ bargap=0.15,
244
+ bargroupgap=0.1,
245
+ plot_bgcolor='rgba(240, 249, 255, 0.5)',
246
+ margin=dict(t=50, b=50, l=20, r=20)
247
+ )
248
+
249
+ st.plotly_chart(fig, use_container_width=True)
250
+
251
+ with chart_tabs[1]: # رسم مقارنة
252
+ # رسم مقارنة بين التسعيرين
253
+ fig = go.Figure()
254
+
255
+ # إضافة خط للتسعير المتوازن
256
+ fig.add_trace(go.Scatter(
257
+ x=chart_data['وصف البند'],
258
+ y=chart_data['التسعير المتوازن'],
259
+ name='التسعير المتوازن',
260
+ mode='lines+markers',
261
+ line=dict(color='rgb(55, 83, 109)', width=3),
262
+ marker=dict(size=10, color='rgb(55, 83, 109)')
263
+ ))
264
+
265
+ # إضافة نقاط للتسعير غير المتوازن
266
+ fig.add_trace(go.Scatter(
267
+ x=chart_data['وصف البند'],
268
+ y=chart_data['التسعير غير المتوازن'],
269
+ name='التسعير غير المتوازن',
270
+ mode='lines+markers',
271
+ line=dict(color='rgb(26, 118, 255)', width=3),
272
+ marker=dict(
273
+ size=12,
274
+ color=bar_colors,
275
+ line=dict(width=2, color='white')
276
+ )
277
+ ))
278
+
279
+ # تحديثات التخطيط
280
+ fig.update_layout(
281
+ title='مقارنة مرئية بين استراتيجيات التسعير',
282
+ xaxis_tickfont_size=14,
283
+ yaxis=dict(
284
+ title='القيمة الإجمالية (ريال)',
285
+ titlefont_size=16,
286
+ tickfont_size=14,
287
+ gridcolor='rgba(200, 200, 200, 0.2)'
288
+ ),
289
+ legend=dict(
290
+ x=0.01,
291
+ y=0.99,
292
+ bgcolor='rgba(255, 255, 255, 0.8)',
293
+ bordercolor='rgba(0, 0, 0, 0.1)',
294
+ borderwidth=1
295
+ ),
296
+ plot_bgcolor='rgba(240, 249, 255, 0.5)',
297
+ margin=dict(t=50, b=50, l=20, r=20)
298
+ )
299
+
300
+ st.plotly_chart(fig, use_container_width=True)
301
+
302
+ with chart_tabs[2]: # مخطط نسبة التغيير
303
+ # مخطط للنسبة المئوية للتغيير
304
+ fig = go.Figure()
305
+
306
+ # إضافة أعمدة لنسبة التغيير مع ألوان مختلفة حسب القيمة
307
+ fig.add_trace(go.Bar(
308
+ x=chart_data['وصف البند'],
309
+ y=chart_data['نسبة التغيير'],
310
+ name='نسبة التغيير',
311
+ marker_color=bar_colors,
312
+ text=[f"{val:.1f}%" for val in chart_data['نسبة التغيير']],
313
+ textposition='auto'
314
+ ))
315
+
316
+ # إضافة خط أفقي عند الصفر
317
+ fig.add_shape(
318
+ type="line",
319
+ x0=-0.5,
320
+ y0=0,
321
+ x1=len(chart_data['وصف البند'])-0.5,
322
+ y1=0,
323
+ line=dict(
324
+ color="black",
325
+ width=2,
326
+ dash="dash",
327
+ )
328
+ )
329
+
330
+ # تحديثات التخطيط
331
+ fig.update_layout(
332
+ title='نسبة التغيير في أسعار البنود (%)',
333
+ xaxis_tickfont_size=14,
334
+ yaxis=dict(
335
+ title='نسبة التغيير (%)',
336
+ titlefont_size=16,
337
+ tickfont_size=14,
338
+ gridcolor='rgba(200, 200, 200, 0.2)',
339
+ zeroline=True,
340
+ zerolinecolor='black',
341
+ zerolinewidth=2
342
+ ),
343
+ plot_bgcolor='rgba(240, 249, 255, 0.5)',
344
+ margin=dict(t=50, b=50, l=20, r=20)
345
+ )
346
+
347
+ st.plotly_chart(fig, use_container_width=True)
348
+
349
+ # إضافة جدول مع نسب التغيير
350
+ st.markdown("#### جدول مفصل بنسب التغيير")
351
+
352
+ # إعداد بيانات الجدول
353
+ table_data = chart_data[['وصف البند', 'التسعير المتوازن', 'التسعير غير المتوازن', 'نسبة التغيير']]
354
+
355
+ # تنسيق الجدول
356
+ def highlight_change(row):
357
+ change = row['نسبة التغيير']
358
+ if change > 5:
359
+ return ['', '', '', 'background-color: rgba(31, 122, 140, 0.3); color: #1F7A8C; font-weight: bold;']
360
+ elif change > 0:
361
+ return ['', '', '', 'background-color: rgba(129, 178, 154, 0.3); color: #2A9D8F; font-weight: bold;']
362
+ elif change > -5:
363
+ return ['', '', '', 'background-color: rgba(242, 204, 143, 0.3); color: #BC6C25; font-weight: bold;']
364
+ else:
365
+ return ['', '', '', 'background-color: rgba(224, 122, 95, 0.3); color: #AE2012; font-weight: bold;']
366
+
367
+ # تطبيق التنسيق
368
+ styled_table = table_data.style.apply(highlight_change, axis=1).format({
369
+ 'التسعير المتوازن': '{:,.2f} ريال',
370
+ 'التسعير غير المتوازن': '{:,.2f} ريال',
371
+ 'نسبة التغيير': '{:+.1f}%'
372
+ })
373
+
374
+ st.dataframe(styled_table, use_container_width=True)
375
+
376
+ # 4. أزرار الحفظ والتصدير مع تصميم محسن
377
+ st.markdown("<hr style='margin-top: 30px; margin-bottom: 20px; border-top: 1px solid #ddd;'>", unsafe_allow_html=True)
378
+ st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>حفظ وتصدير البيانا��</h3>", unsafe_allow_html=True)
379
+
380
+ st.markdown("""
381
+ <style>
382
+ .action-card {
383
+ background: linear-gradient(135deg, #f8f9fa, #e9ecef);
384
+ border-radius: 10px;
385
+ padding: 20px;
386
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
387
+ border-left: 5px solid #1F7A8C;
388
+ transition: all 0.3s ease;
389
+ }
390
+ .action-card:hover {
391
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
392
+ transform: translateY(-2px);
393
+ }
394
+ .action-icon {
395
+ color: #1F7A8C;
396
+ font-size: 24px;
397
+ margin-bottom: 10px;
398
+ }
399
+ .action-title {
400
+ color: #333;
401
+ font-size: 18px;
402
+ font-weight: bold;
403
+ margin-bottom: 10px;
404
+ }
405
+ .action-desc {
406
+ color: #666;
407
+ font-size: 14px;
408
+ margin-bottom: 15px;
409
+ }
410
+ </style>
411
+ """, unsafe_allow_html=True)
412
+
413
+ col1, col2 = st.columns(2)
414
+
415
+ with col1:
416
+ # بطاقة حفظ التسعير
417
+ st.markdown("""
418
+ <div class="action-card">
419
+ <div class="action-icon">💾</div>
420
+ <div class="action-title">حفظ التسعير غير المتوازن</div>
421
+ <div class="action-desc">قم بحفظ التسعير الحالي في المشروع لاستخدامه لاحقاً في التقارير وفي إجمالي التسعير.</div>
422
+ </div>
423
+ """, unsafe_allow_html=True)
424
+
425
+ # زر حفظ التسعير غير المتوازن
426
+ if st.button("حفظ التسعير غير المتوازن", type="primary", use_container_width=True):
427
+ st.success("تم حفظ التسعير غير المتوازن بنجاح!")
428
+ st.balloons() # إضافة تأثير احتفالي عند الحفظ
429
+
430
+ with col2:
431
+ # بطاقة تصدير التسعير
432
+ st.markdown("""
433
+ <div class="action-card">
434
+ <div class="action-icon">📊</div>
435
+ <div class="action-title">تصدير البيانات</div>
436
+ <div class="action-desc">قم بتصدير جدول التسعير الحالي بصيغة CSV لاستخدامه في برامج أخرى مثل Excel.</div>
437
+ </div>
438
+ """, unsafe_allow_html=True)
439
+
440
+ # زر تصدير التسعير
441
+ export_button = st.button("تجهيز ملف للتصدير", use_container_width=True)
442
+ if export_button:
443
+ # تحويل البيانات إلى CSV
444
+ csv = items.to_csv(index=False)
445
+ st.success("تم تجهيز ملف التصدير بنجاح! يمكنك تنزيله الآن.")
446
+ # تقديم البيانات للتنزيل
447
+ st.download_button(
448
+ label="تنزيل ملف CSV",
449
+ data=csv,
450
+ file_name="unbalanced_pricing.csv",
451
+ mime="text/csv",
452
+ use_container_width=True
453
+ )
docs/technical_docs.md ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # التوثيق التقني
2
+ ## نظام تحليل العقود والمناقصات بالذكاء الاصطناعي - شركة شبه الجزيرة للمقاولات
3
+
4
+ <p align="center">
5
+ <img src="../static/images/logo.png" alt="شعار النظام" width="200"/>
6
+ <br>
7
+ <em>إصدار التوثيق: 1.0.2 - تاريخ التحديث: 2025/03/01</em>
8
+ </p>
9
+
10
+ ## جدول المحتويات
11
+
12
+ 1. [نظرة عامة](#نظرة-عامة)
13
+ 2. [المعمارية التقنية](#المعمارية-التقنية)
14
+ 3. [متطلبات النظام](#متطلبات-النظام)
15
+ 4. [الإعداد والتثبيت](#الإعداد-والتثبيت)
16
+ 5. [بيئة Hybrid Face](#بيئة-hybrid-face)
17
+ 6. [هيكل قاعدة البيانات](#هيكل-قاعدة-البيانات)
18
+ 7. [وحدات النظام](#وحدات-النظام)
19
+ 8. [واجهات برمجة التطبيقات (APIs)](#واجهات-برمجة-التطبيقات-apis)
20
+ 9. [الأمان والمصادقة](#الأمان-والمصادقة)
21
+ 10. [الأداء وقابلية التوسع](#الأداء-وقابلية-التوسع)
22
+ 11. [استراتيجية النسخ الاحتياطي واستعادة البيانات](#استراتيجية-النسخ-الاحتياطي-واستعادة-البيانات)
23
+ 12. [إرشادات التطوير](#إرشادات-التطوير)
24
+ 13. [اختبار النظام](#اختبار-النظام)
25
+ 14. [التكامل مع الأنظمة الخارجية](#التكامل-مع-الأنظمة-الخارجية)
26
+ 15. [سجل التغييرات](#سجل-التغييرات)
27
+
28
+ ## نظرة عامة
29
+
30
+ ### عن النظام
31
+
32
+ نظام تحليل العقود والمناقصات بالذكاء الاصطناعي هو منصة متكاملة تعتمد على تقنيات الذكاء الاصطناعي ومعالجة اللغة العربية الطبيعية لمساعدة شركة شبه الجزيرة للمقاولات في تحليل وتسعير المناقصات وإدارة المشاريع.
33
+
34
+ ### المكونات الرئيسية
35
+
36
+ 1. **واجهة المستخدم (Frontend)**: تطبيق ويب تفاعلي مبني بواسطة Streamlit
37
+ 2. **خدمات الخلفية (Backend)**: مجموعة من الخدمات والوحدات البرمجية بلغة Python
38
+ 3. **قاعدة البيانات**: SQLite للتطوير والنشر المحلي، MySQL للنشر المؤسسي
39
+ 4. **محركات الذكاء الاصطناعي**: نماذج معالجة اللغة الطبيعية والتعلم الآلي
40
+ 5. **خدمات التكامل**: واجهات برمجة للتكامل مع الأنظمة الخارجية
41
+
42
+ ## المعمارية التقنية
43
+
44
+ ### المخطط العام للنظام
45
+
46
+ ```mermaid
47
+ graph TD
48
+ User[المستخدم] --> UI[واجهة المستخدم Streamlit]
49
+ UI --> API[طبقة API]
50
+ API --> Core[النواة]
51
+ Core --> DB[(قاعدة البيانات)]
52
+ Core --> NLP[معالجة اللغة العربية]
53
+ Core --> ML[نماذج التعلم الآلي]
54
+ Core --> FS[نظام الملفات]
55
+ Core --> External[أنظمة خارجية]
56
+
57
+ subgraph Core Modules
58
+ NLP
59
+ ML
60
+ Doc[تحليل المستندات]
61
+ Pricing[التسعير]
62
+ Risk[تحليل المخاطر]
63
+ Res[إدارة الموارد]
64
+ Proj[إدارة المشاريع]
65
+ Rep[التقارير]
66
+ end
67
+ ```
68
+
69
+ ### نمط المعمارية
70
+
71
+ النظام يعتمد على نمط المعمارية طبقية (Layered Architecture) ونمط وحدات الخدمة (Service Modules):
72
+
73
+ 1. **طبقة العرض**: واجهة المستخدم Streamlit
74
+ 2. **طبقة الخدمات**: واجهات برمجة التطبيقات RESTful
75
+ 3. **طبقة الأعمال**: وحدات المعالجة المنطقية
76
+ 4. **طبقة البيانات**: الوصول إلى قاعدة البيانات وتخزين الملفات
77
+
78
+ ## متطلبات النظام
79
+
80
+ ### متطلبات الأجهزة
81
+
82
+ | المكون | الحد الأدنى | الموصى به |
83
+ |--------|-------------|-----------|
84
+ | المعالج | Intel Core i5 (8 أنوية) | Intel Core i7 (12 أنوية) أو أعلى |
85
+ | الذاكرة | 16GB RAM | 32GB RAM أو أكثر |
86
+ | التخزين | 10GB + مساحة للمستندات | SSD بسعة 50GB أو أكثر |
87
+ | الشبكة | اتصال إنترنت 10Mbps | اتصال إنترنت 50Mbps أو أسرع |
88
+ | الشاشة | دقة 1080p | دقة 1440p أو أعلى |
89
+
90
+ ### متطلبات البرمجيات
91
+
92
+ | البرمجيات | الإصدار المطلوب |
93
+ |-----------|-----------------|
94
+ | نظام التشغيل | Windows 10/11، MacOS 12+، Ubuntu 20.04+ |
95
+ | Python | 3.9 أو أحدث |
96
+ | بيئة Hybrid Face | 2.5 أو أحدث |
97
+ | متصفح | Chrome 90+، Firefox 88+، Edge 90+ |
98
+ | MySQL (اختياري) | 8.0 أو أحدث |
99
+
100
+ ### المكتبات الأس��سية
101
+
102
+ ```python
103
+ # المكتبات الأساسية المستخدمة
104
+ streamlit==1.10.0
105
+ pandas==1.5.0
106
+ numpy==1.23.0
107
+ scikit-learn==1.1.0
108
+ nltk==3.7.0
109
+ spacy==3.4.0
110
+ transformers==4.20.0
111
+ pyarabic==0.6.15
112
+ sqlalchemy==1.4.40
113
+ plotly==5.9.0
114
+ pymysql==1.0.2
115
+ pdfplumber==0.7.0
116
+ python-docx==0.8.11
117
+ openpyxl==3.0.10
118
+ ezdxf==0.17.2
119
+ ```
120
+
121
+ ## الإعداد والتثبيت
122
+
123
+ ### إعداد بيئة التطوير
124
+
125
+ ```bash
126
+ # إنشاء بيئة Python افتراضية
127
+ python -m venv venv
128
+ source venv/bin/activate # Linux/MacOS
129
+ venv\Scripts\activate # Windows
130
+
131
+ # تثبيت المكتبات المطلوبة
132
+ pip install -r requirements.txt
133
+ pip install -r arabic_support_requirements.txt
134
+ ```
135
+
136
+ ### تثبيت نماذج معالجة اللغة العربية
137
+
138
+ ```bash
139
+ # تثبيت نموذج اللغة العربية لـ SpaCy
140
+ python -m spacy download ar_core_news_lg
141
+
142
+ # تحميل موارد NLTK للغة العربية
143
+ python -m nltk.downloader stopwords
144
+ python -m nltk.downloader punkt
145
+ python -m nltk.downloader wordnet
146
+ ```
147
+
148
+ ### إعداد قاعدة البيانات
149
+
150
+ #### SQLite (للتطوير المحلي)
151
+
152
+ ```bash
153
+ # إنشاء قاعدة بيانات SQLite
154
+ python setup_db.py --mode=local
155
+ ```
156
+
157
+ #### MySQL (للنشر المؤسسي)
158
+
159
+ ```bash
160
+ # إعداد قاعدة بيانات MySQL
161
+ python setup_db.py --mode=enterprise \
162
+ --db-host=YOUR_DB_HOST \
163
+ --db-user=YOUR_DB_USER \
164
+ --db-pass=YOUR_DB_PASS \
165
+ --db-name=tender_analysis_system
docs/user_manual.md ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # دليل المستخدم
2
+ ## نظام تحليل العقود والمناقصات بالذكاء الاصطناعي - شركة شبه الجزيرة للمقاولات
3
+
4
+ <p align="center">
5
+ <img src="../static/images/logo.png" alt="شعار النظام" width="200"/>
6
+ <br>
7
+ <em>الإصدار 1.0.0</em>
8
+ </p>
9
+
10
+ ## جدول المحتويات
11
+
12
+ 1. [مقدمة](#مقدمة)
13
+ 2. [بدء الاستخدام](#بدء-الاستخدام)
14
+ 3. [الواجهة الرئيسية](#الواجهة-الرئيسية)
15
+ 4. [إدارة المناقصات والعقود](#إدارة-المناقصات-والعقود)
16
+ 5. [تحليل المستندات](#تحليل-المستندات)
17
+ 6. [نظام التسعير الشامل](#نظام-التسعير-الشامل)
18
+ 7. [إدارة الموارد والتكاليف](#إدارة-الموارد-والتكاليف)
19
+ 8. [تحليل المخاطر](#تحليل-المخاطر)
20
+ 9. [إدارة المشاريع المرساة](#إدارة-المشاريع-المرساة)
21
+ 10. [التقارير والتحليلات](#التقارير-والتحليلات)
22
+ 11. [الأسئلة الشائعة](#الأسئلة-الشائعة)
23
+ 12. [استكشاف الأخطاء وإصلاحها](#استكشاف-الأخطاء-وإصلاحها)
24
+
25
+ ## مقدمة
26
+
27
+ ### حول النظام
28
+
29
+ نظام تحليل العقود والمناقصات بالذكاء الاصطناعي هو منصة متكاملة تهدف إلى مساعدة شركة شبه الجزيرة للمقاولات في تحليل وتسعير المناقصات بكفاءة عالية. يعتمد النظام على تقنيات الذكاء الاصطناعي ومعالجة اللغة العربية الطبيعية لتحليل المستندات والمساعدة في عملية التسعير واتخاذ القرارات.
30
+
31
+ ### مزايا النظام
32
+
33
+ - تحليل متقدم لكراسات الشروط والعقود باللغة العربية
34
+ - تسعير دقيق ومنهجي للمناقصات
35
+ - تحديد المخاطر وتقييمها بشكل آلي
36
+ - إدارة الموارد والتكاليف بكفاءة
37
+ - دعم المحتوى المحلي السعودي
38
+ - متابعة شاملة للمناقصات والمشاريع
39
+ - تقارير وتحليلات متقدمة لدعم اتخاذ القرار
40
+
41
+ ## بدء الاستخدام
42
+
43
+ ### تسجيل الدخول
44
+
45
+ 1. افتح تطبيق نظام تحليل العقود والمناقصات
46
+ 2. أدخل اسم المستخدم وكلمة المرور
47
+ 3. انقر على زر "تسجيل الدخول"
48
+
49
+ ![شاشة تسجيل الدخول](../static/images/screenshots/login.png)
50
+
51
+ ### الصلاحيات ومستويات الوصول
52
+
53
+ النظام يدعم عدة مستويات من الصلاحيات:
54
+
55
+ | المستوى | الوصف | الصلاحيات |
56
+ |---------|-------|-----------|
57
+ | مدير النظام | المسؤول الرئيسي عن النظام | كامل الصلاحيات |
58
+ | مدير المناقصات | مسؤول عن إدارة المناقصات | إضافة وتعديل وحذف المناقصات، التسعير |
59
+ | محلل عقود | مختص بتحليل العقود والمستندات | قراءة وتحليل المستندات |
60
+ | محاسب | مسؤول عن الجوانب المالية | الوصول للتكاليف والتسعير |
61
+ | مستخدم عادي | مستخدم بصلاحيات محدودة | عرض المناقصات والتقارير فقط |
62
+
63
+ ## الواجهة الرئيسية
64
+
65
+ ### مكونات الواجهة
66
+
67
+ ![الواجهة الرئيسية](../static/images/screenshots/dashboard.png)
68
+
69
+ 1. **شريط القوائم**: للوصول إلى الوظائف الرئيسية
70
+ 2. **لوحة المعلومات**: عرض ملخص للمناقصات والمشاريع
71
+ 3. **المناقصات النشطة**: قائمة بالمناقصات قيد الدراسة
72
+ 4. **المواعيد الهامة**: تنبيهات بالمواعيد النهائية
73
+ 5. **المؤشرات الرئيسية**: إحصائيات ومؤشرات أداء رئيسية
74
+
75
+ ### التنقل في النظام
76
+
77
+ تم تصميم شريط القوائم للوصول السريع إلى جميع وظائف النظام:
78
+
79
+ - **لوحة المعلومات**: الصفحة الرئيسية
80
+ - **المناقصات والعقود**: إدارة المناقصات وتحليل العقود
81
+ - **التسعير**: نظام التسعير الشامل
82
+ - **الموارد والتكاليف**: إدارة المواد والمعدات والعمالة
83
+ - **تحليل المخاطر**: تقييم وإدارة المخاطر
84
+ - **المشاريع**: إدارة المشاريع المرساة
85
+ - **التقارير**: التقارير والتحليلات
86
+ - **الإعدادات**: إعدادات النظام والمستخدمين
87
+
88
+ ## إدارة المناقصات والعقود
89
+
90
+ ### إضافة مناقصة جديدة
91
+
92
+ 1. انقر على "ا��مناقصات والعقود" من شريط القوائم
93
+ 2. اختر "إضافة مناقصة جديدة"
94
+ 3. املأ النموذج بالمعلومات المطلوبة:
95
+ - اسم المناقصة
96
+ - الجهة المالكة
97
+ - رقم المناقصة
98
+ - تاريخ الطرح
99
+ - تاريخ الإقفال
100
+ - موقع المشروع
101
+ - نوع المشروع
102
+
103
+ ![إضافة مناقصة](../static/images/screenshots/add_tender.png)
104
+
105
+ ### رفع المستندات
106
+
107
+ 1. من صفحة تفاصيل المناقصة، انقر على "رفع مستند"
108
+ 2. اختر نوع المستند:
109
+ - كراسة شروط
110
+ - جدول كميات
111
+ - مخططات
112
+ - عقد
113
+ - ملحق
114
+ 3. انقر على "استعراض" واختر الملف من جهازك
115
+ 4. يدعم النظام صيغ المستندات التالية: PDF, DOCX, XLSX, DWG
116
+
117
+ ### متابعة حالة المناقصات
118
+
119
+ يوفر النظام لوحة متابعة للمناقصات تعرض:
120
+
121
+ - المناقصات قيد الدراسة
122
+ - المناقصات المقدمة
123
+ - المناقصات المرساة
124
+ - المناقصات المستبعدة
125
+
126
+ لكل مناقصة، يعرض النظام:
127
+ - الحالة الحالية
128
+ - نسبة الإنجاز
129
+ - المواعيد النهائية
130
+ - المهام المتبقية
131
+
132
+ ## تحليل المستندات
133
+
134
+ ### كيفية تحليل المستندات
135
+
136
+ 1. من صفحة تفاصيل المناقصة، اختر المستند المراد تحليله
137
+ 2. انقر على زر "تحليل المستند"
138
+ 3. اختر نوع التحليل:
139
+ - تحليل كامل
140
+ - استخراج البنود والشروط
141
+ - تحديد المخاطر
142
+ - استخراج معلومات التسعير
143
+
144
+ ![تحليل المستندات](../static/images/screenshots/document_analysis.png)
145
+
146
+ ### مراجعة نتائج التحليل
147
+
148
+ بعد اكتمال التحليل، يعرض النظام:
149
+
150
+ 1. **البنود المستخرجة**: قائمة بالبنود والشروط المهمة مرتبة حسب أهميتها
151
+ 2. **المخاطر المحددة**: المخاطر المحتملة مصنفة حسب نوعها وأهميتها
152
+ 3. **المتطلبات الرئيسية**: قائمة بالمتطلبات الأساسية للمناقصة
153
+ 4. **الكلمات المفتاحية**: الكلمات والمصطلحات المهمة في المستند
154
+
155
+ يمكنك النقر على أي بند لعرض النص الأصلي في المستند وسياقه.
156
+
157
+ ## نظام التسعير الشامل
158
+
159
+ ### بدء عملية التسعير
160
+
161
+ 1. من صفحة تفاصيل المناقصة، انقر على "بدء التسعير"
162
+ 2. اختر جدول الكميات المراد تسعيره
163
+ 3. حدد نوع التسعير:
164
+ - تسعير قياسي
165
+ - تسعير غير متزن
166
+ - تسعير مختلط
167
+
168
+ ![بدء التسعير](../static/images/screenshots/pricing_start.png)
169
+
170
+ ### تسعير البنود
171
+
172
+ 1. لكل بند في جدول الكميات، يعرض النظام:
173
+ - وصف البند
174
+ - الوحدة
175
+ - الكمية
176
+ - التكاليف المقدرة (المواد، العمالة، المعدات)
177
+ 2. يمكنك تعديل التكاليف يدوياً أو الاعتماد على التقديرات الآلية
178
+ 3. النظام يحسب تلقائياً:
179
+ - المصاريف العامة
180
+ - هامش الربح
181
+ - السعر الإجمالي
182
+
183
+ ### التسعير غير المتزن
184
+
185
+ لتطبيق استراتيجية التسعير غير المتزن:
186
+
187
+ 1. انقر على "التسعير غير المتزن" من صفحة التسعير
188
+ 2. اختر نوع الاستراتيجية:
189
+ - التحميل الأمامي
190
+ - التحميل الخلفي
191
+ - التسعير الاستراتيجي
192
+ - التسعير القائم على المخاطر
193
+ 3. عدل المعلمات حسب الحاجة
194
+ 4. راجع التغييرات في توزيع التكاليف والأسعار
195
+
196
+ ![التسعير غير المتزن](../static/images/screenshots/unbalanced_pricing.png)
197
+
198
+ ### المحتوى المحلي
199
+
200
+ لحساب وتحسين نسبة المحتوى المحلي:
201
+
202
+ 1. انقر على "المحتوى المحلي" من صفحة التسعير
203
+ 2. قم بتقييم المعايير المختلفة:
204
+ - نسبة الموظفين السعوديين
205
+ - نسبة المواد المحلية
206
+ - نسبة المعدات المحلية
207
+ - نسبة المقاولين من الباطن المحليين
208
+ 3. راجع الدرجة الإجمالية للمحتوى المحلي والأفضلية السعرية المقابلة
209
+
210
+ ## إدارة الموارد والتكاليف
211
+
212
+ ### إدارة المواد
213
+
214
+ 1. انقر على "الموارد والتكاليف" من شريط القوائم
215
+ 2. اختر "المواد"
216
+ 3. يمكنك:
217
+ - استعراض قائمة المواد
218
+ - إضافة مو��د جديدة
219
+ - تحديث أسعار المواد
220
+ - ربط المواد بالموردين
221
+
222
+ ![إدارة المواد](../static/images/screenshots/materials.png)
223
+
224
+ ### إدارة المعدات
225
+
226
+ 1. انقر على "الموارد والتكاليف" من شريط القوائم
227
+ 2. اختر "المعدات"
228
+ 3. يمكنك:
229
+ - استعراض قائمة المعدات
230
+ - تسجيل معدلات الأداء
231
+ - تحديث أسعار التأجير
232
+ - تسجيل تكاليف التشغيل
233
+
234
+ ### إدارة العمالة
235
+
236
+ 1. انقر على "الموارد والتكاليف" من شريط القوائم
237
+ 2. اختر "العمالة"
238
+ 3. يمكنك:
239
+ - استعراض فئات العمالة
240
+ - تسجيل معدلات الإنتاجية
241
+ - تحديث أسعار العمالة
242
+ - تكوين فرق العمل النموذجية
243
+
244
+ ## تحليل المخاطر
245
+
246
+ ### تقييم المخاطر
247
+
248
+ 1. انقر على "تحليل المخاطر" من شريط القوائم
249
+ 2. اختر المناقصة المراد تقييم مخاطرها
250
+ 3. يعرض النظام المخاطر المحددة مصنفة إلى:
251
+ - مخاطر تعاقدية
252
+ - مخاطر مالية
253
+ - مخاطر فنية
254
+ - مخاطر لوجستية
255
+
256
+ ![تحليل المخاطر](../static/images/screenshots/risk_analysis.png)
257
+
258
+ ### إدارة المخاطر
259
+
260
+ لكل خطر محدد، يمكنك:
261
+
262
+ 1. مراجعة تفاصيل الخطر
263
+ 2. تعديل تقييم احتمالية الحدوث والتأثير
264
+ 3. إضافة إجراءات التخفيف
265
+ 4. تعيين مسؤول المتابعة
266
+ 5. تحديد تكلفة التخفيف
267
+
268
+ ## إدارة المشاريع المرساة
269
+
270
+ ### متابعة المشاريع
271
+
272
+ 1. انقر على "المشاريع" من شريط القوائم
273
+ 2. اختر المشروع المراد متابعته
274
+ 3. يعرض النظام:
275
+ - ملخص المشروع
276
+ - حالة التنفيذ
277
+ - المستخلصات
278
+ - المراسلات
279
+
280
+ ![متابعة المشاريع](../static/images/screenshots/project_management.png)
281
+
282
+ ### إدارة المستخلصات
283
+
284
+ 1. من صفحة تفاصيل المشروع، انقر على "المستخلصات"
285
+ 2. يمكنك:
286
+ - إنشاء مستخلص جديد
287
+ - متابعة حالة المستخلصات
288
+ - الاطلاع على المدفوعات
289
+
290
+ ## التقارير والتحليلات
291
+
292
+ ### إنشاء التقارير
293
+
294
+ 1. انقر على "التقارير" من شريط القوائم
295
+ 2. اختر نوع التقرير:
296
+ - تقرير المناقصات
297
+ - تقرير المشاريع
298
+ - تقرير مالي
299
+ - تقرير المخاطر
300
+ 3. حدد معايير التقرير
301
+ 4. انقر على "إنشاء التقرير"
302
+
303
+ ![التقارير](../static/images/screenshots/reports.png)
304
+
305
+ ### تصدير التقارير
306
+
307
+ يمكن تصدير التقارير بصيغ متعددة:
308
+ - PDF
309
+ - Excel
310
+ - Word
311
+ - PowerPoint
312
+
313
+ ## الأسئلة الشائعة
314
+
315
+ ### أسئلة عامة
316
+
317
+ **س: كيف يمكنني الحصول على حساب للنظام؟**
318
+ ج: يرجى التواصل مع مدير النظام في شركتك.
319
+
320
+ **س: هل يمكن استخدام النظام عبر الأجهزة المحمولة؟**
321
+ ج: نعم، النظام متوافق مع جميع الأجهزة بما فيها الهواتف الذكية والأجهزة اللوحية.
322
+
323
+ ### أسئلة عن تحليل المستندات
324
+
325
+ **س: ما هي أنواع المستندات التي يدعمها النظام؟**
326
+ ج: يدعم النظام مستندات PDF وWord وExcel والمخططات DWG.
327
+
328
+ **س: هل يستطيع النظام تحليل المستندات الممسوحة ضوئياً؟**
329
+ ج: نعم، يمكن للنظام تحليل المستندات الممسوحة ضوئياً، لكن دقة التحليل تعتمد على جودة المسح.
330
+
331
+ ### أسئلة عن التسعير
332
+
333
+ **س: كيف يحدد النظام تكاليف المواد والعمالة؟**
334
+ ج: يعتمد النظام على قاعدة بيانات الأسعار المتاحة ومعدلات الأداء المسجلة.
335
+
336
+ **س: ما هو التسعير غير المتزن؟**
337
+ ج: هو استراتيجية لتوزيع التكاليف بشكل غير متساوٍ على بنود المناقصة لتحقيق ميزة تنافسية أو تحسين التدفق النقدي.
338
+
339
+ ## استكشاف الأخطاء وإصلاحها
340
+
341
+ ### مشاكل تسجيل الدخول
342
+
343
+ **المشكلة: لا يمكن تسجيل الدخول**
344
+ الحل:
345
+ 1. تأكد من صحة اسم المستخدم وكلمة المرور
346
+ 2. تأكد من اتصالك بالإنترنت
347
+ 3. امسح ذاكرة التخزين المؤقت للمتصفح
348
+ 4. إذا استمرت المشكلة، تواصل مع الدعم الفني
349
+
350
+ ### مشاكل تحليل المستندات
351
+
352
+ **المشكلة: فشل تحلي�� المستند**
353
+ الحل:
354
+ 1. تأكد من أن المستند بتنسيق مدعوم
355
+ 2. تحقق من جودة المسح إذا كان المستند ممسوحاً ضوئياً
356
+ 3. قلل حجم الملف إذا كان كبيراً جداً
357
+ 4. جرب تقسيم المستند إلى أجزاء أصغر
358
+
359
+ ### مشاكل التسعير
360
+
361
+ **المشكلة: عدم ظهور التكاليف المقدرة**
362
+ الحل:
363
+ 1. تأكد من تحديث قاعدة بيانات الأسعار
364
+ 2. تحقق من صحة وحدات البنود
365
+ 3. تأكد من ربط البنود بالمواد والعمالة المناسبة
366
+ 4. أعد تشغيل عملية التسعير
367
+
368
+ ---
369
+
370
+ لمزيد من المساعدة، يرجى التواصل مع:
371
+ - البريد الإلكتروني: [email protected]
372
+ - رقم الهاتف: +966 123456789
373
+ - نظام التذاكر: https://support.peninsula-contracting.com
huggingface_app.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import streamlit as st
4
+
5
+ # إضافة المسارات للعثور على الوحدات
6
+ current_dir = os.path.dirname(os.path.abspath(__file__))
7
+ sys.path.append(current_dir)
8
+
9
+ # استيراد التطبيق الرئيسي
10
+ try:
11
+ from app import main
12
+ except ImportError:
13
+ # محاولة استيراد بطريقة بديلة إذا فشلت الطريقة الأولى
14
+ try:
15
+ from tender_analysis_system.app import main
16
+ except ImportError:
17
+ st.error("❌ فشل استيراد التطبيق الرئيسي. تأكد من هيكل المجلدات وتثبيت المكتبات.")
18
+ st.info("ℹ️ قم بالتحقق من ملف requirements.txt وتأكد من تثبيت جميع المكتبات المطلوبة.")
19
+
20
+ # عرض تعليمات حول كيفية إصلاح المشكلة
21
+ with st.expander("🛠️ كيفية إصلاح المشكلة"):
22
+ st.markdown("""
23
+ ## خطوات إصلاح مشكلة الاستيراد
24
+
25
+ 1. تأكد من تثبيت جميع المكتبات المطلوبة:
26
+ ```bash
27
+ pip install -r requirements.txt
28
+ ```
29
+
30
+ 2. تأكد من هيكل المجلدات:
31
+ ```
32
+ /
33
+ ├── huggingface_app.py # هذا الملف الحالي
34
+ ├── app.py # التطبيق الرئيسي
35
+ ├── config.py # ملف الإعدادات
36
+ └── modules/ # وحدات التطبيق
37
+ ├── pricing/
38
+ ├── document_analysis/
39
+ └── ...
40
+ ```
41
+
42
+ 3. قم بفحص سجل الأخطاء أدناه:
43
+ """)
44
+ st.code(str(sys.path), language="python")
45
+
46
+ # إظهار واجهة بديلة بسيطة
47
+ st.header("🚧 نظام تحليل المناقصات والعقود")
48
+ st.subheader("لم يتم تحميل التطبيق بنجاح")
49
+ st.write("هناك مشكلة في تحميل تطبيق تحليل المناقصات. يرجى مراجعة الإعدادات وإعادة المحاولة.")
50
+
51
+ # الخروج من السكريبت
52
+ sys.exit(1)
53
+
54
+ # تهيئة إعدادات الصفحة
55
+ st.set_page_config(
56
+ page_title="نظام تحليل المناقصات والعقود",
57
+ page_icon="📊",
58
+ layout="wide",
59
+ initial_sidebar_state="expanded",
60
+ menu_items={
61
+ 'About': "تطبيق تحليل المناقصات والعقود - إصدار 2.0"
62
+ }
63
+ )
64
+
65
+ # تهيئة متغيرات البيئة
66
+ def setup_environment():
67
+ """تهيئة متغيرات البيئة اللازمة للتطبيق"""
68
+ # التحقق من وجود مفاتيح API
69
+ if os.environ.get("ANTHROPIC_API_KEY") is None:
70
+ st.warning("⚠️ مفتاح API لـ Anthropic غير موجود. بعض الميزات قد لا تعمل.")
71
+ api_key = st.text_input("أدخل مفتاح Anthropic API الخاص بك:", type="password")
72
+ if api_key:
73
+ os.environ["ANTHROPIC_API_KEY"] = api_key
74
+ st.success("✅ تم تعيين مفتاح Anthropic API!")
75
+
76
+ if os.environ.get("HUGGINGFACE_API_KEY") is None:
77
+ st.warning("⚠️ مفتاح API لـ Hugging Face غير موجود. بعض الميزات قد لا تعمل.")
78
+ api_key = st.text_input("أدخل مفتاح Hugging Face API الخاص بك:", type="password")
79
+ if api_key:
80
+ os.environ["HUGGINGFACE_API_KEY"] = api_key
81
+ st.success("✅ تم تعيين مفتاح Hugging Face API!")
82
+
83
+ # تشغيل التطبيق
84
+ if __name__ == "__main__":
85
+ setup_environment()
86
+ main()
improved_app.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ # هذا هو المكان الوحيد المسموح بوضع إعدادات الصفحة فيه
4
+ # Streamlit يتطلب أن يكون هذا الأمر في بداية التطبيق وفي ملف واحد فقط
5
+ st.set_page_config(
6
+ page_title="نظام تحليل العقود والمناقصات",
7
+ page_icon="📋",
8
+ layout="wide",
9
+ initial_sidebar_state="expanded",
10
+ menu_items={
11
+ 'About': "تطبيق تحليل العقود والمناقصات بالذكاء الاصطناعي - إصدار 2.0",
12
+ 'Get help': "https://www.wahbi-ai.com/help",
13
+ 'Report a bug': "https://www.wahbi-ai.com/report-bug"
14
+ }
15
+ )
16
+
17
+ # باقي الاستيرادات عادي
18
+ import os
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ # إضافة مسار المشروع الرئيسي إلى Python path
23
+ ROOT_DIR = Path(__file__).parent
24
+ sys.path.append(str(ROOT_DIR))
25
+
26
+ # استيراد الإعدادات
27
+ import config
28
+
29
+ # استيراد الوحدات
30
+ from modules.projects.projects_app import ProjectsApp
31
+ from modules.pricing.pricing_app import PricingApp
32
+ from modules.resources.resources_app import ResourcesApp
33
+ from modules.document_analysis.document_analysis_app import DocumentAnalysisApp
34
+ from modules.risk_analysis.risk_analysis_app import RiskAnalysisApp
35
+ from modules.reports.reports_app import ReportsApp
36
+ from modules.ai_assistant.ai_assistant_app import AIAssistantApp
37
+
38
+ # استيراد المكونات المشتركة المحسنة
39
+ from utils.components.improved_sidebar import render_sidebar
40
+ from utils.components.improved_header import render_header
41
+ from utils.improved_session_state import initialize_session_state
42
+ from utils.components.improved_system_innovation import display_innovations
43
+ from utils.components.improved_credits import display_credits
44
+
45
+ # تضمين CSS المحسن
46
+ with open(os.path.join(ROOT_DIR, 'static', 'css', 'improved_styles.css')) as f:
47
+ st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
48
+
49
+ # تضمين CSS للتوافق مع RTL
50
+ with open(os.path.join(ROOT_DIR, 'static', 'css', 'rtl-fixes.css')) as f:
51
+ st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
52
+
53
+ # تهيئة حالة الجلسة
54
+ initialize_session_state()
55
+
56
+ # عرض ترويسة الصفحة
57
+ render_header()
58
+
59
+ # عرض الشريط الجانبي
60
+ selected_module = render_sidebar()
61
+
62
+ # تهيئة وحدات النظام
63
+ modules = {
64
+ "الرئيسية": None, # سيتم التعامل معها بشكل خاص
65
+ "إدارة المشاريع": ProjectsApp(),
66
+ "التسعير المتكاملة": PricingApp(),
67
+ "الموارد والتكاليف": ResourcesApp(),
68
+ "تحليل المستندات": DocumentAnalysisApp(),
69
+ "تحليل المخاطر": RiskAnalysisApp(),
70
+ "التقارير والتحليلات": ReportsApp(),
71
+ "المساعد الذكي": AIAssistantApp()
72
+ }
73
+
74
+ # عرض الوحدة المختارة
75
+ if selected_module == "الرئيسية":
76
+ # عرض الصفحة الرئيسية
77
+ st.markdown("<h1 class='main-title'>النظام الشامل لتحليل العقود والمناقصات بالذكاء الاصطناعي</h1>", unsafe_allow_html=True)
78
+
79
+ # عرض لوحة معلومات عامة
80
+ col1, col2, col3 = st.columns(3)
81
+
82
+ with col1:
83
+ st.info("#### المناقصات النشطة\n\n**15** مناقصة", icon="📝")
84
+
85
+ with col2:
86
+ st.success("#### المشاريع المرساة\n\n**8** مشاريع", icon="✅")
87
+
88
+ with col3:
89
+ st.warning("#### مناقصات قيد التسعير\n\n**5** مناقصات", icon="⏳")
90
+
91
+ # عرض الابتكارات النظامية
92
+ st.markdown("## الابتكارات النظامية")
93
+ display_innovations()
94
+
95
+ # عرض المخطط العام للنظام
96
+ st.markdown("## هيكل النظام")
97
+
98
+ st.markdown("""
99
+ ```mermaid
100
+ graph TD
101
+ MAIN[النظام الشامل لتحليل العقود والمناقصات بالذكاء الاصطناعي] --> A
102
+ MAIN --> B
103
+ MAIN --> C
104
+ MAIN --> D
105
+ MAIN --> E
106
+ MAIN --> F
107
+
108
+ A[وحدة تحليل المستندات]
109
+ B[وحدة التسعير المتكاملة]
110
+ C[وحدة الموارد والتكاليف]
111
+ D[وحدة تحليل المخاطر]
112
+ E[وحدة إدارة المشاريع]
113
+ F[وحدة التقارير والتحليلات]
114
+
115
+ DB[(قاعدة البيانات المركزية)] --> A
116
+ DB --> B
117
+ DB --> C
118
+ DB --> D
119
+ DB --> E
120
+ DB --> F
121
+
122
+ AI{وحدة الذكاء الاصطناعي} --> A
123
+ AI --> B
124
+ AI --> F
125
+ ```
126
+ """)
127
+
128
+ # عرض معلومات الفريق
129
+ st.markdown("## فريق التطوير")
130
+ display_credits()
131
+
132
+ else:
133
+ # عرض الوحدة المختارة
134
+ module = modules.get(selected_module)
135
+ if module:
136
+ module.render()
137
+
138
+ # إضافة تذييل الصفحة
139
+ st.markdown("""
140
+ <div class="footer">
141
+ <p>نظام تحليل العقود والمناقصات بالذكاء الاصطناعي - الإصدار 2.0</p>
142
+ <p>© 2025 شركة شبه الجزيرة للمقاولات - جميع الحقوق محفوظة</p>
143
+ </div>
144
+ """, unsafe_allow_html=True)
models/README.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # نماذج التعلم الآلي
2
+
3
+ يحتوي هذا المجلد على نماذج التعلم الآلي المستخدمة في نظام تسعير المناقصات.
4
+
5
+ ## هيكل المجلد
6
+
7
+ - `trained/`: يحتوي على النماذج المدربة جاهزة للاستخدام
8
+ - `datasets/`: يحتوي على مجموعات البيانات المستخدمة في تدريب النماذج
9
+
10
+ ## النماذج المستخدمة
11
+
12
+ يستخدم النظام مجموعة من نماذج التعلم الآلي تشمل:
13
+
14
+ 1. **نموذج التنبؤ بالتكاليف**: يستخدم لتقدير تكاليف المشاريع بناءً على خصائص المشروع
15
+ 2. **نموذج تقييم المخاطر**: يقيم المخاطر المحتملة للمشروع ويقدر تأثيرها
16
+ 3. **نموذج التنبؤ بالمحتوى المحلي**: يتنبأ بنسبة المحتوى المحلي المتوقعة للمشروع
17
+ 4. **نموذج التصنيف الذكي للمستندات**: يصنف مستندات المناقصة تلقائيًا
18
+ 5. **نموذج التعرف على الكيانات**: يستخرج الكيانات المهمة من مستندات المناقصة
19
+
20
+ ## كيفية استخدام النماذج
21
+
22
+ لاستخدام النماذج في التطبيق:
23
+
24
+ ```python
25
+ from models.inference import load_cost_prediction_model, predict_cost
26
+
27
+ # تحميل النموذج
28
+ model = load_cost_prediction_model()
29
+
30
+ # التنبؤ
31
+ features = {
32
+ 'project_type': 'construction',
33
+ 'area': 5000,
34
+ 'location': 'Riyadh',
35
+ 'duration_months': 18
36
+ }
37
+
38
+ predicted_cost = predict_cost(model, features)
39
+ print(f"التكلفة المتوقعة: {predicted_cost} ريال")
40
+ ```
41
+
42
+ ## تدريب النماذج
43
+
44
+ يمكن إعادة تدريب النماذج باستخدام البيانات الجديدة من خلال:
45
+
46
+ ```python
47
+ from models.training import train_cost_prediction_model
48
+
49
+ # تدريب النموذج
50
+ train_cost_prediction_model(new_data_path="datasets/new_cost_data.csv",
51
+ output_model_path="trained/cost_prediction_v2.pkl")
52
+ ```
models/datasets/README.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # مجموعات البيانات
2
+
3
+ يحتوي هذا المجلد على مجموعات البيانات المستخدمة لتدريب نماذج التعلم الآلي في نظام تسعير المناقصات.
4
+
5
+ ## المجموعات المتوفرة
6
+
7
+ - `cost_data.csv`: بيانات تكاليف المشاريع السابقة
8
+ - `risk_data.csv`: بيانات المخاطر وتأثيراتها
9
+ - `local_content_data.csv`: بيانات المحتوى المحلي
10
+ - `documents_data.csv`: بيانات المستندات المصنفة
11
+ - `entities_data.csv`: بيانات الكيانات المستخرجة
12
+
13
+ ## هيكل مجموعات البيانات
14
+
15
+ ### cost_data.csv
16
+
17
+ بيانات تكاليف المشاريع السابقة مع خصائص كل مشروع:
18
+
19
+ | العمود | الوصف | النوع |
20
+ |--------|-------|------|
21
+ | project_id | رقم المشروع | نص |
22
+ | project_type | نوع المشروع | نص |
23
+ | location | الموقع | نص |
24
+ | area | المساحة (م²) | رقم |
25
+ | floors | عدد الطوابق | رقم |
26
+ | duration_months | مدة التنفيذ (شهور) | رقم |
27
+ | tender_type | نوع المناقصة | نص |
28
+ | client_type | نوع العميل | نص |
29
+ | total_cost | إجمالي التكلفة | رقم |
30
+ | cost_per_sqm | تكلفة المتر المربع | رقم |
31
+ | material_cost | تكلفة المواد | رقم |
32
+ | labor_cost | تكلفة العمالة | رقم |
33
+ | equipment_cost | تكلفة المعدات | رقم |
34
+ | overhead_percentage | نسبة المصاريف العامة | رقم |
35
+
36
+ ### risk_data.csv
37
+
38
+ بيانات المخاطر وتأثيراتها:
39
+
40
+ | العمود | الوصف | النوع |
41
+ |--------|-------|------|
42
+ | risk_id | رقم المخاطرة | نص |
43
+ | project_id | رقم المشروع | نص |
44
+ | risk_category | فئة المخاطرة | نص |
45
+ | risk_description | وصف المخاطرة | نص |
46
+ | impact | التأثير | نص |
47
+ | probability | الاحتمالية | نص |
48
+ | risk_score | درجة المخاطرة | رقم |
49
+ | response_strategy | استراتيجية الاستجابة | نص |
50
+ | actual_impact | التأثير الفعلي | نص |
51
+ | actual_cost | التكلفة الفعلية | رقم |
52
+
53
+ ## الإحصاءات
54
+
55
+ - عدد المشاريع: 500+
56
+ - الفترة الزمنية: 2018-2024
57
+ - التوزيع الجغرافي: جميع مناطق المملكة العربية السعودية
58
+
59
+ ## الترخيص والقيود
60
+
61
+ هذه البيانات للاستخدام الداخلي فقط ولا يجوز مشاركتها خارج الشركة.
models/trained/README.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # النماذج المدربة
2
+
3
+ يحتوي هذا المجلد على النماذج المدربة الجاهزة للاستخدام في نظام تسعير المناقصات.
4
+
5
+ ## النماذج المتوفرة
6
+
7
+ - `cost_prediction.pkl`: نموذج التنبؤ بالتكاليف (Random Forest)
8
+ - `risk_assessment.pkl`: نموذج تقييم المخاطر (Gradient Boosting)
9
+ - `local_content_prediction.pkl`: نموذج التنبؤ بالمحتوى المحلي (XGBoost)
10
+ - `document_classifier.pkl`: نموذج تصنيف المستندات (BERT فائق)
11
+ - `entity_recognition.pkl`: نموذج التعرف على الكيانات (BiLSTM-CRF)
12
+
13
+ ## إصدارات النماذج
14
+
15
+ | النموذج | الإصدار | تاريخ التدريب | المؤشرات الرئيسية | مجموعة التدريب |
16
+ |---------|---------|----------------|-------------------|----------------|
17
+ | cost_prediction.pkl | v1.2 | 2024-02-15 | MAE: 45,000 ريال | 500 مشروع |
18
+ | risk_assessment.pkl | v1.1 | 2024-02-10 | Accuracy: 87% | 350 مشروع |
19
+ | local_content_prediction.pkl | v1.0 | 2024-01-25 | RMSE: 3.2% | 280 مشروع |
20
+ | document_classifier.pkl | v2.1 | 2024-03-01 | F1: 0.92 | 1200 مستند |
21
+ | entity_recognition.pkl | v1.3 | 2024-03-05 | F1: 0.88 | 800 مستند |
22
+
23
+ ## ملاحظات الاستخدام
24
+
25
+ - تم تدريب النماذج على بيانات مشاريع البناء والإنشاءات في المملكة العربية السعودية
26
+ - يتم تحديث النماذج بشكل دوري كل 3 أشهر لضمان دقتها
27
+ - للحصول على أفضل النتائج، استخدم البيانات بنفس التنسيق المستخدم في التدريب
modules/ai_assistant/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """
2
+ وحدة المساعد الذكي
3
+ """
4
+
5
+ __version__ = '1.0.0'
modules/ai_assistant/ai_assistant_app.py ADDED
The diff for this file is too large to render. See raw diff
 
modules/ai_models/ai_models_app.py ADDED
@@ -0,0 +1,817 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ وحدة نماذج الذكاء الاصطناعي المتكاملة
3
+ """
4
+
5
+ import streamlit as st
6
+ import pandas as pd
7
+ import random
8
+ from datetime import datetime
9
+ import time
10
+
11
+ class AIModelsApp:
12
+ """
13
+ وحدة نماذج الذكاء الاصطناعي المتكاملة للنظام
14
+ """
15
+
16
+ def __init__(self):
17
+ """
18
+ تهيئة وحدة نماذج الذكاء الاصطناعي
19
+ """
20
+ # تهيئة حالة الجلسة الخاصة بنماذج الذكاء الاصطناعي إذا لم تكن موجودة
21
+ if 'ai_models' not in st.session_state:
22
+ # إنشاء بيانات تجريبية لنماذج الذكاء الاصطناعي
23
+ st.session_state.ai_models = self._generate_sample_models()
24
+
25
+ if 'api_keys' not in st.session_state:
26
+ # إنشاء بيانات تجريبية لمفاتيح API
27
+ st.session_state.api_keys = {
28
+ 'openai': 'sk-**************************',
29
+ 'huggingface': 'hf_**************************',
30
+ 'azure': 'az_**************************',
31
+ 'local': 'local_key_not_required'
32
+ }
33
+
34
+ if 'model_usage' not in st.session_state:
35
+ # إنشاء بيانات تجريبية لاستخدام النماذج
36
+ st.session_state.model_usage = self._generate_sample_usage()
37
+
38
+ def run(self):
39
+ """
40
+ تشغيل وحدة نماذج الذكاء الاصطناعي
41
+ """
42
+ st.markdown("<h2 class='module-title'>وحدة نماذج الذكاء الاصطناعي</h2>", unsafe_allow_html=True)
43
+
44
+ # إنشاء تبويبات لنماذج الذكاء الاصطناعي المختلفة
45
+ tabs = st.tabs(["النماذج المتاحة", "استخدام النماذج", "إدارة API", "سجل الاستخدام"])
46
+
47
+ with tabs[0]:
48
+ self._render_available_models()
49
+
50
+ with tabs[1]:
51
+ self._render_model_usage()
52
+
53
+ with tabs[2]:
54
+ self._render_api_management()
55
+
56
+ with tabs[3]:
57
+ self._render_usage_history()
58
+
59
+ def _render_available_models(self):
60
+ """
61
+ عرض النماذج المتاحة
62
+ """
63
+ st.markdown("### النماذج المتاحة")
64
+ st.markdown("عرض نماذج الذكاء الاصطناعي المتاحة للاستخدام في النظام")
65
+
66
+ # فلترة النماذج
67
+ col1, col2 = st.columns(2)
68
+
69
+ with col1:
70
+ filter_type = st.multiselect(
71
+ "نوع النموذج",
72
+ options=["الكل", "تحليل نصوص", "استخراج بيانات", "تصنيف مستندات", "تلخيص", "ترجمة", "تنبؤ"],
73
+ default=["الكل"]
74
+ )
75
+
76
+ with col2:
77
+ filter_provider = st.multiselect(
78
+ "مزود الخدمة",
79
+ options=["الكل", "OpenAI", "HuggingFace", "Azure", "محلي"],
80
+ default=["الكل"]
81
+ )
82
+
83
+ # تطبيق التصفية
84
+ filtered_models = st.session_state.ai_models
85
+
86
+ if "الكل" not in filter_type:
87
+ filtered_models = [m for m in filtered_models if m['type'] in filter_type]
88
+
89
+ if "الكل" not in filter_provider:
90
+ filtered_models = [m for m in filtered_models if m['provider'] in filter_provider]
91
+
92
+ # عرض النماذج
93
+ if filtered_models:
94
+ # تقسيم النماذج إلى صفوف
95
+ for i in range(0, len(filtered_models), 3):
96
+ cols = st.columns(3)
97
+ for j in range(3):
98
+ if i + j < len(filtered_models):
99
+ model = filtered_models[i + j]
100
+ with cols[j]:
101
+ st.markdown(f"""
102
+ <div style="border: 1px solid #dee2e6; border-radius: 8px; padding: 15px; height: 100%;">
103
+ <h4 style="color: var(--primary-color);">{model['name']}</h4>
104
+ <p><strong>النوع:</strong> {model['type']}</p>
105
+ <p><strong>المزود:</strong> {model['provider']}</p>
106
+ <p><strong>الإصدار:</strong> {model['version']}</p>
107
+ <p><strong>الحالة:</strong> <span style="color: {'green' if model['status'] == 'متاح' else 'orange'};">{model['status']}</span></p>
108
+ <p>{model['description']}</p>
109
+ </div>
110
+ """, unsafe_allow_html=True)
111
+ else:
112
+ st.info("لا توجد نماذج تطابق معايير التصفية", icon="ℹ️")
113
+
114
+ def _render_model_usage(self):
115
+ """
116
+ عرض واجهة استخدام النماذج
117
+ """
118
+ st.markdown("### استخدام نماذج الذكاء الاصطناعي")
119
+ st.markdown("استخدام نماذج الذكاء الاصطناعي لمعالجة البيانات والمستندات")
120
+
121
+ # اختيار النموذج
122
+ model_names = [m['name'] for m in st.session_state.ai_models if m['status'] == 'متاح']
123
+ selected_model = st.selectbox("اختر النموذج", options=model_names, key="selected_model")
124
+
125
+ # الحصول على معلومات النموذج المحدد
126
+ model_info = next((m for m in st.session_state.ai_models if m['name'] == selected_model), None)
127
+
128
+ if model_info:
129
+ st.markdown(f"""
130
+ <div style="background-color: var(--gray-100); padding: 15px; border-radius: 8px; margin-bottom: 20px;">
131
+ <h4 style="color: var(--primary-color);">{model_info['name']}</h4>
132
+ <p><strong>النوع:</strong> {model_info['type']}</p>
133
+ <p><strong>المزود:</strong> {model_info['provider']}</p>
134
+ <p><strong>الوصف:</strong> {model_info['description']}</p>
135
+ </div>
136
+ """, unsafe_allow_html=True)
137
+
138
+ # واجهة الاستخدام حسب نوع النموذج
139
+ if model_info['type'] in ["تحليل نصوص", "تلخيص", "ترجمة"]:
140
+ self._render_text_model_interface(model_info)
141
+ elif model_info['type'] in ["استخراج بيانات", "تصنيف مستندات"]:
142
+ self._render_document_model_interface(model_info)
143
+ elif model_info['type'] == "تنبؤ":
144
+ self._render_prediction_model_interface(model_info)
145
+
146
+ def _render_text_model_interface(self, model_info):
147
+ """
148
+ عرض واجهة استخدام نماذج النصوص
149
+ """
150
+ # إدخال النص
151
+ input_text = st.text_area(
152
+ "أدخل النص",
153
+ height=150,
154
+ placeholder="أدخل النص للمعالجة...",
155
+ key="model_input_text"
156
+ )
157
+
158
+ # خيارات النموذج
159
+ st.markdown("#### خيارات النموذج")
160
+
161
+ col1, col2, col3 = st.columns(3)
162
+
163
+ with col1:
164
+ temperature = st.slider("درجة الإبداعية", 0.0, 1.0, 0.7, 0.1, key="temperature")
165
+
166
+ with col2:
167
+ max_tokens = st.slider("الحد الأقصى للكلمات", 100, 2000, 500, 100, key="max_tokens")
168
+
169
+ with col3:
170
+ if model_info['type'] == "ترجمة":
171
+ target_lang = st.selectbox(
172
+ "اللغة الهدف",
173
+ options=["الإنجليزية", "العربية", "الفرنسية", "الإسبانية", "الألمانية"],
174
+ key="target_lang"
175
+ )
176
+
177
+ # زر المعالجة
178
+ if st.button("معالجة النص", key="process_text_btn"):
179
+ if input_text:
180
+ # محاكاة عملية المعالجة
181
+ with st.spinner("جاري معالجة النص..."):
182
+ # محاكاة وقت المعالجة
183
+ time.sleep(2)
184
+
185
+ # إنشاء نص ناتج تجريبي
186
+ if model_info['type'] == "تحليل نصوص":
187
+ output_text = f"تحليل النص:\n\n1. يحتوي النص على {len(input_text.split())} كلمة.\n2. الموضوع الرئيسي: مناقصات ومشاريع.\n3. المشاعر: محايدة.\n4. الكلمات المفتاحية: مشروع، مناقصة، تحليل، تسعير."
188
+ elif model_info['type'] == "تلخيص":
189
+ output_text = f"ملخص النص:\n\n{input_text.split('.')[0]}. " + "هذا ملخص تجريبي للنص المدخل يحتوي على أهم النقاط والمعلومات."
190
+ elif model_info['type'] == "ترجمة":
191
+ output_text = f"الترجمة إلى {target_lang}:\n\n" + "This is a sample translation of the input text. It contains the main points and information."
192
+
193
+ # عرض النتيجة
194
+ st.markdown("#### نتيجة المعالجة")
195
+ st.text_area("النص الناتج", value=output_text, height=150, key="model_output_text")
196
+
197
+ # إضافة الاستخدام إلى السجل
198
+ self._add_usage_to_history(model_info['name'], model_info['type'], len(input_text.split()))
199
+ else:
200
+ st.warning("يرجى إدخال نص للمعالجة", icon="⚠️")
201
+
202
+ def _render_document_model_interface(self, model_info):
203
+ """
204
+ عرض واجهة استخدام نماذج المستندات
205
+ """
206
+ # رفع المستند
207
+ uploaded_file = st.file_uploader(
208
+ "اختر مستنداً للمعالجة",
209
+ type=["pdf", "docx", "txt"],
210
+ key="model_doc_upload"
211
+ )
212
+
213
+ # خيارات النموذج
214
+ st.markdown("#### خيارات النموذج")
215
+
216
+ col1, col2 = st.columns(2)
217
+
218
+ with col1:
219
+ if model_info['type'] == "استخراج بيانات":
220
+ extraction_type = st.multiselect(
221
+ "نوع البيانات المستخرجة",
222
+ options=["جداول الكميات", "الأسعار", "المواصفات الفنية", "الشروط التعاقدية", "المعلومات العامة"],
223
+ default=["جداول الكميات", "الأسعار"],
224
+ key="extraction_type"
225
+ )
226
+ elif model_info['type'] == "تصنيف مستندات":
227
+ classification_type = st.selectbox(
228
+ "نوع التصنيف",
229
+ options=["نوع المستند", "مجال المشروع", "مستوى المخاطر"],
230
+ key="classification_type"
231
+ )
232
+
233
+ with col2:
234
+ confidence_threshold = st.slider("حد الثقة", 0.0, 1.0, 0.7, 0.1, key="confidence_threshold")
235
+
236
+ # زر المعالجة
237
+ if st.button("معالجة المستند", key="process_doc_btn"):
238
+ if uploaded_file is not None:
239
+ # محاكاة عملية المعالجة
240
+ with st.spinner("جاري معالجة المستند..."):
241
+ # محاكاة وقت المعالجة
242
+ time.sleep(3)
243
+
244
+ st.success("تمت معالجة المستند بنجاح!", icon="✅")
245
+
246
+ # عرض النتائج حسب نوع النموذج
247
+ if model_info['type'] == "استخراج بيانات":
248
+ st.markdown("#### البيانات المستخرجة")
249
+
250
+ # عرض بيانات تجريبية للجداول المستخرجة
251
+ if "جداول الكميات" in extraction_type:
252
+ st.markdown("##### جدول الكميات")
253
+
254
+ # إنشاء بيانات تجريبية
255
+ quantities_data = []
256
+ for i in range(5):
257
+ quantities_data.append({
258
+ "البند": f"بند {i+1}",
259
+ "الوصف": f"وصف البند {i+1}",
260
+ "الوحدة": random.choice(["متر", "متر مربع", "متر مكعب", "طن", "قطعة"]),
261
+ "الكمية": random.randint(10, 1000),
262
+ "السعر الوحدة": random.randint(100, 5000),
263
+ "الإجمالي": 0
264
+ })
265
+ quantities_data[i]["الإجمالي"] = quantities_data[i]["الكمية"] * quantities_data[i]["السعر الوحدة"]
266
+
267
+ # عرض الجدول
268
+ quantities_df = pd.DataFrame(quantities_data)
269
+ st.dataframe(quantities_df, use_container_width=True)
270
+
271
+ if "الأسعار" in extraction_type:
272
+ st.markdown("##### ملخص الأسعار")
273
+
274
+ # إنشاء بيانات تجريبية
275
+ price_summary = {
276
+ "إجمالي قيمة المشروع": f"{random.randint(1000000, 10000000)} ريال",
277
+ "مدة التنفيذ": f"{random.randint(6, 36)} شهر",
278
+ "قيمة الدفعة المقدمة": f"{random.randint(10, 30)}%",
279
+ "غرامة التأخير": f"{random.randint(1, 10)}% (بحد أقصى 10% من قيمة العقد)"
280
+ }
281
+
282
+ # عرض الملخص
283
+ for key, value in price_summary.items():
284
+ st.markdown(f"**{key}:** {value}")
285
+
286
+ elif model_info['type'] == "تصنيف مستندات":
287
+ st.markdown("#### نتائج التصنيف")
288
+
289
+ if classification_type == "نوع المستند":
290
+ # إنشاء بيانات تجريبية
291
+ doc_types = [
292
+ {"نوع المستند": "كراسة شروط", "نسبة الثقة": 0.92},
293
+ {"نوع المستند": "عقد", "نسبة الثقة": 0.05},
294
+ {"نوع المستند": "مواصفات فنية", "نسبة الثقة": 0.02},
295
+ {"نوع المستند": "جدول كميات", "نسبة الثقة": 0.01}
296
+ ]
297
+
298
+ # عرض النتائج
299
+ doc_types_df = pd.DataFrame(doc_types)
300
+ st.dataframe(doc_types_df, use_container_width=True)
301
+
302
+ st.markdown(f"**التصنيف النهائي:** كراسة شروط (بثقة 92%)")
303
+
304
+ # إضافة الاستخدام إلى السجل
305
+ self._add_usage_to_history(model_info['name'], model_info['type'], 1)
306
+ else:
307
+ st.warning("يرجى رفع مستند للمعالجة", icon="⚠️")
308
+
309
+ def _render_prediction_model_interface(self, model_info):
310
+ """
311
+ عرض واجهة استخدام نماذج التنبؤ
312
+ """
313
+ # اختيار نوع التنبؤ
314
+ prediction_type = st.selectbox(
315
+ "نوع التنبؤ",
316
+ options=["تنبؤ بتكلفة المشروع", "تنبؤ بمدة التنفيذ", "تنبؤ بالمخاطر"],
317
+ key="prediction_type"
318
+ )
319
+
320
+ # إدخال البيانات حسب نوع التنبؤ
321
+ if prediction_type == "تنبؤ بتكلفة المشروع":
322
+ st.markdown("#### بيانات المشروع")
323
+
324
+ col1, col2 = st.columns(2)
325
+
326
+ with col1:
327
+ project_type = st.selectbox(
328
+ "نوع المشروع",
329
+ options=["طرق وجسور", "مباني", "بنية تحتية", "مياه وصرف صحي", "كهرباء"],
330
+ key="project_type"
331
+ )
332
+
333
+ project_size = st.selectbox(
334
+ "حجم المشروع",
335
+ options=["صغير", "متوسط", "كبير", "ضخم"],
336
+ key="project_size"
337
+ )
338
+
339
+ with col2:
340
+ project_location = st.selectbox(
341
+ "موقع المشروع",
342
+ options=["الرياض", "جدة", "الدمام", "مكة المكرمة", "المدينة المنورة", "تبوك", "أبها"],
343
+ key="project_location"
344
+ )
345
+
346
+ project_duration = st.slider("مدة التنفيذ (بالشهور)", 3, 60, 12, 3, key="project_duration")
347
+
348
+ elif prediction_type == "تنبؤ بمدة التنفيذ":
349
+ st.markdown("#### بيانات المشروع")
350
+
351
+ col1, col2 = st.columns(2)
352
+
353
+ with col1:
354
+ project_type = st.selectbox(
355
+ "نوع المشروع",
356
+ options=["طرق وجسور", "مباني", "بنية تحتية", "مياه وصرف صحي", "كهرباء"],
357
+ key="duration_project_type"
358
+ )
359
+
360
+ project_budget = st.number_input(
361
+ "ميزانية المشروع (بالمليون ريال)",
362
+ min_value=1.0,
363
+ max_value=1000.0,
364
+ value=10.0,
365
+ step=1.0,
366
+ key="project_budget"
367
+ )
368
+
369
+ with col2:
370
+ project_location = st.selectbox(
371
+ "موقع المشروع",
372
+ options=["الرياض", "جدة", "الدمام", "مكة المكرمة", "المدينة المنورة", "تبوك", "أبها"],
373
+ key="duration_project_location"
374
+ )
375
+
376
+ resources_level = st.selectbox(
377
+ "مستوى الموارد",
378
+ options=["منخفض", "متوسط", "عالي"],
379
+ key="resources_level"
380
+ )
381
+
382
+ elif prediction_type == "تنبؤ بالمخاطر":
383
+ st.markdown("#### بيانات المشروع")
384
+
385
+ col1, col2 = st.columns(2)
386
+
387
+ with col1:
388
+ project_type = st.selectbox(
389
+ "نوع المشروع",
390
+ options=["طرق وجسور", "مباني", "بنية تحتية", "مياه وصرف صحي", "كهرباء"],
391
+ key="risk_project_type"
392
+ )
393
+
394
+ project_complexity = st.selectbox(
395
+ "مستوى تعقيد المشروع",
396
+ options=["بسيط", "متوسط", "معقد", "معقد جداً"],
397
+ key="project_complexity"
398
+ )
399
+
400
+ with col2:
401
+ project_location = st.selectbox(
402
+ "موقع المشروع",
403
+ options=["الرياض", "جدة", "الدمام", "مكة المكرمة", "المدينة المنورة", "تبوك", "أبها"],
404
+ key="risk_project_location"
405
+ )
406
+
407
+ previous_experience = st.selectbox(
408
+ "الخبرة السابقة",
409
+ options=["لا توجد خبرة", "خبرة محدودة", "خبرة متوسطة", "خبرة واسعة"],
410
+ key="previous_experience"
411
+ )
412
+
413
+ # زر التنبؤ
414
+ if st.button("تنفيذ التنبؤ", key="predict_btn"):
415
+ # محاكاة عملية التنبؤ
416
+ with st.spinner("جاري تنفيذ التنبؤ..."):
417
+ # محاكاة وقت المعالجة
418
+ time.sleep(2)
419
+
420
+ st.success("تم تنفيذ التنبؤ بنجاح!", icon="✅")
421
+
422
+ # عرض النتائج حسب نوع التنبؤ
423
+ if prediction_type == "تنبؤ بتكلفة المشروع":
424
+ st.markdown("#### نتائج التنبؤ بالتكلفة")
425
+
426
+ # إنشاء بيانات تجريبية
427
+ base_cost = random.randint(5000000, 50000000)
428
+ min_cost = int(base_cost * 0.9)
429
+ max_cost = int(base_cost * 1.1)
430
+
431
+ st.markdown(f"""
432
+ <div style="background-color: var(--gray-100); padding: 20px; border-radius: 8px; text-align: center;">
433
+ <h3 style="color: var(--primary-color);">{base_cost:,} ريال</h3>
434
+ <p>نطاق التكلفة المتوقع: {min_cost:,} - {max_cost:,} ريال</p>
435
+ </div>
436
+ """, unsafe_allow_html=True)
437
+
438
+ # عرض تفاصيل التكلفة
439
+ st.markdown("##### تفاصيل التكلفة")
440
+
441
+ cost_details = {
442
+ "المواد": int(base_cost * 0.6),
443
+ "العمالة": int(base_cost * 0.25),
444
+ "المعدات": int(base_cost * 0.1),
445
+ "أخرى": int(base_cost * 0.05)
446
+ }
447
+
448
+ # عرض الرسم البياني
449
+ cost_df = pd.DataFrame({
450
+ "البند": list(cost_details.keys()),
451
+ "التكلفة": list(cost_details.values())
452
+ })
453
+
454
+ st.bar_chart(cost_df.set_index("البند"))
455
+
456
+ elif prediction_type == "تنبؤ بمدة التنفيذ":
457
+ st.markdown("#### نتائج التنبؤ بمدة التنفيذ")
458
+
459
+ # إنشاء بيانات تجريبية
460
+ base_duration = random.randint(12, 36)
461
+ min_duration = int(base_duration * 0.9)
462
+ max_duration = int(base_duration * 1.2)
463
+
464
+ st.markdown(f"""
465
+ <div style="background-color: var(--gray-100); padding: 20px; border-radius: 8px; text-align: center;">
466
+ <h3 style="color: var(--primary-color);">{base_duration} شهر</h3>
467
+ <p>نطاق المدة المتوقع: {min_duration} - {max_duration} شهر</p>
468
+ </div>
469
+ """, unsafe_allow_html=True)
470
+
471
+ # عرض الجدول الزمني
472
+ st.markdown("##### الجدول الزمني التقديري")
473
+
474
+ timeline_data = [
475
+ {"المرحلة": "التجهيز والتخطيط", "المدة (شهر)": int(base_duration * 0.1), "النسبة": "10%"},
476
+ {"المرحلة": "الأعمال الأولية", "المدة (شهر)": int(base_duration * 0.2), "النسبة": "20%"},
477
+ {"المرحلة": "الأعمال الرئيسية", "المدة (شهر)": int(base_duration * 0.5), "النسبة": "50%"},
478
+ {"المرحلة": "التشطيبات", "المدة (شهر)": int(base_duration * 0.15), "النسبة": "15%"},
479
+ {"المرحلة": "الاختبار والتسليم", "المدة (شهر)": int(base_duration * 0.05), "النسبة": "5%"}
480
+ ]
481
+
482
+ timeline_df = pd.DataFrame(timeline_data)
483
+ st.dataframe(timeline_df, use_container_width=True)
484
+
485
+ elif prediction_type == "تنبؤ بالمخاطر":
486
+ st.markdown("#### نتائج تحليل ��لمخاطر")
487
+
488
+ # إنشاء بيانات تجريبية للمخاطر
489
+ risks = [
490
+ {"المخاطرة": "تأخر التوريدات", "الاحتمالية": random.randint(30, 70), "التأثير": random.randint(30, 70)},
491
+ {"المخاطرة": "نقص العمالة", "الاحتمالية": random.randint(30, 70), "التأثير": random.randint(30, 70)},
492
+ {"المخاطرة": "تغيير المواصفات", "الاحتمالية": random.randint(30, 70), "التأثير": random.randint(30, 70)},
493
+ {"المخاطرة": "ظروف جوية", "الاحتمالية": random.randint(30, 70), "التأثير": random.randint(30, 70)},
494
+ {"المخاطرة": "مشاكل تمويلية", "الاحتمالية": random.randint(30, 70), "التأثير": random.randint(30, 70)}
495
+ ]
496
+
497
+ # حساب درجة المخاطرة
498
+ for risk in risks:
499
+ risk_score = (risk["الاحتمالية"] * risk["التأثير"]) / 100
500
+ if risk_score < 30:
501
+ risk["المستوى"] = "منخفض"
502
+ risk["اللون"] = "green"
503
+ elif risk_score < 60:
504
+ risk["المستوى"] = "متوسط"
505
+ risk["اللون"] = "orange"
506
+ else:
507
+ risk["المستوى"] = "مرتفع"
508
+ risk["اللون"] = "red"
509
+
510
+ # عرض جدول المخاطر
511
+ risks_df = pd.DataFrame([{k: v for k, v in risk.items() if k != "اللون"} for risk in risks])
512
+ st.dataframe(risks_df, use_container_width=True)
513
+
514
+ # عرض خطة الاستجابة للمخاطر
515
+ st.markdown("##### خطة الاستجابة للمخاطر")
516
+
517
+ for risk in risks:
518
+ if risk["المستوى"] == "مرتفع":
519
+ st.markdown(f"""
520
+ <div style="background-color: #f8d7da; padding: 10px; border-radius: 5px; margin-bottom: 10px;">
521
+ <strong>{risk['المخاطرة']} (مخاطرة مرتفعة):</strong> يجب وضع خطة استجابة فورية وتخصيص موارد إضافية للتعامل مع هذه المخاطرة.
522
+ </div>
523
+ """, unsafe_allow_html=True)
524
+
525
+ # إضافة الاستخدام إلى السجل
526
+ self._add_usage_to_history(model_info['name'], model_info['type'], 1)
527
+
528
+ def _render_api_management(self):
529
+ """
530
+ عرض واجهة إدارة مفاتيح API
531
+ """
532
+ st.markdown("### إدارة مفاتيح API")
533
+ st.markdown("إدارة مفاتيح API للوصول إلى خدمات الذكاء الاصطناعي")
534
+
535
+ # عرض مفاتيح API الحالية
536
+ st.markdown("#### مفاتيح API الحالية")
537
+
538
+ for provider, key in st.session_state.api_keys.items():
539
+ col1, col2, col3 = st.columns([3, 5, 2])
540
+
541
+ with col1:
542
+ st.markdown(f"**{provider.capitalize()}**")
543
+
544
+ with col2:
545
+ # عرض المفتاح بشكل آمن
546
+ st.text_input(
547
+ f"مفتاح {provider}",
548
+ value=key,
549
+ type="password",
550
+ key=f"api_key_{provider}",
551
+ label_visibility="collapsed"
552
+ )
553
+
554
+ with col3:
555
+ st.button("تحديث", key=f"update_{provider}_btn")
556
+
557
+ # إضافة مفتاح API جديد
558
+ st.markdown("#### إضافة مفتاح API جديد")
559
+
560
+ col1, col2, col3 = st.columns([3, 5, 2])
561
+
562
+ with col1:
563
+ new_provider = st.text_input("اسم المزود", key="new_provider")
564
+
565
+ with col2:
566
+ new_key = st.text_input("مفتاح API", type="password", key="new_key")
567
+
568
+ with col3:
569
+ st.markdown("&nbsp;") # فراغ للمحاذاة
570
+ if st.button("إضافة", key="add_api_key_btn"):
571
+ if new_provider and new_key:
572
+ st.success(f"تمت إضافة مفتاح API لـ {new_provider} بنجاح", icon="✅")
573
+ else:
574
+ st.warning("يرجى إدخال اسم المزود ومفتاح API", icon="⚠️")
575
+
576
+ # إعدادات الأمان
577
+ st.markdown("#### إعدادات الأمان")
578
+
579
+ st.checkbox("تشفير مفاتيح API في قاعدة البيا��ات", value=True, key="encrypt_api_keys")
580
+ st.checkbox("تسجيل استخدام مفاتيح API", value=True, key="log_api_usage")
581
+ st.checkbox("تحديد صلاحيات الوصول لمفاتيح API", value=False, key="api_access_control")
582
+
583
+ def _render_usage_history(self):
584
+ """
585
+ عرض سجل استخدام النماذج
586
+ """
587
+ st.markdown("### سجل استخدام النماذج")
588
+ st.markdown("عرض سجل استخدام نماذج الذكاء الاصطناعي مع إحصائيات الاستخدام")
589
+
590
+ # عرض إحصائيات الاستخدام
591
+ st.markdown("#### إحصائيات الاستخدام")
592
+
593
+ col1, col2, col3, col4 = st.columns(4)
594
+
595
+ with col1:
596
+ st.metric("إجمالي الاستخدامات", len(st.session_state.model_usage))
597
+
598
+ with col2:
599
+ total_tokens = sum(usage['tokens'] for usage in st.session_state.model_usage)
600
+ st.metric("إجمالي الرموز", f"{total_tokens:,}")
601
+
602
+ with col3:
603
+ unique_models = len(set(usage['model'] for usage in st.session_state.model_usage))
604
+ st.metric("النماذج المستخدمة", unique_models)
605
+
606
+ with col4:
607
+ # حساب تكلفة تقديرية
608
+ estimated_cost = total_tokens * 0.0001
609
+ st.metric("التكلفة التقديرية", f"{estimated_cost:.2f} $")
610
+
611
+ # عرض الرسم البياني للاستخدام
612
+ st.markdown("#### استخدام النماذج حسب النوع")
613
+
614
+ # تجميع البيانات حسب نوع النموذج
615
+ usage_by_type = {}
616
+ for usage in st.session_state.model_usage:
617
+ if usage['type'] in usage_by_type:
618
+ usage_by_type[usage['type']] += 1
619
+ else:
620
+ usage_by_type[usage['type']] = 1
621
+
622
+ # تحويل البيانات إلى DataFrame
623
+ usage_df = pd.DataFrame({
624
+ "نوع النموذج": list(usage_by_type.keys()),
625
+ "عدد الاستخدامات": list(usage_by_type.values())
626
+ })
627
+
628
+ # عرض الرسم البياني
629
+ st.bar_chart(usage_df.set_index("نوع النموذج"))
630
+
631
+ # عرض سجل الاستخدام
632
+ st.markdown("#### سجل الاستخدام")
633
+
634
+ # خيارات التصفية
635
+ col1, col2 = st.columns(2)
636
+
637
+ with col1:
638
+ filter_model_type = st.multiselect(
639
+ "نوع النموذج",
640
+ options=["الكل"] + list(set(usage['type'] for usage in st.session_state.model_usage)),
641
+ default=["الكل"]
642
+ )
643
+
644
+ with col2:
645
+ date_range = st.selectbox(
646
+ "النطاق الزمني",
647
+ options=["الكل", "اليوم", "الأسبوع الماضي", "الشهر الماضي"]
648
+ )
649
+
650
+ # تطبيق التصفية
651
+ filtered_usage = st.session_state.model_usage
652
+
653
+ if "الكل" not in filter_model_type:
654
+ filtered_usage = [u for u in filtered_usage if u['type'] in filter_model_type]
655
+
656
+ # تحويل البيانات إلى DataFrame
657
+ if filtered_usage:
658
+ usage_df = pd.DataFrame(filtered_usage)
659
+ usage_df = usage_df[['date', 'model', 'type', 'tokens', 'status']]
660
+ usage_df.columns = ['التاريخ', 'النموذج', 'النوع', 'الرموز', 'الحالة']
661
+
662
+ # عرض الجدول
663
+ st.dataframe(usage_df, use_container_width=True)
664
+ else:
665
+ st.info("لا توجد بيانات استخدام تطابق معايير التصفية", icon="ℹ️")
666
+
667
+ def _add_usage_to_history(self, model_name, model_type, tokens_count):
668
+ """
669
+ إضافة استخدام إلى سجل الاستخدام
670
+ """
671
+ new_usage = {
672
+ 'id': len(st.session_state.model_usage) + 1,
673
+ 'model': model_name,
674
+ 'type': model_type,
675
+ 'tokens': tokens_count,
676
+ 'date': datetime.now().strftime("%Y-%m-%d %H:%M"),
677
+ 'status': 'ناجح'
678
+ }
679
+
680
+ st.session_state.model_usage.insert(0, new_usage)
681
+
682
+ def _generate_sample_models(self):
683
+ """
684
+ إنشاء بيانات تجريبية لنماذج الذكاء الاصطناعي
685
+ """
686
+ models = [
687
+ {
688
+ 'id': 1,
689
+ 'name': 'GPT-4',
690
+ 'type': 'تحليل نصوص',
691
+ 'provider': 'OpenAI',
692
+ 'version': '4.0',
693
+ 'status': 'متاح',
694
+ 'description': 'نموذج لغوي متقدم لتحليل النصوص وفهم المحتوى بدقة عالية'
695
+ },
696
+ {
697
+ 'id': 2,
698
+ 'name': 'BERT-Arabic',
699
+ 'type': 'تحليل نصوص',
700
+ 'provider': 'HuggingFace',
701
+ 'version': '2.1',
702
+ 'status': 'متاح',
703
+ 'description': 'نموذج متخصص في تحليل النصوص العربية مع دعم للهجات المختلفة'
704
+ },
705
+ {
706
+ 'id': 3,
707
+ 'name': 'DocExtractor',
708
+ 'type': 'استخراج بيانات',
709
+ 'provider': 'محلي',
710
+ 'version': '1.5',
711
+ 'status': 'متاح',
712
+ 'description': 'نموذج لاستخراج البيانات من المستندات والعقود بدقة عالية'
713
+ },
714
+ {
715
+ 'id': 4,
716
+ 'name': 'TenderClassifier',
717
+ 'type': 'تصنيف مستندات',
718
+ 'provider': 'محلي',
719
+ 'version': '2.0',
720
+ 'status': 'متاح',
721
+ 'description': 'نموذج متخصص في تصنيف مستندات المناقصات والعقود'
722
+ },
723
+ {
724
+ 'id': 5,
725
+ 'name': 'AzureSummarizer',
726
+ 'type': 'تلخيص',
727
+ 'provider': 'Azure',
728
+ 'version': '3.2',
729
+ 'status': 'متاح',
730
+ 'description': 'نموذج لتلخيص المستندات الطويلة مع الحفاظ على المعلومات الأساسية'
731
+ },
732
+ {
733
+ 'id': 6,
734
+ 'name': 'TranslateAI',
735
+ 'type': 'ترجمة',
736
+ 'provider': 'OpenAI',
737
+ 'version': '2.5',
738
+ 'status': 'متاح',
739
+ 'description': 'نموذج للترجمة بين اللغات المختلفة مع دعم خاص للمصطلحات التقنية'
740
+ },
741
+ {
742
+ 'id': 7,
743
+ 'name': 'CostPredictor',
744
+ 'type': 'تنبؤ',
745
+ 'provider': 'محلي',
746
+ 'version': '1.8',
747
+ 'status': 'متاح',
748
+ 'description': 'نموذج للتنبؤ بتكاليف المشاريع بناءً على البيانات التاريخية'
749
+ },
750
+ {
751
+ 'id': 8,
752
+ 'name': 'RiskAnalyzer',
753
+ 'type': 'تنبؤ',
754
+ 'provider': 'Azure',
755
+ 'version': '2.1',
756
+ 'status': 'متاح',
757
+ 'description': 'نموذج لتحليل المخاطر المحتملة في المشاريع والمناقصات'
758
+ },
759
+ {
760
+ 'id': 9,
761
+ 'name': 'GPT-5',
762
+ 'type': 'تحليل نصوص',
763
+ 'provider': 'OpenAI',
764
+ 'version': '5.0-beta',
765
+ 'status': 'قيد التطوير',
766
+ 'description': 'النسخة التجريبية من الجيل الخامس لنماذج GPT مع قدرات متقدمة'
767
+ },
768
+ {
769
+ 'id': 10,
770
+ 'name': 'MultiModalAnalyzer',
771
+ 'type': 'تحليل نصوص',
772
+ 'provider': 'HuggingFace',
773
+ 'version': '1.0',
774
+ 'status': 'قيد التطوير',
775
+ 'description': 'نموذج متعدد الوسائط لتحليل النصوص والصور والمخططات'
776
+ }
777
+ ]
778
+
779
+ return models
780
+
781
+ def _generate_sample_usage(self):
782
+ """
783
+ إنشاء بيانات تجريبية لسجل استخدام النماذج
784
+ """
785
+ models = self._generate_sample_models()
786
+
787
+ usage = []
788
+ for i in range(50):
789
+ # اختيار نموذج عشوائي من النماذج المتاحة
790
+ available_models = [m for m in models if m['status'] == 'متاح']
791
+ model = random.choice(available_models)
792
+
793
+ # تحديد عدد الرموز بناءً على نوع النموذج
794
+ if model['type'] in ['تحليل نصوص', 'تلخيص', 'ترجمة']:
795
+ tokens = random.randint(100, 2000)
796
+ else:
797
+ tokens = random.randint(500, 5000)
798
+
799
+ # تحديد تاريخ عشوائي خلال الشهر الماضي
800
+ days_ago = random.randint(0, 30)
801
+ usage_date = (datetime.now() - pd.Timedelta(days=days_ago)).strftime("%Y-%m-%d %H:%M")
802
+
803
+ entry = {
804
+ 'id': i + 1,
805
+ 'model': model['name'],
806
+ 'type': model['type'],
807
+ 'tokens': tokens,
808
+ 'date': usage_date,
809
+ 'status': 'ناجح' if random.random() < 0.95 else 'فشل'
810
+ }
811
+
812
+ usage.append(entry)
813
+
814
+ # ترتيب السجل حسب التاريخ (الأحدث أولاً)
815
+ usage.sort(key=lambda x: x['date'], reverse=True)
816
+
817
+ return usage
modules/document_analysis/document_analysis_app.py ADDED
@@ -0,0 +1,1114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ وحدة تطبيق تحليل المستندات
4
+
5
+ هذا الملف يحتوي على الفئة الرئيسية لتطبيق تحليل المستندات.
6
+ """
7
+
8
+ # استيراد المكتبات القياسية
9
+ import os
10
+ import sys
11
+ import logging
12
+ import base64
13
+ import json
14
+ import time
15
+ from io import BytesIO
16
+ from pathlib import Path
17
+ from urllib.parse import urlparse
18
+ from tempfile import NamedTemporaryFile
19
+
20
+ # استيراد مكتبة Streamlit
21
+ import streamlit as st
22
+
23
+ # استيراد المكتبات الإضافية
24
+ import requests
25
+ from PIL import Image
26
+
27
+ try:
28
+ # استيراد مكتبات Docling و MLX VLM
29
+ from docling_core.types.doc import ImageRefMode
30
+ from docling_core.types.doc.document import DocTagsDocument, DoclingDocument
31
+ from mlx_vlm import load, generate
32
+ from mlx_vlm.prompt_utils import apply_chat_template
33
+ from mlx_vlm.utils import load_config, stream_generate
34
+ docling_available = True
35
+ except ImportError:
36
+ docling_available = False
37
+ logging.warning("لم يتم العثور على مكتبات Docling و MLX VLM. بعض الوظائف قد لا تعمل.")
38
+
39
+ try:
40
+ # استيراد مكتبة pdf2image للتعامل مع ملفات PDF
41
+ from pdf2image import convert_from_path
42
+ pdf_conversion_available = True
43
+ except ImportError:
44
+ pdf_conversion_available = False
45
+ logging.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.")
46
+
47
+ # إعداد المسار للوحدات النمطية
48
+ current_dir = os.path.dirname(os.path.abspath(__file__))
49
+ parent_dir = os.path.dirname(os.path.dirname(current_dir))
50
+ if parent_dir not in sys.path:
51
+ sys.path.append(parent_dir)
52
+
53
+ # استيراد الخدمات باستخدام المسار النسبي
54
+ try:
55
+ # الطريقة 1: استيراد نسبي مباشر
56
+ from .services.text_extractor import TextExtractor
57
+ from .services.item_extractor import ItemExtractor
58
+ from .services.document_parser import DocumentParser
59
+ except ImportError:
60
+ try:
61
+ # الطريقة 2: استيراد مطلق
62
+ from modules.document_analysis.services.text_extractor import TextExtractor
63
+ from modules.document_analysis.services.item_extractor import ItemExtractor
64
+ from modules.document_analysis.services.document_parser import DocumentParser
65
+ except ImportError:
66
+ # الطريقة 3: تعريف الفئات مباشرة كحل مؤقت
67
+ logging.warning("لا يمكن استيراد خدمات تحليل المستندات. استخدام التعريفات المؤقتة.")
68
+
69
+ class TextExtractor:
70
+ def __init__(self, config=None):
71
+ self.config = config or {}
72
+
73
+ def extract_from_pdf(self, file_path):
74
+ return "نص مستخرج مؤقت من PDF"
75
+
76
+ def extract_from_docx(self, file_path):
77
+ return "نص مستخرج مؤقت من DOCX"
78
+
79
+ def extract_from_image(self, file_path):
80
+ return "نص مستخرج مؤقت من صورة"
81
+
82
+ def extract(self, file_path):
83
+ _, ext = os.path.splitext(file_path)
84
+ ext = ext.lower()
85
+
86
+ if ext == '.pdf':
87
+ return self.extract_from_pdf(file_path)
88
+ elif ext in ('.doc', '.docx'):
89
+ return self.extract_from_docx(file_path)
90
+ elif ext in ('.jpg', '.jpeg', '.png'):
91
+ return self.extract_from_image(file_path)
92
+ else:
93
+ return "نوع ملف غير مدعوم"
94
+
95
+ class ItemExtractor:
96
+ def __init__(self, config=None):
97
+ self.config = config or {}
98
+
99
+ def extract_tables(self, document):
100
+ return [{"عنوان": "جدول مؤقت", "بيانات": []}]
101
+
102
+ def extract(self, file_path):
103
+ return [
104
+ {"بند": "بند مؤقت 1", "قيمة": 1000},
105
+ {"بند": "بند مؤقت 2", "قيمة": 2000},
106
+ {"بند": "بند مؤقت 3", "قيمة": 3000}
107
+ ]
108
+
109
+ class DocumentParser:
110
+ def __init__(self, config=None):
111
+ self.config = config or {}
112
+
113
+ def parse_document(self, file_path):
114
+ return {"نوع": "مستند مؤقت", "محتوى": "محتوى مؤقت"}
115
+
116
+ def parse(self, file_path):
117
+ return {
118
+ "نوع المستند": "مستند مؤقت",
119
+ "عدد الصفحات": 5,
120
+ "تاريخ التحليل": "2025-03-24",
121
+ "درجة الثقة": "80%",
122
+ "ملاحظات": "تحليل مؤقت للمستند"
123
+ }
124
+
125
+
126
+ class DoclingAnalyzer:
127
+ """
128
+ فئة لتحليل المستندات باستخدام نماذج Docling و MLX VLM
129
+ """
130
+ def __init__(self):
131
+ self.model = None
132
+ self.processor = None
133
+ self.config = None
134
+ self.docling_available = False
135
+
136
+ try:
137
+ # تحميل النموذج
138
+ import os
139
+ from mlx_vlm import load, generate
140
+ from mlx_vlm.utils import load_config
141
+
142
+ model_path = "ds4sd/SmolDocling-256M-preview-mlx-bf16"
143
+ self.model, self.processor = load(model_path)
144
+ self.config = load_config(model_path)
145
+ self.docling_available = True
146
+ except Exception as e:
147
+ print(f"خطأ في تحميل نموذج Docling: {str(e)}")
148
+ self.docling_available = False
149
+
150
+ def is_available(self):
151
+ """التحقق من توفر نماذج Docling"""
152
+ return self.docling_available and self.model is not None
153
+
154
+ def analyze_image(self, image_path=None, image_url=None, image_bytes=None, prompt="Convert this page to docling."):
155
+ """
156
+ تحليل صورة باستخدام نموذج Docling
157
+
158
+ المعلمات:
159
+ image_path (str): مسار الصورة المحلية (اختياري)
160
+ image_url (str): رابط الصورة (اختياري)
161
+ image_bytes (bytes): بيانات الصورة (اختياري)
162
+ prompt (str): التوجيه للنموذج
163
+
164
+ العوائد:
165
+ dict: نتائج التحليل متضمنة النص والعلامات والمستند
166
+ """
167
+ if not self.is_available():
168
+ return {
169
+ "error": "Docling غير متوفر. يرجى تثبيت المكتبات المطلوبة."
170
+ }
171
+
172
+ try:
173
+ from io import BytesIO
174
+ from pathlib import Path
175
+ from urllib.parse import urlparse
176
+ import requests
177
+ from PIL import Image
178
+ from docling_core.types.doc import ImageRefMode
179
+ from docling_core.types.doc.document import DocTagsDocument, DoclingDocument
180
+ from mlx_vlm.prompt_utils import apply_chat_template
181
+ from mlx_vlm.utils import stream_generate, load_image
182
+
183
+ # تحميل الصورة
184
+ pil_image = None
185
+ image_source = None
186
+
187
+ if image_url:
188
+ try:
189
+ response = requests.get(image_url, stream=True, timeout=10)
190
+ response.raise_for_status()
191
+ pil_image = Image.open(BytesIO(response.content))
192
+ image_source = image_url
193
+ except Exception as e:
194
+ return {"error": f"فشل في تحميل الصورة من الرابط: {str(e)}"}
195
+ elif image_path:
196
+ try:
197
+ # التأكد من وجود الملف
198
+ if not Path(image_path).exists():
199
+ return {"error": f"ملف الصورة غير موجود: {image_path}"}
200
+ pil_image = Image.open(image_path)
201
+ image_source = image_path
202
+ except Exception as e:
203
+ return {"error": f"فشل في فتح ملف الصورة: {str(e)}"}
204
+ elif image_bytes:
205
+ try:
206
+ pil_image = Image.open(BytesIO(image_bytes))
207
+ # حفظ الصورة مؤقتا للتحليل
208
+ temp_path = "/tmp/temp_image.jpg"
209
+ pil_image.save(temp_path)
210
+ image_source = temp_path
211
+ except Exception as e:
212
+ return {"error": f"فشل في معالجة بيانات الصورة: {str(e)}"}
213
+ else:
214
+ return {"error": "يجب توفير مصدر للصورة (مسار، رابط، أو بيانات)"}
215
+
216
+ # تطبيق قالب المحادثة
217
+ formatted_prompt = apply_chat_template(self.processor, self.config, prompt, num_images=1)
218
+
219
+ # إنشاء النتيجة
220
+ output = ""
221
+
222
+ # تمرير مسار الصورة أو عنوان URL الفعلي
223
+ try:
224
+ for token in stream_generate(
225
+ self.model, self.processor, formatted_prompt, [image_source],
226
+ max_tokens=4096, verbose=False
227
+ ):
228
+ output += token.text
229
+ if "</doctag>" in token.text:
230
+ break
231
+ except Exception as e:
232
+ return {"error": f"فشل في تحليل الصورة: {str(e)}"}
233
+
234
+ # إنشاء مستند Docling
235
+ try:
236
+ doctags_doc = DocTagsDocument.from_doctags_and_image_pairs([output], [pil_image])
237
+ doc = DoclingDocument(name="AnalyzedDocument")
238
+ doc.load_from_doctags(doctags_doc)
239
+
240
+ # إرجاع النتائج
241
+ return {
242
+ "doctags": output,
243
+ "markdown": doc.export_to_markdown(),
244
+ "document": doc,
245
+ "image": pil_image
246
+ }
247
+ except Exception as e:
248
+ return {"error": f"فشل في إنشاء مستند Docling: {str(e)}"}
249
+
250
+ except Exception as e:
251
+ return {"error": f"حدث خطأ غير متوقع: {str(e)}"}
252
+
253
+ def export_to_html(self, doc, output_path="./output.html", show_in_browser=False):
254
+ """
255
+ تصدير المستند إلى HTML
256
+
257
+ المعلمات:
258
+ doc (DoclingDocument): مستند Docling
259
+ output_path (str): مسار ملف الإخراج
260
+ show_in_browser (bool): عرض الملف في المتصفح
261
+
262
+ العوائد:
263
+ str: مسار ملف HTML المولد
264
+ """
265
+ if not self.is_available():
266
+ return None
267
+
268
+ try:
269
+ from pathlib import Path
270
+ from docling_core.types.doc import ImageRefMode
271
+
272
+ # إنشاء مسار الإخراج
273
+ out_path = Path(output_path)
274
+ # التأكد من وجود المجلد
275
+ out_path.parent.mkdir(exist_ok=True, parents=True)
276
+
277
+ doc.save_as_html(out_path, image_mode=ImageRefMode.EMBEDDED)
278
+
279
+ # فتح في المتصفح إذا تم طلب ذلك
280
+ if show_in_browser:
281
+ import webbrowser
282
+ webbrowser.open(f"file:///{str(out_path.resolve())}")
283
+
284
+ return str(out_path)
285
+ except Exception as e:
286
+ print(f"خطأ في تصدير المستند إلى HTML: {str(e)}")
287
+ return None
288
+
289
+
290
+ class ClaudeAnalyzer:
291
+ """
292
+ فئة لتحليل المستندات باستخدام Claude.ai API
293
+ """
294
+ def __init__(self):
295
+ """تهيئة محلل Claude"""
296
+ self.api_url = "https://api.anthropic.com/v1/messages"
297
+
298
+ def get_api_key(self):
299
+ """الحصول على مفتاح API من متغيرات البيئة"""
300
+ api_key = os.environ.get("anthropic")
301
+ if not api_key:
302
+ raise ValueError("مفتاح API لـ Claude غير موجود في متغيرات البيئة")
303
+ return api_key
304
+
305
+ def analyze_document(self, file_path, model_name="claude-3-7-sonnet", prompt=None):
306
+ """
307
+ تحليل مستند باستخدام Claude AI
308
+
309
+ المعلمات:
310
+ file_path: مسار الملف المراد تحليله
311
+ model_name: اسم نموذج Claude المراد استخدامه
312
+ prompt: التوجيه المخصص للتحليل (اختياري)
313
+
314
+ العوائد:
315
+ dict: نتائج التحليل
316
+ """
317
+ try:
318
+ # الحصول على مفتاح API
319
+ api_key = self.get_api_key()
320
+
321
+ # تحديد التوجيه المناسب إذا لم يتم توفيره
322
+ if prompt is None:
323
+ _, ext = os.path.splitext(file_path)
324
+ ext = ext.lower()
325
+
326
+ if ext == '.pdf':
327
+ prompt = "قم بتحليل هذه الصورة المستخرجة من مستند PDF واستخراج المعلومات الرئيسية مثل العناوين، الفقرات، الجداول، والنقاط المهمة."
328
+ elif ext in ('.doc', '.docx'):
329
+ prompt = "قم بتحليل هذه الصورة المستخرجة من مستند Word واستخراج المعلومات الرئيسية والخلاصة."
330
+ elif ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp'):
331
+ prompt = "قم بوصف وتحليل محتوى هذه الصورة بالتفصيل، مع ذكر العناصر المهمة والنصوص والبيانات الموجودة فيها."
332
+ else:
333
+ prompt = "قم بتحليل محتوى هذا الملف واستخراج المعلومات المفيدة منه."
334
+
335
+ # التحقق من نوع الملف وتحويله إذا لزم الأمر
336
+ _, ext = os.path.splitext(file_path)
337
+ ext = ext.lower()
338
+
339
+ processed_file_path = file_path
340
+ temp_files = [] # قائمة للملفات المؤقتة لحذفها لاح��اً
341
+
342
+ # للملفات غير المدعومة مباشرة (مثل PDF)
343
+ if ext not in ('.jpg', '.jpeg', '.png', '.gif', '.webp'):
344
+ # إذا كان الملف PDF، حاول تحويله إلى صورة
345
+ if ext == '.pdf':
346
+ if not pdf_conversion_available:
347
+ return {"error": "لا يمكن تحويل ملف PDF إلى صورة. يرجى تثبيت مكتبة pdf2image."}
348
+
349
+ try:
350
+ # تحويل الصفحة الأولى فقط
351
+ images = convert_from_path(file_path, first_page=1, last_page=1)
352
+ if images:
353
+ # حفظ الصورة بشكل مؤقت
354
+ temp_image_path = "/tmp/temp_pdf_image.jpg"
355
+ images[0].save(temp_image_path, 'JPEG')
356
+ processed_file_path = temp_image_path # استخدام مسار الصورة الجديد
357
+ temp_files.append(temp_image_path)
358
+ else:
359
+ return {"error": "فشل في تحويل ملف PDF إلى صورة"}
360
+ except Exception as e:
361
+ return {"error": f"فشل في تحويل ملف PDF إلى صورة: {str(e)}"}
362
+ else:
363
+ return {"error": f"نوع الملف {ext} غير مدعوم. Claude API يدعم فقط الصور (JPEG, PNG, GIF, WebP) أو PDF (يتم تحويله تلقائياً)."}
364
+
365
+ # ضغط الصورة إذا كان حجمها كبيراً
366
+ try:
367
+ img = Image.open(processed_file_path)
368
+
369
+ # تحقق من حجم الصورة وضغطها إذا كانت كبيرة
370
+ img_width, img_height = img.size
371
+ if img_width > 1500 or img_height > 1500:
372
+ # تحويل الصورة إلى حجم أصغر (1500×1500 بكسل كحد أقصى)
373
+ img.thumbnail((1500, 1500))
374
+
375
+ # حفظ الصورة المضغوطة في ملف مؤقت
376
+ compressed_image_path = "/tmp/compressed_image.jpg"
377
+ img.save(compressed_image_path, format="JPEG", quality=85)
378
+
379
+ # إضافة الملف المؤقت إلى القائمة
380
+ if processed_file_path not in temp_files:
381
+ temp_files.append(compressed_image_path)
382
+
383
+ processed_file_path = compressed_image_path
384
+ except Exception as e:
385
+ logging.warning(f"فشل في ضغط الصورة: {str(e)}. سيتم استخدام الصورة الأصلية.")
386
+
387
+ # قراءة محتوى الملف المعالج
388
+ with open(processed_file_path, 'rb') as f:
389
+ file_content = f.read()
390
+
391
+ # التحقق من حجم الملف (يجب أن يكون أقل من 20 ميجابايت)
392
+ file_size_mb = len(file_content) / (1024 * 1024)
393
+ if file_size_mb > 20:
394
+ # محاولة ضغط الصورة أكثر إذا كان حجمها أكبر من 20 ميجابايت
395
+ try:
396
+ img = Image.open(processed_file_path)
397
+
398
+ # ضغط أكبر - حجم أصغر وجودة أقل
399
+ compressed_image_path = "/tmp/extra_compressed_image.jpg"
400
+ img.thumbnail((1000, 1000))
401
+ img.save(compressed_image_path, format="JPEG", quality=70)
402
+
403
+ # إضافة الملف المؤقت إلى القائمة
404
+ temp_files.append(compressed_image_path)
405
+ processed_file_path = compressed_image_path
406
+
407
+ # قراءة الملف المضغوط
408
+ with open(processed_file_path, 'rb') as f:
409
+ file_content = f.read()
410
+
411
+ # التحقق من الحجم مرة أخرى
412
+ file_size_mb = len(file_content) / (1024 * 1024)
413
+ if file_size_mb > 20:
414
+ # لا يزال الحجم كبيراً
415
+ for temp_file in temp_files:
416
+ try:
417
+ os.unlink(temp_file)
418
+ except:
419
+ pass
420
+ return {"error": f"حجم الملف كبير جدًا ({file_size_mb:.2f} ميجابايت) حتى بعد الضغط. يجب أن يكون أقل من 20 ميجابايت."}
421
+ except Exception as e:
422
+ for temp_file in temp_files:
423
+ try:
424
+ os.unlink(temp_file)
425
+ except:
426
+ pass
427
+ return {"error": f"الملف كبير جدًا ({file_size_mb:.2f} ميجابايت) ولا يمكن ضغطه. يجب أن يكون أقل من 20 ميجابايت."}
428
+
429
+ # تحديد نوع الملف المعالج (بعد التحويل إذا تم)
430
+ file_type = self._get_file_type(processed_file_path)
431
+
432
+ # تحويل المحتوى إلى Base64
433
+ file_base64 = base64.b64encode(file_content).decode('utf-8')
434
+
435
+ # إعداد البيانات للطلب
436
+ headers = {
437
+ "Content-Type": "application/json",
438
+ "x-api-key": api_key,
439
+ "anthropic-version": "2023-06-01"
440
+ }
441
+
442
+ # التحقق من اسم النموذج وتصحيحه إذا لزم الأمر
443
+ valid_models = {
444
+ "claude-3-7-sonnet": "claude-3-7-sonnet-20250219",
445
+ "claude-3-5-haiku": "claude-3-5-haiku-20240307"
446
+ }
447
+
448
+ if model_name in valid_models:
449
+ model_name = valid_models[model_name]
450
+
451
+ # طباعة معلومات التصحيح
452
+ logging.debug(f"إرسال طلب إلى Claude API: {model_name}, نوع الملف: {file_type}")
453
+
454
+ # تحضير payload للـ API
455
+ payload = {
456
+ "model": model_name,
457
+ "max_tokens": 4096,
458
+ "messages": [
459
+ {
460
+ "role": "user",
461
+ "content": [
462
+ {"type": "text", "text": prompt},
463
+ {
464
+ "type": "image",
465
+ "source": {
466
+ "type": "base64",
467
+ "media_type": file_type,
468
+ "data": file_base64
469
+ }
470
+ }
471
+ ]
472
+ }
473
+ ]
474
+ }
475
+
476
+ # إرسال الطلب إلى API مع محاولات إعادة
477
+ for attempt in range(3): # ثلاث محاولات كحد أقصى
478
+ try:
479
+ response = requests.post(
480
+ self.api_url,
481
+ headers=headers,
482
+ json=payload,
483
+ timeout=120 # زيادة مهلة الانتظار إلى دقيقتين
484
+ )
485
+
486
+ # إذا نجح الطلب، نخرج من الحلقة
487
+ if response.status_code == 200:
488
+ break
489
+
490
+ # إذا كان الخطأ 502، ننتظر ونحاول مرة أخرى
491
+ if response.status_code == 502:
492
+ wait_time = (attempt + 1) * 5 # انتظار 5، 10، 15 ثانية
493
+ logging.warning(f"تم استلام خطأ 502. الانتظار {wait_time} ثانية قبل إعادة المحاولة.")
494
+ time.sleep(wait_time)
495
+ else:
496
+ # إذا كان الخطأ ليس 502، نخرج من الحلقة
497
+ break
498
+
499
+ except requests.exceptions.RequestException as e:
500
+ logging.warning(f"فشل الطلب في المحاولة {attempt+1}: {str(e)}")
501
+ if attempt == 2: # آخر محاولة
502
+ # حذف الملفات المؤقتة
503
+ for temp_file in temp_files:
504
+ try:
505
+ os.unlink(temp_file)
506
+ except:
507
+ pass
508
+ return {"error": f"فشل الاتصال بعد عدة محاولات: {str(e)}"}
509
+ time.sleep((attempt + 1) * 5) # انتظار قبل إعادة المحاولة
510
+
511
+ # حذف الملفات المؤقتة
512
+ for temp_file in temp_files:
513
+ try:
514
+ os.unlink(temp_file)
515
+ except:
516
+ pass
517
+
518
+ # التحقق من نجاح الطلب
519
+ if response.status_code != 200:
520
+ error_message = f"فشل طلب API: {response.status_code}"
521
+ try:
522
+ error_details = response.json()
523
+ error_message += f"\nتفاصيل: {error_details}"
524
+ except:
525
+ error_message += f"\nتفاصيل: {response.text}"
526
+
527
+ return {
528
+ "error": error_message
529
+ }
530
+
531
+ # معالجة الاستجابة
532
+ result = response.json()
533
+
534
+ return {
535
+ "success": True,
536
+ "content": result["content"][0]["text"],
537
+ "model": result["model"],
538
+ "usage": result.get("usage", {})
539
+ }
540
+
541
+ except Exception as e:
542
+ # حذف الملفات المؤقتة في حالة حدوث خطأ
543
+ for temp_file in temp_files:
544
+ try:
545
+ os.unlink(temp_file)
546
+ except:
547
+ pass
548
+
549
+ logging.error(f"خطأ أثناء تحليل المستند: {str(e)}")
550
+ import traceback
551
+ stack_trace = traceback.format_exc()
552
+ return {"error": f"فشل في تحليل المستند: {str(e)}\n{stack_trace}"}
553
+
554
+ def _get_file_type(self, file_path):
555
+ """تحديد نوع الملف من امتداده"""
556
+ _, ext = os.path.splitext(file_path)
557
+ ext = ext.lower()
558
+
559
+ # Claude API يدعم فقط أنواع الصور التالية
560
+ if ext in ('.jpg', '.jpeg'):
561
+ return "image/jpeg"
562
+ elif ext == '.png':
563
+ return "image/png"
564
+ elif ext == '.gif':
565
+ return "image/gif"
566
+ elif ext == '.webp':
567
+ return "image/webp"
568
+ else:
569
+ # للملفات الأخرى، نعيد نوع صورة افتراضي
570
+ # هذا سيستخدم فقط إذا تم تحويل الملف إلى صورة أولاً
571
+ return "image/jpeg"
572
+
573
+ def get_available_models(self):
574
+ """
575
+ الحصول على قائمة بالنماذج المتاحة
576
+
577
+ العوائد:
578
+ dict: قائمة بالنماذج مع وصفها
579
+ """
580
+ return {
581
+ "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة",
582
+ "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية"
583
+ }
584
+
585
+ def get_model_full_name(self, short_name):
586
+ """
587
+ تحويل الاسم المختصر للنموذج إلى الاسم الكامل
588
+
589
+ المعلمات:
590
+ short_name: الاسم المختصر للنموذج
591
+
592
+ العوائد:
593
+ str: الاسم الكامل للنموذج
594
+ """
595
+ valid_models = {
596
+ "claude-3-7-sonnet": "claude-3-7-sonnet-20250219",
597
+ "claude-3-5-haiku": "claude-3-5-haiku-20240307"
598
+ }
599
+
600
+ return valid_models.get(short_name, short_name)
601
+
602
+
603
+ class DocumentAnalysisApp:
604
+ def __init__(self):
605
+ # إنشاء كائنات الخدمات
606
+ self.text_extractor = TextExtractor()
607
+ self.item_extractor = ItemExtractor()
608
+ self.document_parser = DocumentParser()
609
+
610
+ # إنشاء محلل Docling
611
+ self.docling_analyzer = DoclingAnalyzer()
612
+
613
+ # إنشاء محلل Claude
614
+ self.claude_analyzer = ClaudeAnalyzer()
615
+
616
+ def render(self):
617
+ """العرض الرئيسي للتطبيق"""
618
+ st.title("تحليل المستندات")
619
+ st.write("اختر ملفًا لتحليله واستخرج البيانات المطلوبة.")
620
+
621
+ # إنشاء علامات تبويب للأنواع المختلفة من التحليل
622
+ tabs = st.tabs(["تحليل عام", "تحليل Docling", "تحليل Claude AI"])
623
+
624
+ with tabs[0]:
625
+ self._render_general_analysis()
626
+
627
+ with tabs[1]:
628
+ self._render_docling_analysis()
629
+
630
+ with tabs[2]:
631
+ self._render_claude_analysis()
632
+
633
+ def _render_general_analysis(self):
634
+ """عرض واجهة التحليل العام"""
635
+ uploaded_file = st.file_uploader("ارفع ملف PDF أو DOCX", type=["pdf", "docx"], key="general_uploader")
636
+
637
+ if uploaded_file:
638
+ with st.spinner("جاري تحليل المستند..."):
639
+ file_path = f"/tmp/{uploaded_file.name}"
640
+ with open(file_path, "wb") as f:
641
+ f.write(uploaded_file.read())
642
+
643
+ # تحديد نوع الملف من امتداده
644
+ _, ext = os.path.splitext(file_path)
645
+ ext = ext.lower()
646
+
647
+ # استخراج النص حسب نوع الملف
648
+ if ext == '.pdf':
649
+ extracted_text = self.text_extractor.extract_from_pdf(file_path)
650
+ elif ext in ('.doc', '.docx'):
651
+ extracted_text = self.text_extractor.extract_from_docx(file_path)
652
+ else:
653
+ extracted_text = "نوع ملف غير مدعوم للنص"
654
+
655
+ # عرض النص المستخرج
656
+ st.subheader("النص المستخرج:")
657
+ st.text_area("النص", extracted_text, height=300)
658
+
659
+ # استخراج البنود
660
+ extracted_items = self.item_extractor.extract(file_path)
661
+ if extracted_items:
662
+ st.subheader("البنود المستخرجة:")
663
+ st.dataframe(extracted_items)
664
+
665
+ # تحليل المستند
666
+ parsed_data = self.document_parser.parse(file_path)
667
+ st.subheader("تحليل المستند:")
668
+ st.json(parsed_data)
669
+
670
+ def _render_docling_analysis(self):
671
+ """عرض واجهة تحليل Docling"""
672
+ import streamlit as st
673
+ from tempfile import NamedTemporaryFile
674
+
675
+ if not self.docling_analyzer.is_available():
676
+ st.warning("مكتبات Docling و MLX VLM غير متوفرة. يرجى تثبيت الحزم المطلوبة.")
677
+ st.code("""
678
+ # يرجى تثبيت الحزم التالية:
679
+ pip install docling-core mlx-vlm pillow>=10.3.0 transformers>=4.49.0 tqdm>=4.66.2
680
+ """)
681
+ return
682
+
683
+ st.subheader("تحليل الصور والمستندات باستخدام Docling")
684
+
685
+ # اختيار مصدر الصورة
686
+ source_option = st.radio("اختر مصدر الصورة:", ["رفع صورة", "رابط صورة"])
687
+
688
+ image_path = None
689
+ image_url = None
690
+ image_data = None
691
+
692
+ if source_option == "رفع صورة":
693
+ uploaded_image = st.file_uploader("ارفع صورة", type=["jpg", "jpeg", "png"], key="docling_uploader")
694
+ if uploaded_image:
695
+ # حفظ الصورة المرفوعة إلى ملف مؤقت
696
+ image_data = uploaded_image.read()
697
+
698
+ # عرض الصورة المرفوعة
699
+ st.image(image_data, caption="الصورة المرفوعة", width=400)
700
+
701
+ # إنشاء ملف مؤقت لحفظ الصورة
702
+ with NamedTemporaryFile(delete=False, suffix=f".{uploaded_image.name.split('.')[-1]}") as temp_file:
703
+ temp_file.write(image_data)
704
+ image_path = temp_file.name
705
+ else:
706
+ image_url = st.text_input("أدخل رابط الصورة:")
707
+ if image_url:
708
+ try:
709
+ # عرض الصورة من الرابط
710
+ st.image(image_url, caption="الصورة من الرابط", width=400)
711
+ except Exception as e:
712
+ st.error(f"خطأ في تحميل الصورة: {str(e)}")
713
+
714
+ # توجيه للنموذج
715
+ prompt = st.text_input("توجيه للنموذج:", value="Convert this page to docling.")
716
+
717
+ # زر التحليل
718
+ if st.button("تحليل الصورة"):
719
+ if image_path or image_url:
720
+ with st.spinner("جاري تحليل الصورة..."):
721
+ # تحليل الصورة
722
+ results = self.docling_analyzer.analyze_image(
723
+ image_path=image_path,
724
+ image_url=image_url,
725
+ image_bytes=None, # نستخدم الملف المؤقت بدلاً من البيانات المباشرة
726
+ prompt=prompt
727
+ )
728
+
729
+ if "error" in results:
730
+ st.error(results["error"])
731
+ else:
732
+ # عرض النتائج
733
+ with st.expander("علامات DocTags", expanded=True):
734
+ st.code(results["doctags"], language="xml")
735
+
736
+ with st.expander("Markdown", expanded=True):
737
+ st.code(results["markdown"], language="markdown")
738
+
739
+ # تصدير إلى HTML
740
+ if st.button("تصدير إلى HTML"):
741
+ html_path = self.docling_analyzer.export_to_html(
742
+ results["document"],
743
+ show_in_browser=True
744
+ )
745
+ if html_path:
746
+ st.success(f"تم تصدير المستند إلى: {html_path}")
747
+ else:
748
+ st.error("فشل تصدير المستند إلى HTML")
749
+
750
+ # حذف الملف المؤقت بعد الانتهاء
751
+ if image_path and os.path.exists(image_path) and image_data:
752
+ try:
753
+ os.unlink(image_path)
754
+ except:
755
+ pass
756
+ else:
757
+ st.warning("يرجى اختيار صورة للتحليل أولاً.")
758
+
759
+ def _render_claude_analysis(self):
760
+ """عرض واجهة تحليل Claude AI مع توسعة البيانات المعروضة"""
761
+ import time
762
+
763
+ st.subheader("تحليل المستندات باستخدام Claude AI")
764
+
765
+ col1, col2 = st.columns([2, 1])
766
+
767
+ with col1:
768
+ # إضافة اختيار النموذج
769
+ claude_models = {
770
+ "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة",
771
+ "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية"
772
+ }
773
+
774
+ selected_model = st.radio(
775
+ "اختر نموذج Claude",
776
+ options=list(claude_models.keys()),
777
+ format_func=lambda x: claude_models[x],
778
+ horizontal=True
779
+ )
780
+
781
+ with col2:
782
+ # إضافة شرح بسيط للنموذج
783
+ if selected_model == "claude-3-7-sonnet":
784
+ st.info("نموذج Claude 3.7 Sonnet هو أحدث نموذج ذكي يقدم تحليلاً متعمقاً للمستندات مع دقة عالية")
785
+ else:
786
+ st.info("نموذج Claude 3.5 Haiku أسرع في التحليل ومناسب للمهام البسيطة والاستخدام اليومي")
787
+
788
+ # تخصيص التوجيه مع اقتراحات للتوجيهات المخصصة
789
+ st.subheader("تخصيص التحليل")
790
+
791
+ prompt_templates = {
792
+ "تحليل عام": "قم بتحليل هذا المستند واستخراج جميع المعلومات المهمة.",
793
+ "استخراج البيانات الأساسية": "استخرج كافة البيانات الأساسية من هذا المستند بما في ذلك الأسماء والتواريخ والأرقام والمبالغ المالية.",
794
+ "تلخيص المستند": "قم بتلخيص هذا المستند بشكل مفصل مع التركيز على النقاط الرئيسية.",
795
+ "تحليل العقود": "حلل هذا العقد واستخرج الأطراف والالتزامات والشروط والتواريخ المهمة.",
796
+ "تحليل فواتير": "استخرج كافة المعلومات من هذه الفاتورة بما في ذلك المورد والعميل وتفاصيل المنتجات والأسعار والمبالغ الإجمالية."
797
+ }
798
+
799
+ prompt_type = st.selectbox(
800
+ "اختر نوع التوجيه",
801
+ options=list(prompt_templates.keys()),
802
+ index=0
803
+ )
804
+
805
+ default_prompt = prompt_templates[prompt_type]
806
+
807
+ custom_prompt = st.text_area(
808
+ "تخصيص التوجيه للتحليل",
809
+ value=default_prompt,
810
+ height=100
811
+ )
812
+
813
+ # خيارات متقدمة
814
+ with st.expander("خيارات متقدمة"):
815
+ extraction_format = st.selectbox(
816
+ "تنسيق استخراج البيانات",
817
+ ["عام", "جداول", "قائمة", "هيكل منظم"],
818
+ index=0
819
+ )
820
+
821
+ detail_level = st.slider(
822
+ "مستوى التفاصيل",
823
+ min_value=1,
824
+ max_value=5,
825
+ value=3,
826
+ help="1: ملخص موجز، 5: تحليل تفصيلي كامل"
827
+ )
828
+
829
+ # تحديث التوجيه بناء على الخيارات المتقدمة
830
+ if extraction_format != "عام" or detail_level != 3:
831
+ custom_prompt += f"\n\nاستخدم تنسيق {extraction_format} مع مستوى تفاصيل {detail_level}/5."
832
+
833
+ # رفع الملف
834
+ uploaded_file = st.file_uploader(
835
+ "ارفع ملفًا للتحليل",
836
+ type=["pdf", "jpg", "jpeg", "png"],
837
+ key="claude_uploader",
838
+ help="يدعم ملفات PDF والصور. سيتم تحويل PDF إلى صور لمعالجتها."
839
+ )
840
+
841
+ # التحقق من وجود مفتاح API
842
+ api_available = True
843
+ try:
844
+ self.claude_analyzer.get_api_key()
845
+ except ValueError:
846
+ api_available = False
847
+ st.warning("مفتاح API لـ Claude غير متوفر. يرجى التأكد من تعيين متغير البيئة 'anthropic'.")
848
+
849
+ # زر التحليل
850
+ analyze_col1, analyze_col2 = st.columns([1, 3])
851
+
852
+ with analyze_col1:
853
+ analyze_button = st.button(
854
+ "تح��يل المستند",
855
+ key="analyze_claude_btn",
856
+ use_container_width=True,
857
+ disabled=not (uploaded_file and api_available)
858
+ )
859
+
860
+ with analyze_col2:
861
+ if not uploaded_file:
862
+ st.info("يرجى رفع ملف للتحليل")
863
+
864
+ # إجراء التحليل
865
+ if uploaded_file and api_available and analyze_button:
866
+ # عرض شريط التقدم
867
+ progress_bar = st.progress(0, text="جاري تجهيز الملف...")
868
+
869
+ with st.spinner(f"جاري التحليل باستخدام {claude_models[selected_model].split('-')[0]}..."):
870
+ # حفظ الملف المرفوع إلى ملف مؤقت
871
+ temp_path = f"/tmp/{uploaded_file.name}"
872
+ with open(temp_path, "wb") as f:
873
+ f.write(uploaded_file.getbuffer())
874
+
875
+ # تحديث شريط التقدم
876
+ progress_bar.progress(25, text="جاري معالجة الملف...")
877
+
878
+ try:
879
+ # تحليل المستند
880
+ progress_bar.progress(40, text="جاري إرسال الطلب إلى Claude AI...")
881
+
882
+ results = self.claude_analyzer.analyze_document(
883
+ temp_path,
884
+ model_name=selected_model,
885
+ prompt=custom_prompt
886
+ )
887
+
888
+ progress_bar.progress(90, text="جاري معالجة النتائج...")
889
+
890
+ if "error" in results:
891
+ st.error(results["error"])
892
+ else:
893
+ progress_bar.progress(100, text="اكتمل التحليل!")
894
+
895
+ # عرض النتائج بشكل منظم
896
+ st.success(f"تم التحليل بنجاح باستخدام {results.get('model', selected_model)}!")
897
+
898
+ # إضافة علامات تبويب فرعية للنتائج
899
+ result_tabs = st.tabs(["التحليل الكامل", "بيانات مستخرجة", "معلومات إضافية"])
900
+
901
+ with result_tabs[0]:
902
+ # عرض النتائج الكاملة
903
+ st.markdown("## نتائج التحليل")
904
+ st.markdown(results["content"])
905
+
906
+ with result_tabs[1]:
907
+ # محاولة استخراج بيانات منظمة من النتائج
908
+ st.markdown("## البيانات المستخرجة")
909
+
910
+ # تقسيم النتائج إلى أقسام
911
+ content_parts = results["content"].split("\n\n")
912
+
913
+ # استخراج العناوين والبيانات الهامة
914
+ headings = []
915
+ key_values = {}
916
+
917
+ for part in content_parts:
918
+ # تحديد العناوين
919
+ if part.startswith("#") or part.startswith("##") or part.startswith("###"):
920
+ headings.append(part.strip())
921
+ continue
922
+
923
+ # محاولة استخراج أزواج المفتاح/القيمة
924
+ if ":" in part and len(part.split(":")) == 2:
925
+ key, value = part.split(":")
926
+ key_values[key.strip()] = value.strip()
927
+
928
+ # عرض العناوين
929
+ if headings:
930
+ st.markdown("### العناوين الرئيسية")
931
+ for heading in headings[:5]: # عرض أهم 5 عناوين
932
+ st.markdown(f"- {heading}")
933
+
934
+ if len(headings) > 5:
935
+ with st.expander(f"عرض {len(headings) - 5} عناوين إضافية"):
936
+ for heading in headings[5:]:
937
+ st.markdown(f"- {heading}")
938
+
939
+ # عرض البيانات الهامة
940
+ if key_values:
941
+ st.markdown("### بيانات هامة")
942
+
943
+ # تحويل البيانات إلى DataFrame
944
+ import pandas as pd
945
+ df = pd.DataFrame([key_values.values()], columns=key_values.keys())
946
+ st.dataframe(df.T)
947
+
948
+ # البحث عن الجداول في النص
949
+ if "| ------ |" in results["content"] or "\n|" in results["content"]:
950
+ st.markdown("### جداول مستخرجة")
951
+ # استخراج الجداول من النص Markdown
952
+ table_parts = []
953
+ in_table = False
954
+ current_table = []
955
+
956
+ for line in results["content"].split("\n"):
957
+ if line.startswith("|") and "-|-" in line.replace(" ", ""):
958
+ in_table = True
959
+ current_table.append(line)
960
+ elif in_table and line.startswith("|"):
961
+ current_table.append(line)
962
+ elif in_table and not line.startswith("|") and line.strip():
963
+ in_table = False
964
+ table_parts.append("\n".join(current_table))
965
+ current_table = []
966
+
967
+ # إضافة الجدول الأخير إذا كان هناك
968
+ if current_table:
969
+ table_parts.append("\n".join(current_table))
970
+
971
+ # عرض الجداول
972
+ for i, table in enumerate(table_parts):
973
+ st.markdown(f"#### جدول {i+1}")
974
+ st.markdown(table)
975
+
976
+ # إذا لم يتم العثور على أي بيانات منظمة
977
+ if not headings and not key_values and not ("| ------ |" in results["content"] or "\n|" in results["content"]):
978
+ st.info("لم يتم العثور على بيانات منظمة في النتائج. يمكنك تعديل التوجيه لطلب تنسيق أكثر هيكلية.")
979
+
980
+ with result_tabs[2]:
981
+ # عرض معلومات إضافية
982
+ st.markdown("## معلومات عن التحليل")
983
+
984
+ # عرض معلومات الاستخدام
985
+ col1, col2 = st.columns(2)
986
+
987
+ with col1:
988
+ st.markdown("### معلومات النموذج")
989
+ st.markdown(f"**النموذج المستخدم**: {results.get('model', selected_model)}")
990
+ st.markdown(f"**تاريخ التحليل**: {time.strftime('%Y-%m-%d %H:%M:%S')}")
991
+
992
+ with col2:
993
+ st.markdown("### إحصائيات الاستخدام")
994
+
995
+ if "usage" in results:
996
+ usage = results["usage"]
997
+ st.markdown(f"**توكنز المدخلات**: {usage.get('input_tokens', 'غير متوفر')}")
998
+ st.markdown(f"**توكنز الإخراج**: {usage.get('output_tokens', 'غير متوفر')}")
999
+ st.markdown(f"**إجمالي التوكنز**: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0)}")
1000
+ else:
1001
+ st.info("معلومات الاستخدام غير متوفرة")
1002
+
1003
+ # إضافة خيارات التصدير
1004
+ st.markdown("### تصدير النتائج")
1005
+
1006
+ export_col1, export_col2 = st.columns(2)
1007
+
1008
+ with export_col1:
1009
+ # تصدير كنص
1010
+ st.download_button(
1011
+ label="تحميل النتائج كملف نصي",
1012
+ data=results["content"],
1013
+ file_name=f"claude_analysis_{uploaded_file.name.split('.')[0]}.txt",
1014
+ mime="text/plain"
1015
+ )
1016
+
1017
+ with export_col2:
1018
+ # تصدير كـ Markdown
1019
+ st.download_button(
1020
+ label="تحميل النتائج كملف Markdown",
1021
+ data=results["content"],
1022
+ file_name=f"claude_analysis_{uploaded_file.name.split('.')[0]}.md",
1023
+ mime="text/markdown"
1024
+ )
1025
+ finally:
1026
+ # حذف الملف المؤقت
1027
+ try:
1028
+ os.unlink(temp_path)
1029
+ except:
1030
+ pass
1031
+
1032
+ def analyze_document(self, file_path):
1033
+ """
1034
+ تحليل مستند وإرجاع نتائج التحليل
1035
+
1036
+ المعلمات:
1037
+ file_path (str): مسار المستند المراد تحليله
1038
+
1039
+ العوائد:
1040
+ dict: نتائج تحليل المستند
1041
+ """
1042
+ # تحديد نوع المستند من امتداد الملف
1043
+ _, ext = os.path.splitext(file_path)
1044
+ ext = ext.lower()
1045
+
1046
+ # تحليل المستند حسب نوعه
1047
+ if ext == '.pdf':
1048
+ text = self.text_extractor.extract_from_pdf(file_path)
1049
+ elif ext in ('.doc', '.docx'):
1050
+ text = self.text_extractor.extract_from_docx(file_path)
1051
+ elif ext in ('.jpg', '.jpeg', '.png'):
1052
+ # استخدام محلل Docling للصور إذا كان متاحًا
1053
+ if self.docling_analyzer.is_available():
1054
+ docling_results = self.docling_analyzer.analyze_image(image_path=file_path)
1055
+ if "error" not in docling_results:
1056
+ return {
1057
+ "نص": docling_results["markdown"],
1058
+ "doctags": docling_results["doctags"],
1059
+ "معلومات": {
1060
+ "نوع المستند": "صورة",
1061
+ "تحليل": "تم تحليله باستخدام Docling"
1062
+ }
1063
+ }
1064
+
1065
+ # استخدام المحلل العادي إذا كان Docling غير متاح
1066
+ text = self.text_extractor.extract_from_image(file_path)
1067
+ else:
1068
+ raise ValueError(f"نوع المستند غير مدعوم: {ext}")
1069
+
1070
+ # تحليل المستند
1071
+ document = self.document_parser.parse_document(file_path)
1072
+
1073
+ # استخراج العناصر المنظمة
1074
+ tables = self.item_extractor.extract_tables(document)
1075
+
1076
+ # إرجاع نتائج التحليل
1077
+ return {
1078
+ "نص": text,
1079
+ "جداول": tables,
1080
+ "معلومات": document
1081
+ }
1082
+
1083
+ def analyze_with_claude(self, file_path, model_name="claude-3-7-sonnet", prompt=None):
1084
+ """
1085
+ تحليل مستند باستخدام Claude AI
1086
+
1087
+ المعلمات:
1088
+ file_path (str): مسار المستند المراد تحليله
1089
+ model_name (str): اسم نموذج Claude المراد استخدامه
1090
+ prompt (str): التوجيه المخصص للتحليل (اختياري)
1091
+
1092
+ العوائد:
1093
+ dict: نتائج التحليل
1094
+ """
1095
+ # محاولة تحليل المستند باستخدام Claude
1096
+ try:
1097
+ # التحقق من وجود المفتاح
1098
+ self.claude_analyzer.get_api_key()
1099
+
1100
+ # تحليل المستند باستخدام Claude
1101
+ return self.claude_analyzer.analyze_document(
1102
+ file_path,
1103
+ model_name=model_name,
1104
+ prompt=prompt
1105
+ )
1106
+ except Exception as e:
1107
+ logging.error(f"خطأ في تحليل المستند باستخدام Claude: {str(e)}")
1108
+ return {"error": f"فشل في تحليل المستند باستخدام Claude: {str(e)}"}
1109
+
1110
+
1111
+ # تشغيل التطبيق
1112
+ if __name__ == "__main__":
1113
+ app = DocumentAnalysisApp()
1114
+ app.render()
modules/document_analysis/services/__init__.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ حزمة خدمات تحليل المستندات
3
+
4
+ توفر هذه الحزمة الأدوات والخدمات اللازمة لتحليل المستندات بمختلف أنواعها
5
+ واستخراج النصوص والبيانات المنظمة منها.
6
+ """
7
+
8
+ # استيراد الفئات الرئيسية
9
+ from .text_extractor import TextExtractor
10
+ from .item_extractor import ItemExtractor
11
+ from .document_parser import DocumentParser
12
+
13
+ # تحديد الفئات التي يمكن استيرادها عند استخدام from services import *
14
+ __all__ = [
15
+ 'TextExtractor',
16
+ 'ItemExtractor',
17
+ 'DocumentParser',
18
+ ]
19
+
20
+ # معلومات الإصدار
21
+ __version__ = '0.1.0'
22
+ __author__ = 'فريق تطوير تحليل المستندات'
modules/document_analysis/services/document_parser.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ خدمة تحليل المستندات
4
+
5
+ هذا الملف يحتوي على الفئة المسؤولة عن تحليل المستندات واستخراج المعلومات الهيكلية منها.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+ import datetime
11
+
12
+ class DocumentParser:
13
+ """فئة تحليل المستندات واستخراج المعلومات منها"""
14
+
15
+ def __init__(self, config=None):
16
+ """
17
+ تهيئة محلل المستندات
18
+
19
+ المعلمات:
20
+ config (dict): إعدادات محلل المستندات
21
+ """
22
+ self.config = config or {}
23
+ self.logger = logging.getLogger(__name__)
24
+
25
+ def parse(self, file_path):
26
+ """
27
+ تحليل المستند واستخراج المعلومات منه
28
+
29
+ المعلمات:
30
+ file_path (str): مسار الملف
31
+
32
+ العوائد:
33
+ dict: معلومات المستند المستخرجة
34
+ """
35
+ self.logger.info(f"جاري تحليل المستند: {file_path}")
36
+
37
+ try:
38
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
39
+ # محاكاة التحليل للعرض
40
+ file_name = os.path.basename(file_path)
41
+ file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
42
+
43
+ # تحديد نوع الملف
44
+ _, ext = os.path.splitext(file_path)
45
+ ext = ext.lower()
46
+
47
+ # تحديد نوع المستند
48
+ document_type = self._get_document_type(ext)
49
+
50
+ # محاكاة معلومات المستند
51
+ current_date = datetime.datetime.now().strftime("%Y-%m-%d")
52
+
53
+ result = {
54
+ "اسم الملف": file_name,
55
+ "حجم الملف": f"{file_size / 1024:.2f} كيلوبايت",
56
+ "نوع الملف": document_type,
57
+ "تاريخ التحليل": current_date,
58
+ "تقدير عدد الصفحات": self._estimate_pages(file_size),
59
+ "نتائج التحليل": {
60
+ "نوع المستند": self._classify_document(file_name),
61
+ "درجة الثقة": "85%",
62
+ "الأقسام الرئيسية": self._get_main_sections(),
63
+ "الكلمات الرئيسية": self._get_main_keywords(),
64
+ "الشروط الهامة": self._get_important_terms()
65
+ }
66
+ }
67
+
68
+ return result
69
+ except Exception as e:
70
+ self.logger.error(f"خطأ في تحليل المستند: {str(e)}")
71
+ return {"خطأ": f"حدث خطأ أثناء تحليل المستند: {str(e)}"}
72
+
73
+ def parse_document(self, file_path):
74
+ """
75
+ تحليل المستند واستخراج المعلومات الأساسية منه
76
+
77
+ المعلمات:
78
+ file_path (str): مسار الملف
79
+
80
+ العوائد:
81
+ dict: معلومات المستند الأساسية
82
+ """
83
+ self.logger.info(f"جاري تحليل المستند الأساسي: {file_path}")
84
+
85
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
86
+ # محاكاة التحليل للعرض
87
+ file_name = os.path.basename(file_path)
88
+
89
+ return {
90
+ "نوع": self._classify_document(file_name),
91
+ "محتوى": "محتوى المستند...",
92
+ "هيكل": {
93
+ "عنوان": "عنوان المستند",
94
+ "أقسام": ["قسم 1", "قسم 2", "قسم 3"]
95
+ }
96
+ }
97
+
98
+ def _get_document_type(self, ext):
99
+ """
100
+ تحديد نوع المستند من امتداد الملف
101
+
102
+ المعلمات:
103
+ ext (str): امتداد الملف
104
+
105
+ العوائد:
106
+ str: نوع المستند
107
+ """
108
+ document_types = {
109
+ '.pdf': 'مستند PDF',
110
+ '.doc': 'مستند Word',
111
+ '.docx': 'مستند Word',
112
+ '.jpg': 'صورة JPEG',
113
+ '.jpeg': 'صورة JPEG',
114
+ '.png': 'صورة PNG',
115
+ '.xlsx': 'جدول Excel',
116
+ '.xls': 'جدول Excel',
117
+ '.txt': 'ملف نصي'
118
+ }
119
+
120
+ return document_types.get(ext, 'نوع ملف غير معروف')
121
+
122
+ def _estimate_pages(self, file_size):
123
+ """
124
+ تقدير عدد صفحات المستند بناءً على حجمه
125
+
126
+ المعلمات:
127
+ file_size (int): حجم الملف بالبايت
128
+
129
+ العوائد:
130
+ int: تقدير عدد الصفحات
131
+ """
132
+ # تقدير بسيط: كل 50 كيلوبايت تقريباً صفحة واحدة
133
+ # هذا تقدير بسيط جداً ويختلف حسب نوع المستند ومحتواه
134
+ return max(1, int(file_size / (50 * 1024)))
135
+
136
+ def _classify_document(self, file_name):
137
+ """
138
+ تصنيف نوع المستند بناءً على اسمه
139
+
140
+ المعلمات:
141
+ file_name (str): اسم الملف
142
+
143
+ العوائد:
144
+ str: تصنيف المستند
145
+ """
146
+ file_name_lower = file_name.lower()
147
+
148
+ if 'عقد' in file_name_lower or 'contract' in file_name_lower:
149
+ return "عقد"
150
+ elif 'مناقصة' in file_name_lower or 'tender' in file_name_lower:
151
+ return "مستند مناقصة"
152
+ elif 'تقرير' in file_name_lower or 'report' in file_name_lower:
153
+ return "تقرير"
154
+ elif 'فاتورة' in file_name_lower or 'invoice' in file_name_lower:
155
+ return "فاتورة"
156
+ elif 'عرض' in file_name_lower or 'proposal' in file_name_lower:
157
+ return "عرض سعر"
158
+ elif 'مواصفات' in file_name_lower or 'spec' in file_name_lower:
159
+ return "مواصفات فنية"
160
+ elif 'كراسة' in file_name_lower or 'شروط' in file_name_lower:
161
+ return "كراسة شروط"
162
+ else:
163
+ return "مستند عام"
164
+
165
+ def _get_main_sections(self):
166
+ """
167
+ الحصول على قائمة الأقسام الرئيسية التقديرية للمستند
168
+
169
+ العوائد:
170
+ list: قائمة الأقسام الرئيسية
171
+ """
172
+ # محاكاة قائمة الأقسام
173
+ return [
174
+ "مقدمة",
175
+ "نطاق العمل",
176
+ "المواصفات الفنية",
177
+ "جدول الكميات",
178
+ "الشروط والأحكام",
179
+ "الجدول الزمني",
180
+ "المتطلبات الخاصة"
181
+ ]
182
+
183
+ def _get_main_keywords(self):
184
+ """
185
+ الحصول على قائمة الكلمات الرئيسية التقديرية للمستند
186
+
187
+ العوائد:
188
+ list: قائمة الكلمات الرئيسية
189
+ """
190
+ # محاكاة قائمة الكلمات الرئيسية
191
+ return [
192
+ "مناقصة",
193
+ "بناء",
194
+ "تشييد",
195
+ "تسليم مفتاح",
196
+ "مواصفات فنية",
197
+ "جدول كميات",
198
+ "ضمان",
199
+ "غرامة تأخير",
200
+ "دفعة مقدمة",
201
+ "محتوى محلي"
202
+ ]
203
+
204
+ def _get_important_terms(self):
205
+ """
206
+ الحصول على قائمة الشروط الهامة التقديرية للمستند
207
+
208
+ العوائد:
209
+ list: قائمة الشروط الهامة
210
+ """
211
+ # محاكاة قائمة الشروط الهامة
212
+ return [
213
+ "مدة تنفيذ المشروع: 18 شهر",
214
+ "غرامة التأخير: 0.5% أسبوعياً بحد أقصى 10%",
215
+ "الدفعة المقدمة: 10%",
216
+ "الضمان النهائي: 5% لمدة سنة",
217
+ "شروط الدفع: دفعات شهرية حسب نسبة الإنجاز",
218
+ "المحتوى المحلي: 70% كحد أدنى"
219
+ ]
modules/document_analysis/services/item_extractor.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ خدمة استخراج البنود من المستندات
4
+
5
+ هذا الملف يحتوي على الفئة المسؤولة عن استخراج البنود والجداول من المستندات.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+
11
+ class ItemExtractor:
12
+ """فئة استخراج البنود من المستندات"""
13
+
14
+ def __init__(self, config=None):
15
+ """
16
+ تهيئة مستخرج البنود
17
+
18
+ المعلمات:
19
+ config (dict): إعدادات مستخرج البنود
20
+ """
21
+ self.config = config or {}
22
+ self.logger = logging.getLogger(__name__)
23
+
24
+ def extract(self, file_path):
25
+ """
26
+ استخراج البنود من ملف
27
+
28
+ المعلمات:
29
+ file_path (str): مسار الملف
30
+
31
+ العوائد:
32
+ list: قائمة البنود المستخرجة
33
+ """
34
+ self.logger.info(f"جاري استخراج البنود من الملف: {file_path}")
35
+
36
+ try:
37
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
38
+ # محاكاة الاستخراج للعرض
39
+ file_name = os.path.basename(file_path)
40
+
41
+ # تحديد نوع الملف
42
+ _, ext = os.path.splitext(file_path)
43
+ ext = ext.lower()
44
+
45
+ if ext == '.pdf':
46
+ return self._extract_items_from_pdf(file_path)
47
+ elif ext in ('.doc', '.docx'):
48
+ return self._extract_items_from_docx(file_path)
49
+ else:
50
+ return [{"بند": "نوع الملف غير مدعوم", "قيمة": 0}]
51
+ except Exception as e:
52
+ self.logger.error(f"خطأ في استخراج البنود: {str(e)}")
53
+ return [{"بند": "حدث خطأ أثناء الاستخراج", "قيمة": 0, "خطأ": str(e)}]
54
+
55
+ def _extract_items_from_pdf(self, file_path):
56
+ """
57
+ استخراج البنود من ملف PDF
58
+
59
+ المعلمات:
60
+ file_path (str): مسار ملف PDF
61
+
62
+ العوائد:
63
+ list: قائمة البنود المستخرجة
64
+ """
65
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
66
+ # محاكاة الاستخراج للعرض
67
+ return [
68
+ {"بند": "أعمال الخرسانة", "وحدة": "م3", "كمية": 250, "سعر الوحدة": 1200, "الإجمالي": 300000},
69
+ {"بند": "أعمال التشطيبات", "وحدة": "م2", "كمية": 1500, "سعر الوحدة": 350, "الإجمالي": 525000},
70
+ {"بند": "أعمال الكهرباء", "وحدة": "نقطة", "كمية": 120, "سعر الوحدة": 200, "الإجمالي": 24000},
71
+ {"بند": "أعمال السباكة", "وحدة": "نقطة", "كمية": 75, "سعر الوحدة": 300, "الإجمالي": 22500},
72
+ {"بند": "أعمال الألمنيوم", "وحدة": "م2", "كمية": 85, "سعر الوحدة": 1500, "الإجمالي": 127500}
73
+ ]
74
+
75
+ def _extract_items_from_docx(self, file_path):
76
+ """
77
+ استخراج البنود من ملف Word
78
+
79
+ المعلمات:
80
+ file_path (str): مسار ملف Word
81
+
82
+ العوائد:
83
+ list: قائمة البنود المستخرجة
84
+ """
85
+ # في البيئة الحقيقية، استخدم تحليل متقدم للمستند
86
+ # محاكاة الاستخراج للعرض
87
+ return [
88
+ {"بند": "استشارات هندسية", "وحدة": "ساعة", "كمية": 120, "سعر الوحدة": 500, "الإجمالي": 60000},
89
+ {"بند": "تصميم معماري", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 100, "الإجمالي": 180000},
90
+ {"بند": "تصميم إنشائي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 80, "الإجمالي": 144000},
91
+ {"بند": "تصميم كهربائي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 40, "الإجمالي": 72000},
92
+ {"بند": "تصميم ميكانيكي", "وحدة": "م2", "كمية": 1800, "سعر الوحدة": 40, "الإجمالي": 72000}
93
+ ]
94
+
95
+ def extract_tables(self, document):
96
+ """
97
+ استخراج الجداول من مستند
98
+
99
+ المعلمات:
100
+ document (dict): المستند المحلل
101
+
102
+ العوائد:
103
+ list: قائمة الجداول المستخرجة
104
+ """
105
+ self.logger.info("جاري استخراج الجداول من المستند")
106
+
107
+ try:
108
+ # في البيئة الحقيقية، ا��تخدم تحليل متقدم للمستند
109
+ # محاكاة الاستخراج للعرض
110
+ return [
111
+ {
112
+ "عنوان": "جدول البنود والتكاليف",
113
+ "بيانات": [
114
+ {"بند": "أعمال الخرسانة", "وحدة": "م3", "كمية": 250, "سعر الوحدة": 1200, "الإجمالي": 300000},
115
+ {"بند": "أعمال التشطيبات", "وحدة": "م2", "كمية": 1500, "سعر الوحدة": 350, "الإجمالي": 525000},
116
+ {"بند": "أعمال الكهرباء", "وحدة": "نقطة", "كمية": 120, "سعر الوحدة": 200, "الإجمالي": 24000},
117
+ {"بند": "أعمال السباكة", "وحدة": "نقطة", "كمية": 75, "سعر الوحدة": 300, "الإجمالي": 22500},
118
+ {"بند": "أعمال الألمنيوم", "وحدة": "م2", "كمية": 85, "سعر الوحدة": 1500, "الإجمالي": 127500}
119
+ ]
120
+ },
121
+ {
122
+ "عنوان": "جدول المعلومات العامة",
123
+ "بيانات": [
124
+ {"اسم المشروع": "مبنى سكني", "المالك": "شركة الإسكان", "الموقع": "الرياض", "المساحة": "2500 م2"},
125
+ {"اسم المشروع": "مبنى تجاري", "المالك": "شركة التطوير", "الموقع": "جدة", "المساحة": "3500 م2"}
126
+ ]
127
+ }
128
+ ]
129
+ except Exception as e:
130
+ self.logger.error(f"خطأ في استخراج الجداول: {str(e)}")
131
+ return [{"عنوان": "حدث خطأ أثناء الاستخراج", "بيانات": []}]
modules/document_analysis/services/text_extractor.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ خدمة استخراج النص من المستندات
4
+
5
+ هذا الملف يحتوي على الفئة المسؤولة عن استخراج النص من أنواع مختلفة من المستندات.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+
11
+ class TextExtractor:
12
+ """فئة استخراج النص من المستندات"""
13
+
14
+ def __init__(self, config=None):
15
+ """
16
+ تهيئة مستخرج النص
17
+
18
+ المعلمات:
19
+ config (dict): إعدادات مستخرج النص
20
+ """
21
+ self.config = config or {}
22
+ self.logger = logging.getLogger(__name__)
23
+
24
+ def extract(self, file_path):
25
+ """
26
+ استخراج النص من ملف بناءً على نوع الملف
27
+
28
+ المعلمات:
29
+ file_path (str): مسار الملف
30
+
31
+ العوائد:
32
+ str: النص المستخرج
33
+ """
34
+ _, ext = os.path.splitext(file_path)
35
+ ext = ext.lower()
36
+
37
+ if ext == '.pdf':
38
+ return self.extract_from_pdf(file_path)
39
+ elif ext in ('.doc', '.docx'):
40
+ return self.extract_from_docx(file_path)
41
+ elif ext in ('.jpg', '.jpeg', '.png'):
42
+ return self.extract_from_image(file_path)
43
+ else:
44
+ self.logger.warning(f"نوع ملف غير مدعوم: {ext}")
45
+ return f"نوع ملف غير مدعوم: {ext}"
46
+
47
+ def extract_from_pdf(self, file_path):
48
+ """
49
+ استخراج النص من ملف PDF
50
+
51
+ المعلمات:
52
+ file_path (str): مسار ملف PDF
53
+
54
+ العوائد:
55
+ str: النص المستخرج
56
+ """
57
+ self.logger.info(f"جاري استخراج النص من ملف PDF: {file_path}")
58
+
59
+ try:
60
+ # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل PyPDF2 أو pdfplumber
61
+ # محاكاة الاستخراج للعرض
62
+ return f"هذا نص مستخرج من ملف PDF: {os.path.basename(file_path)}\n\nيتم استخراج النص من الملف باستخدام مكتبة مناسبة في البيئة الحقيقية."
63
+ except Exception as e:
64
+ self.logger.error(f"خطأ في استخراج النص من PDF: {str(e)}")
65
+ return f"حدث خطأ أثناء استخراج النص: {str(e)}"
66
+
67
+ def extract_from_docx(self, file_path):
68
+ """
69
+ استخراج النص من ملف Word
70
+
71
+ المعلمات:
72
+ file_path (str): مسار ملف Word
73
+
74
+ العوائد:
75
+ str: النص المستخرج
76
+ """
77
+ self.logger.info(f"جاري استخراج النص من ملف Word: {file_path}")
78
+
79
+ try:
80
+ # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل python-docx
81
+ # محاكاة الاستخراج للعرض
82
+ return f"هذا نص مستخرج من ملف Word: {os.path.basename(file_path)}\n\nيتم استخراج النص من الملف باستخدام مكتبة مناسبة في البيئة الحقيقية."
83
+ except Exception as e:
84
+ self.logger.error(f"خطأ في استخراج النص من Word: {str(e)}")
85
+ return f"حدث خطأ أثناء استخراج النص: {str(e)}"
86
+
87
+ def extract_from_image(self, file_path):
88
+ """
89
+ استخراج النص من ملف صورة باستخدام OCR
90
+
91
+ المعلمات:
92
+ file_path (str): مسار ملف الصورة
93
+
94
+ العوائد:
95
+ str: النص المستخرج
96
+ """
97
+ self.logger.info(f"جاري استخراج النص من ملف صورة: {file_path}")
98
+
99
+ try:
100
+ # في البيئة الحقيقية، استخدم مكتبة مناسبة مثل pytesseract
101
+ # محاكاة الاستخراج للعرض
102
+ return f"هذا نص مستخرج من ملف صورة: {os.path.basename(file_path)}\n\nيتم استخراج النص من الصورة باستخدام تقنية OCR في البيئة الحقيقية."
103
+ except Exception as e:
104
+ self.logger.error(f"خطأ في استخراج النص من الصورة: {str(e)}")
105
+ return f"حدث خطأ أثناء استخراج النص: {str(e)}"
modules/file_comparison/file_comparison_app.py ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ وحدة مقارنة الملفات
3
+ """
4
+
5
+ import streamlit as st
6
+ import pandas as pd
7
+ import random
8
+ from datetime import datetime
9
+ import difflib
10
+
11
+ class FileComparisonApp:
12
+ """
13
+ وحدة مقارنة الملفات للنظام
14
+ """
15
+
16
+ def __init__(self):
17
+ """
18
+ تهيئة وحدة مقارنة الملفات
19
+ """
20
+ # تهيئة حالة الجلسة الخاصة بمقارنة الملفات إذا لم تكن موجودة
21
+ if 'comparison_history' not in st.session_state:
22
+ # إنشاء بيانات تجريبية لسجل المقارنات
23
+ st.session_state.comparison_history = self._generate_sample_history()
24
+
25
+ def run(self):
26
+ """
27
+ تشغيل وحدة مقارنة الملفات
28
+ """
29
+ st.markdown("<h2 class='module-title'>وحدة مقارنة الملفات</h2>", unsafe_allow_html=True)
30
+
31
+ # إنشاء تبويبات لمقارنة الملفات المختلفة
32
+ tabs = st.tabs(["مقارنة المستندات", "مقارنة جداول البيانات", "مقارنة النصوص", "سجل المقارنات"])
33
+
34
+ with tabs[0]:
35
+ self._render_document_comparison()
36
+
37
+ with tabs[1]:
38
+ self._render_spreadsheet_comparison()
39
+
40
+ with tabs[2]:
41
+ self._render_text_comparison()
42
+
43
+ with tabs[3]:
44
+ self._render_comparison_history()
45
+
46
+ def _render_document_comparison(self):
47
+ """
48
+ عرض واجهة مقارنة المستندات
49
+ """
50
+ st.markdown("### مقارنة المستندات")
51
+ st.markdown("مقارنة مستندات PDF أو Word مع إظهار الاختلافات بشكل مرئي")
52
+
53
+ # رفع الملفات
54
+ col1, col2 = st.columns(2)
55
+
56
+ with col1:
57
+ st.markdown("#### المستند الأول")
58
+ file1 = st.file_uploader("اختر المستند الأول", type=["pdf", "docx"], key="doc_file1")
59
+
60
+ with col2:
61
+ st.markdown("#### المستند الثاني")
62
+ file2 = st.file_uploader("اختر المستند الثاني", type=["pdf", "docx"], key="doc_file2")
63
+
64
+ # خيارات المقارنة
65
+ st.markdown("#### خيارات المقارنة")
66
+
67
+ col1, col2, col3 = st.columns(3)
68
+
69
+ with col1:
70
+ st.checkbox("تجاهل التنسيق", value=False, key="ignore_formatting")
71
+
72
+ with col2:
73
+ st.checkbox("تجاهل الهوامش", value=True, key="ignore_headers_footers")
74
+
75
+ with col3:
76
+ st.checkbox("إظهار التغييرات الطفيفة", value=False, key="show_minor_changes")
77
+
78
+ # زر المقارنة
79
+ if st.button("مقارنة المستندات", key="compare_docs_btn"):
80
+ if file1 is not None and file2 is not None:
81
+ # محاكاة عملية المقارنة
82
+ with st.spinner("جاري مقارنة المستندات..."):
83
+ # محاكاة وقت المعالجة
84
+ import time
85
+ time.sleep(2)
86
+
87
+ st.success("تمت مقارنة المستندات بنجاح!", icon="✅")
88
+
89
+ # عرض ملخص المقارنة
90
+ st.markdown("#### ملخص المقارنة")
91
+
92
+ comparison_summary = {
93
+ "عدد الصفحات المتطابقة": random.randint(3, 8),
94
+ "عدد الصفحات المختلفة": random.randint(1, 5),
95
+ "إجمالي التغييرات": random.randint(10, 50),
96
+ "إضافات": random.randint(5, 20),
97
+ "حذف": random.randint(5, 20),
98
+ "تعديلات": random.randint(5, 20)
99
+ }
100
+
101
+ summary_df = pd.DataFrame({
102
+ "المعيار": list(comparison_summary.keys()),
103
+ "القيمة": list(comparison_summary.values())
104
+ })
105
+
106
+ st.dataframe(summary_df, use_container_width=True, hide_index=True)
107
+
108
+ # عرض صورة توضيحية للمقارنة
109
+ st.markdown("#### معاينة المقارنة")
110
+ st.info("سيتم عرض معاينة المقارنة هنا مع تمييز الاختلافات بألوان مختلفة", icon="ℹ️")
111
+
112
+ # زر تنزيل تقرير المقارنة
113
+ st.download_button(
114
+ label="تنزيل تقرير المقارنة",
115
+ data=b"محتوى وهمي لتقرير المقارنة",
116
+ file_name="document_comparison_report.pdf",
117
+ mime="application/pdf",
118
+ key="download_doc_comparison"
119
+ )
120
+
121
+ # إضافة المقارنة إلى السجل
122
+ new_entry = {
123
+ 'id': len(st.session_state.comparison_history) + 1,
124
+ 'type': 'مستند',
125
+ 'file1': file1.name,
126
+ 'file2': file2.name,
127
+ 'changes': comparison_summary["إجمالي التغييرات"],
128
+ 'date': datetime.now().strftime("%Y-%m-%d %H:%M")
129
+ }
130
+ st.session_state.comparison_history.insert(0, new_entry)
131
+ else:
132
+ st.warning("يرجى رفع المستندين للمقارنة", icon="⚠️")
133
+
134
+ def _render_spreadsheet_comparison(self):
135
+ """
136
+ عرض واجهة مقارنة جداول البيانات
137
+ """
138
+ st.markdown("### مقارنة جداول البيانات")
139
+ st.markdown("مقارنة ملفات Excel أو CSV مع تحديد الاختلافات في البيانات")
140
+
141
+ # رفع الملفات
142
+ col1, col2 = st.columns(2)
143
+
144
+ with col1:
145
+ st.markdown("#### الجدول الأول")
146
+ sheet1 = st.file_uploader("اختر الجدول الأول", type=["xlsx", "csv"], key="sheet_file1")
147
+
148
+ with col2:
149
+ st.markdown("#### الجدول الثاني")
150
+ sheet2 = st.file_uploader("اختر الجدول الثاني", type=["xlsx", "csv"], key="sheet_file2")
151
+
152
+ # خيارات المقارنة
153
+ st.markdown("#### خيارات المقارنة")
154
+
155
+ col1, col2 = st.columns(2)
156
+
157
+ with col1:
158
+ st.checkbox("مقارنة الصيغ", value=True, key="compare_formulas")
159
+ st.checkbox("مقارنة التنسيق", value=False, key="compare_formatting")
160
+
161
+ with col2:
162
+ st.checkbox("تجاهل الأوراق المخفية", value=True, key="ignore_hidden_sheets")
163
+ st.checkbox("مقارنة حسب القيمة فقط", value=False, key="compare_by_value")
164
+
165
+ # زر المقارنة
166
+ if st.button("مقارنة الجداول", key="compare_sheets_btn"):
167
+ if sheet1 is not None and sheet2 is not None:
168
+ # محاكاة عملية المقارنة
169
+ with st.spinner("جاري مقارنة جداول البيانات..."):
170
+ # محاكاة وقت المعالجة
171
+ import time
172
+ time.sleep(2)
173
+
174
+ st.success("تمت مقارنة جداول البيانات بنجاح!", icon="✅")
175
+
176
+ # عرض ملخص المقارنة
177
+ st.markdown("#### ملخص المقارنة")
178
+
179
+ comparison_summary = {
180
+ "عدد الخلايا المتطابقة": random.randint(500, 2000),
181
+ "عدد الخلايا المختلفة": random.randint(50, 200),
182
+ "صفوف مضافة": random.randint(5, 20),
183
+ "صفوف محذوفة": random.randint(5, 20),
184
+ "أعمدة مضافة": random.randint(1, 5),
185
+ "أعمدة محذوفة": random.randint(1, 5)
186
+ }
187
+
188
+ summary_df = pd.DataFrame({
189
+ "المعيار": list(comparison_summary.keys()),
190
+ "القيمة": list(comparison_summary.values())
191
+ })
192
+
193
+ st.dataframe(summary_df, use_container_width=True, hide_index=True)
194
+
195
+ # عرض جدول الاختلافات
196
+ st.markdown("#### الاختلافات الرئيسية")
197
+
198
+ # إنشاء بيانات تجريبية للاختلافات
199
+ diff_data = []
200
+ for i in range(10):
201
+ diff_data.append({
202
+ "الورقة": f"الورقة {random.randint(1, 3)}",
203
+ "الخلية": f"{chr(65 + random.randint(0, 5))}{random.randint(1, 20)}",
204
+ "القيمة الأولى": f"القيمة {i} (الأولى)",
205
+ "القيمة الثانية": f"القيمة {i} (الثانية)",
206
+ "نوع الاختلاف": random.choice(["تعديل", "إضافة", "حذف"])
207
+ })
208
+
209
+ diff_df = pd.DataFrame(diff_data)
210
+ st.dataframe(diff_df, use_container_width=True)
211
+
212
+ # زر تنزيل تقرير المقارنة
213
+ st.download_button(
214
+ label="تنزيل تقرير المقارنة",
215
+ data=b"محتوى وهمي لتقرير المقارنة",
216
+ file_name="spreadsheet_comparison_report.xlsx",
217
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
218
+ key="download_sheet_comparison"
219
+ )
220
+
221
+ # إضافة المقارنة إلى السجل
222
+ new_entry = {
223
+ 'id': len(st.session_state.comparison_history) + 1,
224
+ 'type': 'جدول بيانات',
225
+ 'file1': sheet1.name,
226
+ 'file2': sheet2.name,
227
+ 'changes': comparison_summary["عدد الخلايا المختلفة"],
228
+ 'date': datetime.now().strftime("%Y-%m-%d %H:%M")
229
+ }
230
+ st.session_state.comparison_history.insert(0, new_entry)
231
+ else:
232
+ st.warning("يرجى رفع جدولي البيانات للمقارنة", icon="⚠️")
233
+
234
+ def _render_text_comparison(self):
235
+ """
236
+ عرض واجهة مقارنة النصوص
237
+ """
238
+ st.markdown("### مقارنة النصوص")
239
+ st.markdown("مقارنة نصوص مباشرة أو ملفات نصية مع إظهار الاختلافات")
240
+
241
+ # اختيار طريقة الإدخال
242
+ input_method = st.radio(
243
+ "طريقة الإدخال",
244
+ options=["إدخال مباشر", "رفع ملفات"],
245
+ horizontal=True,
246
+ key="text_input_method"
247
+ )
248
+
249
+ if input_method == "إدخال مباشر":
250
+ # إدخال النصوص مباشرة
251
+ col1, col2 = st.columns(2)
252
+
253
+ with col1:
254
+ st.markdown("#### النص الأول")
255
+ text1 = st.text_area("أدخل النص الأول", height=200, key="direct_text1")
256
+
257
+ with col2:
258
+ st.markdown("#### النص الثاني")
259
+ text2 = st.text_area("أدخل النص الثاني", height=200, key="direct_text2")
260
+
261
+ # زر المقارنة
262
+ if st.button("مقارنة النصوص", key="compare_direct_text_btn"):
263
+ if text1 and text2:
264
+ self._show_text_comparison_results(text1, text2)
265
+ else:
266
+ st.warning("يرجى إدخال النصين للمقارنة", icon="⚠️")
267
+ else:
268
+ # رفع ملفات نصية
269
+ col1, col2 = st.columns(2)
270
+
271
+ with col1:
272
+ st.markdown("#### الملف الأول")
273
+ text_file1 = st.file_uploader("اختر الملف الأول", type=["txt", "md", "json", "xml", "html", "css", "js", "py"], key="text_file1")
274
+
275
+ with col2:
276
+ st.markdown("#### الملف الثاني")
277
+ text_file2 = st.file_uploader("اختر الملف الثاني", type=["txt", "md", "json", "xml", "html", "css", "js", "py"], key="text_file2")
278
+
279
+ # زر المقارنة
280
+ if st.button("مقارنة الملفات النصية", key="compare_text_files_btn"):
281
+ if text_file1 is not None and text_file2 is not None:
282
+ # قراءة محتوى الملفات
283
+ text1 = text_file1.getvalue().decode("utf-8")
284
+ text2 = text_file2.getvalue().decode("utf-8")
285
+
286
+ self._show_text_comparison_results(text1, text2, text_file1.name, text_file2.name)
287
+ else:
288
+ st.warning("يرجى رفع الملفين النصيين للمقارنة", icon="⚠️")
289
+
290
+ def _show_text_comparison_results(self, text1, text2, file1_name=None, file2_name=None):
291
+ """
292
+ عرض نتائج مقارنة النصوص
293
+ """
294
+ # محاكاة عملية المقارنة
295
+ with st.spinner("جاري مقارنة النصوص..."):
296
+ # استخدام difflib للمقارنة
297
+ d = difflib.Differ()
298
+ diff = list(d.compare(text1.splitlines(), text2.splitlines()))
299
+
300
+ # عرض ملخص المقارنة
301
+ st.markdown("#### ملخص المقارنة")
302
+
303
+ # حساب الإحصائيات
304
+ added = len([line for line in diff if line.startswith('+ ')])
305
+ removed = len([line for line in diff if line.startswith('- ')])
306
+ changed = len([line for line in diff if line.startswith('? ')])
307
+ unchanged = len([line for line in diff if line.startswith(' ')])
308
+
309
+ comparison_summary = {
310
+ "الأسطر المتطابقة": unchanged,
311
+ "الأسطر المضافة": added,
312
+ "الأسطر المحذوفة": removed,
313
+ "الأسطر المتغيرة": changed,
314
+ "إجمالي الاختلافات": added + removed + changed
315
+ }
316
+
317
+ summary_df = pd.DataFrame({
318
+ "المعيار": list(comparison_summary.keys()),
319
+ "القيمة": list(comparison_summary.values())
320
+ })
321
+
322
+ st.dataframe(summary_df, use_container_width=True, hide_index=True)
323
+
324
+ # عرض الاختلافات
325
+ st.markdown("#### عرض الاختلافات")
326
+
327
+ # تنسيق الاختلافات بألوان مختلفة
328
+ html_diff = []
329
+ for line in diff:
330
+ if line.startswith('+ '):
331
+ html_diff.append(f'<div style="background-color: #d4edda; color: #155724; padding: 2px 5px; margin: 2px 0; border-radius: 3px;">{line}</div>')
332
+ elif line.startswith('- '):
333
+ html_diff.append(f'<div style="background-color: #f8d7da; color: #721c24; padding: 2px 5px; margin: 2px 0; border-radius: 3px;">{line}</div>')
334
+ elif line.startswith('? '):
335
+ html_diff.append(f'<div style="background-color: #fff3cd; color: #856404; padding: 2px 5px; margin: 2px 0; border-radius: 3px;">{line}</div>')
336
+ else:
337
+ html_diff.append(f'<div style="padding: 2px 5px; margin: 2px 0;">{line}</div>')
338
+
339
+ st.markdown(''.join(html_diff), unsafe_allow_html=True)
340
+
341
+ # زر تنزيل تقرير المقارنة
342
+ st.download_button(
343
+ label="تنزيل تقرير المقارنة",
344
+ data='\n'.join(diff),
345
+ file_name="text_comparison_report.txt",
346
+ mime="text/plain",
347
+ key="download_text_comparison"
348
+ )
349
+
350
+ # إضافة المقارنة إلى السجل
351
+ new_entry = {
352
+ 'id': len(st.session_state.comparison_history) + 1,
353
+ 'type': 'نص',
354
+ 'file1': file1_name or "نص مباشر 1",
355
+ 'file2': file2_name or "نص مباشر 2",
356
+ 'changes': comparison_summary["إجمالي الاختلافات"],
357
+ 'date': datetime.now().strftime("%Y-%m-%d %H:%M")
358
+ }
359
+ st.session_state.comparison_history.insert(0, new_entry)
360
+
361
+ def _render_comparison_history(self):
362
+ """
363
+ عرض سجل المقارنات
364
+ """
365
+ st.markdown("### سجل المقارنات")
366
+ st.markdown("عرض سجل المقارنات السابقة مع إمكانية البحث والتصفية")
367
+
368
+ # خيارات التصفية
369
+ col1, col2 = st.columns(2)
370
+
371
+ with col1:
372
+ filter_type = st.multiselect(
373
+ "نوع المقارنة",
374
+ options=["الكل", "مستند", "جدول بيانات", "نص"],
375
+ default=["الكل"]
376
+ )
377
+
378
+ with col2:
379
+ date_range = st.selectbox(
380
+ "النطاق الزمني",
381
+ options=["الكل", "اليوم", "الأسبوع الماضي", "الشهر الماضي"]
382
+ )
383
+
384
+ # تطبيق التصفية
385
+ filtered_history = st.session_state.comparison_history
386
+
387
+ if "الكل" not in filter_type:
388
+ filtered_history = [h for h in filtered_history if h['type'] in filter_type]
389
+
390
+ # تحويل البيانات إلى DataFrame
391
+ if filtered_history:
392
+ df = pd.DataFrame(filtered_history)
393
+ df = df[['date', 'type', 'file1', 'file2', 'changes']]
394
+ df.columns = ['التاريخ', 'النوع', 'الملف الأول', 'الملف الثاني', 'عدد الاختلافات']
395
+
396
+ # عرض الجدول
397
+ st.dataframe(df, use_container_width=True)
398
+ else:
399
+ st.info("لا توجد مقارنات تطابق معايير التصفية", icon="ℹ️")
400
+
401
+ def _generate_sample_history(self):
402
+ """
403
+ إنشاء بيانات تجريبية لسجل المقارنات
404
+ """
405
+ comparison_types = ['مستند', 'جدول بيانات', 'نص']
406
+
407
+ file_names = {
408
+ 'مستند': [
409
+ 'مواصفات_المشروع_v1.pdf', 'مواصفات_المشروع_v2.pdf',
410
+ 'العقد_النهائي.docx', 'العقد_المعدل.docx',
411
+ 'تقرير_المشروع_2025.pdf', 'تقرير_المشروع_2025_مراجعة.pdf'
412
+ ],
413
+ 'جدول بيانات': [
414
+ 'جدول_الكميات_v1.xlsx', 'جدول_الكميات_v2.xlsx',
415
+ 'تحليل_الأسعار_2025.xlsx', 'تحليل_الأسعار_2025_محدث.xlsx',
416
+ 'الميزانية_التقديرية.xlsx', 'الميزانية_النهائية.xlsx'
417
+ ],
418
+ 'نص': [
419
+ 'ملاحظات_الاجتماع.txt', 'ملاحظات_الاجتماع_محدثة.txt',
420
+ 'شروط_المناقصة.txt', 'شروط_المناقصة_معدلة.txt',
421
+ 'config.json', 'config_new.json'
422
+ ]
423
+ }
424
+
425
+ history = []
426
+ for i in range(15):
427
+ comp_type = random.choice(comparison_types)
428
+
429
+ # اختيار ملفين من نفس النوع
430
+ file_index = random.randint(0, 2) * 2
431
+ file1 = file_names[comp_type][file_index]
432
+ file2 = file_names[comp_type][file_index + 1]
433
+
434
+ # تحديد تاريخ عشوائي خلال الشهر الماضي
435
+ days_ago = random.randint(0, 30)
436
+ entry_date = (datetime.now() - pd.Timedelta(days=days_ago)).strftime("%Y-%m-%d %H:%M")
437
+
438
+ entry = {
439
+ 'id': i + 1,
440
+ 'type': comp_type,
441
+ 'file1': file1,
442
+ 'file2': file2,
443
+ 'changes': random.randint(10, 100),
444
+ 'date': entry_date
445
+ }
446
+
447
+ history.append(entry)
448
+
449
+ # ترتيب السجل حسب التاريخ (الأحدث أولاً)
450
+ history.sort(key=lambda x: x['date'], reverse=True)
451
+
452
+ return history
modules/maps/maps_app.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ وحدة الخرائط المتكاملة
3
+ """
4
+
5
+ import streamlit as st
6
+ import folium
7
+ from streamlit_folium import folium_static
8
+ import pandas as pd
9
+ import random
10
+
11
+ class MapsApp:
12
+ """
13
+ وحدة الخرائط المتكاملة للنظام
14
+ """
15
+
16
+ def __init__(self):
17
+ """
18
+ تهيئة وحدة الخرائط
19
+ """
20
+ # تهيئة حالة الجلسة الخاصة بالخرائط إذا لم تكن موجودة
21
+ if 'map_projects' not in st.session_state:
22
+ # إنشاء بيانات تجريبية للمشاريع
23
+ st.session_state.map_projects = self._generate_sample_projects()
24
+
25
+ def run(self):
26
+ """
27
+ تشغيل وحدة الخرائط
28
+ """
29
+ st.markdown("<h2 class='module-title'>وحدة الخرائط المتكاملة</h2>", unsafe_allow_html=True)
30
+
31
+ # إنشاء تبويبات للخرائط المختلفة
32
+ tabs = st.tabs(["خريطة المشاريع", "خريطة المناقصات", "تحليل المناطق"])
33
+
34
+ with tabs[0]:
35
+ self._render_projects_map()
36
+
37
+ with tabs[1]:
38
+ self._render_tenders_map()
39
+
40
+ with tabs[2]:
41
+ self._render_region_analysis()
42
+
43
+ def _render_projects_map(self):
44
+ """
45
+ عرض خريطة المشاريع
46
+ """
47
+ st.markdown("### خريطة المشاريع النشطة")
48
+ st.markdown("عرض جميع المشاريع النشطة على الخريطة مع معلومات تفصيلية")
49
+
50
+ # إنشاء خريطة مركزها المملكة العربية السعودية
51
+ m = folium.Map(location=[24.7136, 46.6753], zoom_start=5)
52
+
53
+ # إضافة المشاريع إلى الخريطة
54
+ for project in st.session_state.map_projects:
55
+ popup_html = f"""
56
+ <div style="direction: rtl; text-align: right; width: 200px;">
57
+ <h4>{project['name']}</h4>
58
+ <p><strong>العميل:</strong> {project['client']}</p>
59
+ <p><strong>القيمة:</strong> {project['value']} ريال</p>
60
+ <p><strong>الحالة:</strong> {project['status']}</p>
61
+ </div>
62
+ """
63
+
64
+ # تحديد لون المؤشر بناءً على حالة المشروع
65
+ icon_color = 'green' if project['status'] == 'نشط' else 'orange' if project['status'] == 'قيد التنفيذ' else 'red'
66
+
67
+ folium.Marker(
68
+ location=[project['lat'], project['lng']],
69
+ popup=folium.Popup(popup_html, max_width=300),
70
+ tooltip=project['name'],
71
+ icon=folium.Icon(color=icon_color, icon='info-sign')
72
+ ).add_to(m)
73
+
74
+ # عرض الخريطة
75
+ folium_static(m, width=800, height=500)
76
+
77
+ # عرض جدول المشاريع
78
+ st.markdown("### قائمة المشاريع")
79
+
80
+ # تحويل البيانات إلى DataFrame
81
+ df = pd.DataFrame(st.session_state.map_projects)
82
+ df = df[['name', 'client', 'city', 'value', 'status']]
83
+ df.columns = ['اسم المشروع', 'العميل', 'المدينة', 'القيمة (ريال)', 'الحالة']
84
+
85
+ # عرض الجدول
86
+ st.dataframe(df, use_container_width=True)
87
+
88
+ def _render_tenders_map(self):
89
+ """
90
+ عرض خريطة المناقصات
91
+ """
92
+ st.markdown("### خريطة المناقصات المتاحة")
93
+ st.markdown("عرض المناقصات المتاحة حالياً على الخريطة مع تحليل التوزيع الجغرافي")
94
+
95
+ # إنشاء خريطة مركزها المملكة العربية السعودية
96
+ m = folium.Map(location=[24.7136, 46.6753], zoom_start=5)
97
+
98
+ # إضافة طبقة الكثافة الحرارية للمناقصات
99
+ heat_data = [[p['lat'], p['lng']] for p in st.session_state.map_projects]
100
+
101
+ # إضافة المناقصات إلى الخريطة
102
+ for project in st.session_state.map_projects:
103
+ if project['status'] == 'مناقصة':
104
+ popup_html = f"""
105
+ <div style="direction: rtl; text-align: right; width: 200px;">
106
+ <h4>{project['name']}</h4>
107
+ <p><strong>العميل:</strong> {project['client']}</p>
108
+ <p><strong>القيمة التقديرية:</strong> {project['value']} ريال</p>
109
+ <p><strong>تاريخ الإغلاق:</strong> {project.get('closing_date', '2025-05-15')}</p>
110
+ </div>
111
+ """
112
+
113
+ folium.Marker(
114
+ location=[project['lat'], project['lng']],
115
+ popup=folium.Popup(popup_html, max_width=300),
116
+ tooltip=project['name'],
117
+ icon=folium.Icon(color='blue', icon='info-sign')
118
+ ).add_to(m)
119
+
120
+ # عرض الخريطة
121
+ folium_static(m, width=800, height=500)
122
+
123
+ # عرض إحصائيات المناقصات حسب المنطقة
124
+ st.markdown("### توزيع المناقصات حسب المنطقة")
125
+
126
+ # تحليل بسيط للمناقصات حسب المدينة
127
+ tenders = [p for p in st.session_state.map_projects if p['status'] == 'مناقصة']
128
+ city_counts = {}
129
+ for tender in tenders:
130
+ city = tender['city']
131
+ if city in city_counts:
132
+ city_counts[city] += 1
133
+ else:
134
+ city_counts[city] = 1
135
+
136
+ # تحويل البيانات إلى DataFrame
137
+ df = pd.DataFrame(list(city_counts.items()), columns=['المدينة', 'عدد المناقصات'])
138
+
139
+ # عرض الرسم البياني
140
+ st.bar_chart(df.set_index('المدينة'))
141
+
142
+ def _render_region_analysis(self):
143
+ """
144
+ عرض تحليل المناطق
145
+ """
146
+ st.markdown("### تحليل المناطق الجغرافية")
147
+ st.markdown("تحليل توزيع المشاريع والمناقصات حسب المناطق مع مؤشرات الأداء")
148
+
149
+ # إنشاء بيانات تحليلية للمناطق
150
+ regions = {
151
+ 'الرياض': {'projects': 12, 'tenders': 8, 'success_rate': 75},
152
+ 'مكة المكرمة': {'projects': 8, 'tenders': 5, 'success_rate': 60},
153
+ 'المدينة المنورة': {'projects': 5, 'tenders': 3, 'success_rate': 65},
154
+ 'القصيم': {'projects': 4, 'tenders': 2, 'success_rate': 50},
155
+ 'المنطقة الشرقية': {'projects': 10, 'tenders': 7, 'success_rate': 70},
156
+ 'عسير': {'projects': 6, 'tenders': 4, 'success_rate': 55},
157
+ 'تبوك': {'projects': 3, 'tenders': 2, 'success_rate': 80},
158
+ 'حائل': {'projects': 2, 'tenders': 1, 'success_rate': 45},
159
+ 'الحدود الشمالية': {'projects': 1, 'tenders': 1, 'success_rate': 40},
160
+ 'جازان': {'projects': 3, 'tenders': 2, 'success_rate': 60},
161
+ 'نجران': {'projects': 2, 'tenders': 1, 'success_rate': 50},
162
+ 'الباحة': {'projects': 1, 'tenders': 1, 'success_rate': 55},
163
+ 'الجوف': {'projects': 2, 'tenders': 1, 'success_rate': 60}
164
+ }
165
+
166
+ # تحويل البيانات إلى DataFrame
167
+ df = pd.DataFrame({
168
+ 'المنطقة': list(regions.keys()),
169
+ 'المشاريع النشطة': [r['projects'] for r in regions.values()],
170
+ 'المناقصات الحالية': [r['tenders'] for r in regions.values()],
171
+ 'نسبة النجاح (%)': [r['success_rate'] for r in regions.values()]
172
+ })
173
+
174
+ # عرض الجدول
175
+ st.dataframe(df, use_container_width=True)
176
+
177
+ # عرض الرسم البياني للمشاريع والمناقصات
178
+ st.markdown("### توزيع المشاريع والمناقصات حسب المنطقة")
179
+
180
+ chart_data = pd.DataFrame({
181
+ 'المنطقة': list(regions.keys()),
182
+ 'المشاريع النشطة': [r['projects'] for r in regions.values()],
183
+ 'المناقصات الحالية': [r['tenders'] for r in regions.values()]
184
+ })
185
+
186
+ st.bar_chart(chart_data.set_index('المنطقة'))
187
+
188
+ # عرض خريطة المملكة مع تلوين المناطق حسب نسبة النجاح
189
+ st.markdown("### خريطة نسب النجاح حسب المناطق")
190
+ st.markdown("*قريباً: سيتم إضافة خريطة تفاعلية للمملكة مع تلوين المناطق حسب نسب النجاح*")
191
+
192
+ def _generate_sample_projects(self):
193
+ """
194
+ إنشاء بيانات تجريبية للمشاريع
195
+ """
196
+ # قائمة المدن السعودية مع إحداثياتها
197
+ cities = {
198
+ 'الرياض': {'lat': 24.7136, 'lng': 46.6753},
199
+ 'جدة': {'lat': 21.4858, 'lng': 39.1925},
200
+ 'مكة المكرمة': {'lat': 21.3891, 'lng': 39.8579},
201
+ 'المدينة المنورة': {'lat': 24.5247, 'lng': 39.5692},
202
+ 'الدمام': {'lat': 26.4207, 'lng': 50.0888},
203
+ 'الخبر': {'lat': 26.2172, 'lng': 50.1971},
204
+ 'تبوك': {'lat': 28.3998, 'lng': 36.5715},
205
+ 'أبها': {'lat': 18.2164, 'lng': 42.5053},
206
+ 'بريدة': {'lat': 26.3292, 'lng': 43.9708},
207
+ 'جازان': {'lat': 16.8892, 'lng': 42.5611}
208
+ }
209
+
210
+ # قائمة العملاء
211
+ clients = [
212
+ 'وزارة النقل',
213
+ 'وزارة الإسكان',
214
+ 'وزارة التعليم',
215
+ 'وزارة الصحة',
216
+ 'أمانة منطقة الرياض',
217
+ 'أمانة محافظة جدة',
218
+ 'الهيئة الملكية لمدينة الرياض',
219
+ 'شركة أرامكو السعودية',
220
+ 'شركة سابك',
221
+ 'الهيئة السعودية للمدن الصناعية'
222
+ ]
223
+
224
+ # قائمة حالات المشاريع
225
+ statuses = ['نشط', 'قيد التنفيذ', 'مناقصة', 'مكتمل']
226
+
227
+ # إنشاء قائمة المشاريع
228
+ projects = []
229
+ for i in range(30):
230
+ city_name = random.choice(list(cities.keys()))
231
+ city_data = cities[city_name]
232
+
233
+ # إضافة تغيير طفيف للإحداثيات لتجنب تراكب المؤشرات
234
+ lat_offset = random.uniform(-0.1, 0.1)
235
+ lng_offset = random.uniform(-0.1, 0.1)
236
+
237
+ project = {
238
+ 'id': i + 1,
239
+ 'name': f'مشروع {i + 1}',
240
+ 'client': random.choice(clients),
241
+ 'city': city_name,
242
+ 'lat': city_data['lat'] + lat_offset,
243
+ 'lng': city_data['lng'] + lng_offset,
244
+ 'value': random.randint(1000000, 100000000),
245
+ 'status': random.choice(statuses)
246
+ }
247
+ projects.append(project)
248
+
249
+ return projects
modules/notifications/notifications_app.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ وحدة الإشعارات المتكاملة
3
+ """
4
+
5
+ import streamlit as st
6
+ import pandas as pd
7
+ from datetime import datetime, timedelta
8
+ import random
9
+
10
+ class NotificationsApp:
11
+ """
12
+ وحدة الإشعارات المتكاملة للنظام
13
+ """
14
+
15
+ def __init__(self):
16
+ """
17
+ تهيئة وحدة الإشعارات
18
+ """
19
+ # تهيئة حالة الجلسة الخاصة بالإشعارات إذا لم تكن موجودة
20
+ if 'notifications' not in st.session_state:
21
+ # إنشاء بيانات تجريبية للإشعارات
22
+ st.session_state.notifications = self._generate_sample_notifications()
23
+
24
+ if 'notification_settings' not in st.session_state:
25
+ # إعدادات الإشعارات الافتراضية
26
+ st.session_state.notification_settings = {
27
+ 'email_enabled': True,
28
+ 'sms_enabled': True,
29
+ 'app_enabled': True,
30
+ 'frequency': 'daily',
31
+ 'priority_only': False
32
+ }
33
+
34
+ def run(self):
35
+ """
36
+ تشغيل وحدة الإشعارات
37
+ """
38
+ st.markdown("<h2 class='module-title'>وحدة الإشعارات المتكاملة</h2>", unsafe_allow_html=True)
39
+
40
+ # إنشاء تبويبات للإشعارات المختلفة
41
+ tabs = st.tabs(["الإشعارات الحالية", "إعدادات الإشعارات", "سجل الإشعارات"])
42
+
43
+ with tabs[0]:
44
+ self._render_current_notifications()
45
+
46
+ with tabs[1]:
47
+ self._render_notification_settings()
48
+
49
+ with tabs[2]:
50
+ self._render_notification_history()
51
+
52
+ def _render_current_notifications(self):
53
+ """
54
+ عرض الإشعارات الحالية
55
+ """
56
+ st.markdown("### الإشعارات الحالية")
57
+
58
+ # فلترة الإشعارات غير المقروءة
59
+ unread_notifications = [n for n in st.session_state.notifications if not n['read']]
60
+
61
+ if not unread_notifications:
62
+ st.info("لا توجد إشعارات جديدة", icon="ℹ️")
63
+ else:
64
+ st.success(f"لديك {len(unread_notifications)} إشعارات جديدة", icon="🔔")
65
+
66
+ # عرض الإشعارات غير المقروءة
67
+ for i, notification in enumerate(unread_notifications):
68
+ with st.container():
69
+ col1, col2 = st.columns([5, 1])
70
+
71
+ with col1:
72
+ # تحديد نوع الإشعار ولونه
73
+ if notification['type'] == 'تنبيه':
74
+ st.warning(f"**{notification['title']}**", icon="⚠️")
75
+ elif notification['type'] == 'معلومات':
76
+ st.info(f"**{notification['title']}**", icon="ℹ️")
77
+ elif notification['type'] == 'نجاح':
78
+ st.success(f"**{notification['title']}**", icon="✅")
79
+ else:
80
+ st.error(f"**{notification['title']}**", icon="❌")
81
+
82
+ st.markdown(f"{notification['message']}")
83
+ st.markdown(f"<small>{notification['date']}</small>", unsafe_allow_html=True)
84
+
85
+ with col2:
86
+ # زر لتحديد الإشعار كمقروء
87
+ if st.button("تحديد كمقروء", key=f"mark_read_{i}"):
88
+ notification_id = notification['id']
89
+ for n in st.session_state.notifications:
90
+ if n['id'] == notification_id:
91
+ n['read'] = True
92
+ st.rerun()
93
+
94
+ st.markdown("---")
95
+
96
+ # زر لتحديد جميع الإشعارات كمقروءة
97
+ if st.button("تحديد الكل كمقروء", key="mark_all_read"):
98
+ for n in st.session_state.notifications:
99
+ n['read'] = True
100
+ st.rerun()
101
+
102
+ def _render_notification_settings(self):
103
+ """
104
+ عرض إعدادات الإشعارات
105
+ """
106
+ st.markdown("### إعدادات الإشعارات")
107
+ st.markdown("تخصيص طريقة استلام الإشعارات وتكرارها")
108
+
109
+ settings = st.session_state.notification_settings
110
+
111
+ # قسم طرق الإشعارات
112
+ st.markdown("#### طرق الإشعارات")
113
+ col1, col2, col3 = st.columns(3)
114
+
115
+ with col1:
116
+ email_enabled = st.checkbox("إشعارات البريد الإلكتروني", value=settings['email_enabled'], key="email_enabled")
117
+
118
+ with col2:
119
+ sms_enabled = st.checkbox("إشعارات الرسائل النصية", value=settings['sms_enabled'], key="sms_enabled")
120
+
121
+ with col3:
122
+ app_enabled = st.checkbox("إشعارات التطبيق", value=settings['app_enabled'], key="app_enabled")
123
+
124
+ # قسم تكرار الإشعارات
125
+ st.markdown("#### تكرار الإشعارات")
126
+ frequency = st.radio(
127
+ "تكرار الإشعارات",
128
+ options=["فوري", "يومي", "أسبوعي"],
129
+ index=["فوري", "يومي", "أسبوعي"].index(settings['frequency']),
130
+ horizontal=True,
131
+ key="frequency"
132
+ )
133
+
134
+ # قسم أولوية الإشعارات
135
+ st.markdown("#### أولوية الإشعارات")
136
+ priority_only = st.checkbox(
137
+ "إظهار الإشعارات ذات الأولوية العالية فقط",
138
+ value=settings['priority_only'],
139
+ key="priority_only"
140
+ )
141
+
142
+ # قسم أنواع الإشعارات
143
+ st.markdown("#### أنواع الإشعارات")
144
+ notification_types = {
145
+ "مناقصات جديدة": True,
146
+ "تحديثات المشاريع": True,
147
+ "مواعيد نهائية": True,
148
+ "تنبيهات النظام": True,
149
+ "تقارير دورية": False
150
+ }
151
+
152
+ for ntype, default_value in notification_types.items():
153
+ st.checkbox(ntype, value=default_value, key=f"ntype_{ntype}")
154
+
155
+ # زر حفظ الإعدادات
156
+ if st.button("حفظ الإعدادات", key="save_settings"):
157
+ st.session_state.notification_settings = {
158
+ 'email_enabled': email_enabled,
159
+ 'sms_enabled': sms_enabled,
160
+ 'app_enabled': app_enabled,
161
+ 'frequency': frequency,
162
+ 'priority_only': priority_only
163
+ }
164
+ st.success("تم حفظ الإعدادات بنجاح", icon="✅")
165
+
166
+ def _render_notification_history(self):
167
+ """
168
+ عرض سجل الإشعارات
169
+ """
170
+ st.markdown("### سجل الإشعارات")
171
+ st.markdown("عرض جميع الإشعارات السابقة مع إمكانية البحث والتصفية")
172
+
173
+ # خيارات التصفية
174
+ col1, col2 = st.columns(2)
175
+
176
+ with col1:
177
+ filter_type = st.multiselect(
178
+ "نوع الإشعار",
179
+ options=["الكل", "تنبيه", "معلومات", "نجاح", "خطأ"],
180
+ default=["الكل"]
181
+ )
182
+
183
+ with col2:
184
+ filter_read = st.multiselect(
185
+ "حالة القراءة",
186
+ options=["الكل", "مقروء", "غير مقروء"],
187
+ default=["الكل"]
188
+ )
189
+
190
+ # تطبيق التصفية
191
+ filtered_notifications = st.session_state.notifications
192
+
193
+ if "الكل" not in filter_type:
194
+ filtered_notifications = [n for n in filtered_notifications if n['type'] in filter_type]
195
+
196
+ if "الكل" not in filter_read:
197
+ if "مقروء" in filter_read and "غير مقروء" not in filter_read:
198
+ filtered_notifications = [n for n in filtered_notifications if n['read']]
199
+ elif "غير مقروء" in filter_read and "مقروء" not in filter_read:
200
+ filtered_notifications = [n for n in filtered_notifications if not n['read']]
201
+
202
+ # تحويل البيانات إلى DataFrame
203
+ if filtered_notifications:
204
+ df = pd.DataFrame(filtered_notifications)
205
+ df = df[['date', 'type', 'title', 'read']]
206
+ df.columns = ['التاريخ', 'النوع', 'العنوان', 'مقروء']
207
+ df['مقروء'] = df['مقروء'].map({True: 'نعم', False: 'لا'})
208
+
209
+ # عرض الجدول
210
+ st.dataframe(df, use_container_width=True)
211
+ else:
212
+ st.info("لا توجد إشعارات تطابق معايير التصفية", icon="ℹ️")
213
+
214
+ def _generate_sample_notifications(self):
215
+ """
216
+ إنشاء بيانات تجريبية للإشعارات
217
+ """
218
+ notification_types = ['تنبيه', 'معلومات', 'نجاح', 'خطأ']
219
+
220
+ notifications = []
221
+ now = datetime.now()
222
+
223
+ # إنشاء 20 إشعار تجريبي
224
+ for i in range(20):
225
+ notification_type = random.choice(notification_types)
226
+
227
+ # تحديد العنوان والرسالة بناءً على النوع
228
+ if notification_type == 'تنبيه':
229
+ title = random.choice([
230
+ "موعد نهائي قريب",
231
+ "تحديث هام في المناقصة",
232
+ "تغيير في متطلبات المشروع"
233
+ ])
234
+ message = random.choice([
235
+ "يجب تقديم عرض المناقصة خلال 3 أيام",
236
+ "تم تحديث وثائق المناقصة، يرجى مراجعتها",
237
+ "تم تغيير بعض متطلبات المشروع، يرجى الاطلاع على التفاصيل"
238
+ ])
239
+ elif notification_type == 'معلومات':
240
+ title = random.choice([
241
+ "مناقصة جديدة متاحة",
242
+ "تحديث في النظام",
243
+ "اجتماع قادم"
244
+ ])
245
+ message = random.choice([
246
+ "تم إضافة مناقصة جديدة في قطاع البنية التحتية",
247
+ "تم تحديث النظام إلى الإصدار 2.0.1",
248
+ "اجتماع مراجعة المشروع يوم الخميس القادم الساعة 10 صباحاً"
249
+ ])
250
+ elif notification_type == 'نجاح':
251
+ title = random.choice([
252
+ "تم ترسية المناقصة",
253
+ "تم إكمال المشروع بنجاح",
254
+ "تم قبول العرض الفني"
255
+ ])
256
+ message = random.choice([
257
+ "تمت ترسية المناقصة رقم 2025/123 على شركتكم",
258
+ "تم إكمال مشروع تطوير البنية التحتية بنجاح",
259
+ "تم قبول العرض الفني للمناقصة رقم 2025/456"
260
+ ])
261
+ else: # خطأ
262
+ title = random.choice([
263
+ "خطأ في تقديم العرض",
264
+ "مشكلة في النظام",
265
+ "تأخير في المشروع"
266
+ ])
267
+ message = random.choice([
268
+ "حدث خطأ أثناء تقديم العرض، يرجى المحاولة مرة أخرى",
269
+ "يواجه النظام مشكلة في وحدة التسعير، جاري العمل على إصلاحها",
270
+ "هناك تأخير في تنفيذ المشروع بسبب ظروف خارجية"
271
+ ])
272
+
273
+ # تحديد تاريخ عشوائي خلال الأسبوعين الماضيين
274
+ days_ago = random.randint(0, 14)
275
+ notification_date = (now - timedelta(days=days_ago)).strftime("%Y-%m-%d %H:%M")
276
+
277
+ # تحديد حالة القراءة (الإشعارات الأقدم أكثر احتمالاً أن تكون مقروءة)
278
+ read_probability = days_ago / 14.0
279
+ is_read = random.random() < read_probability
280
+
281
+ notification = {
282
+ 'id': i + 1,
283
+ 'type': notification_type,
284
+ 'title': title,
285
+ 'message': message,
286
+ 'date': notification_date,
287
+ 'read': is_read
288
+ }
289
+
290
+ notifications.append(notification)
291
+
292
+ # ترتيب الإشعارات حسب التاريخ (الأحدث أولاً)
293
+ notifications.sort(key=lambda x: x['date'], reverse=True)
294
+
295
+ return notifications
modules/pricing/constants.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ثوابت وحدة التسعير
3
+ """
4
+
5
+ # أوزان المحتوى المحلي
6
+ LOCAL_CONTENT_WEIGHTS = {
7
+ 'منتجات_البناء': 1.5, # المنتجات الأساسية في البناء لها وزن أكبر
8
+ 'المنتجات_الإنشائية': 1.5, # المنتجات الإنشائية لها وزن أكبر
9
+ 'منتجات_التشطيب': 1.0, # منتجات التشطيب لها وزن عادي
10
+ 'الخدمات_الهندسية': 1.3, # الخدمات الهندسية لها وزن أكبر
11
+ 'الخدمات_الإدارية': 1.0, # الخدمات الإدارية لها وزن عادي
12
+ 'القوى_العاملة_الفنية': 1.2, # القوى العاملة الفنية لها وزن أكبر
13
+ 'القوى_العاملة_العادية': 1.0, # القوى العاملة العادية لها وزن عادي
14
+ 'القوى_العاملة_الإدارية': 0.8 # القوى العاملة الإدارية لها وزن أقل
15
+ }
16
+
17
+ # فئات التكاليف
18
+ COST_CATEGORIES = {
19
+ 'مباشرة': [
20
+ 'مواد',
21
+ 'عمالة',
22
+ 'معدات',
23
+ 'مقاولين من الباطن'
24
+ ],
25
+ 'غير_مباشرة': [
26
+ 'إدارة المشروع',
27
+ 'ضمانات بنكية',
28
+ 'تأمينات',
29
+ 'مكاتب الموقع',
30
+ 'نقل وسكن',
31
+ 'مرافق',
32
+ 'أمن وسلامة'
33
+ ],
34
+ 'مصاريف_عامة': [
35
+ 'مصاريف إدارية',
36
+ 'رواتب إدارية',
37
+ 'إيجارات',
38
+ 'اتصالات',
39
+ 'قرطاسية',
40
+ 'تسويق وعلاقات عامة'
41
+ ],
42
+ 'احتياطيات': [
43
+ 'احتياطي مخاطر',
44
+ 'احتياطي تضخم',
45
+ 'احتياطي تغييرات'
46
+ ]
47
+ }
48
+
49
+ # أنواع التسعير
50
+ PRICING_TYPES = {
51
+ 'قياسي': 'التسعير المتوازن لجميع البنود',
52
+ 'غير_متزن': 'تحميل بعض البنود بسعر أعلى وتخفيض بنود أخرى مع الحفاظ على نفس الإجمالي',
53
+ 'تنافسي': 'التسعير بناءً على أسعار المنافسين',
54
+ 'ربحية': 'التسعير بناءً على هامش الربح المستهدف'
55
+ }
56
+
57
+ # أنواع استراتيجيات التسعير غير المتزن
58
+ UNBALANCED_PRICING_STRATEGIES = {
59
+ 'تحميل_أمامي': 'زيادة أسعار البنود المبكرة في المشروع',
60
+ 'تحميل_خلفي': 'زيادة أسعار البنود المتأخرة في المشروع',
61
+ 'تحميل_مؤكد': 'زيادة أسعار البنود المؤكدة التنفيذ',
62
+ 'تخفيض_متغير': 'تخفيض أسعار البنود المحتمل تغير كمياتها'
63
+ }
64
+
65
+ # معلمات افتراضية للمشروع
66
+ DEFAULT_PROJECT_PARAMS = {
67
+ 'نسبة_المصاريف_العامة': 8.0, # 8% من التكاليف المباشرة
68
+ 'نسبة_الأرباح': 10.0, # 10% من التكاليف الكلية
69
+ 'نسبة_احتياطي_المخاطر': 5.0, # 5% من التكاليف المباشرة
70
+ 'نسبة_ضمان_ابتدائي': 2.0, # 2% من قيمة العطاء
71
+ 'نسبة_ضمان_نهائي': 5.0, # 5% من قيمة العطاء
72
+ 'نسبة_محتجزات': 10.0, # 10% من قيمة المستخلصات
73
+ 'نسبة_دفعة_مقدمة': 10.0 # 10% من قيمة العطاء
74
+ }
75
+
76
+ # وحدات القياس
77
+ UNITS_OF_MEASURE = {
78
+ 'طولية': ['م.ط', 'متر طولي', 'م'],
79
+ 'مسطحة': ['م2', 'متر مربع'],
80
+ 'حجمية': ['م3', 'متر مكعب'],
81
+ 'وزن': ['كجم', 'طن', 'جم'],
82
+ 'عدد': ['عدد', 'وحدة', 'قطعة'],
83
+ 'زمن': ['يوم', 'ساعة', 'شهر'],
84
+ 'نقطة': ['نقطة', 'مخرج']
85
+ }
86
+
87
+ # نسب الزيادة في التكاليف
88
+ COST_INCREASE_FACTORS = {
89
+ 'تعقيد_مرتفع': 1.25, # زيادة 25% للأعمال المعقدة
90
+ 'تعقيد_متوسط': 1.15, # زيادة 15% للأعمال متوسطة التعقيد
91
+ 'منطقة_نائية': 1.2, # زيادة 20% للمناطق النائية
92
+ 'ظروف_جوية_قاسية': 1.15, # زيادة 15% للظروف الجوية القاسية
93
+ 'ظروف_الموقع_صعبة': 1.2, # زيادة 20% لظروف الموقع الصعبة
94
+ 'عاجل': 1.3 # زيادة 30% للأعمال العاجلة
95
+ }
96
+
97
+ # أنواع المشاريع
98
+ PROJECT_TYPES = [
99
+ 'سكني',
100
+ 'تجاري',
101
+ 'صناعي',
102
+ 'تعليمي',
103
+ 'صحي',
104
+ 'بنية تحتية',
105
+ 'طرق',
106
+ 'نقل',
107
+ 'طاقة',
108
+ 'مياه وصرف صحي',
109
+ 'اتصالات',
110
+ 'عسكري',
111
+ 'ترفيهي',
112
+ 'متعدد الاستخدام'
113
+ ]
modules/pricing/exceptions.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ استثناءات وحدة التسعير
3
+ """
4
+
5
+ class PricingError(Exception):
6
+ """استثناء أساسي لأخطاء التسعير"""
7
+ pass
8
+
9
+
10
+ class LocalContentCalculationError(PricingError):
11
+ """استثناء لأخطاء حساب المحتوى المحلي"""
12
+ pass
13
+
14
+
15
+ class PriceEstimationError(PricingError):
16
+ """استثناء لأخطاء تقدير الأسعار"""
17
+ pass
18
+
19
+
20
+ class ResourceNotFoundError(PricingError):
21
+ """استثناء لعدم وجود المورد المطلوب"""
22
+ pass
23
+
24
+
25
+ class InvalidInputError(PricingError):
26
+ """استثناء للمدخلات غير الصالحة"""
27
+ pass
28
+
29
+
30
+ class ModelLoadingError(PricingError):
31
+ """استثناء لأخطاء تحميل النموذج"""
32
+ pass
33
+
34
+
35
+ class DataProcessingError(PricingError):
36
+ """استثناء لأخطاء معالجة البيانات"""
37
+ pass
38
+
39
+
40
+ class UnbalancedPricingError(PricingError):
41
+ """استثناء لأخطاء التسعير غير المتزن"""
42
+ pass
modules/pricing/price_analysis_component.py ADDED
@@ -0,0 +1,932 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ from datetime import datetime
5
+ import time
6
+
7
+ class PriceAnalysisComponent:
8
+ """مكون تحليل الأسعار للبنود"""
9
+
10
+ def __init__(self):
11
+ """تهيئة مكون تحليل الأسعار"""
12
+ # تهيئة قائمة الوحدات المتاحة
13
+ self.unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
14
+
15
+ # تهيئة فئات التكاليف
16
+ self.cost_categories = [
17
+ "مواد",
18
+ "عمالة",
19
+ "معدات",
20
+ "مقاولي الباطن",
21
+ "مصاريف عامة",
22
+ "أرباح"
23
+ ]
24
+
25
+ # تهيئة قائمة البنود وتحليل أسعارها
26
+ if 'items_price_analysis' not in st.session_state:
27
+ st.session_state.items_price_analysis = {}
28
+
29
+ def render(self):
30
+ """عرض واجهة تحليل الأسعار"""
31
+ st.markdown("<h2 class='module-title'>تحليل أسعار البنود</h2>", unsafe_allow_html=True)
32
+
33
+ # التحقق من وجود بنود في التسعير الحالي
34
+ if 'current_pricing' not in st.session_state or 'items' not in st.session_state.current_pricing:
35
+ st.warning("ليس هناك بنود للتحليل. يرجى إنشاء تسعير أولاً.")
36
+ return
37
+
38
+ # الحصول على البنود من التسعير الحالي
39
+ items = st.session_state.current_pricing['items'].copy()
40
+
41
+ # عرض قائمة البنود
42
+ st.markdown("### قائمة البنود")
43
+ st.dataframe(items[['رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي']],
44
+ use_container_width=True, hide_index=True)
45
+
46
+ # اختيار البند لتحليل السعر
47
+ selected_item_id = st.selectbox(
48
+ "اختر البند لتحليل السعر",
49
+ options=items['رقم البند'].tolist(),
50
+ format_func=lambda x: f"{x}: {items[items['رقم البند'] == x]['وصف البند'].values[0][:50]}..."
51
+ )
52
+
53
+ if selected_item_id:
54
+ # الحصول على البند المحدد
55
+ selected_item = items[items['رقم البند'] == selected_item_id].iloc[0]
56
+
57
+ # عرض تفاصيل البند المختار
58
+ col1, col2, col3 = st.columns(3)
59
+
60
+ with col1:
61
+ st.metric("رقم البند", selected_item['رقم البند'])
62
+
63
+ with col2:
64
+ st.metric("الكمية", f"{selected_item['الكمية']} {selected_item['الوحدة']}")
65
+
66
+ with col3:
67
+ st.metric("سعر الوحدة", f"{selected_item['سعر الوحدة']:,.2f} ريال")
68
+
69
+ st.markdown(f"**وصف البند**: {selected_item['وصف البند']}")
70
+
71
+ # إنشاء أو تحديث تحليل السعر للبند المحدد
72
+ if selected_item_id not in st.session_state.items_price_analysis:
73
+ # إنشاء تحليل سعر افتراضي
74
+ self._create_default_price_analysis(selected_item_id, selected_item)
75
+
76
+ # عرض وتحرير تحليل السعر
77
+ self._render_price_analysis_editor(selected_item_id, selected_item)
78
+
79
+ def _create_default_price_analysis(self, item_id, item):
80
+ """إنشاء تحليل سعر افتراضي للبند"""
81
+ # إنشاء قائمة مكونات تحليل السعر
82
+ components = pd.DataFrame(columns=[
83
+ 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
84
+ ])
85
+
86
+ # إضافة مكونات افتراضية بناءً على نوع البند
87
+ is_concrete = 'خرسان' in item['وصف البند']
88
+ is_steel = 'حديد' in item['وصف البند'] or 'تسليح' in item['وصف البند']
89
+ is_bricks = 'بلوك' in item['وصف البند'] or 'طوب' in item['وصف البند']
90
+ is_paint = 'دهان' in item['وصف البند'] or 'طلاء' in item['وصف البند']
91
+ is_insulation = 'عزل' in item['وصف البند']
92
+
93
+ # إضافة المكونات بناءً على نوع البند
94
+ if is_concrete:
95
+ # مكونات الخرسانة
96
+ default_components = pd.DataFrame({
97
+ 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
98
+ 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
99
+ 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
100
+ 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
101
+ 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
102
+ 'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
103
+ })
104
+ components = pd.concat([components, default_components], ignore_index=True)
105
+
106
+ elif is_steel:
107
+ # مكونات الحديد
108
+ default_components = pd.DataFrame({
109
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
110
+ 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
111
+ 'الكمية': [1000, 10, 1, 1, 1],
112
+ 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
113
+ 'سعر الوحدة': [4.5, 50, 300, 200, 300],
114
+ 'الإجمالي': [4500, 500, 300, 200, 300]
115
+ })
116
+ components = pd.concat([components, default_components], ignore_index=True)
117
+
118
+ elif is_bricks:
119
+ # مكونات البلوك
120
+ default_components = pd.DataFrame({
121
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
122
+ 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
123
+ 'الكمية': [12.5, 0.02, 1, 1, 1],
124
+ 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
125
+ 'سعر الوحدة': [8, 500, 80, 15, 20],
126
+ 'الإجمالي': [100, 10, 80, 15, 20]
127
+ })
128
+ components = pd.concat([components, default_components], ignore_index=True)
129
+
130
+ elif is_paint:
131
+ # مكونات الدهانات
132
+ default_components = pd.DataFrame({
133
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
134
+ 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
135
+ 'الكمية': [0.4, 0.1, 1, 1, 1],
136
+ 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
137
+ 'سعر الوحدة': [80, 20, 35, 5, 10],
138
+ 'الإجمالي': [32, 2, 35, 5, 10]
139
+ })
140
+ components = pd.concat([components, default_components], ignore_index=True)
141
+
142
+ elif is_insulation:
143
+ # مكونات العزل
144
+ default_components = pd.DataFrame({
145
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
146
+ 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
147
+ 'الكمية': [1.1, 0.2, 1, 1, 1],
148
+ 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
149
+ 'سعر الوحدة': [60, 30, 25, 10, 15],
150
+ 'الإجمالي': [66, 6, 25, 10, 15]
151
+ })
152
+ components = pd.concat([components, default_components], ignore_index=True)
153
+
154
+ else:
155
+ # مكونات عامة افتراضية
156
+ default_components = pd.DataFrame({
157
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
158
+ 'الوصف': ['مواد أساسية', 'عمالة', 'معدات ومعد مساعدة', 'مصاريف عامة', 'أرباح'],
159
+ 'الكمية': [1, 1, 1, 1, 1],
160
+ 'الوحدة': [item['الوحدة'], 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
161
+ 'سعر الوحدة': [
162
+ item['سعر الوحدة'] * 0.6,
163
+ item['سعر الوحدة'] * 0.2,
164
+ item['سعر الوحدة'] * 0.1,
165
+ item['سعر الوحدة'] * 0.05,
166
+ item['سعر الوحدة'] * 0.05
167
+ ],
168
+ 'الإجمالي': [
169
+ item['سعر الوحدة'] * 0.6,
170
+ item['سعر الوحدة'] * 0.2,
171
+ item['سعر الوحدة'] * 0.1,
172
+ item['سعر الوحدة'] * 0.05,
173
+ item['سعر الوحدة'] * 0.05
174
+ ]
175
+ })
176
+ components = pd.concat([components, default_components], ignore_index=True)
177
+
178
+ # حفظ تحليل السعر للبند
179
+ st.session_state.items_price_analysis[item_id] = components
180
+
181
+ def _render_price_analysis_editor(self, item_id, item):
182
+ """عرض محرر تحليل السعر للبند"""
183
+ st.markdown("### تحليل السعر")
184
+
185
+ # الحصول على مكونات تحليل السعر
186
+ components = st.session_state.items_price_analysis[item_id]
187
+
188
+ # عرض تحليل السعر في محرر بيانات
189
+ st.markdown("#### مكونات السعر")
190
+
191
+ edited_components = st.data_editor(
192
+ components,
193
+ use_container_width=True,
194
+ hide_index=True,
195
+ num_rows="dynamic",
196
+ column_config={
197
+ 'نوع التكلفة': st.column_config.SelectboxColumn(
198
+ 'نوع التكلفة',
199
+ help='فئة التكلفة',
200
+ options=self.cost_categories
201
+ ),
202
+ 'الوحدة': st.column_config.SelectboxColumn(
203
+ 'الوحدة',
204
+ help='وحدة القياس',
205
+ options=self.unit_options + ["وحدة", "ساعة", "يوم"]
206
+ ),
207
+ 'الكمية': st.column_config.NumberColumn(
208
+ 'الكمية',
209
+ help='الكمية',
210
+ min_value=0.0,
211
+ format="%.2f"
212
+ ),
213
+ 'سعر الوحدة': st.column_config.NumberColumn(
214
+ 'سعر الوحدة',
215
+ help='سعر الوحدة',
216
+ min_value=0.0,
217
+ format="%.2f"
218
+ ),
219
+ 'الإجمالي': st.column_config.NumberColumn(
220
+ 'الإجمالي',
221
+ help='الإجمالي',
222
+ min_value=0.0,
223
+ format="%.2f"
224
+ )
225
+ }
226
+ )
227
+
228
+ # إعادة حساب الإجمالي لكل مكون
229
+ edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
230
+
231
+ # حفظ التعديلات
232
+ st.session_state.items_price_analysis[item_id] = edited_components
233
+
234
+ # حساب إجمالي تحليل السعر
235
+ total_analysis_price = edited_components['الإجمالي'].sum()
236
+ unit_price_from_analysis = total_analysis_price / item['الكمية'] if item['الكمية'] > 0 else 0
237
+
238
+ # عرض ملخص تحليل السعر
239
+ st.markdown("#### ملخص تحليل السعر")
240
+
241
+ col1, col2, col3 = st.columns(3)
242
+
243
+ with col1:
244
+ st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
245
+
246
+ with col2:
247
+ st.metric("سعر الوحدة من التحليل", f"{unit_price_from_analysis:,.2f} ريال")
248
+
249
+ with col3:
250
+ # المقارنة مع السعر الأصلي
251
+ diff = unit_price_from_analysis - item['سعر الوحدة']
252
+ st.metric(
253
+ "الفرق عن السعر الأصلي",
254
+ f"{diff:,.2f} ريال",
255
+ delta=f"{(diff/item['سعر الوحدة']*100) if item['سعر الوحدة'] > 0 else 0:.1f}%"
256
+ )
257
+
258
+ # تحليل توزيع التكاليف حسب الفئة
259
+ cost_by_category = edited_components.groupby('نوع التكلفة')['الإجمالي'].sum().reset_index()
260
+
261
+ # عرض مخطط توزيع التكاليف
262
+ st.markdown("#### توزيع التكاليف حسب الفئة")
263
+
264
+ # عرض توزيع التكاليف في جدول
265
+ distribution_df = pd.DataFrame({
266
+ 'نوع التكلفة': cost_by_category['نوع التكلفة'],
267
+ 'القيمة': cost_by_category['الإجمالي'],
268
+ 'النسبة المئوية': (cost_by_category['الإجمالي'] / total_analysis_price * 100).round(2)
269
+ })
270
+
271
+ st.dataframe(
272
+ distribution_df,
273
+ use_container_width=True,
274
+ hide_index=True,
275
+ column_config={
276
+ 'القيمة': st.column_config.NumberColumn(
277
+ 'القيمة',
278
+ help='القيمة',
279
+ format="%.2f"
280
+ ),
281
+ 'النسبة المئوية': st.column_config.ProgressColumn(
282
+ 'النسبة المئوية',
283
+ help='النسبة المئوية',
284
+ format="%.2f%%",
285
+ min_value=0,
286
+ max_value=100
287
+ )
288
+ }
289
+ )
290
+
291
+ # أزرار الإجراءات
292
+ col1, col2, col3 = st.columns(3)
293
+
294
+ with col1:
295
+ if st.button("تحديث سعر البند", use_container_width=True):
296
+ # تحديث سعر البند بناءً على تحليل السعر
297
+ items = st.session_state.current_pricing['items'].copy()
298
+ item_index = items[items['رقم البند'] == item_id].index[0]
299
+
300
+ # تحديث سعر الوحدة والإجمالي
301
+ items.at[item_index, 'سعر الوحدة'] = unit_price_from_analysis
302
+ items.at[item_index, 'الإجمالي'] = unit_price_from_analysis * items.at[item_index, 'الكمية']
303
+
304
+ # حفظ التعديلات في التسعير الحالي
305
+ st.session_state.current_pricing['items'] = items
306
+
307
+ st.success(f"تم تحديث سعر البند بناءً على تحليل السعر: {unit_price_from_analysis:,.2f} ريال")
308
+ time.sleep(0.5)
309
+ st.rerun()
310
+
311
+ with col2:
312
+ if st.button("تصدير تحليل السعر", use_container_width=True):
313
+ st.success("تم إرسال تحليل السعر للتصدير بنجاح!")
314
+
315
+ with col3:
316
+ if st.button("مسح تحليل السعر", use_container_width=True):
317
+ # حذف تحليل السعر للبند
318
+ if item_id in st.session_state.items_price_analysis:
319
+ del st.session_state.items_price_analysis[item_id]
320
+
321
+ st.warning("تم مسح تحليل السعر للبند")
322
+ time.sleep(0.5)
323
+ st.rerun()
324
+
325
+ def add_to_pricing_app(self, pricing_app):
326
+ """إضافة مكون تحليل الأسعار إلى تطبيق التسعير"""
327
+ # إضافة تبويب جديد
328
+ if not hasattr(pricing_app, 'tabs'):
329
+ pricing_app.tabs = []
330
+
331
+ if len(pricing_app.tabs) == 4: # إذا كان هناك 4 تبويبات فقط
332
+ pricing_app.tabs.append("تحليل أسعار البنود")
333
+
334
+ # إضافة دالة العرض
335
+ pricing_app._render_price_analysis_tab = self.render
336
+
337
+
338
+ def render_integrated_item_input():
339
+ """عرض واجهة إدخال البنود مع تحليل السعر المتكامل"""
340
+
341
+ # ضبط CSS لتحسين ظهور الواجهة العربية
342
+ st.markdown("""
343
+ <style>
344
+ input, .stTextArea textarea {
345
+ direction: rtl;
346
+ text-align: right;
347
+ font-family: 'Arial', 'Tahoma', sans-serif !important;
348
+ }
349
+ .stTextInput > div > div > input {
350
+ text-align: right;
351
+ direction: rtl;
352
+ }
353
+ .pricing-analysis-container {
354
+ border: 1px solid #e0e0e0;
355
+ border-radius: 10px;
356
+ padding: 10px;
357
+ margin-top: 10px;
358
+ background-color: #f9f9f9;
359
+ }
360
+ </style>
361
+ """, unsafe_allow_html=True)
362
+
363
+ # تهيئة قائمة الوحدات المتاحة
364
+ unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
365
+
366
+ # تهيئة فئات التكاليف
367
+ cost_categories = [
368
+ "مواد",
369
+ "عمالة",
370
+ "معدات",
371
+ "مقاولي الباطن",
372
+ "مصاريف عامة",
373
+ "أرباح"
374
+ ]
375
+
376
+ # إنشاء جدول البنود اذا لم يكن موجوداً
377
+ if 'manual_items' not in st.session_state:
378
+ manual_items = pd.DataFrame(columns=[
379
+ 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي'
380
+ ])
381
+
382
+ # إضافة بضعة صفوف افتراضية
383
+ default_items = pd.DataFrame({
384
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5"],
385
+ 'وصف البند': [
386
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
387
+ "توريد وتركيب حديد التسليح للأساسات",
388
+ "أعمال العزل المائي للأساسات",
389
+ "أعمال الردم والدك للأساسات",
390
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
391
+ ],
392
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
393
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0],
394
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
395
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
396
+ })
397
+
398
+ manual_items = pd.concat([manual_items, default_items])
399
+ st.session_state.manual_items = manual_items
400
+
401
+ # إنشاء جدول تحليل الأسعار اذا لم يكن موجوداً
402
+ if 'items_price_analysis' not in st.session_state:
403
+ st.session_state.items_price_analysis = {}
404
+
405
+ # عرض واجهة إدخال ا��بنود
406
+ st.markdown("### إدخال تفاصيل البنود مع تحليل الأسعار")
407
+
408
+ # عرض البنود الحالية كجدول للعرض
409
+ st.markdown("### جدول البنود الحالية")
410
+ st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
411
+
412
+ # التبويبات لإضافة بند جديد أو تعديل بند
413
+ tabs = st.tabs(["إضافة بند جديد", "تعديل بند حالي"])
414
+
415
+ with tabs[0]: # إضافة بند جديد
416
+ st.markdown("### إضافة بند جديد مع تحليل السعر")
417
+
418
+ col1, col2 = st.columns(2)
419
+
420
+ with col1:
421
+ new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}", key="new_id")
422
+ new_desc = st.text_area("وصف البند", value="", key="new_desc")
423
+
424
+ with col2:
425
+ new_unit = st.selectbox("الوحدة", options=unit_options, key="new_unit")
426
+ new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f", key="new_qty")
427
+
428
+ # إنشاء تحليل السعر للبند الجديد
429
+ st.markdown('<div class="pricing-analysis-container">', unsafe_allow_html=True)
430
+ st.markdown("#### تحليل سعر البند")
431
+
432
+ # التعرف التلقائي على نوع البند من الوصف
433
+ is_concrete = False
434
+ is_steel = False
435
+ is_bricks = False
436
+ is_paint = False
437
+ is_insulation = False
438
+
439
+ if new_desc:
440
+ is_concrete = 'خرسان' in new_desc
441
+ is_steel = 'حديد' in new_desc or 'تسليح' in new_desc
442
+ is_bricks = 'بلوك' in new_desc or 'طوب' in new_desc
443
+ is_paint = 'دهان' in new_desc or 'طلاء' in new_desc
444
+ is_insulation = 'عزل' in new_desc
445
+
446
+ # تلميح للمستخدم عن التعرف التلقائي
447
+ if any([is_concrete, is_steel, is_bricks, is_paint, is_insulation]):
448
+ detected_type = ""
449
+ if is_concrete:
450
+ detected_type = "أعمال خرسانة"
451
+ elif is_steel:
452
+ detected_type = "أعمال حديد"
453
+ elif is_bricks:
454
+ detected_type = "أعمال بلوك"
455
+ elif is_paint:
456
+ detected_type = "أعمال دهانات"
457
+ elif is_insulation:
458
+ detected_type = "أعمال عزل"
459
+
460
+ st.info(f"تم التعرف تلقائياً على نوع البند: {detected_type}")
461
+
462
+ # إنشاء مصفوفة فارغة لمكونات البند
463
+ if 'new_components' not in st.session_state:
464
+ # إنشاء DataFrame فارغ
465
+ new_components = pd.DataFrame(columns=[
466
+ 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
467
+ ])
468
+
469
+ # إضافة مكونات افتراضية بناءً على نوع البند
470
+ if is_concrete:
471
+ # مكونات الخرسانة
472
+ default_components = pd.DataFrame({
473
+ 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
474
+ 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
475
+ 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
476
+ 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
477
+ 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
478
+ 'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
479
+ })
480
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
481
+
482
+ elif is_steel:
483
+ # مكونات الحديد
484
+ default_components = pd.DataFrame({
485
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
486
+ 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
487
+ 'الكمية': [1000, 10, 1, 1, 1],
488
+ 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
489
+ 'سعر الوحدة': [4.5, 50, 300, 200, 300],
490
+ 'الإجمالي': [4500, 500, 300, 200, 300]
491
+ })
492
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
493
+
494
+ elif is_bricks:
495
+ # مكونات البلوك
496
+ default_components = pd.DataFrame({
497
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
498
+ 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
499
+ 'الكمية': [12.5, 0.02, 1, 1, 1],
500
+ 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
501
+ 'سعر الوحدة': [8, 500, 80, 15, 20],
502
+ 'الإجمالي': [100, 10, 80, 15, 20]
503
+ })
504
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
505
+
506
+ elif is_paint:
507
+ # مكونات الدهانات
508
+ default_components = pd.DataFrame({
509
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
510
+ 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
511
+ 'الكمية': [0.4, 0.1, 1, 1, 1],
512
+ 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
513
+ 'سعر الوحدة': [80, 20, 35, 5, 10],
514
+ 'الإجمالي': [32, 2, 35, 5, 10]
515
+ })
516
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
517
+
518
+ elif is_insulation:
519
+ # مكونات العزل
520
+ default_components = pd.DataFrame({
521
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
522
+ 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
523
+ 'الكمية': [1.1, 0.2, 1, 1, 1],
524
+ 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
525
+ 'سعر الوحدة': [60, 30, 25, 10, 15],
526
+ 'الإجمالي': [66, 6, 25, 10, 15]
527
+ })
528
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
529
+
530
+ else:
531
+ # مكونات عامة افتراضية
532
+ default_components = pd.DataFrame({
533
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
534
+ 'الوصف': ['مواد أساسية', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
535
+ 'الكمية': [1, 1, 1, 1, 1],
536
+ 'الوحدة': [new_unit if new_unit else 'وحدة', 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
537
+ 'سعر الوحدة': [100, 50, 30, 20, 20],
538
+ 'الإجمالي': [100, 50, 30, 20, 20]
539
+ })
540
+ new_components = pd.concat([new_components, default_components], ignore_index=True)
541
+
542
+ st.session_state.new_components = new_components
543
+
544
+ # عرض وتحرير مكونات تحليل السعر
545
+ edited_components = st.data_editor(
546
+ st.session_state.new_components,
547
+ use_container_width=True,
548
+ hide_index=True,
549
+ num_rows="dynamic",
550
+ column_config={
551
+ 'نوع التكلفة': st.column_config.SelectboxColumn(
552
+ 'نوع التكلفة',
553
+ help='فئة التكلفة',
554
+ options=cost_categories
555
+ ),
556
+ 'الوحدة': st.column_config.SelectboxColumn(
557
+ 'الوحدة',
558
+ help='وحدة القياس',
559
+ options=unit_options + ["وحدة", "ساعة", "يوم"]
560
+ ),
561
+ 'الكمية': st.column_config.NumberColumn(
562
+ 'الكمية',
563
+ help='الكمية',
564
+ min_value=0.0,
565
+ format="%.2f"
566
+ ),
567
+ 'سعر الوحدة': st.column_config.NumberColumn(
568
+ 'سعر الوحدة',
569
+ help='سعر الوحدة',
570
+ min_value=0.0,
571
+ format="%.2f"
572
+ ),
573
+ 'الإجمالي': st.column_config.NumberColumn(
574
+ 'الإجمالي',
575
+ help='الإجمالي',
576
+ min_value=0.0,
577
+ format="%.2f"
578
+ )
579
+ }
580
+ )
581
+
582
+ # إعادة حساب الإجمالي لكل مكون
583
+ edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
584
+
585
+ # حفظ الت��ديلات
586
+ st.session_state.new_components = edited_components
587
+
588
+ # حساب إجمالي تحليل السعر
589
+ total_analysis_price = edited_components['الإجمالي'].sum()
590
+ unit_price_from_analysis = total_analysis_price / new_qty if new_qty > 0 else 0
591
+
592
+ # عرض ملخص تحليل السعر
593
+ st.markdown("#### ملخص تحليل السعر")
594
+
595
+ col1, col2 = st.columns(2)
596
+
597
+ with col1:
598
+ st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
599
+
600
+ with col2:
601
+ st.metric("سعر الوحدة المحسوب", f"{unit_price_from_analysis:,.2f} ريال")
602
+
603
+ st.markdown('</div>', unsafe_allow_html=True)
604
+
605
+ # استخدام السعر المحسوب
606
+ use_calculated_price = st.checkbox("استخدام السعر المحسوب من التحليل", value=True)
607
+
608
+ # تحديد سعر الوحدة النهائي
609
+ if use_calculated_price and new_qty > 0:
610
+ new_price = unit_price_from_analysis
611
+ else:
612
+ new_price = st.number_input("سعر الوحدة", value=unit_price_from_analysis if new_qty > 0 else 0.0, min_value=0.0, format="%.2f", key="new_price")
613
+
614
+ # حساب الإجمالي
615
+ new_total = new_qty * new_price
616
+ st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال")
617
+
618
+ # مقارنة السعر المدخل مع السعر المحسوب
619
+ if not use_calculated_price and new_qty > 0 and unit_price_from_analysis > 0:
620
+ price_diff = new_price - unit_price_from_analysis
621
+ diff_percentage = (price_diff / unit_price_from_analysis) * 100
622
+
623
+ if abs(diff_percentage) > 5: # إذا كان الفرق أكبر من 5%
624
+ if diff_percentage > 0:
625
+ st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%")
626
+ else:
627
+ st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%")
628
+
629
+ # زر إضافة البند
630
+ if st.button("إضافة البند"):
631
+ # التحقق من صحة البيانات
632
+ if new_id and new_desc and new_qty > 0:
633
+ # إنشاء صف جديد
634
+ new_row = pd.DataFrame({
635
+ 'رقم البند': [new_id],
636
+ 'وصف البند': [new_desc],
637
+ 'الوحدة': [new_unit],
638
+ 'الكمية': [float(new_qty)],
639
+ 'سعر الوحدة': [float(new_price)],
640
+ 'الإجمالي': [float(new_total)]
641
+ })
642
+
643
+ # إضافة الصف إلى DataFrame
644
+ st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True)
645
+
646
+ # حفظ تحليل سعر البند
647
+ st.session_state.items_price_analysis[new_id] = st.session_state.new_components.copy()
648
+
649
+ # إعادة تهيئة مكونات البند الجديد
650
+ if 'new_components' in st.session_state:
651
+ del st.session_state.new_components
652
+
653
+ st.success("تم إضافة البند وتحليل السعر بنجاح!")
654
+ time.sleep(0.5)
655
+ st.rerun()
656
+ else:
657
+ st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
658
+
659
+ with tabs[1]: # تعديل بند حالي
660
+ st.markdown("### تعديل بند حالي مع تحليل السعر")
661
+
662
+ # اختيار البند للتعديل
663
+ edit_item_id = st.selectbox(
664
+ "اختر البند للتعديل",
665
+ options=st.session_state.manual_items['رقم البند'].tolist(),
666
+ format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}..."
667
+ )
668
+
669
+ if edit_item_id:
670
+ # الحصول على مؤشر الصف للبند المحدد
671
+ idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == edit_item_id].index[0]
672
+ row = st.session_state.manual_items.loc[idx]
673
+
674
+ # إنشاء نموذج تعديل البند
675
+ col1, col2 = st.columns(2)
676
+
677
+ with col1:
678
+ edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id")
679
+ edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc")
680
+
681
+ with col2:
682
+ edited_unit = st.selectbox(
683
+ "الوحدة (تعديل)",
684
+ options=unit_options,
685
+ index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0,
686
+ key="edit_unit"
687
+ )
688
+ edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty")
689
+
690
+ # إنشاء أو تحرير تحليل السعر للبند
691
+ st.markdown('<div class="pricing-analysis-container">', unsafe_allow_html=True)
692
+ st.markdown("#### تحليل سعر البند")
693
+
694
+ # التحقق مما إذا كان البند له تحليل سعر محفوظ
695
+ if edit_item_id in st.session_state.items_price_analysis:
696
+ # استخدام تحليل السعر المحفوظ
697
+ components = st.session_state.items_price_analysis[edit_item_id]
698
+ else:
699
+ # إنشاء تحليل سعر افتراضي
700
+ components = pd.DataFrame(columns=[
701
+ 'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
702
+ ])
703
+
704
+ # فحص نوع البند من الوصف
705
+ is_concrete = 'خرسان' in row['وصف البند']
706
+ is_steel = 'حديد' in row['وصف البند'] or 'تسليح' in row['وصف البند']
707
+ is_bricks = 'بلوك' in row['وصف البند'] or 'طوب' in row['وصف البند']
708
+ is_paint = 'دهان' in row['وصف البند'] or 'طلاء' in row['وصف البند']
709
+ is_insulation = 'عزل' in row['وصف البند']
710
+
711
+ # إضافة مكونات افتراضية بناءً على نوع البند
712
+ if is_concrete:
713
+ # مكونات الخرسانة
714
+ default_components = pd.DataFrame({
715
+ 'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
716
+ 'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
717
+ 'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
718
+ 'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
719
+ 'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
720
+ 'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
721
+ })
722
+ components = pd.concat([components, default_components], ignore_index=True)
723
+
724
+ elif is_steel:
725
+ # مكونات الحديد
726
+ default_components = pd.DataFrame({
727
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
728
+ 'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
729
+ 'الكمية': [1000, 10, 1, 1, 1],
730
+ 'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
731
+ 'سعر الوحدة': [4.5, 50, 300, 200, 300],
732
+ 'الإجمالي': [4500, 500, 300, 200, 300]
733
+ })
734
+ components = pd.concat([components, default_components], ignore_index=True)
735
+
736
+ elif is_bricks:
737
+ # مكونات البلوك
738
+ default_components = pd.DataFrame({
739
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
740
+ 'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
741
+ 'الكمية': [12.5, 0.02, 1, 1, 1],
742
+ 'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
743
+ 'سعر الوحدة': [8, 500, 80, 15, 20],
744
+ 'الإجمالي': [100, 10, 80, 15, 20]
745
+ })
746
+ components = pd.concat([components, default_components], ignore_index=True)
747
+
748
+ elif is_paint:
749
+ # مكونات الدهانات
750
+ default_components = pd.DataFrame({
751
+ 'نوع التكلفة': ['مواد', 'مواد', 'ع��الة', 'مصاريف عامة', 'أرباح'],
752
+ 'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
753
+ 'الكمية': [0.4, 0.1, 1, 1, 1],
754
+ 'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
755
+ 'سعر الوحدة': [80, 20, 35, 5, 10],
756
+ 'الإجمالي': [32, 2, 35, 5, 10]
757
+ })
758
+ components = pd.concat([components, default_components], ignore_index=True)
759
+
760
+ elif is_insulation:
761
+ # مكونات العزل
762
+ default_components = pd.DataFrame({
763
+ 'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
764
+ 'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
765
+ 'الكمية': [1.1, 0.2, 1, 1, 1],
766
+ 'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
767
+ 'سعر الوحدة': [60, 30, 25, 10, 15],
768
+ 'الإجمالي': [66, 6, 25, 10, 15]
769
+ })
770
+ components = pd.concat([components, default_components], ignore_index=True)
771
+
772
+ else:
773
+ # مكونات عامة افتراضية
774
+ default_components = pd.DataFrame({
775
+ 'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
776
+ 'الوصف': ['مواد أساسية', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
777
+ 'الكمية': [1, 1, 1, 1, 1],
778
+ 'الوحدة': [row['الوحدة'], 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
779
+ 'سعر الوحدة': [
780
+ row['سعر الوحدة'] * 0.6,
781
+ row['سعر الوحدة'] * 0.2,
782
+ row['سعر الوحدة'] * 0.1,
783
+ row['سعر الوحدة'] * 0.05,
784
+ row['سعر الوحدة'] * 0.05
785
+ ],
786
+ 'الإجمالي': [
787
+ row['سعر الوحدة'] * 0.6,
788
+ row['سعر الوحدة'] * 0.2,
789
+ row['سعر الوحدة'] * 0.1,
790
+ row['سعر الوحدة'] * 0.05,
791
+ row['سعر الوحدة'] * 0.05
792
+ ]
793
+ })
794
+ components = pd.concat([components, default_components], ignore_index=True)
795
+
796
+ # حفظ تحليل السعر
797
+ st.session_state.items_price_analysis[edit_item_id] = components
798
+
799
+ # عرض وتحرير مكونات تحليل السعر
800
+ edited_components = st.data_editor(
801
+ components,
802
+ use_container_width=True,
803
+ hide_index=True,
804
+ num_rows="dynamic",
805
+ column_config={
806
+ 'نوع التكلفة': st.column_config.SelectboxColumn(
807
+ 'نوع التكلفة',
808
+ help='فئة التكلفة',
809
+ options=cost_categories
810
+ ),
811
+ 'الوحدة': st.column_config.SelectboxColumn(
812
+ 'الوحدة',
813
+ help='وحدة القياس',
814
+ options=unit_options + ["وحدة", "ساعة", "يوم"]
815
+ ),
816
+ 'الكمية': st.column_config.NumberColumn(
817
+ 'الكمية',
818
+ help='الكمية',
819
+ min_value=0.0,
820
+ format="%.2f"
821
+ ),
822
+ 'سعر الوحدة': st.column_config.NumberColumn(
823
+ 'سعر الوحدة',
824
+ help='سعر الوحدة',
825
+ min_value=0.0,
826
+ format="%.2f"
827
+ ),
828
+ 'الإجمالي': st.column_config.NumberColumn(
829
+ 'الإجمالي',
830
+ help='الإجمالي',
831
+ min_value=0.0,
832
+ format="%.2f"
833
+ )
834
+ }
835
+ )
836
+
837
+ # إعادة حساب الإجمالي لكل مكون
838
+ edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
839
+
840
+ # حفظ التعديلات
841
+ st.session_state.items_price_analysis[edit_item_id] = edited_components
842
+
843
+ # حساب إجمالي تحليل السعر
844
+ total_analysis_price = edited_components['الإجمالي'].sum()
845
+ unit_price_from_analysis = total_analysis_price / edited_qty if edited_qty > 0 else 0
846
+
847
+ # عرض ملخص تحليل السعر
848
+ st.markdown("#### ملخص تحليل السعر")
849
+
850
+ col1, col2 = st.columns(2)
851
+
852
+ with col1:
853
+ st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
854
+
855
+ with col2:
856
+ st.metric("سعر الوحدة المحسوب", f"{unit_price_from_analysis:,.2f} ريال")
857
+
858
+ st.markdown('</div>', unsafe_allow_html=True)
859
+
860
+ # استخدام السعر المحسوب
861
+ use_calculated_price = st.checkbox("استخدام السعر المحسوب من التحليل", value=True, key="use_calc_edit")
862
+
863
+ # تحديد سعر الوحدة النهائي
864
+ if use_calculated_price and edited_qty > 0:
865
+ edited_price = unit_price_from_analysis
866
+ else:
867
+ edited_price = st.number_input(
868
+ "سعر الوحدة (تعديل)",
869
+ value=unit_price_from_analysis if edited_qty > 0 and unit_price_from_analysis > 0 else float(row['سعر الوحدة']),
870
+ min_value=0.0,
871
+ format="%.2f",
872
+ key="edit_price"
873
+ )
874
+
875
+ # حساب الإجمالي
876
+ edited_total = edited_qty * edited_price
877
+ st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال")
878
+
879
+ # مقارنة السعر المدخل مع السعر المحسوب
880
+ if not use_calculated_price and edited_qty > 0 and unit_price_from_analysis > 0:
881
+ price_diff = edited_price - unit_price_from_analysis
882
+ diff_percentage = (price_diff / unit_price_from_analysis) * 100
883
+
884
+ if abs(diff_percentage) > 5: # إذا كان الفرق أكبر من 5%
885
+ if diff_percentage > 0:
886
+ st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%")
887
+ else:
888
+ st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%")
889
+
890
+ # أزرار الإجراءات
891
+ col1, col2, col3 = st.columns(3)
892
+
893
+ with col1:
894
+ if st.button("حفظ التعديلات", use_container_width=True):
895
+ # التحقق من صحة البيانات
896
+ if edited_id and edited_desc and edited_qty > 0:
897
+ # التحقق من تغيير رقم البند
898
+ if edited_id != edit_item_id:
899
+ # نقل تحليل السعر إلى الرقم الجديد
900
+ st.session_state.items_price_analysis[edited_id] = st.session_state.items_price_analysis.pop(edit_item_id)
901
+
902
+ # تحديث البند
903
+ st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id
904
+ st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc
905
+ st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit
906
+ st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty
907
+ st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price
908
+ st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total
909
+
910
+ st.success("تم تحديث البند وتحليل السعر بنجاح!")
911
+ time.sleep(0.5)
912
+ st.rerun()
913
+ else:
914
+ st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
915
+
916
+ with col2:
917
+ if st.button("استعادة القيم الأصلية", use_container_width=True):
918
+ # إعادة تحميل الصفحة لاستعادة القيم الأصلية
919
+ st.rerun()
920
+
921
+ with col3:
922
+ if st.button("حذف هذا البند", use_container_width=True):
923
+ # حذف تحليل السعر للبند
924
+ if edit_item_id in st.session_state.items_price_analysis:
925
+ del st.session_state.items_price_analysis[edit_item_id]
926
+
927
+ # حذف البند
928
+ st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True)
929
+
930
+ st.warning("تم حذف البند وتحليل السعر!")
931
+ time.sleep(0.5)
932
+ st.rerun()
modules/pricing/pricing_app.py ADDED
@@ -0,0 +1,1312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ وحدة التسعير المتكاملة
3
+ """
4
+
5
+ import streamlit as st
6
+ import pandas as pd
7
+ import numpy as np
8
+ import random
9
+ from datetime import datetime
10
+ import time
11
+
12
+ class PricingApp:
13
+ """
14
+ وحدة التسعير المتكاملة للنظام
15
+ """
16
+
17
+ def __init__(self):
18
+ """
19
+ تهيئة وحدة التسعير
20
+ """
21
+ # تهيئة حالة الجلسة الخاصة بالتسعير إذا لم تكن موجودة
22
+ if 'pricing_projects' not in st.session_state:
23
+ # إنشاء بيانات تجريبية للمشاريع
24
+ st.session_state.pricing_projects = self._generate_sample_projects()
25
+
26
+ if 'pricing_templates' not in st.session_state:
27
+ # إنشاء بيانات تجريبية لقوالب التسعير
28
+ st.session_state.pricing_templates = self._generate_sample_templates()
29
+
30
+ if 'pricing_resources' not in st.session_state:
31
+ # إنشاء بيانات تجريبية للموارد
32
+ st.session_state.pricing_resources = self._generate_sample_resources()
33
+
34
+ def run(self):
35
+ """
36
+ تشغيل وحدة التسعير
37
+ """
38
+ st.markdown("<h2 class='module-title'>وحدة التسعير المتكاملة</h2>", unsafe_allow_html=True)
39
+
40
+ # إنشاء تبويبات للتسعير المختلفة
41
+ tabs = st.tabs(["لوحة التحكم", "تسعير المناقصات", "جداول الكميات", "تحليل الأسعار", "قوالب التسعير"])
42
+
43
+ with tabs[0]:
44
+ self._render_dashboard()
45
+
46
+ with tabs[1]:
47
+ self._render_tender_pricing()
48
+
49
+ with tabs[2]:
50
+ self._render_bill_of_quantities()
51
+
52
+ with tabs[3]:
53
+ self._render_price_analysis()
54
+
55
+ with tabs[4]:
56
+ self._render_pricing_templates()
57
+
58
+ def _render_dashboard(self):
59
+ """
60
+ عرض لوحة التحكم
61
+ """
62
+ st.markdown("### لوحة تحكم التسعير")
63
+
64
+ # عرض المؤشرات الرئيسية
65
+ col1, col2, col3, col4 = st.columns(4)
66
+
67
+ with col1:
68
+ active_tenders = len([p for p in st.session_state.pricing_projects if p['status'] == 'قيد التسعير'])
69
+ st.info(f"### {active_tenders}\nمناقصات قيد التسعير", icon="📝")
70
+
71
+ with col2:
72
+ completed_tenders = len([p for p in st.session_state.pricing_projects if p['status'] == 'تم التسعير'])
73
+ st.success(f"### {completed_tenders}\nمناقصات تم تسعيرها", icon="✅")
74
+
75
+ with col3:
76
+ awarded_tenders = len([p for p in st.session_state.pricing_projects if p['status'] == 'تمت الترسية'])
77
+ st.success(f"### {awarded_tenders}\nمناقصات تمت ترسيتها", icon="🏆")
78
+
79
+ with col4:
80
+ rejected_tenders = len([p for p in st.session_state.pricing_projects if p['status'] == 'مرفوضة'])
81
+ st.error(f"### {rejected_tenders}\nمناقصات مرفوضة", icon="❌")
82
+
83
+ # عرض المناقصات الحالية
84
+ st.markdown("### المناقصات الحالية")
85
+
86
+ # تصفية المناقصات النشطة
87
+ active_projects = [p for p in st.session_state.pricing_projects if p['status'] in ['قيد التسعير', 'تم التسعير']]
88
+
89
+ if active_projects:
90
+ # تحويل البيانات إلى DataFrame
91
+ df = pd.DataFrame(active_projects)
92
+ df = df[['id', 'name', 'client', 'value', 'deadline', 'status', 'completion']]
93
+ df.columns = ['الرقم', 'اسم المناقصة', 'العميل', 'القيمة التقديرية', 'الموعد النهائي', 'الحالة', 'نسبة الإنجاز']
94
+
95
+ # تنسيق القيم
96
+ df['القيمة التقديرية'] = df['القيمة التقديرية'].apply(lambda x: f"{x:,} ريال")
97
+ df['نسبة الإنجاز'] = df['نسبة الإنجاز'].apply(lambda x: f"{x}%")
98
+
99
+ # عرض الجدول
100
+ st.dataframe(df, use_container_width=True)
101
+ else:
102
+ st.info("لا توجد مناقصات نشطة حالياً", icon="ℹ️")
103
+
104
+ # عرض إحصائيات التسعير
105
+ st.markdown("### إحصائيات التسعير")
106
+
107
+ col1, col2 = st.columns(2)
108
+
109
+ with col1:
110
+ st.markdown("#### نسب النجاح حسب نوع المشروع")
111
+
112
+ # إنشاء بيانات تجريبية
113
+ success_by_type = {
114
+ "طرق وجسور": 75,
115
+ "مباني": 60,
116
+ "بنية تحتية": 80,
117
+ "مياه وصرف صحي": 65,
118
+ "كهرباء": 70
119
+ }
120
+
121
+ # تحويل البيانات إلى DataFrame
122
+ success_df = pd.DataFrame({
123
+ "نوع المشروع": list(success_by_type.keys()),
124
+ "نسبة النجاح": list(success_by_type.values())
125
+ })
126
+
127
+ # عرض الرسم البياني
128
+ st.bar_chart(success_df.set_index("نوع المشروع"))
129
+
130
+ with col2:
131
+ st.markdown("#### متوسط هامش الربح حسب العميل")
132
+
133
+ # إنشاء بيانات تجريبية
134
+ margin_by_client = {
135
+ "وزارة النقل": 12,
136
+ "وزارة الإسكان": 15,
137
+ "أمانة منطقة الرياض": 10,
138
+ "شركة أرامكو": 8,
139
+ "الهيئة الملكية": 14
140
+ }
141
+
142
+ # تحويل البيانات إلى DataFrame
143
+ margin_df = pd.DataFrame({
144
+ "العميل": list(margin_by_client.keys()),
145
+ "هامش الربح (%)": list(margin_by_client.values())
146
+ })
147
+
148
+ # عرض الرسم البياني
149
+ st.bar_chart(margin_df.set_index("العميل"))
150
+
151
+ def _render_tender_pricing(self):
152
+ """
153
+ عرض واجهة تسعير المناقصات
154
+ """
155
+ st.markdown("### تسعير المناقصات")
156
+
157
+ # اختيار المناقصة
158
+ project_names = [p['name'] for p in st.session_state.pricing_projects]
159
+ selected_project = st.selectbox("اختر المناقصة", options=project_names, key="selected_pricing_project")
160
+
161
+ # الحصول على معلومات المناقصة المحددة
162
+ project_info = next((p for p in st.session_state.pricing_projects if p['name'] == selected_project), None)
163
+
164
+ if project_info:
165
+ # عرض معلومات المناقصة
166
+ st.markdown(f"""
167
+ <div style="background-color: var(--gray-100); padding: 15px; border-radius: 8px; margin-bottom: 20px;">
168
+ <h4 style="color: var(--primary-color);">{project_info['name']}</h4>
169
+ <p><strong>العميل:</strong> {project_info['client']}</p>
170
+ <p><strong>القيمة التقديرية:</strong> {project_info['value']:,} ريال</p>
171
+ <p><strong>الموعد النهائي:</strong> {project_info['deadline']}</p>
172
+ <p><strong>الحالة:</strong> {project_info['status']}</p>
173
+ <p><strong>نسبة الإنجاز:</strong> {project_info['completion']}%</p>
174
+ </div>
175
+ """, unsafe_allow_html=True)
176
+
177
+ # تبويبات فرعية للتسعير
178
+ subtabs = st.tabs(["ملخص التسعير", "جدول الكميات", "تحليل التكاليف", "هامش الربح", "المخاطر"])
179
+
180
+ with subtabs[0]:
181
+ self._render_pricing_summary(project_info)
182
+
183
+ with subtabs[1]:
184
+ self._render_project_boq(project_info)
185
+
186
+ with subtabs[2]:
187
+ self._render_cost_analysis(project_info)
188
+
189
+ with subtabs[3]:
190
+ self._render_profit_margin(project_info)
191
+
192
+ with subtabs[4]:
193
+ self._render_risk_analysis(project_info)
194
+
195
+ def _render_pricing_summary(self, project_info):
196
+ """
197
+ عرض ملخص التسعير
198
+ """
199
+ st.markdown("#### ملخص التسعير")
200
+
201
+ # عرض شريط التقدم
202
+ st.progress(project_info['completion'] / 100, text=f"نسبة إكمال التسعير: {project_info['completion']}%")
203
+
204
+ # عرض ملخص التكاليف
205
+ st.markdown("##### ملخص التكاليف")
206
+
207
+ # إنشاء بيانات تجريبية للتكاليف
208
+ direct_cost = project_info['value'] * 0.7
209
+ indirect_cost = project_info['value'] * 0.15
210
+ profit_margin = project_info['value'] * 0.15
211
+
212
+ costs = {
213
+ "التكاليف المباشرة": direct_cost,
214
+ "التكاليف غير المباشرة": indirect_cost,
215
+ "هامش الربح": profit_margin,
216
+ "إجمالي العرض": project_info['value']
217
+ }
218
+
219
+ # عرض جدول التكاليف
220
+ costs_df = pd.DataFrame({
221
+ "البند": list(costs.keys()),
222
+ "القيمة (ريال)": [f"{value:,.2f}" for value in costs.values()],
223
+ "النسبة": [f"{(value / project_info['value']) * 100:.1f}%" for value in costs.values()]
224
+ })
225
+
226
+ st.dataframe(costs_df, use_container_width=True, hide_index=True)
227
+
228
+ # عرض الرسم البياني للتكاليف
229
+ cost_chart_data = pd.DataFrame({
230
+ "البند": ["التكاليف ��لمباشرة", "التكاليف غير المباشرة", "هامش الربح"],
231
+ "القيمة": [direct_cost, indirect_cost, profit_margin]
232
+ })
233
+
234
+ st.bar_chart(cost_chart_data.set_index("البند"))
235
+
236
+ # عرض ملاحظات التسعير
237
+ st.markdown("##### ملاحظات التسعير")
238
+
239
+ notes = [
240
+ "تم تحديث أسعار المواد وفقاً لآخر الأسعار في السوق",
241
+ "تم زيادة هامش المخاطر بنسبة 2% نظراً لموقع المشروع",
242
+ "تم تخفيض تكلفة النقل بناءً على توفر المعدات في الموقع",
243
+ "يجب مراجعة أسعار الحديد قبل تقديم العرض النهائي"
244
+ ]
245
+
246
+ for note in notes:
247
+ st.markdown(f"- {note}")
248
+
249
+ # زر تحديث التسعير
250
+ if st.button("تحديث التسعير", key="update_pricing_btn"):
251
+ st.success("تم تحديث التسعير بنجاح", icon="✅")
252
+
253
+ def _render_project_boq(self, project_info):
254
+ """
255
+ عرض جدول الكميات للمشروع
256
+ """
257
+ st.markdown("#### جدول الكميات")
258
+
259
+ # إنشاء بيانات تجريبية لجدول الكميات
260
+ boq_items = []
261
+ categories = ["أعمال ترابية", "أعمال خرسانية", "أعمال معمارية", "أعمال كهربائية", "أعمال ميكانيكية"]
262
+
263
+ for i, category in enumerate(categories):
264
+ # إنشاء عدة بنود لكل فئة
265
+ for j in range(3):
266
+ item_id = i * 3 + j + 1
267
+ item = {
268
+ "الرقم": f"{i+1}.{j+1}",
269
+ "الفئة": category,
270
+ "البند": f"بند {item_id}",
271
+ "الوصف": f"وصف تفصيلي للبند {item_id} ضمن فئة {category}",
272
+ "الوحدة": random.choice(["متر", "متر مربع", "متر مكعب", "طن", "قطعة"]),
273
+ "الكمية": random.randint(10, 1000),
274
+ "سعر الوحدة": random.randint(100, 5000),
275
+ "الإجمالي": 0
276
+ }
277
+ item["الإجمالي"] = item["الكمية"] * item["سعر الوحدة"]
278
+ boq_items.append(item)
279
+
280
+ # تحويل البيانات إلى DataFrame
281
+ boq_df = pd.DataFrame(boq_items)
282
+
283
+ # تنسيق القيم
284
+ boq_df["سعر الوحدة"] = boq_df["سعر الوحدة"].apply(lambda x: f"{x:,}")
285
+ boq_df["الإجمالي"] = boq_df["الإجمالي"].apply(lambda x: f"{x:,}")
286
+
287
+ # عرض جدول الكميات مع تجميع حسب الفئة
288
+ st.dataframe(boq_df, use_container_width=True)
289
+
290
+ # حساب الإجمالي
291
+ total = sum(item["الإجمالي"] for item in boq_items)
292
+ st.markdown(f"**الإجمالي:** {total:,} ريال")
293
+
294
+ # أزرار التحكم
295
+ col1, col2, col3 = st.columns(3)
296
+
297
+ with col1:
298
+ st.button("إضافة بند", key="add_boq_item_btn")
299
+
300
+ with col2:
301
+ st.button("استيراد من Excel", key="import_boq_btn")
302
+
303
+ with col3:
304
+ st.button("تصدير إلى Excel", key="export_boq_btn")
305
+
306
+ def _render_cost_analysis(self, project_info):
307
+ """
308
+ عرض تحليل التكاليف
309
+ """
310
+ st.markdown("#### تحليل التكاليف")
311
+
312
+ # تبويبات فرعية لتحليل التكاليف
313
+ cost_tabs = st.tabs(["تكاليف المواد", "تكاليف العمالة", "تكاليف المعدات", "التكاليف غير المباشرة"])
314
+
315
+ with cost_tabs[0]:
316
+ # تحليل تكاليف المواد
317
+ st.markdown("##### تكاليف المواد")
318
+
319
+ # إنشاء بيانات تجريبية لتكاليف المواد
320
+ materials = [
321
+ {"المادة": "خرسانة", "الكمية": 500, "الوحدة": "متر مكعب", "سعر الوحدة": 250, "الإجمالي": 125000},
322
+ {"المادة": "حديد تسليح", "الكمية": 50, "الوحدة": "طن", "سعر الوحدة": 3000, "الإجمالي": 150000},
323
+ {"المادة": "طابوق", "الكمية": 10000, "الوحدة": "قطعة", "سعر الوحدة": 5, "الإجمالي": 50000},
324
+ {"المادة": "بلاط", "الكمية": 1000, "الوحدة": "متر مربع", "سعر الوحدة": 80, "الإجمالي": 80000},
325
+ {"المادة": "أسمنت", "الكمية": 1000, "الوحدة": "كيس", "سعر الوحدة": 20, "الإجمالي": 20000}
326
+ ]
327
+
328
+ # تحويل البي��نات إلى DataFrame
329
+ materials_df = pd.DataFrame(materials)
330
+
331
+ # تنسيق القيم
332
+ materials_df["سعر الوحدة"] = materials_df["سعر الوحدة"].apply(lambda x: f"{x:,}")
333
+ materials_df["الإجمالي"] = materials_df["الإجمالي"].apply(lambda x: f"{x:,}")
334
+
335
+ # عرض جدول المواد
336
+ st.dataframe(materials_df, use_container_width=True)
337
+
338
+ # حساب الإجمالي
339
+ total_materials = sum(item["الإجمالي"] for item in materials)
340
+ st.markdown(f"**إجمالي تكاليف المواد:** {total_materials:,} ريال")
341
+
342
+ # عرض الرسم البياني
343
+ materials_chart = pd.DataFrame({
344
+ "المادة": [item["المادة"] for item in materials],
345
+ "التكلفة": [item["الإجمالي"] for item in materials]
346
+ })
347
+
348
+ st.bar_chart(materials_chart.set_index("المادة"))
349
+
350
+ with cost_tabs[1]:
351
+ # تحليل تكاليف العمالة
352
+ st.markdown("##### تكاليف العمالة")
353
+
354
+ # إنشاء بيانات تجريبية لتكاليف العمالة
355
+ labor = [
356
+ {"الوظيفة": "مهندس موقع", "العدد": 2, "المدة (شهر)": 12, "التكلفة الشهرية": 15000, "الإجمالي": 360000},
357
+ {"الوظيفة": "مشرف", "العدد": 4, "المدة (شهر)": 12, "التكلفة الشهرية": 8000, "الإجمالي": 384000},
358
+ {"الوظيفة": "فني", "العدد": 10, "المدة (شهر)": 12, "التكلفة الشهرية": 5000, "الإجمالي": 600000},
359
+ {"الوظيفة": "عامل", "العدد": 20, "المدة (شهر)": 12, "التكلفة الشهرية": 3000, "الإجمالي": 720000},
360
+ {"الوظيفة": "سائق", "العدد": 5, "المدة (شهر)": 12, "التكلفة الشهرية": 4000, "الإجمالي": 240000}
361
+ ]
362
+
363
+ # تحويل البيانات إلى DataFrame
364
+ labor_df = pd.DataFrame(labor)
365
+
366
+ # تنسيق القيم
367
+ labor_df["التكلفة الشهرية"] = labor_df["التكلفة الشهرية"].apply(lambda x: f"{x:,}")
368
+ labor_df["الإجمالي"] = labor_df["الإجمالي"].apply(lambda x: f"{x:,}")
369
+
370
+ # عرض جدول العمالة
371
+ st.dataframe(labor_df, use_container_width=True)
372
+
373
+ # حساب الإجمالي
374
+ total_labor = sum(item["الإجمالي"] for item in labor)
375
+ st.markdown(f"**إجمالي تكاليف العمالة:** {total_labor:,} ريال")
376
+
377
+ with cost_tabs[2]:
378
+ # تحليل تكاليف المعدات
379
+ st.markdown("##### تكاليف المعدات")
380
+
381
+ # إنشاء بيانات تجريبية لتكاليف المعدات
382
+ equipment = [
383
+ {"المعدة": "حفارة", "العدد": 2, "المدة (شهر)": 6, "التكلفة الشهرية": 20000, "الإجمالي": 240000},
384
+ {"المعدة": "لودر", "العدد": 2, "المدة (شهر)": 8, "التكلفة الشهرية": 15000, "الإجمالي": 240000},
385
+ {"المعدة": "شاحنة نقل", "العدد": 4, "المدة (شهر)": 12, "التكلفة الشهرية": 10000, "الإجمالي": 480000},
386
+ {"المعدة": "خلاطة خرسانة", "العدد": 2, "المدة (شهر)": 10, "التكلفة الشهرية": 8000, "الإجمالي": 160000},
387
+ {"المعدة": "رافعة", "العدد": 1, "المدة (شهر)": 4, "التكلفة الشهرية": 25000, "الإجمالي": 100000}
388
+ ]
389
+
390
+ # تحويل البيانات إلى DataFrame
391
+ equipment_df = pd.DataFrame(equipment)
392
+
393
+ # تنسيق القيم
394
+ equipment_df["التكلفة الشهرية"] = equipment_df["التكلفة الشهرية"].apply(lambda x: f"{x:,}")
395
+ equipment_df["الإجمالي"] = equipment_df["الإجمالي"].apply(lambda x: f"{x:,}")
396
+
397
+ # عرض جدول المعدات
398
+ st.dataframe(equipment_df, use_container_width=True)
399
+
400
+ # حساب الإجمالي
401
+ total_equipment = sum(item["الإجمالي"] for item in equipment)
402
+ st.markdown(f"**إجمالي تكاليف المعدات:** {total_equipment:,} ريال")
403
+
404
+ with cost_tabs[3]:
405
+ # تحليل التكاليف غير المباشرة
406
+ st.markdown("##### التكاليف غير المباشرة")
407
+
408
+ # إنشاء بيانات تجريبية للتكاليف غير المباشرة
409
+ indirect_costs = [
410
+ {"البند": "إدارة المشروع", "النسبة": "5%", "القيمة": 250000},
411
+ {"البند": "ضمانات بنكية", "النسبة": "2%", "القيمة": 100000},
412
+ {"البند": "تأمين", "النسبة": "1.5%", "القيمة": 75000},
413
+ {"البند": "مكاتب الموقع", "النسبة": "1%", "القيمة": 50000},
414
+ {"البند": "مصاريف إدارية", "النسبة": "3%", "القيمة": 150000},
415
+ {"البند": "مصاريف نثرية", "النسبة": "1.5%", "القيمة": 75000}
416
+ ]
417
+
418
+ # تحويل البيانات إلى DataFrame
419
+ indirect_costs_df = pd.DataFrame(indirect_costs)
420
+
421
+ # تنسيق القيم
422
+ indirect_costs_df["القيمة"] = indirect_costs_df["القيمة"].apply(lambda x: f"{x:,}")
423
+
424
+ # عرض جدول التكاليف غير المباشرة
425
+ st.dataframe(indirect_costs_df, use_container_width=True)
426
+
427
+ # حساب الإجمالي
428
+ total_indirect = sum(item["القيمة"] for item in indirect_costs)
429
+ st.markdown(f"**إجمالي التكاليف غير المباشرة:** {total_indirect:,} ريال")
430
+
431
+ def _render_profit_margin(self, project_info):
432
+ """
433
+ عرض تحليل هامش الربح
434
+ """
435
+ st.markdown("#### تحليل هامش الربح")
436
+
437
+ # إنشاء بيانات تجريبية لتحليل هامش الربح
438
+ direct_cost = project_info['value'] * 0.7
439
+ indirect_cost = project_info['value'] * 0.15
440
+ total_cost = direct_cost + indirect_cost
441
+ profit = project_info['value'] - total_cost
442
+ profit_percentage = (profit / project_info['value']) * 100
443
+
444
+ # عرض ملخص هامش الربح
445
+ col1, col2, col3 = st.columns(3)
446
+
447
+ with col1:
448
+ st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
449
+
450
+ with col2:
451
+ st.metric("هامش الربح", f"{profit:,.2f} ريال")
452
+
453
+ with col3:
454
+ st.metric("نسبة الربح", f"{profit_percentage:.2f}%")
455
+
456
+ # عرض تحليل الحساسية
457
+ st.markdown("##### تحليل الحساسية")
458
+ st.markdown("تأثير تغير التكاليف على هامش الربح")
459
+
460
+ # إنشاء بيانات تحليل الحساسية
461
+ sensitivity_data = []
462
+ cost_changes = [-10, -5, 0, 5, 10, 15, 20]
463
+
464
+ for change in cost_changes:
465
+ adjusted_cost = total_cost * (1 + change / 100)
466
+ adjusted_profit = project_info['value'] - adjusted_cost
467
+ adjusted_profit_percentage = (adjusted_profit / project_info['value']) * 100
468
+
469
+ sensitivity_data.append({
470
+ "تغير التكاليف": f"{change}%",
471
+ "التكلفة المعدلة": adjusted_cost,
472
+ "الربح المعدل": adjusted_profit,
473
+ "نسبة الربح المعدلة": adjusted_profit_percentage
474
+ })
475
+
476
+ # تحويل البيانات إلى DataFrame
477
+ sensitivity_df = pd.DataFrame(sensitivity_data)
478
+
479
+ # تنسيق القيم
480
+ sensitivity_df["التكلفة المعدلة"] = sensitivity_df["التكلفة المعدلة"].apply(lambda x: f"{x:,.2f}")
481
+ sensitivity_df["الربح المعدل"] = sensitivity_df["الربح المعدل"].apply(lambda x: f"{x:,.2f}")
482
+ sensitivity_df["نسبة الربح المعدلة"] = sensitivity_df["نسبة الربح المعدلة"].apply(lambda x: f"{x:.2f}%")
483
+
484
+ # عرض جدول تحليل الحساسية
485
+ st.dataframe(sensitivity_df, use_container_width=True)
486
+
487
+ # عرض الرسم البياني لتحليل الحساسية
488
+ chart_data = pd.DataFrame({
489
+ "تغير التكاليف": cost_changes,
490
+ "نسبة الربح": [row["نسبة الربح المعدلة"].replace("%", "") for row in sensitivity_data]
491
+ })
492
+
493
+ chart_data["نسبة الربح"] = chart_data["نسبة الربح"].astype(float)
494
+
495
+ st.line_chart(chart_data.set_index("تغير التكاليف"))
496
+
497
+ # عرض توصيات هامش الربح
498
+ st.markdown("##### توصيات هامش الربح")
499
+
500
+ recommendations = [
501
+ "الحفاظ على هامش ربح لا يقل عن 10% لضمان تغطية المخاطر غير المتوقعة",
502
+ "مراجعة أسعار المواد الرئيسية قبل تقديم العرض النهائي",
503
+ "التفاوض مع الموردين للحصول على خصومات إضافية",
504
+ "تقليل التكاليف غير المباشرة من خلال مشا��كة الموارد مع مشاريع أخرى"
505
+ ]
506
+
507
+ for recommendation in recommendations:
508
+ st.markdown(f"- {recommendation}")
509
+
510
+ def _render_risk_analysis(self, project_info):
511
+ """
512
+ عرض تحليل المخاطر
513
+ """
514
+ st.markdown("#### تحليل المخاطر")
515
+
516
+ # إنشاء بيانات تجريبية للمخاطر
517
+ risks = [
518
+ {"المخاطرة": "ارتفاع أسعار المواد", "الاحتمالية": 70, "التأثير": 80, "المستوى": "مرتفع", "الاستجابة": "تضمين بند تعديل الأسعار في العقد"},
519
+ {"المخاطرة": "تأخر التوريدات", "الاحتمالية": 60, "التأثير": 70, "المستوى": "مرتفع", "الاستجابة": "طلب توريدات مبكرة وتخزين المواد الأساسية"},
520
+ {"المخاطرة": "نقص العمالة", "الاحتمالية": 50, "التأثير": 60, "المستوى": "متوسط", "الاستجابة": "التعاقد المسبق مع مقاولي الباطن"},
521
+ {"المخاطرة": "ظروف جوية", "الاحتمالية": 40, "التأثير": 50, "المستوى": "متوسط", "الاستجابة": "تضمين مدة إضافية في الجدول الزمني"},
522
+ {"المخاطرة": "تغيير المواصفات", "الاحتمالية": 30, "التأثير": 80, "المستوى": "متوسط", "الاستجابة": "تضمين بند تغيير الأوامر في العقد"},
523
+ {"المخاطرة": "مشاكل تمويلية", "الاحتمالية": 20, "التأثير": 90, "المستوى": "متوسط", "الاستجابة": "تأمين خط ائتمان احتياطي"}
524
+ ]
525
+
526
+ # تحويل البيانات إلى DataFrame
527
+ risks_df = pd.DataFrame(risks)
528
+
529
+ # عرض جدول المخاطر
530
+ st.dataframe(risks_df, use_container_width=True)
531
+
532
+ # عرض مصفوفة المخاطر
533
+ st.markdown("##### مصفوفة المخاطر")
534
+
535
+ # إنشاء بيانات مصفوفة المخاطر
536
+ risk_matrix = np.zeros((5, 5))
537
+
538
+ # تعيين قيم المخاطر في المصفوفة
539
+ for risk in risks:
540
+ prob_index = min(int(risk["الاحتمالية"] / 20), 4)
541
+ impact_index = min(int(risk["التأثير"] / 20), 4)
542
+ risk_matrix[prob_index, impact_index] += 1
543
+
544
+ # تحويل المصفوفة إلى DataFrame
545
+ prob_labels = ["0-20%", "21-40%", "41-60%", "61-80%", "81-100%"]
546
+ impact_labels = ["منخفض جداً", "منخفض", "متوسط", "مرتفع", "مرتفع جداً"]
547
+
548
+ matrix_df = pd.DataFrame(risk_matrix, index=prob_labels[::-1], columns=impact_labels)
549
+
550
+ # عرض المصفوفة كخريطة حرارية
551
+ st.markdown("الاحتمالية (عمودي) × التأثير (أفقي)")
552
+ st.dataframe(matrix_df, use_container_width=True)
553
+
554
+ # عرض تكلفة المخاطر
555
+ st.markdown("##### تكلفة المخاطر")
556
+
557
+ # حساب تكلفة المخاطر
558
+ risk_cost = project_info['value'] * 0.05
559
+ contingency = project_info['value'] * 0.03
560
+ management_reserve = project_info['value'] * 0.02
561
+
562
+ col1, col2, col3 = st.columns(3)
563
+
564
+ with col1:
565
+ st.metric("تكلفة المخاطر المحددة", f"{risk_cost:,.2f} ريال")
566
+
567
+ with col2:
568
+ st.metric("احتياطي الطوارئ", f"{contingency:,.2f} ريال")
569
+
570
+ with col3:
571
+ st.metric("احتياطي الإدارة", f"{management_reserve:,.2f} ريال")
572
+
573
+ # عرض خطة الاستجابة للمخاطر
574
+ st.markdown("##### خطة الاستجابة للمخاطر")
575
+
576
+ for risk in risks:
577
+ if risk["المستوى"] == "مرتفع":
578
+ st.markdown(f"""
579
+ <div style="background-color: #f8d7da; padding: 10px; border-radius: 5px; margin-bottom: 10px;">
580
+ <strong>{risk['المخاطرة']} (مخاطرة مرتفعة):</strong> {risk['الاستجابة']}
581
+ </div>
582
+ """, unsafe_allow_html=True)
583
+ elif risk["المستوى"] == "متوسط":
584
+ st.markdown(f"""
585
+ <div style="background-color: #fff3cd; padding: 10px; border-radius: 5px; margin-bottom: 10px;">
586
+ <strong>{risk['المخاطرة']} (مخاطرة متوسطة):</strong> {risk['الاستجابة']}
587
+ </div>
588
+ """, unsafe_allow_html=True)
589
+
590
+ def _render_bill_of_quantities(self):
591
+ """
592
+ عرض واجهة جداول الكميات
593
+ """
594
+ st.markdown("### جداول الكميات")
595
+
596
+ # تبويبات فرعية لجداول الكميات
597
+ boq_tabs = st.tabs(["إنشاء جدول كميات", "قوالب جداول الكميات", "استيراد/تصدير"])
598
+
599
+ with boq_tabs[0]:
600
+ # إنشاء جدول كميات جديد
601
+ st.markdown("#### إنشاء جدول كميات جديد")
602
+
603
+ # نموذج إنشاء جدول كميات
604
+ col1, col2 = st.columns(2)
605
+
606
+ with col1:
607
+ boq_name = st.text_input("اسم جدول الكميات", key="new_boq_name")
608
+ boq_project = st.selectbox("المشروع", options=[p['name'] for p in st.session_state.pricing_projects], key="new_boq_project")
609
+
610
+ with col2:
611
+ boq_template = st.selectbox("القالب", options=["قالب جديد"] + [t['name'] for t in st.session_state.pricing_templates], key="new_boq_template")
612
+ boq_currency = st.selectbox("العملة", options=["ريال سعودي", "دولار أمريكي", "يورو"], key="new_boq_currency")
613
+
614
+ # زر إنشاء جدول الكميات
615
+ if st.button("إنشاء جدول الكميات", key="create_boq_btn"):
616
+ if boq_name:
617
+ st.success(f"تم إنشاء جدول الكميات '{boq_name}' بنجاح", icon="✅")
618
+
619
+ # عرض جدول الكميات الفارغ
620
+ st.markdown("#### جدول الكميات الجديد")
621
+
622
+ # إنشاء جدول فارغ
623
+ empty_boq = pd.DataFrame({
624
+ "الرقم": ["1", "2", "3", "4", "5"],
625
+ "البند": ["", "", "", "", ""],
626
+ "الوصف": ["", "", "", "", ""],
627
+ "الوحدة": ["", "", "", "", ""],
628
+ "الكمية": ["", "", "", "", ""],
629
+ "سعر الوحدة": ["", "", "", "", ""],
630
+ "الإجمالي": ["", "", "", "", ""]
631
+ })
632
+
633
+ st.dataframe(empty_boq, use_container_width=True, hide_index=True)
634
+
635
+ # أزرار التحكم
636
+ col1, col2, col3 = st.columns(3)
637
+
638
+ with col1:
639
+ st.button("إضافة بند", key="add_item_to_new_boq_btn")
640
+
641
+ with col2:
642
+ st.button("إضافة فئة", key="add_category_to_new_boq_btn")
643
+
644
+ with col3:
645
+ st.button("حفظ", key="save_new_boq_btn")
646
+ else:
647
+ st.warning("يرجى إدخال اسم لجدول الكميات", icon="⚠️")
648
+
649
+ with boq_tabs[1]:
650
+ # قوالب جداول الكميات
651
+ st.markdown("#### قوالب جداول الكميات")
652
+
653
+ # عرض القوالب المتاحة
654
+ templates = st.session_state.pricing_templates
655
+
656
+ # تقسيم القوالب إلى صفوف
657
+ for i in range(0, len(templates), 3):
658
+ cols = st.columns(3)
659
+ for j in range(3):
660
+ if i + j < len(templates):
661
+ template = templates[i + j]
662
+ with cols[j]:
663
+ st.markdown(f"""
664
+ <div style="border: 1px solid #dee2e6; border-radius: 8px; padding: 15px; height: 100%;">
665
+ <h4 style="color: var(--primary-color);">{template['name']}</h4>
666
+ <p><strong>النوع:</strong> {template['type']}</p>
667
+ <p><strong>عدد البنود:</strong> {template['items_count']}</p>
668
+ <p>{template['description']}</p>
669
+ </div>
670
+ """, unsafe_allow_html=True)
671
+
672
+ # إضافة قالب جديد
673
+ st.markdown("#### إضافة قالب جديد")
674
+
675
+ col1, col2 = st.columns(2)
676
+
677
+ with col1:
678
+ new_template_name = st.text_input("اسم القالب", key="new_template_name")
679
+ new_template_type = st.selectbox("نوع القالب", options=["طرق", "مباني", "بنية تحتية", "مياه وصرف صحي", "كهرباء", "أخرى"], key="new_template_type")
680
+
681
+ with col2:
682
+ new_template_desc = st.text_area("وصف القالب", key="new_template_desc")
683
+
684
+ # زر إضافة القالب
685
+ if st.button("إضافة القالب", key="add_template_btn"):
686
+ if new_template_name:
687
+ st.success(f"تم إضافة القالب '{new_template_name}' بنجاح", icon="✅")
688
+ else:
689
+ st.warning("يرجى إدخال اسم للقالب", icon="⚠️")
690
+
691
+ with boq_tabs[2]:
692
+ # استيراد/تصدير جداول الكميات
693
+ st.markdown("#### استيراد/تصدير جداول الكميات")
694
+
695
+ col1, col2 = st.columns(2)
696
+
697
+ with col1:
698
+ st.markdown("##### استيراد جدول كميات")
699
+ st.file_uploader("اختر ملف جدول الكميات (Excel, CSV)", type=["xlsx", "csv"], key="import_boq_file")
700
+
701
+ import_format = st.radio(
702
+ "تنسيق الاستيراد",
703
+ options=["تنسيق النظام", "تنسيق وزارة المالية", "تنسيق مخصص"],
704
+ horizontal=True,
705
+ key="import_boq_format"
706
+ )
707
+
708
+ st.button("استيراد", key="import_boq_file_btn")
709
+
710
+ with col2:
711
+ st.markdown("##### تصدير جدول كميات")
712
+
713
+ export_boq = st.selectbox(
714
+ "اختر جدول الكميات",
715
+ options=[p['name'] for p in st.session_state.pricing_projects],
716
+ key="export_boq_select"
717
+ )
718
+
719
+ export_format = st.radio(
720
+ "تنسيق التصدير",
721
+ options=["Excel", "CSV", "PDF"],
722
+ horizontal=True,
723
+ key="export_boq_format"
724
+ )
725
+
726
+ if st.button("تصدير", key="export_boq_btn"):
727
+ st.success(f"تم تصدير جدول الكميات '{export_boq}' بنجاح", icon="✅")
728
+
729
+ # زر تنزيل الملف المصدر (وهمي)
730
+ st.download_button(
731
+ label="تنزيل جدول الكميات",
732
+ data=b"محتوى وهمي لجدول الكميات",
733
+ file_name=f"{export_boq}.{export_format.lower()}",
734
+ mime="application/octet-stream",
735
+ key="download_boq"
736
+ )
737
+
738
+ def _render_price_analysis(self):
739
+ """
740
+ عرض واجهة تحليل الأسعار
741
+ """
742
+ st.markdown("### تحليل الأسعار")
743
+
744
+ # تبويبات فرعية لتحليل الأسعار
745
+ price_tabs = st.tabs(["تحليل أسعار المواد", "مقارنة الأسعار", "تحليل الاتجاهات", "تقارير"])
746
+
747
+ with price_tabs[0]:
748
+ # تحليل أسعار المواد
749
+ st.markdown("#### تحليل أسعار المواد")
750
+
751
+ # اختيار المواد للتحليل
752
+ materials = ["خرسانة", "حديد تسليح", "أسمنت", "رمل", "بلاط", "طابوق", "أسلاك كهربائية", "أنابيب"]
753
+ selected_materials = st.multiselect("اختر المواد للتحليل", options=materials, default=materials[:3], key="selected_materials")
754
+
755
+ if selected_materials:
756
+ # إنشاء بيانات تجريبية لأسعار المواد
757
+ price_data = {}
758
+ months = ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"]
759
+
760
+ for material in selected_materials:
761
+ # إنشاء سلسلة أسعار عشوائية مع اتجاه تصاعدي
762
+ base_price = random.randint(100, 1000)
763
+ prices = []
764
+ for i in range(12):
765
+ # إضافة تغير عشوائي مع اتجاه تصاعدي
766
+ change = random.uniform(-0.05, 0.1) * base_price
767
+ price = base_price + change
768
+ base_price = price # تحديث السعر الأساسي للشهر التالي
769
+ prices.append(price)
770
+
771
+ price_data[material] = prices
772
+
773
+ # تحويل البيانات إلى DataFrame
774
+ price_df = pd.DataFrame(price_data, index=months)
775
+
776
+ # عرض الرسم البياني
777
+ st.line_chart(price_df)
778
+
779
+ # عرض جدول الأسعار
780
+ st.dataframe(price_df, use_container_width=True)
781
+
782
+ # تحليل التغيرات
783
+ st.markdown("##### تحليل التغيرات في الأسعار")
784
+
785
+ for material in selected_materials:
786
+ prices = price_data[material]
787
+ first_price = prices[0]
788
+ last_price = prices[-1]
789
+ change = ((last_price - first_price) / first_price) * 100
790
+
791
+ if change > 0:
792
+ st.markdown(f"- **{material}**: ارتفاع بنسبة {change:.2f}% خلال الفترة")
793
+ else:
794
+ st.markdown(f"- **{material}**: انخفاض بنسبة {abs(change):.2f}% خلال الفترة")
795
+
796
+ with price_tabs[1]:
797
+ # مقارنة الأسعار
798
+ st.markdown("#### مقارنة الأسعار بين الموردين")
799
+
800
+ # اختيار المادة للمقارنة
801
+ material_for_comparison = st.selectbox("اختر المادة للمقارنة", options=materials, key="material_for_comparison")
802
+
803
+ # إنشاء بيانات تجريبية للموردين
804
+ suppliers = ["المورد أ", "المورد ب", "المورد ج", "المورد د", "المورد هـ"]
805
+
806
+ # إنشاء أسعار عشوائية للموردين
807
+ base_price = random.randint(100, 1000)
808
+ supplier_prices = [base_price * random.uniform(0.9, 1.1) for _ in range(len(suppliers))]
809
+
810
+ # تحويل البيانات إلى DataFrame
811
+ comparison_df = pd.DataFrame({
812
+ "المورد": suppliers,
813
+ "السعر": supplier_prices,
814
+ "الخصم": [f"{random.randint(0, 15)}%" for _ in range(len(suppliers))],
815
+ "شروط الدفع": [random.choice(["فوري", "30 يوم", "60 يوم", "90 يوم"]) for _ in range(len(suppliers))],
816
+ "وقت التسليم": [f"{random.randint(1, 30)} يوم" for _ in range(len(suppliers))]
817
+ })
818
+
819
+ # تنسيق القيم
820
+ comparison_df["السعر"] = comparison_df["السعر"].apply(lambda x: f"{x:.2f}")
821
+
822
+ # عرض جدول المقارنة
823
+ st.dataframe(comparison_df, use_container_width=True)
824
+
825
+ # عرض الرسم البياني للمقارنة
826
+ chart_data = pd.DataFrame({
827
+ "المورد": suppliers,
828
+ "السعر": supplier_prices
829
+ })
830
+
831
+ st.bar_chart(chart_data.set_index("المورد"))
832
+
833
+ # توصيات الشراء
834
+ st.markdown("##### توصيات الشراء")
835
+
836
+ best_supplier_index = supplier_prices.index(min(supplier_prices))
837
+ best_supplier = suppliers[best_supplier_index]
838
+
839
+ st.markdown(f"""
840
+ <div style="background-color: #d4edda; padding: 15px; border-radius: 8px; margin-top: 20px;">
841
+ <h5 style="color: #155724;">التوصية الأفضل</h5>
842
+ <p>بناءً على تحليل الأسعار وشروط التوريد، يوصى بالشراء من <strong>{best_supplier}</strong> حيث يقدم أفضل سعر مع شروط دفع وتسليم مناسبة.</p>
843
+ </div>
844
+ """, unsafe_allow_html=True)
845
+
846
+ with price_tabs[2]:
847
+ # تحليل الاتجاهات
848
+ st.markdown("#### تحليل اتجاهات الأسعار")
849
+
850
+ # اختيار الفترة الزمنية
851
+ time_period = st.radio(
852
+ "الفترة الزمنية",
853
+ options=["شهري", "ربع سنوي", "سنوي"],
854
+ horizontal=True,
855
+ key="price_trend_period"
856
+ )
857
+
858
+ # إنشاء بيانات تجريبية للاتجاهات
859
+ if time_period == "شهري":
860
+ periods = months
861
+ elif time_period == "ربع سنوي":
862
+ periods = ["الربع الأول", "الربع الثاني", "الربع الثالث", "الربع الرابع"]
863
+ else:
864
+ periods = [f"20{year}" for year in range(18, 26)]
865
+
866
+ # إنشاء مؤشر أسعار تجريبي
867
+ base_index = 100
868
+ price_indices = []
869
+
870
+ for i in range(len(periods)):
871
+ # إضافة تغير عشوائي مع اتجاه تصاعدي
872
+ change = random.uniform(-2, 5)
873
+ index_value = base_index + change
874
+ base_index = index_value
875
+ price_indices.append(index_value)
876
+
877
+ # تحويل البيانات إلى DataFrame
878
+ trend_df = pd.DataFrame({
879
+ "الفترة": periods,
880
+ "مؤشر الأسعار": price_indices
881
+ })
882
+
883
+ # عرض الرسم البياني للاتجاهات
884
+ st.line_chart(trend_df.set_index("الفترة"))
885
+
886
+ # تحليل الاتجاه
887
+ first_index = price_indices[0]
888
+ last_index = price_indices[-1]
889
+ total_change = ((last_index - first_index) / first_index) * 100
890
+
891
+ st.markdown(f"##### تحليل الاتجاه العام")
892
+
893
+ if total_change > 10:
894
+ trend_message = "ارتفاع حاد في الأسعار"
895
+ recommendation = "يوصى بالشراء المبكر وتخزين المواد الأساسية لتجنب الزيادات المستقبلية"
896
+ elif total_change > 5:
897
+ trend_message = "ارتفاع معتدل في الأسعار"
898
+ recommendation = "يوصى بمراقبة الأسعار عن كثب والشراء عند انخفاض الأسعار مؤقتاً"
899
+ elif total_change > 0:
900
+ trend_message = "ارتفاع طفيف في الأسعار"
901
+ recommendation = "الوضع مستقر نسبياً، يمكن الشراء وفق الجدول الزمني للمشروع"
902
+ elif total_change > -5:
903
+ trend_message = "انخفاض طفيف في الأسعار"
904
+ recommendation = "يمكن تأجيل بعض المشتريات للاستفادة من انخفاض الأسعار المتوقع"
905
+ else:
906
+ trend_message = "انخفاض ملحوظ في الأسعار"
907
+ recommendation = "يوصى بتأجيل المشتريات غير العاجلة للاستفادة من انخفاض الأسعار"
908
+
909
+ st.markdown(f"""
910
+ <div style="background-color: #f8f9fa; padding: 15px; border-radius: 8px; margin-top: 20px;">
911
+ <h5>الاتجاه العام: {trend_message}</h5>
912
+ <p>التغير الإجمالي: {total_change:.2f}% خلال الفترة</p>
913
+ <p><strong>التوصية:</strong> {recommendation}</p>
914
+ </div>
915
+ """, unsafe_allow_html=True)
916
+
917
+ with price_tabs[3]:
918
+ # تقارير تحليل الأسعار
919
+ st.markdown("#### تقارير تحليل الأسعار")
920
+
921
+ # قائمة التقارير المتاحة
922
+ reports = [
923
+ "تقرير مقارنة الأسعار الشهري",
924
+ "تقرير تحليل اتجاهات الأسعار",
925
+ "تقرير أسعار المواد الرئيسية",
926
+ "تقرير مقارنة أسعار الموردين",
927
+ "تقرير تحليل تكاليف المشاريع"
928
+ ]
929
+
930
+ selected_report = st.selectbox("اختر التقرير", options=reports, key="selected_price_report")
931
+
932
+ # خيارات التقرير
933
+ col1, col2 = st.columns(2)
934
+
935
+ with col1:
936
+ report_format = st.radio(
937
+ "تنسيق التقرير",
938
+ options=["PDF", "Excel", "Word"],
939
+ horizontal=True,
940
+ key="price_report_format"
941
+ )
942
+
943
+ with col2:
944
+ report_period = st.selectbox(
945
+ "الفترة",
946
+ options=["الشهر الحالي", "الربع الحالي", "النصف الأول 2025", "النصف الثاني 2025", "السنة كاملة"],
947
+ key="price_report_period"
948
+ )
949
+
950
+ # زر إنشاء التقرير
951
+ if st.button("إنشاء التقرير", key="generate_price_report_btn"):
952
+ st.success(f"تم إنشاء التقرير '{selected_report}' بنجاح", icon="✅")
953
+
954
+ # زر تنزيل التقرير (وهمي)
955
+ st.download_button(
956
+ label="تنزيل التقرير",
957
+ data=b"محتوى وهمي للتقرير",
958
+ file_name=f"{selected_report}.{report_format.lower()}",
959
+ mime="application/octet-stream",
960
+ key="download_price_report"
961
+ )
962
+
963
+ def _render_pricing_templates(self):
964
+ """
965
+ عرض واجهة قوالب التسعير
966
+ """
967
+ st.markdown("### قوالب التسعير")
968
+
969
+ # تبويبات فرعية لقوالب التسعير
970
+ template_tabs = st.tabs(["القوالب المتاحة", "إنشاء قالب جديد", "إدارة القوالب"])
971
+
972
+ with template_tabs[0]:
973
+ # عرض القوالب المتاحة
974
+ st.markdown("#### القوالب المتاحة")
975
+
976
+ # فلترة القوالب
977
+ template_type_filter = st.selectbox(
978
+ "تصفية حسب النوع",
979
+ options=["الكل", "طرق", "مباني", "بنية تحتية", "مياه وصرف صحي", "كهرباء", "أخرى"],
980
+ key="template_type_filter"
981
+ )
982
+
983
+ # تطبيق التصفية
984
+ filtered_templates = st.session_state.pricing_templates
985
+
986
+ if template_type_filter != "الكل":
987
+ filtered_templates = [t for t in filtered_templates if t['type'] == template_type_filter]
988
+
989
+ # عرض القوالب
990
+ if filtered_templates:
991
+ # تقسيم القوالب إلى صفوف
992
+ for i in range(0, len(filtered_templates), 3):
993
+ cols = st.columns(3)
994
+ for j in range(3):
995
+ if i + j < len(filtered_templates):
996
+ template = filtered_templates[i + j]
997
+ with cols[j]:
998
+ st.markdown(f"""
999
+ <div style="border: 1px solid #dee2e6; border-radius: 8px; padding: 15px; height: 100%;">
1000
+ <h4 style="color: var(--primary-color);">{template['name']}</h4>
1001
+ <p><strong>النوع:</strong> {template['type']}</p>
1002
+ <p><strong>عدد البنود:</strong> {template['items_count']}</p>
1003
+ <p>{template['description']}</p>
1004
+ <button style="background-color: var(--primary-color); color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer;">استخدام القالب</button>
1005
+ </div>
1006
+ """, unsafe_allow_html=True)
1007
+ else:
1008
+ st.info("لا توجد قوالب تطابق معايير التصفية", icon="ℹ️")
1009
+
1010
+ with template_tabs[1]:
1011
+ # إنشاء قالب جديد
1012
+ st.markdown("#### إنشاء قالب جديد")
1013
+
1014
+ # نموذج إنشاء قالب
1015
+ col1, col2 = st.columns(2)
1016
+
1017
+ with col1:
1018
+ new_template_name = st.text_input("اسم القالب", key="create_template_name")
1019
+ new_template_type = st.selectbox("نوع القالب", options=["طرق", "مباني", "بنية تحتية", "مياه وصرف صحي", "كهرباء", "أخرى"], key="create_template_type")
1020
+
1021
+ with col2:
1022
+ new_template_desc = st.text_area("وصف القالب", key="create_template_desc")
1023
+ new_template_base = st.selectbox("القالب الأساسي", options=["قالب فارغ"] + [t['name'] for t in st.session_state.pricing_templates], key="create_template_base")
1024
+
1025
+ # زر إنشاء القالب
1026
+ if st.button("إنشاء القالب", key="create_template_btn"):
1027
+ if new_template_name:
1028
+ st.success(f"تم إنشاء القالب '{new_template_name}' بنجاح", icon="✅")
1029
+
1030
+ # عرض محرر القالب
1031
+ st.markdown("#### محرر القالب")
1032
+
1033
+ # إنشاء جدول فارغ
1034
+ empty_template = pd.DataFrame({
1035
+ "الرقم": ["1", "2", "3", "4", "5"],
1036
+ "البند": ["", "", "", "", ""],
1037
+ "الوصف": ["", "", "", "", ""],
1038
+ "الوحدة": ["", "", "", "", ""],
1039
+ "ملاحظات": ["", "", "", "", ""]
1040
+ })
1041
+
1042
+ st.dataframe(empty_template, use_container_width=True, hide_index=True)
1043
+
1044
+ # أزرار التحكم
1045
+ col1, col2, col3 = st.columns(3)
1046
+
1047
+ with col1:
1048
+ st.button("إضافة بند", key="add_item_to_template_btn")
1049
+
1050
+ with col2:
1051
+ st.button("إضافة فئة", key="add_category_to_template_btn")
1052
+
1053
+ with col3:
1054
+ st.button("حفظ القالب", key="save_template_btn")
1055
+ else:
1056
+ st.warning("يرجى إدخال اسم للقالب", icon="⚠️")
1057
+
1058
+ with template_tabs[2]:
1059
+ # إدارة القوالب
1060
+ st.markdown("#### إدارة القوالب")
1061
+
1062
+ # عرض قائمة القوالب
1063
+ templates_df = pd.DataFrame({
1064
+ "اسم القالب": [t['name'] for t in st.session_state.pricing_templates],
1065
+ "النوع": [t['type'] for t in st.session_state.pricing_templates],
1066
+ "عدد البنود": [t['items_count'] for t in st.session_state.pricing_templates],
1067
+ "تاريخ الإنشاء": [t['created_at'] for t in st.session_state.pricing_templates],
1068
+ "آخر تحديث": [t['updated_at'] for t in st.session_state.pricing_templates]
1069
+ })
1070
+
1071
+ st.dataframe(templates_df, use_container_width=True)
1072
+
1073
+ # خيارات إدارة القوالب
1074
+ st.markdown("##### خيارات الإدارة")
1075
+
1076
+ col1, col2, col3 = st.columns(3)
1077
+
1078
+ with col1:
1079
+ st.button("تعديل القالب المحدد", key="edit_template_btn")
1080
+
1081
+ with col2:
1082
+ st.button("نسخ القالب المحدد", key="duplicate_template_btn")
1083
+
1084
+ with col3:
1085
+ st.button("حذف القالب المحدد", key="delete_template_btn")
1086
+
1087
+ # استيراد وتصدير القوالب
1088
+ st.markdown("##### استيراد وتصدير القوالب")
1089
+
1090
+ col1, col2 = st.columns(2)
1091
+
1092
+ with col1:
1093
+ st.file_uploader("استيراد قالب", type=["xlsx", "json"], key="import_template_file")
1094
+ st.button("استيراد", key="import_template_btn")
1095
+
1096
+ with col2:
1097
+ export_template = st.selectbox(
1098
+ "تصدير قالب",
1099
+ options=[t['name'] for t in st.session_state.pricing_templates],
1100
+ key="export_template_select"
1101
+ )
1102
+
1103
+ export_template_format = st.radio(
1104
+ "تنسيق التصدير",
1105
+ options=["Excel", "JSON"],
1106
+ horizontal=True,
1107
+ key="export_template_format"
1108
+ )
1109
+
1110
+ if st.button("تصدير", key="export_template_btn"):
1111
+ st.success(f"تم تصدير القالب '{export_template}' بنجاح", icon="✅")
1112
+
1113
+ # زر تنزيل القالب المصدر (وهمي)
1114
+ st.download_button(
1115
+ label="تنزيل القالب",
1116
+ data=b"محتوى وهمي للقالب",
1117
+ file_name=f"{export_template}.{export_template_format.lower()}",
1118
+ mime="application/octet-stream",
1119
+ key="download_template"
1120
+ )
1121
+
1122
+ def _generate_sample_projects(self):
1123
+ """
1124
+ إنشاء بيانات تجريبية للمشاريع
1125
+ """
1126
+ projects = [
1127
+ {
1128
+ 'id': 1,
1129
+ 'name': 'مشروع تطوير الطرق في منطقة الرياض',
1130
+ 'client': 'وزارة النقل',
1131
+ 'value': 25000000,
1132
+ 'deadline': '2025-05-15',
1133
+ 'status': 'قيد التسعير',
1134
+ 'completion': 75
1135
+ },
1136
+ {
1137
+ 'id': 2,
1138
+ 'name': 'مشروع إنشاء مبنى إداري',
1139
+ 'client': 'وزارة الإسكان',
1140
+ 'value': 15000000,
1141
+ 'deadline': '2025-04-30',
1142
+ 'status': 'قيد التسعير',
1143
+ 'completion': 90
1144
+ },
1145
+ {
1146
+ 'id': 3,
1147
+ 'name': 'مشروع تطوير شبكة الصرف الصحي',
1148
+ 'client': 'أمانة منطقة الرياض',
1149
+ 'value': 18000000,
1150
+ 'deadline': '2025-05-10',
1151
+ 'status': 'تم التسعير',
1152
+ 'completion': 100
1153
+ },
1154
+ {
1155
+ 'id': 4,
1156
+ 'name': 'مشروع إنشاء جسر',
1157
+ 'client': 'وزارة النقل',
1158
+ 'value': 30000000,
1159
+ 'deadline': '2025-06-20',
1160
+ 'status': 'قيد التسعير',
1161
+ 'completion': 40
1162
+ },
1163
+ {
1164
+ 'id': 5,
1165
+ 'name': 'مشروع تطوير شبكة المياه',
1166
+ 'client': 'وزارة المياه',
1167
+ 'value': 12000000,
1168
+ 'deadline': '2025-04-25',
1169
+ 'status': 'تم التسعير',
1170
+ 'completion': 100
1171
+ },
1172
+ {
1173
+ 'id': 6,
1174
+ 'name': 'مشروع إنشاء مدرسة',
1175
+ 'client': 'وزارة التعليم',
1176
+ 'value': 8000000,
1177
+ 'deadline': '2025-05-05',
1178
+ 'status': 'تمت الترسية',
1179
+ 'completion': 100
1180
+ },
1181
+ {
1182
+ 'id': 7,
1183
+ 'name': 'مشروع تطوير شبكة الكهرباء',
1184
+ 'client': 'شركة الكهرباء السعودية',
1185
+ 'value': 20000000,
1186
+ 'deadline': '2025-06-10',
1187
+ 'status': 'مرفوضة',
1188
+ 'completion': 100
1189
+ },
1190
+ {
1191
+ 'id': 8,
1192
+ 'name': 'مشروع إنشاء مستشفى',
1193
+ 'client': 'وزارة الصحة',
1194
+ 'value': 40000000,
1195
+ 'deadline': '2025-07-15',
1196
+ 'status': 'قيد التسعير',
1197
+ 'completion': 30
1198
+ },
1199
+ {
1200
+ 'id': 9,
1201
+ 'name': 'مشروع تطوير حديقة عامة',
1202
+ 'client': 'أمانة منطقة الرياض',
1203
+ 'value': 5000000,
1204
+ 'deadline': '2025-04-20',
1205
+ 'status': 'تمت الترسية',
1206
+ 'completion': 100
1207
+ },
1208
+ {
1209
+ 'id': 10,
1210
+ 'name': 'مشروع إنشاء مركز تجاري',
1211
+ 'client': 'شركة تطوير العقارية',
1212
+ 'value': 35000000,
1213
+ 'deadline': '2025-08-10',
1214
+ 'status': 'قيد التسعير',
1215
+ 'completion': 20
1216
+ }
1217
+ ]
1218
+
1219
+ return projects
1220
+
1221
+ def _generate_sample_templates(self):
1222
+ """
1223
+ إنشاء بيانات تجريبية لقوالب التسعير
1224
+ """
1225
+ templates = [
1226
+ {
1227
+ 'id': 1,
1228
+ 'name': 'قالب مشاريع الطرق',
1229
+ 'type': 'طرق',
1230
+ 'items_count': 120,
1231
+ 'description': 'قالب شامل لمشاريع الطرق والجسور يتضمن جميع البنود القياسية',
1232
+ 'created_at': '2024-10-15',
1233
+ 'updated_at': '2025-01-20'
1234
+ },
1235
+ {
1236
+ 'id': 2,
1237
+ 'name': 'قالب المباني الإدارية',
1238
+ 'type': 'مباني',
1239
+ 'items_count': 150,
1240
+ 'description': 'قالب متكامل للمباني الإدارية والتجارية مع تفاصيل التشطيبات',
1241
+ 'created_at': '2024-11-05',
1242
+ 'updated_at': '2025-02-10'
1243
+ },
1244
+ {
1245
+ 'id': 3,
1246
+ 'name': 'قالب شبكات المياه',
1247
+ 'type': 'مياه وصرف صحي',
1248
+ 'items_count': 85,
1249
+ 'description': 'قالب لمشاريع شبكات المياه والصرف الصحي مع المواصفات القياسية',
1250
+ 'created_at': '2024-09-20',
1251
+ 'updated_at': '2025-01-15'
1252
+ },
1253
+ {
1254
+ 'id': 4,
1255
+ 'name': 'قالب الأعمال الكهربائية',
1256
+ 'type': 'كهرباء',
1257
+ 'items_count': 95,
1258
+ 'description': 'قالب للأعمال الكهربائية في المشاريع المختلفة',
1259
+ 'created_at': '2024-12-10',
1260
+ 'updated_at': '2025-03-05'
1261
+ },
1262
+ {
1263
+ 'id': 5,
1264
+ 'name': 'قالب البنية التحتية',
1265
+ 'type': 'بنية تحتية',
1266
+ 'items_count': 110,
1267
+ 'description': 'قالب شامل لمشاريع البنية التحتية والمرافق العامة',
1268
+ 'created_at': '2024-10-25',
1269
+ 'updated_at': '2025-02-15'
1270
+ },
1271
+ {
1272
+ 'id': 6,
1273
+ 'name': 'قالب المدارس',
1274
+ 'type': 'مباني',
1275
+ 'items_count': 130,
1276
+ 'description': 'قالب متخصص لمشاريع المدارس والمنشآت التعليمية',
1277
+ 'created_at': '2024-11-15',
1278
+ 'updated_at': '2025-01-25'
1279
+ }
1280
+ ]
1281
+
1282
+ return templates
1283
+
1284
+ def _generate_sample_resources(self):
1285
+ """
1286
+ إنشاء بيانات تجريبية للموارد
1287
+ """
1288
+ resources = {
1289
+ 'materials': [
1290
+ {'id': 1, 'name': 'خرسانة جاهزة', 'unit': 'متر مكعب', 'price': 250, 'last_update': '2025-03-15'},
1291
+ {'id': 2, 'name': 'حديد تسليح', 'unit': 'طن', 'price': 3000, 'last_update': '2025-03-10'},
1292
+ {'id': 3, 'name': 'طابوق', 'unit': 'قطعة', 'price': 5, 'last_update': '2025-03-05'},
1293
+ {'id': 4, 'name': 'بلاط', 'unit': 'متر مربع', 'price': 80, 'last_update': '2025-03-12'},
1294
+ {'id': 5, 'name': 'أسمنت', 'unit': 'كيس', 'price': 20, 'last_update': '2025-03-08'}
1295
+ ],
1296
+ 'labor': [
1297
+ {'id': 1, 'name': 'مهندس موقع', 'unit': 'شهر', 'price': 15000, 'last_update': '2025-02-20'},
1298
+ {'id': 2, 'name': 'مشرف', 'unit': 'شهر', 'price': 8000, 'last_update': '2025-02-20'},
1299
+ {'id': 3, 'name': 'فني', 'unit': 'شهر', 'price': 5000, 'last_update': '2025-02-20'},
1300
+ {'id': 4, 'name': 'عامل', 'unit': 'شهر', 'price': 3000, 'last_update': '2025-02-20'},
1301
+ {'id': 5, 'name': 'سائق', 'unit': 'شهر', 'price': 4000, 'last_update': '2025-02-20'}
1302
+ ],
1303
+ 'equipment': [
1304
+ {'id': 1, 'name': 'حفارة', 'unit': 'شهر', 'price': 20000, 'last_update': '2025-02-15'},
1305
+ {'id': 2, 'name': 'لودر', 'unit': 'شهر', 'price': 15000, 'last_update': '2025-02-15'},
1306
+ {'id': 3, 'name': 'شاحنة نقل', 'unit': 'شهر', 'price': 10000, 'last_update': '2025-02-15'},
1307
+ {'id': 4, 'name': 'خلاطة خرسانة', 'unit': 'شهر', 'price': 8000, 'last_update': '2025-02-15'},
1308
+ {'id': 5, 'name': 'رافعة', 'unit': 'شهر', 'price': 25000, 'last_update': '2025-02-15'}
1309
+ ]
1310
+ }
1311
+
1312
+ return resources
modules/pricing/pricing_app.py.backup ADDED
@@ -0,0 +1,1242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 random
13
+ import os
14
+ import time
15
+ import io
16
+
17
+ from modules.pricing.services.standard_pricing import StandardPricing
18
+ from modules.pricing.services.unbalanced_pricing import UnbalancedPricing
19
+ from modules.pricing.services.local_content_calculator import LocalContentCalculator
20
+ from modules.pricing.services.price_prediction import PricePrediction
21
+ from utils.excel_handler import export_to_excel
22
+ from utils.helpers import format_number, format_currency
23
+
24
+
25
+ class PricingApp:
26
+ """وحدة التسعير المتكاملة"""
27
+
28
+ def __init__(self):
29
+ """تهيئة وحدة التسعير المتكاملة"""
30
+ self.pricing_methods = [
31
+ "التسعير القياسي",
32
+ "التسعير غير المتزن",
33
+ "التسعير التنافسي",
34
+ "التسعير الموجه بالربحية"
35
+ ]
36
+
37
+ # تهيئة خدمات التسعير
38
+ self.standard_pricing = StandardPricing()
39
+ self.unbalanced_pricing = UnbalancedPricing()
40
+ self.local_content = LocalContentCalculator()
41
+ self.price_prediction = PricePrediction()
42
+
43
+ def render(self):
44
+ """عرض واجهة وحدة التسعير"""
45
+
46
+ st.markdown("<h1 class='module-title'>وحدة التسعير المتكاملة</h1>", unsafe_allow_html=True)
47
+
48
+ tabs = st.tabs([
49
+ "إنشاء تسعير جديد",
50
+ "تحليل سعر البند",
51
+ "نموذج التسعير الشامل",
52
+ "التسعير غير المتزن",
53
+ "المحتوى المحلي"
54
+ ])
55
+
56
+ with tabs[0]:
57
+ self._render_new_pricing_tab()
58
+
59
+ with tabs[1]:
60
+ self._render_item_analysis_tab()
61
+
62
+ with tabs[2]:
63
+ self._render_comprehensive_pricing_tab()
64
+
65
+ with tabs[3]:
66
+ self._render_unbalanced_pricing_tab()
67
+
68
+ with tabs[4]:
69
+ self._render_local_content_tab()
70
+
71
+ def _render_item_analysis_tab(self):
72
+ """عرض تبويب تحليل سعر البند"""
73
+
74
+ st.markdown("### تحليل سعر البند")
75
+
76
+ # التحقق من وجود تسعير حالي
77
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
78
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
79
+ return
80
+
81
+ # اختيار البند للتحليل
82
+ if 'current_pricing' in st.session_state and st.session_state.current_pricing is not None:
83
+ items = st.session_state.current_pricing['items']
84
+ item_options = items['رقم البند'].tolist()
85
+ selected_item = st.selectbox("اختر البند للتحليل", item_options)
86
+
87
+ if selected_item:
88
+ item_data = items[items['رقم البند'] == selected_item].iloc[0]
89
+
90
+ st.markdown(f"### تحليل البند: {selected_item}")
91
+ st.markdown(f"**وصف البند**: {item_data['وصف البند']}")
92
+ st.markdown(f"**الوحدة**: {item_data['الوحدة']}")
93
+ st.markdown(f"**الكمية**: {item_data['الكمية']}")
94
+ st.markdown(f"**سعر الوحدة**: {item_data['سعر الوحدة']:,.2f} ريال")
95
+
96
+ # تحليل مكونات السعر
97
+ st.markdown("### تحليل مكونات السعر")
98
+
99
+ # عناصر التكلفة الافتراضية
100
+ cost_components = {
101
+ 'المواد': 0.6, # 60% من التكلفة
102
+ 'العمالة': 0.25, # 25% من التكلفة
103
+ 'المعدات': 0.1, # 10% من التكلفة
104
+ 'نفقات عامة': 0.05 # 5% من التكلفة
105
+ }
106
+
107
+ # حساب تكلفة كل عنصر
108
+ unit_price = item_data['سعر الوحدة']
109
+ component_values = {k: v * unit_price for k, v in cost_components.items()}
110
+
111
+ # عرض مكونات التكلفة في جدول
112
+ components_df = pd.DataFrame({
113
+ 'العنصر': component_values.keys(),
114
+ 'نسبة من التكلفة': [f"{v*100:.1f}%" for v in cost_components.values()],
115
+ 'القيمة (ريال)': [f"{v:,.2f}" for v in component_values.values()]
116
+ })
117
+
118
+ st.table(components_df)
119
+
120
+ # رسم بياني لمكونات التكلفة
121
+ fig = px.pie(
122
+ names=list(component_values.keys()),
123
+ values=list(component_values.values()),
124
+ title='توزيع مكونات التكلفة'
125
+ )
126
+
127
+ st.plotly_chart(fig)
128
+
129
+ # تحليل تاريخي للأسعار
130
+ st.markdown("### تحليل تاريخي للأسعار")
131
+
132
+ # بيانات تاريخية افتراضية
133
+ historical_data = {
134
+ 'التاريخ': ['2020-01', '2020-07', '2021-01', '2021-07', '2022-01', '2022-07', '2023-01', '2023-07'],
135
+ 'السعر': [
136
+ unit_price * 0.7,
137
+ unit_price * 0.75,
138
+ unit_price * 0.8,
139
+ unit_price * 0.85,
140
+ unit_price * 0.9,
141
+ unit_price * 0.95,
142
+ unit_price,
143
+ unit_price * 1.05
144
+ ]
145
+ }
146
+
147
+ hist_df = pd.DataFrame(historical_data)
148
+
149
+ # رسم بياني للتحليل التاريخي
150
+ fig = px.line(
151
+ hist_df,
152
+ x='التاريخ',
153
+ y='السعر',
154
+ title='تطور سعر الوحدة عبر الزمن',
155
+ markers=True
156
+ )
157
+
158
+ st.plotly_chart(fig)
159
+
160
+ # المقارنة مع الأسعار المرجعية
161
+ st.markdown("### المقارنة مع الأسعار المرجعية")
162
+
163
+ # بيانات مرجعية افتراضية
164
+ reference_data = {
165
+ 'المصدر': ['قاعدة البيانات الداخلية', 'دليل الأسعار الاسترشادي', 'متوسط أسعار السوق', 'أسعار المشاريع المماثلة'],
166
+ 'السعر المرجعي': [
167
+ unit_price * 0.95,
168
+ unit_price * 1.05,
169
+ unit_price * 1.1,
170
+ unit_price * 0.9
171
+ ]
172
+ }
173
+
174
+ ref_df = pd.DataFrame(reference_data)
175
+ ref_df['الفرق عن السعر الحالي'] = ref_df['السعر المرجعي'] - unit_price
176
+ ref_df['نسبة الفرق'] = (ref_df['الفرق عن السعر الحالي'] / unit_price * 100).round(2).astype(str) + '%'
177
+
178
+ st.table(ref_df)
179
+
180
+ def _render_new_pricing_tab(self):
181
+ """عرض تبويب إنشاء تسعير جديد"""
182
+
183
+ st.markdown("### إنشاء تسعير جديد")
184
+
185
+ col1, col2 = st.columns(2)
186
+
187
+ with col1:
188
+ tender_name = st.text_input("اسم المناقصة")
189
+ client = st.text_input("الجهة المالكة")
190
+ pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods)
191
+
192
+ with col2:
193
+ tender_number = st.text_input("رقم المناقصة")
194
+ location = st.text_input("الموقع")
195
+ submission_date = st.date_input("تاريخ التقديم")
196
+
197
+ # خيارات بيانات البنود
198
+ st.markdown("### بيانات البنود")
199
+
200
+ data_source = st.radio(
201
+ "مصدر بيانات البنود",
202
+ ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات"]
203
+ )
204
+
205
+ if data_source == "إدخال يدوي":
206
+ # ضبط CSS لتحسين ظهور الواجهة العربية
207
+ st.markdown("""
208
+ <style>
209
+ input, .stTextArea textarea {
210
+ direction: rtl;
211
+ text-align: right;
212
+ font-family: 'Arial', 'Tahoma', sans-serif !important;
213
+ }
214
+ .stTextInput > div > div > input {
215
+ text-align: right;
216
+ direction: rtl;
217
+ }
218
+ </style>
219
+ """, unsafe_allow_html=True)
220
+
221
+ # تهيئة قائمة الوحدات المتاحة
222
+ unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
223
+
224
+ # إنشاء بيانات افتراضية إذا لم تكن موجودة
225
+ if 'manual_items' not in st.session_state:
226
+ # إنشاء DataFrame فارغ
227
+ manual_items = pd.DataFrame(columns=[
228
+ 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي'
229
+ ])
230
+
231
+ # إضافة بضعة صفوف افتراضية
232
+ default_items = pd.DataFrame({
233
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5"],
234
+ 'وصف البند': [
235
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
236
+ "توريد وتركيب حديد التسليح للأساسات",
237
+ "أعمال العزل المائي للأساسات",
238
+ "أعمال الردم والدك للأساسات",
239
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
240
+ ],
241
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
242
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0],
243
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
244
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
245
+ })
246
+
247
+ manual_items = pd.concat([manual_items, default_items])
248
+ st.session_state.manual_items = manual_items
249
+
250
+ # عرض واجهة إدخال البنود
251
+ st.markdown("### إدخال تفاصيل البنود")
252
+
253
+ # التحقق من استخدام طريقة الإدخال البسيطة
254
+ use_simple_input = st.checkbox("استخدام طريقة الإدخال البسيطة", value=True)
255
+
256
+ if use_simple_input:
257
+ # عرض البنود الحالية كجدول للعرض فقط
258
+ st.markdown("### جدول البنود الحالية")
259
+ st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
260
+
261
+ # إضافة بند جديد
262
+ st.markdown("### إضافة بند جديد")
263
+ col1, col2 = st.columns(2)
264
+
265
+ with col1:
266
+ new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}")
267
+ new_desc = st.text_area("وصف البند", value="")
268
+
269
+ with col2:
270
+ new_unit = st.selectbox("الوحدة", options=unit_options)
271
+ new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f")
272
+ new_price = st.number_input("سعر الوحدة", value=0.0, min_value=0.0, format="%.2f")
273
+
274
+ new_total = new_qty * new_price
275
+ st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال")
276
+
277
+ if st.button("إضافة البند"):
278
+ # التحقق من صحة البيانات
279
+ if new_id and new_desc and new_qty > 0:
280
+ # إنشاء صف جديد
281
+ new_row = pd.DataFrame({
282
+ 'رقم البند': [new_id],
283
+ 'وصف البند': [new_desc],
284
+ 'الوحدة': [new_unit],
285
+ 'الكمية': [float(new_qty)],
286
+ 'سعر الوحدة': [float(new_price)],
287
+ 'الإجمالي': [float(new_total)]
288
+ })
289
+
290
+ # إضافة الصف إلى DataFrame
291
+ st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True)
292
+ st.success("تم إضافة البند بنجاح!")
293
+ st.rerun()
294
+ else:
295
+ st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.")
296
+
297
+ # تعديل البنود الحالية
298
+ st.markdown("### تعديل البنود الحالية")
299
+
300
+ # تحديد البند المراد تعديله
301
+ item_to_edit = st.selectbox(
302
+ "اختر البند للتعديل",
303
+ options=st.session_state.manual_items['رقم البند'].tolist(),
304
+ format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}..."
305
+ )
306
+
307
+ if item_to_edit:
308
+ # الحصول على مؤشر الصف للبند المحدد
309
+ idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == item_to_edit].index[0]
310
+ row = st.session_state.manual_items.loc[idx]
311
+
312
+ # إنشاء نموذج تعديل
313
+ col1, col2 = st.columns(2)
314
+
315
+ with col1:
316
+ edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id")
317
+ edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc")
318
+
319
+ with col2:
320
+ edited_unit = st.selectbox(
321
+ "الوحدة (تعديل)",
322
+ options=unit_options,
323
+ index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0,
324
+ key="edit_unit"
325
+ )
326
+ edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty")
327
+ edited_price = st.number_input("سعر الوحدة (تعديل)", value=float(row['سعر الوحدة']), min_value=0.0, format="%.2f", key="edit_price")
328
+
329
+ edited_total = edited_qty * edited_price
330
+ st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال")
331
+
332
+ col1, col2 = st.columns(2)
333
+ with col1:
334
+ if st.button("حفظ التعديلات"):
335
+ # تحديث البند
336
+ st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id
337
+ st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc
338
+ st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit
339
+ st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty
340
+ st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price
341
+ st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total
342
+
343
+ st.success("تم تحديث البند بنجاح!")
344
+ st.rerun()
345
+
346
+ with col2:
347
+ if st.button("حذف هذا البند"):
348
+ st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True)
349
+ st.warning("تم حذف البند!")
350
+ st.rerun()
351
+
352
+ # المجموع الكلي
353
+ total = st.session_state.manual_items['الإجمالي'].sum()
354
+ st.metric("المجموع الكلي", f"{total:,.2f} ريال")
355
+
356
+ # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية
357
+ edited_items = st.session_state.manual_items.copy()
358
+
359
+ else:
360
+ # عرض رسالة توضح أن طريقة الإدخال البسيطة هي الأفضل
361
+ st.warning("لتجنب مشاكل عدم التوافق في أنواع البيانات، يُفضل استخدام طريقة الإدخال البسيطة.")
362
+
363
+ # محاولة استخدام المحرر القياسي مع معالجة الأخطاء
364
+ try:
365
+ # تحويل البيانات إلى الأنواع المناسبة
366
+ for col in st.session_state.manual_items.columns:
367
+ if col in ['رقم البند', 'وصف البند', 'الوحدة']:
368
+ st.session_state.manual_items[col] = st.session_state.manual_items[col].astype(str)
369
+
370
+ # عرض المحرر (للقراءة فقط)
371
+ st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
372
+
373
+ # إنشاء نظام تعديل منفصل
374
+ st.markdown("### تعديل أسعار الوحدات")
375
+
376
+ for idx, row in st.session_state.manual_items.iterrows():
377
+ col1, col2 = st.columns([3, 1])
378
+
379
+ with col1:
380
+ st.text(f"{row['رقم البند']}: {row['وصف البند'][:50]}")
381
+
382
+ with col2:
383
+ price = st.number_input(
384
+ f"سعر الوحدة ({row['الوحدة']})",
385
+ value=float(row['سعر الوحدة']),
386
+ min_value=0.0,
387
+ key=f"price_{idx}"
388
+ )
389
+
390
+ # تحديث السعر والإجمالي
391
+ st.session_state.manual_items.at[idx, 'سعر الوحدة'] = price
392
+ st.session_state.manual_items.at[idx, 'الإجمالي'] = price * row['الكمية']
393
+
394
+ # المجموع الكلي
395
+ total = st.session_state.manual_items['الإجمالي'].sum()
396
+ st.metric("المجموع الكلي", f"{total:,.2f} ريال")
397
+
398
+ # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية
399
+ edited_items = st.session_state.manual_items.copy()
400
+
401
+ except Exception as e:
402
+ st.error(f"حدث خطأ: {str(e)}")
403
+ st.info("يرجى استخدام طريقة الإدخال البسيطة لتجنب هذه المشكلة.")
404
+
405
+ elif data_source == "استيراد من Excel":
406
+ uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"])
407
+
408
+ if uploaded_file is not None:
409
+ st.success("تم رفع الملف بنجاح")
410
+ # محاكاة قراءة الملف
411
+ st.markdown("### معاينة البيانات المستوردة")
412
+
413
+ # إنشاء بيانات افتراضية
414
+ import_items = pd.DataFrame({
415
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"],
416
+ 'وصف البند': [
417
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
418
+ "توريد وتركيب حديد التسليح للأساسات",
419
+ "أعمال العزل المائي للأساسات",
420
+ "أعمال الردم والدك للأساسات",
421
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
422
+ "توريد وتركيب حديد التسليح للأعمدة",
423
+ "أعمال البلوك للجدران"
424
+ ],
425
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
426
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0],
427
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
428
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
429
+ })
430
+
431
+ st.dataframe(import_items)
432
+
433
+ if st.button("استيراد البيانات"):
434
+ st.session_state.manual_items = import_items.copy()
435
+ st.session_state.manual_items_modified = True
436
+ st.success("تم استيراد البيانات بنجاح!")
437
+ st.rerun()
438
+
439
+ else: # استيراد من وحدة تحليل المستندات
440
+ available_documents = [
441
+ "كراسة شروط مشروع توسعة مستشفى الملك فهد",
442
+ "جدول كميات صيانة محطات المياه",
443
+ "مخططات إنشاء مدرسة ثانوية"
444
+ ]
445
+
446
+ selected_doc = st.selectbox("اختر المستند", available_documents)
447
+
448
+ if st.button("استيراد البيانات من تحليل المستند"):
449
+ # محاكاة استيراد البيانات
450
+ with st.spinner("جاري استيراد البيانات..."):
451
+ time.sleep(2)
452
+
453
+ # إنشاء بيانات افتراضية
454
+ doc_items = pd.DataFrame({
455
+ 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"],
456
+ 'وصف البند': [
457
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
458
+ "توريد وتركيب حديد التسليح للأساسات",
459
+ "أعمال العزل المائي للأساسات",
460
+ "أعمال الردم والدك للأساسات",
461
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
462
+ "توريد وتركيب حديد التسليح للأعمدة",
463
+ "أعمال البلوك للجدران"
464
+ ],
465
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
466
+ 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 400.0],
467
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
468
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
469
+ })
470
+
471
+ st.session_state.manual_items = doc_items.copy()
472
+ st.success("تم استيراد البيانات من تحليل المستند بنجاح!")
473
+ st.dataframe(doc_items)
474
+
475
+ # زر بدء التسعير
476
+ if st.button("بدء التسعير"):
477
+ # تحقق من صحة البيانات
478
+ if 'manual_items' in st.session_state and not st.session_state.manual_items.empty:
479
+ # التأكد من حساب الإجمالي قبل الحفظ
480
+ st.session_state.manual_items['الإجمالي'] = st.session_state.manual_items['الكمية'] * st.session_state.manual_items['سعر الوحدة']
481
+
482
+ # حفظ بيانات التسعير الحالي
483
+ st.session_state.current_pricing = {
484
+ 'name': tender_name,
485
+ 'number': tender_number,
486
+ 'client': client,
487
+ 'location': location,
488
+ 'method': pricing_method,
489
+ 'submission_date': submission_date,
490
+ 'items': st.session_state.manual_items.copy(),
491
+ 'status': 'جديد',
492
+ 'created_at': datetime.now()
493
+ }
494
+
495
+ # الانتقال إلى تبويب نموذج التسعير الشامل
496
+ st.success("تم إنشاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.")
497
+ else:
498
+ st.error("يرجى إدخال بيانات البنود أولاً.")
499
+
500
+ def _render_comprehensive_pricing_tab(self):
501
+ """عرض تبويب نموذج التسعير الشامل"""
502
+
503
+ st.markdown("### نموذج التسعير الشامل")
504
+
505
+ # التحقق من وجود تسعير حالي
506
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
507
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
508
+ return
509
+
510
+ # عرض معلومات التسعير الحالي
511
+ pricing = st.session_state.current_pricing
512
+
513
+ col1, col2, col3 = st.columns(3)
514
+
515
+ with col1:
516
+ st.metric("اسم المناقصة", pricing['name'])
517
+ st.metric("الجهة المالكة", pricing['client'])
518
+
519
+ with col2:
520
+ st.metric("رقم المناقصة", pricing['number'])
521
+ st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d"))
522
+
523
+ with col3:
524
+ st.metric("طريقة التسعير", pricing['method'])
525
+ st.metric("الموقع", pricing['location'])
526
+
527
+ # عرض البنود والتسعير
528
+ st.markdown("### بنود التسعير")
529
+
530
+ items = pricing['items'].copy()
531
+
532
+ # إضافة أسعار الوحدة للمحاكاة
533
+ if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all():
534
+ items['سعر الوحدة'] = [
535
+ round(random.uniform(1000, 3000), 2), # الخرسانة
536
+ round(random.uniform(5000, 7000), 2), # الحديد
537
+ round(random.uniform(100, 200), 2), # العزل
538
+ round(random.uniform(50, 100), 2), # الردم
539
+ round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة
540
+ ]
541
+
542
+ if len(items) > 5:
543
+ for i in range(5, len(items)):
544
+ items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2)
545
+
546
+ # حساب الإجمالي
547
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
548
+
549
+ # عرض البنود
550
+ st.dataframe(items, use_container_width=True, hide_index=True)
551
+
552
+
553
+ # ✅ التوصية الذكية باستخدام OpenAI
554
+ with st.expander("🔍 توليد توصية ذكية باستخدام AI"):
555
+ if st.button("🔍 توليد توصية ذكية باستخدام AI", use_container_width=True):
556
+ import openai
557
+ import os
558
+
559
+ client = openai.OpenAI(api_key=os.environ.get("ai"))
560
+
561
+ items_df = items.copy()
562
+ prompt = f"""قم بتحليل الجدول التالي للبنود في مشروع إنشاء، وقدم توصية ذكية لتحسين التسعير وضمان التوازن المالي. الجدول يحتوي على البنود، الكميات، الأسعار، والإجماليات:\n\n{items_df.to_string(index=False)}\n\nالتوصية:\n"""
563
+
564
+ try:
565
+ with st.spinner("جاري توليد التوصية..."):
566
+ response = client.chat.completions.create(
567
+ model="gpt-4",
568
+ messages=[
569
+ {"role": "system", "content": "أنت خبير في تسعير مشاريع البناء والبنية التحتية."},
570
+ {"role": "user", "content": prompt}
571
+ ],
572
+ temperature=0.4,
573
+ max_tokens=500
574
+ )
575
+
576
+ recommendation = response.choices[0].message.content
577
+ st.success("تم توليد التوصية بنجاح!")
578
+ st.markdown("#### التوصية الذكية:")
579
+ st.info(recommendation)
580
+
581
+ except Exception as e:
582
+ st.error(f"حدث خطأ أثناء الاتصال بنموذج OpenAI: {e}")
583
+ st.info("يجب التأكد من تثبيت أحدث إصدار من مكتبة OpenAI: `pip install openai --upgrade`")
584
+
585
+ # واجهة تعديل أسعار الوحدات
586
+ st.markdown("### تعديل أسعار الوحدات")
587
+
588
+ # تقسيم البنود إلى مجموعتين للعرض
589
+ col1, col2 = st.columns(2)
590
+ half = len(items) // 2 + len(items) % 2
591
+
592
+ with col1:
593
+ for idx in range(half):
594
+ if idx < len(items):
595
+ row = items.iloc[idx]
596
+ price = st.number_input(
597
+ f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})",
598
+ value=float(row['سعر الوحدة']),
599
+ min_value=0.0,
600
+ key=f"price1_{idx}"
601
+ )
602
+ items.at[idx, 'سعر الوحدة'] = price
603
+ items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية']
604
+
605
+ with col2:
606
+ for idx in range(half, len(items)):
607
+ row = items.iloc[idx]
608
+ price = st.number_input(
609
+ f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})",
610
+ value=float(row['سعر الوحدة']),
611
+ min_value=0.0,
612
+ key=f"price2_{idx}"
613
+ )
614
+ items.at[idx, 'سعر الوحدة'] = price
615
+ items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية']
616
+
617
+ # حساب وعرض إجماليات التسعير
618
+ total_price = items['الإجمالي'].sum()
619
+
620
+ st.markdown("### إجماليات التسعير")
621
+
622
+ col1, col2, col3 = st.columns(3)
623
+
624
+ with col1:
625
+ st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال")
626
+
627
+ with col2:
628
+ overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15)
629
+ overhead_value = total_price * overhead_percentage / 100
630
+ st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال")
631
+
632
+ with col3:
633
+ grand_total = total_price + overhead_value
634
+ st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال")
635
+
636
+ # رسم بياني لتوزيع التكاليف
637
+ st.markdown("### تحليل التكاليف")
638
+
639
+ # حساب النسب المئوية لكل بند
640
+ pie_data = items.copy()
641
+ pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100
642
+
643
+ fig = px.pie(
644
+ pie_data,
645
+ values='نسبة من إجمالي التكاليف',
646
+ names='وصف البند',
647
+ title='توزيع التكاليف حسب البنود',
648
+ hole=0.4
649
+ )
650
+
651
+ st.plotly_chart(fig, use_container_width=True)
652
+
653
+ # أزرار العمليات
654
+ col1, col2, col3 = st.columns(3)
655
+
656
+ with col1:
657
+ if st.button("حفظ التسعير"):
658
+ # تحديث بيانات التسعير الحالي
659
+ st.session_state.current_pricing['items'] = items.copy()
660
+ st.success("تم حفظ التسعير بنجاح!")
661
+
662
+ with col2:
663
+ if st.button("تصدير إلى Excel"):
664
+ st.success("تم تصدير التسعير إلى Excel بنجاح!")
665
+
666
+ with col3:
667
+ if st.button("تحليل المخاطر المالية"):
668
+ st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!")
669
+
670
+ def _render_unbalanced_pricing_tab(self):
671
+ """عرض تبويب التسعير غير المتزن"""
672
+
673
+ st.markdown("### التسعير غير المتزن")
674
+
675
+ # التحقق من وجود تسعير حالي
676
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
677
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
678
+ return
679
+
680
+ # شرح التسعير غير المتزن
681
+ with st.expander("ما هو التسعير غير المتزن؟", expanded=False):
682
+ st.markdown("""
683
+ **التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء.
684
+
685
+ ### استراتيجيات التسعير غير المتزن:
686
+
687
+ 1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع.
688
+ 2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع.
689
+ 3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة.
690
+ 4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ.
691
+
692
+ ### مزايا التسعير غير المتزن:
693
+
694
+ - تحسين التدفق النقدي للمشروع.
695
+ - تعظيم الربحية في حالة التغييرات والأوامر التغييرية.
696
+ - زيادة فرص الفوز بالمناقصة.
697
+
698
+ ### مخاطر التسعير غير المتزن:
699
+
700
+ - قد يتم رفض العطاء إذا كان عدم التوازن واضحاً.
701
+ - قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط.
702
+ - قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية.
703
+ """)
704
+
705
+ # عرض بنود التسعير الحالي
706
+ items = st.session_state.current_pricing['items'].copy()
707
+
708
+ # إضافة عمود إستراتيجية التسعير
709
+ if 'إستراتيجية التسعير' not in items.columns:
710
+ items['إستراتيجية التسعير'] = 'متوازن'
711
+
712
+ st.markdown("### إستراتيجية التسعير غير المتزن")
713
+
714
+ # اختيار الإستراتيجية
715
+ strategy = st.selectbox(
716
+ "اختر إستراتيجية التسعير",
717
+ [
718
+ "تحميل أمامي (Front Loading)",
719
+ "تحميل البنود المؤكدة",
720
+ "تخفيض البنود المحتمل زيادتها",
721
+ "إستراتيجية مخصصة"
722
+ ]
723
+ )
724
+
725
+ # تطبيق الإستراتيجية المختارة
726
+ if strategy == "تحميل أمامي (Front Loading)":
727
+ # محاكاة تحميل أمامي
728
+ items_count = len(items)
729
+ early_items = items.iloc[:items_count//3].index
730
+ middle_items = items.iloc[items_count//3:2*items_count//3].index
731
+ late_items = items.iloc[2*items_count//3:].index
732
+
733
+ # تطبيق الزيادة والنقصان
734
+ for idx in early_items:
735
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30%
736
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
737
+
738
+ for idx in middle_items:
739
+ items.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
740
+
741
+ for idx in late_items:
742
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
743
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
744
+
745
+ elif strategy == "تحميل البنود المؤكدة":
746
+ # محاكاة - اعتبار بعض البنود مؤكدة
747
+ confirmed_items = [0, 2, 4] # الأ��فار-مستندة
748
+ variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items]
749
+
750
+ # تطبيق الزيادة والنقصان
751
+ for idx in confirmed_items:
752
+ if idx < len(items):
753
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25%
754
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
755
+
756
+ for idx in variable_items:
757
+ if idx < len(items):
758
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15%
759
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
760
+
761
+ elif strategy == "تخفيض البنود المحتمل زيادتها":
762
+ # محاكاة - اعتبار بعض البنود محتمل زيادتها
763
+ variable_items = [1, 3] # الأصفار-مستندة
764
+ other_items = [idx for idx in range(len(items)) if idx not in variable_items]
765
+
766
+ # تطبيق الزيادة والنقصان
767
+ for idx in variable_items:
768
+ if idx < len(items):
769
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
770
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
771
+
772
+ for idx in other_items:
773
+ if idx < len(items):
774
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15%
775
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
776
+
777
+ else: # إستراتيجية مخصصة
778
+ st.markdown("### تعديل أسعار البنود يدوياً")
779
+ st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً.")
780
+
781
+ # حساب الإجمالي بعد التعديل
782
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
783
+
784
+ # تعيين ألوان للإستراتيجيات
785
+ def highlight_strategy(val):
786
+ if val == 'زيادة':
787
+ return 'background-color: #a8e6cf'
788
+ elif val == 'نقص':
789
+ return 'background-color: #ff9aa2'
790
+ return ''
791
+
792
+ # عرض الجدول مع تنسيق
793
+ st.markdown("### بنود التسعير غير المتزن")
794
+ styled_items = items.style.applymap(highlight_strategy, subset=['إستراتيجية التسعير'])
795
+ st.dataframe(styled_items, use_container_width=True)
796
+
797
+ # المقارنة بين التسعير المتوازن وغير المتوازن
798
+ st.markdown("### مقارنة التسعير المتوازن وغير المتوازن")
799
+
800
+ original_items = st.session_state.current_pricing['items'].copy()
801
+ original_total = original_items['الإجمالي'].sum()
802
+ unbalanced_total = items['الإجمالي'].sum()
803
+
804
+ col1, col2, col3 = st.columns(3)
805
+
806
+ with col1:
807
+ st.metric("إجمالي التسعير المتوازن", f"{original_total:,.2f} ريال")
808
+
809
+ with col2:
810
+ st.metric("إجمالي التسعير غير المتوازن", f"{unbalanced_total:,.2f} ريال")
811
+
812
+ with col3:
813
+ diff = unbalanced_total - original_total
814
+ st.metric("الفرق", f"{diff:,.2f} ريال", delta=f"{diff/original_total*100:.1f}%")
815
+
816
+ # المعايرة للحفاظ على إجمالي التسعير
817
+ if abs(diff) > 1: # إذا كان هناك فرق كبير
818
+ if st.button("معايرة الأسعار للحفاظ على إجمالي التسعير"):
819
+ # تعديل الأسعار للحفاظ على إجمالي التكلفة
820
+ adjustment_factor = original_total / unbalanced_total
821
+ items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor
822
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
823
+
824
+ st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)")
825
+ st.dataframe(items, use_container_width=True)
826
+
827
+ # رسم بياني للمقارنة
828
+ st.markdown("### تحليل بصري للتسعير غير المتوازن")
829
+
830
+ # إعداد البيانات للرسم البياني
831
+ chart_data = pd.DataFrame({
832
+ 'وصف البند': original_items['وصف البند'],
833
+ 'التسعير المتوازن': original_items['الإجمالي'],
834
+ 'التسعير غير المتوازن': items['الإجمالي']
835
+ })
836
+
837
+ # رسم بياني شريطي للمقارنة
838
+ fig = go.Figure()
839
+
840
+ fig.add_trace(go.Bar(
841
+ x=chart_data['وصف البند'],
842
+ y=chart_data['التسعير المتوازن'],
843
+ name='التسعير المتوازن',
844
+ marker_color='rgb(55, 83, 109)'
845
+ ))
846
+
847
+ fig.add_trace(go.Bar(
848
+ x=chart_data['وصف البند'],
849
+ y=chart_data['التسعير غير المتوازن'],
850
+ name='التسعير غير المتوازن',
851
+ marker_color='rgb(26, 118, 255)'
852
+ ))
853
+
854
+ fig.update_layout(
855
+ title='مقارنة بين التسعير المتوازن وغير المتوازن',
856
+ xaxis_tickfont_size=14,
857
+ yaxis=dict(
858
+ title='الإجمالي (ريال)',
859
+ titlefont_size=16,
860
+ tickfont_size=14,
861
+ ),
862
+ legend=dict(
863
+ x=0,
864
+ y=1.0,
865
+ bgcolor='rgba(255, 255, 255, 0)',
866
+ bordercolor='rgba(255, 255, 255, 0)'
867
+ ),
868
+ barmode='group',
869
+ bargap=0.15,
870
+ bargroupgap=0.1
871
+ )
872
+
873
+ st.plotly_chart(fig, use_container_width=True)
874
+
875
+ # زر حفظ التسعير غير المتوازن
876
+ if st.button("حفظ التسعير غير المتوازن"):
877
+ st.session_state.current_pricing['items'] = items.copy()
878
+ st.session_state.current_pricing['method'] = "التسعير غير المتزن"
879
+ st.success("تم حفظ التسعير غير المتوازن بنجاح!")
880
+
881
+ def _render_local_content_tab(self):
882
+ """عرض تبويب المحتوى المحلي"""
883
+
884
+ st.markdown("### تحليل المحتوى المحلي")
885
+
886
+ # التحقق من وجود تسعير حالي
887
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
888
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
889
+ return
890
+
891
+ # شرح المحتوى المحلي
892
+ with st.expander("ما هو المحتوى المحلي؟", expanded=False):
893
+ st.markdown("""
894
+ **المحتوى المحلي** هو نسبة المنتجات والخدمات والقوى العاملة المحلية المستخدمة في المشروع. يهدف إلى زيادة مساهمة المنتجات والخدمات المحلية في المشاريع.
895
+
896
+ ### مكونات المحتوى المحلي:
897
+
898
+ 1. **المنتجات**: المنتجات والمواد المصنعة محلياً.
899
+ 2. **الخدمات**: الخدمات المقدمة من شركات محلية.
900
+ 3. **القوى العاملة**: العمالة والكوادر الفنية والإدارية المحلية.
901
+
902
+ ### أهمية المحتوى المحلي:
903
+
904
+ - تعزيز الاقتصاد المحلي وخلق فرص عمل.
905
+ - تحقيق أهداف رؤية 2030 في زيادة المحتوى المحلي.
906
+ - التأهل للمشاريع الحكومية التي تتطلب نسبة محتوى محلي محددة.
907
+ - الحصول على حوافز وأفضلية في المناقصات الحكومية.
908
+
909
+ ### متطلبات المحتوى المحلي:
910
+
911
+ - نسبة المحتوى المحلي للقوى العاملة: 80%
912
+ - نسبة المحتوى المحلي للمنتجات: 70%
913
+ - نسبة المحتوى المحلي للخدمات: 60%
914
+ """)
915
+
916
+ # عرض لوحة إدخال بيانات المحتوى المحلي
917
+ st.markdown("### بيانات المحتوى المحلي")
918
+
919
+ # التبويبات لأنواع المحتوى المحلي
920
+ lc_tabs = st.tabs(["المنتجات", "الخدمات", "القوى العاملة", "التحليل"])
921
+
922
+ with lc_tabs[0]: # المنتجات
923
+ st.markdown("#### بيانات المنتجات")
924
+
925
+ # إنشاء بيانات افتراضية للمنتجات إذا لم تكن موجودة
926
+ if 'local_content_products' not in st.session_state:
927
+ st.session_state.local_content_products = pd.DataFrame({
928
+ 'المنتج': [
929
+ "خرسانة مسلحة",
930
+ "حديد تسليح",
931
+ "بلوك خرساني",
932
+ "عزل مائي",
933
+ "دهانات"
934
+ ],
935
+ 'الكمية': [250, 25, 400, 500, 600],
936
+ 'سعر_الوحدة': [1200, 6000, 200, 100, 50],
937
+ 'التكلفة_الإجمالية': [300000, 150000, 80000, 50000, 30000],
938
+ 'نسبة_المحتوى_المحلي': [0.95, 0.70, 0.98, 0.60, 0.80]
939
+ })
940
+
941
+ # حساب التكلفة الإجمالية
942
+ st.session_state.local_content_products['التكلفة_الإجمالية'] = st.session_state.local_content_products['الكمية'] * st.session_state.local_content_products['سعر_الوحدة']
943
+
944
+ # عرض جدول البنود مع إمكانية التعديل
945
+ edited_products = st.data_editor(
946
+ st.session_state.local_content_products,
947
+ use_container_width=True,
948
+ hide_index=True,
949
+ num_rows="dynamic"
950
+ )
951
+ st.session_state.local_content_products = edited_products
952
+
953
+ # عرض ملخص المنتجات
954
+ total_products_cost = edited_products['التكلفة_الإجمالية'].sum()
955
+ avg_local_content = (edited_products['التكلفة_الإجمالية'] * edited_products['نسبة_المحتوى_المحلي']).sum() / total_products_cost if total_products_cost > 0 else 0
956
+
957
+ st.markdown(f"""
958
+ **إجمالي تكلفة المنتجات**: {total_products_cost:,.2f} ريال
959
+
960
+ **متوسط نسبة المحتوى المحلي للمنتجات**: {avg_local_content*100:.2f}%
961
+
962
+ **المستهدف**: 70%
963
+
964
+ **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.7 else "❌ غير ملتزم"}
965
+ """)
966
+
967
+ with lc_tabs[1]: # الخدمات
968
+ st.markdown("#### بيانات الخدمات")
969
+
970
+ # إنشاء بيانات افتراضية للخدمات إذا لم تكن موجودة
971
+ if 'local_content_services' not in st.session_state:
972
+ st.session_state.local_content_services = pd.DataFrame({
973
+ 'الخدمة': [
974
+ "تصميم معماري",
975
+ "إشراف هندسي",
976
+ "خدمات نقل",
977
+ "خدمات أمن وسلامة",
978
+ "صيانة ونظافة"
979
+ ],
980
+ 'التكلفة': [100000, 120000, 50000, 30000, 20000],
981
+ 'نسبة_المحتوى_المحلي': [0.90, 0.85, 0.90, 0.95, 0.95]
982
+ })
983
+
984
+ # عرض جدول الخدمات مع إمكانية التعديل
985
+ edited_services = st.data_editor(
986
+ st.session_state.local_content_services,
987
+ use_container_width=True,
988
+ hide_index=True,
989
+ num_rows="dynamic"
990
+ )
991
+ st.session_state.local_content_services = edited_services
992
+
993
+ # عرض ملخص الخدمات
994
+ total_services_cost = edited_services['التكلفة'].sum()
995
+ avg_local_content = (edited_services['التكلفة'] * edited_services['نسبة_المحتوى_المحلي']).sum() / total_services_cost if total_services_cost > 0 else 0
996
+
997
+ st.markdown(f"""
998
+ **إجمالي تكلفة الخدمات**: {total_services_cost:,.2f} ريال
999
+
1000
+ **متوسط نسبة المحتوى المحلي للخدمات**: {avg_local_content*100:.2f}%
1001
+
1002
+ **المستهدف**: 60%
1003
+
1004
+ **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.6 else "❌ غير ملتزم"}
1005
+ """)
1006
+
1007
+ with lc_tabs[2]: # القوى العاملة
1008
+ st.markdown("#### بيانات القوى العاملة")
1009
+
1010
+ # إنشاء بيانات افتراضية للقوى العاملة إذا لم تكن موجودة
1011
+ if 'local_content_labor' not in st.session_state:
1012
+ st.session_state.local_content_labor = pd.DataFrame({
1013
+ 'فئة_العمالة': [
1014
+ "مهندسون",
1015
+ "فنيون",
1016
+ "عمال بناء",
1017
+ "إداريون",
1018
+ "مشرفون"
1019
+ ],
1020
+ 'العدد': [5, 10, 30, 3, 4],
1021
+ 'الراتب_الشهري': [15000, 8000, 3000, 10000, 12000],
1022
+ 'المدة_بالأشهر': [12, 12, 12, 12, 12],
1023
+ 'نسبة_المحتوى_المحلي': [0.75, 0.65, 0.60, 0.90, 0.80]
1024
+ })
1025
+
1026
+ # حساب التكلفة الإجمالية
1027
+ st.session_state.local_content_labor['التكلفة_الإجمالية'] = st.session_state.local_content_labor['العدد'] * st.session_state.local_content_labor['الراتب_الشهري'] * st.session_state.local_content_labor['المدة_بالأشهر']
1028
+
1029
+ # عرض جدول القوى العاملة مع إمكانية التعديل
1030
+ edited_labor = st.data_editor(
1031
+ st.session_state.local_content_labor,
1032
+ use_container_width=True,
1033
+ hide_index=True,
1034
+ num_rows="dynamic"
1035
+ )
1036
+
1037
+ # إعادة حساب التكلفة الإجمالية بعد التعديل
1038
+ edited_labor['التكلفة_الإجمالية'] = edited_labor['العدد'] * edited_labor['الراتب_الشهري'] * edited_labor['المدة_بالأشهر']
1039
+ st.session_state.local_content_labor = edited_labor
1040
+
1041
+ # عرض ملخص القوى العاملة
1042
+ total_labor_cost = edited_labor['التكلفة_الإجمالية'].sum()
1043
+ avg_local_content = (edited_labor['التكلفة_الإجمالية'] * edited_labor['نسبة_المحتوى_المحلي']).sum() / total_labor_cost if total_labor_cost > 0 else 0
1044
+
1045
+ st.markdown(f"""
1046
+ **إجمالي تكلفة القوى العاملة**: {total_labor_cost:,.2f} ريال
1047
+
1048
+ **متوسط نسبة المحتوى المحلي للقوى العاملة**: {avg_local_content*100:.2f}%
1049
+
1050
+ **المستهدف**: 80%
1051
+
1052
+ **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.8 else "❌ غير ملتزم"}
1053
+ """)
1054
+
1055
+ with lc_tabs[3]: # التحليل
1056
+ st.markdown("#### تحليل المحتوى المحلي")
1057
+
1058
+ # حساب المحتوى المحلي الإجمالي
1059
+ try:
1060
+ # تجميع بيانات تحليل المحتوى المحلي
1061
+ products_cost = st.session_state.local_content_products['التكلفة_الإجمالية'].sum()
1062
+ products_local_content = (st.session_state.local_content_products['التكلفة_الإجمالية'] * st.session_state.local_content_products['نسبة_المحتوى_المحلي']).sum() / products_cost if products_cost > 0 else 0
1063
+
1064
+ services_cost = st.session_state.local_content_services['التكلفة'].sum()
1065
+ services_local_content = (st.session_state.local_content_services['التكلفة'] * st.session_state.local_content_services['نسبة_المحتوى_المحلي']).sum() / services_cost if services_cost > 0 else 0
1066
+
1067
+ labor_cost = st.session_state.local_content_labor['التكلفة_الإجمالية'].sum()
1068
+ labor_local_content = (st.session_state.local_content_labor['التكلفة_الإجمالية'] * st.session_state.local_content_labor['نسبة_المحتوى_المحلي']).sum() / labor_cost if labor_cost > 0 else 0
1069
+
1070
+ # حساب الوزن النسبي لكل مكون
1071
+ total_cost = products_cost + services_cost + labor_cost
1072
+ products_weight = products_cost / total_cost if total_cost > 0 else 0
1073
+ services_weight = services_cost / total_cost if total_cost > 0 else 0
1074
+ labor_weight = labor_cost / total_cost if total_cost > 0 else 0
1075
+
1076
+ # حساب المحتوى المحلي الإجمالي
1077
+ total_local_content = (products_local_content * products_weight) + (services_local_content * services_weight) + (labor_local_content * labor_weight)
1078
+
1079
+ # عرض ملخص المحتوى المحلي
1080
+ st.markdown("### ملخص المحتوى المحلي")
1081
+
1082
+ col1, col2, col3 = st.columns(3)
1083
+
1084
+ with col1:
1085
+ st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال")
1086
+
1087
+ with col2:
1088
+ st.metric("نسبة المحتوى المحلي الإجمالية", f"{total_local_content*100:.2f}%")
1089
+
1090
+ with col3:
1091
+ target_local_content = 0.7 # 70%
1092
+ st.metric("الحالة", "ملتزم" if total_local_content >= target_local_content else "غير ملتزم", delta=f"{(total_local_content - target_local_content)*100:.2f}%")
1093
+
1094
+ # عرض رسم بياني للمقارنة
1095
+ st.markdown("### تحليل بصري للمحتوى المحلي")
1096
+
1097
+ # رسم بياني شريطي لنسب المحتوى المحلي
1098
+ categories = ['المنتجات', 'الخدمات', 'القوى العاملة', 'الإجمالي']
1099
+ actual_values = [products_local_content * 100, services_local_content * 100, labor_local_content * 100, total_local_content * 100]
1100
+ target_values = [70, 60, 80, 70] # المستهدفات
1101
+
1102
+ # تهيئة البيانات للرسم البياني
1103
+ chart_data = pd.DataFrame({
1104
+ 'الفئة': categories,
1105
+ 'النسبة الفعلية': actual_values,
1106
+ 'النسبة المستهدفة': target_values
1107
+ })
1108
+
1109
+ # رسم بياني شريطي للمقارنة
1110
+ fig = go.Figure()
1111
+
1112
+ fig.add_trace(go.Bar(
1113
+ x=chart_data['الفئة'],
1114
+ y=chart_data['النسبة الفعلية'],
1115
+ name='النسبة الفعلية',
1116
+ marker_color='rgb(26, 118, 255)'
1117
+ ))
1118
+
1119
+ fig.add_trace(go.Bar(
1120
+ x=chart_data['الفئة'],
1121
+ y=chart_data['النسبة المستهدفة'],
1122
+ name='النسبة المستهدفة',
1123
+ marker_color='rgb(55, 83, 109)'
1124
+ ))
1125
+
1126
+ fig.update_layout(
1127
+ title='مقارنة بين النسب الفعلية والمستهدفة للمحتوى المحلي',
1128
+ xaxis_tickfont_size=14,
1129
+ yaxis=dict(
1130
+ title='النسبة %',
1131
+ titlefont_size=16,
1132
+ tickfont_size=14,
1133
+ ),
1134
+ legend=dict(
1135
+ x=0,
1136
+ y=1.0,
1137
+ bgcolor='rgba(255, 255, 255, 0)',
1138
+ bordercolor='rgba(255, 255, 255, 0)'
1139
+ ),
1140
+ barmode='group',
1141
+ bargap=0.15,
1142
+ bargroupgap=0.1
1143
+ )
1144
+
1145
+ st.plotly_chart(fig, use_container_width=True)
1146
+
1147
+ # عرض توصيات لتحسين نسبة المحتوى المحلي
1148
+ st.markdown("### توصيات لتحسين نسبة المحتوى المحلي")
1149
+
1150
+ recommendations = []
1151
+
1152
+ if products_local_content < 0.7:
1153
+ recommendations.append("- زيادة نسبة المحتوى المحلي للمنتجات من خلال:")
1154
+ recommendations.append(" - البحث عن موردين محليين للمنتجات ذات النسبة المنخفضة")
1155
+ recommendations.append(" - استبدال المنتجات المستوردة ببدائل محلية")
1156
+ recommendations.append(" - التعاون مع المصانع المحلية لتوطين صناعة المنتجات")
1157
+
1158
+ if services_local_content < 0.6:
1159
+ recommendations.append("- زيادة نسبة المحتوى المحلي للخدمات من خلال:")
1160
+ recommendations.append(" - التعاقد مع شركات خدمات محلية")
1161
+ recommendations.append(" - تحويل الخدمات المستعان بها من الخارج إلى شركات محلية")
1162
+ recommendations.append(" - تأهيل الشركات المحلية لتقديم الخدمات المطلوبة")
1163
+
1164
+ if labor_local_content < 0.8:
1165
+ recommendations.append("- زيادة نسبة المحتوى المحلي للقوى العاملة من خلال:")
1166
+ recommendations.append(" - زيادة توظيف الكوادر المحلية")
1167
+ recommendations.append(" - تدريب وتأهيل العمالة المحلية")
1168
+ recommendations.append(" - استبدال العمالة الأجنبية بكوادر محلية تدريجياً")
1169
+
1170
+ if total_local_content < 0.7:
1171
+ recommendations.append("- زيادة نسبة المحتوى المحلي الإجمالية من خلال:")
1172
+ recommendations.append(" - إعادة توزيع الميزانية لصالح المكونات ذات النسبة العالية من المحتوى المحلي")
1173
+ recommendations.append(" - وضع خطة مرحلية لزيادة المحتوى المحلي")
1174
+ recommendations.append(" - التعاون مع اللجنة المحلية لزيادة المحتوى المحلي")
1175
+
1176
+ if recommendations:
1177
+ for rec in recommendations:
1178
+ st.markdown(rec)
1179
+ else:
1180
+ st.success("تهانينا! نسبة المحتوى ��لمحلي متوافقة مع المتطلبات.")
1181
+
1182
+ # حساب تأثير المحتوى المحلي على التسعير
1183
+ st.markdown("### تأثير المحتوى المحلي على التسعير")
1184
+
1185
+ # تحديد عامل تعديل السعر بناءً على نسبة المحتوى المحلي
1186
+ price_adjustment_factor = 1.0
1187
+
1188
+ if total_local_content >= 0.9:
1189
+ price_adjustment_factor = 0.92 # خصم 8% للمحتوى المحلي العالي جداً
1190
+ price_discount = "8%"
1191
+ elif total_local_content >= 0.8:
1192
+ price_adjustment_factor = 0.94 # خصم 6% للمحتوى المحلي العالي
1193
+ price_discount = "6%"
1194
+ elif total_local_content >= 0.7:
1195
+ price_adjustment_factor = 0.96 # خصم 4% للمحتوى المحلي المتوسط
1196
+ price_discount = "4%"
1197
+ elif total_local_content >= 0.6:
1198
+ price_adjustment_factor = 0.98 # خصم 2% للمحتوى المحلي المنخفض
1199
+ price_discount = "2%"
1200
+ else:
1201
+ price_adjustment_factor = 1.0 # لا خصم
1202
+ price_discount = "0%"
1203
+
1204
+ # عرض تأثير المحتوى المحلي على التسعير
1205
+ original_total = st.session_state.current_pricing['items']['الإجمالي'].sum()
1206
+ adjusted_total = original_total * price_adjustment_factor
1207
+ discount_amount = original_total - adjusted_total
1208
+
1209
+ col1, col2, col3 = st.columns(3)
1210
+
1211
+ with col1:
1212
+ st.metric("إجمالي التسعير الأصلي", f"{original_total:,.2f} ريال")
1213
+
1214
+ with col2:
1215
+ st.metric("نسبة الخصم بسبب المحتوى المحلي", price_discount)
1216
+
1217
+ with col3:
1218
+ st.metric("إجمالي التسعير بعد الخصم", f"{adjusted_total:,.2f} ريال", delta=f"-{discount_amount:,.2f} ريال")
1219
+
1220
+ # أزرار العمليات
1221
+ col1, col2 = st.columns(2)
1222
+
1223
+ with col1:
1224
+ if st.button("حفظ تحليل المحتوى المحلي"):
1225
+ # حفظ بيانات المحتوى المحلي في التسعير الحالي
1226
+ st.session_state.current_pricing['local_content'] = {
1227
+ 'products': st.session_state.local_content_products.copy(),
1228
+ 'services': st.session_state.local_content_services.copy(),
1229
+ 'labor': st.session_state.local_content_labor.copy(),
1230
+ 'total_local_content': total_local_content,
1231
+ 'price_adjustment_factor': price_adjustment_factor
1232
+ }
1233
+
1234
+ st.success("تم حفظ تحليل المحتوى المحلي بنجاح!")
1235
+
1236
+ with col2:
1237
+ if st.button("تصدير تقرير المحتوى المحلي"):
1238
+ st.success("تم تصدير تقرير المحتوى المحلي بنجاح!")
1239
+
1240
+ except Exception as e:
1241
+ st.error(f"حدث خطأ أثناء تحليل المحتوى المحلي: {str(e)}")
1242
+ st.warning("تأكد من إدخال بيانات المحتوى المحلي بشكل صحيح في التبويبات السابقة.")
modules/pricing/services/construction_cost_calculator.py ADDED
@@ -0,0 +1,1006 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ خدمة حاسبة تكاليف البناء
3
+ تقوم هذه الخدمة بحساب تكاليف البناء بشكل تفصيلي بناءً على المكونات المختلفة:
4
+ - المواد الخام
5
+ - العمالة
6
+ - المعدات
7
+ - المصاريف الإدارية
8
+ - هامش الربح
9
+ """
10
+
11
+ import pandas as pd
12
+ import numpy as np
13
+ from datetime import datetime
14
+ import os
15
+ import json
16
+ import sys
17
+ from typing import Dict, List, Optional, Union, Any
18
+
19
+ # إضافة مسار النظام للوصول لملفات التكوين
20
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")))
21
+ try:
22
+ import config
23
+ except ImportError:
24
+ # إذا لم يتم العثور على ملف التكوين، نستخدم قيم افتراضية
25
+ class DefaultConfig:
26
+ DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../data"))
27
+ config = DefaultConfig
28
+
29
+ # إنشاء مجلد البيانات إذا لم يكن موجودًا
30
+ if not os.path.exists(config.DATA_DIR):
31
+ os.makedirs(config.DATA_DIR)
32
+
33
+ class ConstructionCostCalculator:
34
+ """خدمة حاسبة تكاليف البناء"""
35
+
36
+ def __init__(self):
37
+ """تهيئة حاسبة تكاليف البناء"""
38
+ # تحميل بيانات المواد والأسعار المرجعية
39
+ self.material_rates = self._load_material_rates()
40
+ self.labor_rates = self._load_labor_rates()
41
+ self.equipment_rates = self._load_equipment_rates()
42
+
43
+ # النسب الافتراضية للمصاريف الإدارية وهامش الربح
44
+ self.default_admin_expenses_percentage = 0.05 # 5%
45
+ self.default_profit_margin_percentage = 0.10 # 10%
46
+
47
+ # معاملات التعديل الافتراضية
48
+ self.default_adjustment_factors = {
49
+ 'location_factor': 1.0, # معامل الموقع
50
+ 'time_factor': 1.0, # معامل الوقت
51
+ 'risk_factor': 1.0, # معامل المخاطر
52
+ 'market_factor': 1.0 # معامل السوق
53
+ }
54
+
55
+ def _load_material_rates(self) -> Dict[str, Dict[str, Any]]:
56
+ """تحميل أسعار المواد"""
57
+ # محاكاة تحميل البيانات من مصدر بيانات
58
+ material_rates = {
59
+ # مواد الخرسانة
60
+ 'خرسانة جاهزة': {
61
+ 'وحدة': 'م3',
62
+ 'سعر_الوحدة': 750.0,
63
+ 'وصف': 'خرسانة جاهزة بقوة 350 كجم/سم2',
64
+ 'فئة': 'أعمال خرسانية'
65
+ },
66
+ 'حديد تسليح': {
67
+ 'وحدة': 'طن',
68
+ 'سعر_الوحدة': 5500.0,
69
+ 'وصف': 'حديد تسليح قطر 8-32 مم',
70
+ 'فئة': 'أعمال خرسانية'
71
+ },
72
+ 'أسمنت': {
73
+ 'وحدة': 'كيس',
74
+ 'سعر_الوحدة': 30.0,
75
+ 'وصف': 'أسمنت بورتلاندي عادي',
76
+ 'فئة': 'أعمال خرسانية'
77
+ },
78
+ 'رمل': {
79
+ 'وحدة': 'م3',
80
+ 'سعر_الوحدة': 120.0,
81
+ 'وصف': 'رمل خشن للخرسانة',
82
+ 'فئة': 'أعمال خرسانية'
83
+ },
84
+ 'زلط': {
85
+ 'وحدة': 'م3',
86
+ 'سعر_الوحدة': 150.0,
87
+ 'وصف': 'زلط مقاس 10-20 مم للخرسانة',
88
+ 'فئة': 'أعمال خرسانية'
89
+ },
90
+
91
+ # مواد البناء
92
+ 'طوب أحمر': {
93
+ 'وحدة': '1000 قطعة',
94
+ 'سعر_الوحدة': 900.0,
95
+ 'وصف': 'طوب أحمر مقاس 25×12×6 سم',
96
+ 'فئة': 'أعمال بناء'
97
+ },
98
+ 'طوب أسمنتي': {
99
+ 'وحدة': 'قطعة',
100
+ 'سعر_الوحدة': 4.5,
101
+ 'وصف': 'بلوك أسمنتي مقاس 20×20×40 سم',
102
+ 'فئة': 'أعمال بناء'
103
+ },
104
+ 'مونة بناء': {
105
+ 'وحدة': 'م3',
106
+ 'سعر_الوحدة': 350.0,
107
+ 'وصف': 'مونة أسمنتية للبناء',
108
+ 'فئة': 'أعمال بناء'
109
+ },
110
+
111
+ # مواد التشطيبات
112
+ 'بلاط سيراميك': {
113
+ 'وحدة': 'م2',
114
+ 'سعر_الوحدة': 120.0,
115
+ 'وصف': 'بلاط سيراميك للأرضيات مقاس 40×40 سم',
116
+ 'فئة': 'تشطيبات'
117
+ },
118
+ 'بلاط بورسلين': {
119
+ 'وحدة': 'م2',
120
+ 'سعر_الوحدة': 180.0,
121
+ 'وصف': 'بلاط بورسلين للأرضيات مقاس 60×60 سم',
122
+ 'فئة': 'تشطيبات'
123
+ },
124
+ 'دهانات بلاستيك': {
125
+ 'وحدة': 'لتر',
126
+ 'سعر_الوحدة': 35.0,
127
+ 'وصف': 'دهان بلاستيك أساس وتشطيب',
128
+ 'فئة': 'تشطيبات'
129
+ },
130
+ 'جبس بورد': {
131
+ 'وحدة': 'م2',
132
+ 'سعر_الوحدة': 95.0,
133
+ 'وصف': 'ألواح جبس بورد سمك 12 مم',
134
+ 'فئة': 'تشطيبات'
135
+ },
136
+
137
+ # مواد العزل
138
+ 'عزل مائي': {
139
+ 'وحدة': 'م2',
140
+ 'سعر_الوحدة': 45.0,
141
+ 'وصف': 'عزل مائي من البيتومين المؤكسد',
142
+ 'فئة': 'أعمال عزل'
143
+ },
144
+ 'عزل حراري': {
145
+ 'وحدة': 'م2',
146
+ 'سعر_الوحدة': 65.0,
147
+ 'وصف': 'ألواح عزل حراري من البوليسترين سمك 5 سم',
148
+ 'فئة': 'أعمال عزل'
149
+ }
150
+ }
151
+
152
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
153
+ try:
154
+ file_path = os.path.join(config.DATA_DIR, 'material_rates.json')
155
+ if os.path.exists(file_path):
156
+ with open(file_path, 'r', encoding='utf-8') as f:
157
+ loaded_data = json.load(f)
158
+ material_rates.update(loaded_data)
159
+ except Exception as e:
160
+ print(f"خطأ في تحميل بيانات أسعار المواد: {str(e)}")
161
+
162
+ return material_rates
163
+
164
+ def _load_labor_rates(self) -> Dict[str, Dict[str, Any]]:
165
+ """تحميل أسعار العمالة"""
166
+ # محاكاة تحميل البيانات من مصدر بيانات
167
+ labor_rates = {
168
+ # عمالة الخرسانات
169
+ 'نجار مسلح': {
170
+ 'وحدة': 'يوم',
171
+ 'سعر_الوحدة': 250.0,
172
+ 'وصف': 'نجار مسلح لأعمال الشدات والفرم',
173
+ 'فئة': 'أعمال خرسانية',
174
+ 'إنتاجية_يومية': {
175
+ 'شدة أساسات': 12, # متر مربع
176
+ 'شدة أعمدة': 10, # متر مربع
177
+ 'شدة أسقف': 12 # متر مربع
178
+ }
179
+ },
180
+ 'حداد مسلح': {
181
+ 'وحدة': 'يوم',
182
+ 'سعر_الوحدة': 250.0,
183
+ 'وصف': 'حداد مسلح لأعمال حديد التسليح',
184
+ 'فئة': 'أعمال خرسانية',
185
+ 'إنتاجية_يومية': {
186
+ 'تجهيز وتركيب حديد أساسات': 700, # كجم
187
+ 'تجهيز وتركيب حديد أعمدة': 600, # كجم
188
+ 'تجهيز وتركيب حديد أسقف': 650 # كجم
189
+ }
190
+ },
191
+ 'عامل خرسانة': {
192
+ 'وحدة': 'يوم',
193
+ 'سعر_الوحدة': 150.0,
194
+ 'وصف': 'عامل لصب وتسوية الخرسانة',
195
+ 'فئة': 'أعمال خرسانية',
196
+ 'إنتاجية_يومية': {
197
+ 'صب خرسانة': 15 # متر مكعب
198
+ }
199
+ },
200
+
201
+ # عمالة البناء
202
+ 'بناء': {
203
+ 'وحدة': 'يوم',
204
+ 'سعر_الوحدة': 200.0,
205
+ 'وصف': 'عامل بناء للطوب والبلوك',
206
+ 'فئة': 'أعمال بناء',
207
+ 'إنتاجية_يومية': {
208
+ 'بناء طوب أحمر': 500, # قطعة
209
+ 'بناء بلوك أسمنتي': 80 # قطعة
210
+ }
211
+ },
212
+ 'مساعد بناء': {
213
+ 'وحدة': 'يوم',
214
+ 'سعر_الوحدة': 120.0,
215
+ 'وصف': 'مساعد عامل بناء',
216
+ 'فئة': 'أعمال بناء',
217
+ 'إنتاجية_يومية': {}
218
+ },
219
+
220
+ # عمالة التشطيبات
221
+ 'مبلط': {
222
+ 'وحدة': 'يوم',
223
+ 'سعر_الوحدة': 250.0,
224
+ 'وصف': 'عامل تركيب بلاط وسيراميك',
225
+ 'فئة': 'تشطيبات',
226
+ 'إنتاجية_يومية': {
227
+ 'تركيب سيراميك أرضيات': 15, # متر مربع
228
+ 'تركيب سيراميك حوائط': 12, # متر مربع
229
+ 'تركيب بورسلين': 12 # متر مربع
230
+ }
231
+ },
232
+ 'نقاش': {
233
+ 'وحدة': 'يوم',
234
+ 'سعر_الوحدة': 200.0,
235
+ 'وصف': 'عامل دهانات',
236
+ 'فئة': 'تشطيبات',
237
+ 'إنتاجية_يومية': {
238
+ 'دهانات بلاستيك': 35, # متر مربع
239
+ 'دهانات زيتية': 25 # متر مربع
240
+ }
241
+ },
242
+ 'كهربائي': {
243
+ 'وحدة': 'يوم',
244
+ 'سعر_الوحدة': 270.0,
245
+ 'وصف': 'فني كهرباء',
246
+ 'فئة': 'تشطيبات',
247
+ 'إنتاجية_يومية': {
248
+ 'تأسيس نقاط كهرباء': 15, # نقطة
249
+ 'تركيب لوحات توزيع': 2 # لوحة
250
+ }
251
+ },
252
+ 'سباك': {
253
+ 'وحدة': 'يوم',
254
+ 'سعر_الوحدة': 250.0,
255
+ 'وصف': 'فني سباكة',
256
+ 'فئة': 'تشطيبات',
257
+ 'إنتاجية_يومية': {
258
+ 'تأسيس نقاط صرف': 8, # نقطة
259
+ 'تأسيس نقاط تغذية': 10, # نقطة
260
+ 'تركيب أطقم حمامات': 2 # طقم
261
+ }
262
+ },
263
+
264
+ # مراقبة وإشراف
265
+ 'مهندس موقع': {
266
+ 'وحدة': 'يوم',
267
+ 'سعر_الوحدة': 500.0,
268
+ 'وصف': 'مهندس إشراف موقع',
269
+ 'فئة': 'إشراف'
270
+ },
271
+ 'مراقب فني': {
272
+ 'وحدة': 'يوم',
273
+ 'سعر_الوحدة': 300.0,
274
+ 'وصف': 'مراقب فني للتنفيذ',
275
+ 'فئة': 'إشراف'
276
+ }
277
+ }
278
+
279
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
280
+ try:
281
+ file_path = os.path.join(config.DATA_DIR, 'labor_rates.json')
282
+ if os.path.exists(file_path):
283
+ with open(file_path, 'r', encoding='utf-8') as f:
284
+ loaded_data = json.load(f)
285
+ labor_rates.update(loaded_data)
286
+ except Exception as e:
287
+ print(f"خطأ في تحميل بيانات أسعار العمالة: {str(e)}")
288
+
289
+ return labor_rates
290
+
291
+ def _load_equipment_rates(self) -> Dict[str, Dict[str, Any]]:
292
+ """تحميل أسعار المعدات"""
293
+ # محاكاة تحميل البيانات من مصدر بيانات
294
+ equipment_rates = {
295
+ # معدات الحفر والتسوية
296
+ 'حفار صغير': {
297
+ 'وحدة': 'يوم',
298
+ 'سعر_الوحدة': 1200.0,
299
+ 'وصف': 'حفار صغير (بوبكات) بقدرة 70 حصان',
300
+ 'فئة': 'معدات حفر',
301
+ 'إنتاجية_يومية': {
302
+ 'حفر في تربة عادية': 60 # متر مكعب
303
+ }
304
+ },
305
+ 'حفار متوسط': {
306
+ 'وحدة': 'يوم',
307
+ 'سعر_الوحدة': 2500.0,
308
+ 'وصف': 'حفار متوسط الحجم بقدرة 150 حصان',
309
+ 'فئة': 'معدات حفر',
310
+ 'إنتاجية_يومية': {
311
+ 'حفر في تربة عادية': 200 # متر مكعب
312
+ }
313
+ },
314
+ 'لودر': {
315
+ 'وحدة': 'يوم',
316
+ 'سعر_الوحدة': 2000.0,
317
+ 'وصف': 'لودر أمامي لنقل التربة',
318
+ 'فئة': 'معدات حفر',
319
+ 'إنتاجية_يومية': {
320
+ 'تحميل تربة': 300, # متر مكعب
321
+ 'تسوية موقع': 1500 # متر مربع
322
+ }
323
+ },
324
+ 'جريدر': {
325
+ 'وحدة': 'يوم',
326
+ 'سعر_الوحدة': 2200.0,
327
+ 'وصف': 'جريدر لتسوية الموقع',
328
+ 'فئة': 'معدات حفر',
329
+ 'إنتاجية_يومية': {
330
+ 'تسوية طرق': 3000 # متر مربع
331
+ }
332
+ },
333
+
334
+ # معدات الخرسانة
335
+ 'خلاطة خرسانة': {
336
+ 'وحدة': 'يوم',
337
+ 'سعر_الوحدة': 350.0,
338
+ 'وصف': 'خلاطة خرسانة بسعة 0.5 متر مكعب',
339
+ 'فئة': 'معدات خرسانة',
340
+ 'إنتاجية_يومية': {
341
+ 'خلط خرسانة': 15 # متر مكعب
342
+ }
343
+ },
344
+ 'هزاز خرسانة': {
345
+ 'وحدة': 'يوم',
346
+ 'سعر_الوحدة': 150.0,
347
+ 'وصف': 'هزاز خرسانة كهربائي',
348
+ 'فئة': 'معدات خرسانة',
349
+ 'إنتاجية_يومية': {
350
+ 'دمك خرسانة': 40 # متر مكعب
351
+ }
352
+ },
353
+ 'شاحنة خرسانة جاهزة': {
354
+ 'وحدة': 'يوم',
355
+ 'سعر_الوحدة': 3000.0,
356
+ 'وصف': 'شاحنة خرسانة جاهزة (مكسر) سعة 8 متر مكعب',
357
+ 'فئة': 'معدات خرسانة',
358
+ 'إنتاجية_يومية': {
359
+ 'نقل وصب خرسانة': 50 # متر مكعب
360
+ }
361
+ },
362
+ 'مضخة خرسانة': {
363
+ 'وحدة': 'يوم',
364
+ 'سعر_الوحدة': 5000.0,
365
+ 'وصف': 'مضخة خرسانة بذراع 42 متر',
366
+ 'فئة': 'معدات خرسانة',
367
+ 'إنتاجية_يومية': {
368
+ 'ضخ خرسانة': 120 # متر مكعب
369
+ }
370
+ },
371
+
372
+ # معدات رفع ونقل
373
+ 'رافعة برجية': {
374
+ 'وحدة': 'شهر',
375
+ 'سعر_الوحدة': 35000.0,
376
+ 'وصف': 'رافعة برجية بارتفاع 40 متر',
377
+ 'فئة': 'معدات رفع',
378
+ },
379
+ 'ونش شوكة': {
380
+ 'وحدة': 'يوم',
381
+ 'سعر_الوحدة': 1500.0,
382
+ 'وصف': 'ونش شوكة لرفع مواد البناء',
383
+ 'فئة': 'معدات رفع',
384
+ 'إنتاجية_يومية': {
385
+ 'رفع ونقل مواد': 100 # طن
386
+ }
387
+ },
388
+ 'شاحنة نقل': {
389
+ 'وحدة': 'يوم',
390
+ 'سعر_الوحدة': 1200.0,
391
+ 'وصف': 'شاحنة نقل حمولة 20 طن',
392
+ 'فئة': 'معدات نقل',
393
+ 'إنتاجية_يومية': {
394
+ 'نقل مواد': 80 # طن
395
+ }
396
+ }
397
+ }
398
+
399
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
400
+ try:
401
+ file_path = os.path.join(config.DATA_DIR, 'equipment_rates.json')
402
+ if os.path.exists(file_path):
403
+ with open(file_path, 'r', encoding='utf-8') as f:
404
+ loaded_data = json.load(f)
405
+ equipment_rates.update(loaded_data)
406
+ except Exception as e:
407
+ print(f"خطأ في تحميل بيانات أسعار المعدات: {str(e)}")
408
+
409
+ return equipment_rates
410
+
411
+ def calculate_item_cost(self, item_data: Dict[str, Any]) -> Dict[str, Any]:
412
+ """
413
+ حساب تكلفة بند محدد بكافة مكوناته
414
+
415
+ المعلمات:
416
+ item_data (dict): بيانات البند، تتضمن:
417
+ - وصف_البند (str): وصف البند
418
+ - الكمية (float): كمية البند
419
+ - الوحدة (str): وحدة القياس
420
+ - المواد (list): قائمة المواد المستخدمة وكمياتها
421
+ - العمالة (list): قائمة العمالة المستخدمة وعددها
422
+ - المعدات (list): قائمة المعدات المستخدمة وساعات عملها
423
+ - المصاريف_الإدارية (float, optional): نسبة المصاريف الإدارية (افتراضياً 5%)
424
+ - هامش_الربح (float, optional): نسبة هامش الربح (افتراضياً 10%)
425
+ - عوامل_التعديل (dict, optional): عوامل تعديل التكلفة
426
+
427
+ العوائد:
428
+ dict: تفاصيل تكلفة البند بكافة عناصرها
429
+ """
430
+ # استخراج البيانات الأساسية للبند
431
+ item_description = item_data.get('وصف_البند', 'بند غير محدد')
432
+ quantity = item_data.get('الكمية', 0.0)
433
+ unit = item_data.get('الوحدة', 'وحدة')
434
+
435
+ # حساب تكلفة المواد
436
+ materials_cost = self._calculate_materials_cost(item_data.get('المواد', []))
437
+
438
+ # حساب تكلفة العمالة
439
+ labor_cost = self._calculate_labor_cost(item_data.get('العمالة', []))
440
+
441
+ # حساب تكلفة المعدات
442
+ equipment_cost = self._calculate_equipment_cost(item_data.get('المعدات', []))
443
+
444
+ # حساب التكلفة المباشرة الإجمالية
445
+ direct_cost = materials_cost['الإجمالي'] + labor_cost['الإجمالي'] + equipment_cost['الإجمالي']
446
+
447
+ # حساب المصاريف الإدارية
448
+ admin_percentage = item_data.get('المصاريف_الإدارية', self.default_admin_expenses_percentage)
449
+ admin_cost = direct_cost * admin_percentage
450
+
451
+ # حساب هامش الربح
452
+ profit_percentage = item_data.get('هامش_الربح', self.default_profit_margin_percentage)
453
+ profit_margin = (direct_cost + admin_cost) * profit_percentage
454
+
455
+ # حساب التكلفة الإجمالية
456
+ total_cost = direct_cost + admin_cost + profit_margin
457
+
458
+ # حساب سعر الوحدة
459
+ unit_price = total_cost / quantity if quantity > 0 else 0.0
460
+
461
+ # تطبيق عوامل التعديل إذا وجدت
462
+ adjustment_factors = item_data.get('عوامل_التعديل', self.default_adjustment_factors)
463
+ adjustment_factor = self._calculate_adjustment_factor(adjustment_factors)
464
+
465
+ adjusted_unit_price = unit_price * adjustment_factor
466
+ adjusted_total_cost = total_cost * adjustment_factor
467
+
468
+ # إعداد النتائج
469
+ result = {
470
+ 'وصف_البند': item_description,
471
+ 'الكمية': quantity,
472
+ 'الوحدة': unit,
473
+ 'تكاليف_مباشرة': {
474
+ 'المواد': materials_cost,
475
+ 'العمالة': labor_cost,
476
+ 'المعدات': equipment_cost,
477
+ 'إجمالي_تكاليف_مباشرة': direct_cost
478
+ },
479
+ 'مصاريف_إدارية': {
480
+ 'نسبة': admin_percentage * 100,
481
+ 'قيمة': admin_cost
482
+ },
483
+ 'هامش_ربح': {
484
+ 'نسبة': profit_percentage * 100,
485
+ 'قيمة': profit_margin
486
+ },
487
+ 'التكلفة_الإجمالية': total_cost,
488
+ 'سعر_الوحدة': unit_price,
489
+ 'عوامل_التعديل': {
490
+ 'المعامل_الإجمالي': adjustment_factor,
491
+ 'التفاصيل': adjustment_factors
492
+ },
493
+ 'السعر_المعدل': {
494
+ 'سعر_الوحدة': adjusted_unit_price,
495
+ 'إجمالي': adjusted_total_cost
496
+ }
497
+ }
498
+
499
+ return result
500
+
501
+ def _calculate_materials_cost(self, materials: List[Dict[str, Any]]) -> Dict[str, Any]:
502
+ """
503
+ حساب تكلفة المواد
504
+
505
+ المعلمات:
506
+ materials (list): قائمة المواد المستخدمة وكمياتها
507
+ - الاسم (str): اسم المادة
508
+ - الكمية (float): الكمية المستخدمة
509
+ - الوحدة (str, optional): وحدة القياس
510
+ - سعر_الوحدة (float, optional): سعر الوحدة (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده)
511
+
512
+ العوائد:
513
+ dict: تفاصيل تكلفة المواد
514
+ """
515
+ materials_details = []
516
+ total_cost = 0.0
517
+
518
+ for material in materials:
519
+ material_name = material.get('الاسم', '')
520
+ quantity = material.get('الكمية', 0.0)
521
+
522
+ # البحث عن سعر المادة من البيانات المرجعية إذا لم يتم تحديده
523
+ if 'سعر_الوحدة' in material:
524
+ unit_price = material.get('سعر_الوحدة', 0.0)
525
+ unit = material.get('الوحدة', 'وحدة')
526
+ elif material_name in self.material_rates:
527
+ ref_material = self.material_rates[material_name]
528
+ unit_price = ref_material.get('سعر_الوحدة', 0.0)
529
+ unit = ref_material.get('وحدة', 'وحدة')
530
+ else:
531
+ unit_price = 0.0
532
+ unit = material.get('الوحدة', 'وحدة')
533
+
534
+ # حساب التكلفة
535
+ cost = quantity * unit_price
536
+ total_cost += cost
537
+
538
+ # إضافة التفاصيل
539
+ materials_details.append({
540
+ 'الاسم': material_name,
541
+ 'الكمية': quantity,
542
+ 'الوحدة': unit,
543
+ 'سعر_الوحدة': unit_price,
544
+ 'التكلفة': cost
545
+ })
546
+
547
+ return {
548
+ 'التفاصيل': materials_details,
549
+ 'الإجمالي': total_cost
550
+ }
551
+
552
+ def _calculate_labor_cost(self, labor: List[Dict[str, Any]]) -> Dict[str, Any]:
553
+ """
554
+ حساب تكلفة العمالة
555
+
556
+ المعلمات:
557
+ labor (list): قائمة العمالة المستخدمة وعددها
558
+ - النوع (str): نوع العامل
559
+ - العدد (int): عدد العمال
560
+ - المدة (float): مدة العمل بالأيام
561
+ - سعر_اليوم (float, optional): أجر اليوم (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده)
562
+
563
+ العوائد:
564
+ dict: تفاصيل تكلفة العمالة
565
+ """
566
+ labor_details = []
567
+ total_cost = 0.0
568
+
569
+ for worker in labor:
570
+ worker_type = worker.get('النوع', '')
571
+ count = worker.get('العدد', 0)
572
+ duration = worker.get('المدة', 0.0)
573
+
574
+ # البحث عن سعر العامل من البيانات المرجعية إذا لم يتم تحديده
575
+ if 'سعر_اليوم' in worker:
576
+ daily_rate = worker.get('سعر_اليوم', 0.0)
577
+ elif worker_type in self.labor_rates:
578
+ daily_rate = self.labor_rates[worker_type].get('سعر_الوحدة', 0.0)
579
+ else:
580
+ daily_rate = 0.0
581
+
582
+ # حساب التكلفة
583
+ cost = count * duration * daily_rate
584
+ total_cost += cost
585
+
586
+ # إضافة التفاصيل
587
+ labor_details.append({
588
+ 'النوع': worker_type,
589
+ 'العدد': count,
590
+ 'المدة': duration,
591
+ 'سعر_اليوم': daily_rate,
592
+ 'التكلفة': cost
593
+ })
594
+
595
+ return {
596
+ 'التفاصيل': labor_details,
597
+ 'الإجمالي': total_cost
598
+ }
599
+
600
+ def _calculate_equipment_cost(self, equipment: List[Dict[str, Any]]) -> Dict[str, Any]:
601
+ """
602
+ حساب تكلفة المعدات
603
+
604
+ المعلمات:
605
+ equipment (list): قائمة المعدات المستخدمة وساعات عملها
606
+ - النوع (str): نوع المعدة
607
+ - العدد (int): عدد المعدات
608
+ - المدة (float): مدة الاستخدام بالأيام
609
+ - سعر_اليوم (float, optional): أجر اليوم (يستخدم السعر من البيانات المرجعية إذا لم يتم تحديده)
610
+
611
+ العوائد:
612
+ dict: تفاصيل تكلفة المعدات
613
+ """
614
+ equipment_details = []
615
+ total_cost = 0.0
616
+
617
+ for equip in equipment:
618
+ equip_type = equip.get('النوع', '')
619
+ count = equip.get('العدد', 0)
620
+ duration = equip.get('المدة', 0.0)
621
+
622
+ # البحث عن سعر المعدة من البيانات المرجعية إذا لم يتم تحديده
623
+ if 'سعر_اليوم' in equip:
624
+ daily_rate = equip.get('سعر_اليوم', 0.0)
625
+ elif equip_type in self.equipment_rates:
626
+ daily_rate = self.equipment_rates[equip_type].get('سعر_الوحدة', 0.0)
627
+ else:
628
+ daily_rate = 0.0
629
+
630
+ # حساب التكلفة
631
+ cost = count * duration * daily_rate
632
+ total_cost += cost
633
+
634
+ # إضافة التفاصيل
635
+ equipment_details.append({
636
+ 'النوع': equip_type,
637
+ 'العدد': count,
638
+ 'المدة': duration,
639
+ 'سعر_اليوم': daily_rate,
640
+ 'التكلفة': cost
641
+ })
642
+
643
+ return {
644
+ 'التفاصيل': equipment_details,
645
+ 'الإجمالي': total_cost
646
+ }
647
+
648
+ def _calculate_adjustment_factor(self, factors: Dict[str, float]) -> float:
649
+ """
650
+ حساب المعامل الإجمالي لتعديل التكلفة
651
+
652
+ المعلمات:
653
+ factors (dict): عوامل التعديل
654
+
655
+ العوائد:
656
+ float: المعامل الإجمالي
657
+ """
658
+ # دمج العوامل المحددة مع العوامل الافتراضية
659
+ effective_factors = self.default_adjustment_factors.copy()
660
+ effective_factors.update(factors)
661
+
662
+ # حساب المعامل الإجمالي
663
+ total_factor = 1.0
664
+ for factor in effective_factors.values():
665
+ total_factor *= factor
666
+
667
+ return total_factor
668
+
669
+ def calculate_project_cost(self, project_data: Dict[str, Any]) -> Dict[str, Any]:
670
+ """
671
+ حساب التكلفة الإجمالية لمشروع بناء كامل
672
+
673
+ المعلمات:
674
+ project_data (dict): بيانات المشروع، تتضمن:
675
+ - اسم_المشروع (str): اسم المشروع
676
+ - وصف_المشروع (str): وصف المشروع
677
+ - البنود (list): قائمة بنود المشروع
678
+ - المصاريف_الإدارية (float, optional): نسبة المصاريف الإدارية الإجمالية (افتراضياً 5%)
679
+ - هامش_الربح (float, optional): نسبة هامش الربح الإجمالي (افتراضياً 10%)
680
+ - عوامل_التعديل (dict, optional): عوامل تعديل التكلفة للمشروع
681
+
682
+ العوائد:
683
+ dict: تفاصيل تكلفة المشروع بكافة عناصرها
684
+ """
685
+ # استخراج البيانات الأساسية للمشروع
686
+ project_name = project_data.get('اسم_المشروع', 'مشروع غير محدد')
687
+ project_description = project_data.get('وصف_المشروع', '')
688
+ items = project_data.get('البنود', [])
689
+
690
+ # استخراج النسب الإجمالية
691
+ admin_percentage = project_data.get('المصاريف_الإدارية', self.default_admin_expenses_percentage)
692
+ profit_percentage = project_data.get('هامش_الربح', self.default_profit_margin_percentage)
693
+
694
+ # حساب تكلفة كل بند
695
+ items_costs = []
696
+ total_direct_cost = 0.0
697
+ total_materials_cost = 0.0
698
+ total_labor_cost = 0.0
699
+ total_equipment_cost = 0.0
700
+
701
+ for item_data in items:
702
+ # تحديث نسب المصاريف والربح للبند إذا لم تكن محددة
703
+ if 'المصاريف_الإدارية' not in item_data:
704
+ item_data['المصاريف_الإدارية'] = admin_percentage
705
+
706
+ if 'هامش_الربح' not in item_data:
707
+ item_data['هامش_الربح'] = profit_percentage
708
+
709
+ # حساب تكلفة البند
710
+ item_cost = self.calculate_item_cost(item_data)
711
+ items_costs.append(item_cost)
712
+
713
+ # تحديث الإجماليات
714
+ total_materials_cost += item_cost['تكاليف_مباشرة']['المواد']['الإجمالي']
715
+ total_labor_cost += item_cost['تكاليف_مباشرة']['العمالة']['الإجمالي']
716
+ total_equipment_cost += item_cost['تكاليف_مباشرة']['المعدات']['الإجمالي']
717
+ total_direct_cost += item_cost['تكاليف_مباشرة']['إجمالي_تكاليف_مباشرة']
718
+
719
+ # حساب المصاريف الإدارية
720
+ admin_cost = total_direct_cost * admin_percentage
721
+
722
+ # حساب هامش الربح
723
+ profit_margin = (total_direct_cost + admin_cost) * profit_percentage
724
+
725
+ # حساب التكلفة الإجمالية
726
+ total_cost = total_direct_cost + admin_cost + profit_margin
727
+
728
+ # تطبيق عوامل التعديل إذا وجدت
729
+ adjustment_factors = project_data.get('عوامل_التعديل', self.default_adjustment_factors)
730
+ adjustment_factor = self._calculate_adjustment_factor(adjustment_factors)
731
+
732
+ adjusted_total_cost = total_cost * adjustment_factor
733
+
734
+ # إعداد النتائج
735
+ result = {
736
+ 'اسم_المشروع': project_name,
737
+ 'وصف_المشروع': project_description,
738
+ 'تكاليف_مباشرة': {
739
+ 'المواد': {
740
+ 'الإجمالي': total_materials_cost,
741
+ 'النسبة_المئوية': (total_materials_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0
742
+ },
743
+ 'العمالة': {
744
+ 'الإجمالي': total_labor_cost,
745
+ 'النسبة_المئوية': (total_labor_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0
746
+ },
747
+ 'المعدات': {
748
+ 'الإجمالي': total_equipment_cost,
749
+ 'النسبة_المئوية': (total_equipment_cost / total_direct_cost * 100) if total_direct_cost > 0 else 0
750
+ },
751
+ 'إجمالي_تكاليف_مباشرة': total_direct_cost
752
+ },
753
+ 'مصاريف_إدارية': {
754
+ 'نسبة': admin_percentage * 100,
755
+ 'قيمة': admin_cost
756
+ },
757
+ 'هامش_ربح': {
758
+ 'نسبة': profit_percentage * 100,
759
+ 'قيمة': profit_margin
760
+ },
761
+ 'التكلفة_الإجمالية': total_cost,
762
+ 'عوامل_التعديل': {
763
+ 'المعامل_الإجمالي': adjustment_factor,
764
+ 'التفاصيل': adjustment_factors
765
+ },
766
+ 'التكلفة_النهائية_المعدلة': adjusted_total_cost,
767
+ 'تفاصيل_البنود': items_costs,
768
+ 'عدد_البنود': len(items)
769
+ }
770
+
771
+ return result
772
+
773
+ def get_rate_info(self, item_type: str, item_name: str) -> Dict[str, Any]:
774
+ """
775
+ الحصول على معلومات تفصيلية عن معدل وسعر عنصر محدد (مادة، عمالة، معدة)
776
+
777
+ المعلمات:
778
+ item_type (str): نوع العنصر - 'مادة'، 'عمالة'، 'معدة'
779
+ item_name (str): اسم العنصر
780
+
781
+ العوائد:
782
+ dict: معلومات تفصيلية عن العنصر
783
+ """
784
+ # تحديد القاموس المناسب حسب نوع العنصر
785
+ if item_type == 'مادة':
786
+ rates_dict = self.material_rates
787
+ elif item_type == 'عمالة':
788
+ rates_dict = self.labor_rates
789
+ elif item_type == 'معدة':
790
+ rates_dict = self.equipment_rates
791
+ else:
792
+ return {'خطأ': 'نوع العنصر غير صحيح'}
793
+
794
+ # البحث عن العنصر في القاموس
795
+ if item_name in rates_dict:
796
+ return rates_dict[item_name]
797
+ else:
798
+ return {'خطأ': 'العنصر غير موجود'}
799
+
800
+ def get_all_rates(self, item_type: str = None, category: str = None) -> Dict[str, Any]:
801
+ """
802
+ الحصول على قوائم معدلات الأسعار (لجميع المواد أو العمالة أو المعدات)
803
+
804
+ المعلمات:
805
+ item_type (str, optional): نوع العنصر - 'مادة'، 'عمالة'، 'معدة'، أو None لجميع الأنواع
806
+ category (str, optional): فئة محددة للتصفية
807
+
808
+ العوائد:
809
+ dict: قوائم معدلات الأسعار
810
+ """
811
+ result = {}
812
+
813
+ # جمع المواد حسب الفئة
814
+ if item_type is None or item_type == 'مادة':
815
+ materials = {}
816
+ for name, info in self.material_rates.items():
817
+ if category is None or info.get('فئة') == category:
818
+ materials[name] = info
819
+ result['المواد'] = materials
820
+
821
+ # جمع العمالة حسب الفئة
822
+ if item_type is None or item_type == 'عمالة':
823
+ labor = {}
824
+ for name, info in self.labor_rates.items():
825
+ if category is None or info.get('فئة') == category:
826
+ labor[name] = info
827
+ result['العمالة'] = labor
828
+
829
+ # جمع المعدات حسب الفئة
830
+ if item_type is None or item_type == 'معدة':
831
+ equipment = {}
832
+ for name, info in self.equipment_rates.items():
833
+ if category is None or info.get('فئة') == category:
834
+ equipment[name] = info
835
+ result['المعدات'] = equipment
836
+
837
+ return result
838
+
839
+ def generate_sample_project_data(self) -> Dict[str, Any]:
840
+ """
841
+ توليد بيانات نموذجية لمشروع بناء صغير للاختبار
842
+
843
+ العوائد:
844
+ dict: بيانات المشروع النموذجية
845
+ """
846
+ # إنشاء بيانات المشروع
847
+ project_data = {
848
+ 'اسم_المشروع': 'مبنى سكني صغير',
849
+ 'وصف_المشروع': 'مبنى سكني مكون من دور أرضي بمساحة 250 متر مربع',
850
+ 'المصاريف_الإدارية': 0.05, # 5%
851
+ 'هامش_الربح': 0.10, # 10%
852
+ 'عوامل_التعديل': {
853
+ 'location_factor': 1.2, # معامل الموقع (منطقة مرتفعة التكلفة)
854
+ 'time_factor': 1.0, # معامل الوقت
855
+ 'risk_factor': 1.05, # معامل المخاطر
856
+ 'market_factor': 1.0 # معامل السوق
857
+ },
858
+ 'البنود': [
859
+ # الأساسات
860
+ {
861
+ 'وصف_البند': 'حفر الأساسات بعمق 2 متر',
862
+ 'الكمية': 150.0,
863
+ 'الوحدة': 'م3',
864
+ 'المواد': [],
865
+ 'العمالة': [
866
+ {'النوع': 'عامل خرسانة', 'العدد': 4, 'المدة': 3}
867
+ ],
868
+ 'المعدات': [
869
+ {'النوع': 'حفار متوسط', 'العدد': 1, 'المدة': 2}
870
+ ]
871
+ },
872
+ {
873
+ 'وصف_البند': 'توريد وصب خرسانة عادية للأساسات',
874
+ 'الكمية': 25.0,
875
+ 'الوحدة': 'م3',
876
+ 'المواد': [
877
+ {'الاسم': 'خرسانة جاهزة', 'الكمية': 25.0}
878
+ ],
879
+ 'العمالة': [
880
+ {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1}
881
+ ],
882
+ 'المعدات': [
883
+ {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5}
884
+ ]
885
+ },
886
+ {
887
+ 'وصف_البند': 'توريد وتركيب حديد تسليح للأساسات',
888
+ 'الكمية': 3.5,
889
+ 'الوحدة': 'طن',
890
+ 'المواد': [
891
+ {'الاسم': 'حديد تسليح', 'الكمية': 3.5}
892
+ ],
893
+ 'العمالة': [
894
+ {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3}
895
+ ],
896
+ 'المعدات': []
897
+ },
898
+ {
899
+ 'وصف_البند': 'نجارة وفك شدة الأساسات',
900
+ 'الكمية': 120.0,
901
+ 'الوحدة': 'م2',
902
+ 'المواد': [],
903
+ 'العمالة': [
904
+ {'النوع': 'نجار مسلح', 'العدد': 4, 'المدة': 3}
905
+ ],
906
+ 'المعدات': []
907
+ },
908
+ {
909
+ 'وصف_البند': 'توريد وصب خرسانة مسلحة للأساسات',
910
+ 'الكمية': 30.0,
911
+ 'الوحدة': 'م3',
912
+ 'المواد': [
913
+ {'الاسم': 'خرسانة جاهزة', 'الكمية': 30.0}
914
+ ],
915
+ 'العمالة': [
916
+ {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1}
917
+ ],
918
+ 'المعدات': [
919
+ {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5},
920
+ {'النوع': 'هزاز خرسانة', 'العدد': 2, 'المدة': 1}
921
+ ]
922
+ },
923
+
924
+ # الأعمدة والأسقف
925
+ {
926
+ 'وصف_البند': 'توريد وتركيب حديد تسليح للأعمدة',
927
+ 'الكمية': 2.8,
928
+ 'الوحدة': 'طن',
929
+ 'المواد': [
930
+ {'الاسم': 'حديد تسليح', 'الكمية': 2.8}
931
+ ],
932
+ 'العمالة': [
933
+ {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3}
934
+ ],
935
+ 'المعدات': []
936
+ },
937
+ {
938
+ 'وصف_البند': 'نجارة وفك شدة الأعمدة',
939
+ 'الكمية': 85.0,
940
+ 'الوحدة': 'م2',
941
+ 'المواد': [],
942
+ 'العمالة': [
943
+ {'النوع': 'نجار مسلح', 'العدد': 3, 'المدة': 3}
944
+ ],
945
+ 'المعدات': []
946
+ },
947
+ {
948
+ 'وصف_البند': 'توريد وصب خرسانة مسلحة للأعمدة',
949
+ 'الكمية': 12.0,
950
+ 'الوحدة': 'م3',
951
+ 'المواد': [
952
+ {'الاسم': 'خرسانة جاهزة', 'الكمية': 12.0}
953
+ ],
954
+ 'العمالة': [
955
+ {'النوع': 'عامل خرسانة', 'العدد': 4, 'المدة': 1}
956
+ ],
957
+ 'المعدات': [
958
+ {'النوع': 'مضخة خرسانة', 'العدد': 1, 'المدة': 0.5},
959
+ {'النوع': 'هزاز خرسانة', 'العدد': 2, 'المدة': 1}
960
+ ]
961
+ },
962
+
963
+ # أعمال البناء
964
+ {
965
+ 'وصف_البند': 'توريد وبناء حوائط من الطوب الأحمر',
966
+ 'الكمية': 220.0,
967
+ 'الوحدة': 'م2',
968
+ 'المواد': [
969
+ {'الاسم': 'طوب أحمر', 'الكمية': 16.5} # بالألف
970
+ ],
971
+ 'العمالة': [
972
+ {'النوع': 'بناء', 'العدد': 4, 'المدة': 8},
973
+ {'النوع': 'مساعد بناء', 'العدد': 4, 'المدة': 8}
974
+ ],
975
+ 'المعدات': []
976
+ },
977
+
978
+ # أعمال التشطيبات
979
+ {
980
+ 'وصف_البند': 'توريد وتركيب بلاط سيراميك للأرضيات',
981
+ 'الكمية': 250.0,
982
+ 'الوحدة': 'م2',
983
+ 'المواد': [
984
+ {'الاسم': 'بلاط سيراميك', 'الكمية': 250.0}
985
+ ],
986
+ 'العمالة': [
987
+ {'النوع': 'مبلط', 'العدد': 4, 'المدة': 7}
988
+ ],
989
+ 'المعدات': []
990
+ },
991
+ {
992
+ 'وصف_البند': 'توريد وتنفيذ دهانات للحوائط',
993
+ 'الكمية': 450.0,
994
+ 'الوحدة': 'م2',
995
+ 'المواد': [
996
+ {'الاسم': 'دهانات بلاستيك', 'الكمية': 90.0} # بالتر
997
+ ],
998
+ 'العمالة': [
999
+ {'النوع': 'نقاش', 'العدد': 3, 'المدة': 8}
1000
+ ],
1001
+ 'المعدات': []
1002
+ }
1003
+ ]
1004
+ }
1005
+
1006
+ return project_data
modules/pricing/services/construction_templates.py ADDED
@@ -0,0 +1,748 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ كتالوج بنود نموذجية للمقاولات
3
+ يحتوي هذا الملف على قائمة كاملة من النماذج الجاهزة للبنود الشائعة في مشاريع المقاولات، مثل:
4
+ - أعمال الخرسانة بأنواعها
5
+ - المناهل وأنواع المواسير
6
+ - التركيبات المختلفة
7
+ - الطرق والأسفلت
8
+ - وغيرها من أعمال المقاولات
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import sys
14
+ from typing import Dict, List, Any, Optional
15
+ from datetime import datetime
16
+
17
+ # إضافة مسار النظام للوصول لملفات التكوين
18
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")))
19
+ try:
20
+ import config
21
+ except ImportError:
22
+ # إذا لم يتم العثور على ملف التكوين، نستخدم قيم افتراضية
23
+ class DefaultConfig:
24
+ DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../data"))
25
+ config = DefaultConfig
26
+
27
+ # إنشاء مجلد البيانات إذا لم يكن موجودًا
28
+ if not os.path.exists(config.DATA_DIR):
29
+ os.makedirs(config.DATA_DIR)
30
+
31
+
32
+ class ConstructionTemplates:
33
+ """كتالوج بنود نموذجية للمقاولات"""
34
+
35
+ def __init__(self):
36
+ """تهيئة كتالوج البنود النموذجية"""
37
+ self.templates_file = os.path.join(config.DATA_DIR, 'construction_templates.json')
38
+ self.market_prices_file = os.path.join(config.DATA_DIR, 'saudi_market_prices.json')
39
+
40
+ # تحميل قوالب البنود النموذجية
41
+ self.templates = self._load_templates()
42
+
43
+ # تحميل أسعار السوق السعودي
44
+ self.market_prices = self._load_market_prices()
45
+
46
+ def _load_templates(self) -> Dict[str, Dict[str, Any]]:
47
+ """تحميل قوالب البنود النموذجية من الملف"""
48
+ if os.path.exists(self.templates_file):
49
+ try:
50
+ with open(self.templates_file, 'r', encoding='utf-8') as f:
51
+ return json.load(f)
52
+ except Exception as e:
53
+ print(f"خطأ في تحميل قوالب البنود النموذجية: {str(e)}")
54
+
55
+ # إنشاء بيانات افتراضية إذا لم يتم العثور على الملف
56
+ default_templates = self._create_default_templates()
57
+
58
+ # حفظ البيانات الافتراضية
59
+ self._save_templates(default_templates)
60
+
61
+ return default_templates
62
+
63
+ def _save_templates(self, templates: Dict[str, Dict[str, Any]]) -> None:
64
+ """حفظ قوالب البنود النموذجية إلى الملف"""
65
+ try:
66
+ with open(self.templates_file, 'w', encoding='utf-8') as f:
67
+ json.dump(templates, f, ensure_ascii=False, indent=4)
68
+ except Exception as e:
69
+ print(f"خطأ في حفظ قوالب البنود النموذجية: {str(e)}")
70
+
71
+ def _load_market_prices(self) -> Dict[str, Dict[str, Any]]:
72
+ """تحميل أسعار السوق السعودي من الملف"""
73
+ if os.path.exists(self.market_prices_file):
74
+ try:
75
+ with open(self.market_prices_file, 'r', encoding='utf-8') as f:
76
+ return json.load(f)
77
+ except Exception as e:
78
+ print(f"خطأ في تحميل أسعار السوق السعودي: {str(e)}")
79
+
80
+ # إنشاء بيانات افتراضية إذا لم يتم العثور على الملف
81
+ default_prices = self._create_default_market_prices()
82
+
83
+ # حفظ البيانات الافتراضية
84
+ self._save_market_prices(default_prices)
85
+
86
+ return default_prices
87
+
88
+ def _save_market_prices(self, prices: Dict[str, Dict[str, Any]]) -> None:
89
+ """حفظ أسعار السوق السعودي إلى الملف"""
90
+ try:
91
+ with open(self.market_prices_file, 'w', encoding='utf-8') as f:
92
+ json.dump(prices, f, ensure_ascii=False, indent=4)
93
+ except Exception as e:
94
+ print(f"خطأ في حفظ أسعار السوق السعودي: {str(e)}")
95
+
96
+ def _create_default_templates(self) -> Dict[str, Dict[str, Any]]:
97
+ """إنشاء قوالب افتراضية للبنود النموذجية"""
98
+ templates = {
99
+ "categories": {
100
+ "أعمال_خرسانية": {
101
+ "name": "أعمال خرسانية",
102
+ "description": "بنود أعمال الخرسانة المسلحة والعادية",
103
+ "icon": "building"
104
+ },
105
+ "أعمال_صحية": {
106
+ "name": "أعمال صحية",
107
+ "description": "بنود أعمال المناهل والمواسير والتركيبات الصحية",
108
+ "icon": "pipe"
109
+ },
110
+ "أعمال_طرق": {
111
+ "name": "أعمال طرق",
112
+ "description": "بنود أعمال الطرق والأسفلت والرصف",
113
+ "icon": "road"
114
+ },
115
+ "أعمال_كهربائية": {
116
+ "name": "أعمال كهربائية",
117
+ "description": "بنود أعمال الكهرباء والإنارة",
118
+ "icon": "zap"
119
+ },
120
+ "أعمال_ميكانيكية": {
121
+ "name": "أعمال ميكانيكية",
122
+ "description": "بنود أعمال التكييف والتهوية والتبريد",
123
+ "icon": "thermometer"
124
+ }
125
+ },
126
+ "templates": {
127
+ # نماذج أعمال خرسانية
128
+ "خرسانة_مسلحة_أساسات": {
129
+ "category": "أعمال_خرسانية",
130
+ "name": "خرسانة مسلحة للأساسات",
131
+ "description": "توريد وصب خرسانة مسلحة للأساسات بقوة لا تقل عن 300 كجم/سم2",
132
+ "unit": "م3",
133
+ "components": {
134
+ "materials": [
135
+ {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0},
136
+ {"الاسم": "حديد تسليح", "الكمية": 0.12, "الوحدة": "طن", "سعر_الوحدة": 5500.0}
137
+ ],
138
+ "labor": [
139
+ {"النوع": "عامل خرسانة", "العدد": 4, "المدة": 0.3, "سعر_اليوم": 150.0},
140
+ {"النوع": "نجار مسلح", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 250.0},
141
+ {"النوع": "حداد مسلح", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 250.0}
142
+ ],
143
+ "equipment": [
144
+ {"النوع": "هزاز خرسانة", "العدد": 1, "المدة": 0.3, "سعر_اليوم": 150.0}
145
+ ]
146
+ },
147
+ "admin_expenses": 0.05,
148
+ "profit_margin": 0.10,
149
+ "tags": ["خرسانة", "أساسات", "مسلحة"]
150
+ },
151
+ "خرسانة_مسلحة_أعمدة": {
152
+ "category": "أعمال_خرسانية",
153
+ "name": "خرسانة مسلحة للأعمدة",
154
+ "description": "توريد وصب خرسانة مسلحة للأعمدة بقوة لا تقل عن 350 كجم/سم2",
155
+ "unit": "م3",
156
+ "components": {
157
+ "materials": [
158
+ {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0},
159
+ {"الاسم": "حديد تسليح", "الكمية": 0.18, "الوحدة": "طن", "سعر_الوحدة": 5500.0}
160
+ ],
161
+ "labor": [
162
+ {"النوع": "عامل خرسانة", "العدد": 4, "المدة": 0.4, "سعر_اليوم": 150.0},
163
+ {"النوع": "نجار مسلح", "العدد": 2, "المدة": 0.7, "سعر_اليوم": 250.0},
164
+ {"النوع": "حداد مسلح", "العدد": 2, "المدة": 0.7, "سعر_اليوم": 250.0}
165
+ ],
166
+ "equipment": [
167
+ {"النوع": "هزاز خرسانة", "العدد": 2, "المدة": 0.4, "سعر_اليوم": 150.0}
168
+ ]
169
+ },
170
+ "admin_expenses": 0.05,
171
+ "profit_margin": 0.10,
172
+ "tags": ["خرسانة", "أعمدة", "مسلحة"]
173
+ },
174
+ "خرسانة_مسلحة_أسقف": {
175
+ "category": "أعمال_خرسانية",
176
+ "name": "خرسانة مسلحة للأسقف",
177
+ "description": "توريد وصب خرسانة مسلحة للأسقف والبلاطات بقوة لا تقل عن 350 كجم/سم2",
178
+ "unit": "م3",
179
+ "components": {
180
+ "materials": [
181
+ {"الاسم": "خرسانة جاهزة", "الكمية": 1.0, "الوحدة": "م3", "سعر_الوحدة": 750.0},
182
+ {"الاسم": "حديد تسليح", "الكمية": 0.16, "الوحدة": "طن", "سعر_الوحدة": 5500.0}
183
+ ],
184
+ "labor": [
185
+ {"النوع": "عامل خرسانة", "العدد": 5, "المدة": 0.5, "سعر_اليوم": 150.0},
186
+ {"النوع": "نجار مسلح", "العدد": 3, "المدة": 0.8, "سعر_اليوم": 250.0},
187
+ {"النوع": "حداد مسلح", "العدد": 3, "المدة": 0.8, "سعر_اليوم": 250.0}
188
+ ],
189
+ "equipment": [
190
+ {"النوع": "هزاز خرسانة", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 150.0}
191
+ ]
192
+ },
193
+ "admin_expenses": 0.05,
194
+ "profit_margin": 0.10,
195
+ "tags": ["خرسانة", "أسقف", "بلاطات", "مسلحة"]
196
+ },
197
+
198
+ # نماذج أعمال صحية
199
+ "منهل_تفتيش_خرساني": {
200
+ "category": "أعمال_صحية",
201
+ "name": "منهل تفتيش خرساني",
202
+ "description": "توريد وتركيب منهل تفتيش خرساني قطر 1 متر وعمق 2 متر",
203
+ "unit": "عدد",
204
+ "components": {
205
+ "materials": [
206
+ {"الاسم": "خرسانة جاهزة", "الكمية": 1.5, "الوحدة": "م3", "سعر_الوحدة": 750.0},
207
+ {"الاسم": "حديد تسليح", "الكمية": 0.15, "الوحدة": "طن", "سعر_الوحدة": 5500.0},
208
+ {"الاسم": "غطاء منهل حديد", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 1500.0}
209
+ ],
210
+ "labor": [
211
+ {"النوع": "عامل خرسانة", "العدد": 3, "المدة": 1, "سعر_اليوم": 150.0},
212
+ {"النوع": "نجار مسلح", "العدد": 2, "المدة": 1, "سعر_اليوم": 250.0},
213
+ {"النوع": "حداد مسلح", "العدد": 1, "المدة": 1, "سعر_اليوم": 250.0},
214
+ {"النوع": "سباك", "العدد": 2, "المدة": 1, "سعر_اليوم": 250.0}
215
+ ],
216
+ "equipment": [
217
+ {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.5, "سعر_اليوم": 1200.0}
218
+ ]
219
+ },
220
+ "admin_expenses": 0.05,
221
+ "profit_margin": 0.12,
222
+ "tags": ["صرف صحي", "منهل", "تفتيش"]
223
+ },
224
+ "مواسير_بلاستيك_قطر_200_مم": {
225
+ "category": "أعمال_صحية",
226
+ "name": "مواسير بلاستيك قطر 200 مم",
227
+ "description": "توريد وتركيب مواسير بلاستيك UPVC قطر 200 مم لشبكات الصرف الصحي",
228
+ "unit": "م.ط",
229
+ "components": {
230
+ "materials": [
231
+ {"الاسم": "مواسير بلاستيك UPVC قطر 200 مم", "الكمية": 1.05, "الوحدة": "م.ط", "سعر_الوحدة": 180.0},
232
+ {"الاسم": "وصلات ومثبتات", "الكمية": 1, "الوحدة": "مجموعة", "سعر_الوحدة": 35.0},
233
+ {"الاسم": "مواد لاصقة", "الكمية": 0.1, "الوحدة": "لتر", "سعر_الوحدة": 120.0}
234
+ ],
235
+ "labor": [
236
+ {"النوع": "سباك", "العدد": 2, "المدة": 0.2, "سعر_اليوم": 250.0},
237
+ {"النوع": "مساعد سباك", "العدد": 2, "المدة": 0.2, "سعر_اليوم": 120.0}
238
+ ],
239
+ "equipment": [
240
+ {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.1, "سعر_اليوم": 1200.0}
241
+ ]
242
+ },
243
+ "admin_expenses": 0.05,
244
+ "profit_margin": 0.12,
245
+ "tags": ["صرف صحي", "مواسير", "بلاستيك"]
246
+ },
247
+
248
+ # نماذج أعمال طرق
249
+ "طبقة_أساس_للطرق": {
250
+ "category": "أعمال_طرق",
251
+ "name": "طبقة أساس للطرق",
252
+ "description": "توريد وفرد ودمك طبقة أساس للطرق سمك 20 سم، درجة دمك 98%",
253
+ "unit": "م3",
254
+ "components": {
255
+ "materials": [
256
+ {"الاسم": "مواد طبقة أساس", "��لكمية": 1.25, "الوحدة": "م3", "سعر_الوحدة": 90.0},
257
+ {"الاسم": "مياه للدمك", "الكمية": 0.2, "الوحدة": "م3", "سعر_الوحدة": 10.0}
258
+ ],
259
+ "labor": [
260
+ {"النوع": "عامل طرق", "العدد": 4, "المدة": 0.05, "سعر_اليوم": 150.0},
261
+ {"النوع": "مراقب فني", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 300.0}
262
+ ],
263
+ "equipment": [
264
+ {"النوع": "جريدر", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 2200.0},
265
+ {"النوع": "رصاصة دمك", "العدد": 1, "المدة": 0.05, "سعر_اليوم": 1800.0},
266
+ {"النوع": "شاحنة نقل", "العدد": 2, "المدة": 0.05, "سعر_اليوم": 1200.0}
267
+ ]
268
+ },
269
+ "admin_expenses": 0.05,
270
+ "profit_margin": 0.12,
271
+ "tags": ["طرق", "أساس", "دمك"]
272
+ },
273
+ "طبقة_إسفلت_سطحية": {
274
+ "category": "أعمال_طرق",
275
+ "name": "طبقة إسفلت سطحية",
276
+ "description": "توريد وفرد ودمك طبقة إسفلت سطحية سمك 5 سم",
277
+ "unit": "م2",
278
+ "components": {
279
+ "materials": [
280
+ {"الاسم": "خلطة إسفلتية ساخنة", "الكمية": 0.125, "الوحدة": "طن", "سعر_الوحدة": 400.0},
281
+ {"الاسم": "مواد رش تأسيسي", "الكمية": 0.5, "الوحدة": "لتر", "سعر_الوحدة": 8.0}
282
+ ],
283
+ "labor": [
284
+ {"النوع": "عامل طرق", "العدد": 6, "المدة": 0.01, "سعر_اليوم": 150.0},
285
+ {"النوع": "مراقب فني", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 300.0}
286
+ ],
287
+ "equipment": [
288
+ {"النوع": "فرادة إسفلت", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 4000.0},
289
+ {"النوع": "رصاصة دمك", "العدد": 2, "المدة": 0.01, "سعر_اليوم": 1800.0},
290
+ {"النوع": "سيارة رش إسفلت", "العدد": 1, "المدة": 0.01, "سعر_اليوم": 2000.0},
291
+ {"النوع": "شاحنة نقل", "العدد": 4, "المدة": 0.01, "سعر_اليوم": 1200.0}
292
+ ]
293
+ },
294
+ "admin_expenses": 0.05,
295
+ "profit_margin": 0.12,
296
+ "tags": ["طرق", "إسفلت", "سطحية"]
297
+ },
298
+
299
+ # نماذج أعمال كهربائية
300
+ "عمود_إنارة_10_متر": {
301
+ "category": "أعمال_كهربائية",
302
+ "name": "عمود إنارة 10 متر",
303
+ "description": "توريد وتركيب عمود إنارة جلفانيزي بارتفاع 10 متر مع ذراع مفردة وكشاف LED بقدرة 150 واط",
304
+ "unit": "عدد",
305
+ "components": {
306
+ "materials": [
307
+ {"الاسم": "عمود إنارة جلفانيزي 10 متر", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 3500.0},
308
+ {"الاسم": "ذراع إنارة مفردة", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 450.0},
309
+ {"الاسم": "كشاف LED 150 واط", "الكمية": 1, "الوحدة": "عدد", "سعر_الوحدة": 850.0},
310
+ {"الاسم": "كابل كهرباء 3×4 مم²", "الكمية": 15, "الوحدة": "م.ط", "سعر_الوحدة": 32.0},
311
+ {"الاسم": "قاعدة خرسانية مسلحة", "الكمية": 0.25, "الوحدة": "م3", "سعر_الوحدة": 750.0}
312
+ ],
313
+ "labor": [
314
+ {"النوع": "كهربائي", "العدد": 2, "المدة": 1, "سعر_اليوم": 270.0},
315
+ {"النوع": "مساعد كهربائي", "العدد": 2, "المدة": 1, "سعر_اليوم": 120.0},
316
+ {"النوع": "عامل خرسانة", "العدد": 2, "المدة": 0.5, "سعر_اليوم": 150.0}
317
+ ],
318
+ "equipment": [
319
+ {"النوع": "ونش شوكة", "العدد": 1, "المدة": 0.5, "سعر_اليوم": 1500.0},
320
+ {"النوع": "حفار صغير", "العدد": 1, "المدة": 0.2, "سعر_اليوم": 1200.0}
321
+ ]
322
+ },
323
+ "admin_expenses": 0.05,
324
+ "profit_margin": 0.12,
325
+ "tags": ["كهرباء", "إنارة", "LED"]
326
+ }
327
+ }
328
+ }
329
+
330
+ return templates
331
+
332
+ def _create_default_market_prices(self) -> Dict[str, Dict[str, Any]]:
333
+ """إنشاء بيانات افتراضية لأسعار السوق السعودي"""
334
+ current_date = datetime.now().strftime("%Y-%m-%d")
335
+
336
+ prices = {
337
+ "metadata": {
338
+ "last_update": current_date,
339
+ "source": "أسعار السوق السعودي الافتراضية",
340
+ "disclaimer": "هذه الأسعار تقريبية وقد تختلف حسب المنطقة والكميات والموردين"
341
+ },
342
+ "materials": {
343
+ # مواد الخرسانة
344
+ "خرسانة_جاهزة": {
345
+ "name": "خرسانة جاهزة",
346
+ "unit": "م3",
347
+ "current_price": 750.0,
348
+ "previous_price": 730.0,
349
+ "price_trend": "up",
350
+ "category": "أعمال خرسانية",
351
+ "specifications": "خرسانة جاهزة بقوة 350 كجم/سم2",
352
+ "note": "السعر يشمل توريد فقط، الضخ بتكلفة إضافية",
353
+ "price_history": [
354
+ {"date": "2023-06-01", "price": 700.0},
355
+ {"date": "2023-09-01", "price": 715.0},
356
+ {"date": "2023-12-01", "price": 730.0},
357
+ {"date": current_date, "price": 750.0}
358
+ ]
359
+ },
360
+ "حديد_تسليح": {
361
+ "name": "حديد تسليح",
362
+ "unit": "طن",
363
+ "current_price": 5500.0,
364
+ "previous_price": 5200.0,
365
+ "price_trend": "up",
366
+ "category": "أعمال خرسانية",
367
+ "specifications": "حديد تسليح قطر 8-32 مم، انتاج سابك",
368
+ "note": "السعر يتغير بشكل دوري حسب أسعار الحديد العالمية",
369
+ "price_history": [
370
+ {"date": "2023-06-01", "price": 4800.0},
371
+ {"date": "2023-09-01", "price": 5000.0},
372
+ {"date": "2023-12-01", "price": 5200.0},
373
+ {"date": current_date, "price": 5500.0}
374
+ ]
375
+ },
376
+ "أسمنت": {
377
+ "name": "أسمنت",
378
+ "unit": "كيس",
379
+ "current_price": 30.0,
380
+ "previous_price": 28.0,
381
+ "price_trend": "up",
382
+ "category": "أعمال خرسانية",
383
+ "specifications": "أسمنت بورتلاندي عادي، كيس 50 كجم",
384
+ "note": "السعر للكميات الكبيرة",
385
+ "price_history": [
386
+ {"date": "2023-06-01", "price": 25.0},
387
+ {"date": "2023-09-01", "price": 27.0},
388
+ {"date": "2023-12-01", "price": 28.0},
389
+ {"date": current_date, "price": 30.0}
390
+ ]
391
+ },
392
+
393
+ # مواد الطرق والإسفلت
394
+ "خلطة_إسفلتية_ساخنة": {
395
+ "name": "خلطة إسفلتية ساخنة",
396
+ "unit": "طن",
397
+ "current_price": 400.0,
398
+ "previous_price": 380.0,
399
+ "price_trend": "up",
400
+ "category": "أعمال طرق",
401
+ "specifications": "خلطة إسفلتية ساخنة للطبقة السطحية",
402
+ "note": "السعر يشمل التوريد من المصنع، النقل بتكلفة إضافية",
403
+ "price_history": [
404
+ {"date": "2023-06-01", "price": 350.0},
405
+ {"date": "2023-09-01", "price": 370.0},
406
+ {"date": "2023-12-01", "price": 380.0},
407
+ {"date": current_date, "price": 400.0}
408
+ ]
409
+ },
410
+
411
+ # مواد صحية
412
+ "مواسير_بلاستيك_UPVC": {
413
+ "name": "مواسير بلاستيك UPVC قطر 200 مم",
414
+ "unit": "م.ط",
415
+ "current_price": 180.0,
416
+ "previous_price": 165.0,
417
+ "price_trend": "up",
418
+ "category": "أعمال صحية",
419
+ "specifications": "مواسير بلاستيك UPVC قطر 200 مم لشبكات الصرف الصحي",
420
+ "note": "السعر للكميات الكبيرة",
421
+ "price_history": [
422
+ {"date": "2023-06-01", "price": 150.0},
423
+ {"date": "2023-09-01", "price": 160.0},
424
+ {"date": "2023-12-01", "price": 165.0},
425
+ {"date": current_date, "price": 180.0}
426
+ ]
427
+ },
428
+
429
+ # مواد كهربائية
430
+ "كشاف_LED": {
431
+ "name": "كشاف LED 150 واط",
432
+ "unit": "عدد",
433
+ "current_price": 850.0,
434
+ "previous_price": 820.0,
435
+ "price_trend": "up",
436
+ "category": "أعمال كهربائية",
437
+ "specifications": "كشاف إنارة LED بقدرة 150 واط للاستخدام الخارجي، IP65",
438
+ "note": "السعر شامل الضريبة",
439
+ "price_history": [
440
+ {"date": "2023-06-01", "price": 780.0},
441
+ {"date": "2023-09-01", "price": 800.0},
442
+ {"date": "2023-12-01", "price": 820.0},
443
+ {"date": current_date, "price": 850.0}
444
+ ]
445
+ }
446
+ },
447
+ "labor": {
448
+ "عامل_خرسانة": {
449
+ "name": "عامل خرسانة",
450
+ "unit": "يوم",
451
+ "current_price": 150.0,
452
+ "previous_price": 140.0,
453
+ "price_trend": "up",
454
+ "category": "عمالة",
455
+ "specifications": "عامل لصب وتسوية الخرسانة",
456
+ "note": "أجرة اليوم الواحد لا تشمل السكن والمواصلات",
457
+ "price_history": [
458
+ {"date": "2023-06-01", "price": 130.0},
459
+ {"date": "2023-09-01", "price": 135.0},
460
+ {"date": "2023-12-01", "price": 140.0},
461
+ {"date": current_date, "price": 150.0}
462
+ ]
463
+ },
464
+ "مهندس_موقع": {
465
+ "name": "مهندس موقع",
466
+ "unit": "يوم",
467
+ "current_price": 500.0,
468
+ "previous_price": 480.0,
469
+ "price_trend": "up",
470
+ "category": "إشراف",
471
+ "specifications": "مهندس إشراف موقع",
472
+ "note": "أجرة اليوم الواحد لا تشمل السكن والمواصلات",
473
+ "price_history": [
474
+ {"date": "2023-06-01", "price": 450.0},
475
+ {"date": "2023-09-01", "price": 470.0},
476
+ {"date": "2023-12-01", "price": 480.0},
477
+ {"date": current_date, "price": 500.0}
478
+ ]
479
+ }
480
+ },
481
+ "equipment": {
482
+ "حفار_صغير": {
483
+ "name": "حفار صغير",
484
+ "unit": "يوم",
485
+ "current_price": 1200.0,
486
+ "previous_price": 1150.0,
487
+ "price_trend": "up",
488
+ "category": "معدات حفر",
489
+ "specifications": "حفار صغير (بوبكات) بقدرة 70 حصان",
490
+ "note": "السعر يشمل المشغل والوقود",
491
+ "price_history": [
492
+ {"date": "2023-06-01", "price": 1100.0},
493
+ {"date": "2023-09-01", "price": 1120.0},
494
+ {"date": "2023-12-01", "price": 1150.0},
495
+ {"date": current_date, "price": 1200.0}
496
+ ]
497
+ },
498
+ "فرادة_إسفلت": {
499
+ "name": "فرادة إسفلت",
500
+ "unit": "يوم",
501
+ "current_price": 4000.0,
502
+ "previous_price": 3800.0,
503
+ "price_trend": "up",
504
+ "category": "معدات طرق",
505
+ "specifications": "فرادة إسفلت بعرض 3 متر",
506
+ "note": "السعر يشمل المشغل والوقود",
507
+ "price_history": [
508
+ {"date": "2023-06-01", "price": 3500.0},
509
+ {"date": "2023-09-01", "price": 3650.0},
510
+ {"date": "2023-12-01", "price": 3800.0},
511
+ {"date": current_date, "price": 4000.0}
512
+ ]
513
+ }
514
+ }
515
+ }
516
+
517
+ return prices
518
+
519
+ def get_all_templates(self) -> Dict[str, Dict[str, Any]]:
520
+ """الحصول على جميع القوالب النموذجية"""
521
+ return self.templates
522
+
523
+ def get_templates_by_category(self, category_id: str) -> List[Dict[str, Any]]:
524
+ """الحصول على القوالب النموذجية حسب الفئة"""
525
+ result = []
526
+
527
+ # التحقق من وجود الفئة
528
+ if category_id not in self.templates["categories"]:
529
+ return result
530
+
531
+ # جمع القوالب التي تنتمي إلى الفئة المحددة
532
+ for template_id, template in self.templates["templates"].items():
533
+ if template["category"] == category_id:
534
+ template_copy = template.copy()
535
+ template_copy["id"] = template_id
536
+ result.append(template_copy)
537
+
538
+ return result
539
+
540
+ def get_template_by_id(self, template_id: str) -> Optional[Dict[str, Any]]:
541
+ """الحصول على قالب نموذجي بواسطة المعرف"""
542
+ if template_id in self.templates["templates"]:
543
+ template = self.templates["templates"][template_id].copy()
544
+ template["id"] = template_id
545
+ return template
546
+
547
+ return None
548
+
549
+ def add_template(self, template_data: Dict[str, Any]) -> str:
550
+ """إضافة قالب نموذجي جديد"""
551
+ # إنشاء معرف فريد للقالب
552
+ template_name = template_data.get("name", "").strip()
553
+ if not template_name:
554
+ raise ValueError("يجب تحديد اسم القالب")
555
+
556
+ # تحويل الاسم إلى معرف (باستبدال المسافات بالشرطات السفلية وإزالة الأحرف الخاصة)
557
+ import re
558
+ template_id = re.sub(r'[^\w\s]', '', template_name)
559
+ template_id = template_id.replace(" ", "_")
560
+
561
+ # إضافة رقم عشوائي لتجنب التكرار
562
+ import random
563
+ if template_id in self.templates["templates"]:
564
+ template_id = f"{template_id}_{random.randint(1000, 9999)}"
565
+
566
+ # إضافة القالب إلى القائمة
567
+ self.templates["templates"][template_id] = template_data
568
+
569
+ # حفظ التغييرات
570
+ self._save_templates(self.templates)
571
+
572
+ return template_id
573
+
574
+ def update_template(self, template_id: str, template_data: Dict[str, Any]) -> bool:
575
+ """تحديث قالب نموذجي موجود"""
576
+ if template_id not in self.templates["templates"]:
577
+ return False
578
+
579
+ # تحديث القالب
580
+ self.templates["templates"][template_id] = template_data
581
+
582
+ # حفظ التغييرات
583
+ self._save_templates(self.templates)
584
+
585
+ return True
586
+
587
+ def delete_template(self, template_id: str) -> bool:
588
+ """حذف قالب نموذجي"""
589
+ if template_id not in self.templates["templates"]:
590
+ return False
591
+
592
+ # حذف القالب
593
+ del self.templates["templates"][template_id]
594
+
595
+ # حفظ التغييرات
596
+ self._save_templates(self.templates)
597
+
598
+ return True
599
+
600
+ def get_market_prices(self, category: Optional[str] = None, item_type: Optional[str] = None) -> Dict[str, Any]:
601
+ """الحصول على أسعار السوق السعودي"""
602
+ result = {
603
+ "metadata": self.market_prices["metadata"]
604
+ }
605
+
606
+ # تحديد نوع العناصر المطلوبة
607
+ sections = []
608
+ if item_type:
609
+ if item_type in ["materials", "المواد"]:
610
+ sections = ["materials"]
611
+ elif item_type in ["labor", "العمالة"]:
612
+ sections = ["labor"]
613
+ elif item_type in ["equipment", "المعدات"]:
614
+ sections = ["equipment"]
615
+ else:
616
+ sections = ["materials", "labor", "equipment"]
617
+
618
+ # جمع العناصر
619
+ for section in sections:
620
+ result[section] = {}
621
+ for item_id, item_data in self.market_prices[section].items():
622
+ if not category or (item_data.get("category", "") == category):
623
+ result[section][item_id] = item_data
624
+
625
+ return result
626
+
627
+ def update_market_price(self, item_type: str, item_id: str, new_price: float) -> bool:
628
+ """تحديث سعر في قائمة أسعار السوق"""
629
+ section = ""
630
+ if item_type in ["materials", "المواد"]:
631
+ section = "materials"
632
+ elif item_type in ["labor", "العمالة"]:
633
+ section = "labor"
634
+ elif item_type in ["equipment", "المعدات"]:
635
+ section = "equipment"
636
+ else:
637
+ return False
638
+
639
+ if item_id not in self.market_prices[section]:
640
+ return False
641
+
642
+ # تحديث السعر
643
+ current_price = self.market_prices[section][item_id]["current_price"]
644
+ self.market_prices[section][item_id]["previous_price"] = current_price
645
+ self.market_prices[section][item_id]["current_price"] = new_price
646
+
647
+ # تحديد اتجاه السعر
648
+ if new_price > current_price:
649
+ self.market_prices[section][item_id]["price_trend"] = "up"
650
+ elif new_price < current_price:
651
+ self.market_prices[section][item_id]["price_trend"] = "down"
652
+ else:
653
+ self.market_prices[section][item_id]["price_trend"] = "stable"
654
+
655
+ # إضافة السعر الجديد إلى تاريخ الأسعار
656
+ current_date = datetime.now().strftime("%Y-%m-%d")
657
+ self.market_prices[section][item_id]["price_history"].append({
658
+ "date": current_date,
659
+ "price": new_price
660
+ })
661
+
662
+ # تحديث تاريخ آخر تحديث
663
+ self.market_prices["metadata"]["last_update"] = current_date
664
+
665
+ # حفظ التغييرات
666
+ self._save_market_prices(self.market_prices)
667
+
668
+ return True
669
+
670
+ def add_market_price_item(self, item_type: str, item_data: Dict[str, Any]) -> str:
671
+ """إضافة عنصر جديد إلى قائمة أسعار السوق"""
672
+ section = ""
673
+ if item_type in ["materials", "المواد"]:
674
+ section = "materials"
675
+ elif item_type in ["labor", "العمالة"]:
676
+ section = "labor"
677
+ elif item_type in ["equipment", "المعدات"]:
678
+ section = "equipment"
679
+ else:
680
+ raise ValueError("نوع العنصر غير صحيح")
681
+
682
+ # التحقق من البيانات الأساسية
683
+ if "name" not in item_data or "current_price" not in item_data or "unit" not in item_data:
684
+ raise ValueError("يجب تحديد الاسم والسعر الحالي والوحدة")
685
+
686
+ # إنشاء معرف فريد للعنصر
687
+ item_name = item_data["name"].strip()
688
+ import re
689
+ item_id = re.sub(r'[^\w\s]', '', item_name)
690
+ item_id = item_id.replace(" ", "_")
691
+
692
+ # إضافة رقم عشوائي لتجنب التكرار
693
+ import random
694
+ if item_id in self.market_prices[section]:
695
+ item_id = f"{item_id}_{random.randint(1000, 9999)}"
696
+
697
+ # إعداد بيانات العنصر
698
+ current_date = datetime.now().strftime("%Y-%m-%d")
699
+ new_item = {
700
+ "name": item_name,
701
+ "unit": item_data["unit"],
702
+ "current_price": item_data["current_price"],
703
+ "previous_price": item_data.get("previous_price", item_data["current_price"]),
704
+ "price_trend": "stable",
705
+ "category": item_data.get("category", ""),
706
+ "specifications": item_data.get("specifications", ""),
707
+ "note": item_data.get("note", ""),
708
+ "price_history": [
709
+ {"date": current_date, "price": item_data["current_price"]}
710
+ ]
711
+ }
712
+
713
+ # إضافة العنصر إلى القائمة
714
+ self.market_prices[section][item_id] = new_item
715
+
716
+ # تحديث تاريخ آخر تحديث
717
+ self.market_prices["metadata"]["last_update"] = current_date
718
+
719
+ # حفظ التغييرات
720
+ self._save_market_prices(self.market_prices)
721
+
722
+ return item_id
723
+
724
+ def convert_template_to_item(self, template_id: str) -> Dict[str, Any]:
725
+ """تحويل قالب نموذجي إلى بند للاستخدام في حاسبة تكاليف البناء"""
726
+ template = self.get_template_by_id(template_id)
727
+ if not template:
728
+ raise ValueError("القالب غير موجود")
729
+
730
+ # تحويل القالب إلى صيغة بند
731
+ item = {
732
+ "وصف_البند": template["description"],
733
+ "الكمية": 1.0,
734
+ "الوحدة": template["unit"],
735
+ "المواد": template["components"]["materials"],
736
+ "العمالة": template["components"]["labor"],
737
+ "المعدات": template["components"]["equipment"],
738
+ "المصاريف_الإدارية": template["admin_expenses"],
739
+ "هامش_الربح": template["profit_margin"],
740
+ "عوامل_التعديل": {
741
+ "location_factor": 1.0,
742
+ "time_factor": 1.0,
743
+ "risk_factor": 1.0,
744
+ "market_factor": 1.0
745
+ }
746
+ }
747
+
748
+ return item
modules/pricing/services/local_content_calculator.py ADDED
@@ -0,0 +1,577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ خدمة حساب المحتوى المحلي
3
+ """
4
+ import pandas as pd
5
+ import numpy as np
6
+ from datetime import datetime
7
+ import os
8
+ import config
9
+
10
+ class LocalContentCalculator:
11
+ """خدمة حساب وتحسين المحتوى المحلي"""
12
+
13
+ def __init__(self):
14
+ """تهيئة خدمة حساب المحتوى المحلي"""
15
+ # تحميل بيانات المواد المحلية ونسب المحتوى المحلي
16
+ self.local_products = self._load_local_products()
17
+ self.local_services = self._load_local_services()
18
+ self.local_labor = self._load_local_labor()
19
+
20
+ # تحديد الأوزان النسبية لمكونات المحتوى المحلي
21
+ self.component_weights = {
22
+ 'القوى العاملة': 0.3, # 30% من وزن المحتوى المحلي
23
+ 'المنتجات': 0.5, # 50% من وزن المحتوى المحلي
24
+ 'الخدمات': 0.2 # 20% من وزن المحتوى المحلي
25
+ }
26
+
27
+ # تحديد المستهدفات (متطلبات المحتوى المحلي)
28
+ self.targets = {
29
+ 'القوى العاملة': 0.8, # 80% محتوى محلي للقوى العاملة
30
+ 'المنتجات': 0.7, # 70% محتوى محلي للمنتجات
31
+ 'الخدمات': 0.6 # 60% محتوى محلي للخدمات
32
+ }
33
+
34
+ def _load_local_products(self):
35
+ """تحميل بيانات المنتجات المحلية ونسب المحتوى المحلي"""
36
+ # محاكاة تحميل البيانات من مصدر بيانات
37
+ local_products = {
38
+ 'خرسانة': {
39
+ 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي
40
+ 'بديل_محلي': True,
41
+ 'مصدر': 'محلي',
42
+ 'ملاحظات': 'منتج محلي بالكامل'
43
+ },
44
+ 'حديد تسليح': {
45
+ 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي
46
+ 'بديل_محلي': True,
47
+ 'مصدر': 'محلي/مستورد',
48
+ 'ملاحظات': 'متوفر من مصانع محلية ومستورد'
49
+ },
50
+ 'عزل مائي': {
51
+ 'نسبة_المحتوى_المحلي': 0.60, # 60% محتوى محلي
52
+ 'بديل_محلي': True,
53
+ 'مصدر': 'محلي/مستورد',
54
+ 'ملاحظات': 'منتج محلي متوفر بجودة معقولة'
55
+ },
56
+ 'بلوك خرساني': {
57
+ 'نسبة_المحتوى_المحلي': 0.98, # 98% محتوى محلي
58
+ 'بديل_محلي': True,
59
+ 'مصدر': 'محلي',
60
+ 'ملاحظات': 'منتج محلي بالكامل'
61
+ },
62
+ 'رخام': {
63
+ 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي
64
+ 'بديل_محلي': True,
65
+ 'مصدر': 'محلي',
66
+ 'ملاحظات': 'متوفر من محاجر محلية'
67
+ },
68
+ 'أثاث مكتبي': {
69
+ 'نسبة_المحتوى_المحلي': 0.75, # 75% محتوى محلي
70
+ 'بديل_محلي': True,
71
+ 'مصدر': 'محلي',
72
+ 'ملاحظات': 'يُصنع محليًا ويستخدم بعض المكونات المستوردة'
73
+ },
74
+ 'أجهزة تكييف': {
75
+ 'نسبة_المحتوى_المحلي': 0.40, # 40% محتوى محلي
76
+ 'بديل_محلي': True,
77
+ 'مصدر': 'محلي/مستورد',
78
+ 'ملاحظات': 'تجميع محلي مع مكونات مستوردة'
79
+ },
80
+ 'أنظمة إضاءة': {
81
+ 'نسبة_المحتوى_المحلي': 0.55, # 55% محتوى محلي
82
+ 'بديل_محلي': True,
83
+ 'مصدر': 'محلي/مستورد',
84
+ 'ملاحظات': 'متوفر محليًا وبجودة متفاوتة'
85
+ },
86
+ 'زجاج': {
87
+ 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي
88
+ 'بديل_محلي': True,
89
+ 'مصدر': 'محلي/مستورد',
90
+ 'ملاحظات': 'إنتاج محلي بمواصفات جيدة'
91
+ },
92
+ 'أسلاك كهربائية': {
93
+ 'نسبة_المحتوى_المحلي': 0.85, # 85% محتوى محلي
94
+ 'بديل_محلي': True,
95
+ 'مصدر': 'محلي',
96
+ 'ملاحظات': 'تصنيع محلي بجودة عالية'
97
+ }
98
+ }
99
+
100
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
101
+ try:
102
+ file_path = os.path.join(config.DATA_DIR, 'local_products.csv')
103
+ if os.path.exists(file_path):
104
+ df = pd.read_csv(file_path, encoding='utf-8')
105
+ local_products = {}
106
+ for _, row in df.iterrows():
107
+ local_products[row['اسم_المنتج']] = {
108
+ 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'],
109
+ 'بديل_محلي': row['بديل_محلي'],
110
+ 'مصدر': row['مصدر'],
111
+ 'ملاحظات': row['ملاحظات']
112
+ }
113
+ except Exception as e:
114
+ print(f"خطأ في تحميل بيانات المنتجات المحلية: {str(e)}")
115
+
116
+ return local_products
117
+
118
+ def _load_local_services(self):
119
+ """تحميل بيانات الخدمات المحلية ونسب المحتوى المحلي"""
120
+ # محاكاة تحميل البيانات من مصدر بيانات
121
+ local_services = {
122
+ 'تصميم معماري': {
123
+ 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي
124
+ 'بديل_محلي': True,
125
+ 'مصدر': 'محلي',
126
+ 'ملاحظات': 'متوفرة من مكاتب استشارية محلية'
127
+ },
128
+ 'إشراف هندسي': {
129
+ 'نسبة_المحتوى_المحلي': 0.85, # 85% محتوى محلي
130
+ 'بديل_محلي': True,
131
+ 'مصدر': 'محلي',
132
+ 'ملاحظات': 'متوفر من شركات محلية'
133
+ },
134
+ 'خدمات تنسيق المواقع': {
135
+ 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي
136
+ 'بديل_محلي': True,
137
+ 'مصدر': 'محلي',
138
+ 'ملاحظات': 'شركات محلية متخصصة'
139
+ },
140
+ 'خدمات أمن وسلامة': {
141
+ 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي
142
+ 'بديل_محلي': True,
143
+ 'مصدر': 'محلي',
144
+ 'ملاحظات': 'شركات محلية متخصصة'
145
+ },
146
+ 'استشارات بيئية': {
147
+ 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي
148
+ 'بديل_محلي': True,
149
+ 'مصدر': 'محلي/دولي',
150
+ 'ملاحظات': 'متوفرة محليًا مع بعض الخبرات الأجنبية'
151
+ },
152
+ 'دراسات جدوى': {
153
+ 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي
154
+ 'بديل_محلي': True,
155
+ 'مصدر': 'محلي/دولي',
156
+ 'ملاحظات': 'متوفرة من مكاتب استشارية محلية'
157
+ },
158
+ 'خدمات نقل': {
159
+ 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي
160
+ 'بديل_محلي': True,
161
+ 'مصدر': 'محلي',
162
+ 'ملاحظات': 'شركات نقل محلية متعددة'
163
+ },
164
+ 'صيانة ونظافة': {
165
+ 'نسبة_المحتوى_المحلي': 0.95, # 95% محتوى محلي
166
+ 'بديل_محلي': True,
167
+ 'مصدر': 'محلي',
168
+ 'ملاحظات': 'شركات محلية متخصصة'
169
+ }
170
+ }
171
+
172
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
173
+ try:
174
+ file_path = os.path.join(config.DATA_DIR, 'local_services.csv')
175
+ if os.path.exists(file_path):
176
+ df = pd.read_csv(file_path, encoding='utf-8')
177
+ local_services = {}
178
+ for _, row in df.iterrows():
179
+ local_services[row['اسم_الخدمة']] = {
180
+ 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'],
181
+ 'بديل_محلي': row['بديل_محلي'],
182
+ 'مصدر': row['مصدر'],
183
+ 'ملاحظات': row['ملاحظات']
184
+ }
185
+ except Exception as e:
186
+ print(f"خطأ في تحميل بيانات الخدمات المحلية: {str(e)}")
187
+
188
+ return local_services
189
+
190
+ def _load_local_labor(self):
191
+ """تحميل بيانات القوى العاملة المحلية ونسب المحتوى المحلي"""
192
+ # محاكاة تحميل البيانات من مصدر بيانات
193
+ local_labor = {
194
+ 'عمال بناء': {
195
+ 'نسبة_المحتوى_المحلي': 0.60, # 60% محتوى محلي
196
+ 'بديل_محلي': True,
197
+ 'مصدر': 'محلي/أجنبي',
198
+ 'ملاحظات': 'متوفر محليًا مع نسبة من العمالة الأجنبية'
199
+ },
200
+ 'مهندسون': {
201
+ 'نسبة_المحتوى_المحلي': 0.75, # 75% محتوى محلي
202
+ 'بديل_محلي': True,
203
+ 'مصدر': 'محلي/أجنبي',
204
+ 'ملاحظات': 'كفاءات محلية متوفرة'
205
+ },
206
+ 'فنيون': {
207
+ 'نسبة_المحتوى_المحلي': 0.65, # 65% محتوى محلي
208
+ 'بديل_محلي': True,
209
+ 'مصدر': 'محلي/أجنبي',
210
+ 'ملاحظات': 'متوفر محليًا بنسب متفاوتة'
211
+ },
212
+ 'إداريون': {
213
+ 'نسبة_المحتوى_المحلي': 0.90, # 90% محتوى محلي
214
+ 'بديل_محلي': True,
215
+ 'مصدر': 'محلي',
216
+ 'ملاحظات': 'معظمهم من الكوادر المحلية'
217
+ },
218
+ 'مشرفون': {
219
+ 'نسبة_المحتوى_المحلي': 0.80, # 80% محتوى محلي
220
+ 'بديل_محلي': True,
221
+ 'مصدر': 'محلي',
222
+ 'ملاحظات': 'معظمهم من الكوادر المحلية'
223
+ },
224
+ 'مصممون': {
225
+ 'نسبة_المحتوى_المحلي': 0.70, # 70% محتوى محلي
226
+ 'بديل_محلي': True,
227
+ 'مصدر': 'محلي/أجنبي',
228
+ 'ملاحظات': 'كفاءات محلية مع بعض الخبرات الأجنبية'
229
+ },
230
+ 'عمال مهرة': {
231
+ 'نسبة_المحتوى_المحلي': 0.55, # 55% محتوى محلي
232
+ 'بديل_محلي': True,
233
+ 'مصدر': 'محلي/أجنبي',
234
+ 'ملاحظات': 'نسبة من العمالة الأجنبية ذات الخبرة'
235
+ }
236
+ }
237
+
238
+ # محاولة تحميل البيانات من ملف إذا كان متاحًا
239
+ try:
240
+ file_path = os.path.join(config.DATA_DIR, 'local_labor.csv')
241
+ if os.path.exists(file_path):
242
+ df = pd.read_csv(file_path, encoding='utf-8')
243
+ local_labor = {}
244
+ for _, row in df.iterrows():
245
+ local_labor[row['فئة_العمالة']] = {
246
+ 'نسبة_المحتوى_المحلي': row['نسبة_المحتوى_المحلي'],
247
+ 'بديل_محلي': row['بديل_محلي'],
248
+ 'مصدر': row['مصدر'],
249
+ 'ملاحظات': row['ملاحظات']
250
+ }
251
+ except Exception as e:
252
+ print(f"خطأ في تحميل بيانات القوى العاملة المحلية: {str(e)}")
253
+
254
+ return local_labor
255
+
256
+ def calculate_project_local_content(self, project_data):
257
+ """
258
+ حساب نسبة المحتوى المحلي للمشروع
259
+
260
+ المعلمات:
261
+ project_data: بيانات المشروع، تتضمن مكونات المنتجات والخدمات والقوى العاملة
262
+
263
+ إرجاع:
264
+ نسبة المحتوى المحلي الإجمالية، وتفاصيل حسب كل مكون
265
+ """
266
+ # تهيئة نتائج الحساب
267
+ results = {
268
+ 'نسبة_المحتوى_المحلي_الإجمالية': 0,
269
+ 'تفاصيل_المكونات': {
270
+ 'المنتجات': {'نسبة': 0, 'تفاصيل': {}},
271
+ 'الخدمات': {'نسبة': 0, 'تفاصيل': {}},
272
+ 'القوى العاملة': {'نسبة': 0, 'تفاصيل': {}}
273
+ },
274
+ 'ملخص_المحتوى_المحلي': {},
275
+ 'توصيات_التحسين': []
276
+ }
277
+
278
+ # حساب نسبة المحتوى المحلي للمنتجات
279
+ if 'المنتجات' in project_data:
280
+ products_local_content = self._calculate_products_local_content(project_data['المنتجات'])
281
+ results['تفاصيل_المكونات']['المنتجات'] = products_local_content
282
+
283
+ # حساب نسبة المحتوى المحلي للخدمات
284
+ if 'الخدمات' in project_data:
285
+ services_local_content = self._calculate_services_local_content(project_data['الخدمات'])
286
+ results['تفاصيل_المكونات']['الخدمات'] = services_local_content
287
+
288
+ # حساب نسبة المحتوى المحلي للقوى العاملة
289
+ if 'القوى العاملة' in project_data:
290
+ labor_local_content = self._calculate_labor_local_content(project_data['القوى العاملة'])
291
+ results['تفاصيل_المكونات']['القوى العاملة'] = labor_local_content
292
+
293
+ # حساب النسبة الإجمالية للمحتوى المحلي بناءً على الأوزان النسبية
294
+ total_local_content = 0
295
+ for component, weight in self.component_weights.items():
296
+ if component in results['تفاصيل_المكونات']:
297
+ component_percentage = results['تفاصيل_المكونات'][component]['نسبة']
298
+ total_local_content += component_percentage * weight
299
+
300
+ results['نسبة_المحتوى_المحلي_الإجمالية'] = total_local_content
301
+
302
+ # تحديد ملخص المحتوى المحلي ومقارنته بالمستهدف
303
+ for component, target in self.targets.items():
304
+ if component in results['تفاصيل_المكونات']:
305
+ actual = results['تفاصيل_المكونات'][component]['نسبة']
306
+ status = 'مطابق' if actual >= target else 'غير مطابق'
307
+ gap = round((target - actual) * 100, 2) if actual < target else 0
308
+
309
+ results['ملخص_المحتوى_المحلي'][component] = {
310
+ 'المستهدف': target * 100,
311
+ 'الفعلي': round(actual * 100, 2),
312
+ 'الحالة': status,
313
+ 'الفجوة (%)': gap
314
+ }
315
+
316
+ # توليد توصيات لتحسين نسبة المحتوى المحلي
317
+ results['توصيات_التحسين'] = self._generate_improvement_recommendations(results)
318
+
319
+ return results
320
+
321
+ def _calculate_products_local_content(self, products_data):
322
+ """
323
+ حساب نسبة المحتوى المحلي للمنتجات
324
+
325
+ المعلمات:
326
+ products_data: بيانات المنتجات المستخدمة في المشروع
327
+
328
+ إرجاع:
329
+ تفاصيل نسبة المحتوى المحلي للمنتجات
330
+ """
331
+ total_cost = 0
332
+ local_content_value = 0
333
+ details = {}
334
+
335
+ for product_name, product_info in products_data.items():
336
+ quantity = product_info.get('الكمية', 0)
337
+ unit_price = product_info.get('سعر_الوحدة', 0)
338
+ total_product_cost = quantity * unit_price
339
+
340
+ # البحث عن نسبة المحتوى المحلي للمنتج
341
+ local_content_percentage = 0
342
+ if product_name in self.local_products:
343
+ local_content_percentage = self.local_products[product_name]['نسبة_المحتوى_المحلي']
344
+
345
+ # حساب قيمة المحتوى المحلي للمنتج
346
+ product_local_content_value = total_product_cost * local_content_percentage
347
+
348
+ # تحديث الإجماليات
349
+ total_cost += total_product_cost
350
+ local_content_value += product_local_content_value
351
+
352
+ # تسجيل التفاصيل
353
+ details[product_name] = {
354
+ 'الكمية': quantity,
355
+ 'سعر_الوحدة': unit_price,
356
+ 'التكلفة_الإجمالية': total_product_cost,
357
+ 'نسبة_المحتوى_المحلي': local_content_percentage,
358
+ 'قيمة_المحتوى_المحلي': product_local_content_value,
359
+ 'مصدر': self.local_products.get(product_name, {}).get('مصدر', 'غير معروف'),
360
+ 'ملاحظات': self.local_products.get(product_name, {}).get('ملاحظات', '')
361
+ }
362
+
363
+ # حساب النسبة الإجمالية للمحتوى المحلي للمنتجات
364
+ local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0
365
+
366
+ return {
367
+ 'نسبة': local_content_percentage,
368
+ 'إجمالي_التكلفة': total_cost,
369
+ 'قيمة_المحتوى_المحلي': local_content_value,
370
+ 'تفاصيل': details
371
+ }
372
+
373
+ def _calculate_services_local_content(self, services_data):
374
+ """
375
+ حساب نسبة المحتوى المحلي للخدمات
376
+
377
+ المعلمات:
378
+ services_data: بيانات الخدمات المستخدمة في المشروع
379
+
380
+ إرجاع:
381
+ تفاصيل نسبة المحتوى المحلي للخدمات
382
+ """
383
+ total_cost = 0
384
+ local_content_value = 0
385
+ details = {}
386
+
387
+ for service_name, service_info in services_data.items():
388
+ cost = service_info.get('التكلفة', 0)
389
+
390
+ # البحث عن نسبة المحتوى المحلي للخدمة
391
+ local_content_percentage = 0
392
+ if service_name in self.local_services:
393
+ local_content_percentage = self.local_services[service_name]['نسبة_المحتوى_المحلي']
394
+
395
+ # حساب قيمة المحتوى المحلي للخدمة
396
+ service_local_content_value = cost * local_content_percentage
397
+
398
+ # تحديث الإجماليات
399
+ total_cost += cost
400
+ local_content_value += service_local_content_value
401
+
402
+ # تسجيل التفاصيل
403
+ details[service_name] = {
404
+ 'التكلفة': cost,
405
+ 'نسبة_المحتوى_المحلي': local_content_percentage,
406
+ 'قيمة_المحتوى_المحلي': service_local_content_value,
407
+ 'مصدر': self.local_services.get(service_name, {}).get('مصدر', 'غير معروف'),
408
+ 'ملاحظات': self.local_services.get(service_name, {}).get('ملاحظات', '')
409
+ }
410
+
411
+ # حساب النسبة الإجمالية للمحتوى المحلي للخدمات
412
+ local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0
413
+
414
+ return {
415
+ 'نسبة': local_content_percentage,
416
+ 'إجمالي_التكلفة': total_cost,
417
+ 'قيمة_المحتوى_المحلي': local_content_value,
418
+ 'تفاصيل': details
419
+ }
420
+
421
+ def _calculate_labor_local_content(self, labor_data):
422
+ """
423
+ حساب نسبة المحتوى المحلي للقوى العاملة
424
+
425
+ المعلمات:
426
+ labor_data: بيانات القوى العاملة المستخدمة في المشروع
427
+
428
+ إرجاع:
429
+ تفاصيل نسبة المحتوى المحلي للقوى العاملة
430
+ """
431
+ total_cost = 0
432
+ local_content_value = 0
433
+ details = {}
434
+
435
+ for labor_type, labor_info in labor_data.items():
436
+ count = labor_info.get('العدد', 0)
437
+ monthly_salary = labor_info.get('الراتب_الشهري', 0)
438
+ duration_months = labor_info.get('المدة_بالأشهر', 0)
439
+
440
+ total_labor_cost = count * monthly_salary * duration_months
441
+
442
+ # البحث عن نسبة المحتوى المحلي للقوى العاملة
443
+ local_content_percentage = 0
444
+ if labor_type in self.local_labor:
445
+ local_content_percentage = self.local_labor[labor_type]['نسبة_المحتوى_المحلي']
446
+
447
+ # حساب قيمة المحتوى المحلي للقوى العاملة
448
+ labor_local_content_value = total_labor_cost * local_content_percentage
449
+
450
+ # تحديث الإجماليات
451
+ total_cost += total_labor_cost
452
+ local_content_value += labor_local_content_value
453
+
454
+ # تسجيل التفاصيل
455
+ details[labor_type] = {
456
+ 'العدد': count,
457
+ 'الراتب_الشهري': monthly_salary,
458
+ 'المدة_بالأشهر': duration_months,
459
+ 'التكلفة_الإجمالية': total_labor_cost,
460
+ 'نسبة_المحتوى_المحلي': local_content_percentage,
461
+ 'قيمة_المحتوى_المحلي': labor_local_content_value,
462
+ 'مصدر': self.local_labor.get(labor_type, {}).get('مصدر', 'غير معروف'),
463
+ 'ملاحظات': self.local_labor.get(labor_type, {}).get('ملاحظات', '')
464
+ }
465
+
466
+ # حساب النسبة الإجمالية للمحتوى المحلي للقوى العاملة
467
+ local_content_percentage = local_content_value / total_cost if total_cost > 0 else 0
468
+
469
+ return {
470
+ 'نسبة': local_content_percentage,
471
+ 'إجمالي_التكلفة': total_cost,
472
+ 'قيمة_المحتوى_المحلي': local_content_value,
473
+ 'تفاصيل': details
474
+ }
475
+
476
+ def _generate_improvement_recommendations(self, results):
477
+ """
478
+ توليد توصيات لتحسين نسبة المحتوى المحلي
479
+
480
+ المعلمات:
481
+ results: نتائج حساب المحتوى المحلي
482
+
483
+ إرجاع:
484
+ قائمة بالتوصيات لتحسين نسبة المحتوى المحلي
485
+ """
486
+ recommendations = []
487
+
488
+ # تحليل المكونات التي تحتاج إلى تحسين
489
+ for component, summary in results['ملخص_المحتوى_المحلي'].items():
490
+ if summary['الحالة'] == 'غير مطابق':
491
+ if component == 'المنتجات':
492
+ # تحديد المنتجات ذات ا��محتوى المحلي المنخفض
493
+ low_content_products = []
494
+ for product, details in results['تفاصيل_المكونات']['المنتجات']['تفاصيل'].items():
495
+ if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50%
496
+ low_content_products.append({
497
+ 'اسم': product,
498
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'],
499
+ 'التكلفة_الإجمالية': details['التكلفة_الإجمالية']
500
+ })
501
+
502
+ elif component == 'الخدمات':
503
+ # تحديد البنود ذات المحتوى المحلي المنخفض
504
+ low_content_services = []
505
+ for service, details in results['تفاصيل_المكونات']['الخدمات']['تفاصيل'].items():
506
+ if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50%
507
+ low_content_services.append({
508
+ 'اسم': service,
509
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'],
510
+ 'التكلفة': details['التكلفة']
511
+ })
512
+
513
+ elif component == 'القوى العاملة':
514
+ # تحديد فئات العمالة ذات المحتوى المحلي المنخفض
515
+ low_content_labor = []
516
+ for labor_type, details in results['تفاصيل_المكونات']['القوى العاملة']['تفاصيل'].items():
517
+ if details['نسبة_المحتوى_المحلي'] < 0.5: # أقل من 50%
518
+ low_content_labor.append({
519
+ 'اسم': labor_type,
520
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي'],
521
+ 'التكلفة_الإجمالية': details['التكلفة_الإجمالية']
522
+ })
523
+
524
+ # إنشاء توصيات لتحسين المحتوى المحلي
525
+ # توصيات للمنتجات
526
+ if 'المنتجات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['المنتجات']['الحالة'] == 'غير مطابق':
527
+ low_content_products = []
528
+ for product, details in results['تفاصيل_المكونات']['المنتجات']['تفاصيل'].items():
529
+ if details['نسبة_المحتوى_المحلي'] < 0.5:
530
+ low_content_products.append({
531
+ 'اسم': product,
532
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي']
533
+ })
534
+
535
+ if low_content_products:
536
+ recommendations.append(f"استبدال المنتجات ذات المحتوى المحلي المنخفض: {', '.join([p['اسم'] for p in low_content_products[:3]])}")
537
+ recommendations.append("البحث عن موردين محليين للمنتجات ذات الأولوية العالية")
538
+
539
+ # توصيات للخدمات
540
+ if 'الخدمات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['الخدمات']['الحالة'] == 'غير مطابق':
541
+ low_content_services = []
542
+ for service, details in results['تفاصيل_المكونات']['الخدمات']['تفاصيل'].items():
543
+ if details['نسبة_المحتوى_المحلي'] < 0.5:
544
+ low_content_services.append({
545
+ 'اسم': service,
546
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي']
547
+ })
548
+
549
+ if low_content_services:
550
+ recommendations.append(f"تحسين نسبة المحتوى المحلي للخدمات: {', '.join([s['اسم'] for s in low_content_services[:3]])}")
551
+ recommendations.append("التعاقد مع شركات خدمية محلية")
552
+
553
+ # توصيات للقوى العاملة
554
+ if 'القوى العاملة' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['القوى العاملة']['الحالة'] == 'غير مطابق':
555
+ low_content_labor = []
556
+ for labor_type, details in results['تفاصيل_المكونات']['القوى العاملة']['تفاصيل'].items():
557
+ if details['نسبة_��لمحتوى_المحلي'] < 0.5:
558
+ low_content_labor.append({
559
+ 'اسم': labor_type,
560
+ 'نسبة_المحتوى_المحلي': details['نسبة_المحتوى_المحلي']
561
+ })
562
+
563
+ if low_content_labor:
564
+ recommendations.append(f"زيادة توظيف العمالة المحلية في الفئات: {', '.join([l['اسم'] for l in low_content_labor[:3]])}")
565
+ recommendations.append("الاستثمار في برامج تدريب وتأهيل الكوادر المحلية")
566
+
567
+ # توصيات عامة
568
+ if 'المنتجات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['المنتجات'].get('الفجوة (%)', 0) > 10:
569
+ recommendations.append(f"خطة تطوير المحتوى المحلي للمنتجات لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['المنتجات']['الفجوة (%)']}%")
570
+
571
+ if 'الخدمات' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['الخدمات'].get('الفجوة (%)', 0) > 10:
572
+ recommendations.append(f"خطة تطوير المحتوى المحلي للخدمات لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['الخدمات']['الفجوة (%)']}%")
573
+
574
+ if 'القوى العاملة' in results['ملخص_المحتوى_المحلي'] and results['ملخص_المحتوى_المحلي']['القوى العاملة'].get('الفجوة (%)', 0) > 10:
575
+ recommendations.append(f"خطة تطوير المحتوى المحلي للقوى العاملة لتقليل الفجوة البالغة {results['ملخص_المحتوى_المحلي']['القوى العاملة']['الفجوة (%)']}%")
576
+
577
+ return recommendations
modules/pricing/services/price_prediction.py ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ خدمة التنبؤ بالأسعار
3
+ """
4
+
5
+ import pandas as pd
6
+ import numpy as np
7
+ import joblib
8
+ import os
9
+ from datetime import datetime, timedelta
10
+ from sklearn.ensemble import RandomForestRegressor
11
+ from sklearn.model_selection import train_test_split
12
+ from sklearn.preprocessing import StandardScaler
13
+ from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
14
+
15
+ import config
16
+
17
+
18
+ class PricePrediction:
19
+ """خدمة التنبؤ بالأسعار باستخدام التعلم الآلي"""
20
+
21
+ def __init__(self):
22
+ """تهيئة خدمة التنبؤ بالأسعار"""
23
+ self.model_path = config.PRICE_PREDICTION_MODEL
24
+ self.model = self._load_model()
25
+ self.scaler = None
26
+ self.materials_data = self._load_materials_data()
27
+ self.market_indices = self._load_market_indices()
28
+
29
+ def _load_model(self):
30
+ """تحميل نموذج التنبؤ المدرب مسبقاً"""
31
+ try:
32
+ if os.path.exists(self.model_path):
33
+ model = joblib.load(self.model_path)
34
+ return model
35
+ else:
36
+ # إذا لم يكن النموذج موجوداً، قم بإنشاء نموذج جديد
37
+ model = RandomForestRegressor(
38
+ n_estimators=100,
39
+ max_depth=15,
40
+ min_samples_split=5,
41
+ min_samples_leaf=2,
42
+ random_state=42
43
+ )
44
+ return model
45
+ except Exception as e:
46
+ print(f"خطأ في تحميل نموذج التنبؤ: {str(e)}")
47
+ return RandomForestRegressor(random_state=42)
48
+
49
+ def _load_materials_data(self):
50
+ """تحميل بيانات المواد وأسعارها التاريخية"""
51
+ # محاكاة تحميل البيانات من مصدر بيانات
52
+ materials_data = {
53
+ 'خرسانة': {
54
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
55
+ 'سعر': [750, 740, 735, 730, 720, 715, 710, 700, 695, 690, 685, 680],
56
+ 'وحدة': 'م3'
57
+ },
58
+ 'حديد تسليح': {
59
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
60
+ 'سعر': [5500, 5450, 5400, 5350, 5300, 5250, 5200, 5150, 5100, 5050, 5000, 4950],
61
+ 'وحدة': 'طن'
62
+ },
63
+ 'إسمنت': {
64
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
65
+ 'سعر': [25, 25, 24.5, 24.5, 24, 24, 23.5, 23.5, 23, 23, 22.5, 22.5],
66
+ 'وحدة': 'كيس'
67
+ },
68
+ 'رمل': {
69
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
70
+ 'سعر': [140, 140, 135, 135, 130, 130, 125, 125, 120, 120, 115, 115],
71
+ 'وحدة': 'م3'
72
+ },
73
+ 'بلوك خرساني': {
74
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
75
+ 'سعر': [11, 11, 10.5, 10.5, 10, 10, 9.5, 9.5, 9, 9, 8.5, 8.5],
76
+ 'وحدة': 'قطعة'
77
+ }
78
+ }
79
+ return materials_data
80
+
81
+ def _load_market_indices(self):
82
+ """تحميل مؤشرات السوق المؤثرة على الأسعار"""
83
+ # محاكاة تحميل البيانات من مصدر بيانات
84
+ market_indices = {
85
+ 'تاريخ': [datetime(2025, 1, 1) - timedelta(days=30*i) for i in range(12)],
86
+ 'مؤشر_البناء': [105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94],
87
+ 'مؤشر_النفط': [80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69],
88
+ 'مؤشر_سعر_الصرف': [3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75, 3.75],
89
+ 'مؤشر_التضخم': [2.5, 2.4, 2.3, 2.2, 2.1, 2.0, 1.9, 1.8, 1.7, 1.6, 1.5, 1.4]
90
+ }
91
+ return market_indices
92
+
93
+ def train(self, training_data=None):
94
+ """
95
+ تدريب نموذج التنبؤ بالأسعار
96
+
97
+ المعلمات:
98
+ training_data: بيانات التدريب (اختياري)، إذا لم يتم توفيرها سيتم استخدام البيانات المتاحة
99
+
100
+ إرجاع:
101
+ مؤشرات أداء النموذج
102
+ """
103
+ # تجهيز بيانات التدريب
104
+ if training_data is None:
105
+ # استخدام البيانات المتاحة لتوليد مجموعة تدريب
106
+ X, y = self._prepare_training_data()
107
+ else:
108
+ X, y = self._extract_features_target(training_data)
109
+
110
+ # تقسيم البيانات إلى تدريب واختبار
111
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
112
+
113
+ # تطبيع البيانات
114
+ self.scaler = StandardScaler()
115
+ X_train_scaled = self.scaler.fit_transform(X_train)
116
+ X_test_scaled = self.scaler.transform(X_test)
117
+
118
+ # تدريب النموذج
119
+ self.model.fit(X_train_scaled, y_train)
120
+
121
+ # تقييم النموذج
122
+ y_pred = self.model.predict(X_test_scaled)
123
+
124
+ # حساب مؤشرات الأداء
125
+ mae = mean_absolute_error(y_test, y_pred)
126
+ rmse = np.sqrt(mean_squared_error(y_test, y_pred))
127
+ r2 = r2_score(y_test, y_pred)
128
+
129
+ # حفظ النموذج
130
+ try:
131
+ joblib.dump(self.model, self.model_path)
132
+ joblib.dump(self.scaler, os.path.join(os.path.dirname(self.model_path), 'price_scaler.pkl'))
133
+ except Exception as e:
134
+ print(f"خطأ في حفظ النموذج: {str(e)}")
135
+
136
+ return {
137
+ 'mae': mae,
138
+ 'rmse': rmse,
139
+ 'r2': r2
140
+ }
141
+
142
+ def _prepare_training_data(self):
143
+ """تجهيز بيانات التدريب من البيانات المتاحة"""
144
+ # توليد بيانات تدريب افتراضية
145
+ data = []
146
+ target = []
147
+
148
+ # استخدام بيانات المواد وأسعارها التاريخية
149
+ for material_name, material_info in self.materials_data.items():
150
+ for i in range(len(material_info['تاريخ'])):
151
+ # استخراج المؤشرات في التاريخ المقابل
152
+ date_index = self.market_indices['تاريخ'].index(material_info['تاريخ'][i]) if material_info['تاريخ'][i] in self.market_indices['تاريخ'] else 0
153
+
154
+ # تكوين ميزات التدريب (المؤشرات السوقية والشهر)
155
+ features = [
156
+ material_info['تاريخ'][i].month, # الشهر
157
+ self.market_indices['مؤشر_البناء'][date_index],
158
+ self.market_indices['مؤشر_النفط'][date_index],
159
+ self.market_indices['مؤشر_سعر_الصرف'][date_index],
160
+ self.market_indices['مؤشر_التضخم'][date_index]
161
+ ]
162
+
163
+ # إضافة معرّف للمادة (تمثيل رقمي)
164
+ material_id = list(self.materials_data.keys()).index(material_name)
165
+ features.append(material_id)
166
+
167
+ data.append(features)
168
+ target.append(material_info['سعر'][i])
169
+
170
+ # إضافة ضوضاء عشوائية لزيادة حجم البيانات
171
+ for _ in range(5):
172
+ noisy_features = features.copy()
173
+ for j in range(1, 5): # إضافة ضوضاء للمؤشرات فقط
174
+ noisy_features[j] += np.random.normal(0, 0.5)
175
+
176
+ noisy_price = material_info['سعر'][i] * (1 + np.random.normal(0, 0.02)) # ضوضاء 2%
177
+
178
+ data.append(noisy_features)
179
+ target.append(noisy_price)
180
+
181
+ return np.array(data), np.array(target)
182
+
183
+ def _extract_features_target(self, training_data):
184
+ """استخراج الميزات والأهداف من بيانات التدريب"""
185
+ # استخراج الميزات والأهداف من البيانات المقدمة
186
+ features = []
187
+ target = []
188
+
189
+ for item in training_data:
190
+ features.append([
191
+ item['date'].month, # الشهر
192
+ item['building_index'],
193
+ item['oil_index'],
194
+ item['exchange_rate'],
195
+ item['inflation_rate'],
196
+ item['material_id']
197
+ ])
198
+ target.append(item['price'])
199
+
200
+ return np.array(features), np.array(target)
201
+
202
+ def predict_prices(self, materials, prediction_date=None, market_conditions=None):
203
+ """
204
+ التنبؤ بأسعار المواد
205
+
206
+ المعلمات:
207
+ materials: قائمة المواد المطلوب التنبؤ بأسعارها
208
+ prediction_date: تاريخ التنبؤ (اختياري)، إذا لم يتم توفيره سيتم استخدام التاريخ الحالي
209
+ market_conditions: ظروف السوق (اختياري)، إذا لم يتم توفيرها سيتم استخدام آخر قيم متاحة
210
+
211
+ إرجاع:
212
+ قاموس بأسعار المواد المتنبأ بها
213
+ """
214
+ if prediction_date is None:
215
+ prediction_date = datetime.now()
216
+
217
+ if market_conditions is None:
218
+ # استخدام آخر قيم متاحة للمؤشرات
219
+ market_conditions = {
220
+ 'مؤشر_البناء': self.market_indices['مؤشر_البناء'][0],
221
+ 'مؤشر_النفط': self.market_indices['مؤشر_النفط'][0],
222
+ 'مؤشر_سعر_الصرف': self.market_indices['مؤشر_سعر_الصرف'][0],
223
+ 'مؤشر_التضخم': self.market_indices['مؤشر_التضخم'][0]
224
+ }
225
+
226
+ # التحقق من وجود المواد في البيانات
227
+ material_names = list(self.materials_data.keys())
228
+ valid_materials = [m for m in materials if m in material_names]
229
+
230
+ if not valid_materials:
231
+ return {}
232
+
233
+ # تحميل المعايير إذا كانت متوفرة
234
+ scaler_path = os.path.join(os.path.dirname(self.model_path), 'price_scaler.pkl')
235
+ if self.scaler is None and os.path.exists(scaler_path):
236
+ try:
237
+ self.scaler = joblib.load(scaler_path)
238
+ except Exception as e:
239
+ print(f"خطأ في تحميل المعايير: {str(e)}")
240
+ # إنشاء معايير جديدة
241
+ X, _ = self._prepare_training_data()
242
+ self.scaler = StandardScaler()
243
+ self.scaler.fit(X)
244
+
245
+ # إعداد ميزات التنبؤ
246
+ features = []
247
+ for material in valid_materials:
248
+ material_id = material_names.index(material)
249
+
250
+ material_features = [
251
+ prediction_date.month, # الشهر
252
+ market_conditions['مؤشر_البناء'],
253
+ market_conditions['مؤشر_النفط'],
254
+ market_conditions['مؤشر_سعر_الصرف'],
255
+ market_conditions['مؤشر_التضخم'],
256
+ material_id
257
+ ]
258
+
259
+ features.append(material_features)
260
+
261
+ # تطبيع الميزات
262
+ if self.scaler is not None:
263
+ features_scaled = self.scaler.transform(features)
264
+ else:
265
+ features_scaled = features
266
+
267
+ # التنبؤ بالأسعار
268
+ predicted_prices = self.model.predict(features_scaled)
269
+
270
+ # إرجاع النتائج
271
+ results = {}
272
+ for i, material in enumerate(valid_materials):
273
+ # تطبيق عامل تصحيح (2% عشوائية)
274
+ correction_factor = 1.0 + np.random.uniform(-0.02, 0.02)
275
+ price = max(0, predicted_prices[i] * correction_factor)
276
+
277
+ results[material] = {
278
+ 'سعر': price,
279
+ 'وحدة': self.materials_data[material]['وحدة'],
280
+ 'تاريخ_التنبؤ': prediction_date.strftime('%Y-%m-%d'),
281
+ 'هامش_الخطأ': '±5%' # تقدير هامش الخطأ
282
+ }
283
+
284
+ return results
285
+
286
+ def get_price_trends(self, material, periods=6):
287
+ """
288
+ الحصول على اتجاهات الأسعار المستقبلية
289
+
290
+ المعلمات:
291
+ material: المادة المطلوب التنبؤ باتجاهات أسعارها
292
+ periods: عدد الفترات المستقبلية (الشهور)
293
+
294
+ إرجاع:
295
+ قائمة بالأسعار المتوقعة للفترات المستقبلية
296
+ """
297
+ if material not in self.materials_data:
298
+ return []
299
+
300
+ # الحصول على التاريخ الحالي
301
+ current_date = datetime.now()
302
+
303
+ # التنبؤ بالأسعار للفترات المستقبلية
304
+ price_trends = []
305
+
306
+ for i in range(periods):
307
+ prediction_date = current_date + timedelta(days=30 * (i + 1))
308
+
309
+ # افتراض تغيرات طفيفة في المؤشرات مع مرور الوقت
310
+ market_conditions = {
311
+ 'مؤشر_البناء': self.market_indices['مؤشر_البناء'][0] * (1 + 0.01 * i), # زيادة 1% شهرياً
312
+ 'مؤشر_النفط': self.market_indices['مؤشر_النفط'][0] * (1 + 0.005 * i), # زيادة 0.5% شهرياً
313
+ 'مؤشر_سعر_الصرف': self.market_indices['مؤشر_سعر_الصرف'][0], # ثابت
314
+ 'مؤشر_التضخم': self.market_indices['مؤشر_التضخم'][0] * (1 + 0.01 * i) # زيادة 1% شهرياً
315
+ }
316
+
317
+ # التنبؤ بالسعر
318
+ predicted_price = self.predict_prices([material], prediction_date, market_conditions)
319
+
320
+ price_trends.append({
321
+ 'تاريخ': prediction_date.strftime('%Y-%m'),
322
+ 'سعر': predicted_price[material]['سعر'] if material in predicted_price else 0
323
+ })
324
+
325
+ return price_trends
326
+
327
+ def analyze_factors(self, material):
328
+ """
329
+ تحليل العوامل المؤثرة على سعر المادة
330
+
331
+ المعلمات:
332
+ material: المادة المطلوب تحليلها
333
+
334
+ إرجاع:
335
+ قاموس بالعوامل المؤثرة وأهميتها النسبية
336
+ """
337
+ if material not in self.materials_data or not hasattr(self.model, 'feature_importances_'):
338
+ return {}
339
+
340
+ # الحصول على أهمية الميزات من النموذج
341
+ feature_importances = self.model.feature_importances_
342
+
343
+ # أسماء الميزات
344
+ feature_names = ['الشهر', 'مؤشر البناء', 'مؤشر النفط', 'سعر الصرف', 'معدل التضخم', 'نوع المادة']
345
+
346
+ # ترتيب الميزات حسب الأهمية
347
+ importance_pairs = [(name, importance) for name, importance in zip(feature_names, feature_importances)]
348
+ importance_pairs.sort(key=lambda x: x[1], reverse=True)
349
+
350
+ # إرجاع العوامل المؤثرة وأهميتها
351
+ factors = {}
352
+ for name, importance in importance_pairs:
353
+ factors[name] = round(importance * 100, 2) # تحويل إلى نسبة مئوية
354
+
355
+ return {
356
+ 'العوامل_المؤثرة': factors,
357
+ 'المادة': material,
358
+ 'وحدة': self.materials_data[material]['وحدة'],
359
+ 'سعر_حالي': self.materials_data[material]['سعر'][0],
360
+ 'اتجاه_السعر': self._get_price_trend(material)
361
+ }
362
+
363
+ def _get_price_trend(self, material):
364
+ """تحديد اتجاه سعر المادة بناءً على البيانات التاريخية"""
365
+ if material not in self.materials_data:
366
+ return "غير معروف"
367
+
368
+ prices = self.materials_data[material]['سعر']
369
+ if len(prices) < 2:
370
+ return "غير معروف"
371
+
372
+ # حساب متوسط التغير الشهري
373
+ price_changes = [(prices[i] - prices[i+1]) / prices[i+1] * 100 for i in range(len(prices)-1)]
374
+ avg_monthly_change = sum(price_changes) / len(price_changes)
375
+
376
+ if avg_monthly_change > 1:
377
+ return "ارتفاع حاد"
378
+ elif avg_monthly_change > 0.2:
379
+ return "ارتفاع معتدل"
380
+ elif avg_monthly_change > -0.2:
381
+ return "استقرار"
382
+ elif avg_monthly_change > -1:
383
+ return "انخفاض معتدل"
384
+ else:
385
+ return "انخفاض حاد"
386
+
387
+ def export_price_forecast(self, materials, periods=6, output_file=None):
388
+ """
389
+ تصدير توقعات الأسعار إلى ملف
390
+
391
+ المعلمات:
392
+ materials: قائمة المواد المطلوب التنبؤ بأسعارها
393
+ periods: عدد الفترات المستقبلية (الشهور)
394
+ output_file: مسار ملف الإخراج (اختياري)
395
+
396
+ إرجاع:
397
+ مسار الملف المصدر أو البيانات مباشرة إذا لم يتم تحديد ملف
398
+ """
399
+ # التحقق من وجود المواد في البيانات
400
+ valid_materials = [m for m in materials if m in self.materials_data]
401
+
402
+ if not valid_materials:
403
+ return None
404
+
405
+ # إعداد بيانات التوقعات
406
+ forecast_data = []
407
+
408
+ for material in valid_materials:
409
+ # الحصول على اتجاهات الأسعار
410
+ price_trends = self.get_price_trends(material, periods)
411
+
412
+ for trend in price_trends:
413
+ forecast_data.append({
414
+ 'المادة': material,
415
+ 'الوحدة': self.materials_data[material]['وحدة'],
416
+ 'التاريخ': trend['تاريخ'],
417
+ 'السعر المتوقع': trend['سعر'],
418
+ 'هامش الخطأ': '±5%'
419
+ })
420
+
421
+ # تحويل البيانات إلى DataFrame
422
+ forecast_df = pd.DataFrame(forecast_data)
423
+
424
+ # تصدير البيانات إلى ملف إذا تم تحديده
425
+ if output_file:
426
+ try:
427
+ ext = os.path.splitext(output_file)[1].lower()
428
+
429
+ if ext == '.csv':
430
+ forecast_df.to_csv(output_file, index=False, encoding='utf-8-sig')
431
+ elif ext in ['.xlsx', '.xls']:
432
+ forecast_df.to_excel(output_file, index=False)
433
+ elif ext == '.json':
434
+ forecast_df.to_json(output_file, orient='records', force_ascii=False)
435
+ else:
436
+ print(f"تنسيق غير مدعوم: {ext}")
437
+ return None
438
+
439
+ return output_file
440
+ except Exception as e:
441
+ print(f"خطأ في تصدير توقعات الأسعار: {str(e)}")
442
+ return None
443
+
444
+ return forecast_df
modules/pricing/services/standard_pricing.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ خدمة التسعير القياسي
3
+ """
4
+
5
+ import pandas as pd
6
+ import numpy as np
7
+ from datetime import datetime
8
+ import os
9
+ import config
10
+
11
+
12
+ class StandardPricing:
13
+ """خدمة التسعير القياسي للبنود"""
14
+
15
+ def __init__(self):
16
+ """تهيئة خدمة التسعير القياسي"""
17
+ # تحميل بيانات المواد والأسعار المرجعية
18
+ self.material_prices = self._load_material_prices()
19
+ self.labor_rates = self._load_labor_rates()
20
+ self.equipment_rates = self._load_equipment_rates()
21
+
22
+ def _load_material_prices(self):
23
+ """تحميل أسعار المواد"""
24
+ # محاكاة تحميل البيانات من مصدر بيانات
25
+ material_prices = {
26
+ 'خرسانة': {
27
+ 'م3': 750.0, # سعر المتر المكعب بالريال
28
+ 'وحدة_قياسية': 'م3',
29
+ 'آخر_تحديث': datetime(2025, 3, 1)
30
+ },
31
+ 'حديد تسليح': {
32
+ 'طن': 5500.0, # سعر الطن بالريال
33
+ 'وحدة_قياسية': 'طن',
34
+ 'آخر_تحديث': datetime(2025, 3, 1)
35
+ },
36
+ 'عزل مائي': {
37
+ 'م2': 80.0, # سعر المتر المربع بالريال
38
+ 'وحدة_قياسية': 'م2',
39
+ 'آخر_تحديث': datetime(2025, 3, 1)
40
+ },
41
+ 'بلوك خرساني': {
42
+ '20سم': 11.0, # سعر البلكة بالريال
43
+ 'وحدة_قياسية': 'عدد',
44
+ 'آخر_تحديث': datetime(2025, 3, 1)
45
+ },
46
+ 'رمل': {
47
+ 'م3': 140.0, # سعر المتر المكعب بالريال
48
+ 'وحدة_قياسية': 'م3',
49
+ 'آخر_تحديث': datetime(2025, 3, 1)
50
+ },
51
+ 'اسمنت': {
52
+ 'كيس': 25.0, # سعر الكيس بالريال
53
+ 'وحدة_قياسية': 'كيس',
54
+ 'آخر_تحديث': datetime(2025, 3, 1)
55
+ }
56
+ }
57
+ return material_prices
58
+
59
+ def _load_labor_rates(self):
60
+ """تحميل معدلات أجور العمالة"""
61
+ # محاكاة تحميل البيانات من مصدر بيانات
62
+ labor_rates = {
63
+ 'عامل': {
64
+ 'يومي': 150.0, # الأجر اليومي بالريال
65
+ 'وحدة_قياسية': 'يوم',
66
+ 'آخر_تحديث': datetime(2025, 3, 1)
67
+ },
68
+ 'نجار': {
69
+ 'يومي': 250.0, # الأجر اليومي بالريال
70
+ 'وحدة_قياسية': 'يوم',
71
+ 'آخر_تحديث': datetime(2025, 3, 1)
72
+ },
73
+ 'حداد': {
74
+ 'يومي': 250.0, # الأجر اليومي بالريال
75
+ 'وحدة_قياسية': 'يوم',
76
+ 'آخر_تحديث': datetime(2025, 3, 1)
77
+ },
78
+ 'سباك': {
79
+ 'يومي': 300.0, # الأجر اليومي بالريال
80
+ 'وحدة_قياسية': 'يوم',
81
+ 'آخر_تحديث': datetime(2025, 3, 1)
82
+ },
83
+ 'كهربائي': {
84
+ 'يومي': 300.0, # الأجر اليومي بالريال
85
+ 'وحدة_قياسية': 'يوم',
86
+ 'آخر_تحديث': datetime(2025, 3, 1)
87
+ },
88
+ 'مراقب': {
89
+ 'يومي': 400.0, # الأجر اليومي بالريال
90
+ 'وحدة_قياسية': 'يوم',
91
+ 'آخر_تحديث': datetime(2025, 3, 1)
92
+ }
93
+ }
94
+ return labor_rates
95
+
96
+ def _load_equipment_rates(self):
97
+ """تحميل معدلات تأجير المعدات"""
98
+ # محاكاة تحميل البيانات من مصدر بيانات
99
+ equipment_rates = {
100
+ 'خلاطة خرسانة': {
101
+ 'يومي': 800.0, # الإيجار اليومي بالريال
102
+ 'وحدة_قياسية': 'يوم',
103
+ 'آخر_تحديث': datetime(2025, 3, 1)
104
+ },
105
+ 'هزاز خرسانة': {
106
+ 'يومي': 150.0, # الإيجار اليومي بالريال
107
+ 'وحدة_قياسية': 'يوم',
108
+ 'آخر_تحديث': datetime(2025, 3, 1)
109
+ },
110
+ 'حفارة': {
111
+ 'يومي': 1500.0, # الإيجار اليومي بالريال
112
+ 'وحدة_قياسية': 'يوم',
113
+ 'آخر_تحديث': datetime(2025, 3, 1)
114
+ },
115
+ 'لودر': {
116
+ 'يومي': 1200.0, # الإيجار اليومي بالريال
117
+ 'وحدة_قياسية': 'يوم',
118
+ 'آخر_تحديث': datetime(2025, 3, 1)
119
+ },
120
+ 'رافعة': {
121
+ 'يومي': 2000.0, # الإيجار اليومي بالريال
122
+ 'وحدة_قياسية': 'يوم',
123
+ 'آخر_تحديث': datetime(2025, 3, 1)
124
+ },
125
+ 'شاحنة نقل': {
126
+ 'يومي': 900.0, # الإيجار اليومي بالريال
127
+ 'وحدة_قياسية': 'يوم',
128
+ 'آخر_تحديث': datetime(2025, 3, 1)
129
+ }
130
+ }
131
+ return equipment_rates
132
+
133
+ def calculate_prices(self, items_df):
134
+ """حساب الأسعار للبنود باستخدام التسعير القياسي"""
135
+ # نسخة من البيانات المدخلة للعمل عليها
136
+ df = items_df.copy()
137
+
138
+ # التأكد من وجود العمود المطلوب
139
+ if 'سعر الوحدة' not in df.columns:
140
+ df['سعر الوحدة'] = 0.0
141
+
142
+ if 'الإجمالي' not in df.columns:
143
+ df['الإجمالي'] = 0.0
144
+
145
+ # حساب أسعار الوحدات لكل بند
146
+ for idx, row in df.iterrows():
147
+ # حساب سعر الوحدة بناءً على وصف البند
148
+ unit_price = self._estimate_unit_price(row['وصف البند'], row['الوحدة'])
149
+ df.at[idx, 'سعر الوحدة'] = unit_price
150
+
151
+ # حساب الإجمالي لكل بند
152
+ df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
153
+
154
+ return df
155
+
156
+ def _estimate_unit_price(self, description, unit):
157
+ """تقدير سعر الوحدة بناءً على وصف البند ووحدة القياس"""
158
+ description = description.lower()
159
+
160
+ # تقدير سعر الوحدة بناءً على وصف البند
161
+ if 'خرسان' in description:
162
+ if 'أساسات' in description:
163
+ return 1200.0 if unit == 'م3' else 0.0
164
+ elif 'أعمدة' in description:
165
+ return 1800.0 if unit == 'م3' else 0.0
166
+ elif 'سقف' in description:
167
+ return 1500.0 if unit == 'م3' else 0.0
168
+ else:
169
+ return 1400.0 if unit == 'م3' else 0.0
170
+
171
+ elif 'حديد' in description and 'تسليح' in description:
172
+ if 'أساسات' in description:
173
+ return 6000.0 if unit == 'طن' else 0.0
174
+ elif 'أعمدة' in description or 'سقف' in description:
175
+ return 6500.0 if unit == 'طن' else 0.0
176
+ else:
177
+ return 6200.0 if unit == 'طن' else 0.0
178
+
179
+ elif 'عزل' in description:
180
+ if 'مائي' in description:
181
+ return 120.0 if unit == 'م2' else 0.0
182
+ elif 'حراري' in description:
183
+ return 90.0 if unit == 'م2' else 0.0
184
+ else:
185
+ return 100.0 if unit == 'م2' else 0.0
186
+
187
+ elif 'ردم' in description or 'حفر' in description:
188
+ return 75.0 if unit == 'م3' else 0.0
189
+
190
+ elif 'بلوك' in description or 'طوب' in description:
191
+ return 250.0 if unit == 'م2' else 0.0
192
+
193
+ elif 'لياسة' in description or 'بياض' in description:
194
+ return 80.0 if unit == 'م2' else 0.0
195
+
196
+ elif 'دهان' in description or 'طلاء' in description:
197
+ return 65.0 if unit == 'م2' else 0.0
198
+
199
+ elif 'سيراميك' in description or 'بلاط' in description:
200
+ return 180.0 if unit == 'م2' else 0.0
201
+
202
+ elif 'كهرباء' in description:
203
+ return 150.0 if unit == 'نقطة' else 500.0
204
+
205
+ # قيمة افتراضية إذا لم تتطابق مع أي وصف
206
+ return 100.0
207
+
208
+ def adjust_prices_for_factors(self, items_df, factors=None):
209
+ """تعديل الأسعار بناءً على عوامل مؤثرة"""
210
+ # نسخة من البيانات المدخلة للعمل عليها
211
+ df = items_df.copy()
212
+
213
+ # إذا لم يتم تحديد عوامل، استخدم العوامل الافتراضية
214
+ if factors is None:
215
+ factors = {
216
+ 'location_factor': 1.0, # معامل الموقع
217
+ 'time_factor': 1.0, # معامل الوقت
218
+ 'risk_factor': 1.1, # معامل المخاطر
219
+ 'market_factor': 1.05 # معامل السوق
220
+ }
221
+
222
+ # حساب المعامل الإجمالي
223
+ total_factor = (factors['location_factor'] * factors['time_factor'] *
224
+ factors['risk_factor'] * factors['market_factor'])
225
+
226
+ # تعديل سعر الوحدة بناءً على المعامل ا��إجمالي
227
+ df['سعر الوحدة'] = df['سعر الوحدة'] * total_factor
228
+
229
+ # حساب الإجمالي بعد التعديل
230
+ df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
231
+
232
+ return df
modules/pricing/services/unbalanced_pricing.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ خدمة التسعير غير المتزن
3
+ """
4
+
5
+ import pandas as pd
6
+ import numpy as np
7
+ from datetime import datetime
8
+ import os
9
+ import config
10
+
11
+
12
+ class UnbalancedPricing:
13
+ """خدمة التسعير غير المتزن للبنود"""
14
+
15
+ def __init__(self):
16
+ """تهيئة خدمة التسعير غير المتزن"""
17
+ self.strategies = {
18
+ 'front_loading': self.apply_front_loading,
19
+ 'back_loading': self.apply_back_loading,
20
+ 'confirmed_items': self.apply_confirmed_items_loading,
21
+ 'variable_items': self.apply_variable_items_discount
22
+ }
23
+
24
+ def apply_strategy(self, items_df, strategy, params=None):
25
+ """تطبيق استراتيجية تسعير غير متزن على البنود"""
26
+ # نسخة من البيانات المدخلة للعمل عليها
27
+ df = items_df.copy()
28
+
29
+ # إضافة عمود إستراتيجية التسعير إذا لم يكن موجوداً
30
+ if 'إستراتيجية التسعير' not in df.columns:
31
+ df['إستراتيجية التسعير'] = 'متوازن'
32
+
33
+ # تطبيق الإستراتيجية المطلوبة
34
+ if strategy in self.strategies:
35
+ df = self.strategies[strategy](df, params)
36
+ else:
37
+ # إذا كانت الإستراتيجية غير معروفة، أعد البيانات بدون تغيير
38
+ pass
39
+
40
+ # حساب الإجمالي بعد التعديل
41
+ df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
42
+
43
+ return df
44
+
45
+ def apply_front_loading(self, items_df, params=None):
46
+ """تطبيق استراتيجية التحميل الأمامي (Front Loading)"""
47
+ df = items_df.copy()
48
+
49
+ # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
50
+ if params is None:
51
+ params = {
52
+ 'early_increase': 1.3, # زيادة 30% للبنود المبكرة
53
+ 'late_decrease': 0.7, # تخفيض 30% للبنود المتأخرة
54
+ 'early_percentage': 0.33, # نسبة البنود المبكرة 33%
55
+ 'late_percentage': 0.33 # نسبة البنود المتأخرة 33%
56
+ }
57
+
58
+ # تحديد البنود المبكرة والمتأخرة والمتوسطة
59
+ items_count = len(df)
60
+ early_count = int(items_count * params['early_percentage'])
61
+ late_count = int(items_count * params['late_percentage'])
62
+
63
+ early_items = df.iloc[:early_count].index
64
+ middle_items = df.iloc[early_count:items_count-late_count].index
65
+ late_items = df.iloc[items_count-late_count:].index
66
+
67
+ # تطبيق الزيادة والنقصان
68
+ for idx in early_items:
69
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['early_increase']
70
+ df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
71
+
72
+ for idx in middle_items:
73
+ df.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
74
+
75
+ for idx in late_items:
76
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['late_decrease']
77
+ df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
78
+
79
+ return df
80
+
81
+ def apply_back_loading(self, items_df, params=None):
82
+ """تطبيق استراتيجية التحميل الخلفي (Back Loading)"""
83
+ df = items_df.copy()
84
+
85
+ # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
86
+ if params is None:
87
+ params = {
88
+ 'early_decrease': 0.7, # تخفيض 30% للبنود المبكرة
89
+ 'late_increase': 1.3, # زيادة 30% للبنود المتأخرة
90
+ 'early_percentage': 0.33, # نسبة البنود المبكرة 33%
91
+ 'late_percentage': 0.33 # نسبة البنود المتأخرة 33%
92
+ }
93
+
94
+ # تحديد البنود المبكرة والمتأخرة والمتوسطة
95
+ items_count = len(df)
96
+ early_count = int(items_count * params['early_percentage'])
97
+ late_count = int(items_count * params['late_percentage'])
98
+
99
+ early_items = df.iloc[:early_count].index
100
+ middle_items = df.iloc[early_count:items_count-late_count].index
101
+ late_items = df.iloc[items_count-late_count:].index
102
+
103
+ # تطبيق الزيادة والنقصان
104
+ for idx in early_items:
105
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['early_decrease']
106
+ df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
107
+
108
+ for idx in middle_items:
109
+ df.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
110
+
111
+ for idx in late_items:
112
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['late_increase']
113
+ df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
114
+
115
+ return df
116
+
117
+ def apply_confirmed_items_loading(self, items_df, params=None):
118
+ """تطبيق استراتيجية تحميل البنود المؤكدة"""
119
+ df = items_df.copy()
120
+
121
+ # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
122
+ if params is None:
123
+ params = {
124
+ 'confirmed_increase': 1.25, # زيادة 25% للبنود المؤكدة
125
+ 'others_decrease': 0.85, # تخفيض 15% للبنود الأخرى
126
+ 'confirmed_items_indices': [] # قائمة مؤشرات البنود المؤكدة
127
+ }
128
+
129
+ # إذا لم يتم تحديد البنود المؤكدة، استخدم قواعد اختيار افتراضية
130
+ if not params['confirmed_items_indices']:
131
+ # البنود التي تحتوي على كلمات مثل "أساسات" أو "هيكل" عادة ما تكون مؤكدة
132
+ confirmed_items = []
133
+ for idx, row in df.iterrows():
134
+ description = row['وصف البند'].lower()
135
+ if any(term in description for term in ['أساس', 'خرسان', 'هيكل', 'إنشائي']):
136
+ confirmed_items.append(idx)
137
+ else:
138
+ confirmed_items = params['confirmed_items_indices']
139
+
140
+ # تحديد البنود غير المؤكدة
141
+ all_indices = set(range(len(df)))
142
+ confirmed_indices = set(confirmed_items)
143
+ variable_indices = list(all_indices - confirmed_indices)
144
+
145
+ # تطبيق الزيادة والنقصان
146
+ for idx in confirmed_items:
147
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['confirmed_increase']
148
+ df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
149
+
150
+ for idx in variable_indices:
151
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['others_decrease']
152
+ df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
153
+
154
+ return df
155
+
156
+ def apply_variable_items_discount(self, items_df, params=None):
157
+ """تطبيق استراتيجية تخفيض البنود المحتمل زيادتها"""
158
+ df = items_df.copy()
159
+
160
+ # استخراج المعلمات الافتراضية إذا لم يتم تحديدها
161
+ if params is None:
162
+ params = {
163
+ 'variable_decrease': 0.7, # تخفيض 30% للبنود المحتمل زيادتها
164
+ 'others_increase': 1.15, # زيادة 15% للبنود الأخرى
165
+ 'variable_items_indices': [] # قائمة مؤشرات البنود المحتمل زيادتها
166
+ }
167
+
168
+ # إذا لم يتم تحديد البنود المحتمل زيادتها، استخدم قواعد اختيار افتراضية
169
+ if not params['variable_items_indices']:
170
+ # البنود التي تحتوي على كلمات مثل "حفر" أو "ردم" عادة ما تكون محتمل زيادتها
171
+ variable_items = []
172
+ for idx, row in df.iterrows():
173
+ description = row['وصف البند'].lower()
174
+ if any(term in description for term in ['حفر', 'ردم', 'تمديد', 'صرف', 'مياه']):
175
+ variable_items.append(idx)
176
+ else:
177
+ variable_items = params['variable_items_indices']
178
+
179
+ # تحديد البنود الأخرى
180
+ all_indices = set(range(len(df)))
181
+ variable_indices = set(variable_items)
182
+ other_indices = list(all_indices - variable_indices)
183
+
184
+ # تطبيق الزيادة والنقصان
185
+ for idx in variable_items:
186
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['variable_decrease']
187
+ df.at[idx, 'إستراتيجية التسعير'] = 'نقص'
188
+
189
+ for idx in other_indices:
190
+ df.at[idx, 'سعر الوحدة'] = df.at[idx, 'سعر الوحدة'] * params['others_increase']
191
+ df.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
192
+
193
+ return df
194
+
195
+ def calibrate_prices(self, original_df, unbalanced_df):
196
+ """معايرة الأسعار للحفاظ على إجمالي التسعير الأصلي"""
197
+ # حساب الإجماليات
198
+ original_total = original_df['الإجمالي'].sum()
199
+ unbalanced_total = unbalanced_df['الإجمالي'].sum()
200
+
201
+ # نسخة من البيانات المدخلة للعمل عليها
202
+ df = unbalanced_df.copy()
203
+
204
+ # حساب معامل التعديل
205
+ adjustment_factor = original_total / unbalanced_total if unbalanced_total > 0 else 1.0
206
+
207
+ # تعديل الأسعار
208
+ df['سعر الوحدة'] = df['سعر الوحدة'] * adjustment_factor
209
+
210
+ # حساب الإجمالي بعد التعديل
211
+ df['الإجمالي'] = df['الكمية'] * df['سعر الوحدة']
212
+
213
+ return df
modules/pricing/specs_analyzer.py ADDED
@@ -0,0 +1,527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 random
13
+ import os
14
+ import time
15
+ import io
16
+
17
+ from modules.pricing.services.standard_pricing import StandardPricing
18
+ from modules.pricing.services.unbalanced_pricing import UnbalancedPricing
19
+ from modules.pricing.services.local_content import LocalContentCalculator
20
+ from modules.pricing.services.price_prediction import PricePrediction
21
+ from utils.excel_handler import export_to_excel
22
+ from utils.helpers import format_number, format_currency
23
+
24
+
25
+ class PricingApp:
26
+ """وحدة التسعير المتكاملة"""
27
+
28
+ def __init__(self):
29
+ self.pricing_methods = [
30
+ "التسعير القياسي",
31
+ "التسعير غير المتزن",
32
+ "التسعير التنافسي",
33
+ "التسعير الموجه بالربحية"
34
+ ]
35
+
36
+ # تهيئة خدمات التسعير
37
+ self.standard_pricing = StandardPricing()
38
+ self.unbalanced_pricing = UnbalancedPricing()
39
+ self.local_content = LocalContentCalculator()
40
+ self.price_prediction = PricePrediction()
41
+
42
+ def render(self):
43
+ """عرض واجهة وحدة التسعير"""
44
+
45
+ st.markdown("<h1 class='module-title'>وحدة التسعير المتكاملة</h1>", unsafe_allow_html=True)
46
+
47
+ tabs = st.tabs([
48
+ "إنشاء تسعير جديد",
49
+ "نموذج التسعير الشامل",
50
+ "التسعير غير المتزن",
51
+ "المحتوى المحلي"
52
+ ])
53
+
54
+ with tabs[0]:
55
+ self._render_new_pricing_tab()
56
+
57
+ with tabs[1]:
58
+ self._render_comprehensive_pricing_tab()
59
+
60
+ with tabs[2]:
61
+ self._render_unbalanced_pricing_tab()
62
+
63
+ with tabs[3]:
64
+ self._render_local_content_tab()
65
+
66
+ def _render_new_pricing_tab(self):
67
+ """عرض تبويب إنشاء تسعير جديد"""
68
+
69
+ st.markdown("### إنشاء تسعير جديد")
70
+
71
+ col1, col2 = st.columns(2)
72
+
73
+ with col1:
74
+ tender_name = st.text_input("اسم المناقصة")
75
+ client = st.text_input("الجهة المالكة")
76
+ pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods)
77
+
78
+ with col2:
79
+ tender_number = st.text_input("رقم المناقصة")
80
+ location = st.text_input("الموقع")
81
+ submission_date = st.date_input("تاريخ التقديم")
82
+
83
+ # خيارات بيانات البنود
84
+ st.markdown("### بيانات البنود")
85
+
86
+ data_source = st.radio(
87
+ "مصدر بيانات البنود",
88
+ ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات"]
89
+ )
90
+
91
+ if data_source == "إدخال يدوي":
92
+ # إنشاء بيانات افتراضية
93
+ if 'manual_items' not in st.session_state:
94
+ st.session_state.manual_items = pd.DataFrame({
95
+ 'رقم البند': [f"A{i}" for i in range(1, 6)],
96
+ 'وصف البند': [
97
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
98
+ "توريد وتركيب حديد التسليح للأساسات",
99
+ "أعمال العزل المائي للأساسات",
100
+ "أعمال الردم والدك للأساسات",
101
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
102
+ ],
103
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
104
+ 'الكمية': [250, 25, 500, 300, 120],
105
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
106
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
107
+ })
108
+
109
+ # عرض جدول البنود مع إمكانية التعديل
110
+ edited_items = st.data_editor(
111
+ st.session_state.manual_items,
112
+ use_container_width=True,
113
+ hide_index=True,
114
+ num_rows="dynamic"
115
+ )
116
+ st.session_state.manual_items = edited_items
117
+
118
+ elif data_source == "استيراد من Excel":
119
+ uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"])
120
+
121
+ if uploaded_file is not None:
122
+ st.success("تم رفع الملف بنجاح")
123
+ # محاكاة قراءة الملف
124
+ st.markdown("### معاينة البيانات المستوردة")
125
+
126
+ # إنشاء بيانات افتراضية
127
+ import_items = pd.DataFrame({
128
+ 'رقم البند': [f"A{i}" for i in range(1, 8)],
129
+ 'وصف البند': [
130
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
131
+ "توريد وتركيب حديد التسليح للأساسات",
132
+ "أعمال العزل المائي للأساسات",
133
+ "أعمال الردم والدك للأساسات",
134
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
135
+ "توريد وتركيب حديد التسليح للأعمدة",
136
+ "أعمال البلوك للجدران"
137
+ ],
138
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
139
+ 'الكمية': [250, 25, 500, 300, 120, 10, 400],
140
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
141
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
142
+ })
143
+
144
+ st.dataframe(import_items)
145
+
146
+ if st.button("استيراد البيانات"):
147
+ st.session_state.manual_items = import_items.copy()
148
+ st.session_state.manual_items_modified = True
149
+ st.success("تم استيراد البيانات بنجاح!")
150
+
151
+ else: # استيراد من وحدة تحليل المستندات
152
+ available_documents = [
153
+ "كراسة شروط مشروع توسعة مستشفى الملك فهد",
154
+ "جدول كميات صيانة محطات المياه",
155
+ "مخططات إنشاء مدرسة ثانوية"
156
+ ]
157
+
158
+ selected_doc = st.selectbox("اختر المستند", available_documents)
159
+
160
+ if st.button("استيراد البيانات من تحليل المستند"):
161
+ # محاكاة استيراد البيانات
162
+ with st.spinner("جاري استيراد البيانات..."):
163
+ time.sleep(2)
164
+
165
+ # إنشاء بيانات افتراضية
166
+ doc_items = pd.DataFrame({
167
+ 'رقم البند': [f"A{i}" for i in range(1, 8)],
168
+ 'وصف البند': [
169
+ "توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
170
+ "توريد وتركيب حديد التسليح للأساسات",
171
+ "أعمال العزل المائي للأساسات",
172
+ "أعمال الردم والدك للأساسات",
173
+ "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة",
174
+ "توريد وتركيب حديد التسليح للأعمدة",
175
+ "أعمال البلوك للجدران"
176
+ ],
177
+ 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"],
178
+ 'الكمية': [250, 25, 500, 300, 120, 10, 400],
179
+ 'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
180
+ 'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
181
+ })
182
+
183
+ st.session_state.manual_items = doc_items.copy()
184
+ st.success("تم استيراد البيانات من تحليل المستند بنجاح!")
185
+ st.dataframe(doc_items)
186
+
187
+ # زر بدء التسعير
188
+ if st.button("بدء التسعير"):
189
+ # تحقق من صحة البيانات
190
+ if 'manual_items' in st.session_state and not st.session_state.manual_items.empty:
191
+ # حفظ بيانات التسعير الحالي
192
+ st.session_state.current_pricing = {
193
+ 'name': tender_name,
194
+ 'number': tender_number,
195
+ 'client': client,
196
+ 'location': location,
197
+ 'method': pricing_method,
198
+ 'submission_date': submission_date,
199
+ 'items': st.session_state.manual_items.copy(),
200
+ 'status': 'جديد',
201
+ 'created_at': datetime.now()
202
+ }
203
+
204
+ # الانتقال إلى تبويب نموذج التسعير الشامل
205
+ st.success("تم إ��شاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.")
206
+ else:
207
+ st.error("يرجى إدخال بيانات البنود أولاً.")
208
+
209
+ def _render_comprehensive_pricing_tab(self):
210
+ """عرض تبويب نموذج التسعير الشامل"""
211
+
212
+ st.markdown("### نموذج التسعير الشامل")
213
+
214
+ # التحقق من وجود تسعير حالي
215
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
216
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
217
+ return
218
+
219
+ # عرض معلومات التسعير الحالي
220
+ pricing = st.session_state.current_pricing
221
+
222
+ col1, col2, col3 = st.columns(3)
223
+
224
+ with col1:
225
+ st.metric("اسم المناقصة", pricing['name'])
226
+ st.metric("الجهة المالكة", pricing['client'])
227
+
228
+ with col2:
229
+ st.metric("رقم المناقصة", pricing['number'])
230
+ st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d"))
231
+
232
+ with col3:
233
+ st.metric("طريقة التسعير", pricing['method'])
234
+ st.metric("الموقع", pricing['location'])
235
+
236
+ # عرض البنود والتسعير
237
+ st.markdown("### بنود التسعير")
238
+
239
+ items = pricing['items'].copy()
240
+
241
+ # إضافة أسعار الوحدة للمحاكاة
242
+ if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all():
243
+ items['سعر الوحدة'] = [
244
+ round(random.uniform(1000, 3000), 2), # الخرسانة
245
+ round(random.uniform(5000, 7000), 2), # الحديد
246
+ round(random.uniform(100, 200), 2), # العزل
247
+ round(random.uniform(50, 100), 2), # الردم
248
+ round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة
249
+ ]
250
+
251
+ if len(items) > 5:
252
+ for i in range(5, len(items)):
253
+ items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2)
254
+
255
+ # حساب الإجمالي
256
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
257
+
258
+ # عرض الجدول مع إمكانية التعديل
259
+ edited_items = st.data_editor(
260
+ items,
261
+ use_container_width=True,
262
+ hide_index=True,
263
+ disabled=('رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'الإجمالي')
264
+ )
265
+
266
+ # حساب الإجمالي بعد التعديل
267
+ edited_items['الإجمالي'] = edited_items['الكمية'] * edited_items['سعر الوحدة']
268
+ st.session_state.current_pricing['items'] = edited_items
269
+
270
+ # حساب وعرض إجماليات التسعير
271
+ total_price = edited_items['الإجمالي'].sum()
272
+
273
+ st.markdown("### إجماليات التسعير")
274
+
275
+ col1, col2, col3 = st.columns(3)
276
+
277
+ with col1:
278
+ st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال")
279
+
280
+ with col2:
281
+ overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15)
282
+ overhead_value = total_price * overhead_percentage / 100
283
+ st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال")
284
+
285
+ with col3:
286
+ grand_total = total_price + overhead_value
287
+ st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال")
288
+
289
+ # رسم بياني لتوزيع التكاليف
290
+ st.markdown("### تحليل التكاليف")
291
+
292
+ # حساب النسب المئوية لكل بند
293
+ pie_data = edited_items.copy()
294
+ pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100
295
+
296
+ fig = px.pie(
297
+ pie_data,
298
+ values='نسبة من إجمالي التكاليف',
299
+ names='وصف البند',
300
+ title='توزيع التكاليف حسب البنود',
301
+ hole=0.4
302
+ )
303
+
304
+ st.plotly_chart(fig, use_container_width=True)
305
+
306
+ # أزرار العمليات
307
+ col1, col2, col3 = st.columns(3)
308
+
309
+ with col1:
310
+ if st.button("حفظ التسعير"):
311
+ st.success("تم حفظ التسعير بنجاح!")
312
+
313
+ with col2:
314
+ if st.button("تصدير إلى Excel"):
315
+ st.success("تم تصدير التسعير إلى Excel بنجاح!")
316
+
317
+ with col3:
318
+ if st.button("تحليل المخاطر المالية"):
319
+ st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!")
320
+
321
+ def _render_unbalanced_pricing_tab(self):
322
+ """عرض تبويب التسعير غير المتزن"""
323
+
324
+ st.markdown("### التسعير غير المتزن")
325
+
326
+ # التحقق من وجود تسعير حالي
327
+ if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None:
328
+ st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.")
329
+ return
330
+
331
+ # شرح التسعير غير المتزن
332
+ with st.expander("ما هو التسعير غير المتزن؟", expanded=False):
333
+ st.markdown("""
334
+ **التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء.
335
+
336
+ ### استراتيجيات التسعير غير المتزن:
337
+
338
+ 1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع.
339
+ 2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع.
340
+ 3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة.
341
+ 4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ.
342
+
343
+ ### مزايا التسعير غير المتزن:
344
+
345
+ - تحسين التدفق النقدي للمشروع.
346
+ - تعظيم الربحية في حالة التغييرات والأوامر التغييرية.
347
+ - زيادة فرص الفوز بالمناقصة.
348
+
349
+ ### مخاطر التسعير غير المتزن:
350
+
351
+ - قد يتم رفض العطاء إذا كان عدم التوازن واضحاً.
352
+ - قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط.
353
+ - قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية.
354
+ """)
355
+
356
+ # عرض بنود التسعير الحالي
357
+ items = st.session_state.current_pricing['items'].copy()
358
+
359
+ # إضافة عمود إستراتيجية التسعير
360
+ if 'إستراتيجية التسعير' not in items.columns:
361
+ items['إستراتيجية التسعير'] = 'متوازن'
362
+
363
+ st.markdown("### إستراتيجية التسعير غير المتزن")
364
+
365
+ # اختيار الإستراتيجية
366
+ strategy = st.selectbox(
367
+ "اختر إستراتيجية التسعير",
368
+ [
369
+ "تحميل أمامي (Front Loading)",
370
+ "تحميل البنود المؤكدة",
371
+ "تخفيض البنود المحتمل زيادتها",
372
+ "إستراتيجية مخصصة"
373
+ ]
374
+ )
375
+
376
+ # تطبيق الإستراتيجية المختارة
377
+ if strategy == "تحميل أمامي (Front Loading)":
378
+ # محاكاة تحميل أمامي
379
+ items_count = len(items)
380
+ early_items = items.iloc[:items_count//3].index
381
+ middle_items = items.iloc[items_count//3:2*items_count//3].index
382
+ late_items = items.iloc[2*items_count//3:].index
383
+
384
+ # تطبيق الزيادة والنقصان
385
+ for idx in early_items:
386
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30%
387
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
388
+
389
+ for idx in middle_items:
390
+ items.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
391
+
392
+ for idx in late_items:
393
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
394
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
395
+
396
+ elif strategy == "تحميل البنود المؤكدة":
397
+ # محاكاة - اعتبار بعض البنود مؤكدة
398
+ confirmed_items = [0, 2, 4] # الأصفار-مستندة
399
+ variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items]
400
+
401
+ # تطبيق الزيادة والنقصان
402
+ for idx in confirmed_items:
403
+ if idx < len(items):
404
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25%
405
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
406
+
407
+ for idx in variable_items:
408
+ if idx < len(items):
409
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15%
410
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
411
+
412
+ elif strategy == "تخفيض البنود المحتمل زيادتها":
413
+ # محاكاة - اعتبار بعض البنود محتمل زيادتها
414
+ variable_items = [1, 3] # الأصفار-مستندة
415
+ other_items = [idx for idx in range(len(items)) if idx not in variable_items]
416
+
417
+ # تطبيق الزيادة والنقصان
418
+ for idx in variable_items:
419
+ if idx < len(items):
420
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30%
421
+ items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
422
+
423
+ for idx in other_items:
424
+ if idx < len(items):
425
+ items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15%
426
+ items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
427
+
428
+ else: # إستراتيجية مخصصة
429
+ st.markdown("### تعديل أسعار البنود يدوياً")
430
+ st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً.")
431
+
432
+ # حساب الإجمالي بعد التعديل
433
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
434
+
435
+ # تعيين ألوان للإستراتيجيات
436
+ def highlight_strategy(val):
437
+ if val == 'زيادة':
438
+ return 'background-color: #a8e6cf'
439
+ elif val == 'نقص':
440
+ return 'background-color: #ff9aa2'
441
+ return ''
442
+
443
+ # عرض الجدول مع تنسيق
444
+ st.markdown("### بنود التسعير غير المتزن")
445
+ styled_items = items.style.applymap(highlight_strategy, subset=['إستراتيجية التسعير'])
446
+ st.dataframe(styled_items, use_container_width=True)
447
+
448
+ # المقارنة بين التسعير المتوازن وغير المتوازن
449
+ st.markdown("### مقارنة التسعير المتوازن وغير المتوازن")
450
+
451
+ original_items = st.session_state.current_pricing['items'].copy()
452
+ original_total = original_items['الإجمالي'].sum()
453
+ unbalanced_total = items['الإجمالي'].sum()
454
+
455
+ col1, col2, col3 = st.columns(3)
456
+
457
+ with col1:
458
+ st.metric("إجمالي التسعير المتوازن", f"{original_total:,.2f} ريال")
459
+
460
+ with col2:
461
+ st.metric("إجمالي التسعير غير المتوازن", f"{unbalanced_total:,.2f} ريال")
462
+
463
+ with col3:
464
+ diff = unbalanced_total - original_total
465
+ st.metric("الفرق", f"{diff:,.2f} ريال", delta=f"{diff/original_total*100:.1f}%")
466
+
467
+ # المعايرة للحفاظ على إجمالي التسعير
468
+ if abs(diff) > 1: # إذا كان هناك فرق كبير
469
+ if st.button("معايرة الأسعار للحفاظ على إجمالي التسعير"):
470
+ # تعديل الأسعار للحفاظ على إجمالي التكلفة
471
+ adjustment_factor = original_total / unbalanced_total
472
+ items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor
473
+ items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة']
474
+
475
+ st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)")
476
+ st.dataframe(items, use_container_width=True)
477
+
478
+ # رسم بياني للمقارنة
479
+ st.markdown("### تحليل بصري للتسعير غير المتوازن")
480
+
481
+ # إعداد البيانات للرسم البياني
482
+ chart_data = pd.DataFrame({
483
+ 'وصف البند': original_items['وصف البند'],
484
+ 'التسعير المتوازن': original_items['الإجمالي'],
485
+ 'التسعير غير المتوازن': items['��لإجمالي']
486
+ })
487
+
488
+ # رسم بياني شريطي للمقارنة
489
+ fig = go.Figure()
490
+
491
+ fig.add_trace(go.Bar(
492
+ x=chart_data['وصف البند'],
493
+ y=chart_data['التسعير المتوازن'],
494
+ name='التسعير المتوازن',
495
+ marker_color='rgb(55, 83, 109)'
496
+ ))
497
+
498
+ fig.add_trace(go.Bar(
499
+ x=chart_data['وصف البند'],
500
+ y=chart_data['التسعير غير المتوازن'],
501
+ name='التسعير غير المتوازن',
502
+ marker_color='rgb(26, 118, 255)'
503
+ ))
504
+
505
+ fig.update_layout(
506
+ title='مقارنة بين التسعير المتوازن وغير المتوازن',
507
+ xaxis_tickfont_size=14,
508
+ yaxis=dict(
509
+ title='الإجمالي (ريال)',
510
+ titlefont_size=16,
511
+ tickfont_size=14,
512
+ ),
513
+ legend=dict(
514
+ x=0,
515
+ y=1.0,
516
+ bgcolor='rgba(255, 255, 255, 0)',
517
+ bordercolor='rgba(255, 255, 255, 0)'
518
+ ),
519
+ barmode='group',
520
+ bargap=0.15,
521
+ bargroupgap=0.1
522
+ )
523
+
524
+ st.plotly_chart(fig, use_container_width=True)
525
+
526
+ # زر حفظ التسعير غير المتوازن
527
+ if st.button("
modules/projects/projects_app.py ADDED
@@ -0,0 +1,630 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
13
+ from utils.helpers import format_number, format_currency
14
+ from utils.excel_handler import export_to_excel
15
+
16
+
17
+ class ProjectsApp:
18
+ """وحدة إدارة المشاريع"""
19
+
20
+ def __init__(self):
21
+ """تهيئة وحدة إدارة المشاريع"""
22
+ # تهيئة البيانات المبدئية
23
+ if 'projects' not in st.session_state:
24
+ st.session_state.projects = self._generate_sample_projects()
25
+
26
+ def render(self):
27
+ """عرض واجهة وحدة إدارة المشاريع"""
28
+
29
+ st.markdown("<h1 class='module-title'>وحدة إدارة المشاريع</h1>", unsafe_allow_html=True)
30
+
31
+ tabs = st.tabs([
32
+ "قائمة المشاريع",
33
+ "إضافة مشروع جديد",
34
+ "تفاصيل المشروع",
35
+ "متابعة المشاريع"
36
+ ])
37
+
38
+ with tabs[0]:
39
+ self._render_projects_list_tab()
40
+
41
+ with tabs[1]:
42
+ self._render_add_project_tab()
43
+
44
+ with tabs[2]:
45
+ self._render_project_details_tab()
46
+
47
+ with tabs[3]:
48
+ self._render_projects_tracking_tab()
49
+
50
+ def _render_projects_list_tab(self):
51
+ """عرض تبويب قائمة المشاريع"""
52
+
53
+ st.markdown("### قائمة المشاريع")
54
+
55
+ # فلترة المشاريع
56
+ col1, col2, col3 = st.columns(3)
57
+
58
+ with col1:
59
+ search_term = st.text_input("البحث في المشاريع", key="project_search")
60
+
61
+ with col2:
62
+ status_filter = st.multiselect(
63
+ "حالة المشروع",
64
+ ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
65
+ default=["جديد", "قيد التسعير", "تم التقديم"],
66
+ key="project_status_filter"
67
+ )
68
+
69
+ with col3:
70
+ client_filter = st.multiselect(
71
+ "الجهة المالكة",
72
+ list(set([p['client'] for p in st.session_state.projects])),
73
+ key="project_client_filter"
74
+ )
75
+
76
+ # تطبيق الفلترة
77
+ filtered_projects = st.session_state.projects
78
+
79
+ if search_term:
80
+ filtered_projects = [p for p in filtered_projects if search_term.lower() in p['name'].lower() or search_term in p['number']]
81
+
82
+ if status_filter:
83
+ filtered_projects = [p for p in filtered_projects if p['status'] in status_filter]
84
+
85
+ if client_filter:
86
+ filtered_projects = [p for p in filtered_projects if p['client'] in client_filter]
87
+
88
+ # تحويل المشاريع المفلترة إلى DataFrame للعرض
89
+ if filtered_projects:
90
+ projects_df = pd.DataFrame(filtered_projects)
91
+
92
+ # اختيار وترتيب الأعمدة
93
+ display_columns = [
94
+ 'name', 'number', 'client', 'location', 'status',
95
+ 'submission_date', 'tender_type', 'created_at'
96
+ ]
97
+
98
+ # تغيير أسماء الأعمدة للعرض
99
+ column_names = {
100
+ 'name': 'اسم المشروع',
101
+ 'number': 'رقم المناقصة',
102
+ 'client': 'الجهة المالكة',
103
+ 'location': 'الموقع',
104
+ 'status': 'الحالة',
105
+ 'submission_date': 'تاريخ التقديم',
106
+ 'tender_type': 'نوع المناقصة',
107
+ 'created_at': 'تاريخ الإنشاء'
108
+ }
109
+
110
+ display_df = projects_df[display_columns].rename(columns=column_names)
111
+
112
+ # تنسيق التواريخ
113
+ date_columns = ['تاريخ التقديم', 'تاريخ الإنشاء']
114
+ for col in date_columns:
115
+ if col in display_df.columns:
116
+ display_df[col] = pd.to_datetime(display_df[col]).dt.strftime('%Y-%m-%d')
117
+
118
+ # عرض الجدول
119
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
120
+
121
+ # زر تصدير المشاريع
122
+ if st.button("تصدير المشاريع إلى Excel"):
123
+ # محاكاة التصدير
124
+ st.success("تم تصدير المشاريع بنجاح!")
125
+ else:
126
+ st.info("لا توجد مشاريع تطابق معايير البحث.")
127
+
128
+ def _render_add_project_tab(self):
129
+ """عرض تبويب إضافة مشروع جديد"""
130
+
131
+ st.markdown("### إضافة مشروع جديد")
132
+
133
+ # نموذج إدخال بيانات المشروع
134
+ with st.form("new_project_form"):
135
+ col1, col2 = st.columns(2)
136
+
137
+ with col1:
138
+ project_name = st.text_input("اسم المشروع", key="new_project_name")
139
+ client = st.text_input("الجهة المالكة", key="new_project_client")
140
+ location = st.text_input("الموقع", key="new_project_location")
141
+ tender_type = st.selectbox(
142
+ "نوع المناقصة",
143
+ ["عامة", "خاصة", "أمر مباشر"],
144
+ key="new_project_tender_type"
145
+ )
146
+
147
+ with col2:
148
+ tender_number = st.text_input("رقم المناقصة", key="new_project_number")
149
+ submission_date = st.date_input("تاريخ التقديم", key="new_project_submission_date")
150
+ pricing_method = st.selectbox(
151
+ "طريقة التسعير",
152
+ ["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
153
+ key="new_project_pricing_method"
154
+ )
155
+ status = st.selectbox(
156
+ "حالة المشروع",
157
+ ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
158
+ index=0,
159
+ key="new_project_status"
160
+ )
161
+
162
+ description = st.text_area("وصف المشروع", key="new_project_description")
163
+
164
+ submitted = st.form_submit_button("إضافة المشروع")
165
+
166
+ if submitted:
167
+ # التحقق من تعبئة الحقول الإلزامية
168
+ if not project_name or not tender_number or not client:
169
+ st.error("يرجى تعبئة جميع الحقول الإلزامية (اسم المشروع، رقم المناقصة، الجهة المالكة).")
170
+ else:
171
+ # إنشاء مشروع جديد
172
+ new_project = {
173
+ 'id': len(st.session_state.projects) + 1,
174
+ 'name': project_name,
175
+ 'number': tender_number,
176
+ 'client': client,
177
+ 'location': location,
178
+ 'description': description,
179
+ 'status': status,
180
+ 'tender_type': tender_type,
181
+ 'pricing_method': pricing_method,
182
+ 'submission_date': submission_date,
183
+ 'created_at': datetime.now(),
184
+ 'created_by_id': 1 # معرف المستخدم الحالي
185
+ }
186
+
187
+ # إضافة المشروع إلى قائمة المشاريع
188
+ st.session_state.projects.append(new_project)
189
+
190
+ # رسالة نجاح
191
+ st.success(f"تم إضافة المشروع [{project_name}] بنجاح!")
192
+
193
+ # تعيين المشروع الحالي
194
+ st.session_state.current_project = new_project
195
+
196
+ def _render_project_details_tab(self):
197
+ """عرض تبويب تفاصيل المشروع"""
198
+
199
+ st.markdown("### تفاصيل المشروع")
200
+
201
+ # التحقق من وجود مشروع حالي
202
+ if 'current_project' not in st.session_state or st.session_state.current_project is None:
203
+ # إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع
204
+ project_names = [p['name'] for p in st.session_state.projects]
205
+ selected_project_name = st.selectbox("اختر المشروع", project_names)
206
+
207
+ if selected_project_name:
208
+ selected_project = next((p for p in st.session_state.projects if p['name'] == selected_project_name), None)
209
+ if selected_project:
210
+ st.session_state.current_project = selected_project
211
+ else:
212
+ st.warning("لم يتم العثور على المشروع المحدد.")
213
+ return
214
+ else:
215
+ st.info("يرجى اختيار مشروع لعرض تفاصيله.")
216
+ return
217
+
218
+ # عرض تفاصيل المشروع
219
+ project = st.session_state.current_project
220
+
221
+ # عرض معلومات المشروع الأساسية
222
+ col1, col2, col3 = st.columns(3)
223
+
224
+ with col1:
225
+ st.markdown(f"**اسم المشروع**: {project['name']}")
226
+ st.markdown(f"**رقم المناقصة**: {project['number']}")
227
+ st.markdown(f"**الجهة المالكة**: {project['client']}")
228
+
229
+ with col2:
230
+ st.markdown(f"**الموقع**: {project['location']}")
231
+ st.markdown(f"**نوع المناقصة**: {project['tender_type']}")
232
+ st.markdown(f"**حالة المشروع**: {project['status']}")
233
+
234
+ with col3:
235
+ st.markdown(f"**طريقة التسعير**: {project['pricing_method']}")
236
+ st.markdown(f"**تاريخ التقديم**: {project['submission_date'].strftime('%Y-%m-%d') if isinstance(project['submission_date'], datetime) else project['submission_date']}")
237
+ st.markdown(f"**تاريخ الإنشاء**: {project['created_at'].strftime('%Y-%m-%d') if isinstance(project['created_at'], datetime) else project['created_at']}")
238
+
239
+ # عرض وصف المشروع
240
+ st.markdown("#### وصف المشروع")
241
+ st.text_area("", value=project.get('description', ''), disabled=True, height=100)
242
+
243
+ # عرض المستندات المرتبطة بالمشروع
244
+ st.markdown("#### مستندات المشروع")
245
+
246
+ if 'documents' in project and project['documents']:
247
+ docs_df = pd.DataFrame(project['documents'])
248
+ st.dataframe(docs_df, use_container_width=True, hide_index=True)
249
+ else:
250
+ st.info("لا توجد مستندات مرتبطة بهذا المشروع حاليًا.")
251
+
252
+ # زر إضافة مستندات
253
+ if st.button("إضافة مستندات"):
254
+ st.session_state.upload_documents = True
255
+
256
+ # واجهة تحميل المستندات
257
+ if 'upload_documents' in st.session_state and st.session_state.upload_documents:
258
+ st.markdown("#### تحميل مستندات جديدة")
259
+
260
+ uploaded_file = st.file_uploader("اختر ملفًا", type=['pdf', 'docx', 'xlsx', 'png', 'jpg', 'dwg'])
261
+ doc_type = st.selectbox("نوع المستند", ["كراسة شروط", "عقد", "مخططات", "جدول كميات", "مواصفات فنية", "تعديلات وملاحق"])
262
+
263
+ if uploaded_file and st.button("تحميل المستند"):
264
+ # محاكاة تحميل المستند
265
+ with st.spinner("جاري تحميل المستند..."):
266
+ time.sleep(2)
267
+
268
+ # إنشاء مستند جديد
269
+ new_document = {
270
+ 'filename': uploaded_file.name,
271
+ 'type': doc_type,
272
+ 'upload_date': datetime.now().strftime('%Y-%m-%d'),
273
+ 'size': f"{uploaded_file.size / 1024:.1f} KB"
274
+ }
275
+
276
+ # إضافة المستند إلى المشروع
277
+ if 'documents' not in project:
278
+ project['documents'] = []
279
+
280
+ project['documents'].append(new_document)
281
+
282
+ st.success(f"تم تحميل المستند [{uploaded_file.name}] بنجاح!")
283
+ st.session_state.upload_documents = False
284
+ st.experimental_rerun()
285
+
286
+ # عرض البنود والكميات
287
+ st.markdown("#### بنود وكميات المشروع")
288
+
289
+ if 'items' in project and project['items']:
290
+ items_df = pd.DataFrame(project['items'])
291
+ st.dataframe(items_df, use_container_width=True, hide_index=True)
292
+
293
+ # زر لتحويل البنود إلى وحدة التسعير
294
+ if st.button("تحويل البنود إلى وحدة التسعير"):
295
+ if 'manual_items' not in st.session_state:
296
+ st.session_state.manual_items = pd.DataFrame()
297
+
298
+ st.session_state.manual_items = items_df.copy()
299
+ st.success("تم تحويل البنود إلى وحدة التسعير بنجاح!")
300
+ else:
301
+ st.info("لا توجد بنود وكميات لهذا المشروع حاليًا.")
302
+
303
+ # زر استيراد البنود من وحدة تحليل المستندات
304
+ if st.button("استيراد البنود من تحليل المستندات"):
305
+ st.warning("ميزة استيراد البنود من تحليل المستندات قيد التطوير.")
306
+
307
+ # أزرار الإجراءات
308
+ col1, col2, col3 = st.columns(3)
309
+
310
+ with col1:
311
+ if st.button("تعديل المشروع"):
312
+ st.session_state.edit_project = True
313
+ st.experimental_rerun()
314
+
315
+ with col2:
316
+ if st.button("تصدير بيانات المشروع"):
317
+ st.success("تم تصدير بيانات المشروع بنجاح!")
318
+
319
+ with col3:
320
+ if st.button("إرسال للاعتماد"):
321
+ st.success("تم إرسال المشروع للاعتماد بنجاح!")
322
+
323
+ # نموذج تعديل المشروع
324
+ if 'edit_project' in st.session_state and st.session_state.edit_project:
325
+ st.markdown("#### تعديل المشروع")
326
+
327
+ with st.form("edit_project_form"):
328
+ col1, col2 = st.columns(2)
329
+
330
+ with col1:
331
+ project_name = st.text_input("اسم المشروع", value=project['name'])
332
+ client = st.text_input("الجهة المالكة", value=project['client'])
333
+ location = st.text_input("الموقع", value=project['location'])
334
+ tender_type = st.selectbox(
335
+ "نوع المناقصة",
336
+ ["عامة", "خاصة", "أمر مباشر"],
337
+ index=["عامة", "خاصة", "أمر مباشر"].index(project['tender_type'])
338
+ )
339
+
340
+ with col2:
341
+ tender_number = st.text_input("رقم المناقصة", value=project['number'])
342
+ submission_date = st.date_input(
343
+ "تاريخ التقديم",
344
+ value=datetime.strptime(project['submission_date'], "%Y-%m-%d") if isinstance(project['submission_date'], str) else project['submission_date']
345
+ )
346
+ pricing_method = st.selectbox(
347
+ "طريقة التسعير",
348
+ ["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"],
349
+ index=["قياسي", "غير متزن", "تنافسي", "موجه بالربحية"].index(project['pricing_method'])
350
+ )
351
+ status = st.selectbox(
352
+ "حالة المشروع",
353
+ ["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"],
354
+ index=["جديد", "قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ", "منتهي", "ملغي"].index(project['status'])
355
+ )
356
+
357
+ description = st.text_area("وصف المشروع", value=project.get('description', ''))
358
+
359
+ col1, col2 = st.columns(2)
360
+
361
+ with col1:
362
+ submit = st.form_submit_button("حفظ التعديلات")
363
+
364
+ with col2:
365
+ cancel = st.form_submit_button("إلغاء")
366
+
367
+ if submit:
368
+ # تحديث بيانات المشروع
369
+ project['name'] = project_name
370
+ project['number'] = tender_number
371
+ project['client'] = client
372
+ project['location'] = location
373
+ project['description'] = description
374
+ project['status'] = status
375
+ project['tender_type'] = tender_type
376
+ project['pricing_method'] = pricing_method
377
+ project['submission_date'] = submission_date
378
+
379
+ st.success("تم تحديث بيانات المشروع بنجاح!")
380
+ st.session_state.edit_project = False
381
+ st.experimental_rerun()
382
+
383
+ elif cancel:
384
+ st.session_state.edit_project = False
385
+ st.experimental_rerun()
386
+
387
+ def _render_projects_tracking_tab(self):
388
+ """عرض تبويب متابعة المشاريع"""
389
+
390
+ st.markdown("### متابعة المشاريع")
391
+
392
+ # عرض إحصائيات المشاريع
393
+ col1, col2, col3, col4 = st.columns(4)
394
+
395
+ projects = st.session_state.projects
396
+
397
+ with col1:
398
+ total_projects = len(projects)
399
+ st.metric("إجمالي المشاريع", total_projects)
400
+
401
+ with col2:
402
+ active_projects = len([p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]])
403
+ st.metric("المشاريع النشطة", active_projects)
404
+
405
+ with col3:
406
+ pending_submission = len([p for p in projects if p['status'] in ["جديد", "قيد التسعير"]])
407
+ st.metric("مشاريع قيد التسعير", pending_submission)
408
+
409
+ with col4:
410
+ completed_projects = len([p for p in projects if p['status'] in ["منتهي"]])
411
+ st.metric("المشاريع المنتهية", completed_projects)
412
+
413
+ # عرض رسم بياني لحالة المشاريع
414
+ st.markdown("#### توزيع المشاريع حسب الحالة")
415
+
416
+ status_counts = {}
417
+ for p in projects:
418
+ status = p['status']
419
+ status_counts[status] = status_counts.get(status, 0) + 1
420
+
421
+ status_df = pd.DataFrame({
422
+ 'الحالة': list(status_counts.keys()),
423
+ 'عدد المشاريع': list(status_counts.values())
424
+ })
425
+
426
+ st.bar_chart(status_df.set_index('الحالة'))
427
+
428
+ # عرض المشاريع قيد المتابعة
429
+ st.markdown("#### المشاريع قيد المتابعة")
430
+
431
+ # عرض المشاريع النشطة المرتبة حسب تاريخ التقديم
432
+ active_projects_list = [p for p in projects if p['status'] in ["قيد التسعير", "تم التقديم", "تمت الترسية", "قيد التنفيذ"]]
433
+
434
+ if active_projects_list:
435
+ # تحويل التواريخ إلى كائنات تاريخ إذا كانت نصوصًا
436
+ for p in active_projects_list:
437
+ if isinstance(p['submission_date'], str):
438
+ p['submission_date'] = datetime.strptime(p['submission_date'], "%Y-%m-%d")
439
+
440
+ # ترتيب المشاريع حسب تاريخ التقديم
441
+ active_projects_list.sort(key=lambda x: x['submission_date'])
442
+
443
+ # تحويل إلى DataFrame
444
+ active_df = pd.DataFrame(active_projects_list)
445
+
446
+ # اختيار وترتيب الأعمدة
447
+ display_columns = [
448
+ 'name', 'number', 'client', 'status',
449
+ 'submission_date', 'tender_type'
450
+ ]
451
+
452
+ # تغيير أسماء الأعمدة
453
+ column_names = {
454
+ 'name': 'اسم المشروع',
455
+ 'number': 'رقم المناقصة',
456
+ 'client': 'الجهة المالكة',
457
+ 'status': 'الحالة',
458
+ 'submission_date': 'تاريخ التقديم',
459
+ 'tender_type': 'نوع المناقصة'
460
+ }
461
+
462
+ # تنسيق البيانات
463
+ display_df = active_df[display_columns].rename(columns=column_names)
464
+ display_df['تاريخ التقديم'] = pd.to_datetime(display_df['تاريخ التقديم']).dt.strftime('%Y-%m-%d')
465
+
466
+ # عرض الجدول
467
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
468
+ else:
469
+ st.info("لا توجد مشاريع نشطة حاليًا.")
470
+
471
+ # عرض المشاريع المقبلة
472
+ st.markdown("#### المواعيد المقبلة")
473
+
474
+ upcoming_events = []
475
+ today = datetime.now().date()
476
+
477
+ for p in projects:
478
+ submission_date = p['submission_date']
479
+ if isinstance(submission_date, str):
480
+ submission_date = datetime.strptime(submission_date, "%Y-%m-%d").date()
481
+ elif isinstance(submission_date, datetime):
482
+ submission_date = submission_date.date()
483
+
484
+ # المشاريع التي موعد تقديمها خلال الأسبوعين القادمين
485
+ if today <= submission_date <= today + timedelta(days=14) and p['status'] in ["قيد التسعير"]:
486
+ days_left = (submission_date - today).days
487
+ upcoming_events.append({
488
+ 'المشروع': p['name'],
489
+ 'الحدث': 'موعد تقديم المناقصة',
490
+ 'التاريخ': submission_date.strftime('%Y-%m-%d'),
491
+ 'الأيام المتبقية': days_left
492
+ })
493
+
494
+ if upcoming_events:
495
+ events_df = pd.DataFrame(upcoming_events)
496
+ st.dataframe(events_df, use_container_width=True, hide_index=True)
497
+ else:
498
+ st.info("لا توجد مواعيد قريبة.")
499
+
500
+ def _generate_sample_projects(self):
501
+ """توليد بيانات افتراضية للمشاريع"""
502
+
503
+ projects = [
504
+ {
505
+ 'id': 1,
506
+ 'name': "إنشاء مبنى مستشفى الولادة والأطفال بمنطقة الشرقية",
507
+ 'number': "SHPD-2025-001",
508
+ 'client': "وزارة الصحة",
509
+ 'location': "الدمام، المنطقة الشرقية",
510
+ 'description': "يشمل المشروع إنشاء وتجهيز مبنى مستشفى الولادة والأطفال بسعة 300 سرير، ويتكون المبنى من 4 طوابق بمساحة إجمالية 15,000 متر مربع.",
511
+ 'status': "قيد التسعير",
512
+ 'tender_type': "عامة",
513
+ 'pricing_method': "قياسي",
514
+ 'submission_date': (datetime.now() + timedelta(days=5)),
515
+ 'created_at': datetime.now() - timedelta(days=10),
516
+ 'created_by_id': 1,
517
+ 'documents': [
518
+ {
519
+ 'filename': "كراسة الشروط والمواصفات.pdf",
520
+ 'type': "كراسة شروط",
521
+ 'upload_date': (datetime.now() - timedelta(days=9)).strftime('%Y-%m-%d'),
522
+ 'size': "5.2 MB"
523
+ },
524
+ {
525
+ 'filename': "المخططات الهندسية.dwg",
526
+ 'type': "مخططات",
527
+ 'upload_date': (datetime.now() - timedelta(days=8)).strftime('%Y-%m-%d'),
528
+ 'size': "25.7 MB"
529
+ },
530
+ {
531
+ 'filename': "جدول الكميات.xlsx",
532
+ 'type': "جدول كميات",
533
+ 'upload_date': (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'),
534
+ 'size': "1.8 MB"
535
+ }
536
+ ],
537
+ 'items': [
538
+ {
539
+ 'رقم البند': "A1",
540
+ 'وصف البند': "أعمال الحفر والردم",
541
+ 'الوحدة': "م3",
542
+ 'الكمية': 12500
543
+ },
544
+ {
545
+ 'رقم البند': "A2",
546
+ 'وصف البند': "أعمال الخرسانة المسلحة للأساسات",
547
+ 'الوحدة': "م3",
548
+ 'الكمية': 3500
549
+ },
550
+ {
551
+ 'رقم البند': "A3",
552
+ 'وصف البند': "أعمال حديد التسليح",
553
+ 'الوحدة': "طن",
554
+ 'الكمية': 450
555
+ }
556
+ ]
557
+ },
558
+ {
559
+ 'id': 2,
560
+ 'name': "صيانة وتطوير طريق الملك عبدالله",
561
+ 'number': "MOT-2025-042",
562
+ 'client': "وزارة النقل",
563
+ 'location': "الرياض، المنطقة الوسطى",
564
+ 'description': "صيانة وتطوير طريق الملك عبدالله بطول 25 كم، ويشمل المشروع إعادة الرصف وتحسين الإنارة وتركيب اللوحات الإرشادية.",
565
+ 'status': "تم التقديم",
566
+ 'tender_type': "عامة",
567
+ 'pricing_method': "غير متزن",
568
+ 'submission_date': (datetime.now() - timedelta(days=15)),
569
+ 'created_at': datetime.now() - timedelta(days=45),
570
+ 'created_by_id': 1
571
+ },
572
+ {
573
+ 'id': 3,
574
+ 'name': "إنشاء محطة معالجة مياه الصرف الصحي",
575
+ 'number': "SWPC-2025-007",
576
+ 'client': "شركة المياه الوطنية",
577
+ 'location': "جدة، المنطقة الغربية",
578
+ 'description': "إنشاء محطة معالجة مياه الصرف الصحي بطاقة استيعابية 50,000 م3/يوم، مع جميع الأعمال المدنية والكهروميكانيكية.",
579
+ 'status': "تمت الترسية",
580
+ 'tender_type': "عامة",
581
+ 'pricing_method': "قياسي",
582
+ 'submission_date': (datetime.now() - timedelta(days=90)),
583
+ 'created_at': datetime.now() - timedelta(days=120),
584
+ 'created_by_id': 1
585
+ },
586
+ {
587
+ 'id': 4,
588
+ 'name': "إنشاء منتزه الملك سلمان",
589
+ 'number': "RAM-2025-015",
590
+ 'client': "أمانة منطقة الرياض",
591
+ 'location': "الرياض، المنطقة الوسطى",
592
+ 'description': "إنشاء منتزه الملك سلمان على مساحة 500,000 متر مربع، ويشمل المشروع أعمال التشجير والتنسيق والمسطحات المائية والمباني الخدمية.",
593
+ 'status': "قيد التنفيذ",
594
+ 'tender_type': "عامة",
595
+ 'pricing_method': "قياسي",
596
+ 'submission_date': (datetime.now() - timedelta(days=180)),
597
+ 'created_at': datetime.now() - timedelta(days=210),
598
+ 'created_by_id': 1
599
+ },
600
+ {
601
+ 'id': 5,
602
+ 'name': "إنشاء مبنى مختبرات كلية العلوم",
603
+ 'number': "KSU-2025-032",
604
+ 'client': "جامعة الملك سعود",
605
+ 'location': "الرياض، المنطقة الوسطى",
606
+ 'description': "إنشاء مبنى المختبرات الجديد لكلية العلوم بمساحة 8,000 متر مربع، ويتكون من 3 طوابق ويشمل تجهيز المعامل والمختبرات العلمية.",
607
+ 'status': "جديد",
608
+ 'tender_type': "خاصة",
609
+ 'pricing_method': "تنافسي",
610
+ 'submission_date': (datetime.now() + timedelta(days=10)),
611
+ 'created_at': datetime.now() - timedelta(days=5),
612
+ 'created_by_id': 1
613
+ },
614
+ {
615
+ 'id': 6,
616
+ 'name': "توريد وتركيب أنظمة الطاقة الشمسية",
617
+ 'number': "SEC-2025-098",
618
+ 'client': "الشركة السعودية للكهرباء",
619
+ 'location': "تبوك، المنطقة الشمالية",
620
+ 'description': "توريد وتركيب أنظمة الطاقة الشمسية بقدرة 5 ميجاوات، مع جميع الأعمال المدنية والكهربائية.",
621
+ 'status': "جديد",
622
+ 'tender_type': "عامة",
623
+ 'pricing_method': "قياسي",
624
+ 'submission_date': (datetime.now() + timedelta(days=20)),
625
+ 'created_at': datetime.now() - timedelta(days=2),
626
+ 'created_by_id': 1
627
+ }
628
+ ]
629
+
630
+ return projects
modules/reports/reports_app.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ pass
12
+
13
+ def render(self):
14
+ st.markdown("<h1 class='module-title'>وحدة التقارير والتحليلات</h1>", unsafe_allow_html=True)
15
+ tabs = st.tabs(["لوحة المعلومات", "تقارير المشاريع", "تقارير التسعير", "تقارير المخاطر", "التقارير المخصصة"])
16
+
17
+ with tabs[0]:
18
+ self._render_dashboard_tab()
19
+
20
+ # باقي التبويبات موجودة ولكن لم يتم طلب تصحيحها في هذا السياق
21
+
22
+ def _render_dashboard_tab(self):
23
+ st.markdown("### لوحة معلومات النظام")
24
+
25
+ col1, col2, col3, col4 = st.columns(4)
26
+
27
+ with col1:
28
+ total_projects = self._get_total_projects()
29
+ st.metric("إجمالي المشاريع", total_projects)
30
+
31
+ with col2:
32
+ active_projects = self._get_active_projects()
33
+ st.metric("المشاريع النشطة", active_projects, delta=f"{active_projects/total_projects*100:.1f}%" if total_projects > 0 else "0%")
34
+
35
+ with col3:
36
+ won_projects = self._get_won_projects()
37
+ st.metric("المشاريع المرساة", won_projects, delta=f"{won_projects/total_projects*100:.1f}%" if total_projects > 0 else "0%")
38
+
39
+ with col4:
40
+ avg_local_content = self._get_avg_local_content()
41
+ st.metric("متوسط المحتوى المحلي", f"{avg_local_content:.1f}%", delta=f"{avg_local_content-70:.1f}%" if avg_local_content > 0 else "0%")
42
+
43
+ st.markdown("#### توزيع المشاريع حسب الحالة")
44
+ project_status_data = self._get_project_status_data()
45
+ fig = px.pie(project_status_data, values='count', names='status', title='توزيع المشاريع حسب الحالة', hole=0.4)
46
+ st.plotly_chart(fig, use_container_width=True)
47
+
48
+ st.markdown("#### اتجاه المشاريع الشهري")
49
+ monthly_data = self._get_monthly_project_data()
50
+ fig = px.line(monthly_data, x='month', y=['new', 'submitted', 'won'], title='اتجاه المشاريع الشهري')
51
+ st.plotly_chart(fig, use_container_width=True)
52
+
53
+ st.markdown("#### توزيع قيم المشاريع")
54
+ project_value_data = self._get_project_value_data()
55
+ fig = px.bar(project_value_data, x='range', y='count', title='توزيع قيم المشاريع')
56
+ st.plotly_chart(fig, use_container_width=True)
57
+
58
+ def _get_total_projects(self):
59
+ return 10
60
+
61
+ def _get_active_projects(self):
62
+ return 7
63
+
64
+ def _get_won_projects(self):
65
+ return 4
66
+
67
+ def _get_avg_local_content(self):
68
+ return 72.5
69
+
70
+ def _get_project_status_data(self):
71
+ return pd.DataFrame({
72
+ 'status': ['جديد', 'قيد التنفيذ', 'تمت الترسية', 'ملغي'],
73
+ 'count': [5, 3, 1, 1]
74
+ })
75
+
76
+ def _get_monthly_project_data(self):
77
+ return pd.DataFrame({
78
+ 'month': ['يناير', 'فبراير', 'مارس'],
79
+ 'new': [2, 3, 4],
80
+ 'submitted': [1, 2, 3],
81
+ 'won': [0, 1, 2]
82
+ })
83
+
84
+ def _get_project_value_data(self):
85
+ return pd.DataFrame({
86
+ 'range': ['0-500K', '500K-1M', '1M-2M', '2M+'],
87
+ 'count': [2, 3, 4, 1]
88
+ })
modules/resources/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """
2
+ وحدة إدارة الموارد
3
+ """
4
+
5
+ __version__ = '1.0.0'
modules/resources/resources_app.py ADDED
@@ -0,0 +1,1410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, timedelta
12
+ import time
13
+ import io
14
+ import os
15
+ import tempfile
16
+ import random
17
+
18
+
19
+ class ResourcesApp:
20
+ """وحدة إدارة الموارد"""
21
+
22
+ def __init__(self):
23
+ """تهيئة وحدة الموارد"""
24
+
25
+ # تهيئة البيانات في حالة الجلسة إذا لم تكن موجودة
26
+ if 'materials' not in st.session_state:
27
+ st.session_state.materials = [
28
+ {
29
+ 'id': 1,
30
+ 'name': 'خرسانة جاهزة',
31
+ 'category': 'مواد إنشائية',
32
+ 'unit': 'م3',
33
+ 'price': 250,
34
+ 'supplier': 'شركة الخرسانة الوطنية',
35
+ 'local_content': 95,
36
+ 'last_updated': '2024-01-15'
37
+ },
38
+ {
39
+ 'id': 2,
40
+ 'name': 'حديد تسليح',
41
+ 'category': 'مواد إنشائية',
42
+ 'unit': 'طن',
43
+ 'price': 4500,
44
+ 'supplier': 'مصنع الحديد السعودي',
45
+ 'local_content': 45,
46
+ 'last_updated': '2024-02-10'
47
+ },
48
+ {
49
+ 'id': 3,
50
+ 'name': 'بلوك خرساني',
51
+ 'category': 'مواد إنشائية',
52
+ 'unit': 'م3',
53
+ 'price': 350,
54
+ 'supplier': 'مصنع البلوك الحديث',
55
+ 'local_content': 95,
56
+ 'last_updated': '2024-01-20'
57
+ },
58
+ {
59
+ 'id': 4,
60
+ 'name': 'رمل',
61
+ 'category': 'مواد إنشائية',
62
+ 'unit': 'م3',
63
+ 'price': 60,
64
+ 'supplier': 'مؤسسة توريدات البناء',
65
+ 'local_content': 100,
66
+ 'last_updated': '2024-01-15'
67
+ },
68
+ {
69
+ 'id': 5,
70
+ 'name': 'بلاط سيراميك',
71
+ 'category': 'مواد تشطيب',
72
+ 'unit': 'م2',
73
+ 'price': 120,
74
+ 'supplier': 'شركة السيراميك الوطنية',
75
+ 'local_content': 80,
76
+ 'last_updated': '2024-02-05'
77
+ }
78
+ ]
79
+
80
+ if 'labor' not in st.session_state:
81
+ st.session_state.labor = [
82
+ {
83
+ 'id': 1,
84
+ 'name': 'مهندس مدني',
85
+ 'category': 'هندسة',
86
+ 'unit': 'شهر',
87
+ 'price': 15000,
88
+ 'supplier': 'داخلي',
89
+ 'local_content': 90,
90
+ 'last_updated': '2024-01-10'
91
+ },
92
+ {
93
+ 'id': 2,
94
+ 'name': 'مهندس معماري',
95
+ 'category': 'هندسة',
96
+ 'unit': 'شهر',
97
+ 'price': 14000,
98
+ 'supplier': 'داخلي',
99
+ 'local_content': 85,
100
+ 'last_updated': '2024-01-10'
101
+ },
102
+ {
103
+ 'id': 3,
104
+ 'name': 'مساح',
105
+ 'category': 'هندسة',
106
+ 'unit': 'شهر',
107
+ 'price': 8000,
108
+ 'supplier': 'داخلي',
109
+ 'local_content': 100,
110
+ 'last_updated': '2024-01-10'
111
+ },
112
+ {
113
+ 'id': 4,
114
+ 'name': 'فني كهرباء',
115
+ 'category': 'فني',
116
+ 'unit': 'شهر',
117
+ 'price': 7000,
118
+ 'supplier': 'داخلي',
119
+ 'local_content': 95,
120
+ 'last_updated': '2024-01-10'
121
+ },
122
+ {
123
+ 'id': 5,
124
+ 'name': 'عامل بناء',
125
+ 'category': 'عمالة',
126
+ 'unit': 'يوم',
127
+ 'price': 200,
128
+ 'supplier': 'شركة توريد عمالة',
129
+ 'local_content': 60,
130
+ 'last_updated': '2024-01-20'
131
+ }
132
+ ]
133
+
134
+ if 'equipment' not in st.session_state:
135
+ st.session_state.equipment = [
136
+ {
137
+ 'id': 1,
138
+ 'name': 'حفارة كبيرة',
139
+ 'category': 'معدات ثقيلة',
140
+ 'unit': 'يوم',
141
+ 'price': 2500,
142
+ 'supplier': 'شركة المعدات الثقيلة',
143
+ 'local_content': 70,
144
+ 'last_updated': '2024-01-15'
145
+ },
146
+ {
147
+ 'id': 2,
148
+ 'name': 'خلاطة خرسانة',
149
+ 'category': 'معدات إنشائية',
150
+ 'unit': 'يوم',
151
+ 'price': 1800,
152
+ 'supplier': 'مؤسسة معدات البناء',
153
+ 'local_content': 65,
154
+ 'last_updated': '2024-01-20'
155
+ },
156
+ {
157
+ 'id': 3,
158
+ 'name': 'رافعة برجية',
159
+ 'category': 'معدات ثقيلة',
160
+ 'unit': 'شهر',
161
+ 'price': 45000,
162
+ 'supplier': 'شركة المعدات الثقيلة',
163
+ 'local_content': 50,
164
+ 'last_updated': '2024-02-05'
165
+ },
166
+ {
167
+ 'id': 4,
168
+ 'name': 'مولد كهربائي',
169
+ 'category': 'معدات مساندة',
170
+ 'unit': 'شهر',
171
+ 'price': 12000,
172
+ 'supplier': 'شركة المعدات الكهربائية',
173
+ 'local_content': 75,
174
+ 'last_updated': '2024-01-25'
175
+ },
176
+ {
177
+ 'id': 5,
178
+ 'name': 'سقالات معدنية',
179
+ 'category': 'معدات مساندة',
180
+ 'unit': 'م2/شهر',
181
+ 'price': 50,
182
+ 'supplier': 'مؤسسة معدات البناء',
183
+ 'local_content': 90,
184
+ 'last_updated': '2024-01-15'
185
+ }
186
+ ]
187
+
188
+ if 'subcontractors' not in st.session_state:
189
+ st.session_state.subcontractors = [
190
+ {
191
+ 'id': 1,
192
+ 'name': 'مؤسسة الإنشاءات المتكاملة',
193
+ 'category': 'أعمال إنشائية',
194
+ 'specialization': 'تنفيذ الهيكل الخرساني',
195
+ 'rating': 4.8,
196
+ 'city': 'الرياض',
197
+ 'contact_person': 'محمد العتيبي',
198
+ 'phone': '0555555555',
199
+ 'email': '[email protected]',
200
+ 'local_content': 85,
201
+ 'last_updated': '2024-01-15'
202
+ },
203
+ {
204
+ 'id': 2,
205
+ 'name': 'شركة التكييف والتبريد',
206
+ 'category': 'أعمال كهروميكانيكية',
207
+ 'specialization': 'تركيب أنظمة التكييف والتبريد',
208
+ 'rating': 4.5,
209
+ 'city': 'جدة',
210
+ 'contact_person': 'أحمد الغامدي',
211
+ 'phone': '0566666666',
212
+ 'email': '[email protected]',
213
+ 'local_content': 75,
214
+ 'last_updated': '2024-01-20'
215
+ },
216
+ {
217
+ 'id': 3,
218
+ 'name': 'مؤسسة الكهرباء الحديثة',
219
+ 'category': 'أعمال كهروميكانيكية',
220
+ 'specialization': 'تنفيذ الأعمال الكهربائية',
221
+ 'rating': 4.6,
222
+ 'city': 'الرياض',
223
+ 'contact_person': 'فهد السويلم',
224
+ 'phone': '0577777777',
225
+ 'email': '[email protected]',
226
+ 'local_content': 90,
227
+ 'last_updated': '2024-02-05'
228
+ },
229
+ {
230
+ 'id': 4,
231
+ 'name': 'شركة المقاولات المتخصصة',
232
+ 'category': 'أعمال تشطيبات',
233
+ 'specialization': 'تنفيذ أعمال التشطيبات الداخلية',
234
+ 'rating': 4.7,
235
+ 'city': 'الدمام',
236
+ 'contact_person': 'خالد الدوسري',
237
+ 'phone': '0588888888',
238
+ 'email': '[email protected]',
239
+ 'local_content': 80,
240
+ 'last_updated': '2024-01-25'
241
+ },
242
+ {
243
+ 'id': 5,
244
+ 'name': 'مؤسسة الصيانة والتشغيل',
245
+ 'category': 'أعمال صيانة',
246
+ 'specialization': 'صيانة وتش��يل المباني',
247
+ 'rating': 4.4,
248
+ 'city': 'الرياض',
249
+ 'contact_person': 'عبدالله العنزي',
250
+ 'phone': '0599999999',
251
+ 'email': '[email protected]',
252
+ 'local_content': 95,
253
+ 'last_updated': '2024-02-10'
254
+ }
255
+ ]
256
+
257
+ if 'price_history' not in st.session_state:
258
+ st.session_state.price_history = [
259
+ # تاريخ أسعار الخرسانة الجاهزة
260
+ *[{'material_id': 1, 'date': (datetime.now() - timedelta(days=i*30)).strftime('%Y-%m-%d'), 'price': 250 - (i * 5) if i < 3 else 250 - 15 + (i - 2) * 10} for i in range(12)],
261
+
262
+ # تاريخ أسعار حديد التسليح
263
+ *[{'material_id': 2, 'date': (datetime.now() - timedelta(days=i*30)).strftime('%Y-%m-%d'), 'price': 4500 - (i * 100) if i < 4 else 4500 - 400 + (i - 3) * 150} for i in range(12)],
264
+
265
+ # تاريخ أسعار البلوك الخرساني
266
+ *[{'material_id': 3, 'date': (datetime.now() - timedelta(days=i*30)).strftime('%Y-%m-%d'), 'price': 350 - (i * 10) if i < 6 else 350 - 60 + (i - 5) * 15} for i in range(12)]
267
+ ]
268
+
269
+ def render(self):
270
+ """عرض واجهة وحدة الموارد"""
271
+
272
+ st.markdown("<h1 class='module-title'>وحدة إدارة الموارد</h1>", unsafe_allow_html=True)
273
+
274
+ tabs = st.tabs([
275
+ "لوحة المعلومات",
276
+ "المواد",
277
+ "العمالة",
278
+ "المعدات",
279
+ "المقاولين من الباطن",
280
+ "تحليل الأسعار"
281
+ ])
282
+
283
+ with tabs[0]:
284
+ self._render_dashboard_tab()
285
+
286
+ with tabs[1]:
287
+ self._render_materials_tab()
288
+
289
+ with tabs[2]:
290
+ self._render_labor_tab()
291
+
292
+ with tabs[3]:
293
+ self._render_equipment_tab()
294
+
295
+ with tabs[4]:
296
+ self._render_subcontractors_tab()
297
+
298
+ with tabs[5]:
299
+ self._render_price_analysis_tab()
300
+
301
+ def _render_dashboard_tab(self):
302
+ """عرض تبويب لوحة المعلومات"""
303
+
304
+ st.markdown("### لوحة معلومات إدارة الموارد")
305
+
306
+ # عرض مؤشرات الأداء الرئيسية
307
+ col1, col2, col3, col4 = st.columns(4)
308
+
309
+ with col1:
310
+ total_materials = len(st.session_state.materials)
311
+ st.metric("عدد المواد", total_materials)
312
+
313
+ with col2:
314
+ total_labor = len(st.session_state.labor)
315
+ st.metric("عدد موارد العمالة", total_labor)
316
+
317
+ with col3:
318
+ total_equipment = len(st.session_state.equipment)
319
+ st.metric("عدد المعدات", total_equipment)
320
+
321
+ with col4:
322
+ total_subcontractors = len(st.session_state.subcontractors)
323
+ st.metric("عدد المقاولين من الباطن", total_subcontractors)
324
+
325
+ # رسم بياني لتوزيع المحتوى المحلي
326
+ st.markdown("### المحتوى المحلي للموارد")
327
+
328
+ # إعداد البيانات
329
+ local_content_data = []
330
+
331
+ # إضافة بيانات المواد
332
+ for material in st.session_state.materials:
333
+ local_content_data.append({
334
+ 'النوع': 'المواد',
335
+ 'اسم المورد': material['name'],
336
+ 'نسبة المحتوى المحلي': material['local_content']
337
+ })
338
+
339
+ # إضافة بيانات العمالة
340
+ for labor in st.session_state.labor:
341
+ local_content_data.append({
342
+ 'النوع': 'العمالة',
343
+ 'اسم المورد': labor['name'],
344
+ 'نسبة المحتوى المحلي': labor['local_content']
345
+ })
346
+
347
+ # إضافة بيانات المعدات
348
+ for equipment in st.session_state.equipment:
349
+ local_content_data.append({
350
+ 'النوع': 'المعدات',
351
+ 'اسم المورد': equipment['name'],
352
+ 'نسبة المحتوى المحلي': equipment['local_content']
353
+ })
354
+
355
+ # إضافة بيانات المقاولين من الباطن
356
+ for subcontractor in st.session_state.subcontractors:
357
+ local_content_data.append({
358
+ 'النوع': 'المقاولين من الباطن',
359
+ 'اسم المورد': subcontractor['name'],
360
+ 'نسبة المحتوى المحلي': subcontractor['local_content']
361
+ })
362
+
363
+ # تحويل البيانات إلى DataFrame
364
+ local_content_df = pd.DataFrame(local_content_data)
365
+
366
+ # حساب متوسط المحتوى المحلي لكل نوع
367
+ avg_local_content = local_content_df.groupby('النوع')['نسبة المحتوى المحلي'].mean().reset_index()
368
+
369
+ # رسم المخطط الشريطي
370
+ fig = px.bar(
371
+ avg_local_content,
372
+ x='النوع',
373
+ y='نسبة المحتوى المحلي',
374
+ title='متوسط نسبة المحتوى المحلي حسب نوع المورد',
375
+ color='النوع',
376
+ text_auto='.1f'
377
+ )
378
+
379
+ fig.update_traces(texttemplate='%{text}%', textposition='outside')
380
+
381
+ fig.add_shape(
382
+ type="line",
383
+ x0=-0.5,
384
+ x1=len(avg_local_content) - 0.5,
385
+ y0=70, # النسبة المستهدفة
386
+ y1=70,
387
+ line=dict(color="red", width=2, dash="dash"),
388
+ name="النسبة المستهدفة"
389
+ )
390
+
391
+ fig.add_annotation(
392
+ x=1,
393
+ y=75,
394
+ text=f"النسبة المستهدفة (70%)",
395
+ showarrow=False,
396
+ font=dict(color="red")
397
+ )
398
+
399
+ st.plotly_chart(fig, use_container_width=True)
400
+
401
+ # عرض تنبيهات الموارد
402
+ st.markdown("### تنبيهات الموارد")
403
+
404
+ # محاكاة تنبيهات الموارد
405
+ alerts = [
406
+ {
407
+ "type": "تغير في الأسعار",
408
+ "resource": "حديد تسليح",
409
+ "message": "ارتفاع في سعر الحديد بنسبة 5% في الأسبوع الماضي",
410
+ "date": "2024-03-15",
411
+ "severity": "متوسطة"
412
+ },
413
+ {
414
+ "type": "نقص في المخزون",
415
+ "resource": "بلاط سيراميك",
416
+ "message": "انخفاض مخزون السيراميك إلى أقل من 20% من المستوى المطلوب",
417
+ "date": "2024-03-18",
418
+ "severity": "عالية"
419
+ },
420
+ {
421
+ "type": "انتهاء صلاحية عقود",
422
+ "resource": "مؤسسة الإنشاءات المتكاملة",
423
+ "message": "سينتهي العقد مع المقاول خلال 30 يوماً",
424
+ "date": "2024-03-10",
425
+ "severity": "منخفضة"
426
+ },
427
+ {
428
+ "type": "تغير في المحتوى المحلي",
429
+ "resource": "شركة التكييف والتبريد",
430
+ "message": "انخفاض نسبة المحتوى المحلي إلى أقل من النسبة المستهدفة",
431
+ "date": "2024-03-12",
432
+ "severity": "متوسطة"
433
+ }
434
+ ]
435
+
436
+ # عرض التنبيهات
437
+ for alert in alerts:
438
+ if alert["severity"] == "عالية":
439
+ st.error(f"**{alert['type']}**: {alert['message']} - *{alert['resource']}* ({alert['date']})")
440
+ elif alert["severity"] == "متوسطة":
441
+ st.warning(f"**{alert['type']}**: {alert['message']} - *{alert['resource']}* ({alert['date']})")
442
+ else:
443
+ st.info(f"**{alert['type']}**: {alert['message']} - *{alert['resource']}* ({alert['date']})")
444
+
445
+ # عرض نظرة عامة على الأسعار
446
+ st.markdown("### نظرة عامة على تطور الأسعار")
447
+
448
+ # إعداد البيانات
449
+ price_history_data = []
450
+ material_names = {material['id']: material['name'] for material in st.session_state.materials}
451
+
452
+ for entry in st.session_state.price_history:
453
+ material_id = entry['material_id']
454
+ if material_id in material_names:
455
+ price_history_data.append({
456
+ 'المادة': material_names[material_id],
457
+ 'التاريخ': pd.to_datetime(entry['date']),
458
+ 'السعر': entry['price']
459
+ })
460
+
461
+ # تحويل البيانات إلى DataFrame
462
+ price_history_df = pd.DataFrame(price_history_data)
463
+
464
+ # رسم المخطط الخطي
465
+ fig = px.line(
466
+ price_history_df,
467
+ x='التاريخ',
468
+ y='السعر',
469
+ color='المادة',
470
+ title='تطور أسعار المواد الرئيسية خلال العام الماضي',
471
+ labels={'price': 'السعر (ريال)', 'date': 'التاريخ'}
472
+ )
473
+
474
+ st.plotly_chart(fig, use_container_width=True)
475
+
476
+ def _render_materials_tab(self):
477
+ """عرض تبويب المواد"""
478
+
479
+ st.markdown("### إدارة المواد")
480
+
481
+ # عرض أدوات البحث والتصفية
482
+ col1, col2 = st.columns(2)
483
+
484
+ with col1:
485
+ search_query = st.text_input("بحث في المواد", placeholder="ابحث باسم المادة أو الفئة أو المورد...")
486
+
487
+ with col2:
488
+ category_filter = st.multiselect(
489
+ "تصفية حسب الفئة",
490
+ options=list(set(material['category'] for material in st.session_state.materials)),
491
+ default=[]
492
+ )
493
+
494
+ # تطبيق البحث والتصفية
495
+ filtered_materials = st.session_state.materials
496
+
497
+ if search_query:
498
+ filtered_materials = [
499
+ material for material in filtered_materials
500
+ if (search_query.lower() in material['name'].lower() or
501
+ search_query.lower() in material['category'].lower() or
502
+ search_query.lower() in material['supplier'].lower())
503
+ ]
504
+
505
+ if category_filter:
506
+ filtered_materials = [material for material in filtered_materials if material['category'] in category_filter]
507
+
508
+ # زر إضافة مادة جديدة
509
+ if st.button("إضافة مادة جديدة"):
510
+ st.session_state.show_material_form = True
511
+
512
+ # نموذج إضافة مادة جديدة
513
+ if st.session_state.get('show_material_form', False):
514
+ with st.form("add_material_form"):
515
+ st.markdown("#### إضافة مادة جديدة")
516
+
517
+ col1, col2 = st.columns(2)
518
+
519
+ with col1:
520
+ new_material_name = st.text_input("اسم المادة", key="new_material_name")
521
+ new_material_category = st.text_input("الفئة", key="new_material_category")
522
+ new_material_unit = st.text_input("وحدة القياس", key="new_material_unit")
523
+
524
+ with col2:
525
+ new_material_price = st.number_input("السعر (ريال)", min_value=0.0, key="new_material_price")
526
+ new_material_supplier = st.text_input("المورد", key="new_material_supplier")
527
+ new_material_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_material_local_content")
528
+
529
+ submitted = st.form_submit_button("إضافة المادة")
530
+ cancel = st.form_submit_button("إلغاء")
531
+
532
+ if submitted and new_material_name and new_material_category and new_material_unit:
533
+ # إضافة المادة الجديدة
534
+ new_material = {
535
+ 'id': max([material['id'] for material in st.session_state.materials], default=0) + 1,
536
+ 'name': new_material_name,
537
+ 'category': new_material_category,
538
+ 'unit': new_material_unit,
539
+ 'price': new_material_price,
540
+ 'supplier': new_material_supplier,
541
+ 'local_content': new_material_local_content,
542
+ 'last_updated': datetime.now().strftime('%Y-%m-%d')
543
+ }
544
+
545
+ st.session_state.materials.append(new_material)
546
+ st.success(f"تمت إضافة المادة '{new_material_name}' بنجاح!")
547
+ st.session_state.show_material_form = False
548
+ st.rerun()
549
+
550
+ if cancel:
551
+ st.session_state.show_material_form = False
552
+ st.experimental_rerun()
553
+
554
+ # عرض قائمة المواد
555
+ if filtered_materials:
556
+ # تحويل البيانات إلى DataFrame
557
+ materials_df = pd.DataFrame(filtered_materials)
558
+
559
+ # تنسيق البيانات للعرض
560
+ display_df = materials_df.copy()
561
+ display_df['price'] = display_df['price'].apply(lambda x: f"{x:,.2f} ريال")
562
+ display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%")
563
+
564
+ # تغيير أسماء الأعمدة للعرض
565
+ display_df.columns = [
566
+ 'معرف', 'اسم المادة', 'الفئة', 'وحدة القياس', 'السعر', 'المورد', 'نسبة المحتوى المحلي', 'آخر تحديث'
567
+ ]
568
+
569
+ # عرض الجدول
570
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
571
+
572
+ # عرض ملخص إحصائي
573
+ st.markdown("#### ملخص إحصائي للمواد")
574
+
575
+ col1, col2, col3 = st.columns(3)
576
+
577
+ with col1:
578
+ st.metric("إجمالي عدد المواد", len(filtered_materials))
579
+
580
+ with col2:
581
+ avg_price = sum(material['price'] for material in filtered_materials) / len(filtered_materials)
582
+ st.metric("متوسط سعر المواد", f"{avg_price:,.2f} ريال")
583
+
584
+ with col3:
585
+ avg_local_content = sum(material['local_content'] for material in filtered_materials) / len(filtered_materials)
586
+ st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%")
587
+
588
+ # عرض مخطط توزيع المواد حسب الفئة
589
+ category_counts = materials_df.groupby('category').size().reset_index(name='count')
590
+
591
+ fig = px.pie(
592
+ category_counts,
593
+ names='category',
594
+ values='count',
595
+ title='توزيع المواد حسب الفئة'
596
+ )
597
+
598
+ st.plotly_chart(fig, use_container_width=True)
599
+ else:
600
+ st.warning("لا توجد مواد مطابقة لمعايير البحث.")
601
+
602
+ def _render_labor_tab(self):
603
+ """عرض تبويب العمالة"""
604
+
605
+ st.markdown("### إدارة العمالة")
606
+
607
+ # عرض أدوات البحث والتصفية
608
+ col1, col2 = st.columns(2)
609
+
610
+ with col1:
611
+ search_query = st.text_input("بحث في العمالة", placeholder="ابحث باسم العامل أو الفئة أو المورد...")
612
+
613
+ with col2:
614
+ category_filter = st.multiselect(
615
+ "تصفية حسب الفئة",
616
+ options=list(set(labor['category'] for labor in st.session_state.labor)),
617
+ default=[]
618
+ )
619
+
620
+ # تطبيق البحث والتصفية
621
+ filtered_labor = st.session_state.labor
622
+
623
+ if search_query:
624
+ filtered_labor = [
625
+ labor for labor in filtered_labor
626
+ if (search_query.lower() in labor['name'].lower() or
627
+ search_query.lower() in labor['category'].lower() or
628
+ search_query.lower() in labor['supplier'].lower())
629
+ ]
630
+
631
+ if category_filter:
632
+ filtered_labor = [labor for labor in filtered_labor if labor['category'] in category_filter]
633
+
634
+ # زر إضافة عامل جديد
635
+ if st.button("إضافة عامل جديد"):
636
+ st.session_state.show_labor_form = True
637
+
638
+ # نموذج إضافة عامل جديد
639
+ if st.session_state.get('show_labor_form', False):
640
+ with st.form("add_labor_form"):
641
+ st.markdown("#### إضافة عامل جديد")
642
+
643
+ col1, col2 = st.columns(2)
644
+
645
+ with col1:
646
+ new_labor_name = st.text_input("اسم العامل", key="new_labor_name")
647
+ new_labor_category = st.text_input("الفئة", key="new_labor_category")
648
+ new_labor_name = st.text_input("اسم العامل", key="new_labor_name")
649
+ new_labor_category = st.text_input("الفئة", key="new_labor_category")
650
+ new_labor_unit = st.text_input("وحدة القياس", key="new_labor_unit")
651
+
652
+ with col2:
653
+ new_labor_price = st.number_input("السعر (ريال)", min_value=0.0, key="new_labor_price")
654
+ new_labor_supplier = st.text_input("المورد", key="new_labor_supplier")
655
+ new_labor_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_labor_local_content")
656
+
657
+ submitted = st.form_submit_button("إضافة العامل")
658
+ cancel = st.form_submit_button("إلغاء")
659
+
660
+ if submitted and new_labor_name and new_labor_category and new_labor_unit:
661
+ # إضافة العامل الجديد
662
+ new_labor = {
663
+ 'id': max([labor['id'] for labor in st.session_state.labor], default=0) + 1,
664
+ 'name': new_labor_name,
665
+ 'category': new_labor_category,
666
+ 'unit': new_labor_unit,
667
+ 'price': new_labor_price,
668
+ 'supplier': new_labor_supplier,
669
+ 'local_content': new_labor_local_content,
670
+ 'last_updated': datetime.now().strftime('%Y-%m-%d')
671
+ }
672
+
673
+ st.session_state.labor.append(new_labor)
674
+ st.success(f"تمت إضافة العامل '{new_labor_name}' بنجاح!")
675
+ st.session_state.show_labor_form = False
676
+ st.experimental_rerun()
677
+
678
+ if cancel:
679
+ st.session_state.show_labor_form = False
680
+ st.experimental_rerun()
681
+
682
+ # عرض قائمة العمالة
683
+ if filtered_labor:
684
+ # تحويل البيانات إلى DataFrame
685
+ labor_df = pd.DataFrame(filtered_labor)
686
+
687
+ # تنسيق البيانات للعرض
688
+ display_df = labor_df.copy()
689
+ display_df['price'] = display_df['price'].apply(lambda x: f"{x:,.2f} ريال")
690
+ display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%")
691
+
692
+ # تغيير أسماء الأعمدة للعرض
693
+ display_df.columns = [
694
+ 'معرف', 'اسم العامل', 'الفئة', 'وحدة القياس', 'السعر', 'المورد', 'نسبة المحتوى المحلي', 'آخر تحديث'
695
+ ]
696
+
697
+ # عرض الجدول
698
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
699
+
700
+ # عرض ملخص إحصائي
701
+ st.markdown("#### ملخص إحصائي للعمالة")
702
+
703
+ col1, col2, col3 = st.columns(3)
704
+
705
+ with col1:
706
+ st.metric("إجمالي عدد العمالة", len(filtered_labor))
707
+
708
+ with col2:
709
+ avg_price = sum(labor['price'] for labor in filtered_labor) / len(filtered_labor)
710
+ st.metric("متوسط سعر العمالة", f"{avg_price:,.2f} ريال")
711
+
712
+ with col3:
713
+ avg_local_content = sum(labor['local_content'] for labor in filtered_labor) / len(filtered_labor)
714
+ st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%")
715
+
716
+ # عرض مخطط توزيع العمالة حسب الفئة
717
+ category_counts = labor_df.groupby('category').size().reset_index(name='count')
718
+
719
+ fig = px.pie(
720
+ category_counts,
721
+ names='category',
722
+ values='count',
723
+ title='توزيع العمالة حسب الفئة'
724
+ )
725
+
726
+ st.plotly_chart(fig, use_container_width=True)
727
+ else:
728
+ st.warning("لا توجد عمالة مطابقة لمعايير البحث.")
729
+
730
+ def _render_equipment_tab(self):
731
+ """عرض تبويب المعدات"""
732
+
733
+ st.markdown("### إدارة المعدات")
734
+
735
+ # عرض أدوات البحث والتصفية
736
+ col1, col2 = st.columns(2)
737
+
738
+ with col1:
739
+ search_query = st.text_input("بحث في المعدات", placeholder="ابحث باسم المعدة أو الفئة أو المورد...")
740
+
741
+ with col2:
742
+ category_filter = st.multiselect(
743
+ "تصفية حسب الفئة",
744
+ options=list(set(equipment['category'] for equipment in st.session_state.equipment)),
745
+ default=[]
746
+ )
747
+
748
+ # تطبيق البحث والتصفية
749
+ filtered_equipment = st.session_state.equipment
750
+
751
+ if search_query:
752
+ filtered_equipment = [
753
+ equipment for equipment in filtered_equipment
754
+ if (search_query.lower() in equipment['name'].lower() or
755
+ search_query.lower() in equipment['category'].lower() or
756
+ search_query.lower() in equipment['supplier'].lower())
757
+ ]
758
+
759
+ if category_filter:
760
+ filtered_equipment = [equipment for equipment in filtered_equipment if equipment['category'] in category_filter]
761
+
762
+ # زر إضافة معدة جديدة
763
+ if st.button("إضافة معدة جديدة"):
764
+ st.session_state.show_equipment_form = True
765
+
766
+ # نموذج إضافة معدة جديدة
767
+ if st.session_state.get('show_equipment_form', False):
768
+ with st.form("add_equipment_form"):
769
+ st.markdown("#### إضافة معدة جديدة")
770
+
771
+ col1, col2 = st.columns(2)
772
+
773
+ with col1:
774
+ new_equipment_name = st.text_input("اسم المعدة", key="new_equipment_name")
775
+ new_equipment_category = st.text_input("الفئة", key="new_equipment_category")
776
+ new_equipment_unit = st.text_input("وحدة القياس", key="new_equipment_unit")
777
+
778
+ with col2:
779
+ new_equipment_price = st.number_input("السعر (ريال)", min_value=0.0, key="new_equipment_price")
780
+ new_equipment_supplier = st.text_input("المورد", key="new_equipment_supplier")
781
+ new_equipment_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_equipment_local_content")
782
+
783
+ submitted = st.form_submit_button("إضافة المعدة")
784
+ cancel = st.form_submit_button("إلغاء")
785
+
786
+ if submitted and new_equipment_name and new_equipment_category and new_equipment_unit:
787
+ # إضافة المعدة الجديدة
788
+ new_equipment = {
789
+ 'id': max([equipment['id'] for equipment in st.session_state.equipment], default=0) + 1,
790
+ 'name': new_equipment_name,
791
+ 'category': new_equipment_category,
792
+ 'unit': new_equipment_unit,
793
+ 'price': new_equipment_price,
794
+ 'supplier': new_equipment_supplier,
795
+ 'local_content': new_equipment_local_content,
796
+ 'last_updated': datetime.now().strftime('%Y-%m-%d')
797
+ }
798
+
799
+ st.session_state.equipment.append(new_equipment)
800
+ st.success(f"تمت إضافة المعدة '{new_equipment_name}' بنجاح!")
801
+ st.session_state.show_equipment_form = False
802
+ st.experimental_rerun()
803
+
804
+ if cancel:
805
+ st.session_state.show_equipment_form = False
806
+ st.experimental_rerun()
807
+
808
+ # عرض قائمة المعدات
809
+ if filtered_equipment:
810
+ # تحويل البيانات إلى DataFrame
811
+ equipment_df = pd.DataFrame(filtered_equipment)
812
+
813
+ # تنسيق البيانات للعرض
814
+ display_df = equipment_df.copy()
815
+ display_df['price'] = display_df['price'].apply(lambda x: f"{x:,.2f} ريال")
816
+ display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%")
817
+
818
+ # تغيير أسماء الأعمدة للعرض
819
+ display_df.columns = [
820
+ 'معرف', 'اسم المعدة', 'الفئة', 'وحدة القياس', 'السعر', 'المورد', 'نسبة المحتوى المحلي', 'آخر تحديث'
821
+ ]
822
+
823
+ # عرض الجدول
824
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
825
+
826
+ # عرض ملخص إحصائي
827
+ st.markdown("#### ملخص إحصائي للمعدات")
828
+
829
+ col1, col2, col3 = st.columns(3)
830
+
831
+ with col1:
832
+ st.metric("إجمالي عدد المعدات", len(filtered_equipment))
833
+
834
+ with col2:
835
+ avg_price = sum(equipment['price'] for equipment in filtered_equipment) / len(filtered_equipment)
836
+ st.metric("متوسط سعر المعدات", f"{avg_price:,.2f} ريال")
837
+
838
+ with col3:
839
+ avg_local_content = sum(equipment['local_content'] for equipment in filtered_equipment) / len(filtered_equipment)
840
+ st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%")
841
+
842
+ # عرض مخطط توزيع المعدات حسب الفئة
843
+ category_counts = equipment_df.groupby('category').size().reset_index(name='count')
844
+
845
+ fig = px.bar(
846
+ category_counts,
847
+ x='category',
848
+ y='count',
849
+ title='توزيع المعدات حسب الفئة',
850
+ color='category',
851
+ labels={'category': 'الفئة', 'count': 'العدد'}
852
+ )
853
+
854
+ st.plotly_chart(fig, use_container_width=True)
855
+ else:
856
+ st.warning("لا توجد معدات مطابقة لمعايير البحث.")
857
+
858
+ def _render_subcontractors_tab(self):
859
+ """عرض تبويب المقاولين من الباطن"""
860
+
861
+ st.markdown("### إدارة المقاولين من الباطن")
862
+
863
+ # عرض أدوات البحث والتصفية
864
+ col1, col2, col3 = st.columns(3)
865
+
866
+ with col1:
867
+ search_query = st.text_input("بحث في المقاولين", placeholder="ابحث باسم المقاول أو التخصص...")
868
+
869
+ with col2:
870
+ category_filter = st.multiselect(
871
+ "تصفية حسب الفئة",
872
+ options=list(set(subcontractor['category'] for subcontractor in st.session_state.subcontractors)),
873
+ default=[]
874
+ )
875
+
876
+ with col3:
877
+ city_filter = st.multiselect(
878
+ "تصفية حسب المدينة",
879
+ options=list(set(subcontractor['city'] for subcontractor in st.session_state.subcontractors)),
880
+ default=[]
881
+ )
882
+
883
+ # تطبيق البحث والتصفية
884
+ filtered_subcontractors = st.session_state.subcontractors
885
+
886
+ if search_query:
887
+ filtered_subcontractors = [
888
+ subcontractor for subcontractor in filtered_subcontractors
889
+ if (search_query.lower() in subcontractor['name'].lower() or
890
+ search_query.lower() in subcontractor['specialization'].lower())
891
+ ]
892
+
893
+ if category_filter:
894
+ filtered_subcontractors = [subcontractor for subcontractor in filtered_subcontractors if subcontractor['category'] in category_filter]
895
+
896
+ if city_filter:
897
+ filtered_subcontractors = [subcontractor for subcontractor in filtered_subcontractors if subcontractor['city'] in city_filter]
898
+
899
+ # زر إضافة مقاول جديد
900
+ if st.button("إضافة مقاول جديد"):
901
+ st.session_state.show_subcontractor_form = True
902
+
903
+ # نموذج إضافة مقاول جديد
904
+ if st.session_state.get('show_subcontractor_form', False):
905
+ with st.form("add_subcontractor_form"):
906
+ st.markdown("#### إضافة مقاول جديد")
907
+
908
+ col1, col2 = st.columns(2)
909
+
910
+ with col1:
911
+ new_subcontractor_name = st.text_input("اسم المقاول", key="new_subcontractor_name")
912
+ new_subcontractor_category = st.text_input("الفئة", key="new_subcontractor_category")
913
+ new_subcontractor_specialization = st.text_input("التخصص", key="new_subcontractor_specialization")
914
+ new_subcontractor_city = st.text_input("المدينة", key="new_subcontractor_city")
915
+
916
+ with col2:
917
+ new_subcontractor_contact = st.text_input("جهة الاتصال", key="new_subcontractor_contact")
918
+ new_subcontractor_phone = st.text_input("رقم الهاتف", key="new_subcontractor_phone")
919
+ new_subcontractor_email = st.text_input("البريد الإلكتروني", key="new_subcontractor_email")
920
+ new_subcontractor_rating = st.slider("التقييم", 1.0, 5.0, 3.0, 0.1, key="new_subcontractor_rating")
921
+ new_subcontractor_local_content = st.slider("نسبة المحتوى المحلي (%)", 0, 100, 50, key="new_subcontractor_local_content")
922
+
923
+ submitted = st.form_submit_button("إضافة المقاول")
924
+ cancel = st.form_submit_button("إلغاء")
925
+
926
+ if submitted and new_subcontractor_name and new_subcontractor_category and new_subcontractor_specialization:
927
+ # إضافة المقاول الجديد
928
+ new_subcontractor = {
929
+ 'id': max([subcontractor['id'] for subcontractor in st.session_state.subcontractors], default=0) + 1,
930
+ 'name': new_subcontractor_name,
931
+ 'category': new_subcontractor_category,
932
+ 'specialization': new_subcontractor_specialization,
933
+ 'rating': new_subcontractor_rating,
934
+ 'city': new_subcontractor_city,
935
+ 'contact_person': new_subcontractor_contact,
936
+ 'phone': new_subcontractor_phone,
937
+ 'email': new_subcontractor_email,
938
+ 'local_content': new_subcontractor_local_content,
939
+ 'last_updated': datetime.now().strftime('%Y-%m-%d')
940
+ }
941
+
942
+ st.session_state.subcontractors.append(new_subcontractor)
943
+ st.success(f"تمت إضافة المقاول '{new_subcontractor_name}' بنجاح!")
944
+ st.session_state.show_subcontractor_form = False
945
+ st.experimental_rerun()
946
+
947
+ if cancel:
948
+ st.session_state.show_subcontractor_form = False
949
+ st.experimental_rerun()
950
+
951
+ # عرض قائمة المقاولين
952
+ if filtered_subcontractors:
953
+ # تحويل البيانات إلى DataFrame
954
+ subcontractors_df = pd.DataFrame(filtered_subcontractors)
955
+
956
+ # تنسيق البيانات للعرض
957
+ display_df = subcontractors_df.copy()
958
+ display_df['local_content'] = display_df['local_content'].apply(lambda x: f"{x}%")
959
+
960
+ # تغيير أسماء الأعمدة للعرض
961
+ display_df.columns = [
962
+ 'معرف', 'اسم المقاول', 'الفئة', 'التخصص', 'التقييم', 'المدينة',
963
+ 'جهة الاتصال', 'رقم الهاتف', 'البريد الإلكتروني', 'نسبة المحتوى المحلي', 'آخر تحديث'
964
+ ]
965
+
966
+ # عرض الجدول
967
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
968
+
969
+ # عرض ملخص إحصائي
970
+ st.markdown("#### ملخص إحصائي للمقاولين")
971
+
972
+ col1, col2, col3 = st.columns(3)
973
+
974
+ with col1:
975
+ st.metric("إجمالي عدد المقاولين", len(filtered_subcontractors))
976
+
977
+ with col2:
978
+ avg_rating = sum(subcontractor['rating'] for subcontractor in filtered_subcontractors) / len(filtered_subcontractors)
979
+ st.metric("متوسط التقييم", f"{avg_rating:.1f}/5.0")
980
+
981
+ with col3:
982
+ avg_local_content = sum(subcontractor['local_content'] for subcontractor in filtered_subcontractors) / len(filtered_subcontractors)
983
+ st.metric("متوسط نسبة المحتوى المحلي", f"{avg_local_content:.1f}%")
984
+
985
+ # عرض مخطط توزيع المقاولين حسب الفئة
986
+ category_counts = subcontractors_df.groupby('category').size().reset_index(name='count')
987
+
988
+ fig = px.pie(
989
+ category_counts,
990
+ names='category',
991
+ values='count',
992
+ title='توزيع المقاولين حسب الفئة',
993
+ hole=0.4
994
+ )
995
+
996
+ st.plotly_chart(fig, use_container_width=True)
997
+
998
+ # عرض مخطط توزيع المقاولين حسب المدينة
999
+ city_counts = subcontractors_df.groupby('city').size().reset_index(name='count')
1000
+
1001
+ fig = px.bar(
1002
+ city_counts,
1003
+ x='city',
1004
+ y='count',
1005
+ title='توزيع المقاولين حسب المدينة',
1006
+ color='city',
1007
+ labels={'city': 'المدينة', 'count': 'العدد'}
1008
+ )
1009
+
1010
+ st.plotly_chart(fig, use_container_width=True)
1011
+ else:
1012
+ st.warning("لا يوجد مقاولين مطابقين لمعايير البحث.")
1013
+
1014
+ def _render_price_analysis_tab(self):
1015
+ """عرض تبويب تحليل الأسعار"""
1016
+
1017
+ st.markdown("### تحليل الأسعار")
1018
+
1019
+ # اختيار نوع التحليل
1020
+ analysis_type = st.radio(
1021
+ "نوع التحليل",
1022
+ ["تحليل أسعار المواد", "مقارنة الأسعار", "توقع الأسعار المستقبلية"],
1023
+ horizontal=True
1024
+ )
1025
+
1026
+ if analysis_type == "تحليل أسعار المواد":
1027
+ self._render_material_price_analysis()
1028
+ elif analysis_type == "مقارنة الأسعار":
1029
+ self._render_price_comparison()
1030
+ else:
1031
+ self._render_price_forecast()
1032
+
1033
+ def _render_material_price_analysis(self):
1034
+ """عرض تحليل أسعار المواد"""
1035
+
1036
+ st.markdown("#### تحليل أسعار المواد")
1037
+
1038
+ # اختيار المواد للتحليل
1039
+ material_options = [material['name'] for material in st.session_state.materials]
1040
+ selected_materials = st.multiselect(
1041
+ "اختر المواد للتحليل",
1042
+ options=material_options,
1043
+ default=material_options[:3] if len(material_options) >= 3 else material_options
1044
+ )
1045
+
1046
+ if not selected_materials:
1047
+ st.warning("الرجاء اختيار مادة واحدة على الأقل للتحليل.")
1048
+ return
1049
+
1050
+ # إعداد البيانات للتحليل
1051
+ material_ids = {material['name']: material['id'] for material in st.session_state.materials}
1052
+ selected_ids = [material_ids[name] for name in selected_materials if name in material_ids]
1053
+
1054
+ price_history_data = []
1055
+ for entry in st.session_state.price_history:
1056
+ if entry['material_id'] in selected_ids:
1057
+ material_name = next((material['name'] for material in st.session_state.materials if material['id'] == entry['material_id']), "")
1058
+ price_history_data.append({
1059
+ 'المادة': material_name,
1060
+ 'التاريخ': pd.to_datetime(entry['date']),
1061
+ 'السعر': entry['price']
1062
+ })
1063
+
1064
+ if not price_history_data:
1065
+ st.warning("لا توجد بيانات أسعار متاحة للمواد المختارة.")
1066
+ return
1067
+
1068
+ # تحويل البيانات إلى DataFrame
1069
+ price_history_df = pd.DataFrame(price_history_data)
1070
+
1071
+ # عرض المخطط الخطي للأسعار
1072
+ fig = px.line(
1073
+ price_history_df,
1074
+ x='التاريخ',
1075
+ y='السعر',
1076
+ color='المادة',
1077
+ title='تطور أسعار المواد المختارة',
1078
+ labels={'السعر': 'السعر (ريال)'}
1079
+ )
1080
+
1081
+ st.plotly_chart(fig, use_container_width=True)
1082
+
1083
+ # حساب التغيرات في الأسعار
1084
+ materials_price_changes = []
1085
+
1086
+ for material_name in selected_materials:
1087
+ material_prices = price_history_df[price_history_df['المادة'] == material_name].sort_values('التاريخ')
1088
+
1089
+ if len(material_prices) >= 2:
1090
+ first_price = material_prices.iloc[0]['السعر']
1091
+ last_price = material_prices.iloc[-1]['السعر']
1092
+ price_change = last_price - first_price
1093
+ price_change_percent = (price_change / first_price) * 100
1094
+
1095
+ # حساب التقلب (الانحراف المعياري)
1096
+ price_volatility = material_prices['السعر'].std()
1097
+
1098
+ materials_price_changes.append({
1099
+ 'المادة': material_name,
1100
+ 'السعر الأول': first_price,
1101
+ 'السعر الأخير': last_price,
1102
+ 'التغير المطلق': price_change,
1103
+ 'نسبة التغير (%)': price_change_percent,
1104
+ 'التقلب (الانحراف المعياري)': price_volatility
1105
+ })
1106
+
1107
+ # عرض جدول التغيرات في الأسعار
1108
+ if materials_price_changes:
1109
+ st.markdown("#### تغيرات الأسعار خلال الفترة")
1110
+
1111
+ changes_df = pd.DataFrame(materials_price_changes)
1112
+
1113
+ # تنسيق البيانات للعرض
1114
+ display_df = changes_df.copy()
1115
+ display_df['السعر الأول'] = display_df['السعر الأول'].apply(lambda x: f"{x:,.2f} ريال")
1116
+ display_df['السعر الأخير'] = display_df['السعر الأخير'].apply(lambda x: f"{x:,.2f} ريال")
1117
+ display_df['التغير المطلق'] = display_df['التغير المطلق'].apply(lambda x: f"{x:,.2f} ريال")
1118
+ display_df['نسبة التغير (%)'] = display_df['نسبة التغير (%)'].apply(lambda x: f"{x:.2f}%")
1119
+ display_df['التقلب (الانحراف المعياري)'] = display_df['التقلب (الانحراف المعياري)'].apply(lambda x: f"{x:.2f}")
1120
+
1121
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
1122
+
1123
+ # عرض مخطط شريطي للتغيرات في الأسعار
1124
+ fig = px.bar(
1125
+ changes_df,
1126
+ x='المادة',
1127
+ y='نسبة التغير (%)',
1128
+ title='نسبة التغير في الأسعار',
1129
+ color='المادة',
1130
+ text_auto='.1f'
1131
+ )
1132
+
1133
+ fig.update_traces(texttemplate='%{text}%', textposition='outside')
1134
+
1135
+ st.plotly_chart(fig, use_container_width=True)
1136
+
1137
+ def _render_price_comparison(self):
1138
+ """عرض مقارنة الأسعار"""
1139
+
1140
+ st.markdown("#### مقارنة الأسعار")
1141
+
1142
+ # اختيار نوع المورد للمقارنة
1143
+ resource_type = st.selectbox(
1144
+ "نوع المورد",
1145
+ ["المواد", "العمالة", "المعدات"]
1146
+ )
1147
+
1148
+ if resource_type == "المواد":
1149
+ resources = st.session_state.materials
1150
+ elif resource_type == "العمالة":
1151
+ resources = st.session_state.labor
1152
+ else:
1153
+ resources = st.session_state.equipment
1154
+
1155
+ # اختيار الفئة للمقارنة
1156
+ categories = list(set([resource['category'] for resource in resources]))
1157
+ selected_category = st.selectbox(
1158
+ "الفئة",
1159
+ options=["الكل"] + categories
1160
+ )
1161
+
1162
+ # فلترة الموارد حسب الفئة
1163
+ if selected_category != "الكل":
1164
+ filtered_resources = [resource for resource in resources if resource['category'] == selected_category]
1165
+ else:
1166
+ filtered_resources = resources
1167
+
1168
+ if not filtered_resources:
1169
+ st.warning("لا توجد موارد مطابقة للفئة المختارة.")
1170
+ return
1171
+
1172
+ # إعداد بيانات المقارنة
1173
+ comparison_data = []
1174
+
1175
+ for resource in filtered_resources:
1176
+ comparison_data.append({
1177
+ 'الاسم': resource['name'],
1178
+ 'الفئة': resource['category'],
1179
+ 'الوحدة': resource['unit'],
1180
+ 'السعر': resource['price'],
1181
+ 'المورد': resource['supplier'],
1182
+ 'نسبة المحتوى المحلي': resource['local_content']
1183
+ })
1184
+
1185
+ # تحويل البيانات إلى DataFrame
1186
+ comparison_df = pd.DataFrame(comparison_data)
1187
+
1188
+ # عرض المخطط الشريطي للأسعار
1189
+ fig = px.bar(
1190
+ comparison_df,
1191
+ x='الاسم',
1192
+ y='السعر',
1193
+ title=f'مقارنة أسعار {resource_type}',
1194
+ color='الفئة' if selected_category == "الكل" else 'المورد',
1195
+ text_auto='.2s',
1196
+ labels={'السعر': 'السعر (ريال)'}
1197
+ )
1198
+
1199
+ fig.update_traces(texttemplate='%{text} ريال', textposition='outside')
1200
+
1201
+ st.plotly_chart(fig, use_container_width=True)
1202
+
1203
+ # عرض العلاقة بين السعر ونسبة المحتوى المحلي
1204
+ fig = px.scatter(
1205
+ comparison_df,
1206
+ x='نسبة المحتوى المحلي',
1207
+ y='السعر',
1208
+ color='الفئة' if selected_category == "الكل" else None,
1209
+ title='العلاقة بين السعر ونسبة المحتوى المحلي',
1210
+ labels={'نسبة المحتوى المحلي': 'نسبة المحتوى المحلي (%)', 'السعر': 'السعر (ريال)'},
1211
+ size=[50] * len(comparison_df),
1212
+ text='الاسم'
1213
+ )
1214
+
1215
+ fig.update_traces(textposition='top center')
1216
+
1217
+ st.plotly_chart(fig, use_container_width=True)
1218
+
1219
+ # عرض جدول المقارنة
1220
+ st.markdown("#### جدول مقارنة الأسعار")
1221
+
1222
+ # تنسيق البيانات للعرض
1223
+ display_df = comparison_df.copy()
1224
+ display_df['السعر'] = display_df['السعر'].apply(lambda x: f"{x:,.2f} ريال")
1225
+ display_df['نسبة المحتوى المحلي'] = display_df['نسبة المحتوى المحلي'].apply(lambda x: f"{x}%")
1226
+
1227
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
1228
+
1229
+ def _render_price_forecast(self):
1230
+ """عرض توقع الأسعار المستقبلية"""
1231
+
1232
+ st.markdown("#### توقع الأسعار المستقبلية")
1233
+
1234
+ # اختيار المادة للتوقع
1235
+ material_options = [material['name'] for material in st.session_state.materials]
1236
+ selected_material = st.selectbox(
1237
+ "اختر المادة للتوقع",
1238
+ options=material_options
1239
+ )
1240
+
1241
+ # اختيار فترة التوقع
1242
+ forecast_period = st.slider(
1243
+ "فترة التوقع (أشهر)",
1244
+ min_value=1,
1245
+ max_value=12,
1246
+ value=6
1247
+ )
1248
+
1249
+ if not selected_material:
1250
+ st.warning("الرجاء اختيار مادة للتوقع.")
1251
+ return
1252
+
1253
+ # الحصول على معرف المادة
1254
+ material_id = next((material['id'] for material in st.session_state.materials if material['name'] == selected_material), None)
1255
+
1256
+ if material_id is None:
1257
+ st.error("المادة المحددة غير موجودة.")
1258
+ return
1259
+
1260
+ # الحصول على بيانات الأسعار التاريخية
1261
+ price_history_data = []
1262
+ for entry in st.session_state.price_history:
1263
+ if entry['material_id'] == material_id:
1264
+ price_history_data.append({
1265
+ 'التاريخ': pd.to_datetime(entry['date']),
1266
+ 'السعر': entry['price']
1267
+ })
1268
+
1269
+ if not price_history_data:
1270
+ st.warning("لا توجد بيانات تاريخية كافية للمادة المحددة للقيام بالتوقع.")
1271
+ return
1272
+
1273
+ # تحويل البيانات إلى DataFrame
1274
+ price_history_df = pd.DataFrame(price_history_data).sort_values('التاريخ')
1275
+
1276
+ # إجراء التوقع
1277
+ # في الواقع، ستستخدم نماذج تعلم آلي مثل ARIMA أو Prophet
1278
+ # هنا سنستخدم توقعًا بسيطًا للأغراض التوضيحية
1279
+
1280
+ # حساب متوسط التغير الشهري
1281
+ monthly_changes = []
1282
+ for i in range(1, len(price_history_df)):
1283
+ monthly_changes.append(price_history_df.iloc[i]['السعر'] - price_history_df.iloc[i-1]['السعر'])
1284
+
1285
+ if monthly_changes:
1286
+ avg_monthly_change = sum(monthly_changes) / len(monthly_changes)
1287
+ else:
1288
+ avg_monthly_change = 0
1289
+
1290
+ # إنشاء بيانات التوقع
1291
+ last_date = price_history_df['التاريخ'].max()
1292
+ last_price = price_history_df.loc[price_history_df['التاريخ'] == last_date, 'السعر'].values[0]
1293
+
1294
+ forecast_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=forecast_period, freq='M')
1295
+ forecast_prices = [last_price + (i+1) * avg_monthly_change for i in range(forecast_period)]
1296
+
1297
+ # إضافة بعض التقلبات العشوائية للتوقع
1298
+ forecast_prices = [price + random.uniform(-price*0.05, price*0.05) for price in forecast_prices]
1299
+
1300
+ forecast_df = pd.DataFrame({
1301
+ 'التاريخ': forecast_dates,
1302
+ 'السعر': forecast_prices,
1303
+ 'النوع': ['توقع'] * forecast_period
1304
+ })
1305
+
1306
+ # دمج البيانات التاريخية والتوقع
1307
+ historical_df = price_history_df.copy()
1308
+ historical_df['النوع'] = ['تاريخي'] * len(historical_df)
1309
+
1310
+ combined_df = pd.concat([historical_df, forecast_df], ignore_index=True)
1311
+
1312
+ # عرض المخطط
1313
+ fig = px.line(
1314
+ combined_df,
1315
+ x='التاريخ',
1316
+ y='السعر',
1317
+ color='النوع',
1318
+ title=f'توقع أسعار {selected_material} للـ {forecast_period} أشهر القادمة',
1319
+ labels={'السعر': 'السعر (ريال)'},
1320
+ color_discrete_map={'تاريخي': 'blue', 'توقع': 'red'}
1321
+ )
1322
+
1323
+ # إضافة فترة الثقة حول التوقع
1324
+ confidence = 0.1 # 10% فترة ثقة
1325
+ upper_bound = [price * (1 + confidence) for price in forecast_prices]
1326
+ lower_bound = [price * (1 - confidence) for price in forecast_prices]
1327
+
1328
+ fig.add_scatter(
1329
+ x=forecast_dates,
1330
+ y=upper_bound,
1331
+ fill=None,
1332
+ mode='lines',
1333
+ line_color='rgba(255, 0, 0, 0.3)',
1334
+ line_width=0,
1335
+ showlegend=False
1336
+ )
1337
+
1338
+ fig.add_scatter(
1339
+ x=forecast_dates,
1340
+ y=lower_bound,
1341
+ fill='tonexty',
1342
+ mode='lines',
1343
+ line_color='rgba(255, 0, 0, 0.3)',
1344
+ line_width=0,
1345
+ name='فترة الثقة (±10%)'
1346
+ )
1347
+
1348
+ st.plotly_chart(fig, use_container_width=True)
1349
+
1350
+ # عرض جدول التوقع
1351
+ st.markdown("#### جدول توقع الأسعار")
1352
+
1353
+ forecast_table = forecast_df.copy()
1354
+ forecast_table['التاريخ'] = forecast_table['التاريخ'].dt.strftime('%Y-%m')
1355
+ forecast_table['السعر'] = forecast_table['السعر'].apply(lambda x: f"{x:,.2f} ريال")
1356
+ forecast_table = forecast_table.drop(columns=['النوع'])
1357
+
1358
+ st.dataframe(forecast_table, use_container_width=True, hide_index=True)
1359
+
1360
+ # عرض ملخص التوقع
1361
+ st.markdown("#### ملخص التوقع")
1362
+
1363
+ col1, col2, col3 = st.columns(3)
1364
+
1365
+ with col1:
1366
+ st.metric(
1367
+ "السعر الحالي",
1368
+ f"{last_price:,.2f} ريال"
1369
+ )
1370
+
1371
+ with col2:
1372
+ forecasted_price = forecast_prices[-1]
1373
+ price_change = forecasted_price - last_price
1374
+ price_change_percent = (price_change / last_price) * 100
1375
+
1376
+ st.metric(
1377
+ f"السعر المتوقع بعد {forecast_period} أشهر",
1378
+ f"{forecasted_price:,.2f} ريال",
1379
+ delta=f"{price_change_percent:.1f}%"
1380
+ )
1381
+
1382
+ with col3:
1383
+ avg_forecasted_price = sum(forecast_prices) / len(forecast_prices)
1384
+
1385
+ st.metric(
1386
+ "متوسط السعر المتوقع",
1387
+ f"{avg_forecasted_price:,.2f} ريال"
1388
+ )
1389
+
1390
+ # عرض ملاحظات وتوصيات
1391
+ if price_change_percent > 10:
1392
+ st.warning("""
1393
+ ### توقع ارتفاع كبير في الأسعار
1394
+ - ينصح بشراء المواد مبكراً وتخزينها إذا أمكن
1395
+ - التفاوض على عقود توريد طويلة الأجل بأسعار ثابتة
1396
+ - البحث عن موردين بديلين أو مواد بديلة
1397
+ """)
1398
+ elif price_change_percent < -10:
1399
+ st.success("""
1400
+ ### توقع انخفاض كبير في الأسعار
1401
+ - ينصح بتأجيل شراء المواد إذا أمكن
1402
+ - شراء كميات أقل والاحتفاظ بمخزون منخفض
1403
+ - التفاوض على عقود مرنة مع الموردين
1404
+ """)
1405
+ else:
1406
+ st.info("""
1407
+ ### توقع استقرار نسبي في الأسعار
1408
+ - يمكن الشراء حسب الاحتياج دون الحاجة لتخزين كميات كبيرة
1409
+ - متابعة أسعار السوق بشكل دوري للتأكد من دقة التوقعات
1410
+ """)
modules/risk_analysis/risk_analysis_app.py ADDED
@@ -0,0 +1,751 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 random
13
+ import os
14
+ import time
15
+ import io
16
+
17
+ from utils.helpers import format_number, format_currency
18
+ from utils.excel_handler import export_to_excel
19
+
20
+
21
+ class RiskAnalysisApp:
22
+ """وحدة تحليل المخاطر"""
23
+
24
+ def __init__(self):
25
+ """تهيئة وحدة تحليل المخاطر"""
26
+ # تهيئة المخاطر المحتملة
27
+ self.risk_categories = [
28
+ "مخاطر مالية",
29
+ "مخاطر زمنية",
30
+ "مخاطر فنية",
31
+ "مخاطر إدارية",
32
+ "مخاطر تنظيمية",
33
+ "مخاطر سوقية",
34
+ "مخاطر تعاقدية"
35
+ ]
36
+
37
+ self.impact_levels = ["منخفض", "متوسط", "عالي"]
38
+ self.probability_levels = ["غير محتمل", "محتمل", "مؤكد"]
39
+
40
+ def render(self):
41
+ """عرض واجهة وحدة تحليل المخاطر"""
42
+
43
+ st.markdown("<h1 class='module-title'>وحدة تحليل المخاطر</h1>", unsafe_allow_html=True)
44
+
45
+ tabs = st.tabs([
46
+ "تحليل المخاطر",
47
+ "سجل المخاطر",
48
+ "مصفوفة المخاطر",
49
+ "خطة الاستجابة للمخاطر"
50
+ ])
51
+
52
+ with tabs[0]:
53
+ self._render_risk_analysis_tab()
54
+
55
+ with tabs[1]:
56
+ self._render_risk_register_tab()
57
+
58
+ with tabs[2]:
59
+ self._render_risk_matrix_tab()
60
+
61
+ with tabs[3]:
62
+ self._render_risk_response_tab()
63
+
64
+ def _render_risk_analysis_tab(self):
65
+ """عرض تبويب تحليل المخاطر"""
66
+
67
+ st.markdown("### تحليل المخاطر")
68
+
69
+ # التحقق من وجود مشروع حالي
70
+ if 'current_project' not in st.session_state or st.session_state.current_project is None:
71
+ # إذا لم يكن هناك مشروع محدد، اعرض قائمة باختيار المشروع
72
+ if 'projects' in st.session_state and st.session_state.projects:
73
+ project_names = [p['name'] for p in st.session_state.projects]
74
+ selected_project_name = st.selectbox("اختر المشروع", project_names)
75
+
76
+ if selected_project_name:
77
+ selected_project = next((p for p in st.session_state.projects if p['name'] == selected_project_name), None)
78
+ if selected_project:
79
+ st.session_state.current_project = selected_project
80
+ else:
81
+ st.warning("لم يتم العثور على المشروع المحدد.")
82
+ return
83
+ else:
84
+ st.info("يرجى اختيار مشروع لتحليل مخاطره.")
85
+ return
86
+ else:
87
+ st.warning("لا توجد مشاريع متاحة. يرجى إنشاء مشروع جديد أولاً.")
88
+ return
89
+
90
+ # عرض معلومات المشروع
91
+ project = st.session_state.current_project
92
+
93
+ col1, col2, col3 = st.columns(3)
94
+ with col1:
95
+ st.metric("اسم المشروع", project['name'])
96
+ with col2:
97
+ st.metric("رقم المناقصة", project['number'])
98
+ with col3:
99
+ st.metric("الجهة المالكة", project['client'])
100
+
101
+ # التحقق من وجود سجل المخاطر للمشروع
102
+ if 'risks' not in project:
103
+ project['risks'] = []
104
+
105
+ # نموذج إضافة مخاطر
106
+ with st.form("add_risk_form"):
107
+ st.markdown("#### إضافة مخاطرة جديدة")
108
+
109
+ col1, col2 = st.columns(2)
110
+
111
+ with col1:
112
+ risk_code = st.text_input("رمز المخاطرة", f"R{len(project['risks']) + 1}")
113
+ risk_category = st.selectbox("فئة المخاطرة", self.risk_categories)
114
+ impact = st.select_slider("التأثير", self.impact_levels, value="متوسط")
115
+
116
+ with col2:
117
+ risk_description = st.text_area("وصف المخاطرة", height=80)
118
+ probability = st.select_slider("الاحتمالية", self.probability_levels, value="محتمل")
119
+ response_strategy = st.text_area("استراتيجية الاستجابة", height=80)
120
+
121
+ submitted = st.form_submit_button("إضافة المخاطرة")
122
+
123
+ if submitted:
124
+ # التحقق من تعبئة الحقول الإلزامية
125
+ if not risk_description:
126
+ st.error("يرجى إدخال وصف المخاطرة.")
127
+ else:
128
+ # إنشاء مخاطرة جديدة
129
+ new_risk = {
130
+ 'id': len(project['risks']) + 1,
131
+ 'risk_code': risk_code,
132
+ 'description': risk_description,
133
+ 'category': risk_category,
134
+ 'impact': impact,
135
+ 'probability': probability,
136
+ 'response_strategy': response_strategy,
137
+ 'status': "نشط",
138
+ 'created_at': datetime.now().strftime('%Y-%m-%d'),
139
+ 'risk_score': self._calculate_risk_score(impact, probability)
140
+ }
141
+
142
+ # إضافة المخاطرة إلى سجل المخاطر
143
+ project['risks'].append(new_risk)
144
+
145
+ st.success(f"تمت إضافة المخاطرة [{risk_code}] بنجاح!")
146
+ st.balloons()
147
+
148
+ # خيارات تحليل المخاطر
149
+ st.markdown("#### خيارات تحليل المخاطر")
150
+
151
+ col1, col2 = st.columns(2)
152
+
153
+ with col1:
154
+ automated_analysis = st.button("تحليل تلقائي للمخاطر")
155
+
156
+ with col2:
157
+ from_document_analysis = st.button("استيراد المخاطر من تحليل المستندات")
158
+
159
+ if automated_analysis:
160
+ with st.spinner("جاري تحليل المخاطر..."):
161
+ time.sleep(2)
162
+ self._generate_automated_risks(project)
163
+ st.success("تم تحليل المخاطر بنجاح!")
164
+ st.balloons()
165
+
166
+ if from_document_analysis:
167
+ with st.spinner("جاري استيراد المخاطر من تحليل المستندات..."):
168
+ time.sleep(2)
169
+
170
+ # هذه مجرد محاكاة، في الواقع يجب استدعاء الوظيفة الفعلية لاستيراد المخاطر
171
+ document_risks = self._get_risks_from_documents()
172
+
173
+ if document_risks:
174
+ existing_risk_codes = [r['risk_code'] for r in project['risks']]
175
+
176
+ for risk in document_risks:
177
+ # تجنب تكرار المخاطر
178
+ if risk['risk_code'] not in existing_risk_codes:
179
+ project['risks'].append(risk)
180
+
181
+ st.success(f"تم استيراد {len(document_risks)} مخاطرة من تحليل المستندات!")
182
+ else:
183
+ st.warning("لم يتم العثور على مخاطر في المستندات.")
184
+
185
+ # عرض ملخص المخاطر
186
+ if project['risks']:
187
+ self._show_risk_summary(project['risks'])
188
+
189
+ def _render_risk_register_tab(self):
190
+ """عرض تبويب سجل المخاطر"""
191
+
192
+ st.markdown("### سجل المخاطر")
193
+
194
+ # التحقق من وجود مشروع حالي
195
+ if 'current_project' not in st.session_state or st.session_state.current_project is None:
196
+ st.info("يرجى اختيار مشروع من تبويب تحليل المخاطر أولاً.")
197
+ return
198
+
199
+ project = st.session_state.current_project
200
+
201
+ if 'risks' not in project or not project['risks']:
202
+ st.info("لا توجد مخاطر مسجلة لهذا المشروع. يمكنك إضافة مخاطر من تبويب تحليل المخاطر.")
203
+ return
204
+
205
+ # فلترة سجل المخاطر
206
+ col1, col2, col3 = st.columns(3)
207
+
208
+ with col1:
209
+ search_term = st.text_input("البحث في سجل المخاطر")
210
+
211
+ with col2:
212
+ category_filter = st.multiselect("فلترة حسب الفئة", self.risk_categories)
213
+
214
+ with col3:
215
+ impact_filter = st.multiselect("فلترة حسب التأثير", self.impact_levels)
216
+
217
+ # تطبيق الفلترة
218
+ filtered_risks = project['risks']
219
+
220
+ if search_term:
221
+ filtered_risks = [r for r in filtered_risks if search_term.lower() in r.get('description', '').lower()]
222
+
223
+ if category_filter:
224
+ filtered_risks = [r for r in filtered_risks if r.get('category') in category_filter]
225
+
226
+ if impact_filter:
227
+ filtered_risks = [r for r in filtered_risks if r.get('impact') in impact_filter]
228
+
229
+ # عرض سجل المخاطر
230
+ if filtered_risks:
231
+ # تحويل المخاطر إلى DataFrame
232
+ risk_df = pd.DataFrame(filtered_risks)
233
+
234
+ # تحديد الأعمدة المراد عرضها وترتيبها
235
+ display_columns = [
236
+ 'risk_code', 'description', 'category', 'impact',
237
+ 'probability', 'risk_score', 'status'
238
+ ]
239
+
240
+ # تغيير أسماء الأعمدة للعرض
241
+ column_names = {
242
+ 'risk_code': 'رمز المخاطرة',
243
+ 'description': 'وصف المخاطرة',
244
+ 'category': 'الفئة',
245
+ 'impact': 'التأثير',
246
+ 'probability': 'الاحتمالية',
247
+ 'risk_score': 'درجة المخاطرة',
248
+ 'status': 'الحالة',
249
+ 'response_strategy': 'استراتيجية الاستجابة',
250
+ 'created_at': 'تاريخ الإنشاء'
251
+ }
252
+
253
+ # إعداد DataFrame للعرض
254
+ if 'response_strategy' in risk_df.columns:
255
+ display_columns.append('response_strategy')
256
+
257
+ if 'created_at' in risk_df.columns:
258
+ display_columns.append('created_at')
259
+
260
+ # الحصول على الأعمدة المتوفرة فقط
261
+ available_columns = [col for col in display_columns if col in risk_df.columns]
262
+
263
+ if available_columns:
264
+ display_df = risk_df[available_columns].rename(columns=column_names)
265
+
266
+ # عرض الجدول
267
+ st.dataframe(display_df, use_container_width=True, hide_index=True)
268
+
269
+ # أزرار العمليات
270
+ col1, col2 = st.columns(2)
271
+
272
+ with col1:
273
+ if st.button("تصدير سجل المخاطر إلى Excel"):
274
+ st.success("تم تصدير سجل المخاطر بنجاح!")
275
+
276
+ with col2:
277
+ if st.button("طباعة تقرير المخاطر"):
278
+ st.success("تم إنشاء تقرير المخاطر بنجاح!")
279
+ else:
280
+ st.warning("هناك مشكلة في بنية بيانات المخاطر. يرجى التحقق من سلامة البيانات.")
281
+ else:
282
+ st.info("لا توجد مخاطر تطابق معايير البحث.")
283
+
284
+ def _render_risk_matrix_tab(self):
285
+ """عرض تبويب مصفوفة المخاطر"""
286
+
287
+ st.markdown("### مصفوفة المخاطر")
288
+
289
+ # التحقق من وجود مشروع حالي
290
+ if 'current_project' not in st.session_state or st.session_state.current_project is None:
291
+ st.info("يرجى اختيار مشروع من تبويب تحليل المخاطر أولاً.")
292
+ return
293
+
294
+ project = st.session_state.current_project
295
+
296
+ if 'risks' not in project or not project['risks']:
297
+ st.info("لا توجد مخاطر مسجلة لهذا المشروع. يمكنك إضافة مخاطر من تبويب تحليل المخاطر.")
298
+ return
299
+
300
+ # إنشاء ضبط مصفوفة المخاطر (3×3)
301
+ impact_values = {"منخفض": 1, "متوسط": 2, "عالي": 3}
302
+ probability_values = {"غير محتمل": 1, "محتمل": 2, "مؤكد": 3}
303
+
304
+ # إنشاء DataFrame لتمثيل مصفوفة المخاطر
305
+ matrix_data = []
306
+
307
+ for p in probability_values.keys():
308
+ for i in impact_values.keys():
309
+ p_value = probability_values[p]
310
+ i_value = impact_values[i]
311
+ risk_score = p_value * i_value
312
+
313
+ # تحديد اللون حسب درجة المخاطرة
314
+ if risk_score <= 2:
315
+ color = 'green' # منخفضة
316
+ elif risk_score <= 6:
317
+ color = 'orange' # متوسطة
318
+ else:
319
+ color = 'red' # عالية
320
+
321
+ # استخراج المخاطر التي تقع في هذه الخلية
322
+ cell_risks = [r for r in project['risks'] if r.get('impact') == i and r.get('probability') == p]
323
+
324
+ # إضافة بيانات الخلية
325
+ matrix_data.append({
326
+ 'احتمالية': p,
327
+ 'تأثير': i,
328
+ 'درجة_المخاطرة': risk_score,
329
+ 'عدد_المخاطر': len(cell_risks),
330
+ 'المخاطر': [r.get('risk_code') for r in cell_risks],
331
+ 'لون': color
332
+ })
333
+
334
+ # تحو��ل إلى DataFrame
335
+ matrix_df = pd.DataFrame(matrix_data)
336
+
337
+ # رسم مصفوفة المخاطر باستخدام Plotly
338
+ fig = go.Figure()
339
+
340
+ for index, row in matrix_df.iterrows():
341
+ # إنشاء نص الخلية
342
+ if row['عدد_المخاطر'] > 0:
343
+ cell_text = f"{', '.join(row['المخاطر'])}<br>({row['عدد_المخاطر']} مخاطر)"
344
+ else:
345
+ cell_text = ''
346
+
347
+ # إنشاء خلية المصفوفة
348
+ fig.add_trace(go.Scatter(
349
+ x=[row['تأثير']],
350
+ y=[row['احتمالية']],
351
+ mode='markers+text',
352
+ marker=dict(
353
+ color=row['لون'],
354
+ size=20 + (row['عدد_المخاطر'] * 5),
355
+ opacity=0.8
356
+ ),
357
+ text=cell_text,
358
+ textposition="middle center",
359
+ name=f"{row['احتمالية']} - {row['تأثير']}"
360
+ ))
361
+
362
+ # تكوين المحاور
363
+ fig.update_layout(
364
+ title="مصفوفة المخاطر (الاحتمالية × التأثير)",
365
+ xaxis=dict(
366
+ title="التأثير",
367
+ tickmode='array',
368
+ tickvals=[1, 2, 3],
369
+ ticktext=["منخفض", "متوسط", "عالي"],
370
+ gridcolor='lightgray'
371
+ ),
372
+ yaxis=dict(
373
+ title="الاحتمالية",
374
+ tickmode='array',
375
+ tickvals=[1, 2, 3],
376
+ ticktext=["غير محتمل", "محتمل", "مؤكد"],
377
+ gridcolor='lightgray'
378
+ ),
379
+ height=600
380
+ )
381
+
382
+ # عرض المصفوفة
383
+ st.plotly_chart(fig, use_container_width=True)
384
+
385
+ # عرض توزيع المخاطر حسب الفئة
386
+ st.markdown("#### توزيع المخاطر حسب الفئة")
387
+
388
+ # حساب عدد المخاطر في كل فئة
389
+ category_counts = {}
390
+ for r in project['risks']:
391
+ category = r.get('category', 'أخرى')
392
+ category_counts[category] = category_counts.get(category, 0) + 1
393
+
394
+ # إنشاء DataFrame
395
+ category_df = pd.DataFrame({
396
+ 'الفئة': list(category_counts.keys()),
397
+ 'عدد المخاطر': list(category_counts.values())
398
+ })
399
+
400
+ # رسم مخطط دائري
401
+ fig = px.pie(
402
+ category_df,
403
+ values='عدد المخاطر',
404
+ names='الفئة',
405
+ title='توزيع المخاطر حسب الفئة',
406
+ hole=0.4
407
+ )
408
+
409
+ st.plotly_chart(fig, use_container_width=True)
410
+
411
+ def _render_risk_response_tab(self):
412
+ """عرض تبويب خطة الاستجابة للمخاطر"""
413
+
414
+ st.markdown("### خطة الاستجابة للمخاطر")
415
+
416
+ # التحقق من وجود مشروع حالي
417
+ if 'current_project' not in st.session_state or st.session_state.current_project is None:
418
+ st.info("يرجى اختيار مشروع من تبويب تحليل المخاطر أولاً.")
419
+ return
420
+
421
+ project = st.session_state.current_project
422
+
423
+ if 'risks' not in project or not project['risks']:
424
+ st.info("لا توجد مخاطر مسجلة لهذا المشروع. يمكنك إضافة مخاطر من تبويب تحليل المخاطر.")
425
+ return
426
+
427
+ # ترتيب المخاطر حسب درجة المخاطرة (من الأعلى إلى الأقل)
428
+ sorted_risks = sorted(project['risks'], key=lambda x: x.get('risk_score', 0), reverse=True)
429
+
430
+ # عرض خطة الاستجابة للمخاطر
431
+ for i, risk in enumerate(sorted_risks):
432
+ with st.expander(f"{risk.get('risk_code', '')}: {risk.get('description', 'بدون وصف')}", expanded=(i < 3)):
433
+ col1, col2, col3 = st.columns(3)
434
+
435
+ with col1:
436
+ st.markdown(f"**الفئة**: {risk.get('category', 'غير محدد')}")
437
+ st.markdown(f"**التأثير**: {risk.get('impact', 'غير محدد')}")
438
+
439
+ with col2:
440
+ st.markdown(f"**الاحتمالية**: {risk.get('probability', 'غير محدد')}")
441
+ st.markdown(f"**درجة المخاطرة**: {risk.get('risk_score', 'غير محدد')}")
442
+
443
+ with col3:
444
+ st.markdown(f"**الحالة**: {risk.get('status', 'نشط')}")
445
+ risk_owner = risk.get('risk_owner', 'غير محدد')
446
+ st.markdown(f"**مسؤول المخاطرة**: {risk_owner}")
447
+
448
+ st.markdown("---")
449
+ st.markdown("#### استراتيجية الاستجابة")
450
+ current_strategy = risk.get('response_strategy', '')
451
+ new_strategy = st.text_area(f"استراتيجية الاستجابة للمخاطرة {risk.get('risk_code', '')}",
452
+ value=current_strategy,
453
+ height=100,
454
+ key=f"strategy_{risk.get('risk_code', '')}")
455
+
456
+ # تحديث استراتيجية الاستجابة إذا تم تغييرها
457
+ if new_strategy != current_strategy:
458
+ risk['response_strategy'] = new_strategy
459
+
460
+ st.markdown("#### إجراءات التحكم")
461
+ control_measures = risk.get('control_measures', [])
462
+
463
+ if control_measures:
464
+ for j, measure in enumerate(control_measures):
465
+ st.markdown(f"{j+1}. {measure}")
466
+ else:
467
+ st.info("لم يتم تعريف إجراءات تحكم لهذه المخاطرة.")
468
+
469
+ # إضافة إجراء تحكم جديد
470
+ new_measure = st.text_input(f"إجراء تحكم جديد للمخاطرة {risk.get('risk_code', '')}",
471
+ key=f"measure_{risk.get('risk_code', '')}")
472
+
473
+ if st.button(f"إضافة إجراء", key=f"add_measure_{risk.get('risk_code', '')}"):
474
+ if new_measure:
475
+ if 'control_measures' not in risk:
476
+ risk['control_measures'] = []
477
+
478
+ risk['control_measures'].append(new_measure)
479
+ st.success(f"تم إضافة إجراء التحكم بنجاح!")
480
+ st.experimental_rerun()
481
+ else:
482
+ st.error("يرجى إدخال إجراء التحكم.")
483
+
484
+ # زر تصدير خطة الاستجابة للمخاطر
485
+ if st.button("تصدير خطة الاستجابة للمخاطر"):
486
+ st.success("تم تصدير خطة الاستجابة للمخاطر بنجاح!")
487
+
488
+ def _calculate_risk_score(self, impact, probability):
489
+ """حساب درجة المخاطرة بناءً على التأثير والاحتمالية"""
490
+ impact_values = {"منخفض": 1, "متوسط": 2, "عالي": 3}
491
+ probability_values = {"غير محتمل": 1, "محتمل": 2, "مؤكد": 3}
492
+
493
+ impact_value = impact_values.get(impact, 1)
494
+ probability_value = probability_values.get(probability, 1)
495
+
496
+ return impact_value * probability_value
497
+
498
+ def _generate_automated_risks(self, project):
499
+ """توليد مخاطر تلقائية بناءً على خصائص المشروع"""
500
+
501
+ # قائمة المخاطر الشائعة في مشاريع المقاولات
502
+ common_risks = [
503
+ {
504
+ 'risk_code': 'RF01',
505
+ 'description': 'غرامة تأخير مرتفعة (10% من قيمة العقد)',
506
+ 'category': 'مخاطر مالية',
507
+ 'impact': 'عالي',
508
+ 'probability': 'محتمل',
509
+ 'response_strategy': 'تخصيص مبلغ احتياطي للغرامات المحتملة ووضع خطة لإدارة الجدول الزمني بشكل فعال',
510
+ 'status': 'نشط',
511
+ 'risk_score': 6
512
+ },
513
+ {
514
+ 'risk_code': 'RF02',
515
+ 'description': 'متطلبات ضمان بنكي مرتفعة (15% من قيمة العقد)',
516
+ 'category': 'مخاطر مالية',
517
+ 'impact': 'متوسط',
518
+ 'probability': 'مؤكد',
519
+ 'response_strategy': 'التفاوض مع العميل لتخفيض نسبة الضمان البنكي أو تقسيمه على مراحل المشروع',
520
+ 'status': 'نشط',
521
+ 'risk_score': 6
522
+ },
523
+ {
524
+ 'risk_code': 'RF03',
525
+ 'description': 'شروط دفع متأخرة (60 يوم)',
526
+ 'category': 'مخاطر مالية',
527
+ 'impact': 'متوسط',
528
+ 'probability': 'مؤكد',
529
+ 'response_strategy': 'التخطيط للتدفق النقدي مع الأخذ بالاعتبار تأخر الدفعات وتأمين خط ائتمان احتياطي',
530
+ 'status': 'نشط',
531
+ 'risk_score': 6
532
+ },
533
+ {
534
+ 'risk_code': 'RT01',
535
+ 'description': 'مدة تنفيذ قصيرة (12 شهر)',
536
+ 'category': 'مخاطر زمنية',
537
+ 'impact': 'عالي',
538
+ 'probability': 'محتمل',
539
+ 'response_strategy': 'زيادة فريق العمل واستخدام موارد إضافية مع وضع خطة عمل تفصيلية ومراقبتها أسبوعياً',
540
+ 'status': 'نشط',
541
+ 'risk_score': 6
542
+ },
543
+ {
544
+ 'risk_code': 'RT02',
545
+ 'description': 'احتمالية تأخر توريد المواد الرئيسية',
546
+ 'category': 'مخاطر زمنية',
547
+ 'impact': 'عالي',
548
+ 'probability': 'محتمل',
549
+ 'response_strategy': 'تحديد المواد ذات فترات التوريد الطويلة وطلبها مبكراً مع التعاقد مع موردين بدلاء',
550
+ 'status': 'نشط',
551
+ 'risk_score': 6
552
+ },
553
+ {
554
+ 'risk_code': 'RTE01',
555
+ 'description': 'غموض في بعض المواصفات الفنية',
556
+ 'category': 'مخاطر فنية',
557
+ 'impact': 'متوسط',
558
+ 'probability': 'محتمل',
559
+ 'response_strategy': 'طلب توضيح من العميل قبل البدء بالتنفيذ وتوثيق جميع الردود والتوضيحات',
560
+ 'status': 'نشط',
561
+ 'risk_score': 4
562
+ },
563
+ {
564
+ 'risk_code': 'RTE02',
565
+ 'description': 'تضارب بين المخططات والمواصفات',
566
+ 'category': 'مخاطر فنية',
567
+ 'impact': 'متوسط',
568
+ 'probability': 'محتمل',
569
+ 'response_strategy': 'مراجعة شاملة للمستندات وتوثيق التضاربات وطلب توضيح من العميل',
570
+ 'status': 'نشط',
571
+ 'risk_score': 4
572
+ },
573
+ {
574
+ 'risk_code': 'RM01',
575
+ 'description': 'عدم وضوح آلية استلام الأعمال',
576
+ 'category': 'مخاطر إدارية',
577
+ 'impact': 'منخفض',
578
+ 'probability': 'محتمل',
579
+ 'response_strategy': 'طلب توضيح آلية الاستلام من العميل ووضع إجراءات داخلية للتحقق من جودة الأعمال قبل التقديم للاستلام',
580
+ 'status': 'نشط',
581
+ 'risk_score': 2
582
+ },
583
+ {
584
+ 'risk_code': 'RR01',
585
+ 'description': 'شروط تعجيزية للمحتوى المحلي',
586
+ 'category': 'مخاطر تنظيمية',
587
+ 'impact': 'عالي',
588
+ 'probability': 'محتمل',
589
+ 'response_strategy': 'دراسة متطلبات المحتوى المحلي بدقة ووضع خطة لتحقيقها مع الاحتفاظ بسجلات التوثيق اللازمة',
590
+ 'status': 'نشط',
591
+ 'risk_score': 6
592
+ },
593
+ {
594
+ 'risk_code': 'RM01',
595
+ 'description': 'خطر التغييرات في أسعار المواد',
596
+ 'category': 'مخاطر سوقية',
597
+ 'impact': 'عالي',
598
+ 'probability': 'محتمل',
599
+ 'response_strategy': 'تثبيت أسعار المواد الرئيسية مع الموردين وإدراج بند تعديل الأسعار في العقد',
600
+ 'status': 'نشط',
601
+ 'risk_score': 6
602
+ },
603
+ {
604
+ 'risk_code': 'RC01',
605
+ 'description': 'عدم وضوح بعض بنود العقد',
606
+ 'category': 'مخاطر تعاقدية',
607
+ 'impact': 'متوسط',
608
+ 'probability': 'محتمل',
609
+ 'response_strategy': 'مراجعة العقد من قبل مستشار قانوني متخصص وطلب توضيح للبنود الغامضة قبل التوقيع',
610
+ 'status': 'نشط',
611
+ 'risk_score': 4
612
+ }
613
+ ]
614
+
615
+ # إضافة المخاطر الشائعة إلى المشروع
616
+ existing_risk_codes = [r['risk_code'] for r in project['risks']]
617
+
618
+ for risk in common_risks:
619
+ # تجنب تكرار المخاطر
620
+ if risk['risk_code'] not in existing_risk_codes:
621
+ risk['id'] = len(project['risks']) + 1
622
+ risk['created_at'] = datetime.now().strftime('%Y-%m-%d')
623
+ project['risks'].append(risk)
624
+
625
+ def _get_risks_from_documents(self):
626
+ """استيراد المخاطر من تحليل المستندات"""
627
+
628
+ # محاكاة لاستيراد المخاطر من تحليل المستندات
629
+ # في التطبيق الفعلي، يجب استدعاء الوظيفة المناسبة من وحدة تحليل المستندات
630
+
631
+ document_risks = [
632
+ {
633
+ 'risk_code': 'RD01',
634
+ 'description': 'غرامة تأخير مرتفعة تصل إلى 20% من قيمة العقد',
635
+ 'category': 'مخاطر مالية',
636
+ 'impact': 'عالي',
637
+ 'probability': 'مؤكد',
638
+ 'response_strategy': 'التفاوض على تخفيض الغرامة أو تقسيمها حسب مراحل المشروع مع وضع خطة محكمة للجدول الزمني',
639
+ 'status': 'نشط',
640
+ 'risk_score': 9,
641
+ 'created_at': datetime.now().strftime('%Y-%m-%d')
642
+ },
643
+ {
644
+ 'risk_code': 'RD02',
645
+ 'description': 'يحق للمالك إيقاف المشروع لمدة تصل إلى 90 يوم دون تعويض',
646
+ 'category': 'مخاطر تعاقدية',
647
+ 'impact': 'عالي',
648
+ 'probability': 'محتمل',
649
+ 'response_strategy': 'طلب إضافة بند للتعويض عن التكاليف الإضافية الناتجة عن الإيقاف لفترات طويلة',
650
+ 'status': 'نشط',
651
+ 'risk_score': 6,
652
+ 'created_at': datetime.now().strftime('%Y-%m-%d')
653
+ },
654
+ {
655
+ 'risk_code': 'RD03',
656
+ 'description': 'تحمل المقاول مسؤولية استخراج جميع التصاريح الحكومية',
657
+ 'category': 'مخاطر تنظيمية',
658
+ 'impact': 'متوسط',
659
+ 'probability': 'مؤكد',
660
+ 'response_strategy': 'حصر جميع التصاريح المطلوبة والبدء في إجراءات استخراجها مبكراً مع تخصيص فريق لمتابعتها',
661
+ 'status': 'نشط',
662
+ 'risk_score': 6,
663
+ 'created_at': datetime.now().strftime('%Y-%m-%d')
664
+ },
665
+ {
666
+ 'risk_code': 'RD04',
667
+ 'description': 'شروط الدفعة المقدمة مقيدة بضمان بنكي بقيمة 120% من قيمة الدفعة',
668
+ 'category': 'مخاطر مالية',
669
+ 'impact': 'متوسط',
670
+ 'probability': 'مؤكد',
671
+ 'response_strategy': 'التفاوض على خفض نسبة الضمان البنكي أو تقديم ضمان شركة بدلاً من الضمان البنكي',
672
+ 'status': 'نشط',
673
+ 'risk_score': 6,
674
+ 'created_at': datetime.now().strftime('%Y-%m-%d')
675
+ }
676
+ ]
677
+
678
+ return document_risks
679
+
680
+ def _show_risk_summary(self, risks):
681
+ """عرض ملخص المخاطر"""
682
+
683
+ st.markdown("#### ملخص المخاطر")
684
+
685
+ # حساب إحصائيات المخاطر
686
+ total_risks = len(risks)
687
+ risk_levels = {
688
+ 'عالية': len([r for r in risks if r.get('risk_score', 0) >= 6]),
689
+ 'متوسطة': len([r for r in risks if 3 <= r.get('risk_score', 0) < 6]),
690
+ 'منخفضة': len([r for r in risks if r.get('risk_score', 0) < 3])
691
+ }
692
+
693
+ # عرض الإحصائيات
694
+ col1, col2, col3, col4 = st.columns(4)
695
+
696
+ with col1:
697
+ st.metric("إجمالي المخاطر", total_risks)
698
+
699
+ with col2:
700
+ st.metric("المخاطر العالية", risk_levels['عالية'], delta=f"{risk_levels['عالية']/total_risks*100:.1f}%", delta_color="inverse")
701
+
702
+ with col3:
703
+ st.metric("المخاطر المتوسطة", risk_levels['متوسطة'], delta=f"{risk_levels['متوسطة']/total_risks*100:.1f}%", delta_color="off")
704
+
705
+ with col4:
706
+ st.metric("المخاطر المنخفضة", risk_levels['منخفضة'], delta=f"{risk_levels['منخفضة']/total_risks*100:.1f}%", delta_color="normal")
707
+
708
+ # عرض الرسم البياني للمخاطر
709
+ risk_level_df = pd.DataFrame({
710
+ 'مستوى المخاطرة': list(risk_levels.keys()),
711
+ 'عدد المخاطر': list(risk_levels.values())
712
+ })
713
+
714
+ fig = px.bar(
715
+ risk_level_df,
716
+ x='مستوى المخاطرة',
717
+ y='عدد المخاطر',
718
+ color='مستوى المخاطرة',
719
+ color_discrete_map={
720
+ 'عالية': 'red',
721
+ 'متوسطة': 'orange',
722
+ 'منخفضة': 'green'
723
+ },
724
+ title='توزيع المخاطر حسب المستوى'
725
+ )
726
+
727
+ st.plotly_chart(fig, use_container_width=True)
728
+
729
+ # عرض أعل�� 5 مخاطر من حيث درجة المخاطرة
730
+ st.markdown("#### أعلى 5 مخاطر")
731
+
732
+ # ترتيب المخاطر حسب درجة المخاطرة
733
+ sorted_risks = sorted(risks, key=lambda x: x.get('risk_score', 0), reverse=True)
734
+ top_risks = sorted_risks[:5]
735
+
736
+ # إنشاء DataFrame للعرض
737
+ if top_risks:
738
+ top_risks_data = []
739
+
740
+ for r in top_risks:
741
+ top_risks_data.append({
742
+ 'رمز المخاطرة': r.get('risk_code', ''),
743
+ 'وصف المخاطرة': r.get('description', ''),
744
+ 'الفئة': r.get('category', ''),
745
+ 'التأثير': r.get('impact', ''),
746
+ 'الاحتمالية': r.get('probability', ''),
747
+ 'درجة المخاطرة': r.get('risk_score', 0)
748
+ })
749
+
750
+ top_risks_df = pd.DataFrame(top_risks_data)
751
+ st.dataframe(top_risks_df, use_container_width=True, hide_index=True)
modules/services/item_extractor.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ خدمة استخراج البنود من المستندات
3
+ """
4
+
5
+ import re
6
+ import pandas as pd
7
+ import numpy as np
8
+ import nltk
9
+ from nltk.tokenize import sent_tokenize
10
+ from pathlib import Path
11
+ import config
12
+
13
+ class ItemExtractor:
14
+ """استخراج البنود من المستندات"""
15
+
16
+ def __init__(self):
17
+ # تحميل موارد NLTK إذا لم تكن موجودة
18
+ try:
19
+ nltk.data.find('tokenizers/punkt')
20
+ except LookupError:
21
+ nltk.download('punkt')
22
+
23
+ # قائمة الكلمات المفتاحية التي تشير إلى بداية البنود
24
+ self.item_indicators = [
25
+ 'توريد', 'تركيب', 'تنفيذ', 'تصنيع', 'أعمال', 'تأمين',
26
+ 'تقديم', 'إنشاء', 'صيانة', 'إزالة', 'نقل', 'تجهيز',
27
+ 'فك', 'تسليم', 'تطبيق', 'تثبيت', 'تشطيب', 'تجهيز'
28
+ ]
29
+
30
+ # قائمة فئات البنود
31
+ self.categories = {
32
+ 'أعمال الأساسات': ['أساس', 'قاعدة', 'حفر', 'ردم', 'خرسانة', 'اسمنت', 'قواعد'],
33
+ 'أعمال الهيكل الإنشائي': ['عمود', 'سقف', 'كمرة', 'خرسانة', 'حديد تسليح', 'بلاطة', 'هيكل'],
34
+ 'أعمال التشطيبات': ['دهان', 'بلاط', 'سيراميك', 'رخام', 'جبس', 'زجاج', 'باب', 'نافذة', 'أرضية'],
35
+ 'أعمال الكهرباء': ['كهرباء', 'إضاءة', 'مفتاح', 'سلك', 'لوحة', 'كابل', 'تمديد'],
36
+ 'أعمال السباكة': ['ماء', 'صرف', 'مواسير', 'حمام', 'مغسلة', 'خزان', 'مضخة'],
37
+ 'أعمال التكييف': ['تكييف', 'تبريد', 'تهوية', 'مكيف', 'مجرى هواء', 'فلتر'],
38
+ 'أعمال الموقع': ['تسوية', 'تخطيط', 'أسوار', 'بوابات', 'طرق', 'رصف', 'تشجير'],
39
+ 'المستندات': ['مخططات', 'رسومات', 'تقارير', 'شهادات', 'اختبارات']
40
+ }
41
+
42
+ def extract_items(self, text):
43
+ """استخراج البنود من النص"""
44
+ if not text:
45
+ return pd.DataFrame()
46
+
47
+ # تقسيم النص إلى جمل
48
+ sentences = sent_tokenize(text)
49
+
50
+ # البحث عن البنود المحتملة
51
+ items = []
52
+ item_id = 1
53
+
54
+ for sentence in sentences:
55
+ # تحقق مما إذا كانت الجملة تحتوي على مؤشر بند
56
+ if any(indicator in sentence for indicator in self.item_indicators):
57
+ # تحديد الفئة
58
+ category = self._determine_category(sentence)
59
+
60
+ # تحديد الأهمية
61
+ importance = self._determine_importance(sentence)
62
+
63
+ # إضافة البند إلى القائمة
64
+ items.append({
65
+ 'رقم البند': f"I{item_id:03d}",
66
+ 'وصف البند': sentence.strip(),
67
+ 'الفئة': category,
68
+ 'الأهمية': importance,
69
+ 'الثقة': round(np.random.uniform(0.75, 0.95), 2) # محاكاة ثقة التعرف
70
+ })
71
+
72
+ item_id += 1
73
+
74
+ # تحويل القائمة إلى DataFrame
75
+ items_df = pd.DataFrame(items)
76
+
77
+ # التأكد من وجود بيانات
78
+ if items_df.empty:
79
+ # إنشاء DataFrame فارغ بالأعمدة المطلوبة
80
+ items_df = pd.DataFrame(columns=[
81
+ 'رقم البند', 'وصف البند', 'الفئة', 'الأهمية', 'الثقة'
82
+ ])
83
+
84
+ return items_df
85
+
86
+ def _determine_category(self, text):
87
+ """تحديد فئة البند بناءً على محتواه"""
88
+ # البحث عن الكلمات المفتاحية في النص
89
+ scores = {}
90
+
91
+ for category, keywords in self.categories.items():
92
+ score = sum(1 for keyword in keywords if keyword in text.lower())
93
+ scores[category] = score
94
+
95
+ # اختيار الفئة ذات الدرجة الأعلى
96
+ if max(scores.values()) > 0:
97
+ return max(scores.items(), key=lambda x: x[1])[0]
98
+ else:
99
+ return "أخرى"
100
+
101
+ def _determine_importance(self, text):
102
+ """تحديد أهمية البند بناءً على محتواه"""
103
+ # كلمات تشير إلى أهمية عالية
104
+ high_importance_words = [
105
+ 'ضروري', 'هام', 'أساسي', 'رئيسي', 'كبير', 'مهم',
106
+ 'حرج', 'أمان', 'سلامة', 'صحة', 'بيئة'
107
+ ]
108
+
109
+ # كلمات تشير إلى أهمية منخفضة
110
+ low_importance_words = [
111
+ 'ثانوي', 'إضافي', 'تجميلي', 'مكمل', 'اختياري'
112
+ ]
113
+
114
+ # حساب درجة الأهمية
115
+ high_score = sum(1 for word in high_importance_words if word in text.lower())
116
+ low_score = sum(1 for word in low_importance_words if word in text.lower())
117
+
118
+ # تحديد الأهمية بناءً على الدرجات
119
+ if high_score > low_score:
120
+ return "عالية"
121
+ elif low_score > high_score:
122
+ return "منخفضة"
123
+ else:
124
+ return "متوسطة"
modules/services/quantity_extractor.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ خدمة استخراج الكميات من المستندات
3
+ """
4
+
5
+ import re
6
+ import pandas as pd
7
+ import numpy as np
8
+ from pathlib import Path
9
+ import config
10
+
11
+ class QuantityExtractor:
12
+ """استخراج الكميات من المستندات"""
13
+
14
+ def __init__(self):
15
+ # وحدات القياس الشائعة
16
+ self.units = {
17
+ 'أعمال الخرسانة': 'م3',
18
+ 'أعمال الحفر': 'م3',
19
+ 'أعمال الردم': 'م3',
20
+ 'حديد التسليح': 'طن',
21
+ 'أعمال البلاط': 'م2',
22
+ 'أعمال السيراميك': 'م2',
23
+ 'أعمال الرخام': 'م2',
24
+ 'أعمال البلوك': 'م2',
25
+ 'أعمال الدهان': 'م2',
26
+ 'أعمال اللياسة': 'م2',
27
+ 'أعمال العزل': 'م2',
28
+ 'أعمال تمديدات الكهرباء': 'نقطة',
29
+ 'أعمال تمديدات السباكة': 'نقطة',
30
+ 'أعمال الأبواب': 'عدد',
31
+ 'أعمال النوافذ': 'عدد',
32
+ 'أعمال مجاري التكييف': 'م.ط',
33
+ 'أعمال الرصف': 'م2',
34
+ 'أعمال التسوية': 'م2',
35
+ 'مواسير الصرف': 'م.ط',
36
+ 'مواسير المياه': 'م.ط'
37
+ }
38
+
39
+ # تعبيرات منتظمة لاستخراج الأرقام والوحدات
40
+ self.number_pattern = r'(\d+(?:,\d+)*(?:\.\d+)?)'
41
+ self.unit_pattern = r'(م3|م2|طن|م\.ط|نقطة|عدد|وحدة)'
42
+
43
+ def extract_quantities(self, text, excel_data=None):
44
+ """استخراج الكميات من النص أو بيانات Excel"""
45
+ quantities = []
46
+
47
+ # إذا كانت البيانات من Excel
48
+ if excel_data is not None:
49
+ quantities = self._extract_from_excel(excel_data)
50
+ # وإلا استخراج من النص
51
+ elif text:
52
+ quantities = self._extract_from_text(text)
53
+
54
+ # تحويل القائمة إلى DataFrame
55
+ quantities_df = pd.DataFrame(quantities)
56
+
57
+ # التأكد من وجود بيانات
58
+ if quantities_df.empty:
59
+ # إنشاء DataFrame فارغ بالأعمدة المطلوبة
60
+ quantities_df = pd.DataFrame(columns=[
61
+ 'رقم البند', 'وصف العمل', 'الوحدة', 'الكمية المستخرجة',
62
+ 'الثقة', 'الملاحظات'
63
+ ])
64
+
65
+ return quantities_df
66
+
67
+ def _extract_from_excel(self, excel_data):
68
+ """استخراج الكميات من بيانات Excel"""
69
+ quantities = []
70
+ item_id = 1
71
+
72
+ # التحقق من وجود أعمدة مهمة
73
+ required_cols = ['الوصف', 'البند', 'الكمية', 'الوحدة']
74
+ present_cols = [col for col in required_cols if any(col in str(c).lower() for c in excel_data.columns)]
75
+
76
+ if not present_cols:
77
+ return quantities
78
+
79
+ # تحديد أعمدة البيانات
80
+ desc_col = next((c for c in excel_data.columns if 'وصف' in str(c).lower() or 'بند' in str(c).lower()), None)
81
+ qty_col = next((c for c in excel_data.columns if 'كمية' in str(c).lower() or 'عدد' in str(c).lower()), None)
82
+ unit_col = next((c for c in excel_data.columns if 'وحدة' in str(c).lower()), None)
83
+
84
+ if not (desc_col and qty_col):
85
+ return quantities
86
+
87
+ # استخراج الكميات من كل صف
88
+ for _, row in excel_data.iterrows():
89
+ if pd.notna(row[desc_col]) and pd.notna(row[qty_col]):
90
+ description = str(row[desc_col]).strip()
91
+
92
+ # تجاهل الصفوف الفارغة أو العناوين
93
+ if len(description) < 5 or description.isupper():
94
+ continue
95
+
96
+ # استخراج الكمية والوحدة
97
+ quantity = float(row[qty_col]) if pd.notna(row[qty_col]) else 0
98
+ unit = str(row[unit_col]).strip() if unit_col and pd.notna(row[unit_col]) else self._determine_unit(description)
99
+
100
+ # إضافة البند إلى القائمة
101
+ quantities.append({
102
+ 'رقم البند': f"Q{item_id:03d}",
103
+ 'وصف العمل': description,
104
+ 'الوحدة': unit,
105
+ 'الكمية المستخرجة': quantity,
106
+ 'الثقة': round(np.random.uniform(0.85, 0.99), 2),
107
+ 'الملاحظات': "تم استخراج الكمية من جدول الكميات"
108
+ })
109
+
110
+ item_id += 1
111
+
112
+ return quantities
113
+
114
+ def _extract_from_text(self, text):
115
+ """استخراج الكميات من النص"""
116
+ quantities = []
117
+ item_id = 1
118
+
119
+ # البحث عن العبارات التي تحتوي على أرقام ووحدات
120
+ lines = text.split('\n')
121
+
122
+ for line in lines:
123
+ # البحث عن أعمال محددة
124
+ for work_type in self.units.keys():
125
+ if work_type in line:
126
+ # البحث عن الأرقام في النص
127
+ numbers = re.findall(self.number_pattern, line)
128
+
129
+ if numbers:
130
+ # اختيار أول رقم (الأكثر احتمالاً أن يكون الكمية)
131
+ quantity = float(numbers[0].replace(',', ''))
132
+ unit = self.units[work_type]
133
+
134
+ # إضافة البند إلى القائمة
135
+ quantities.append({
136
+ 'رقم البند': f"Q{item_id:03d}",
137
+ 'وصف العمل': work_type,
138
+ 'الوحدة': unit,
139
+ 'الكمية المستخرجة': quantity,
140
+ 'الثقة': round(np.random.uniform(0.7, 0.9), 2),
141
+ 'الملاحظات': "تم حساب الكمية من النص"
142
+ })
143
+
144
+ item_id += 1
145
+ break
146
+
147
+ # البحث عن وحدات قياس في النص
148
+ unit_matches = re.findall(self.unit_pattern, line)
149
+ if unit_matches and re.search(self.number_pattern, line):
150
+ numbers = re.findall(self.number_pattern, line)
151
+
152
+ if numbers:
153
+ # اختيار أول رقم وأول وحدة
154
+ quantity = float(numbers[0].replace(',', ''))
155
+ unit = unit_matches[0]
156
+
157
+ # استخراج وصف العمل - أول 50 حرف من النص
158
+ description = line[:50] + "..." if len(line) > 50 else line
159
+
160
+ # إضافة البند إلى القائمة (إذا لم يتم إضافته بالفعل)
161
+ if not any(q['وصف العمل'] == description for q in quantities):
162
+ quantities.append({
163
+ 'رقم البند': f"Q{item_id:03d}",
164
+ 'وصف العمل': description,
165
+ 'الوحدة': unit,
166
+ 'الكمية المستخرجة': quantity,
167
+ 'الثقة': round(np.random.uniform(0.6, 0.85), 2),
168
+ 'الملاحظات': "تم استخراج الكمية من النص"
169
+ })
170
+
171
+ item_id += 1
172
+
173
+ return quantities
174
+
175
+ def _determine_unit(self, description):
176
+ """تحديد وحدة القياس المناسبة بناءً على وصف العمل"""
177
+ for work_type, unit in self.units.items():
178
+ if work_type in description:
179
+ return unit
180
+
181
+ # افتراضي إذا لم يتم العثور على وحدة مناسبة
182
+ return "وحدة"
modules/services/risk_analyzer.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ خدمة تحليل المخاطر في المستندات
3
+ """
4
+
5
+ import re
6
+ import pandas as pd
7
+ import numpy as np
8
+ from nltk.tokenize import sent_tokenize
9
+ import config
10
+
11
+ class RiskAnalyzer:
12
+ """تحليل المخاطر في المستندات"""
13
+
14
+ def __init__(self):
15
+ # قائمة بالمصطلحات التي تشير إلى المخاطر
16
+ self.risk_indicators = {
17
+ 'مخاطر مالية': [
18
+ 'غرامة', 'عقوبة', 'تعويض', 'دفعة', 'ضمان', 'تأخير', 'سعر',
19
+ 'تكلفة', 'زيادة', 'تمويل', 'استرداد', 'مصادرة', 'كفالة',
20
+ 'مستحقات', 'فاتورة', 'سداد', 'دفع', 'مطالبة', 'تقلبات'
21
+ ],
22
+ 'مخاطر زمنية': [
23
+ 'مدة', 'فترة', 'تاريخ', 'موعد', 'تأخير', 'جدول زمني', 'تمديد',
24
+ 'تسليم', 'تسريع', 'إنجاز', 'تنفيذ', 'انتهاء', 'بدء', 'تعليق'
25
+ ],
26
+ 'مخاطر فنية': [
27
+ 'مواصفات', 'معايير', 'اختبار', 'فحص', 'جودة', 'عيب', 'خلل',
28
+ 'تقنية', 'فني', 'تصميم', 'أداء', 'مخططات', 'تشغيل', 'صيانة'
29
+ ],
30
+ 'مخاطر إدارية': [
31
+ 'مراسلات', 'اجتماع', 'تنسيق', 'تواصل', 'إشراف', 'إدارة',
32
+ 'تغيير', 'تعديل', 'موافقة', 'رفض', 'تفويض', 'صلاحية'
33
+ ],
34
+ 'مخاطر تنظيمية': [
35
+ 'لائحة', 'تصريح', 'ترخيص', 'قانون', 'نظام', 'حكومي', 'بلدية',
36
+ 'تشريع', 'امتثال', 'تعميم', 'شهادة', 'موافقة'
37
+ ],
38
+ 'مخاطر سوقية': [
39
+ 'توريد', 'مورد', 'سوق', 'منافسة', 'مواد', 'نقص', 'تقلب', 'أسعار',
40
+ 'استيراد', 'تصدير', 'جمارك', 'نقل', 'تخزين'
41
+ ],
42
+ }
43
+
44
+ # قائمة بالمصطلحات التي تشير إلى تأثير المخاطر
45
+ self.impact_indicators = {
46
+ 'عالي': [
47
+ 'كبير', 'خطير', 'جسيم', 'كلي', 'مرتفع', 'عالي', 'ضخم', 'هام',
48
+ 'جوهري', 'أساسي', 'رئيسي'
49
+ ],
50
+ 'متوسط': [
51
+ 'متوسط', 'معتدل', 'وسط', 'مقبول', 'عادي', 'معقول'
52
+ ],
53
+ 'منخفض': [
54
+ 'صغير', 'قليل', 'ضئيل', 'بسيط', 'منخفض', 'هامشي', 'محدود',
55
+ 'طفيف', 'غير مؤثر'
56
+ ]
57
+ }
58
+
59
+ # قائمة بالمصطلحات التي تشير إلى احتمالية المخاطر
60
+ self.probability_indicators = {
61
+ 'مؤكد': [
62
+ 'مؤكد', 'حتمي', 'قطعي', 'دائماً', 'يجب', 'ملزم', 'إلزامي',
63
+ 'مطلوب'
64
+ ],
65
+ 'محتمل': [
66
+ 'محتمل', 'ممكن', 'قد', 'ربما', 'يمكن', 'متوقع'
67
+ ],
68
+ 'غير محتمل': [
69
+ 'نادر', 'بعيد', 'استثنائي', 'غير متوقع', 'غير محتمل', 'ضئيل'
70
+ ]
71
+ }
72
+
73
+ # استراتيجيات معالجة المخاطر
74
+ self.mitigation_strategies = {
75
+ 'مخاطر مالية': [
76
+ "تخصيص مبلغ احتياطي",
77
+ "التفاوض مع العميل لتخفيف الشروط المالية",
78
+ "تحديد سقف للغرامات",
79
+ "التخطيط للتدفق النقدي",
80
+ "تأمين خط ائتمان احتياطي"
81
+ ],
82
+ 'مخاطر زمنية': [
83
+ "زيادة فريق العمل",
84
+ "استخدام موارد إضافية",
85
+ "وضع خطة عمل بديلة",
86
+ "استباق التأخيرات المحتملة",
87
+ "تقديم طلب تمديد مسبق"
88
+ ],
89
+ 'مخاطر فنية': [
90
+ "طلب توضيح من العميل",
91
+ "استشارة خبراء متخصصين",
92
+ "إجراء اختبارات إضافية",
93
+ "توثيق المراسلات الفنية",
94
+ "تعيين مسؤول ضبط جودة"
95
+ ],
96
+ 'مخاطر إدارية': [
97
+ "تحسين آليات التواصل",
98
+ "توثيق جميع المراسلات",
99
+ "وضع خطة اتصال واضحة",
100
+ "عقد اجتماعات دورية",
101
+ "تعيين مدير مشروع متفرغ"
102
+ ],
103
+ 'مخاطر تنظيمية': [
104
+ "التخطيط المسبق للمتطلبات التنظيمية",
105
+ "التواصل مع الجهات المعنية",
106
+ "الاستعانة بمستشار قانوني",
107
+ "متابعة التغييرات التنظيمية",
108
+ "تجهيز الوثائق المطلوبة مبكراً"
109
+ ],
110
+ 'مخاطر سوقية': [
111
+ "تثبيت أسعار المواد مع الموردين",
112
+ "البحث عن موردين بدلاء",
113
+ "شراء المواد الرئيسية مبكراً",
114
+ "إبرام عقود توريد طويلة الأجل",
115
+ "مراقبة تقلبات السوق"
116
+ ]
117
+ }
118
+
119
+ def analyze_risks(self, text):
120
+ """تحليل المخاطر في النص المعطى"""
121
+ if not text:
122
+ return pd.DataFrame()
123
+
124
+ # تقسيم النص إلى جمل
125
+ sentences = sent_tokenize(text)
126
+
127
+ # تحليل المخاطر في كل جملة
128
+ risks = []
129
+ risk_id = 1
130
+
131
+ for sentence in sentences:
132
+ # تحديد نوع المخاطرة إذا وجدت
133
+ risk_category = self._determine_risk_category(sentence)
134
+
135
+ if risk_category:
136
+ # تحديد التأثير والاحتمالية
137
+ impact = self._determine_impact(sentence)
138
+ probability = self._determine_probability(sentence)
139
+
140
+ # اختيار استراتيجية المعالجة
141
+ mitigation = np.random.choice(self.mitigation_strategies.get(risk_category, ["مراجعة فريق المخاطر"]))
142
+
143
+ # إضافة المخاطرة إلى القائمة
144
+ risks.append({
145
+ 'رقم المخاطرة': f"R{risk_id:02d}",
146
+ 'وصف المخاطرة': sentence.strip(),
147
+ 'الفئة': risk_category,
148
+ 'التأثير': impact,
149
+ 'الاحتمالية': probability,
150
+ 'استراتيجية المعالجة': mitigation
151
+ })
152
+
153
+ risk_id += 1
154
+
155
+ # تحويل القائمة إلى DataFrame
156
+ risks_df = pd.DataFrame(risks)
157
+
158
+ # التأكد من وجود بيانات
159
+ if risks_df.empty:
160
+ # إنشاء DataFrame فارغ بالأعمدة المطلوبة
161
+ risks_df = pd.DataFrame(columns=[
162
+ 'رقم المخاطرة', 'وصف المخاطرة', 'الفئة',
163
+ 'التأثير', 'الاحتمالية', 'استراتيجية المعالجة'
164
+ ])
165
+
166
+ return risks_df
167
+
168
+ def _determine_risk_category(self, text):
169
+ """تحديد فئة المخاطرة بناءً على محتوى النص"""
170
+ # البحث عن الكلمات المفتاحية في النص
171
+ scores = {}
172
+
173
+ for category, indicators in self.risk_indicators.items():
174
+ score = sum(1 for indicator in indicators if indicator in text.lower())
175
+ scores[category] = score
176
+
177
+ # اختيار الفئة ذات الدرجة الأعلى إذا وجدت
178
+ if max(scores.values(), default=0) > 0:
179
+ return max(scores.items(), key=lambda x: x[1])[0]
180
+ else:
181
+ return None
182
+
183
+ def _determine_impact(self, text):
184
+ """تحديد تأثير المخاطرة بناءً على محتوى النص"""
185
+ # البحث عن الكلمات المفتاحية في النص
186
+ scores = {}
187
+
188
+ for impact, indicators in self.impact_indicators.items():
189
+ score = sum(1 for indicator in indicators if indicator in text.lower())
190
+ scores[impact] = score
191
+
192
+ # اختيار التأثير ذو الدرجة الأعلى
193
+ if max(scores.values(), default=0) > 0:
194
+ return max(scores.items(), key=lambda x: x[1])[0]
195
+ else:
196
+ # اختيار عشوائي مع ترجيح أكبر للتأثير المتوسط
197
+ return np.random.choice(
198
+ ["عالي", "متوسط", "منخفض"],
199
+ p=[0.3, 0.5, 0.2]
200
+ )
201
+
202
+ def _determine_probability(self, text):
203
+ """تحديد احتمالية المخاطرة بناءً على محتوى النص"""
204
+ # البحث عن الكلمات المفتاحية في النص
205
+ scores = {}
206
+
207
+ for probability, indicators in self.probability_indicators.items():
208
+ score = sum(1 for indicator in indicators if indicator in text.lower())
209
+ scores[probability] = score
210
+
211
+ # ا��تيار الاحتمالية ذات الدرجة الأعلى
212
+ if max(scores.values(), default=0) > 0:
213
+ return max(scores.items(), key=lambda x: x[1])[0]
214
+ else:
215
+ # اختيار عشوائي مع ترجيح أكبر للاحتمالية المتوسطة
216
+ return np.random.choice(
217
+ ["مؤكد", "محتمل", "غير محتمل"],
218
+ p=[0.2, 0.6, 0.2]
219
+ )
modules/services/specs_analyzer.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ خدمة تحليل المواصفات من المستندات
3
+ """
4
+
5
+ import re
6
+ import pandas as pd
7
+ import numpy as np
8
+ import nltk
9
+ from nltk.tokenize import sent_tokenize
10
+ import config
11
+
12
+ class SpecificationsAnalyzer:
13
+ """تحليل المواصفات الفنية في المستندات"""
14
+
15
+ def __init__(self):
16
+ # تحميل موارد NLTK إذا لم تكن موجودة
17
+ try:
18
+ nltk.data.find('tokenizers/punkt')
19
+ except LookupError:
20
+ nltk.download('punkt')
21
+
22
+ # فئات المواصفات الرئيسية
23
+ self.specification_categories = {
24
+ 'الخرسانة': [
25
+ 'خرسانة', 'اسمنت', 'رتبة', 'مقاومة', 'ضغط', 'شك', 'معالجة',
26
+ 'صب', 'قالب', 'قوالب', 'تسليح', 'خلطة', 'ركام', 'حصى'
27
+ ],
28
+ 'حديد التسليح': [
29
+ 'حديد', 'تسليح', 'قضبان', 'شد', 'جهد خضوع', 'درجة', 'قطر',
30
+ 'ربط', 'غطاء خرساني', 'تشكيل', 'ثني', 'شبكة'
31
+ ],
32
+ 'العزل المائي': [
33
+ 'عزل', 'مائي', 'رطوبة', 'بيتومين', 'لفائف', 'رولات', 'طبقة',
34
+ 'رش', 'تسرب', 'مانع تسرب', 'مقاومة الماء', 'حرارة'
35
+ ],
36
+ 'العزل الحراري': [
37
+ 'عزل', 'حراري', 'صوف صخري', 'صوف زجاجي', 'فوم', 'بوليسترين',
38
+ 'موصلية', 'انتقال الحرارة', 'بولي يوريثان'
39
+ ],
40
+ 'أعمال البلاط': [
41
+ 'بلاط', 'سيراميك', 'بورسلين', 'رخام', 'جرانيت', 'ترويبة',
42
+ 'لاصق', 'مونة', 'تركيب', 'مسافات', 'أبعاد'
43
+ ],
44
+ 'أعمال الدهان': [
45
+ 'دهان', 'طلاء', 'وجه تأسيس', 'وجه نهائي', 'رش', 'فرشاة',
46
+ 'رولة', 'معجون', 'مائي', 'زيتي', 'لامع', 'مطفي'
47
+ ],
48
+ 'المواد الكهربائية': [
49
+ 'كهرباء', 'أسلاك', 'كابلات', 'لوحات', 'مفاتيح', 'تمديدات',
50
+ 'جهد', 'قدرة', 'توزيع', 'تأريض', 'قواطع', 'تيار'
51
+ ],
52
+ 'أعمال السباكة': [
53
+ 'سباكة', 'مواسير', 'صرف', 'تغذية', 'مياه', 'بي في سي',
54
+ 'نحاس', 'حديد', 'خزان', 'مضخة', 'صمام', 'محبس'
55
+ ],
56
+ 'أعمال التكييف': [
57
+ 'تكييف', 'تبريد', 'تدفئة', 'مجاري هواء', 'دكت', 'مناولة',
58
+ 'تهوية', 'وحدة', 'مكيف', 'فلتر', 'مروحة'
59
+ ]
60
+ }
61
+
62
+ # المواصفات القياسية المعروفة
63
+ self.standard_specs = {
64
+ 'ASTM': {
65
+ 'C150': 'اسمنت بورتلاندي',
66
+ 'A615': 'حديد تسليح',
67
+ 'D6164': 'عزل مائي بيتوميني',
68
+ 'C33': 'ركام الخرسانة',
69
+ 'C494': 'إضافات الخرسانة',
70
+ 'C979': 'صبغات الخرسانة',
71
+ 'C578': 'عزل البوليسترين'
72
+ },
73
+ 'AASHTO': {
74
+ 'M85': 'اسمنت بورتلاندي',
75
+ 'M31': 'حديد تسليح',
76
+ 'M320': 'بيتومين للطرق'
77
+ },
78
+ 'IEC': {
79
+ '60502': 'كابلات الطاقة',
80
+ '60364': 'تمديدات كهربائية',
81
+ '61439': 'لوحات توزيع الطاقة'
82
+ },
83
+ 'BS': {
84
+ '8500': 'الخرسانة',
85
+ '4449': 'حديد التسليح',
86
+ '6700': 'أنظمة المياه',
87
+ '5950': 'المنشآت الفولاذية'
88
+ },
89
+ 'EN': {
90
+ '197-1': 'الاسمنت',
91
+ '10080': 'حديد التسليح',
92
+ '13162': 'العزل الحراري'
93
+ },
94
+ 'كود البناء السعودي': {
95
+ 'SBC 201': 'الأحمال',
96
+ 'SBC 304': 'الخرسانة الإنشائية',
97
+ 'SBC 305': 'المباني المعدنية',
98
+ 'SBC 501': 'السباكة',
99
+ 'SBC 401': 'الكهرباء',
100
+ 'SBC 601': 'البناء الصديق للبيئة'
101
+ }
102
+ }
103
+
104
+ def analyze_specifications(self, text):
105
+ """تحليل المواصفات الفنية من النص"""
106
+ if not text:
107
+ return {}, [], pd.DataFrame()
108
+
109
+ # تقسيم النص إلى جمل
110
+ sentences = sent_tokenize(text)
111
+
112
+ # استخراج المواصفات حسب الفئة
113
+ specs = {}
114
+ for category, keywords in self.specification_categories.items():
115
+ specs[category] = self._extract_category_specs(sentences, keywords, category)
116
+
117
+ # استخراج المتطلبات الخاصة
118
+ special_requirements = self._extract_special_requirements(sentences)
119
+
120
+ # استخراج متطلبات المحتوى المحلي
121
+ local_content = self._extract_local_content(sentences)
122
+
123
+ return specs, special_requirements, local_content
124
+
125
+ def _extract_category_specs(self, sentences, keywords, category):
126
+ """استخراج مواصفات فئة محددة من الجمل"""
127
+ category_specs = {}
128
+
129
+ # البحث عن الجمل التي تحتوي على الكلمات المفتاحية للفئة
130
+ category_sentences = [s for s in sentences if any(k in s.lower() for k in keywords)]
131
+
132
+ if not category_sentences:
133
+ return category_specs
134
+
135
+ # استخراج المواصفات حسب نوع الفئة
136
+ if category == 'الخرسانة':
137
+ # البحث عن قوة الضغط
138
+ for s in category_sentences:
139
+ if any(term in s.lower() for term in ['قوة', 'مقاومة', 'ضغط']):
140
+ match = re.search(r'(\d+)\s*(?:نيوتن|ميجا باسكال|نيوتن/مم²|MPa|N/mm)', s)
141
+ if match:
142
+ category_specs['قوة الضغط'] = f"{match.group(1)} نيوتن/مم²"
143
+
144
+ # البحث عن نسبة الماء للأسمنت
145
+ if any(term in s.lower() for term in ['نسبة', 'ماء', 'اسمنت']):
146
+ match = re.search(r'(\d+(?:\.\d+)?)\s*(?:%|نسبة)', s)
147
+ if match:
148
+ category_specs['نسبة الماء للأسمنت'] = f"{match.group(1)} كحد أقصى"
149
+
150
+ # البحث عن المعالجة
151
+ if 'معالجة' in s.lower():
152
+ match = re.search(r'(\d+)\s*(?:يوم|أيام)', s)
153
+ if match:
154
+ category_specs['المعالجة'] = f"لا تقل عن {match.group(1)} أيام"
155
+
156
+ # البحث عن المواصفات المرجعية
157
+ for std_org, std_codes in self.standard_specs.items():
158
+ for std_code, std_desc in std_codes.items():
159
+ if std_code in s and (std_org in s or category in std_desc.lower()):
160
+ category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
161
+
162
+ elif category == 'حديد التسليح':
163
+ # البحث عن نوع الحديد
164
+ for s in category_sentences:
165
+ if any(term in s.lower() for term in ['درجة', 'جهد', 'خضوع', 'grade']):
166
+ match = re.search(r'(?:درجة|جريد|Grade)\s*(\d+)', s, re.IGNORECASE)
167
+ if match:
168
+ category_specs['نوع الحديد'] = f"عالي المقاومة للشد (Grade {match.group(1)})"
169
+
170
+ # البحث عن إجهاد الخضوع
171
+ if any(term in s.lower() for term in ['إجهاد', 'خضوع', 'شد']):
172
+ match = re.search(r'(\d+)\s*(?:نيوتن|ميجا باسكال|نيوتن/مم²|MPa|N/mm)', s)
173
+ if match:
174
+ category_specs['إجهاد الخضوع'] = f"{match.group(1)} نيوتن/مم²"
175
+
176
+ # البحث عن المواصفات المرجعية
177
+ for std_org, std_codes in self.standard_specs.items():
178
+ for std_code, std_desc in std_codes.items():
179
+ if std_code in s and (std_org in s or category in std_desc.lower()):
180
+ category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
181
+
182
+ elif category == 'العزل المائي':
183
+ # البحث عن نوع العزل
184
+ for s in category_sentences:
185
+ if any(term in s.lower() for term in ['نوع', 'بيتومين', 'بوليستر', 'رول']):
186
+ if 'بيتومين' in s.lower() and 'بوليستر' in s.lower():
187
+ category_specs['النوع'] = 'أغشية بيتومينية مدعمة بالبوليستر'
188
+ elif 'بيتومين' in s.lower():
189
+ category_specs['النوع'] = 'أغشية بيتومينية'
190
+ elif 'pvc' in s.lower():
191
+ category_specs['النوع'] = 'أغشية PVC'
192
+
193
+ # البحث عن السماكة
194
+ if any(term in s.lower() for term in ['سماكة', 'سمك', 'مم']):
195
+ match = re.search(r'(\d+(?:\.\d+)?)\s*(?:مم|mm)', s, re.IGNORECASE)
196
+ if match:
197
+ category_specs['السماكة'] = f"{match.group(1)} مم"
198
+
199
+ # البحث عن مقاومة درجة الحرارة
200
+ if any(term in s.lower() for term in ['حرارة', 'درجة', 'مقاومة']):
201
+ match = re.search(r'(\d+)\s*(?:درجة|°)', s)
202
+ if match:
203
+ category_specs['مقاومة درجة الحرارة'] = f"حتى {match.group(1)} درجة مئوية"
204
+
205
+ # البحث عن المواصفات المرجعية
206
+ for std_org, std_codes in self.standard_specs.items():
207
+ for std_code, std_desc in std_codes.items():
208
+ if std_code in s and (std_org in s or 'عزل' in std_desc.lower()):
209
+ category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
210
+
211
+ elif category == 'المواد الكهربائية':
212
+ # البحث عن نوع الكابلات
213
+ for s in category_sentences:
214
+ if any(term in s.lower() for term in ['كابل', 'سلك', 'نحاس', 'ألمنيوم']):
215
+ if 'نحاس' in s.lower() and 'xlpe' in s.lower():
216
+ category_specs['الكابلات'] = 'نحاس معزول XLPE'
217
+ elif 'نحاس' in s.lower() and 'pvc' in s.lower():
218
+ category_specs['الكابلات'] = 'نحاس معزول PVC'
219
+ elif 'نحاس' in s.lower():
220
+ category_specs['الكابلات'] = 'نحاس معزول'
221
+ elif 'ألمنيوم' in s.lower():
222
+ category_specs['الكابلات'] = 'ألمنيوم معزول'
223
+
224
+ # البحث عن المواصفات المرجعية
225
+ for std_org, std_codes in self.standard_specs.items():
226
+ for std_code, std_desc in std_codes.items():
227
+ if std_code in s and (std_org in s or 'كهربا' in std_desc.lower()):
228
+ category_specs['المواصفات المرجعية'] = f"{std_org} {std_code}"
229
+
230
+ # إذا لم يتم العثور على مواصفات محددة، أضف مواصفات افتراضية للفئات الرئيسية
231
+ if not category_specs and category in ['الخرسانة', 'حديد التسليح', 'العزل المائي', 'المواد الكهربائية']:
232
+ if category == 'الخرسانة':
233
+ category_specs = {
234
+ 'قوة الضغط': '30 نيوتن/مم²',
235
+ 'نسبة الماء للأسمنت': '0.45 كحد أقصى',
236
+ 'المعالجة': 'لا تقل عن 7 أيام',
237
+ 'المواصفات المرجعية': 'ASTM C150'
238
+ }
239
+ elif category == 'حديد التسليح':
240
+ category_specs = {
241
+ 'نوع الحديد': 'عالي المقاومة للشد (Grade 60)',
242
+ 'إجهاد الخضوع': '420 نيوتن/مم²',
243
+ 'المواصفات المرجعية': 'ASTM A615'
244
+ }
245
+ elif category == 'العزل المائي':
246
+ category_specs = {
247
+ 'النوع': 'أغشية بيتومينية مدعمة بالبوليستر',
248
+ 'السماكة': '4 مم',
249
+ 'مقاومة درجة الحرارة': 'حتى 100 درجة مئوية',
250
+ 'المواصفات المرجعية': 'ASTM D6164'
251
+ }
252
+ elif category == 'المواد الكهربائية':
253
+ category_specs = {
254
+ 'الكابلات': 'نحاس معزول XLPE',
255
+ 'المواصفات المرجعية': 'IEC 60502'
256
+ }
257
+
258
+ return category_specs
259
+
260
+ def _extract_special_requirements(self, sentences):
261
+ """استخراج المتطلبات الخاصة من الجمل"""
262
+ special_requirements = []
263
+
264
+ # الكلمات المفتاحية التي تشير إلى متطلبات خاصة
265
+ special_keywords = [
266
+ 'يجب', 'ضرورة', 'يلزم', 'اشتراط', 'متطلب', 'إلزامي',
267
+ 'اعتماد', 'موافقة', 'تقديم', 'تأكيد', 'ضمان', 'توافق'
268
+ ]
269
+
270
+ # استخراج الجمل التي تحتوي على الكلمات المفتاحية
271
+ for s in sentences:
272
+ if any(keyword in s.lower() for keyword in special_keywords):
273
+ # تنظيف ��لجملة
274
+ req = s.strip()
275
+
276
+ # التأكد من أن الجملة تبدأ بيجب أو إذا لم تكن كذلك أضف "يجب" في البداية
277
+ if not any(req.startswith(start) for start in ['يجب', 'ضرورة', 'يلزم']):
278
+ req = f"يجب {req}"
279
+
280
+ # التأكد من أن الجملة تنتهي بنقطة
281
+ if not req.endswith('.'):
282
+ req = f"{req}."
283
+
284
+ # إضافة المتطلب إلى القائمة إذا لم يكن موجوداً بالفعل
285
+ if req not in special_requirements:
286
+ special_requirements.append(req)
287
+
288
+ # إضافة متطلبات افتراضية إذا لم يتم العثور على متطلبات
289
+ if not special_requirements:
290
+ special_requirements = [
291
+ "يجب أن تكون جميع المواد معتمدة من المهندس المشرف قبل التوريد.",
292
+ "يجب تقديم عينات لجميع المواد المستخدمة للاعتماد.",
293
+ "يجب تقديم شهادات ضمان لمدة سنة لجميع الأعمال المنفذة.",
294
+ "يجب الالتزام بكود البناء السعودي في جميع الأعمال.",
295
+ "يجب توفير اختبارات ضبط الجودة لأعمال الخرسانة.",
296
+ "يجب الالتزام بنسبة المحتوى المحلي لا تقل عن 70%."
297
+ ]
298
+
299
+ return special_requirements
300
+
301
+ def _extract_local_content(self, sentences):
302
+ """استخراج متطلبات المحتوى المحلي من الجمل"""
303
+ local_content_df = pd.DataFrame()
304
+
305
+ # الكلمات المفتاحية للمحتوى المحلي
306
+ lc_keywords = ['محتوى محلي', 'منتج وطني', 'صناعة محلية', 'توطين']
307
+
308
+ # استخراج الجمل التي تحتوي على كلمات مفتاحية للمحتوى المحلي
309
+ lc_sentences = [s for s in sentences if any(k in s.lower() for k in lc_keywords)]
310
+
311
+ # إذا وجدت جمل متعلقة بالمحتوى المحلي
312
+ if lc_sentences:
313
+ lc_data = []
314
+
315
+ # البحث عن نسب محددة في الجمل
316
+ for s in lc_sentences:
317
+ # البحث عن نسب مئوية
318
+ percentages = re.findall(r'(\d+)(?:\.\d+)?%', s)
319
+
320
+ if percentages:
321
+ # محاولة استخراج الفئة من الجملة
322
+ if 'عمال' in s.lower() or 'قوى' in s.lower() or 'موظف' in s.lower():
323
+ lc_data.append({
324
+ 'الفئة': 'القوى العاملة',
325
+ 'النسبة المطلوبة': f"{percentages[0]}%",
326
+ 'الملاحظات': 'تشمل العمالة والمهندسين والإداريين'
327
+ })
328
+ elif 'منتج' in s.lower() or 'صناع' in s.lower() or 'مواد' in s.lower() or 'معدات' in s.lower():
329
+ lc_data.append({
330
+ 'الفئة': 'المنتجات',
331
+ 'النسبة المطلوبة': f"{percentages[0]}%",
332
+ 'الملاحظات': 'تشمل المواد والمعدات المصنعة محلياً'
333
+ })
334
+ elif 'خدم' in s.lower() or 'نقل' in s.lower() or 'تأمين' in s.lower():
335
+ lc_data.append({
336
+ 'الفئة': 'الخدمات',
337
+ 'النسبة المطلوبة': f"{percentages[0]}%",
338
+ 'الملاحظات': 'تشمل خدمات النقل والتأمين والاستشارات'
339
+ })
340
+ else:
341
+ # إذا لم يتم تحديد الفئة، اعتبرها إجمالي
342
+ lc_data.append({
343
+ 'الفئة': 'إجمالي المشروع',
344
+ 'النسبة المطلوبة': f"{percentages[0]}%",
345
+ 'الملاحظات': 'نسبة المحتوى المحلي الإجمالية للمشروع'
346
+ })
347
+
348
+ # تحويل البيانات إلى DataFrame
349
+ if lc_data:
350
+ local_content_df = pd.DataFrame(lc_data)
351
+
352
+ # إذا لم يتم العثور على متطلبات محتوى محلي، استخدم بيانات افتراضية
353
+ if local_content_df.empty:
354
+ local_content_df = pd.DataFrame({
355
+ 'الفئة': ['القوى العاملة', 'المنتجات', 'الخدمات'],
356
+ 'النسبة المطلوبة': ['80%', '70%', '60%'],
357
+ 'الملاحظات': [
358
+ 'تشمل العمالة والمهندسين والإداريين',
359
+ 'تشمل المواد والمعدات المصنعة محلياً',
360
+ 'تشمل خدمات النقل والتأمين والاستشارات'
361
+ ]
362
+ })
363
+
364
+ return local_content_df
modules/services/text_extractor.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ خدمة استخراج النصوص من المستندات
3
+ """
4
+
5
+ import os
6
+ import PyPDF2
7
+ import docx
8
+ import pandas as pd
9
+ from pathlib import Path
10
+ import config
11
+
12
+ class TextExtractor:
13
+ """استخراج النصوص من المستندات المختلفة"""
14
+
15
+ def __init__(self):
16
+ pass
17
+
18
+ def extract_from_pdf(self, file_path):
19
+ """استخراج النص من ملف PDF"""
20
+ text = ""
21
+
22
+ try:
23
+ with open(file_path, 'rb') as file:
24
+ pdf_reader = PyPDF2.PdfReader(file)
25
+ for page_num in range(len(pdf_reader.pages)):
26
+ page = pdf_reader.pages[page_num]
27
+ text += page.extract_text() + "\n\n"
28
+ except Exception as e:
29
+ print(f"خطأ في استخراج النص من PDF: {str(e)}")
30
+ return ""
31
+
32
+ return text
33
+
34
+ def extract_from_docx(self, file_path):
35
+ """استخراج النص من ملف Word"""
36
+ text = ""
37
+
38
+ try:
39
+ doc = docx.Document(file_path)
40
+ for para in doc.paragraphs:
41
+ text += para.text + "\n"
42
+ except Exception as e:
43
+ print(f"خطأ في استخراج النص من DOCX: {str(e)}")
44
+ return ""
45
+
46
+ return text
47
+
48
+ def extract_from_excel(self, file_path):
49
+ """استخراج البيانات من ملف Excel"""
50
+ try:
51
+ # قراءة جميع الصفحات
52
+ excel_data = pd.read_excel(file_path, sheet_name=None)
53
+
54
+ # تجميع البيانات من جميع الصفحات
55
+ text = ""
56
+ for sheet_name, sheet_data in excel_data.items():
57
+ text += f"صفحة: {sheet_name}\n"
58
+ text += sheet_data.to_string(index=False) + "\n\n"
59
+ except Exception as e:
60
+ print(f"خطأ في استخراج النص من Excel: {str(e)}")
61
+ return ""
62
+
63
+ return text
64
+
65
+ def extract_from_text(self, file_path):
66
+ """استخراج النص من ملف نصي"""
67
+ try:
68
+ with open(file_path, 'r', encoding='utf-8') as file:
69
+ text = file.read()
70
+ except Exception as e:
71
+ print(f"خطأ في استخراج النص من الملف النصي: {str(e)}")
72
+ return ""
73
+
74
+ return text
75
+
76
+ def extract_text(self, file_path):
77
+ """استخراج النص من أي نوع ملف مدعوم"""
78
+ file_ext = Path(file_path).suffix.lower()
79
+
80
+ if file_ext == '.pdf':
81
+ return self.extract_from_pdf(file_path)
82
+ elif file_ext in ['.docx', '.doc']:
83
+ return self.extract_from_docx(file_path)
84
+ elif file_ext in ['.xlsx', '.xls']:
85
+ return self.extract_from_excel(file_path)
86
+ elif file_ext == '.txt':
87
+ return self.extract_from_text(file_path)
88
+ else:
89
+ print(f"نوع الملف غير مدعوم: {file_ext}")
90
+ return ""
modules/translation/translation_app.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ وحدة الترجمة المتكاملة
3
+ """
4
+
5
+ import streamlit as st
6
+ import pandas as pd
7
+ import random
8
+
9
+ class TranslationApp:
10
+ """
11
+ وحدة الترجمة المتكاملة للنظام
12
+ """
13
+
14
+ def __init__(self):
15
+ """
16
+ تهيئة وحدة الترجمة
17
+ """
18
+ # تهيئة حالة الجلسة الخاصة بالترجمة إذا لم تكن موجودة
19
+ if 'translation_history' not in st.session_state:
20
+ # إنشاء بيانات تجريبية لسجل الترجمة
21
+ st.session_state.translation_history = self._generate_sample_history()
22
+
23
+ if 'translation_settings' not in st.session_state:
24
+ # إعدادات الترجمة الافتراضية
25
+ st.session_state.translation_settings = {
26
+ 'default_source': 'ar',
27
+ 'default_target': 'en',
28
+ 'auto_translate': True,
29
+ 'save_history': True,
30
+ 'use_glossary': True
31
+ }
32
+
33
+ def run(self):
34
+ """
35
+ تشغيل وحدة الترجمة
36
+ """
37
+ st.markdown("<h2 class='module-title'>وحدة الترجمة المتكاملة</h2>", unsafe_allow_html=True)
38
+
39
+ # إنشاء تبويبات للترجمة المختلفة
40
+ tabs = st.tabs(["ترجمة النصوص", "ترجمة المستندات", "المصطلحات والقاموس", "الإعدادات"])
41
+
42
+ with tabs[0]:
43
+ self._render_text_translation()
44
+
45
+ with tabs[1]:
46
+ self._render_document_translation()
47
+
48
+ with tabs[2]:
49
+ self._render_glossary()
50
+
51
+ with tabs[3]:
52
+ self._render_translation_settings()
53
+
54
+ def _render_text_translation(self):
55
+ """
56
+ عرض واجهة ترجمة النصوص
57
+ """
58
+ st.markdown("### ترجمة النصوص")
59
+ st.markdown("ترجمة النصوص بين اللغات المختلفة مع دعم المصطلحات التقنية")
60
+
61
+ # اختيار اللغات
62
+ col1, col2 = st.columns(2)
63
+
64
+ with col1:
65
+ source_lang = st.selectbox(
66
+ "اللغة المصدر",
67
+ options=["العربية", "الإنجليزية", "الفرنسية", "الإسبانية", "الألمانية"],
68
+ index=0,
69
+ key="source_lang"
70
+ )
71
+
72
+ with col2:
73
+ target_lang = st.selectbox(
74
+ "اللغة الهدف",
75
+ options=["الإنجليزية", "العربية", "الفرنسية", "الإسبانية", "الألمانية"],
76
+ index=0,
77
+ key="target_lang"
78
+ )
79
+
80
+ # زر تبديل اللغات
81
+ col1, col2, col3 = st.columns([1, 1, 4])
82
+ with col2:
83
+ if st.button("تبديل اللغات", key="switch_langs"):
84
+ st.session_state.source_lang, st.session_state.target_lang = st.session_state.target_lang, st.session_state.source_lang
85
+ st.rerun()
86
+
87
+ # مربعات النص
88
+ source_text = st.text_area(
89
+ "النص المصدر",
90
+ height=150,
91
+ placeholder="أدخل النص المراد ترجمته هنا...",
92
+ key="source_text"
93
+ )
94
+
95
+ # زر الترجمة
96
+ if st.button("ترجمة", key="translate_btn"):
97
+ if source_text:
98
+ # محاكاة عملية الترجمة
99
+ translated_text = self._mock_translation(source_text, source_lang, target_lang)
100
+
101
+ # عرض النص المترجم
102
+ st.text_area(
103
+ "النص المترجم",
104
+ value=translated_text,
105
+ height=150,
106
+ key="translated_text"
107
+ )
108
+
109
+ # إضافة الترجمة إلى السجل
110
+ if st.session_state.translation_settings['save_history']:
111
+ new_entry = {
112
+ 'id': len(st.session_state.translation_history) + 1,
113
+ 'source_lang': source_lang,
114
+ 'target_lang': target_lang,
115
+ 'source_text': source_text[:50] + ('...' if len(source_text) > 50 else ''),
116
+ 'date': pd.Timestamp.now().strftime("%Y-%m-%d %H:%M")
117
+ }
118
+ st.session_state.translation_history.insert(0, new_entry)
119
+ else:
120
+ st.warning("يرجى إدخال نص للترجمة", icon="⚠️")
121
+
122
+ # عرض سجل الترجمات الأخيرة
123
+ if st.session_state.translation_history:
124
+ st.markdown("### آخر الترجمات")
125
+
126
+ # عرض آخر 5 ترجمات
127
+ recent_translations = st.session_state.translation_history[:5]
128
+ for i, entry in enumerate(recent_translations):
129
+ with st.expander(f"{entry['source_lang']} إلى {entry['target_lang']} - {entry['date']}"):
130
+ st.markdown(f"**النص المصدر:** {entry['source_text']}")
131
+
132
+ def _render_document_translation(self):
133
+ """
134
+ عرض واجهة ترجمة المستندات
135
+ """
136
+ st.markdown("### ترجمة المستندات")
137
+ st.markdown("ترجمة المستندات بتنسيقات مختلفة مع الحفاظ على التنسيق الأصلي")
138
+
139
+ # اختيار اللغات
140
+ col1, col2 = st.columns(2)
141
+
142
+ with col1:
143
+ doc_source_lang = st.selectbox(
144
+ "اللغة المصدر",
145
+ options=["العربية", "الإنجليزية", "الفرنسية", "الإسبانية", "الألمانية"],
146
+ index=0,
147
+ key="doc_source_lang"
148
+ )
149
+
150
+ with col2:
151
+ doc_target_lang = st.selectbox(
152
+ "اللغة الهدف",
153
+ options=["الإنجليزية", "العربية", "الفرنسية", "الإسبانية", "الألمانية"],
154
+ index=0,
155
+ key="doc_target_lang"
156
+ )
157
+
158
+ # رفع الملف
159
+ uploaded_file = st.file_uploader(
160
+ "اختر ملفاً للترجمة",
161
+ type=["pdf", "docx", "xlsx", "pptx", "txt"],
162
+ key="doc_upload"
163
+ )
164
+
165
+ # خيارات الترجمة
166
+ st.markdown("#### خيارات الترجمة")
167
+
168
+ col1, col2 = st.columns(2)
169
+
170
+ with col1:
171
+ st.checkbox("الحفاظ على التنسيق الأصلي", value=True, key="preserve_format")
172
+ st.checkbox("ترجمة الجداول", value=True, key="translate_tables")
173
+
174
+ with col2:
175
+ st.checkbox("ترجمة النصوص في الصور", value=False, key="translate_images")
176
+ st.checkbox("استخدام المصطلحات المخصصة", value=True, key="use_custom_terms")
177
+
178
+ # زر الترجمة
179
+ if st.button("ترجمة المستند", key="translate_doc_btn"):
180
+ if uploaded_file is not None:
181
+ # محاكاة عملية الترجمة
182
+ with st.spinner("جاري ترجمة المستند..."):
183
+ # محاكاة وقت المعالجة
184
+ import time
185
+ time.sleep(2)
186
+
187
+ st.success("تمت ترجمة المستند بنجاح!", icon="✅")
188
+
189
+ # زر تنزيل الملف المترجم (وهمي)
190
+ st.download_button(
191
+ label="تنزيل المستند المترجم",
192
+ data=b"محتوى وهمي للملف المترجم",
193
+ file_name=f"translated_{uploaded_file.name}",
194
+ mime="application/octet-stream",
195
+ key="download_translated_doc"
196
+ )
197
+ else:
198
+ st.warning("يرجى رفع ملف للترجمة", icon="⚠️")
199
+
200
+ def _render_glossary(self):
201
+ """
202
+ عرض واجهة المصطلحات والقاموس
203
+ """
204
+ st.markdown("### المصطلحات والقاموس")
205
+ st.markdown("إدارة المصطلحات التقنية والقاموس المخصص للمشاريع")
206
+
207
+ # إنشاء بيانات تجريبية للمصطلحات
208
+ glossary_terms = [
209
+ {"term_ar": "مناقصة", "term_en": "Tender", "domain": "عقود"},
210
+ {"term_ar": "عطاء", "term_en": "Bid", "domain": "عقود"},
211
+ {"term_ar": "ضمان ابتدائي", "term_en": "Bid Bond", "domain": "عقود"},
212
+ {"term_ar": "جدول الكميات", "term_en": "Bill of Quantities", "domain": "هندسة"},
213
+ {"term_ar": "مواصفات فنية", "term_en": "Technical Specifications", "domain": "هندسة"},
214
+ {"term_ar": "شروط تعاقدية", "term_en": "Contractual Terms", "domain": "قانوني"},
215
+ {"term_ar": "غرامة تأخير", "term_en": "Delay Penalty", "domain": "قانوني"},
216
+ {"term_ar": "تحليل الأسعار", "term_en": "Price Analysis", "domain": "مالي"},
217
+ {"term_ar": "تدفق نقدي", "term_en": "Cash Flow", "domain": "مالي"},
218
+ {"term_ar": "تقييم المخاطر", "term_en": "Risk Assessment", "domain": "إدارة"}
219
+ ]
220
+
221
+ # تبويبات فرعية للمصطلحات
222
+ subtabs = st.tabs(["عرض المصطلحات", "إضافة مصطلح", "استيراد/تصدير"])
223
+
224
+ with subtabs[0]:
225
+ # فلترة المصطلحات
226
+ domain_filter = st.multiselect(
227
+ "تصفية حسب المجال",
228
+ options=["الكل", "عقود", "هندسة", "قانوني", "مالي", "إدارة"],
229
+ default=["الكل"],
230
+ key="domain_filter"
231
+ )
232
+
233
+ # تطبيق التصفية
234
+ filtered_terms = glossary_terms
235
+ if "الكل" not in domain_filter:
236
+ filtered_terms = [term for term in glossary_terms if term['domain'] in domain_filter]
237
+
238
+ # عرض المصطلحات
239
+ if filtered_terms:
240
+ df = pd.DataFrame(filtered_terms)
241
+ df.columns = ["المصطلح (عربي)", "المصطلح (إنجليزي)", "المجال"]
242
+ st.dataframe(df, use_container_width=True)
243
+ else:
244
+ st.info("لا توجد مصطلحات تطابق معايير التصفية", icon="ℹ️")
245
+
246
+ with subtabs[1]:
247
+ # نموذج إضافة مصطلح جديد
248
+ st.markdown("#### إضافة مصطلح جديد")
249
+
250
+ col1, col2 = st.columns(2)
251
+
252
+ with col1:
253
+ new_term_ar = st.text_input("المصطلح بالعربية", key="new_term_ar")
254
+
255
+ with col2:
256
+ new_term_en = st.text_input("المصطلح بالإنجليزية", key="new_term_en")
257
+
258
+ new_term_domain = st.selectbox(
259
+ "المجال",
260
+ options=["عقود", "هندسة", "قانوني", "مالي", "إدارة", "أخرى"],
261
+ key="new_term_domain"
262
+ )
263
+
264
+ new_term_desc = st.text_area("الوصف (اختياري)", key="new_term_desc")
265
+
266
+ if st.button("إضافة المصطلح", key="add_term_btn"):
267
+ if new_term_ar and new_term_en:
268
+ st.success("تمت إضافة المصطلح بنجاح", icon="✅")
269
+ else:
270
+ st.warning("يرجى إدخال المصطلح باللغتين العربية والإنجليزية", icon="⚠️")
271
+
272
+ with subtabs[2]:
273
+ # استيراد وتصدير المصطلحات
274
+ st.markdown("#### استيراد وتصدير المصطلحات")
275
+
276
+ col1, col2 = st.columns(2)
277
+
278
+ with col1:
279
+ st.markdown("##### استيراد المصطلحات")
280
+ st.file_uploader("اختر ملف المصطلحات (CSV, Excel)", type=["csv", "xlsx"], key="import_glossary")
281
+ st.button("استيراد", key="import_btn")
282
+
283
+ with col2:
284
+ st.markdown("##### تصدير المصطلحات")
285
+ export_format = st.radio(
286
+ "صيغة التصدير",
287
+ options=["CSV", "Excel", "PDF"],
288
+ horizontal=True,
289
+ key="export_format"
290
+ )
291
+
292
+ if st.button("تصدير", key="export_btn"):
293
+ st.success("تم تصدير المصطلحات بنجاح", icon="✅")
294
+
295
+ # زر تنزيل الملف المصدر (وهمي)
296
+ st.download_button(
297
+ label="تنزيل ملف المصطلحات",
298
+ data=b"محتوى وهمي لملف المصطلحات",
299
+ file_name=f"glossary_terms.{export_format.lower()}",
300
+ mime="application/octet-stream",
301
+ key="download_glossary"
302
+ )
303
+
304
+ def _render_translation_settings(self):
305
+ """
306
+ عرض إعدادات الترجمة
307
+ """
308
+ st.markdown("### إعدادات الترجمة")
309
+ st.markdown("تخصيص إعدادات وحدة الترجمة")
310
+
311
+ settings = st.session_state.translation_settings
312
+
313
+ # اللغات الافتراضية
314
+ st.markdown("#### اللغات الافتراضية")
315
+
316
+ col1, col2 = st.columns(2)
317
+
318
+ with col1:
319
+ default_source = st.selectbox(
320
+ "اللغة المصدر الافتراضية",
321
+ options=["العربية", "الإنجليزية", "الفرنسية", "الإسبانية", "الألمانية"],
322
+ index=0,
323
+ key="default_source"
324
+ )
325
+
326
+ with col2:
327
+ default_target = st.selectbox(
328
+ "اللغة الهدف الافتراضية",
329
+ options=["الإنجليزية", "العربية", "الفرنسية", "الإسبانية", "الألمانية"],
330
+ index=0,
331
+ key="default_target"
332
+ )
333
+
334
+ # خيارات عامة
335
+ st.markdown("#### خيارات عامة")
336
+
337
+ auto_translate = st.checkbox(
338
+ "ترجمة تلقائية أثناء الكتابة",
339
+ value=settings['auto_translate'],
340
+ key="auto_translate"
341
+ )
342
+
343
+ save_history = st.checkbox(
344
+ "حفظ سجل الترجمات",
345
+ value=settings['save_history'],
346
+ key="save_history"
347
+ )
348
+
349
+ use_glossary = st.checkbox(
350
+ "استخدام المصطلحات المخصصة",
351
+ value=settings['use_glossary'],
352
+ key="use_glossary"
353
+ )
354
+
355
+ # خيارات متقدمة
356
+ st.markdown("#### خيارات متقدمة")
357
+
358
+ translation_model = st.selectbox(
359
+ "نموذج الترجمة",
360
+ options=["نموذج عام", "نموذج متخصص للعقود", "نموذج متخصص للهندسة"],
361
+ key="translation_model"
362
+ )
363
+
364
+ # زر حفظ الإعدادات
365
+ if st.button("حفظ الإعدادات", key="save_translation_settings"):
366
+ st.session_state.translation_settings = {
367
+ 'default_source': default_source,
368
+ 'default_target': default_target,
369
+ 'auto_translate': auto_translate,
370
+ 'save_history': save_history,
371
+ 'use_glossary': use_glossary
372
+ }
373
+ st.success("تم حفظ الإعدادات بنجاح", icon="✅")
374
+
375
+ def _mock_translation(self, text, source_lang, target_lang):
376
+ """
377
+ محاكاة عملية الترجمة (للعرض فقط)
378
+ """
379
+ # قاموس بسيط للترجمة
380
+ translations = {
381
+ "مناقصة": "Tender",
382
+ "عطاء": "Bid",
383
+ "مشروع": "Project",
384
+ "عقد": "Contract",
385
+ "تسعير": "Pricing",
386
+ "تحليل": "Analysis",
387
+ "وثائق": "Documents",
388
+ "شركة": "Company",
389
+ "مقاول": "Contractor",
390
+ "مالك": "Owner",
391
+ "Tender": "مناقصة",
392
+ "Bid": "عطاء",
393
+ "Project": "مشروع",
394
+ "Contract": "عقد",
395
+ "Pricing": "تسعير",
396
+ "Analysis": "تحليل",
397
+ "Documents": "وثائق",
398
+ "Company": "شركة",
399
+ "Contractor": "مقاول",
400
+ "Owner": "مالك"
401
+ }
402
+
403
+ # محاكاة بسيطة للترجمة
404
+ if source_lang == "العربية" and target_lang == "الإنجليزية":
405
+ for ar, en in translations.items():
406
+ text = text.replace(ar, en)
407
+ return text
408
+ elif source_lang == "الإنجليزية" and target_lang == "العربية":
409
+ for en, ar in translations.items():
410
+ text = text.replace(en, ar)
411
+ return text
412
+ else:
413
+ # للغات الأخرى، نعيد النص كما هو مع إضافة ملاحظة
414
+ return text + "\n\n[محاكاة للترجمة - هذه ليست ترجمة حقيقية]"
415
+
416
+ def _generate_sample_history(self):
417
+ """
418
+ إنشاء بيانات تجريبية لسجل الترجمة
419
+ """
420
+ sample_texts = [
421
+ "تحليل وثائق المناقصة للمشروع",
422
+ "إعداد جدول الكميات والأسعار",
423
+ "مراجعة الشروط التعاقدية للمشروع",
424
+ "تقييم المخاطر المحتملة للمشروع",
425
+ "إعداد العرض الفني والمالي",
426
+ "Analysis of tender documents for the project",
427
+ "Preparation of bill of quantities and prices",
428
+ "Review of contractual terms for the project",
429
+ "Assessment of potential project risks",
430
+ "Preparation of technical and financial offer"
431
+ ]
432
+
433
+ languages = ["العربية", "الإنجليزية"]
434
+
435
+ history = []
436
+ for i in range(10):
437
+ source_lang = random.choice(languages)
438
+ target_lang = "الإنجليزية" if source_lang == "العربية" else "العربية"
439
+
440
+ # اختيار نص مناسب للغة المصدر
441
+ if source_lang == "العربية":
442
+ text_index = random.randint(0, 4)
443
+ else:
444
+ text_index = random.randint(5, 9)
445
+
446
+ # إنشاء تاريخ عشوائي خلال الشهر الماضي
447
+ days_ago = random.randint(0, 30)
448
+ entry_date = (pd.Timestamp.now() - pd.Timedelta(days=days_ago)).strftime("%Y-%m-%d %H:%M")
449
+
450
+ entry = {
451
+ 'id': i + 1,
452
+ 'source_lang': source_lang,
453
+ 'target_lang': target_lang,
454
+ 'source_text': sample_texts[text_index],
455
+ 'date': entry_date
456
+ }
457
+
458
+ history.append(entry)
459
+
460
+ # ترتيب السجل حسب التاريخ (الأحدث أولاً)
461
+ history.sort(key=lambda x: x['date'], reverse=True)
462
+
463
+ return history
packages.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ libgl1-mesa-glx
2
+ poppler-utils
3
+ tesseract-ocr
4
+ libtesseract-dev
5
+ tesseract-ocr-ara
6
+ tesseract-ocr-eng
7
+ fonts-freefont-ttf
pyproject.toml ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "document-analysis-app"
7
+ version = "0.1.0"
8
+ description = "تطبيق تحليل المستندات باستخدام Docling و MLX VLM"
9
+ requires-python = ">=3.8"
10
+ dependencies = [
11
+ # الاعتماديات الأساسية
12
+ "streamlit==1.32.0",
13
+ "pandas==2.2.0",
14
+ "numpy==1.26.3",
15
+ "matplotlib==3.8.2",
16
+ "seaborn==0.13.1",
17
+ "plotly==5.18.0",
18
+
19
+ # معالجة البيانات
20
+ "openpyxl==3.1.2",
21
+ "xlrd==2.0.1",
22
+ "xlsxwriter==3.1.9",
23
+ "pyarrow==14.0.1",
24
+
25
+ # تحليل المستندات
26
+ "PyPDF2==3.0.1",
27
+ "python-docx==1.1.0",
28
+ "pdf2image==1.17.0",
29
+ "pytesseract==0.3.10",
30
+ "pymupdf==1.23.7",
31
+ "pdfplumber==0.10.3",
32
+ "opencv-python-headless==4.8.1.78",
33
+
34
+ # معالجة اللغة العربية
35
+ "arabic-reshaper==3.0.0",
36
+ "python-bidi==0.4.2",
37
+ "langdetect==1.0.9",
38
+ "farasapy==0.0.14",
39
+
40
+ # الذكاء الاصطناعي والتعلم الآلي
41
+ "scikit-learn==1.4.0",
42
+ "transformers>=4.49.0", # تم تحديث الإصدار ليتوافق مع mlx-vlm
43
+ "torch==2.1.2",
44
+ "nltk==3.8.1",
45
+ "gensim==4.3.2",
46
+
47
+ # قواعد البيانات
48
+ "SQLAlchemy==2.0.25",
49
+ "SQLAlchemy-Utils==0.41.1",
50
+ "alembic==1.13.1",
51
+ "sqlite-utils==3.35.1",
52
+
53
+ # مكونات واجهة المستخدم
54
+ "streamlit-option-menu==0.3.2",
55
+ "streamlit-elements==0.1.0",
56
+ "streamlit-aggrid==0.3.4.post3",
57
+ "streamlit-authenticator==0.2.3",
58
+ "streamlit-extras==0.3.5",
59
+ "streamlit-image-coordinates==0.1.6",
60
+
61
+ # أدوات وتبعيات إضافية
62
+ "pycountry==23.12.11",
63
+ "watchdog==3.0.0",
64
+ "python-dateutil==2.8.2",
65
+ "python-dotenv==1.0.0",
66
+ "requests==2.31.0",
67
+ "tqdm>=4.66.2",
68
+ "joblib==1.3.2",
69
+ "ipython==8.20.0",
70
+
71
+ # مكتبات Docling و MLX VLM للتحليل المتقدم
72
+ "docling-core>=0.1.0",
73
+ "mlx-vlm>=0.1.0",
74
+ "mlx>=0.0.4",
75
+ "pillow>=10.3.0", # تم تحديث الإصدار ليتوافق مع mlx-vlm
76
+ "protobuf>=3.19.0,<4.0.0",
77
+ ]
78
+
79
+ [project.optional-dependencies]
80
+ dev = [
81
+ "pytest>=6.0",
82
+ "black>=22.1.0",
83
+ "flake8>=4.0.0",
84
+ ]
85
+
86
+ [tool.setuptools]
87
+ packages = ["modules"]
88
+
89
+ # متطلبات النموذج
90
+ [tool.script]
91
+ requires-python = ">=3.8"
92
+ dependencies = [
93
+ "docling-core",
94
+ "mlx-vlm",
95
+ "pillow>=10.3.0",
96
+ "tqdm>=4.66.2",
97
+ "transformers>=4.49.0"
98
+ ]
requirements.txt ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # الاعتماديات الأساسية
2
+ streamlit==1.32.0
3
+ pandas==2.2.0
4
+ numpy==1.26.3
5
+ matplotlib==3.8.2
6
+ seaborn==0.13.1
7
+ plotly==5.18.0
8
+
9
+ # معالجة البيانات
10
+ openpyxl==3.1.2
11
+ xlrd==2.0.1
12
+ xlsxwriter==3.1.9
13
+ pyarrow==14.0.1
14
+
15
+ # تحليل المستندات
16
+ PyPDF2==3.0.1
17
+ python-docx==1.1.0
18
+ pdf2image==1.17.0
19
+ pytesseract==0.3.10
20
+ pymupdf==1.23.7
21
+ pdfplumber==0.10.3
22
+ opencv-python-headless==4.8.1.78
23
+ # poppler-utils ← يُثبت من apt
24
+
25
+ # معالجة اللغة العربية
26
+ arabic-reshaper==3.0.0
27
+ python-bidi==0.4.2
28
+ langdetect==1.0.9
29
+ farasapy==0.0.14
30
+ # cameltools==1.1.0
31
+
32
+ # الذكاء الاصطناعي والتعلم الآلي
33
+ scikit-learn==1.4.0
34
+ transformers==4.39.3
35
+ torch==2.1.2
36
+ nltk==3.8.1
37
+ gensim==4.3.2
38
+ openai==1.69.0
39
+ anthropic==0.5.0
40
+ pydantic==2.3.0
41
+ joblib==1.3.2
42
+
43
+ # قواعد البيانات
44
+ SQLAlchemy==2.0.25
45
+ SQLAlchemy-Utils==0.41.1
46
+ alembic==1.13.1
47
+ sqlite-utils==3.35.1
48
+
49
+ # مكونات واجهة المستخدم
50
+ streamlit-option-menu==0.3.2
51
+ streamlit-elements==0.1.0
52
+ streamlit-aggrid==0.3.4.post3
53
+ streamlit-authenticator==0.2.3
54
+ streamlit-extras==0.3.5
55
+ streamlit-echarts==0.4.0
56
+ streamlit-image-coordinates==0.1.6
57
+
58
+ # أدوات وتبعيات إضافية
59
+ pycountry==23.12.11
60
+ watchdog==3.0