Spaces:
Sleeping
Sleeping
نسبة توفر المادة محلياً (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": "[email protected]", | |
"nitaqat_category": "بلاتيني" | |
}, | |
{ | |
"name": "شركة امجاد للسباكة", | |
"region": "الرياض", | |
"category": "سباكة", | |
"products": ["أنابيب مياه", "خزانات مياه", "مواسير صرف"], | |
"reliability": 3.9, | |
"contact": "[email protected]", | |
"nitaqat_category": "أخضر متوسط" | |
}, | |
{ | |
"name": "مصنع الخليج للزجاج", | |
"region": "جدة", | |
"category": "مواد بناء", | |
"products": ["زجاج", "ألواح زجاجية", "واجهات زجاجية"], | |
"reliability": 4.0, | |
"contact": "[email protected]", | |
"nitaqat_category": "أخضر مرتفع" | |
}, | |
{ | |
"name": "مؤسسة الديار للدهانات", | |
"region": "الرياض", | |
"category": "مواد بناء", | |
"products": ["دهانات", "طلاء جدران", "عوازل"], | |
"reliability": 3.7, | |
"contact": "[email protected]", | |
"nitaqat_category": "أخضر منخفض" | |
}, | |
{ | |
"name": "شركة الأثاث المكتبي المتحدة", | |
"region": "الرياض", | |
"category": "أثاث", | |
"products": ["أثاث مكتبي", "كراسي", "طاولات", "خزائن"], | |
"reliability": 4.1, | |
"contact": "[email protected]", | |
"nitaqat_category": "أخضر مرتفع" | |
}, | |
{ | |
"name": "شركة ابن غنيم للنجارة", | |
"region": "الدمام", | |
"category": "نجارة", | |
"products": ["أبواب خشبية", "نوافذ خشبية", "أثاث منزلي"], | |
"reliability": 4.3, | |
"contact": "[email protected]", | |
"nitaqat_category": "أخضر مرتفع" | |
}, | |
{ | |
"name": "الشركة العربية للألمنيوم", | |
"region": "جدة", | |
"category": "ألمنيوم", | |
"products": ["نوافذ ألمنيوم", "أبواب ألمنيوم", "واجهات ألمنيوم"], | |
"reliability": 4.4, | |
"contact": "[email protected]", | |
"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": "[email protected]", | |
"nitaqat_category": "بلاتيني" | |
}, | |
{ | |
"name": "الشركة السعودية للصناعات الكهربائية", | |
"region": "جدة", | |
"category": "كهرباء", | |
"products": ["أسلاك كهربائية", "لوحات كهربائية", "مفاتيح كهربائية"], | |
"reliability": 4.2, | |
"contact": "[email protected]", | |
"nitaqat_category": "أخضر مرتفع" | |
}, | |
{ | |
"name": "شركة الزامل للتكييف", | |
"region": "الدمام", | |
"category": "تكييف", | |
"products": ["وحدات تكييف", "معدات تبريد", "أجهزة تهوية"], | |
"reliability": 4.8, | |
"contact": "[email protected]", | |
"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 | |