نسبة توفر المادة محلياً (0 إلى 100) """ # البحث في قاعدة بيانات المواد المحلية material_name = material_name.lower() # مطابقة الاسم بالكامل for local_material in self.local_materials_db: if local_material.get('name', '').lower() == material_name: return local_material.get('availability_percentage', 100.0) # مطابقة جزئية matches = [] for local_material in self.local_materials_db: local_name = local_material.get('name', '').lower() # حساب درجة التشابه البسيط similarity = self._simple_similarity(material_name, local_name) if similarity > 0.7: # عتبة التشابه matches.append((local_material, similarity)) if matches: # ترتيب المطابقات حسب درجة التشابه matches.sort(key=lambda x: x[1], reverse=True) return matches[0][0].get('availability_percentage', 0.0) # إذا لم يتم العثور على مطابقات، نفترض نسبة منخفضة return 20.0 # قيمة افتراضية منخفضة def _simple_similarity(self, str1: str, str2: str) -> float: """ حساب درجة التشابه البسيط بين سلسلتين المعاملات: ---------- str1 : str السلسلة الأولى str2 : str السلسلة الثانية المخرجات: -------- float درجة التشابه (0 إلى 1) """ # تنظيف السلاسل str1 = str1.strip().lower() str2 = str2.strip().lower() # تحويل السلاسل إلى مجموعات من الكلمات words1 = set(str1.split()) words2 = set(str2.split()) # حساب معامل جاكارد intersection = len(words1.intersection(words2)) union = len(words1.union(words2)) if union == 0: return 0.0 return intersection / union def _extract_tables(self, text: str) -> List[str]: """ استخراج أقسام الجداول من النص المعاملات: ---------- text : str النص المستخرج من المناقصة المخرجات: -------- List[str] قائمة بأقسام الجداول """ # بحث بسيط عن أقسام الجداول # البحث عن أقسام تبدأ بعناوين مثل "جدول المواصفات" أو "قائمة الكميات" table_headers = [ "جدول المواصفات", "جدول الكميات", "قائمة المواد", "قائمة الكميات", "جدول المنتجات", "المواد المطلوبة", "قائمة البنود", "بنود المناقصة" ] tables = [] for header in table_headers: # البحث عن أقسام تبدأ بالعنوان المحدد matches = re.finditer(f"{header}.*?(?=\n\n|\Z)", text, re.DOTALL) for match in matches: table_section = match.group(0) if len(table_section.split('\n')) > 2: # التأكد من أن القسم يحتوي على أكثر من سطرين tables.append(table_section) # البحث عن أنماط الجداول (أسطر تحتوي على عدة عناصر مفصولة بـ | أو مسافات متعددة) table_pattern = r'(?:\n|^)((?:[^\n]+\|[^\n]+\|[^\n]+\n){3,})' table_matches = re.finditer(table_pattern, text) for match in table_matches: tables.append(match.group(1)) return tables def _parse_table_for_materials(self, table_text: str) -> List[Dict[str, Any]]: """ تحليل نص الجدول لاستخراج المواد المعاملات: ---------- table_text : str نص الجدول المخرجات: -------- List[Dict[str, Any]] قائمة بالمواد المستخرجة """ items = [] # تقسيم الجدول إلى أسطر lines = table_text.strip().split('\n') # تحديد الأعمدة من السطر الأول (العناوين) if len(lines) < 2: return [] # تعرف على العناوين headers = lines[0].strip() header_cols = [] # البحث عن أعمدة محتملة للمواد والكميات name_col_idx = -1 quantity_col_idx = -1 unit_col_idx = -1 # تقسيم العناوين إما بفواصل | أو مسافات متعددة if '|' in headers: header_cols = [col.strip() for col in headers.split('|')] else: # محاولة تقسيم بناءً على المسافات المتعددة header_cols = re.split(r'\s{2,}', headers) # تحديد أعمدة المواد والكميات والوحدات for i, col in enumerate(header_cols): col_lower = col.lower() if any(term in col_lower for term in ['اسم', 'وصف', 'البند', 'المادة', 'منتج']): name_col_idx = i elif any(term in col_lower for term in ['كمية', 'العدد']): quantity_col_idx = i elif any(term in col_lower for term in ['وحدة', 'القياس']): unit_col_idx = i # إذا لم نتمكن من العثور على عمود الاسم، نحاول استخدام نهج بديل if name_col_idx == -1: # افتراض أن العمود الأول يحتوي على الاسم name_col_idx = 0 # معالجة كل سطر في الجدول (نبدأ من السطر الثاني لتخطي العناوين) for i in range(1, len(lines)): line = lines[i].strip() if not line: continue # تقسيم السطر إلى أعمدة cols = [] if '|' in line: cols = [col.strip() for col in line.split('|')] else: # محاولة تقسيم بناءً على المسافات المتعددة cols = re.split(r'\s{2,}', line) # تخطي الأسطر القصيرة جدًا if len(cols) < 2: continue # استخراج المعلومات المطلوبة name = cols[name_col_idx] if name_col_idx < len(cols) else "" # تنظيف الاسم name = re.sub(r'\d+', '', name).strip() # استخراج الكمية والوحدة إذا كانت متوفرة quantity = "" if quantity_col_idx != -1 and quantity_col_idx < len(cols): quantity = cols[quantity_col_idx] # محاولة تحويل الكمية إلى رقم try: quantity = float(re.search(r'\d+(?:\.\d+)?', quantity).group(0)) except (ValueError, AttributeError): quantity = "" unit = "" if unit_col_idx != -1 and unit_col_idx < len(cols): unit = cols[unit_col_idx] # إضافة المادة إلى القائمة إذا كان الاسم غير فارغ if name: items.append({ "name": name, "quantity": quantity, "unit": unit, "source": "table" }) return items def _extract_materials_from_text(self, text: str) -> List[Dict[str, Any]]: """ استخراج المواد من النص العادي المعاملات: ---------- text : str النص المستخرج من المناقصة المخرجات: -------- List[Dict[str, Any]] قائمة بالمواد المستخرجة """ items = [] # البحث عن قوائم بنقاط bullet_lists = re.findall(r'(?<=\n)(?:[-•*]\s+[^\n]+(?:\n|$))+', text) for bullet_list in bullet_lists: # تقسيم القائمة إلى عناصر list_items = re.findall(r'[-•*]\s+([^\n]+)(?:\n|$)', bullet_list) # معالجة كل عنصر for item in list_items: # البحث عن الكمية والوحدة في العنصر quantity_match = re.search(r'(\d+(?:\.\d+)?)\s*([كمقطعةوحدةمترطنكجم]*)', item) quantity = "" unit = "" name = item if quantity_match: quantity = quantity_match.group(1) unit = quantity_match.group(2) # إزالة الكمية والوحدة من الاسم name = item.replace(quantity_match.group(0), "").strip() # تنظيف الاسم name = re.sub(r'^[-\s]*', '', name) name = re.sub(r'[-\s]*""" محلل المحتوى المحلي يقوم بتحليل متطلبات المحتوى المحلي في المناقصات وتقييم نسب المحتوى المحلي """ import re import json import logging import os from typing import Dict, List, Any, Tuple, Optional, Union import numpy as np logger = logging.getLogger(__name__) class LocalContentAnalyzer: """ محلل المحتوى المحلي في المناقصات """ def __init__(self, model_loader, config=None): """ تهيئة محلل المحتوى المحلي المعاملات: ---------- model_loader : ModelLoader محمّل النماذج المستخدمة للتحليل config : Dict, optional إعدادات المحلل """ self.config = config or {} self.model_loader = model_loader # تحميل قوائم المنتجات والمواد المحلية self.local_materials_db = self._load_local_materials() self.local_suppliers_db = self._load_local_suppliers() # تحميل قواعد ولوائح المحتوى المحلي self.local_content_regulations = self._load_regulations() # تحميل نموذج تحليل النصوص إذا كان متاحاً self.ner_model = None if hasattr(model_loader, 'get_ner_model'): try: self.ner_model = model_loader.get_ner_model() logger.info("تم تحميل نموذج التعرف على الكيانات المسماة") except Exception as e: logger.warning(f"فشل في تحميل نموذج التعرف على الكيانات المسماة: {str(e)}") logger.info("تم تهيئة محلل المحتوى المحلي") def analyze(self, extracted_text: str) -> Dict[str, Any]: """ تحليل المحتوى المحلي من نص المناقصة المعاملات: ---------- extracted_text : str النص المستخرج من المناقصة المخرجات: -------- Dict[str, Any] نتائج تحليل المحتوى المحلي """ try: logger.info("بدء تحليل المحتوى المحلي") # استخراج متطلبات المحتوى المحلي local_content_requirements = self._extract_local_content_requirements(extracted_text) # استخراج المواد والمنتجات المطلوبة required_materials = self._extract_required_materials(extracted_text) # تقدير نسبة المحتوى المحلي المتوقعة estimated_local_content = self._estimate_local_content(required_materials) # تحديد النسبة المطلوبة من المحتوى المحلي required_local_content = self._get_required_local_content(local_content_requirements, extracted_text) # تحديد الموردين المحليين المحتملين potential_suppliers = self._identify_potential_suppliers(required_materials) # اقتراح استراتيجيات تحسين المحتوى المحلي improvement_strategies = self._generate_improvement_strategies( estimated_local_content, required_local_content, required_materials ) # إعداد النتائج results = { "estimated_local_content": estimated_local_content, "required_local_content": required_local_content, "local_content_requirements": local_content_requirements, "required_materials": required_materials, "potential_suppliers": potential_suppliers, "improvement_strategies": improvement_strategies } logger.info(f"اكتمل تحليل المحتوى المحلي: تقدير {estimated_local_content:.1f}%، مطلوب {required_local_content:.1f}%") return results except Exception as e: logger.error(f"فشل في تحليل المحتوى المحلي: {str(e)}") return { "estimated_local_content": 0, "required_local_content": 0, "local_content_requirements": [], "required_materials": [], "potential_suppliers": [], "improvement_strategies": [ "حدث خطأ في تحليل المحتوى المحلي. يرجى التحقق من البيانات المدخلة." ], "error": str(e) } def _extract_local_content_requirements(self, text: str) -> List[Dict[str, Any]]: """ استخراج متطلبات المحتوى المحلي من نص المناقصة المعاملات: ---------- text : str النص المستخرج من المناقصة المخرجات: -------- List[Dict[str, Any]] قائمة بمتطلبات المحتوى المحلي """ requirements = [] # البحث عن الفقرات المتعلقة بالمحتوى المحلي local_content_paragraphs = self._find_local_content_paragraphs(text) for paragraph in local_content_paragraphs: # البحث عن النسب المئوية percentage_matches = re.findall(r'(\d+(?:\.\d+)?)\s*(%|في المائة|بالمائة|نسبة)', paragraph) for match in percentage_matches: percentage = float(match[0]) # التحقق مما إذا كانت النسبة في النطاق المعقول للمحتوى المحلي if 0 <= percentage <= 100: # تحديد نوع المتطلب req_type = "غير محدد" if re.search(r'الحد الأدنى|الأقل|على الأقل', paragraph): req_type = "الحد الأدنى" elif re.search(r'الحد الأقصى|كحد أقصى', paragraph): req_type = "الحد الأقصى" elif re.search(r'هدف|مستهدف', paragraph): req_type = "مستهدف" # تحديد الفئة category = "عام" if re.search(r'توظيف|عمالة|السعودة|التوطين|الموظفين', paragraph): category = "توظيف" elif re.search(r'خدمات|استشارات', paragraph): category = "خدمات" elif re.search(r'توريد|مواد|منتجات|بضائع', paragraph): category = "منتجات" elif re.search(r'تدريب|تأهيل|تطوير', paragraph): category = "تدريب" # تحديد الإلزامية is_mandatory = bool(re.search(r'إلزامي|يجب|ضروري|لا بد|لابد|مطلوب|يلتزم|ملزم', paragraph)) requirements.append({ "description": paragraph[:200] + ("..." if len(paragraph) > 200 else ""), "percentage": percentage, "type": req_type, "category": category, "is_mandatory": is_mandatory }) return requirements def _find_local_content_paragraphs(self, text: str) -> List[str]: """ البحث عن الفقرات المتعلقة بالمحتوى المحلي المعاملات: ---------- text : str النص المستخرج من المناقصة المخرجات: -------- List[str] قائمة بالفقرات المتعلقة بالمحتوى المحلي """ # تقسيم النص إلى فقرات paragraphs = re.split(r'\n\s*\n', text) # الكلمات المفتاحية للمحتوى المحلي local_content_keywords = [ 'المحتوى المحلي', 'التوطين', 'المواد المحلية', 'المنتجات المحلية', 'نسبة المحتوى', 'مبادرة المحتوى المحلي', 'هيئة المحتوى المحلي', 'منتجات وطنية', 'صنع في السعودية', 'المصنّعين المحليين', 'الموردين المحليين', 'القيمة المضافة', 'نقل التقنية', 'توطين الوظائف', 'السعودة', ] # البحث عن الفقرات التي تحتوي على كلمات مفتاحية للمحتوى المحلي local_content_paragraphs = [] for paragraph in paragraphs: paragraph = paragraph.strip() if not paragraph: continue # البحث عن الكلمات المفتاحية في الفقرة for keyword in local_content_keywords: if keyword in paragraph.lower(): local_content_paragraphs.append(paragraph) break return local_content_paragraphs def _extract_required_materials(self, text: str) -> List[Dict[str, Any]]: """ استخراج المواد والمنتجات المطلوبة من نص المناقصة المعاملات: ---------- text : str النص المستخرج من المناقصة المخرجات: -------- List[Dict[str, Any]] قائمة بالمواد والمنتجات المطلوبة """ required_materials = [] # البحث عن جداول المواصفات والكميات tables = self._extract_tables(text) # البحث عن قوائم المواد في النص for table in tables: items = self._parse_table_for_materials(table) if items: required_materials.extend(items) # البحث عن قوائم المواد في النص العادي text_materials = self._extract_materials_from_text(text) if text_materials: required_materials.extend(text_materials) # إزالة التكرارات unique_materials = [] material_names = set() for material in required_materials: name = material.get('name', '').lower() if name and name not in material_names: material_names.add(name) unique_materials.append(material) # تقييم توفر المواد محلياً for material in unique_materials: material['local_availability'] = self._check_local_availability(material['name']) return unique_materials def _check_local_availability(self, material_name: str) -> float: """ تقييم مدى توفر المادة محلياً المعاملات: ---------- material_name : str اسم المادة المخرجات: -------- float نسبة توفر المادة مح, '', name) # إضافة المادة إلى القائمة إذا كان الاسم غير فارغ if name and len(name) > 3: # تجاهل الأسماء القصيرة جدًا items.append({ "name": name, "quantity": quantity, "unit": unit, "source": "text" }) return items def _estimate_local_content(self, required_materials: List[Dict[str, Any]]) -> float: """ تقدير نسبة المحتوى المحلي المتوقعة المعاملات: ---------- required_materials : List[Dict[str, Any]] قائمة المواد المطلوبة المخرجات: -------- float نسبة المحتوى المحلي المقدرة """ if not required_materials: return 0.0 # حساب متوسط توفر المواد محلياً availability_sum = sum(material.get('local_availability', 0) for material in required_materials) avg_availability = availability_sum / len(required_materials) # تعديل التقدير بناءً على بيانات إضافية من اللوائح base_estimate = avg_availability # تعديل بناءً على فئة المشروع (مثال افتراضي) project_category = self.config.get('project_category', 'general') category_adjustment = self.local_content_regulations.get('category_adjustments', {}).get(project_category, 0) # توليد تقدير نهائي final_estimate = min(100.0, max(0.0, base_estimate + category_adjustment)) return round(final_estimate, 1) def _get_required_local_content(self, local_content_requirements: List[Dict[str, Any]], text: str) -> float: """ تحديد النسبة المطلوبة من المحتوى المحلي المعاملات: ---------- local_content_requirements : List[Dict[str, Any]] متطلبات المحتوى المحلي المستخرجة text : str النص الكامل للمناقصة المخرجات: -------- float النسبة المطلوبة من المحتوى المحلي """ # البحث في متطلبات المحتوى المحلي المستخرجة if local_content_requirements: # البحث عن متطلبات الحد الأدنى الإلزامية mandatory_mins = [req['percentage'] for req in local_content_requirements if req['type'] == 'الحد الأدنى' and req['is_mandatory']] if mandatory_mins: return max(mandatory_mins) # البحث في النص عن النسب المطلوبة percentage_pattern = r'(?:نسبة|حد أدنى)[^%\d١٢٣٤٥٦٧٨٩٠]*?(\d+(?:\.\d+)?)\s*%' percentage_matches = re.findall(percentage_pattern, text) if percentage_matches: # استخدام أول نسبة مئوية تم العثور عليها try: return float(percentage_matches[0]) except ValueError: pass # البحث في اللوائح عن النسبة الافتراضية # استخدام قيمة افتراضية إذا لم يتم العثور على نسبة محددة return self.local_content_regulations.get('default_percentage', 30.0) def _identify_potential_suppliers(self, required_materials: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """ تحديد الموردين المحليين المحتملين للمواد المطلوبة المعاملات: ---------- required_materials : List[Dict[str, Any]] قائمة المواد المطلوبة المخرجات: -------- List[Dict[str, Any]] قائمة الموردين المحتملين """ potential_suppliers = [] # البحث عن موردين لكل مادة for material in required_materials: material_name = material['name'] suppliers_for_material = [] # البحث في قاعدة بيانات الموردين for supplier in self.local_suppliers_db: # التحقق مما إذا كان المورد يوفر هذه المادة if any(self._simple_similarity(material_name, product) > 0.6 for product in supplier.get('products', [])): suppliers_for_material.append({ "name": supplier.get('name', ''), "region": supplier.get('region', ''), "reliability": supplier.get('reliability', 0), "contact": supplier.get('contact', '') }) # إضافة الموردين المحتملين للمادة if suppliers_for_material: material['potential_suppliers'] = suppliers_for_material for supplier in suppliers_for_material: # إضافة المورد إلى القائمة العامة إذا لم يكن موجودًا بالفعل if not any(s.get('name') == supplier['name'] for s in potential_suppliers): supplier_info = supplier.copy() supplier_info['materials'] = [material_name] potential_suppliers.append(supplier_info) else: # إضافة المادة إلى قائمة المواد التي يوفرها المورد for s in potential_suppliers: if s.get('name') == supplier['name'] and material_name not in s.get('materials', []): s.setdefault('materials', []).append(material_name) # ترتيب الموردين حسب عدد المواد ودرجة الموثوقية return sorted(potential_suppliers, key=lambda s: (len(s.get('materials', [])), s.get('reliability', 0)), reverse=True) def _generate_improvement_strategies(self, estimated_local_content: float, required_local_content: float, required_materials: List[Dict[str, Any]]) -> List[str]: """ اقتراح استراتيجيات تحسين المحتوى المحلي المعاملات: ---------- estimated_local_content : float نسبة المحتوى المحلي المقدرة required_local_content : float نسبة المحتوى المحلي المطلوبة required_materials : List[Dict[str, Any]] قائمة المواد المطلوبة المخرجات: -------- List[str] استراتيجيات تحسين المحتوى المحلي """ strategies = [] # التحقق مما إذا كانت النسبة المقدرة أقل من النسبة المطلوبة if estimated_local_content < required_local_content: strategies.append(f"زيادة نسبة المحتوى المحلي من {estimated_local_content:.1f}% إلى {required_local_content:.1f}% على الأقل") # تحديد المواد ذات التوفر المنخفض محلياً low_availability_materials = [m for m in required_materials if m.get('local_availability', 0) < 50] if low_availability_materials: materials_str = ", ".join([m['name'] for m in low_availability_materials[:3]]) if len(low_availability_materials) > 3: materials_str += f" وغيرها ({len(low_availability_materials) - 3} مواد أخرى)" strategies.append(f"البحث عن بدائل محلية للمواد التالية: {materials_str}") # استراتيجيات عامة strategies.extend([ "الشراكة مع شركات محلية لتوفير المواد والخدمات", "الاستفادة من برامج دعم المحتوى المحلي المقدمة من هيئة المحتوى المحلي", "تدريب وتوظيف كوادر سعودية لزيادة نسبة التوطين", "التعاقد مع مصنعين محليين للمساهمة في التصنيع المحلي" ]) else: strategies.append(f"الحفاظ على نسبة المحتوى المحلي الحالية ({estimated_local_content:.1f}%) التي تفوق المتطلبات ({required_local_content:.1f}%)") # استراتيجيات للتحسين المستمر strategies.extend([ "توثيق وإبراز نسبة المحتوى المحلي العالية في العرض المقدم", "استخدام نسبة المحتوى المحلي كميزة تنافسية في المناقصة", "التركيز على رفع جودة المكونات المحلية لتعزيز القيمة المضافة" ]) # استراتيجيات لتحسين التوثيق والامتثال strategies.extend([ "توثيق مصادر المواد والخدمات المحلية بشكل دقيق", "تطبيق منهجية حساب المحتوى المحلي وفقًا لمتطلبات هيئة المحتوى المحلي", "إعداد خطة تفصيلية للمحتوى المحلي ومتابعة تنفيذها خلال المشروع" ]) return strategies def _load_local_materials(self) -> List[Dict[str, Any]]: """ تحميل قاعدة بيانات المواد المحلية المخرجات: -------- List[Dict[str, Any]] قاعدة بيانات المواد المحلية """ try: file_path = 'data/templates/local_materials.json' if os.path.exists(file_path): with open(file_path, 'r', encoding='utf-8') as f: return json.load(f) else: logger.warning(f"ملف قاعدة بيانات المواد المحلية غير موجود: {file_path}") # إنشاء قاعدة بيانات افتراضية return self._create_default_materials_db() except Exception as e: logger.error(f"فشل في تحميل قاعدة بيانات المواد المحلية: {str(e)}") return self._create_default_materials_db() def _load_local_suppliers(self) -> List[Dict[str, Any]]: """ تحميل قاعدة بيانات الموردين المحليين المخرجات: -------- List[Dict[str, Any]] قاعدة بيانات الموردين المحليين """ try: file_path = 'data/templates/local_suppliers.json' if os.path.exists(file_path): with open(file_path, 'r', encoding='utf-8') as f: return json.load(f) else: logger.warning(f"ملف قاعدة بيانات الموردين المحليين غير موجود: {file_path}") # إنشاء قاعدة بيانات افتراضية return self._create_default_suppliers_db() except Exception as e: logger.error(f"فشل في تحميل قاعدة بيانات الموردين المحليين: {str(e)}") return self._create_default_suppliers_db() def _load_regulations(self) -> Dict[str, Any]: """ تحميل لوائح وقواعد المحتوى المحلي المخرجات: -------- Dict[str, Any] لوائح وقواعد المحتوى المحلي """ try: file_path = 'data/templates/local_content_regulations.json' if os.path.exists(file_path): with open(file_path, 'r', encoding='utf-8') as f: return json.load(f) else: logger.warning(f"ملف لوائح المحتوى المحلي غير موجود: {file_path}") # إنشاء لوائح افتراضية return self._create_default_regulations() except Exception as e: logger.error(f"فشل في تحميل لوائح المحتوى المحلي: {str(e)}") return self._create_default_regulations() def _create_default_materials_db(self) -> List[Dict[str, Any]]: """ إنشاء قاعدة بيانات افتراضية للمواد المحلية المخرجات: -------- List[Dict[str, Any]] قاعدة بيانات افتراضية للمواد المحلية """ return [ {"name": "حديد تسليح", "category": "مواد بناء", "availability_percentage": 90.0}, {"name": "أسمنت", "category": "مواد بناء", "availability_percentage": 95.0}, {"name": "خرسانة جاهزة", "category": "مواد بناء", "availability_percentage": 100.0}, {"name": "بلاط", "category": "مواد بناء", "availability_percentage": 80.0}, {"name": "رخام", "category": "مواد بناء", "availability_percentage": 60.0}, {"name": "دهانات", "category": "مواد بناء", "availability_percentage": 75.0}, {"name": "زجاج", "category": "مواد بناء", "availability_percentage": 50.0}, {"name": "أسلاك كهربائية", "category": "كهرباء", "availability_percentage": 60.0}, {"name": "لوحات كهربائية", "category": "كهرباء", "availability_percentage": 55.0}, {"name": "مفاتيح كهربائية", "category": "كهرباء", "availability_percentage": 45.0}, {"name": "أنابيب مياه", "category": "سباكة", "availability_percentage": 70.0}, {"name": "خزانات مياه", "category": "سباكة", "availability_percentage": 95.0}, {"name": "مواسير صرف", "category": "سباكة", "availability_percentage": 85.0}, {"name": "وحدات تكييف", "category": "تكييف", "availability_percentage": 30.0}, {"name": "معدات تبريد", "category": "تكييف", "availability_percentage": 25.0}, {"name": "أجهزة تهوية", "category": "تكييف", "availability_percentage": 40.0}, {"name": "أثاث مكتبي", "category": "أثاث", "availability_percentage": 70.0}, {"name": "أثاث منزلي", "category": "أثاث", "availability_percentage": 65.0}, {"name": "أبواب خشبية", "category": "نجارة", "availability_percentage": 80.0}, {"name": "نوافذ ألمنيوم", "category": "ألمنيوم", "availability_percentage": 75.0} ] def _create_default_suppliers_db(self) -> List[Dict[str, Any]]: """ إنشاء قاعدة بيانات افتراضية للموردين المحليين المخرجات: -------- List[Dict[str, Any]] قاعدة بيانات افتراضية للموردين المحليين """ return [ { "name": "شركة الراجحي للحديد", "region": "الرياض", "category": "مواد بناء", "products": ["حديد تسليح", "حديد مجلفن"], "reliability": 4.5, "contact": "info@rajhi-steel.sa", "nitaqat_category": "بلاتيني" }, { "name": "شركة امجاد للسباكة", "region": "الرياض", "category": "سباكة", "products": ["أنابيب مياه", "خزانات مياه", "مواسير صرف"], "reliability": 3.9, "contact": "info@amjad-plumbing.sa", "nitaqat_category": "أخضر متوسط" }, { "name": "مصنع الخليج للزجاج", "region": "جدة", "category": "مواد بناء", "products": ["زجاج", "ألواح زجاجية", "واجهات زجاجية"], "reliability": 4.0, "contact": "info@gulf-glass.sa", "nitaqat_category": "أخضر مرتفع" }, { "name": "مؤسسة الديار للدهانات", "region": "الرياض", "category": "مواد بناء", "products": ["دهانات", "طلاء جدران", "عوازل"], "reliability": 3.7, "contact": "info@aldiyar-paints.sa", "nitaqat_category": "أخضر منخفض" }, { "name": "شركة الأثاث المكتبي المتحدة", "region": "الرياض", "category": "أثاث", "products": ["أثاث مكتبي", "كراسي", "طاولات", "خزائن"], "reliability": 4.1, "contact": "info@united-furniture.sa", "nitaqat_category": "أخضر مرتفع" }, { "name": "شركة ابن غنيم للنجارة", "region": "الدمام", "category": "نجارة", "products": ["أبواب خشبية", "نوافذ خشبية", "أثاث منزلي"], "reliability": 4.3, "contact": "info@ibnghonaim.sa", "nitaqat_category": "أخضر مرتفع" }, { "name": "الشركة العربية للألمنيوم", "region": "جدة", "category": "ألمنيوم", "products": ["نوافذ ألمنيوم", "أبواب ألمنيوم", "واجهات ألمنيوم"], "reliability": 4.4, "contact": "info@arabian-aluminum.sa", "nitaqat_category": "بلاتيني" } ] def _create_default_regulations(self) -> Dict[str, Any]: """ إنشاء لوائح افتراضية للمحتوى المحلي المخرجات: -------- Dict[str, Any] لوائح افتراضية للمحتوى المحلي """ return { "default_percentage": 30.0, "category_adjustments": { "construction": 5.0, "it": -5.0, "manufacturing": 10.0, "services": 0.0, "general": 0.0 }, "sector_requirements": { "oil_and_gas": 40.0, "electricity": 35.0, "water": 25.0, "telecommunications": 20.0, "transportation": 30.0, "healthcare": 25.0, "education": 35.0 }, "calculation_method": "السعر العرض الذي يحتوي على نسبة أعلى من المحتوى المحلي يحصل على ميزة سعرية تصل إلى 10% من السعر", "documentation_requirements": [ "شهادات المنشأ للمواد والمنتجات", "عقود التوريد مع الموردين المحليين", "كشوف رواتب الموظفين السعوديين", "شهادات تصنيف المقاولين", "شهادات نطاقات للتوطين" ], "reference_documents": [ "دليل هيئة المحتوى المحلي وتنمية القطاع الخاص", "لائحة تفضيل المنتجات المحلية في المشتريات الحكومية", "دليل تطبيق آلية الوزن النسبي للمحتوى المحلي في التقييم المالي" ], "last_updated": "2023-06-15" }category": "بلاتيني" }, { "name": "اسمنت اليمامة", "region": "الرياض", "category": "مواد بناء", "products": ["أسمنت", "خرسانة جاهزة"], "reliability": 4.7, "contact": "info@yamama-cement.sa", "nitaqat_category": "بلاتيني" }, { "name": "الشركة السعودية للصناعات الكهربائية", "region": "جدة", "category": "كهرباء", "products": ["أسلاك كهربائية", "لوحات كهربائية", "مفاتيح كهربائية"], "reliability": 4.2, "contact": "info@siec.sa", "nitaqat_category": "أخضر مرتفع" }, { "name": "شركة الزامل للتكييف", "region": "الدمام", "category": "تكييف", "products": ["وحدات تكييف", "معدات تبريد", "أجهزة تهوية"], "reliability": 4.8, "contact": "info@zamil-ac.sa", "nitaqat_""" محلل المحتوى المحلي يقوم بتحليل متطلبات المحتوى المحلي في المناقصات وتقييم نسب المحتوى المحلي """ import re import json import logging import os from typing import Dict, List, Any, Tuple, Optional, Union import numpy as np logger = logging.getLogger(__name__) class LocalContentAnalyzer: """ محلل المحتوى المحلي في المناقصات """ def __init__(self, model_loader, config=None): """ تهيئة محلل المحتوى المحلي المعاملات: ---------- model_loader : ModelLoader محمّل النماذج المستخدمة للتحليل config : Dict, optional إعدادات المحلل """ self.config = config or {} self.model_loader = model_loader # تحميل قوائم المنتجات والمواد المحلية self.local_materials_db = self._load_local_materials() self.local_suppliers_db = self._load_local_suppliers() # تحميل قواعد ولوائح المحتوى المحلي self.local_content_regulations = self._load_regulations() # تحميل نموذج تحليل النصوص إذا كان متاحاً self.ner_model = None if hasattr(model_loader, 'get_ner_model'): try: self.ner_model = model_loader.get_ner_model() logger.info("تم تحميل نموذج التعرف على الكيانات المسماة") except Exception as e: logger.warning(f"فشل في تحميل نموذج التعرف على الكيانات المسماة: {str(e)}") logger.info("تم تهيئة محلل المحتوى المحلي") def analyze(self, extracted_text: str) -> Dict[str, Any]: """ تحليل المحتوى المحلي من نص المناقصة المعاملات: ---------- extracted_text : str النص المستخرج من المناقصة المخرجات: -------- Dict[str, Any] نتائج تحليل المحتوى المحلي """ try: logger.info("بدء تحليل المحتوى المحلي") # استخراج متطلبات المحتوى المحلي local_content_requirements = self._extract_local_content_requirements(extracted_text) # استخراج المواد والمنتجات المطلوبة required_materials = self._extract_required_materials(extracted_text) # تقدير نسبة المحتوى المحلي المتوقعة estimated_local_content = self._estimate_local_content(required_materials) # تحديد النسبة المطلوبة من المحتوى المحلي required_local_content = self._get_required_local_content(local_content_requirements, extracted_text) # تحديد الموردين المحليين المحتملين potential_suppliers = self._identify_potential_suppliers(required_materials) # اقتراح استراتيجيات تحسين المحتوى المحلي improvement_strategies = self._generate_improvement_strategies( estimated_local_content, required_local_content, required_materials ) # إعداد النتائج results = { "estimated_local_content": estimated_local_content, "required_local_content": required_local_content, "local_content_requirements": local_content_requirements, "required_materials": required_materials, "potential_suppliers": potential_suppliers, "improvement_strategies": improvement_strategies } logger.info(f"اكتمل تحليل المحتوى المحلي: تقدير {estimated_local_content:.1f}%، مطلوب {required_local_content:.1f}%") return results except Exception as e: logger.error(f"فشل في تحليل المحتوى المحلي: {str(e)}") return { "estimated_local_content": 0, "required_local_content": 0, "local_content_requirements": [], "required_materials": [], "potential_suppliers": [], "improvement_strategies": [ "حدث خطأ في تحليل المحتوى المحلي. يرجى التحقق من البيانات المدخلة." ], "error": str(e) } def _extract_local_content_requirements(self, text: str) -> List[Dict[str, Any]]: """ استخراج متطلبات المحتوى المحلي من نص المناقصة المعاملات: ---------- text : str النص المستخرج من المناقصة المخرجات: -------- List[Dict[str, Any]] قائمة بمتطلبات المحتوى المحلي """ requirements = [] # البحث عن الفقرات المتعلقة بالمحتوى المحلي local_content_paragraphs = self._find_local_content_paragraphs(text) for paragraph in local_content_paragraphs: # البحث عن النسب المئوية percentage_matches = re.findall(r'(\d+(?:\.\d+)?)\s*(%|في المائة|بالمائة|نسبة)', paragraph) for match in percentage_matches: percentage = float(match[0]) # التحقق مما إذا كانت النسبة في النطاق المعقول للمحتوى المحلي if 0 <= percentage <= 100: # تحديد نوع المتطلب req_type = "غير محدد" if re.search(r'الحد الأدنى|الأقل|على الأقل', paragraph): req_type = "الحد الأدنى" elif re.search(r'الحد الأقصى|كحد أقصى', paragraph): req_type = "الحد الأقصى" elif re.search(r'هدف|مستهدف', paragraph): req_type = "مستهدف" # تحديد الفئة category = "عام" if re.search(r'توظيف|عمالة|السعودة|التوطين|الموظفين', paragraph): category = "توظيف" elif re.search(r'خدمات|استشارات', paragraph): category = "خدمات" elif re.search(r'توريد|مواد|منتجات|بضائع', paragraph): category = "منتجات" elif re.search(r'تدريب|تأهيل|تطوير', paragraph): category = "تدريب" # تحديد الإلزامية is_mandatory = bool(re.search(r'إلزامي|يجب|ضروري|لا بد|لابد|مطلوب|يلتزم|ملزم', paragraph)) requirements.append({ "description": paragraph[:200] + ("..." if len(paragraph) > 200 else ""), "percentage": percentage, "type": req_type, "category": category, "is_mandatory": is_mandatory }) return requirements def _find_local_content_paragraphs(self, text: str) -> List[str]: """ البحث عن الفقرات المتعلقة بالمحتوى المحلي المعاملات: ---------- text : str النص المستخرج من المناقصة المخرجات: -------- List[str] قائمة بالفقرات المتعلقة بالمحتوى المحلي """ # تقسيم النص إلى فقرات paragraphs = re.split(r'\n\s*\n', text) # الكلمات المفتاحية للمحتوى المحلي local_content_keywords = [ 'المحتوى المحلي', 'التوطين', 'المواد المحلية', 'المنتجات المحلية', 'نسبة المحتوى', 'مبادرة المحتوى المحلي', 'هيئة المحتوى المحلي', 'منتجات وطنية', 'صنع في السعودية', 'المصنّعين المحليين', 'الموردين المحليين', 'القيمة المضافة', 'نقل التقنية', 'توطين الوظائف', 'السعودة', ] # البحث عن الفقرات التي تحتوي على كلمات مفتاحية للمحتوى المحلي local_content_paragraphs = [] for paragraph in paragraphs: paragraph = paragraph.strip() if not paragraph: continue # البحث عن الكلمات المفتاحية في الفقرة for keyword in local_content_keywords: if keyword in paragraph.lower(): local_content_paragraphs.append(paragraph) break return local_content_paragraphs def _extract_required_materials(self, text: str) -> List[Dict[str, Any]]: """ استخراج المواد والمنتجات المطلوبة من نص المناقصة المعاملات: ---------- text : str النص المستخرج من المناقصة المخرجات: -------- List[Dict[str, Any]] قائمة بالمواد والمنتجات المطلوبة """ required_materials = [] # البحث عن جداول المواصفات والكميات tables = self._extract_tables(text) # البحث عن قوائم المواد في النص for table in tables: items = self._parse_table_for_materials(table) if items: required_materials.extend(items) # البحث عن قوائم المواد في النص العادي text_materials = self._extract_materials_from_text(text) if text_materials: required_materials.extend(text_materials) # إزالة التكرارات unique_materials = [] material_names = set() for material in required_materials: name = material.get('name', '').lower() if name and name not in material_names: material_names.add(name) unique_materials.append(material) # تقييم توفر المواد محلياً for material in unique_materials: material['local_availability'] = self._check_local_availability(material['name']) return unique_materials def _check_local_availability(self, material_name: str) -> float: """ تقييم مدى توفر المادة محلياً بناءً على قاعدة بيانات أو تحليل البيانات المتاحة. المعاملات: ---------- material_name : str اسم المادة التي نريد التحقق من توفرها محليًا. المخرجات: -------- float نسبة توفر المادة محليًا، تتراوح بين 0.0 (غير متوفرة) إلى 1.0 (متوفرة بالكامل). """ # قاعدة بيانات تقديرية لتوفر المواد محليًا (يمكن استبدالها بمصدر بيانات فعلي) local_materials = { "حديد": 0.95, # متوفر بنسبة 95% "أسمنت": 0.9, # متوفر بنسبة 90% "خشب": 0.7, # متوفر بنسبة 70% "زجاج": 0.6, # متوفر بنسبة 60% "ألمنيوم": 0.85, # متوفر بنسبة 85% "بلاستيك": 0.8, # متوفر بنسبة 80% "نحاس": 0.5, # متوفر بنسبة 50% "إلكترونيات متخصصة": 0.3 # متوفر بنسبة 30% } # البحث عن المادة في القائمة for key in local_materials: if key in material_name: return local_materials[key] # إذا لم تكن المادة موجودة في القائمة، نفترض توفرها بنسبة 0.5 كافتراضي return 0.5