EGYADMIN commited on
Commit
cd931f3
·
verified ·
1 Parent(s): 2b98269

Create supply_chain/suppliers_database.py

Browse files
Files changed (1) hide show
  1. supply_chain/suppliers_database.py +534 -0
supply_chain/suppliers_database.py ADDED
@@ -0,0 +1,534 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ قاعدة بيانات الموردين المحليين
3
+ يوفر واجهة للوصول إلى بيانات الموردين والمنتجات المحلية
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ import os
9
+ import re
10
+ from typing import Dict, List, Any, Tuple, Optional, Union
11
+ from collections import defaultdict
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class SuppliersDatabase:
16
+ """
17
+ قاعدة بيانات الموردين المحليين
18
+ """
19
+
20
+ def __init__(self, config=None):
21
+ """
22
+ تهيئة قاعدة بيانات الموردين
23
+
24
+ المعاملات:
25
+ ----------
26
+ config : Dict, optional
27
+ إعدادات قاعدة البيانات
28
+ """
29
+ self.config = config or {}
30
+
31
+ # تحميل بيانات الموردين
32
+ self.suppliers = self._load_suppliers()
33
+
34
+ # تحميل بيانات المنتجات
35
+ self.products = self._load_products()
36
+
37
+ # تهيئة فهارس البحث
38
+ self._initialize_indexes()
39
+
40
+ logger.info(f"تم تهيئة قاعدة بيانات الموردين: {len(self.suppliers)} مورد")
41
+
42
+ def search_suppliers(self, query: str, category: Optional[str] = None) -> List[Dict[str, Any]]:
43
+ """
44
+ البحث عن الموردين
45
+
46
+ المعاملات:
47
+ ----------
48
+ query : str
49
+ استعلام البحث
50
+ category : str, optional
51
+ فئة المورد
52
+
53
+ المخرجات:
54
+ --------
55
+ List[Dict[str, Any]]
56
+ قائمة الموردين المطابقين
57
+ """
58
+ results = []
59
+
60
+ # تنظيف الاستعلام
61
+ query = query.lower().strip()
62
+
63
+ # البحث عن الموردين المطابقين
64
+ for supplier in self.suppliers:
65
+ # التحقق من الفئة إذا تم تحديدها
66
+ if category and category != "الكل" and supplier.get("category") != category:
67
+ continue
68
+
69
+ # البحث في اسم المورد
70
+ if query in supplier.get("name", "").lower():
71
+ results.append(supplier)
72
+ continue
73
+
74
+ # البحث في المنطقة
75
+ if query in supplier.get("region", "").lower():
76
+ results.append(supplier)
77
+ continue
78
+
79
+ # البحث في المنتجات
80
+ if any(query in product.lower() for product in supplier.get("products", [])):
81
+ results.append(supplier)
82
+ continue
83
+
84
+ return results
85
+
86
+ def find_matching_suppliers(self, materials: List[Dict[str, Any]]) -> Dict[str, Any]:
87
+ """
88
+ البحث عن الموردين المطابقين للمواد المطلوبة
89
+
90
+ المعاملات:
91
+ ----------
92
+ materials : List[Dict[str, Any]]
93
+ قائمة المواد المطلوبة
94
+
95
+ المخرجات:
96
+ --------
97
+ Dict[str, Any]
98
+ نتائج البحث
99
+ """
100
+ results = {
101
+ "local_suppliers": [],
102
+ "material_coverage": {},
103
+ "region_distribution": {},
104
+ "summary": {}
105
+ }
106
+
107
+ material_suppliers = {}
108
+ all_matching_suppliers = []
109
+
110
+ # البحث عن الموردين المطابقين لكل مادة
111
+ for material in materials:
112
+ material_name = material.get("name", "")
113
+ if not material_name:
114
+ continue
115
+
116
+ # البحث عن المنتجات المطابقة
117
+ matching_products = []
118
+ for product_id, product in self.products.items():
119
+ similarity = self._calculate_similarity(material_name, product.get("name", ""))
120
+ if similarity > 0.7: # عتبة التشابه
121
+ matching_products.append((product_id, similarity))
122
+
123
+ # ترتيب المنتجات حسب التشابه
124
+ matching_products.sort(key=lambda x: x[1], reverse=True)
125
+
126
+ # البحث عن الموردين لهذه المنتجات
127
+ suppliers_for_material = []
128
+ for product_id, similarity in matching_products[:5]: # أفضل 5 منتجات
129
+ product_suppliers = self.product_suppliers.get(product_id, [])
130
+ for supplier_id in product_suppliers:
131
+ supplier = next((s for s in self.suppliers if s.get("id") == supplier_id), None)
132
+ if supplier:
133
+ suppliers_for_material.append(supplier)
134
+ all_matching_suppliers.append(supplier)
135
+
136
+ # إزالة التكرارات
137
+ unique_suppliers = []
138
+ supplier_ids = set()
139
+ for supplier in suppliers_for_material:
140
+ supplier_id = supplier.get("id")
141
+ if supplier_id not in supplier_ids:
142
+ supplier_ids.add(supplier_id)
143
+ unique_suppliers.append(supplier)
144
+
145
+ # تخزين الموردين لهذه المادة
146
+ material_suppliers[material_name] = unique_suppliers
147
+
148
+ # حساب نسبة التغطية
149
+ coverage = min(100.0, len(unique_suppliers) * 20.0) # كل مورد يغطي 20% بحد أقصى 100%
150
+ results["material_coverage"][material_name] = coverage
151
+
152
+ # إزالة التكرارات من قائمة الموردين
153
+ unique_matching_suppliers = []
154
+ supplier_ids = set()
155
+ for supplier in all_matching_suppliers:
156
+ supplier_id = supplier.get("id")
157
+ if supplier_id not in supplier_ids:
158
+ supplier_ids.add(supplier_id)
159
+
160
+ # إضافة معلومات المواد التي يوفرها المورد
161
+ supplier_materials = []
162
+ for material_name, suppliers in material_suppliers.items():
163
+ if any(s.get("id") == supplier_id for s in suppliers):
164
+ supplier_materials.append(material_name)
165
+
166
+ supplier_copy = supplier.copy()
167
+ supplier_copy["materials"] = supplier_materials
168
+
169
+ unique_matching_suppliers.append(supplier_copy)
170
+
171
+ # ترتيب الموردين حسب عدد المواد وتصنيف الموثوقية
172
+ unique_matching_suppliers.sort(
173
+ key=lambda s: (len(s.get("materials", [])), s.get("reliability", 0)),
174
+ reverse=True
175
+ )
176
+
177
+ # إضافة الموردين المطابقين
178
+ results["local_suppliers"] = unique_matching_suppliers
179
+
180
+ # حساب توزيع المناطق
181
+ region_counts = defaultdict(int)
182
+ for supplier in unique_matching_suppliers:
183
+ region = supplier.get("region", "غير محدد")
184
+ region_counts[region] += 1
185
+
186
+ results["region_distribution"] = dict(region_counts)
187
+
188
+ # إعداد ملخص
189
+ results["summary"] = {
190
+ "total_materials": len(materials),
191
+ "materials_with_suppliers": len(material_suppliers),
192
+ "total_matching_suppliers": len(unique_matching_suppliers),
193
+ "average_coverage": sum(results["material_coverage"].values()) / max(1, len(results["material_coverage"]))
194
+ }
195
+
196
+ return results
197
+
198
+ def get_total_suppliers(self) -> int:
199
+ """
200
+ الحصول على إجمالي عدد الموردين
201
+
202
+ المخرجات:
203
+ --------
204
+ int
205
+ إجمالي عدد الموردين
206
+ """
207
+ return len(self.suppliers)
208
+
209
+ def get_total_products(self) -> int:
210
+ """
211
+ الحصول على إجمالي عدد المنتجات
212
+
213
+ المخرجات:
214
+ --------
215
+ int
216
+ إجمالي عدد المنتجات
217
+ """
218
+ return len(self.products)
219
+
220
+ def get_covered_regions(self) -> int:
221
+ """
222
+ الحصول على عدد المناطق المغطاة
223
+
224
+ المخرجات:
225
+ --------
226
+ int
227
+ عدد المناطق المغطاة
228
+ """
229
+ regions = set(supplier.get("region", "") for supplier in self.suppliers if supplier.get("region"))
230
+ return len(regions)
231
+
232
+ def get_suppliers_by_region(self) -> Dict[str, int]:
233
+ """
234
+ الحصول على توزيع الموردين حسب المنطقة
235
+
236
+ المخرجات:
237
+ --------
238
+ Dict[str, int]
239
+ توزيع الموردين حسب المنطقة
240
+ """
241
+ region_counts = defaultdict(int)
242
+ for supplier in self.suppliers:
243
+ region = supplier.get("region", "غير محدد")
244
+ region_counts[region] += 1
245
+
246
+ return dict(region_counts)
247
+
248
+ def get_suppliers_by_category(self) -> Dict[str, int]:
249
+ """
250
+ الحصول على توزيع الموردين حسب الفئة
251
+
252
+ المخرجات:
253
+ --------
254
+ Dict[str, int]
255
+ توزيع الموردين حسب الفئة
256
+ """
257
+ category_counts = defaultdict(int)
258
+ for supplier in self.suppliers:
259
+ category = supplier.get("category", "غير محدد")
260
+ category_counts[category] += 1
261
+
262
+ return dict(category_counts)
263
+
264
+ def _load_suppliers(self) -> List[Dict[str, Any]]:
265
+ """
266
+ تحميل بيانات الموردين
267
+
268
+ المخرجات:
269
+ --------
270
+ List[Dict[str, Any]]
271
+ بيانات الموردين
272
+ """
273
+ try:
274
+ file_path = 'data/templates/local_suppliers.json'
275
+ if os.path.exists(file_path):
276
+ with open(file_path, 'r', encoding='utf-8') as f:
277
+ suppliers = json.load(f)
278
+
279
+ # إضافة معرف فريد لكل مورد إذا لم يكن موجودًا
280
+ for i, supplier in enumerate(suppliers):
281
+ if "id" not in supplier:
282
+ supplier["id"] = f"S{i+1:04d}"
283
+
284
+ return suppliers
285
+ else:
286
+ logger.warning(f"ملف بيانات الموردين غير موجود: {file_path}")
287
+ # إنشاء بيانات افتراضية
288
+ return self._create_default_suppliers()
289
+ except Exception as e:
290
+ logger.error(f"فشل في تحميل بيانات الموردين: {str(e)}")
291
+ return self._create_default_suppliers()
292
+
293
+ def _load_products(self) -> Dict[str, Dict[str, Any]]:
294
+ """
295
+ تحميل بيانات المنتجات
296
+
297
+ المخرجات:
298
+ --------
299
+ Dict[str, Dict[str, Any]]
300
+ بيانات المنتجات
301
+ """
302
+ try:
303
+ file_path = 'data/templates/local_materials.json'
304
+ if os.path.exists(file_path):
305
+ with open(file_path, 'r', encoding='utf-8') as f:
306
+ materials = json.load(f)
307
+
308
+ products = {}
309
+ for i, material in enumerate(materials):
310
+ product_id = f"P{i+1:04d}"
311
+ products[product_id] = {
312
+ "id": product_id,
313
+ "name": material.get("name", ""),
314
+ "category": material.get("category", ""),
315
+ "availability_percentage": material.get("availability_percentage", 0)
316
+ }
317
+
318
+ return products
319
+ else:
320
+ logger.warning(f"ملف بيانات المنتجات غير موجود: {file_path}")
321
+ # إنشاء بيانات افتراضية
322
+ return self._create_default_products()
323
+ except Exception as e:
324
+ logger.error(f"فشل في تحميل بيانات المنتجات: {str(e)}")
325
+ return self._create_default_products()
326
+
327
+ def _initialize_indexes(self):
328
+ """
329
+ تهيئة فهارس البحث
330
+ """
331
+ # إنشاء فهرس للمنتجات حسب المورد
332
+ self.supplier_products = defaultdict(list)
333
+
334
+ # إنشاء فهرس للموردين حسب المنتج
335
+ self.product_suppliers = defaultdict(list)
336
+
337
+ # ربط المنتجات بالموردين
338
+ for supplier in self.suppliers:
339
+ supplier_id = supplier.get("id")
340
+ supplier_products = supplier.get("products", [])
341
+
342
+ for product_name in supplier_products:
343
+ # البحث عن المنتج المطابق
344
+ matching_product_id = None
345
+ best_similarity = 0
346
+
347
+ for product_id, product in self.products.items():
348
+ similarity = self._calculate_similarity(product_name, product.get("name", ""))
349
+ if similarity > 0.7 and similarity > best_similarity: # عتبة التشابه
350
+ best_similarity = similarity
351
+ matching_product_id = product_id
352
+
353
+ if matching_product_id:
354
+ self.supplier_products[supplier_id].append(matching_product_id)
355
+ self.product_suppliers[matching_product_id].append(supplier_id)
356
+
357
+ def _calculate_similarity(self, str1: str, str2: str) -> float:
358
+ """
359
+ حساب درجة التشابه بين سلسلتين
360
+
361
+ المعاملات:
362
+ ----------
363
+ str1 : str
364
+ السلسلة الأولى
365
+ str2 : str
366
+ السلسلة الثانية
367
+
368
+ المخرجات:
369
+ --------
370
+ float
371
+ درجة التشابه (0 إلى 1)
372
+ """
373
+ # تنظيف السلاسل
374
+ str1 = str1.lower().strip()
375
+ str2 = str2.lower().strip()
376
+
377
+ # تحويل السلاسل إلى مجموعات من الكلمات
378
+ words1 = set(str1.split())
379
+ words2 = set(str2.split())
380
+
381
+ # حساب معامل جاكارد
382
+ intersection = len(words1.intersection(words2))
383
+ union = len(words1.union(words2))
384
+
385
+ if union == 0:
386
+ return 0.0
387
+
388
+ return intersection / union
389
+
390
+ def _create_default_suppliers(self) -> List[Dict[str, Any]]:
391
+ """
392
+ إنشاء بيانات افتراضية للموردين
393
+
394
+ المخرجات:
395
+ --------
396
+ List[Dict[str, Any]]
397
+ بيانات افتراضية للموردين
398
+ """
399
+ return [
400
+ {
401
+ "id": "S0001",
402
+ "name": "شركة الراجحي للحديد",
403
+ "region": "الرياض",
404
+ "category": "مواد بناء",
405
+ "products": ["حديد تسليح", "حديد مجلفن"],
406
+ "reliability": 4.5,
407
+ "contact": "[email protected]",
408
+ "nitaqat_category": "أخضر مرتفع"
409
+ },
410
+ {
411
+ "id": "S0004",
412
+ "name": "شركة الزامل للتكييف",
413
+ "region": "الدمام",
414
+ "category": "تكييف",
415
+ "products": ["وحدات تكييف", "معدات تبريد", "أجهزة تهوية"],
416
+ "reliability": 4.8,
417
+ "contact": "[email protected]",
418
+ "nitaqat_category": "بلاتيني"
419
+ },
420
+ {
421
+ "id": "S0005",
422
+ "name": "شركة امجاد للسباكة",
423
+ "region": "الرياض",
424
+ "category": "سباكة",
425
+ "products": ["أنابيب مياه", "خزانات مياه", "مواسير صرف"],
426
+ "reliability": 3.9,
427
+ "contact": "[email protected]",
428
+ "nitaqat_category": "أخضر متوسط"
429
+ },
430
+ {
431
+ "id": "S0006",
432
+ "name": "مصنع الخليج للزجاج",
433
+ "region": "جدة",
434
+ "category": "مواد بناء",
435
+ "products": ["زجاج", "ألواح زجاجية", "واجهات زجاجية"],
436
+ "reliability": 4.0,
437
+ "contact": "[email protected]",
438
+ "nitaqat_category": "أخضر مرتفع"
439
+ },
440
+ {
441
+ "id": "S0007",
442
+ "name": "مؤسسة الديار للدهانات",
443
+ "region": "الرياض",
444
+ "category": "مواد بناء",
445
+ "products": ["دهانات", "طلاء جدران", "عوازل"],
446
+ "reliability": 3.7,
447
+ "contact": "[email protected]",
448
+ "nitaqat_category": "أخضر منخفض"
449
+ },
450
+ {
451
+ "id": "S0008",
452
+ "name": "شركة الأثاث المكتبي المتحدة",
453
+ "region": "الرياض",
454
+ "category": "أثاث",
455
+ "products": ["أثاث مكتبي", "كراسي", "طاولات", "خزائن"],
456
+ "reliability": 4.1,
457
+ "contact": "[email protected]",
458
+ "nitaqat_category": "أخضر مرتفع"
459
+ },
460
+ {
461
+ "id": "S0009",
462
+ "name": "شركة ابن غنيم للنجارة",
463
+ "region": "الدمام",
464
+ "category": "نجارة",
465
+ "products": ["أبواب خشبية", "نوافذ خشبية", "أثاث منزلي"],
466
+ "reliability": 4.3,
467
+ "contact": "[email protected]",
468
+ "nitaqat_category": "أخضر مرتفع"
469
+ },
470
+ {
471
+ "id": "S0010",
472
+ "name": "الشركة العربية للألمنيوم",
473
+ "region": "جدة",
474
+ "category": "ألمنيوم",
475
+ "products": ["نوافذ ألمنيوم", "أبواب ألمنيوم", "واجهات ألمنيوم"],
476
+ "reliability": 4.4,
477
+ "contact": "[email protected]",
478
+ "nitaqat_category": "بلاتيني"
479
+ }
480
+ ]
481
+
482
+ def _create_default_products(self) -> Dict[str, Dict[str, Any]]:
483
+ """
484
+ إنشاء بيانات افتراضية للمنتجات
485
+
486
+ المخرجات:
487
+ --------
488
+ Dict[str, Dict[str, Any]]
489
+ بيانات افتراضية للمنتجات
490
+ """
491
+ products = {
492
+ "P0001": {"id": "P0001", "name": "حديد تسليح", "category": "مواد بناء", "availability_percentage": 90.0},
493
+ "P0002": {"id": "P0002", "name": "أسمنت", "category": "مواد بناء", "availability_percentage": 95.0},
494
+ "P0003": {"id": "P0003", "name": "خرسانة جاهزة", "category": "مواد بناء", "availability_percentage": 100.0},
495
+ "P0004": {"id": "P0004", "name": "بلاط", "category": "مواد بناء", "availability_percentage": 80.0},
496
+ "P0005": {"id": "P0005", "name": "رخام", "category": "مواد بناء", "availability_percentage": 60.0},
497
+ "P0006": {"id": "P0006", "name": "دهانات", "category": "مواد بناء", "availability_percentage": 75.0},
498
+ "P0007": {"id": "P0007", "name": "زجاج", "category": "مواد بناء", "availability_percentage": 50.0},
499
+ "P0008": {"id": "P0008", "name": "أسلاك كهربائية", "category": "كهرباء", "availability_percentage": 60.0},
500
+ "P0009": {"id": "P0009", "name": "لوحات كهربائية", "category": "كهرباء", "availability_percentage": 55.0},
501
+ "P0010": {"id": "P0010", "name": "مفاتيح كهربائية", "category": "كهرباء", "availability_percentage": 45.0},
502
+ "P0011": {"id": "P0011", "name": "أنابيب مياه", "category": "سباكة", "availability_percentage": 70.0},
503
+ "P0012": {"id": "P0012", "name": "خزانات مياه", "category": "سباكة", "availability_percentage": 95.0},
504
+ "P0013": {"id": "P0013", "name": "مواسير صرف", "category": "سباكة", "availability_percentage": 85.0},
505
+ "P0014": {"id": "P0014", "name": "وحدات تكييف", "category": "تكييف", "availability_percentage": 30.0},
506
+ "P0015": {"id": "P0015", "name": "معدات تبريد", "category": "تكييف", "availability_percentage": 25.0},
507
+ "P0016": {"id": "P0016", "name": "أجهزة تهوية", "category": "تكييف", "availability_percentage": 40.0},
508
+ "P0017": {"id": "P0017", "name": "أثاث مكتبي", "category": "أثاث", "availability_percentage": 70.0},
509
+ "P0018": {"id": "P0018", "name": "أثاث منزلي", "category": "أثاث", "availability_percentage": 65.0},
510
+ "P0019": {"id": "P0019", "name": "أبواب خشبية", "category": "نجارة", "availability_percentage": 80.0},
511
+ "P0020": {"id": "P0020", "name": "نوافذ ألمنيوم", "category": "ألمنيوم", "availability_percentage": 75.0}
512
+ }
513
+
514
+ return productsrajhi-steel.sa",
515
+ "nitaqat_category": "بلاتيني"
516
+ },
517
+ {
518
+ "id": "S0002",
519
+ "name": "اسمنت اليمامة",
520
+ "region": "الرياض",
521
+ "category": "مواد بناء",
522
+ "products": ["أسمنت", "خرسانة جاهزة"],
523
+ "reliability": 4.7,
524
+ "contact": "[email protected]",
525
+ "nitaqat_category": "بلاتيني"
526
+ },
527
+ {
528
+ "id": "S0003",
529
+ "name": "الشركة السعودية للصناعات الكهربائية",
530
+ "region": "جدة",
531
+ "category": "كهرباء",
532
+ "products": ["أسلاك كهربائية", "لوحات كهربائية", "مفاتيح كهربائية"],
533
+ "reliability": 4.2,
534
+ "contact": "info@