EGYADMIN commited on
Commit
4d074e6
·
verified ·
1 Parent(s): 08e4229

Create analysis/local_content_analyzer.py

Browse files
Files changed (1) hide show
  1. analysis/local_content_analyzer.py +1293 -0
analysis/local_content_analyzer.py ADDED
@@ -0,0 +1,1293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ نسبة توفر المادة محلياً (0 إلى 100)
2
+ """
3
+ # البحث في قاعدة بيانات المواد المحلية
4
+ material_name = material_name.lower()
5
+
6
+ # مطابقة الاسم بالكامل
7
+ for local_material in self.local_materials_db:
8
+ if local_material.get('name', '').lower() == material_name:
9
+ return local_material.get('availability_percentage', 100.0)
10
+
11
+ # مطابقة جزئية
12
+ matches = []
13
+ for local_material in self.local_materials_db:
14
+ local_name = local_material.get('name', '').lower()
15
+ # حساب درجة التشابه البسيط
16
+ similarity = self._simple_similarity(material_name, local_name)
17
+ if similarity > 0.7: # عتبة التشابه
18
+ matches.append((local_material, similarity))
19
+
20
+ if matches:
21
+ # ترتيب المطابقات حسب درجة التشابه
22
+ matches.sort(key=lambda x: x[1], reverse=True)
23
+ return matches[0][0].get('availability_percentage', 0.0)
24
+
25
+ # إذا لم يتم العثور على مطابقات، نفترض نسبة منخفضة
26
+ return 20.0 # قيمة افتراضية منخفضة
27
+
28
+ def _simple_similarity(self, str1: str, str2: str) -> float:
29
+ """
30
+ حساب درجة التشابه البسيط بين سلسلتين
31
+
32
+ المعاملات:
33
+ ----------
34
+ str1 : str
35
+ السلسلة الأولى
36
+ str2 : str
37
+ السلسلة الثانية
38
+
39
+ المخرجات:
40
+ --------
41
+ float
42
+ درجة التشابه (0 إلى 1)
43
+ """
44
+ # تنظيف السلاسل
45
+ str1 = str1.strip().lower()
46
+ str2 = str2.strip().lower()
47
+
48
+ # تحويل السلاسل إلى مجموعات من الكلمات
49
+ words1 = set(str1.split())
50
+ words2 = set(str2.split())
51
+
52
+ # حساب معامل جاكارد
53
+ intersection = len(words1.intersection(words2))
54
+ union = len(words1.union(words2))
55
+
56
+ if union == 0:
57
+ return 0.0
58
+
59
+ return intersection / union
60
+
61
+ def _extract_tables(self, text: str) -> List[str]:
62
+ """
63
+ استخراج أقسام الجداول من النص
64
+
65
+ المعاملات:
66
+ ----------
67
+ text : str
68
+ النص المستخرج من المناقصة
69
+
70
+ المخرجات:
71
+ --------
72
+ List[str]
73
+ قائمة بأقسام الجداول
74
+ """
75
+ # بحث بسيط عن أقسام الجداول
76
+ # البحث عن أقسام تبدأ بعناوين مثل "جدول المواصفات" أو "قائمة الكميات"
77
+ table_headers = [
78
+ "جدول المواصفات",
79
+ "جدول الكميات",
80
+ "قائمة المواد",
81
+ "قائمة الكميات",
82
+ "جدول المنتجات",
83
+ "المواد المطلوبة",
84
+ "قائمة البنود",
85
+ "بنود المناقصة"
86
+ ]
87
+
88
+ tables = []
89
+ for header in table_headers:
90
+ # البحث عن أقسام تبدأ بالعنوان المحدد
91
+ matches = re.finditer(f"{header}.*?(?=\n\n|\Z)", text, re.DOTALL)
92
+ for match in matches:
93
+ table_section = match.group(0)
94
+ if len(table_section.split('\n')) > 2: # التأكد من أن القسم يحتوي على أكثر من سطرين
95
+ tables.append(table_section)
96
+
97
+ # البحث عن أنماط الجداول (أسطر تحتوي على عدة عناصر مفصولة بـ | أو مسافات متعددة)
98
+ table_pattern = r'(?:\n|^)((?:[^\n]+\|[^\n]+\|[^\n]+\n){3,})'
99
+ table_matches = re.finditer(table_pattern, text)
100
+ for match in table_matches:
101
+ tables.append(match.group(1))
102
+
103
+ return tables
104
+
105
+ def _parse_table_for_materials(self, table_text: str) -> List[Dict[str, Any]]:
106
+ """
107
+ تحليل نص الجدول لاستخراج المواد
108
+
109
+ المعاملات:
110
+ ----------
111
+ table_text : str
112
+ نص الجدول
113
+
114
+ المخرجات:
115
+ --------
116
+ List[Dict[str, Any]]
117
+ قائمة بالمواد المستخرجة
118
+ """
119
+ items = []
120
+
121
+ # تقسيم الجدول إلى أسطر
122
+ lines = table_text.strip().split('\n')
123
+
124
+ # تحديد الأعمدة من السطر الأول (العناوين)
125
+ if len(lines) < 2:
126
+ return []
127
+
128
+ # تعرف على العناوين
129
+ headers = lines[0].strip()
130
+ header_cols = []
131
+
132
+ # البحث عن أع��دة محتملة للمواد والكميات
133
+ name_col_idx = -1
134
+ quantity_col_idx = -1
135
+ unit_col_idx = -1
136
+
137
+ # تقسيم العناوين إما بفواصل | أو مسافات متعددة
138
+ if '|' in headers:
139
+ header_cols = [col.strip() for col in headers.split('|')]
140
+ else:
141
+ # محاولة تقسيم بناءً على المسافات المتعددة
142
+ header_cols = re.split(r'\s{2,}', headers)
143
+
144
+ # تحديد أعمدة المواد والكميات والوحدات
145
+ for i, col in enumerate(header_cols):
146
+ col_lower = col.lower()
147
+ if any(term in col_lower for term in ['اسم', 'وصف', 'البند', 'المادة', 'منتج']):
148
+ name_col_idx = i
149
+ elif any(term in col_lower for term in ['كمية', 'العدد']):
150
+ quantity_col_idx = i
151
+ elif any(term in col_lower for term in ['وحدة', 'القياس']):
152
+ unit_col_idx = i
153
+
154
+ # إذا لم نتمكن من العثور على عمود الاسم، نحاول استخدام نهج بديل
155
+ if name_col_idx == -1:
156
+ # افتراض أن العمود الأول يحتوي على الاسم
157
+ name_col_idx = 0
158
+
159
+ # معالجة كل سطر في الجدول (نبدأ من السطر الثاني لتخطي العناوين)
160
+ for i in range(1, len(lines)):
161
+ line = lines[i].strip()
162
+ if not line:
163
+ continue
164
+
165
+ # تقسيم السطر إلى أعمدة
166
+ cols = []
167
+ if '|' in line:
168
+ cols = [col.strip() for col in line.split('|')]
169
+ else:
170
+ # محاولة تقسيم بناءً على المسافات المتعددة
171
+ cols = re.split(r'\s{2,}', line)
172
+
173
+ # تخطي الأسطر القصيرة جدًا
174
+ if len(cols) < 2:
175
+ continue
176
+
177
+ # استخراج المعلومات المطلوبة
178
+ name = cols[name_col_idx] if name_col_idx < len(cols) else ""
179
+
180
+ # تنظيف الاسم
181
+ name = re.sub(r'\d+', '', name).strip()
182
+
183
+ # استخراج الكمية والوحدة إذا كانت متوفرة
184
+ quantity = ""
185
+ if quantity_col_idx != -1 and quantity_col_idx < len(cols):
186
+ quantity = cols[quantity_col_idx]
187
+
188
+ # محاولة تحويل الكمية إلى رقم
189
+ try:
190
+ quantity = float(re.search(r'\d+(?:\.\d+)?', quantity).group(0))
191
+ except (ValueError, AttributeError):
192
+ quantity = ""
193
+
194
+ unit = ""
195
+ if unit_col_idx != -1 and unit_col_idx < len(cols):
196
+ unit = cols[unit_col_idx]
197
+
198
+ # إضافة المادة إلى القائمة إذا كان الاسم غير فارغ
199
+ if name:
200
+ items.append({
201
+ "name": name,
202
+ "quantity": quantity,
203
+ "unit": unit,
204
+ "source": "table"
205
+ })
206
+
207
+ return items
208
+
209
+ def _extract_materials_from_text(self, text: str) -> List[Dict[str, Any]]:
210
+ """
211
+ استخراج المواد من النص العادي
212
+
213
+ المعاملات:
214
+ ----------
215
+ text : str
216
+ النص المستخرج من المناقصة
217
+
218
+ المخرجات:
219
+ --------
220
+ List[Dict[str, Any]]
221
+ قائمة بالمواد المستخرجة
222
+ """
223
+ items = []
224
+
225
+ # البحث عن قوائم بنقاط
226
+ bullet_lists = re.findall(r'(?<=\n)(?:[-•*]\s+[^\n]+(?:\n|$))+', text)
227
+
228
+ for bullet_list in bullet_lists:
229
+ # تقسيم القائمة إلى عناصر
230
+ list_items = re.findall(r'[-•*]\s+([^\n]+)(?:\n|$)', bullet_list)
231
+
232
+ # معالجة كل عنصر
233
+ for item in list_items:
234
+ # البحث عن الكمية والوحدة في العنصر
235
+ quantity_match = re.search(r'(\d+(?:\.\d+)?)\s*([كمقطعةوحدةمترطنكجم]*)', item)
236
+
237
+ quantity = ""
238
+ unit = ""
239
+ name = item
240
+
241
+ if quantity_match:
242
+ quantity = quantity_match.group(1)
243
+ unit = quantity_match.group(2)
244
+ # إزالة الكمية والوحدة من الاسم
245
+ name = item.replace(quantity_match.group(0), "").strip()
246
+
247
+ # تنظيف الاسم
248
+ name = re.sub(r'^[-\s]*', '', name)
249
+ name = re.sub(r'[-\s]*"""
250
+ محلل المحتوى المحلي
251
+ يقوم بتحليل متطلبات المحتوى المحلي في المناقصات وتقييم نسب المحتوى المحلي
252
+ """
253
+
254
+ import re
255
+ import json
256
+ import logging
257
+ import os
258
+ from typing import Dict, List, Any, Tuple, Optional, Union
259
+ import numpy as np
260
+
261
+ logger = logging.getLogger(__name__)
262
+
263
+ class LocalContentAnalyzer:
264
+ """
265
+ محلل المحتوى المحلي في المناقصات
266
+ """
267
+
268
+ def __init__(self, model_loader, config=None):
269
+ """
270
+ تهيئة محلل المحتوى المحلي
271
+
272
+ المعاملات:
273
+ ----------
274
+ model_loader : ModelLoader
275
+ محمّل النماذج المستخدمة للتحليل
276
+ config : Dict, optional
277
+ إعدادات المحلل
278
+ """
279
+ self.config = config or {}
280
+ self.model_loader = model_loader
281
+
282
+ # تحميل قوائم المنتجات والمواد المحلية
283
+ self.local_materials_db = self._load_local_materials()
284
+ self.local_suppliers_db = self._load_local_suppliers()
285
+
286
+ # تحميل قواعد ولوائح المحتوى المحلي
287
+ self.local_content_regulations = self._load_regulations()
288
+
289
+ # تحميل نموذج تحليل النصوص إذا كان متاحاً
290
+ self.ner_model = None
291
+ if hasattr(model_loader, 'get_ner_model'):
292
+ try:
293
+ self.ner_model = model_loader.get_ner_model()
294
+ logger.info("تم تحميل نموذج التعرف على الكيانات المسماة")
295
+ except Exception as e:
296
+ logger.warning(f"فشل في تحميل نموذج التعرف على الكيانات المسماة: {str(e)}")
297
+
298
+ logger.info("تم تهيئة محلل المحتوى المحلي")
299
+
300
+ def analyze(self, extracted_text: str) -> Dict[str, Any]:
301
+ """
302
+ تحليل المحتوى المحلي من نص المناقصة
303
+
304
+ المعاملات:
305
+ ----------
306
+ extracted_text : str
307
+ النص المستخرج من المناقصة
308
+
309
+ المخرجات:
310
+ --------
311
+ Dict[str, Any]
312
+ نتائج تحليل المحتوى المحلي
313
+ """
314
+ try:
315
+ logger.info("بدء تحليل المحتوى المحلي")
316
+
317
+ # استخراج متطلبات المحتوى المحلي
318
+ local_content_requirements = self._extract_local_content_requirements(extracted_text)
319
+
320
+ # استخراج المواد والمنتجات المطلوبة
321
+ required_materials = self._extract_required_materials(extracted_text)
322
+
323
+ # تقدير نسبة المحتوى المحلي المتوقعة
324
+ estimated_local_content = self._estimate_local_content(required_materials)
325
+
326
+ # تحديد النسبة المطلوبة من المحتوى المحلي
327
+ required_local_content = self._get_required_local_content(local_content_requirements, extracted_text)
328
+
329
+ # تحديد الموردين المحليين المحتملين
330
+ potential_suppliers = self._identify_potential_suppliers(required_materials)
331
+
332
+ # اقتراح استراتيجيات تحسين المحتوى المحلي
333
+ improvement_strategies = self._generate_improvement_strategies(
334
+ estimated_local_content,
335
+ required_local_content,
336
+ required_materials
337
+ )
338
+
339
+ # إعداد النتائج
340
+ results = {
341
+ "estimated_local_content": estimated_local_content,
342
+ "required_local_content": required_local_content,
343
+ "local_content_requirements": local_content_requirements,
344
+ "required_materials": required_materials,
345
+ "potential_suppliers": potential_suppliers,
346
+ "improvement_strategies": improvement_strategies
347
+ }
348
+
349
+ logger.info(f"اكتمل تحليل المحتوى المحلي: تقدير {estimated_local_content:.1f}%، مطلوب {required_local_content:.1f}%")
350
+ return results
351
+
352
+ except Exception as e:
353
+ logger.error(f"فشل في تحليل المحتوى المحلي: {str(e)}")
354
+ return {
355
+ "estimated_local_content": 0,
356
+ "required_local_content": 0,
357
+ "local_content_requirements": [],
358
+ "required_materials": [],
359
+ "potential_suppliers": [],
360
+ "improvement_strategies": [
361
+ "حدث خطأ في تحليل المحتوى المحلي. يرجى التحقق من البيانات المدخلة."
362
+ ],
363
+ "error": str(e)
364
+ }
365
+
366
+ def _extract_local_content_requirements(self, text: str) -> List[Dict[str, Any]]:
367
+ """
368
+ استخراج متطلبات المحتوى المحلي من نص المناقصة
369
+
370
+ المعاملات:
371
+ ----------
372
+ text : str
373
+ النص المستخرج من المناقصة
374
+
375
+ المخرجات:
376
+ --------
377
+ List[Dict[str, Any]]
378
+ قائمة بمتطلبات المحتوى المحلي
379
+ """
380
+ requirements = []
381
+
382
+ # البحث عن الفقرات المتعلقة بالمحتوى المحلي
383
+ local_content_paragraphs = self._find_local_content_paragraphs(text)
384
+
385
+ for paragraph in local_content_paragraphs:
386
+ # البحث عن النسب المئوية
387
+ percentage_matches = re.findall(r'(\d+(?:\.\d+)?)\s*(%|في المائة|بالمائة|نسبة)', paragraph)
388
+
389
+ for match in percentage_matches:
390
+ percentage = float(match[0])
391
+
392
+ # التحقق مما إذا كانت النسبة في النطاق المعقول للمحتوى المحلي
393
+ if 0 <= percentage <= 100:
394
+ # تحديد نوع المتطلب
395
+ req_type = "غير محدد"
396
+
397
+ if re.search(r'الحد الأدنى|الأقل|على الأقل', paragraph):
398
+ req_type = "الحد الأدنى"
399
+ elif re.search(r'الحد الأقصى|كحد أقصى', paragraph):
400
+ req_type = "الحد الأقصى"
401
+ elif re.search(r'هدف|مستهدف', paragraph):
402
+ req_type = "مستهدف"
403
+
404
+ # تحديد الفئة
405
+ category = "عام"
406
+
407
+ if re.search(r'توظيف|عمالة|السعودة|التوطين|الموظفين', paragraph):
408
+ category = "توظيف"
409
+ elif re.search(r'خدمات|استشارات', paragraph):
410
+ category = "خدمات"
411
+ elif re.search(r'توريد|مواد|منتجات|بضائع', paragraph):
412
+ category = "منتجات"
413
+ elif re.search(r'تدريب|تأهيل|تطوير', paragraph):
414
+ category = "تدريب"
415
+
416
+ # تحديد الإلزامية
417
+ is_mandatory = bool(re.search(r'إلزامي|يجب|ضروري|لا بد|لابد|مطلوب|يلتزم|ملزم', paragraph))
418
+
419
+ requirements.append({
420
+ "description": paragraph[:200] + ("..." if len(paragraph) > 200 else ""),
421
+ "percentage": percentage,
422
+ "type": req_type,
423
+ "category": category,
424
+ "is_mandatory": is_mandatory
425
+ })
426
+
427
+ return requirements
428
+
429
+ def _find_local_content_paragraphs(self, text: str) -> List[str]:
430
+ """
431
+ البحث عن الفقرات المتعلقة بالمحتوى المحلي
432
+
433
+ المعاملات:
434
+ ----------
435
+ text : str
436
+ النص المستخرج من المناقصة
437
+
438
+ المخرجات:
439
+ --------
440
+ List[str]
441
+ قائمة بالفقرات المتعلقة بالمحتوى المحلي
442
+ """
443
+ # تقسيم النص إلى فقرات
444
+ paragraphs = re.split(r'\n\s*\n', text)
445
+
446
+ # الكلمات المفتاحية للمحتوى المحلي
447
+ local_content_keywords = [
448
+ 'المحتوى المحلي',
449
+ 'التوطين',
450
+ 'المواد المحلية',
451
+ 'المنتجات المحلية',
452
+ 'نسبة المحتوى',
453
+ 'مبادرة المحتوى المحلي',
454
+ 'هيئة المحتوى المحلي',
455
+ 'منتجات وطنية',
456
+ 'صنع في السعودية',
457
+ 'المصنّعين المحليين',
458
+ 'الموردين المحليين',
459
+ 'القيمة المضافة',
460
+ 'نقل التقنية',
461
+ 'توطين الوظائف',
462
+ 'السعودة',
463
+ ]
464
+
465
+ # البحث عن الفقرات التي تحتوي على كلمات مفتاحية للمحتوى المحلي
466
+ local_content_paragraphs = []
467
+
468
+ for paragraph in paragraphs:
469
+ paragraph = paragraph.strip()
470
+ if not paragraph:
471
+ continue
472
+
473
+ # البحث عن الكلمات المفتاحية في الفقرة
474
+ for keyword in local_content_keywords:
475
+ if keyword in paragraph.lower():
476
+ local_content_paragraphs.append(paragraph)
477
+ break
478
+
479
+ return local_content_paragraphs
480
+
481
+ def _extract_required_materials(self, text: str) -> List[Dict[str, Any]]:
482
+ """
483
+ استخراج المواد والمنتجات المطلوبة من نص المناقصة
484
+
485
+ المعاملات:
486
+ ----------
487
+ text : str
488
+ النص المستخرج من المناقصة
489
+
490
+ المخرجات:
491
+ --------
492
+ List[Dict[str, Any]]
493
+ قائمة بالمواد والمنتجات المطلوبة
494
+ """
495
+ required_materials = []
496
+
497
+ # البحث عن جداول المواصفات والكميات
498
+ tables = self._extract_tables(text)
499
+
500
+ # البحث عن قوائم المواد في النص
501
+ for table in tables:
502
+ items = self._parse_table_for_materials(table)
503
+ if items:
504
+ required_materials.extend(items)
505
+
506
+ # البحث عن قوائم المواد في النص العادي
507
+ text_materials = self._extract_materials_from_text(text)
508
+ if text_materials:
509
+ required_materials.extend(text_materials)
510
+
511
+ # إزالة التكرارات
512
+ unique_materials = []
513
+ material_names = set()
514
+
515
+ for material in required_materials:
516
+ name = material.get('name', '').lower()
517
+ if name and name not in material_names:
518
+ material_names.add(name)
519
+ unique_materials.append(material)
520
+
521
+ # تقييم توفر المواد محلياً
522
+ for material in unique_materials:
523
+ material['local_availability'] = self._check_local_availability(material['name'])
524
+
525
+ return unique_materials
526
+
527
+ def _check_local_availability(self, material_name: str) -> float:
528
+ """
529
+ تقييم مدى توفر المادة محلياً
530
+
531
+ المعاملات:
532
+ ----------
533
+ material_name : str
534
+ اسم المادة
535
+
536
+ المخرجات:
537
+ --------
538
+ float
539
+ نسبة توفر المادة مح, '', name)
540
+
541
+ # إضافة المادة إلى القائمة إذا كان الاسم غير فارغ
542
+ if name and len(name) > 3: # تجاهل الأسماء القصيرة جدًا
543
+ items.append({
544
+ "name": name,
545
+ "quantity": quantity,
546
+ "unit": unit,
547
+ "source": "text"
548
+ })
549
+
550
+ return items
551
+
552
+ def _estimate_local_content(self, required_materials: List[Dict[str, Any]]) -> float:
553
+ """
554
+ تقدير نسبة المحتوى المحلي المتوقعة
555
+
556
+ المعاملات:
557
+ ----------
558
+ required_materials : List[Dict[str, Any]]
559
+ قائمة المواد المطلوبة
560
+
561
+ المخرجات:
562
+ --------
563
+ float
564
+ نسبة المحتوى المحلي المقدرة
565
+ """
566
+ if not required_materials:
567
+ return 0.0
568
+
569
+ # حساب متوسط توفر المواد محلياً
570
+ availability_sum = sum(material.get('local_availability', 0) for material in required_materials)
571
+ avg_availability = availability_sum / len(required_materials)
572
+
573
+ # تعديل التقدير بناءً على بيانات إضافية من اللوائح
574
+ base_estimate = avg_availability
575
+
576
+ # تعديل بناءً على فئة المشروع (مثال افتراضي)
577
+ project_category = self.config.get('project_category', 'general')
578
+ category_adjustment = self.local_content_regulations.get('category_adjustments', {}).get(project_category, 0)
579
+
580
+ # توليد تقدير نهائي
581
+ final_estimate = min(100.0, max(0.0, base_estimate + category_adjustment))
582
+
583
+ return round(final_estimate, 1)
584
+
585
+ def _get_required_local_content(self, local_content_requirements: List[Dict[str, Any]], text: str) -> float:
586
+ """
587
+ تحديد النسبة المطلوبة من المحتوى المحلي
588
+
589
+ المعاملات:
590
+ ----------
591
+ local_content_requirements : List[Dict[str, Any]]
592
+ متطلبات المحتوى المحلي المستخرجة
593
+ text : str
594
+ النص الكامل للمناقصة
595
+
596
+ المخرجات:
597
+ --------
598
+ float
599
+ النسبة المطلوبة من المحتوى المحلي
600
+ """
601
+ # البحث في متطلبات المحتوى المحلي المستخرجة
602
+ if local_content_requirements:
603
+ # البحث عن متطلبات الحد الأدنى الإلزامية
604
+ mandatory_mins = [req['percentage'] for req in local_content_requirements
605
+ if req['type'] == 'ا��حد الأدنى' and req['is_mandatory']]
606
+
607
+ if mandatory_mins:
608
+ return max(mandatory_mins)
609
+
610
+ # البحث في النص عن النسب المطلوبة
611
+ percentage_pattern = r'(?:نسبة|حد أدنى)[^%\d١٢٣٤٥٦٧٨٩٠]*?(\d+(?:\.\d+)?)\s*%'
612
+ percentage_matches = re.findall(percentage_pattern, text)
613
+
614
+ if percentage_matches:
615
+ # استخدام أول نسبة مئوية تم العثور عليها
616
+ try:
617
+ return float(percentage_matches[0])
618
+ except ValueError:
619
+ pass
620
+
621
+ # البحث في اللوائح عن النسبة الافتراضية
622
+ # استخدام قيمة افتراضية إذا لم يتم العثور على نسبة محددة
623
+ return self.local_content_regulations.get('default_percentage', 30.0)
624
+
625
+ def _identify_potential_suppliers(self, required_materials: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
626
+ """
627
+ تحديد الموردين المحليين المحتملين للمواد المطلوبة
628
+
629
+ المعاملات:
630
+ ----------
631
+ required_materials : List[Dict[str, Any]]
632
+ قائمة المواد المطلوبة
633
+
634
+ المخرجات:
635
+ --------
636
+ List[Dict[str, Any]]
637
+ قائمة الموردين المحتملين
638
+ """
639
+ potential_suppliers = []
640
+
641
+ # البحث عن موردين لكل مادة
642
+ for material in required_materials:
643
+ material_name = material['name']
644
+ suppliers_for_material = []
645
+
646
+ # البحث في قاعدة بيانات الموردين
647
+ for supplier in self.local_suppliers_db:
648
+ # التحقق مما إذا كان المورد يوفر هذه المادة
649
+ if any(self._simple_similarity(material_name, product) > 0.6 for product in supplier.get('products', [])):
650
+ suppliers_for_material.append({
651
+ "name": supplier.get('name', ''),
652
+ "region": supplier.get('region', ''),
653
+ "reliability": supplier.get('reliability', 0),
654
+ "contact": supplier.get('contact', '')
655
+ })
656
+
657
+ # إضافة الموردين المحتملين للمادة
658
+ if suppliers_for_material:
659
+ material['potential_suppliers'] = suppliers_for_material
660
+
661
+ for supplier in suppliers_for_material:
662
+ # إضافة المورد إلى القائمة العامة إذا لم يكن موجودًا بالفعل
663
+ if not any(s.get('name') == supplier['name'] for s in potential_suppliers):
664
+ supplier_info = supplier.copy()
665
+ supplier_info['materials'] = [material_name]
666
+ potential_suppliers.append(supplier_info)
667
+ else:
668
+ # إضافة المادة إلى قائمة المواد التي يوفرها المورد
669
+ for s in potential_suppliers:
670
+ if s.get('name') == supplier['name'] and material_name not in s.get('materials', []):
671
+ s.setdefault('materials', []).append(material_name)
672
+
673
+ # ترتيب الموردين حسب عدد المواد ودرجة الموثوقية
674
+ return sorted(potential_suppliers,
675
+ key=lambda s: (len(s.get('materials', [])), s.get('reliability', 0)),
676
+ reverse=True)
677
+
678
+ def _generate_improvement_strategies(self, estimated_local_content: float,
679
+ required_local_content: float,
680
+ required_materials: List[Dict[str, Any]]) -> List[str]:
681
+ """
682
+ اقتراح استراتيجيات تحسين المحتوى المحلي
683
+
684
+ المعاملات:
685
+ ----------
686
+ estimated_local_content : float
687
+ نسبة المحتوى المحلي المقدرة
688
+ required_local_content : float
689
+ نسبة المحتوى المحلي المطلوبة
690
+ required_materials : List[Dict[str, Any]]
691
+ قائمة المواد المطلوبة
692
+
693
+ المخرجات:
694
+ --------
695
+ List[str]
696
+ استراتيجيات تحسين المحتوى المحلي
697
+ """
698
+ strategies = []
699
+
700
+ # التحقق مما إذا كانت النسبة المقدرة أقل من النسبة المطلوبة
701
+ if estimated_local_content < required_local_content:
702
+ strategies.append(f"زيادة نسبة المحتوى المحلي من {estimated_local_content:.1f}% إلى {required_local_content:.1f}% على الأقل")
703
+
704
+ # تحديد الموا�� ذات التوفر المنخفض محلياً
705
+ low_availability_materials = [m for m in required_materials if m.get('local_availability', 0) < 50]
706
+
707
+ if low_availability_materials:
708
+ materials_str = ", ".join([m['name'] for m in low_availability_materials[:3]])
709
+ if len(low_availability_materials) > 3:
710
+ materials_str += f" وغيرها ({len(low_availability_materials) - 3} مواد أخرى)"
711
+
712
+ strategies.append(f"البحث عن بدائل محلية للمواد التالية: {materials_str}")
713
+
714
+ # استراتيجيات عامة
715
+ strategies.extend([
716
+ "الشراكة مع شركات محلية لتوفير المواد والخدمات",
717
+ "الاستفادة من برامج دعم المحتوى المحلي المقدمة من هيئة المحتوى المحلي",
718
+ "تدريب وتوظيف كوادر سعودية لزيادة نسبة التوطين",
719
+ "التعاقد مع مصنعين محليين للمساهمة في التصنيع المحلي"
720
+ ])
721
+
722
+ else:
723
+ strategies.append(f"الحفاظ على نسبة المحتوى المحلي الحالية ({estimated_local_content:.1f}%) التي تفوق المتطلبات ({required_local_content:.1f}%)")
724
+
725
+ # استراتيجيات للتحسين المستمر
726
+ strategies.extend([
727
+ "توثيق وإبراز نسبة المحتوى المحلي العالية في العرض المقدم",
728
+ "استخدام نسبة المحتوى المحلي كميزة تنافسية في المناقصة",
729
+ "التركيز على رفع جودة المكونات المحلية لتعزيز القيمة المضافة"
730
+ ])
731
+
732
+ # استراتيجيات لتحسين التوثيق والامتثال
733
+ strategies.extend([
734
+ "توثيق مصادر المواد والخدمات المحلية بشكل دقيق",
735
+ "تطبيق منهجية حساب المحتوى المحلي وفقًا لمتطلبات هيئة المحتوى المحلي",
736
+ "إعداد خطة تفصيلية للمحتوى المحلي ومتابعة تنفيذها خلال المشروع"
737
+ ])
738
+
739
+ return strategies
740
+
741
+ def _load_local_materials(self) -> List[Dict[str, Any]]:
742
+ """
743
+ تحميل قاعدة بيانات المواد المحلية
744
+
745
+ المخرجات:
746
+ --------
747
+ List[Dict[str, Any]]
748
+ قاعدة بيانات المواد المحلية
749
+ """
750
+ try:
751
+ file_path = 'data/templates/local_materials.json'
752
+ if os.path.exists(file_path):
753
+ with open(file_path, 'r', encoding='utf-8') as f:
754
+ return json.load(f)
755
+ else:
756
+ logger.warning(f"ملف قاعدة بيانات المواد المحلية غير موجود: {file_path}")
757
+ # إنشاء قاعدة بيانات افتراضية
758
+ return self._create_default_materials_db()
759
+ except Exception as e:
760
+ logger.error(f"فشل في تحميل قاعدة بيانات المواد المحلية: {str(e)}")
761
+ return self._create_default_materials_db()
762
+
763
+ def _load_local_suppliers(self) -> List[Dict[str, Any]]:
764
+ """
765
+ تحميل قاعدة بيانات الموردين المحليين
766
+
767
+ المخرجات:
768
+ --------
769
+ List[Dict[str, Any]]
770
+ قاعدة بيانات الموردين المحليين
771
+ """
772
+ try:
773
+ file_path = 'data/templates/local_suppliers.json'
774
+ if os.path.exists(file_path):
775
+ with open(file_path, 'r', encoding='utf-8') as f:
776
+ return json.load(f)
777
+ else:
778
+ logger.warning(f"ملف قاعدة بيانات الموردين المحليين غير موجود: {file_path}")
779
+ # إنشاء قاعدة بيانات افتراضية
780
+ return self._create_default_suppliers_db()
781
+ except Exception as e:
782
+ logger.error(f"فشل في تحميل قاعدة بيانات الموردين المحليين: {str(e)}")
783
+ return self._create_default_suppliers_db()
784
+
785
+ def _load_regulations(self) -> Dict[str, Any]:
786
+ """
787
+ تحميل لوائح وقواعد المحتوى المحلي
788
+
789
+ المخرجات:
790
+ --------
791
+ Dict[str, Any]
792
+ لوائح وقواعد المحتوى المحلي
793
+ """
794
+ try:
795
+ file_path = 'data/templates/local_content_regulations.json'
796
+ if os.path.exists(file_path):
797
+ with open(file_path, 'r', encoding='utf-8') as f:
798
+ return json.load(f)
799
+ else:
800
+ logger.warning(f"ملف لوائح المحتوى المحلي غير موجود: {file_path}")
801
+ # إنشاء لوائح افتراضية
802
+ return self._create_default_regulations()
803
+ except Exception as e:
804
+ logger.error(f"فشل في تحميل لوائح المحتوى المحلي: {str(e)}")
805
+ return self._create_default_regulations()
806
+
807
+ def _create_default_materials_db(self) -> List[Dict[str, Any]]:
808
+ """
809
+ إنشاء قاعدة بيانات افتراضية للمواد المحلية
810
+
811
+ المخرجات:
812
+ --------
813
+ List[Dict[str, Any]]
814
+ قاعدة بيانات افتراضية للمواد المحلية
815
+ """
816
+ return [
817
+ {"name": "حديد تسليح", "category": "مواد بناء", "availability_percentage": 90.0},
818
+ {"name": "أسمنت", "category": "مواد بناء", "availability_percentage": 95.0},
819
+ {"name": "خرسانة جاهزة", "category": "مواد بناء", "availability_percentage": 100.0},
820
+ {"name": "بلاط", "category": "مواد بناء", "availability_percentage": 80.0},
821
+ {"name": "رخام", "category": "مواد بناء", "availability_percentage": 60.0},
822
+ {"name": "دهانات", "category": "مواد بناء", "availability_percentage": 75.0},
823
+ {"name": "زجاج", "category": "مواد بناء", "availability_percentage": 50.0},
824
+ {"name": "أسلاك كهربائية", "category": "كهرباء", "availability_percentage": 60.0},
825
+ {"name": "لوحات كهربائية", "category": "كهرباء", "availability_percentage": 55.0},
826
+ {"name": "مفاتيح كهربائية", "category": "كهرباء", "availability_percentage": 45.0},
827
+ {"name": "أنابيب مياه", "category": "سباكة", "availability_percentage": 70.0},
828
+ {"name": "خزانات مياه", "category": "سباكة", "availability_percentage": 95.0},
829
+ {"name": "مواسير صرف", "category": "سباكة", "availability_percentage": 85.0},
830
+ {"name": "وحدات تكييف", "category": "تكييف", "availability_percentage": 30.0},
831
+ {"name": "معدات تبريد", "category": "تكييف", "availability_percentage": 25.0},
832
+ {"name": "أجهزة تهوية", "category": "تكييف", "availability_percentage": 40.0},
833
+ {"name": "أثاث مكتبي", "category": "أثاث", "availability_percentage": 70.0},
834
+ {"name": "أثاث منزلي", "category": "أثاث", "availability_percentage": 65.0},
835
+ {"name": "أبواب خشبية", "category": "نجارة", "availability_percentage": 80.0},
836
+ {"name": "نوافذ ألمنيوم", "category": "ألمنيوم", "availability_percentage": 75.0}
837
+ ]
838
+
839
+ def _create_default_suppliers_db(self) -> List[Dict[str, Any]]:
840
+ """
841
+ إنشاء قاعدة بيانات افتراضية للموردين المحليين
842
+
843
+ المخرجات:
844
+ --------
845
+ List[Dict[str, Any]]
846
+ قاعدة بيانات افتراضية للموردين المحليين
847
+ """
848
+ return [
849
+ {
850
+ "name": "شركة الراجحي للحديد",
851
+ "region": "الرياض",
852
+ "category": "مواد بناء",
853
+ "products": ["حديد تسليح", "حديد مجلفن"],
854
+ "reliability": 4.5,
855
+ "contact": "[email protected]",
856
+ "nitaqat_category": "بلاتيني"
857
+ },
858
+ {
859
+ "name": "شركة امجاد للسباكة",
860
+ "region": "الرياض",
861
+ "category": "سباكة",
862
+ "products": ["أنابيب مياه", "خزانات مياه", "مواسير صرف"],
863
+ "reliability": 3.9,
864
+ "contact": "[email protected]",
865
+ "nitaqat_category": "أخضر متوسط"
866
+ },
867
+ {
868
+ "name": "مصنع الخليج للزجاج",
869
+ "region": "جدة",
870
+ "category": "مواد بناء",
871
+ "products": ["زجاج", "ألواح زجاجية", "واجهات زجاجية"],
872
+ "reliability": 4.0,
873
+ "contact": "[email protected]",
874
+ "nitaqat_category": "أخضر مرتفع"
875
+ },
876
+ {
877
+ "name": "مؤسسة الديار للدهانات",
878
+ "region": "الرياض",
879
+ "category": "مواد بناء",
880
+ "products": ["دهانات", "طلاء جدران", "عوازل"],
881
+ "reliability": 3.7,
882
+ "contact": "[email protected]",
883
+ "nitaqat_category": "أخضر منخفض"
884
+ },
885
+ {
886
+ "name": "شركة الأثاث المكتبي المتحدة",
887
+ "region": "الرياض",
888
+ "category": "أثاث",
889
+ "products": ["أثاث مكتبي", "كراسي", "طاولات", "خزائن"],
890
+ "reliability": 4.1,
891
+ "contact": "[email protected]",
892
+ "nitaqat_category": "أخضر مرتفع"
893
+ },
894
+ {
895
+ "name": "شركة ابن غنيم للنجارة",
896
+ "region": "الدمام",
897
+ "category": "نجارة",
898
+ "products": ["أبواب خشبية", "نوافذ خشبية", "أثاث منزلي"],
899
+ "reliability": 4.3,
900
+ "contact": "[email protected]",
901
+ "nitaqat_category": "أخضر مرتفع"
902
+ },
903
+ {
904
+ "name": "الشركة العربية للألمنيوم",
905
+ "region": "جدة",
906
+ "category": "ألمنيوم",
907
+ "products": ["نوافذ ألمنيوم", "أبواب ألمنيوم", "واجهات ألمنيوم"],
908
+ "reliability": 4.4,
909
+ "contact": "[email protected]",
910
+ "nitaqat_category": "بلاتيني"
911
+ }
912
+ ]
913
+
914
+ def _create_default_regulations(self) -> Dict[str, Any]:
915
+ """
916
+ إنشاء لوائح افتراضية للمحتوى المحلي
917
+
918
+ المخرجات:
919
+ --------
920
+ Dict[str, Any]
921
+ لوائح افتراضية للمحتوى المحلي
922
+ """
923
+ return {
924
+ "default_percentage": 30.0,
925
+ "category_adjustments": {
926
+ "construction": 5.0,
927
+ "it": -5.0,
928
+ "manufacturing": 10.0,
929
+ "services": 0.0,
930
+ "general": 0.0
931
+ },
932
+ "sector_requirements": {
933
+ "oil_and_gas": 40.0,
934
+ "electricity": 35.0,
935
+ "water": 25.0,
936
+ "telecommunications": 20.0,
937
+ "transportation": 30.0,
938
+ "healthcare": 25.0,
939
+ "education": 35.0
940
+ },
941
+ "calculation_method": "السعر العرض الذي يحتوي على نسبة أعلى من المحتوى المحلي يحصل على ميزة سعرية تصل إلى 10% من السعر",
942
+ "documentation_requirements": [
943
+ "شهادات المنشأ للمواد والمنتجات",
944
+ "عقود التوريد مع الموردين المحليين",
945
+ "كشوف رواتب الموظفين السعوديين",
946
+ "شهادات تصنيف المقاولين",
947
+ "شهادات نطاقات للتوطين"
948
+ ],
949
+ "reference_documents": [
950
+ "دليل هيئة المحتوى المحلي وتنمية القطاع الخاص",
951
+ "لائحة تفضيل المنتجات المحلية في المشتريات الحكومية",
952
+ "دليل تطبيق آلية الوزن النسبي للمحتوى المحلي في التقييم المالي"
953
+ ],
954
+ "last_updated": "2023-06-15"
955
+ }category": "بلاتيني"
956
+ },
957
+ {
958
+ "name": "اسمنت اليمامة",
959
+ "region": "الرياض",
960
+ "category": "مواد بناء",
961
+ "products": ["أسمنت", "خرسانة جاهزة"],
962
+ "reliability": 4.7,
963
+ "contact": "[email protected]",
964
+ "nitaqat_category": "بلاتيني"
965
+ },
966
+ {
967
+ "name": "الشركة السعودية للصناعات الكهربائية",
968
+ "region": "جدة",
969
+ "category": "كهرباء",
970
+ "products": ["أسلاك كهربائية", "لوحات كهربائية", "مفاتيح كهربائية"],
971
+ "reliability": 4.2,
972
+ "contact": "[email protected]",
973
+ "nitaqat_category": "أخضر مرتفع"
974
+ },
975
+ {
976
+ "name": "شركة الزامل للتكييف",
977
+ "region": "الدمام",
978
+ "category": "تكييف",
979
+ "products": ["وحدات تكييف", "معدات تبريد", "أجهزة تهوية"],
980
+ "reliability": 4.8,
981
+ "contact": "[email protected]",
982
+ "nitaqat_"""
983
+ محلل المحتوى المحلي
984
+ يقوم بتحليل متطلبات المحتوى المحلي في المناقصات وتقييم نسب المحتوى المحلي
985
+ """
986
+
987
+ import re
988
+ import json
989
+ import logging
990
+ import os
991
+ from typing import Dict, List, Any, Tuple, Optional, Union
992
+ import numpy as np
993
+
994
+ logger = logging.getLogger(__name__)
995
+
996
+ class LocalContentAnalyzer:
997
+ """
998
+ محلل المحتوى المحلي في المناقصات
999
+ """
1000
+
1001
+ def __init__(self, model_loader, config=None):
1002
+ """
1003
+ تهيئة محلل المحتوى المحلي
1004
+
1005
+ المعاملات:
1006
+ ----------
1007
+ model_loader : ModelLoader
1008
+ محمّل النماذج المستخدمة للتحليل
1009
+ config : Dict, optional
1010
+ إعدادات المحلل
1011
+ """
1012
+ self.config = config or {}
1013
+ self.model_loader = model_loader
1014
+
1015
+ # تحميل قوائم المنتجات والمواد المحلية
1016
+ self.local_materials_db = self._load_local_materials()
1017
+ self.local_suppliers_db = self._load_local_suppliers()
1018
+
1019
+ # تحميل قواعد ولوائح المحتوى المحلي
1020
+ self.local_content_regulations = self._load_regulations()
1021
+
1022
+ # تحميل نموذج تحليل النصوص إذا كان متاحاً
1023
+ self.ner_model = None
1024
+ if hasattr(model_loader, 'get_ner_model'):
1025
+ try:
1026
+ self.ner_model = model_loader.get_ner_model()
1027
+ logger.info("تم تحميل نموذج التعرف على الكيانات المسماة")
1028
+ except Exception as e:
1029
+ logger.warning(f"فشل في تحميل نموذج التعرف على الكيانات المسماة: {str(e)}")
1030
+
1031
+ logger.info("تم تهيئة محلل المحتوى المحلي")
1032
+
1033
+ def analyze(self, extracted_text: str) -> Dict[str, Any]:
1034
+ """
1035
+ تحليل المحتوى المحلي من نص المناقصة
1036
+
1037
+ المعاملات:
1038
+ ----------
1039
+ extracted_text : str
1040
+ النص المستخرج من المناقصة
1041
+
1042
+ المخرجات:
1043
+ --------
1044
+ Dict[str, Any]
1045
+ نتائج تحليل المحتوى المحلي
1046
+ """
1047
+ try:
1048
+ logger.info("بدء تحليل المحتوى المحلي")
1049
+
1050
+ # استخراج متطلبات المحتوى المحلي
1051
+ local_content_requirements = self._extract_local_content_requirements(extracted_text)
1052
+
1053
+ # استخراج المواد والمنتجات المطلوبة
1054
+ required_materials = self._extract_required_materials(extracted_text)
1055
+
1056
+ # تقدير نسبة المحتوى المحلي المتوقعة
1057
+ estimated_local_content = self._estimate_local_content(required_materials)
1058
+
1059
+ # تحديد النسبة المطلوبة من المحتوى المحلي
1060
+ required_local_content = self._get_required_local_content(local_content_requirements, extracted_text)
1061
+
1062
+ # تحديد الموردين المحليين المحتملين
1063
+ potential_suppliers = self._identify_potential_suppliers(required_materials)
1064
+
1065
+ # اقتراح استراتيجيات تحسين المحتوى المحلي
1066
+ improvement_strategies = self._generate_improvement_strategies(
1067
+ estimated_local_content,
1068
+ required_local_content,
1069
+ required_materials
1070
+ )
1071
+
1072
+ # إعداد النتائج
1073
+ results = {
1074
+ "estimated_local_content": estimated_local_content,
1075
+ "required_local_content": required_local_content,
1076
+ "local_content_requirements": local_content_requirements,
1077
+ "required_materials": required_materials,
1078
+ "potential_suppliers": potential_suppliers,
1079
+ "improvement_strategies": improvement_strategies
1080
+ }
1081
+
1082
+ logger.info(f"اكتمل تحليل المحتوى المحلي: تقدير {estimated_local_content:.1f}%، مطلوب {required_local_content:.1f}%")
1083
+ return results
1084
+
1085
+ except Exception as e:
1086
+ logger.error(f"فشل في تحليل المحتوى المحلي: {str(e)}")
1087
+ return {
1088
+ "estimated_local_content": 0,
1089
+ "required_local_content": 0,
1090
+ "local_content_requirements": [],
1091
+ "required_materials": [],
1092
+ "potential_suppliers": [],
1093
+ "improvement_strategies": [
1094
+ "حدث خطأ في تحليل المحتوى المحلي. يرجى التحقق من البيانات المدخلة."
1095
+ ],
1096
+ "error": str(e)
1097
+ }
1098
+
1099
+ def _extract_local_content_requirements(self, text: str) -> List[Dict[str, Any]]:
1100
+ """
1101
+ استخراج متطلبات المحتوى المحلي من نص المناقصة
1102
+
1103
+ المعاملات:
1104
+ ----------
1105
+ text : str
1106
+ النص المستخرج من المناقصة
1107
+
1108
+ المخرجات:
1109
+ --------
1110
+ List[Dict[str, Any]]
1111
+ قائمة بمتطلبات المحتوى المحلي
1112
+ """
1113
+ requirements = []
1114
+
1115
+ # البحث عن الفقرات المتعلقة بالمحتوى المحلي
1116
+ local_content_paragraphs = self._find_local_content_paragraphs(text)
1117
+
1118
+ for paragraph in local_content_paragraphs:
1119
+ # البحث عن النسب المئوية
1120
+ percentage_matches = re.findall(r'(\d+(?:\.\d+)?)\s*(%|في المائة|بالمائة|نسبة)', paragraph)
1121
+
1122
+ for match in percentage_matches:
1123
+ percentage = float(match[0])
1124
+
1125
+ # التحقق مما إذا كانت النسبة في النطاق المعقول للمحتوى المحلي
1126
+ if 0 <= percentage <= 100:
1127
+ # تحديد نوع المتطلب
1128
+ req_type = "غير محدد"
1129
+
1130
+ if re.search(r'الحد الأدنى|الأقل|على الأقل', paragraph):
1131
+ req_type = "الحد الأدنى"
1132
+ elif re.search(r'الحد الأقصى|كحد أقصى', paragraph):
1133
+ req_type = "الحد الأقصى"
1134
+ elif re.search(r'هدف|مستهدف', paragraph):
1135
+ req_type = "مستهدف"
1136
+
1137
+ # تحديد الفئة
1138
+ category = "عام"
1139
+
1140
+ if re.search(r'توظيف|عمالة|السعودة|التوطين|الموظفين', paragraph):
1141
+ category = "توظيف"
1142
+ elif re.search(r'خدمات|استشارات', paragraph):
1143
+ category = "خدمات"
1144
+ elif re.search(r'توريد|مواد|منتجات|بضائع', paragraph):
1145
+ category = "منتجات"
1146
+ elif re.search(r'تدريب|تأهيل|تطوير', paragraph):
1147
+ category = "تدريب"
1148
+
1149
+ # تحديد الإلزامية
1150
+ is_mandatory = bool(re.search(r'إلزامي|يجب|ضروري|لا بد|لابد|مطلوب|يلتزم|ملزم', paragraph))
1151
+
1152
+ requirements.append({
1153
+ "description": paragraph[:200] + ("..." if len(paragraph) > 200 else ""),
1154
+ "percentage": percentage,
1155
+ "type": req_type,
1156
+ "category": category,
1157
+ "is_mandatory": is_mandatory
1158
+ })
1159
+
1160
+ return requirements
1161
+
1162
+ def _find_local_content_paragraphs(self, text: str) -> List[str]:
1163
+ """
1164
+ البحث عن الفقرات المتعلقة بالمحتوى المحلي
1165
+
1166
+ المعاملات:
1167
+ ----------
1168
+ text : str
1169
+ النص المستخرج من المناقصة
1170
+
1171
+ المخرجات:
1172
+ --------
1173
+ List[str]
1174
+ قائمة بالفقرات المتعلقة بالمحتوى المحلي
1175
+ """
1176
+ # تقسيم النص إلى فقرات
1177
+ paragraphs = re.split(r'\n\s*\n', text)
1178
+
1179
+ # الكلمات المفتاحية للمحتوى المحلي
1180
+ local_content_keywords = [
1181
+ 'المحتوى المحلي',
1182
+ 'التوطين',
1183
+ 'المواد المحلية',
1184
+ 'المنتجات المحلية',
1185
+ 'نسبة المحتوى',
1186
+ 'مبادرة المحتوى المحلي',
1187
+ 'هيئة المحتوى المحلي',
1188
+ 'منتجات وطنية',
1189
+ 'صنع في السعودية',
1190
+ 'المصنّعين المحليين',
1191
+ 'الموردين المحليين',
1192
+ 'القيمة المضافة',
1193
+ 'نقل التقنية',
1194
+ 'توطين الوظائف',
1195
+ 'السعودة',
1196
+ ]
1197
+
1198
+ # البحث عن الفقرات التي تحتوي على كلمات مفتاحية للمحتوى المحلي
1199
+ local_content_paragraphs = []
1200
+
1201
+ for paragraph in paragraphs:
1202
+ paragraph = paragraph.strip()
1203
+ if not paragraph:
1204
+ continue
1205
+
1206
+ # البحث عن الكلمات المفتاحية في الفقرة
1207
+ for keyword in local_content_keywords:
1208
+ if keyword in paragraph.lower():
1209
+ local_content_paragraphs.append(paragraph)
1210
+ break
1211
+
1212
+ return local_content_paragraphs
1213
+
1214
+ def _extract_required_materials(self, text: str) -> List[Dict[str, Any]]:
1215
+ """
1216
+ استخراج المواد والمنتجات المطلوبة من نص المناقصة
1217
+
1218
+ المعاملات:
1219
+ ----------
1220
+ text : str
1221
+ النص المستخرج من المناقصة
1222
+
1223
+ المخرجات:
1224
+ --------
1225
+ List[Dict[str, Any]]
1226
+ قائمة بالمواد والمنت��ات المطلوبة
1227
+ """
1228
+ required_materials = []
1229
+
1230
+ # البحث عن جداول المواصفات والكميات
1231
+ tables = self._extract_tables(text)
1232
+
1233
+ # البحث عن قوائم المواد في النص
1234
+ for table in tables:
1235
+ items = self._parse_table_for_materials(table)
1236
+ if items:
1237
+ required_materials.extend(items)
1238
+
1239
+ # البحث عن قوائم المواد في النص العادي
1240
+ text_materials = self._extract_materials_from_text(text)
1241
+ if text_materials:
1242
+ required_materials.extend(text_materials)
1243
+
1244
+ # إزالة التكرارات
1245
+ unique_materials = []
1246
+ material_names = set()
1247
+
1248
+ for material in required_materials:
1249
+ name = material.get('name', '').lower()
1250
+ if name and name not in material_names:
1251
+ material_names.add(name)
1252
+ unique_materials.append(material)
1253
+
1254
+ # تقييم توفر المواد محلياً
1255
+ for material in unique_materials:
1256
+ material['local_availability'] = self._check_local_availability(material['name'])
1257
+
1258
+ return unique_materials
1259
+
1260
+ def _check_local_availability(self, material_name: str) -> float:
1261
+ """
1262
+ تقييم مدى توفر المادة محلياً بناءً على قاعدة بيانات أو تحليل البيانات المتاحة.
1263
+
1264
+ المعاملات:
1265
+ ----------
1266
+ material_name : str
1267
+ اسم المادة التي نريد التحقق من توفرها محليًا.
1268
+
1269
+ المخرجات:
1270
+ --------
1271
+ float
1272
+ نسبة توفر المادة محليًا، تتراوح بين 0.0 (غير متوفرة) إلى 1.0 (متوفرة بالكامل).
1273
+ """
1274
+
1275
+ # قاعدة بيانات تقديرية لتوفر المواد محليًا (يمكن استبدالها بمصدر بيانات فعلي)
1276
+ local_materials = {
1277
+ "حديد": 0.95, # متوفر بنسبة 95%
1278
+ "أسمنت": 0.9, # متوفر بنسبة 90%
1279
+ "خشب": 0.7, # متوفر بنسبة 70%
1280
+ "زجاج": 0.6, # متوفر بنسبة 60%
1281
+ "ألمنيوم": 0.85, # متوفر بنسبة 85%
1282
+ "بلاستيك": 0.8, # متوفر بنسبة 80%
1283
+ "نحاس": 0.5, # متوفر بنسبة 50%
1284
+ "إلكترونيات متخصصة": 0.3 # متوفر بنسبة 30%
1285
+ }
1286
+
1287
+ # البحث عن المادة في القائمة
1288
+ for key in local_materials:
1289
+ if key in material_name:
1290
+ return local_materials[key]
1291
+
1292
+ # إذا لم تكن المادة موجودة في القائمة، نفترض توفرها بنسبة 0.5 كافتراضي
1293
+ return 0.5