Spaces:
Paused
Paused
Create modules/local_content.py
Browse files- modules/local_content.py +706 -0
modules/local_content.py
ADDED
@@ -0,0 +1,706 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
}
|
2 |
+
|
3 |
+
def calculate(self, extracted_data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
|
4 |
+
"""
|
5 |
+
حساب نسبة المحتوى المحلي بناءً على البيانات المستخرجة من المستندات
|
6 |
+
|
7 |
+
المعاملات:
|
8 |
+
----------
|
9 |
+
extracted_data : Dict[str, Any]
|
10 |
+
البيانات المستخرجة من المستندات
|
11 |
+
**kwargs : Dict[str, Any]
|
12 |
+
معاملات إضافية مثل نوع المشروع، الميزانية، الموقع، المدة
|
13 |
+
|
14 |
+
المخرجات:
|
15 |
+
--------
|
16 |
+
Dict[str, Any]
|
17 |
+
نتائج حساب المحتوى المحلي
|
18 |
+
"""
|
19 |
+
# تهيئة نتائج حساب المحتوى المحلي
|
20 |
+
local_content_results = {
|
21 |
+
"overall_percentage": 0.0,
|
22 |
+
"breakdown": {},
|
23 |
+
"requirements": [],
|
24 |
+
"recommendations": [],
|
25 |
+
"nitaqat_analysis": {},
|
26 |
+
}
|
27 |
+
|
28 |
+
# تحديد نوع المشروع والقطاع
|
29 |
+
project_type = kwargs.get("project_type", "")
|
30 |
+
sector = self._determine_sector(project_type, extracted_data)
|
31 |
+
|
32 |
+
# استخراج متطلبات المحتوى المحلي من البيانات
|
33 |
+
local_content_info = self._extract_local_content_info(extracted_data)
|
34 |
+
|
35 |
+
# حساب المحتوى المحلي بناءً على القطاع
|
36 |
+
sector_data = self.local_content_data["sectors"].get(sector, self.local_content_data["sectors"]["عام"])
|
37 |
+
|
38 |
+
# تحليل القوى العاملة (الموارد البشرية)
|
39 |
+
manpower_analysis = self._analyze_manpower(extracted_data, sector_data)
|
40 |
+
local_content_results["breakdown"]["manpower"] = manpower_analysis["percentage"]
|
41 |
+
|
42 |
+
# تحليل المواد
|
43 |
+
materials_analysis = self._analyze_materials(extracted_data, sector_data)
|
44 |
+
local_content_results["breakdown"]["materials"] = materials_analysis["percentage"]
|
45 |
+
|
46 |
+
# تحليل الخدمات
|
47 |
+
services_analysis = self._analyze_services(extracted_data, sector_data)
|
48 |
+
local_content_results["breakdown"]["services"] = services_analysis["percentage"]
|
49 |
+
|
50 |
+
# حساب النسبة الإجمالية للمحتوى المحلي
|
51 |
+
weights = sector_data["weights"]
|
52 |
+
overall_percentage = (
|
53 |
+
weights["manpower"] * manpower_analysis["percentage"] +
|
54 |
+
weights["materials"] * materials_analysis["percentage"] +
|
55 |
+
weights["services"] * services_analysis["percentage"]
|
56 |
+
)
|
57 |
+
|
58 |
+
local_content_results["overall_percentage"] = round(overall_percentage, 2)
|
59 |
+
|
60 |
+
# تحليل الامتثال لمتطلبات نطاقات
|
61 |
+
nitaqat_analysis = self._analyze_nitaqat_compliance(extracted_data, sector)
|
62 |
+
local_content_results["nitaqat_analysis"] = nitaqat_analysis
|
63 |
+
|
64 |
+
# إعداد متطلبات المحتوى المحلي
|
65 |
+
requirements = self._prepare_local_content_requirements(sector_data, local_content_info)
|
66 |
+
local_content_results["requirements"] = requirements
|
67 |
+
|
68 |
+
# إعداد توصيات لتحسين المحتوى المحلي
|
69 |
+
recommendations = self._generate_recommendations(
|
70 |
+
overall_percentage,
|
71 |
+
sector_data,
|
72 |
+
manpower_analysis,
|
73 |
+
materials_analysis,
|
74 |
+
services_analysis,
|
75 |
+
nitaqat_analysis
|
76 |
+
)
|
77 |
+
local_content_results["recommendations"] = recommendations
|
78 |
+
|
79 |
+
return local_content_results
|
80 |
+
|
81 |
+
def _determine_sector(self, project_type: str, extracted_data: Dict[str, Any]) -> str:
|
82 |
+
"""
|
83 |
+
تحديد القطاع بناءً على نوع المشروع والبيانات المستخرجة
|
84 |
+
"""
|
85 |
+
# قاموس لتحويل أنواع المشاريع الشائعة إلى قطاعات
|
86 |
+
project_to_sector = {
|
87 |
+
"إنشاءات": "الإنشاءات",
|
88 |
+
"مباني": "الإنشاءات",
|
89 |
+
"طرق": "الإنشاءات",
|
90 |
+
"جسور": "الإنشاءات",
|
91 |
+
"تقنية معلومات": "تقنية المعلومات",
|
92 |
+
"برمجيات": "تقنية المعلومات",
|
93 |
+
"تطبيقات": "تقنية المعلومات",
|
94 |
+
"اتصالات": "الاتصالات",
|
95 |
+
"صحة": "الصحة",
|
96 |
+
"مستشفى": "الصحة",
|
97 |
+
"طبي": "الصحة",
|
98 |
+
"تعليم": "التعليم",
|
99 |
+
"مدارس": "التعليم",
|
100 |
+
"جامعات": "التعليم",
|
101 |
+
"نقل": "النقل",
|
102 |
+
"مواصلات": "النقل",
|
103 |
+
"طاقة": "الطاقة",
|
104 |
+
"كهرباء": "الطاقة",
|
105 |
+
"مياه": "المياه",
|
106 |
+
"صرف صحي": "المياه",
|
107 |
+
"بيئة": "البيئة",
|
108 |
+
"صناعة": "الص��اعة",
|
109 |
+
"مصانع": "الصناعة",
|
110 |
+
"تجارة": "التجارة",
|
111 |
+
"أسواق": "التجارة",
|
112 |
+
"سياحة": "السياحة",
|
113 |
+
"فنادق": "السياحة",
|
114 |
+
"مالية": "الخدمات المالية",
|
115 |
+
"بنوك": "الخدمات المالية"
|
116 |
+
}
|
117 |
+
|
118 |
+
# محاولة تحديد القطاع من نوع المشروع
|
119 |
+
if project_type:
|
120 |
+
for key, value in project_to_sector.items():
|
121 |
+
if key in project_type.lower():
|
122 |
+
return value
|
123 |
+
|
124 |
+
# إذا لم يتم تحديد القطاع من نوع المشروع، نحاول تحديده من البيانات المستخرجة
|
125 |
+
if "text" in extracted_data:
|
126 |
+
text = extracted_data["text"].lower()
|
127 |
+
sector_scores = {}
|
128 |
+
|
129 |
+
for sector in self.sectors:
|
130 |
+
score = text.count(sector.lower())
|
131 |
+
sector_scores[sector] = score
|
132 |
+
|
133 |
+
# اختيار القطاع الأكثر ذكراً
|
134 |
+
if sector_scores:
|
135 |
+
max_sector = max(sector_scores, key=sector_scores.get)
|
136 |
+
if sector_scores[max_sector] > 0:
|
137 |
+
return max_sector
|
138 |
+
|
139 |
+
# القطاع الافتراضي إذا لم نتمكن من تحديده
|
140 |
+
return "عام"
|
141 |
+
|
142 |
+
def _extract_local_content_info(self, extracted_data: Dict[str, Any]) -> Dict[str, Any]:
|
143 |
+
"""
|
144 |
+
استخراج معلومات المحتوى المحلي من البيانات المستخرجة
|
145 |
+
"""
|
146 |
+
local_content_info = {
|
147 |
+
"mentioned_percentage": None,
|
148 |
+
"requirements": [],
|
149 |
+
"companies": []
|
150 |
+
}
|
151 |
+
|
152 |
+
# استخراج النسبة المذكورة للمحتوى المحلي
|
153 |
+
if "local_content" in extracted_data and extracted_data["local_content"]:
|
154 |
+
local_content_data = extracted_data["local_content"]
|
155 |
+
|
156 |
+
# استخراج النسب المئوية
|
157 |
+
percentages = local_content_data.get("percentages", [])
|
158 |
+
if percentages:
|
159 |
+
# اختيار أعلى نسبة مذكورة
|
160 |
+
max_percentage = max(percentages, key=lambda x: x["value"])
|
161 |
+
local_content_info["mentioned_percentage"] = max_percentage["value"]
|
162 |
+
|
163 |
+
# استخراج المتطلبات
|
164 |
+
requirements = local_content_data.get("requirements", [])
|
165 |
+
local_content_info["requirements"] = requirements
|
166 |
+
|
167 |
+
# استخراج الشركات المحلية المذكورة
|
168 |
+
if "entities" in extracted_data and "organizations" in extracted_data["entities"]:
|
169 |
+
organizations = extracted_data["entities"]["organizations"]
|
170 |
+
|
171 |
+
for org in organizations:
|
172 |
+
org_name = org["name"].lower()
|
173 |
+
|
174 |
+
# البحث في قاعدة بيانات الشركات المحلية
|
175 |
+
for company_id, company_data in self.local_companies.items():
|
176 |
+
company_name = company_data["name"].lower()
|
177 |
+
|
178 |
+
# إذا وجدنا تطابق
|
179 |
+
if company_name in org_name or org_name in company_name:
|
180 |
+
local_content_info["companies"].append(company_data)
|
181 |
+
|
182 |
+
return local_content_info
|
183 |
+
|
184 |
+
def _analyze_manpower(self, extracted_data: Dict[str, Any], sector_data: Dict[str, Any]) -> Dict[str, Any]:
|
185 |
+
"""
|
186 |
+
تحليل المحتوى المحلي للقوى العاملة
|
187 |
+
"""
|
188 |
+
manpower_analysis = {
|
189 |
+
"percentage": 0.0,
|
190 |
+
"saudi_employees": 0,
|
191 |
+
"expat_employees": 0,
|
192 |
+
"total_employees": 0
|
193 |
+
}
|
194 |
+
|
195 |
+
# استخراج معلومات الموظفين من البيانات
|
196 |
+
saudi_keywords = ["سعودي", "مواطن", "توطين", "سعودة"]
|
197 |
+
expat_keywords = ["أجنبي", "وافد", "غير سعودي"]
|
198 |
+
|
199 |
+
# لدينا بعض البيانات الافتراضية للتوضيح
|
200 |
+
saudi_count = 0
|
201 |
+
expat_count = 0
|
202 |
+
|
203 |
+
# إذا كانت هناك شركات معروفة في البيانات المستخرجة، نستخدم نسب التوطين الخاصة بها
|
204 |
+
if "entities" in extracted_data and "organizations" in extracted_data["entities"]:
|
205 |
+
organizations = extracted_data["entities"]["organizations"]
|
206 |
+
|
207 |
+
for org in organizations:
|
208 |
+
org_name = org["name"].lower()
|
209 |
+
|
210 |
+
for company_id, company_data in self.local_companies.items():
|
211 |
+
company_name = company_data["name"].lower()
|
212 |
+
|
213 |
+
if company_name in org_name or org_name in company_name:
|
214 |
+
saudi_percentage = company_data["saudi_employees_percentage"] / 100
|
215 |
+
saudi_count += 50 * saudi_percentage # افتراض 50 موظف كمتوسط
|
216 |
+
expat_count += 50 * (1 - saudi_percentage)
|
217 |
+
|
218 |
+
# إذا لم نجد شركات معروفة، نحاول تقدير النسب من النص
|
219 |
+
if saudi_count == 0 and expat_count == 0 and "text" in extracted_data:
|
220 |
+
text = extracted_data["text"].lower()
|
221 |
+
|
222 |
+
# عدد مرات ذكر الموظفين السعوديين
|
223 |
+
saudi_mentions = sum(text.count(keyword) for keyword in saudi_keywords)
|
224 |
+
|
225 |
+
# عدد مرات ذكر الموظفين الأجانب
|
226 |
+
expat_mentions = sum(text.count(keyword) for keyword in expat_keywords)
|
227 |
+
|
228 |
+
# تقدير تقريبي للنسب
|
229 |
+
total_mentions = saudi_mentions + expat_mentions
|
230 |
+
if total_mentions > 0:
|
231 |
+
saudi_ratio = saudi_mentions / total_mentions
|
232 |
+
saudi_count = int(100 * saudi_ratio)
|
233 |
+
expat_count = 100 - saudi_count
|
234 |
+
|
235 |
+
# إذا لم نتمكن من تقدير النسب، نستخدم قيمة افتراضية
|
236 |
+
if saudi_count == 0 and expat_count == 0:
|
237 |
+
# القيمة الافتراضية بناءً على متوسط نسب التوطين في القطاع
|
238 |
+
if "الإنشاءات" in sector_data:
|
239 |
+
saudi_count = 25
|
240 |
+
elif "تقنية المعلومات" in sector_data:
|
241 |
+
saudi_count = 35
|
242 |
+
else:
|
243 |
+
saudi_count = 30
|
244 |
+
|
245 |
+
expat_count = 100 - saudi_count
|
246 |
+
|
247 |
+
# حساب النسبة المئوية للمحتوى المحلي في القوى العاملة
|
248 |
+
total_count = saudi_count + expat_count
|
249 |
+
|
250 |
+
if total_count > 0:
|
251 |
+
manpower_analysis["saudi_employees"] = saudi_count
|
252 |
+
manpower_analysis["expat_employees"] = expat_count
|
253 |
+
manpower_analysis["total_employees"] = total_count
|
254 |
+
|
255 |
+
# حساب النسبة المئوية باستخدام الأوزان
|
256 |
+
manpower_percentage = (
|
257 |
+
self.local_content_data["manpower_categories"]["saudi_employee"] * saudi_count +
|
258 |
+
self.local_content_data["manpower_categories"]["expat_employee"] * expat_count
|
259 |
+
) / total_count * 100
|
260 |
+
|
261 |
+
manpower_analysis["percentage"] = round(manpower_percentage, 2)
|
262 |
+
|
263 |
+
return manpower_analysis
|
264 |
+
|
265 |
+
def _analyze_materials(self, extracted_data: Dict[str, Any], sector_data: Dict[str, Any]) -> Dict[str, Any]:
|
266 |
+
"""
|
267 |
+
تحليل المحتوى المحلي للمواد
|
268 |
+
"""
|
269 |
+
materials_analysis = {
|
270 |
+
"percentage": 0.0,
|
271 |
+
"local_manufacturing": 0,
|
272 |
+
"local_assembly": 0,
|
273 |
+
"imported_with_local_value_add": 0,
|
274 |
+
"fully_imported": 0,
|
275 |
+
"total_materials": 0
|
276 |
+
}
|
277 |
+
|
278 |
+
# كلمات دلالية للفئات المختلفة من المواد
|
279 |
+
local_manufacturing_keywords = ["تصنيع محلي", "منتج محلي", "صناعة محلية", "صنع في السعودية", "منشأ سعودي"]
|
280 |
+
local_assembly_keywords = ["تجميع محلي", "مجمع محلياً", "تم تجميعه في السعودية"]
|
281 |
+
imported_with_local_value_keywords = ["قيمة مضافة محلية", "معالجة محلية", "قيمة محلية"]
|
282 |
+
fully_imported_keywords = ["مستورد", "استيراد كامل", "منتج أجنبي", "صنع في الخارج"]
|
283 |
+
|
284 |
+
# تقدير نسب المواد من النص
|
285 |
+
if "text" in extracted_data:
|
286 |
+
text = extracted_data["text"].lower()
|
287 |
+
|
288 |
+
# عدد مرات ذكر كل فئة
|
289 |
+
local_manufacturing_count = sum(text.count(keyword) for keyword in local_manufacturing_keywords)
|
290 |
+
local_assembly_count = sum(text.count(keyword) for keyword in local_assembly_keywords)
|
291 |
+
imported_with_local_value_count = sum(text.count(keyword) for keyword in imported_with_local_value_keywords)
|
292 |
+
fully_imported_count = sum(text.count(keyword) for keyword in fully_imported_keywords)
|
293 |
+
|
294 |
+
# إجمالي عدد المرات
|
295 |
+
total_count = (local_manufacturing_count + local_assembly_count +
|
296 |
+
imported_with_local_value_count + fully_imported_count)
|
297 |
+
|
298 |
+
if total_count > 0:
|
299 |
+
materials_analysis["local_manufacturing"] = local_manufacturing_count
|
300 |
+
materials_analysis["local_assembly"] = local_assembly_count
|
301 |
+
materials_analysis["imported_with_local_value_add"] = imported_with_local_value_count
|
302 |
+
materials_analysis["fully_imported"] = fully_imported_count
|
303 |
+
materials_analysis["total_materials"] = total_count
|
304 |
+
|
305 |
+
# إذا لم نتمكن من تقدير النسب، نستخدم قيم افتراضية بناءً على القطاع
|
306 |
+
if materials_analysis["total_materials"] == 0:
|
307 |
+
if "الإنشاءات" in sector_data:
|
308 |
+
materials_analysis["local_manufacturing"] = 30
|
309 |
+
materials_analysis["local_assembly"] = 20
|
310 |
+
materials_analysis["imported_with_local_value_add"] = 15
|
311 |
+
materials_analysis["fully_imported"] = 35
|
312 |
+
elif "تقنية المعلومات" in sector_data:
|
313 |
+
materials_analysis["local_manufacturing"] = 10
|
314 |
+
materials_analysis["local_assembly"] = 25
|
315 |
+
materials_analysis["imported_with_local_value_add"] = 15
|
316 |
+
materials_analysis["fully_imported"] = 50
|
317 |
+
else:
|
318 |
+
materials_analysis["local_manufacturing"] = 20
|
319 |
+
materials_analysis["local_assembly"] = 20
|
320 |
+
materials_analysis["imported_with_local_value_add"] = 15
|
321 |
+
materials_analysis["fully_imported"] = 45
|
322 |
+
|
323 |
+
materials_analysis["total_materials"] = (
|
324 |
+
materials_analysis["local_manufacturing"] +
|
325 |
+
materials_analysis["local_assembly"] +
|
326 |
+
materials_analysis["imported_with_local_value_add"] +
|
327 |
+
materials_analysis["fully_imported"]
|
328 |
+
)
|
329 |
+
|
330 |
+
# حساب النسبة المئوية للمحتوى المحلي في المواد
|
331 |
+
total_materials = materials_analysis["total_materials"]
|
332 |
+
|
333 |
+
if total_materials > 0:
|
334 |
+
material_categories = self.local_content_data["material_categories"]
|
335 |
+
materials_percentage = (
|
336 |
+
material_categories["local_manufacturing"] * materials_analysis["local_manufacturing"] +
|
337 |
+
material_categories["local_assembly"] * materials_analysis["local_assembly"] +
|
338 |
+
material_categories["imported_with_local_value_add"] * materials_analysis["imported_with_local_value_add"] +
|
339 |
+
material_categories["fully_imported"] * materials_analysis["fully_imported"]
|
340 |
+
) / total_materials * 100
|
341 |
+
|
342 |
+
materials_analysis["percentage"] = round(materials_percentage, 2)
|
343 |
+
|
344 |
+
return materials_analysis
|
345 |
+
|
346 |
+
def _analyze_services(self, extracted_data: Dict[str, Any], sector_data: Dict[str, Any]) -> Dict[str, Any]:
|
347 |
+
"""
|
348 |
+
تحليل المحتوى المحلي للخدمات
|
349 |
+
"""
|
350 |
+
services_analysis = {
|
351 |
+
"percentage": 0.0,
|
352 |
+
"local_service_provider": 0,
|
353 |
+
"joint_venture": 0,
|
354 |
+
"foreign_provider_with_local_partner": 0,
|
355 |
+
"foreign_provider": 0,
|
356 |
+
"total_services": 0
|
357 |
+
}
|
358 |
+
|
359 |
+
# كلمات دلالية للفئات المختلفة من الخدمات
|
360 |
+
local_provider_keywords = ["مزود خدمة محلي", "شركة محلية", "شركة سعودية", "مؤسسة محلية"]
|
361 |
+
joint_venture_keywords = ["مشروع مشترك", "شراكة", "تحالف", "ائتلاف"]
|
362 |
+
foreign_with_local_keywords = ["شريك محلي", "وكيل محلي", "وكيل سعودي", "تمثيل محلي"]
|
363 |
+
foreign_provider_keywords = ["مزود خدمة أجنبي", "شركة أجنبية", "مقاول أجنبي"]
|
364 |
+
|
365 |
+
# تقدير نسب الخدمات من النص
|
366 |
+
if "text" in extracted_data:
|
367 |
+
text = extracted_data["text"].lower()
|
368 |
+
|
369 |
+
# عدد مرات ذكر كل فئة
|
370 |
+
local_provider_count = sum(text.count(keyword) for keyword in local_provider_keywords)
|
371 |
+
joint_venture_count = sum(text.count(keyword) for keyword in joint_venture_keywords)
|
372 |
+
foreign_with_local_count = sum(text.count(keyword) for keyword in foreign_with_local_keywords)
|
373 |
+
foreign_provider_count = sum(text.count(keyword) for keyword in foreign_provider_keywords)
|
374 |
+
|
375 |
+
# إجمالي عدد المرات
|
376 |
+
total_count = (local_provider_count + joint_venture_count +
|
377 |
+
foreign_with_local_count + foreign_provider_count)
|
378 |
+
|
379 |
+
if total_count > 0:
|
380 |
+
services_analysis["local_service_provider"] = local_provider_count
|
381 |
+
services_analysis["joint_venture"] = joint_venture_count
|
382 |
+
services_analysis["foreign_provider_with_local_partner"] = foreign_with_local_count
|
383 |
+
services_analysis["foreign_provider"] = foreign_provider_count
|
384 |
+
services_analysis["total_services"] = total_count
|
385 |
+
|
386 |
+
# إذا لم نتمكن من تقدير النسب، نستخدم قيم افتراضية بناءً على القطاع
|
387 |
+
if services_analysis["total_services"] == 0:
|
388 |
+
if "الإنشاءات" in sector_data:
|
389 |
+
services_analysis["local_service_provider"] = 35
|
390 |
+
services_analysis["joint_venture"] = 25
|
391 |
+
services_analysis["foreign_provider_with_local_partner"] = 20
|
392 |
+
services_analysis["foreign_provider"] = 20
|
393 |
+
elif "تقنية المعلومات" in sector_data:
|
394 |
+
services_analysis["local_service_provider"] = 30
|
395 |
+
services_analysis["joint_venture"] = 20
|
396 |
+
services_analysis["foreign_provider_with_local_partner"] = 25
|
397 |
+
services_analysis["foreign_provider"] = 25
|
398 |
+
else:
|
399 |
+
services_analysis["local_service_provider"] = 30
|
400 |
+
services_analysis["joint_venture"] = 25
|
401 |
+
services_analysis["foreign_provider_with_local_partner"] = 20
|
402 |
+
services_analysis["foreign_provider"] = 25
|
403 |
+
|
404 |
+
services_analysis["total_services"] = (
|
405 |
+
services_analysis["local_service_provider"] +
|
406 |
+
services_analysis["joint_venture"] +
|
407 |
+
services_analysis["foreign_provider_with_local_partner"] +
|
408 |
+
services_analysis["foreign_provider"]
|
409 |
+
)
|
410 |
+
|
411 |
+
# حساب النسبة المئوية للمحتوى المحلي في الخدمات
|
412 |
+
total_services = services_analysis["total_services"]
|
413 |
+
|
414 |
+
if total_services > 0:
|
415 |
+
service_categories = self.local_content_data["service_categories"]
|
416 |
+
services_percentage = (
|
417 |
+
service_categories["local_service_provider"] * services_analysis["local_service_provider"] +
|
418 |
+
service_categories["joint_venture"] * services_analysis["joint_venture"] +
|
419 |
+
service_categories["foreign_provider_with_local_partner"] * services_analysis["foreign_provider_with_local_partner"] +
|
420 |
+
service_categories["foreign_provider"] * services_analysis["foreign_provider"]
|
421 |
+
) / total_services * 100
|
422 |
+
|
423 |
+
services_analysis["percentage"] = round(services_percentage, 2)
|
424 |
+
|
425 |
+
return services_analysis
|
426 |
+
|
427 |
+
def _analyze_nitaqat_compliance(self, extracted_data: Dict[str, Any], sector: str) -> Dict[str, Any]:
|
428 |
+
"""
|
429 |
+
تحليل الامتثال لمتطلبات نطاقات
|
430 |
+
"""
|
431 |
+
nitaqat_analysis = {
|
432 |
+
"compliant": False,
|
433 |
+
"nitaqat_category": "غير محدد",
|
434 |
+
"company_size": "غير محدد",
|
435 |
+
"required_saudi_percentage": 0,
|
436 |
+
"estimated_saudi_percentage": 0
|
437 |
+
}
|
438 |
+
|
439 |
+
# تحديد حجم الشركة بناءً على القيمة التقديرية للمشروع
|
440 |
+
project_value = 0
|
441 |
+
if "financial_data" in extracted_data and "total_cost" in extracted_data["financial_data"]:
|
442 |
+
project_value = extracted_data["financial_data"]["total_cost"]["value"]
|
443 |
+
|
444 |
+
if project_value == 0 and "text" in extracted_data:
|
445 |
+
# محاولة استخراج قيمة المشروع من النص
|
446 |
+
text = extracted_data["text"].lower()
|
447 |
+
value_patterns = [
|
448 |
+
r'قيمة المشروع[^\d]*([\d.,]+)[^\d]*(ريال|ر\.س)',
|
449 |
+
r'قيمة العقد[^\d]*([\d.,]+)[^\d]*(ريال|ر\.س)',
|
450 |
+
r'القيمة الإجمالية[^\d]*([\d.,]+)[^\d]*(ريال|ر\.س)'
|
451 |
+
]
|
452 |
+
|
453 |
+
for pattern in value_patterns:
|
454 |
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
455 |
+
if matches:
|
456 |
+
try:
|
457 |
+
project_value = float(matches[0][0].replace(',', ''))
|
458 |
+
break
|
459 |
+
except:
|
460 |
+
pass
|
461 |
+
|
462 |
+
# تحديد حجم الشركة بناءً على قيمة المشروع
|
463 |
+
company_size = "صغيرة"
|
464 |
+
if project_value >= 10000000: # أكثر من 10 مليون ريال
|
465 |
+
company_size = "كبيرة"
|
466 |
+
elif project_value >= 3000000: # بين 3 و 10 مليون ريال
|
467 |
+
company_size = "متوسطة"
|
468 |
+
|
469 |
+
nitaqat_analysis["company_size"] = company_size
|
470 |
+
|
471 |
+
# تحديد نسبة التوطين المطلوبة
|
472 |
+
if sector in self.nitaqat_data and company_size in self.nitaqat_data[sector]:
|
473 |
+
# نفترض أننا نريد الامتثال للفئة الخضراء المتوسطة
|
474 |
+
required_percentage = (
|
475 |
+
self.nitaqat_data[sector][company_size]["أخضر متوسط"]["min"] +
|
476 |
+
self.nitaqat_data[sector][company_size]["أخضر متوسط"]["max"]
|
477 |
+
) / 2
|
478 |
+
|
479 |
+
nitaqat_analysis["required_saudi_percentage"] = required_percentage
|
480 |
+
else:
|
481 |
+
# قيمة افتراضية
|
482 |
+
nitaqat_analysis["required_saudi_percentage"] = 20
|
483 |
+
|
484 |
+
# تقدير نسبة التوطين الفعلية
|
485 |
+
estimated_saudi_percentage = 0
|
486 |
+
|
487 |
+
# إذا كانت هناك شركات معروفة في البيانات المستخرجة، نستخدم نسبة التوطين الخاصة بها
|
488 |
+
if "entities" in extracted_data and "organizations" in extracted_data["entities"]:
|
489 |
+
organizations = extracted_data["entities"]["organizations"]
|
490 |
+
|
491 |
+
for org in organizations:
|
492 |
+
org_name = org["name"].lower()
|
493 |
+
|
494 |
+
for company_id, company_data in self.local_companies.items():
|
495 |
+
company_name = company_data["name"].lower()
|
496 |
+
|
497 |
+
if company_name in org_name or org_name in company_name:
|
498 |
+
estimated_saudi_percentage = company_data["saudi_employees_percentage"]
|
499 |
+
nitaqat_analysis["nitaqat_category"] = company_data["nitaqat_category"]
|
500 |
+
break
|
501 |
+
|
502 |
+
# إذا لم نتمكن من تقدير النسبة، نستخدم قيمة افتراضية
|
503 |
+
if estimated_saudi_percentage == 0:
|
504 |
+
if "الإنشاءات" in sector:
|
505 |
+
estimated_saudi_percentage = 15
|
506 |
+
elif "تقنية المعلومات" in sector:
|
507 |
+
estimated_saudi_percentage = 25
|
508 |
+
else:
|
509 |
+
estimated_saudi_percentage = 20
|
510 |
+
|
511 |
+
nitaqat_analysis["estimated_saudi_percentage"] = estimated_saudi_percentage
|
512 |
+
|
513 |
+
# تحديد فئة نطاقات والامتثال
|
514 |
+
if sector in self.nitaqat_data and company_size in self.nitaqat_data[sector]:
|
515 |
+
for category, range_data in self.nitaqat_data[sector][company_size].items():
|
516 |
+
if range_data["min"] <= estimated_saudi_percentage <= range_data["max"]:
|
517 |
+
nitaqat_analysis["nitaqat_category"] = category
|
518 |
+
break
|
519 |
+
|
520 |
+
# تحديد الامتثال
|
521 |
+
nitaqat_analysis["compliant"] = (
|
522 |
+
nitaqat_analysis["nitaqat_category"] != "أحمر" and
|
523 |
+
estimated_saudi_percentage >= nitaqat_analysis["required_saudi_percentage"]
|
524 |
+
)
|
525 |
+
|
526 |
+
return nitaqat_analysis
|
527 |
+
|
528 |
+
def _prepare_local_content_requirements(self, sector_data: Dict[str, Any], local_content_info: Dict[str, Any]) -> List[Dict[str, Any]]:
|
529 |
+
"""
|
530 |
+
إعداد متطلبات المحتوى المحلي
|
531 |
+
"""
|
532 |
+
requirements = []
|
533 |
+
|
534 |
+
# الحد الأدنى للمimport os
|
535 |
+
import json
|
536 |
+
import numpy as np
|
537 |
+
import pandas as pd
|
538 |
+
from typing import Dict, List, Any, Union, Tuple, Optional
|
539 |
+
from datetime import datetime
|
540 |
+
|
541 |
+
class LocalContentCalculator:
|
542 |
+
"""
|
543 |
+
فئة لحساب وتحليل المحتوى المحلي في المناقصات
|
544 |
+
"""
|
545 |
+
|
546 |
+
def __init__(self):
|
547 |
+
"""
|
548 |
+
تهيئة حاسبة المحتوى المحلي
|
549 |
+
"""
|
550 |
+
# تحميل بيانات المحتوى المحلي
|
551 |
+
self.local_content_data = self._load_local_content_data()
|
552 |
+
|
553 |
+
# تحميل قاعدة بيانات الشركات المحلية
|
554 |
+
self.local_companies = self._load_local_companies()
|
555 |
+
|
556 |
+
# تحميل نطاقات الشركات
|
557 |
+
self.nitaqat_data = self._load_nitaqat_data()
|
558 |
+
|
559 |
+
# قائمة القطاعات
|
560 |
+
self.sectors = [
|
561 |
+
"الإنشاءات", "تقنية المعلومات", "الاتصالات", "الصحة",
|
562 |
+
"التعليم", "النقل", "الطاقة", "المياه", "البيئة",
|
563 |
+
"الصناعة", "التجارة", "السياحة", "الخدمات المالية"
|
564 |
+
]
|
565 |
+
|
566 |
+
def _load_local_content_data(self) -> Dict[str, Any]:
|
567 |
+
"""
|
568 |
+
تحميل بيانات المحتوى المحلي
|
569 |
+
"""
|
570 |
+
# في التطبيق الفعلي، قد تُحمل هذه البيانات من ملف أو قاعدة بيانات
|
571 |
+
return {
|
572 |
+
"sectors": {
|
573 |
+
"الإنشاءات": {
|
574 |
+
"min_percentage": 30,
|
575 |
+
"target_percentage": 60,
|
576 |
+
"weights": {
|
577 |
+
"manpower": 0.35,
|
578 |
+
"materials": 0.45,
|
579 |
+
"services": 0.2
|
580 |
+
}
|
581 |
+
},
|
582 |
+
"تقنية المعلومات": {
|
583 |
+
"min_percentage": 25,
|
584 |
+
"target_percentage": 50,
|
585 |
+
"weights": {
|
586 |
+
"manpower": 0.55,
|
587 |
+
"materials": 0.15,
|
588 |
+
"services": 0.3
|
589 |
+
}
|
590 |
+
},
|
591 |
+
"عام": {
|
592 |
+
"min_percentage": 20,
|
593 |
+
"target_percentage": 40,
|
594 |
+
"weights": {
|
595 |
+
"manpower": 0.4,
|
596 |
+
"materials": 0.3,
|
597 |
+
"services": 0.3
|
598 |
+
}
|
599 |
+
}
|
600 |
+
},
|
601 |
+
"material_categories": {
|
602 |
+
"local_manufacturing": 1.0,
|
603 |
+
"local_assembly": 0.7,
|
604 |
+
"imported_with_local_value_add": 0.4,
|
605 |
+
"fully_imported": 0.0
|
606 |
+
},
|
607 |
+
"manpower_categories": {
|
608 |
+
"saudi_employee": 1.0,
|
609 |
+
"expat_employee": 0.0
|
610 |
+
},
|
611 |
+
"service_categories": {
|
612 |
+
"local_service_provider": 1.0,
|
613 |
+
"joint_venture": 0.6,
|
614 |
+
"foreign_provider_with_local_partner": 0.3,
|
615 |
+
"foreign_provider": 0.0
|
616 |
+
}
|
617 |
+
}
|
618 |
+
|
619 |
+
def _load_local_companies(self) -> Dict[str, Dict[str, Any]]:
|
620 |
+
"""
|
621 |
+
تحميل قاعدة بيانات الشركات المحلية
|
622 |
+
"""
|
623 |
+
# في التطبيق الفعلي، قد تُحمل هذه البيانات من ملف أو قاعدة بيانات
|
624 |
+
return {
|
625 |
+
"company1": {
|
626 |
+
"name": "شركة البناء السعودية",
|
627 |
+
"cr_number": "1010XXXXXX",
|
628 |
+
"sector": "الإنشاءات",
|
629 |
+
"local_content_certificate": True,
|
630 |
+
"saudi_employees_percentage": 35,
|
631 |
+
"nitaqat_category": "أخضر متوسط",
|
632 |
+
"local_content_percentage": 45
|
633 |
+
},
|
634 |
+
"company2": {
|
635 |
+
"name": "شركة تقنية المستقبل",
|
636 |
+
"cr_number": "1020XXXXXX",
|
637 |
+
"sector": "تقنية المعلومات",
|
638 |
+
"local_content_certificate": True,
|
639 |
+
"saudi_employees_percentage": 42,
|
640 |
+
"nitaqat_category": "أخضر مرتفع",
|
641 |
+
"local_content_percentage": 52
|
642 |
+
},
|
643 |
+
"company3": {
|
644 |
+
"name": "مصنع المنتجات المعدنية",
|
645 |
+
"cr_number": "1030XXXXXX",
|
646 |
+
"sector": "الصناعة",
|
647 |
+
"local_content_certificate": True,
|
648 |
+
"saudi_employees_percentage": 28,
|
649 |
+
"nitaqat_category": "أخضر منخفض",
|
650 |
+
"local_content_percentage": 61
|
651 |
+
}
|
652 |
+
}
|
653 |
+
|
654 |
+
def _load_nitaqat_data(self) -> Dict[str, Dict[str, Any]]:
|
655 |
+
"""
|
656 |
+
تحميل بيانات نطاقات الشركات
|
657 |
+
"""
|
658 |
+
# في التطبيق الفعلي، قد تُحمل هذه البيانات من ملف أو قاعدة بيانات
|
659 |
+
return {
|
660 |
+
"الإنشاءات": {
|
661 |
+
"صغيرة": {
|
662 |
+
"أحمر": {"min": 0, "max": 5},
|
663 |
+
"أخضر منخفض": {"min": 6, "max": 10},
|
664 |
+
"أخضر متوسط": {"min": 11, "max": 15},
|
665 |
+
"أخضر مرتفع": {"min": 16, "max": 25},
|
666 |
+
"بلاتيني": {"min": 26, "max": 100}
|
667 |
+
},
|
668 |
+
"متوسطة": {
|
669 |
+
"أحمر": {"min": 0, "max": 7},
|
670 |
+
"أخضر منخفض": {"min": 8, "max": 14},
|
671 |
+
"أخضر متوسط": {"min": 15, "max": 20},
|
672 |
+
"أخضر مرتفع": {"min": 21, "max": 30},
|
673 |
+
"بلاتيني": {"min": 31, "max": 100}
|
674 |
+
},
|
675 |
+
"كبيرة": {
|
676 |
+
"أحمر": {"min": 0, "max": 9},
|
677 |
+
"أخضر منخفض": {"min": 10, "max": 16},
|
678 |
+
"أخضر متوسط": {"min": 17, "max": 23},
|
679 |
+
"أخضر مرتفع": {"min": 24, "max": 35},
|
680 |
+
"بلاتيني": {"min": 36, "max": 100}
|
681 |
+
}
|
682 |
+
},
|
683 |
+
"تقنية المعلومات": {
|
684 |
+
"صغيرة": {
|
685 |
+
"أحمر": {"min": 0, "max": 6},
|
686 |
+
"أخضر منخفض": {"min": 7, "max": 12},
|
687 |
+
"أخضر متوسط": {"min": 13, "max": 19},
|
688 |
+
"أخضر مرتفع": {"min": 20, "max": 29},
|
689 |
+
"بلاتيني": {"min": 30, "max": 100}
|
690 |
+
},
|
691 |
+
"متوسطة": {
|
692 |
+
"أحمر": {"min": 0, "max": 13},
|
693 |
+
"أخضر منخفض": {"min": 14, "max": 20},
|
694 |
+
"أخضر متوسط": {"min": 21, "max": 27},
|
695 |
+
"أخضر مرتفع": {"min": 28, "max": 35},
|
696 |
+
"بلاتيني": {"min": 36, "max": 100}
|
697 |
+
},
|
698 |
+
"كبيرة": {
|
699 |
+
"أحمر": {"min": 0, "max": 17},
|
700 |
+
"أخضر منخفض": {"min": 18, "max": 25},
|
701 |
+
"أخضر متوسط": {"min": 26, "max": 33},
|
702 |
+
"أخضر مرتفع": {"min": 34, "max": 40},
|
703 |
+
"بلاتيني": {"min": 41, "max": 100}
|
704 |
+
}
|
705 |
+
}
|
706 |
+
}
|