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

Create supply_chain/procurement_planner.py

Browse files
Files changed (1) hide show
  1. supply_chain/procurement_planner.py +443 -0
supply_chain/procurement_planner.py ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ مخطط المشتريات
3
+ يقوم بإعداد خطة المشتريات الأمثل بناءً على متطلبات المشروع وقاعدة بيانات الموردين
4
+ """
5
+
6
+ import logging
7
+ import datetime
8
+ from typing import Dict, List, Any, Tuple, Optional, Union
9
+ from collections import defaultdict
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class ProcurementPlanner:
14
+ """
15
+ مخطط المشتريات
16
+ """
17
+
18
+ def __init__(self, suppliers_db, config=None):
19
+ """
20
+ تهيئة مخطط المشتريات
21
+
22
+ المعاملات:
23
+ ----------
24
+ suppliers_db : SuppliersDatabase
25
+ قاعدة بيانات الموردين
26
+ config : Dict, optional
27
+ إعدادات مخطط المشتريات
28
+ """
29
+ self.config = config or {}
30
+ self.suppliers_db = suppliers_db
31
+
32
+ logger.info("تم تهيئة مخطط المشتريات")
33
+
34
+ def generate_plan(self, local_content_data: Dict[str, Any],
35
+ suppliers_data: Dict[str, Any]) -> Dict[str, Any]:
36
+ """
37
+ إعداد خطة المشتريات
38
+
39
+ المعاملات:
40
+ ----------
41
+ local_content_data : Dict[str, Any]
42
+ بيانات المحتوى المحلي
43
+ suppliers_data : Dict[str, Any]
44
+ بيانات الموردين
45
+
46
+ المخرجات:
47
+ --------
48
+ Dict[str, Any]
49
+ خطة المشتريات
50
+ """
51
+ try:
52
+ logger.info("بدء إعداد خطة المشتريات")
53
+
54
+ # استخراج المواد المطلوبة
55
+ required_materials = local_content_data.get("required_materials", [])
56
+
57
+ # استخراج الموردين المحليين
58
+ local_suppliers = suppliers_data.get("local_suppliers", [])
59
+
60
+ # إعداد خطة المشتريات
61
+ procurement_plan = self._prepare_procurement_plan(required_materials, local_suppliers)
62
+
63
+ # حساب المؤشرات الرئيسية
64
+ stats = self._calculate_procurement_stats(procurement_plan)
65
+
66
+ # إضافة استراتيجية المشتريات
67
+ procurement_strategy = self._generate_procurement_strategy(
68
+ procurement_plan,
69
+ local_content_data.get("estimated_local_content", 0),
70
+ local_content_data.get("required_local_content", 0)
71
+ )
72
+
73
+ # إعداد النتائج
74
+ results = {
75
+ "items": procurement_plan,
76
+ "stats": stats,
77
+ "procurement_strategy": procurement_strategy
78
+ }
79
+
80
+ logger.info(f"اكتملت خطة المشتريات: {len(procurement_plan)} عنصر")
81
+ return results
82
+
83
+ except Exception as e:
84
+ logger.error(f"فشل في إعداد خطة المشتريات: {str(e)}")
85
+ return {
86
+ "items": [],
87
+ "stats": {},
88
+ "procurement_strategy": {},
89
+ "error": str(e)
90
+ }
91
+
92
+ def _prepare_procurement_plan(self, required_materials: List[Dict[str, Any]],
93
+ local_suppliers: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
94
+ """
95
+ إعداد خطة المشتريات التفصيلية
96
+
97
+ المعاملات:
98
+ ----------
99
+ required_materials : List[Dict[str, Any]]
100
+ المواد المطلوبة
101
+ local_suppliers : List[Dict[str, Any]]
102
+ الموردين المحليين
103
+
104
+ المخرجات:
105
+ --------
106
+ List[Dict[str, Any]]
107
+ خطة المشتريات
108
+ """
109
+ procurement_items = []
110
+
111
+ # تاريخ البدء الافتراضي (اليوم)
112
+ start_date = datetime.datetime.now()
113
+
114
+ # معالجة كل مادة
115
+ for material in required_materials:
116
+ material_name = material.get("name", "")
117
+ if not material_name:
118
+ continue
119
+
120
+ # البحث عن الموردين لهذه المادة
121
+ material_suppliers = []
122
+ for supplier in local_suppliers:
123
+ if material_name in supplier.get("materials", []):
124
+ material_suppliers.append(supplier)
125
+
126
+ # تحديد مصدر التوريد
127
+ if material_suppliers:
128
+ # المورد المحلي الأكثر موثوقية
129
+ supplier = max(material_suppliers, key=lambda s: s.get("reliability", 0))
130
+ source_type = "local"
131
+ source_name = supplier.get("name", "")
132
+ source_region = supplier.get("region", "")
133
+ lead_time = self._estimate_lead_time(source_region)
134
+ estimated_cost = self._estimate_cost(material, source_type)
135
+ else:
136
+ # توريد من الخارج
137
+ source_type = "import"
138
+ source_name = "مورد خارجي"
139
+ source_region = "دولي"
140
+ lead_time = 60 # تقدير افتراضي: 60 يوم
141
+ estimated_cost = self._estimate_cost(material, source_type)
142
+
143
+ # تحديد تاريخ التوريد
144
+ delivery_date = start_date + datetime.timedelta(days=lead_time)
145
+
146
+ # إعداد عنصر المشتريات
147
+ item = {
148
+ "material": material_name,
149
+ "quantity": material.get("quantity", 0),
150
+ "unit": material.get("unit", ""),
151
+ "source_type": source_type,
152
+ "supplier": source_name,
153
+ "region": source_region,
154
+ "lead_time": lead_time,
155
+ "delivery_date": delivery_date.strftime("%Y-%m-%d"),
156
+ "estimated_cost": estimated_cost,
157
+ "local_content_contribution": 100 if source_type == "local" else 0,
158
+ "status": "planned"
159
+ }
160
+
161
+ procurement_items.append(item)
162
+
163
+ # ترتيب العناصر حسب تاريخ التوريد
164
+ procurement_items.sort(key=lambda x: x.get("delivery_date", ""))
165
+
166
+ return procurement_items
167
+
168
+ def _estimate_lead_time(self, region: str) -> int:
169
+ """
170
+ تقدير وقت التوريد بناءً على المنطقة
171
+
172
+ المعاملات:
173
+ ----------
174
+ region : str
175
+ المنطقة
176
+
177
+ المخرجات:
178
+ --------
179
+ int
180
+ وقت التوريد المقدر (بالأيام)
181
+ """
182
+ # قيم افتراضية للمناطق المختلفة
183
+ lead_times = {
184
+ "الرياض": 7,
185
+ "جدة": 10,
186
+ "الدمام": 10,
187
+ "مكة المكرمة": 12,
188
+ "المدينة المنورة": 14,
189
+ "القصيم": 14,
190
+ "حائل": 15,
191
+ "تبوك": 16,
192
+ "عسير": 16,
193
+ "الباحة": 18,
194
+ "جازان": 18,
195
+ "نجران": 20
196
+ }
197
+
198
+ return lead_times.get(region, 15) # قيمة افتراضية: 15 يوم
199
+
200
+ def _estimate_cost(self, material: Dict[str, Any], source_type: str) -> float:
201
+ """
202
+ تقدير تكلفة المادة
203
+
204
+ المعاملات:
205
+ ----------
206
+ material : Dict[str, Any]
207
+ معلومات المادة
208
+ source_type : str
209
+ نوع المصدر (محلي أو استيراد)
210
+
211
+ المخرجات:
212
+ --------
213
+ float
214
+ التكلفة المقدرة
215
+ """
216
+ # قيم افتراضية
217
+ base_cost = 1000.0
218
+
219
+ # تعديل التكلفة بناءً على الكمية
220
+ quantity = float(material.get("quantity", 1))
221
+ if quantity <= 0:
222
+ quantity = 1
223
+
224
+ cost = base_cost * quantity
225
+
226
+ # تعديل التكلفة بناءً على المصدر
227
+ if source_type == "import":
228
+ # زيادة التكلفة بنسبة 30% للاستيراد
229
+ cost *= 1.3
230
+
231
+ return cost
232
+
233
+ def _calculate_procurement_stats(self, procurement_plan: List[Dict[str, Any]]) -> Dict[str, Any]:
234
+ """
235
+ حساب إحصائيات خطة المشتريات
236
+
237
+ المعاملات:
238
+ ----------
239
+ procurement_plan : List[Dict[str, Any]]
240
+ خطة المشتريات
241
+
242
+ المخرجات:
243
+ --------
244
+ Dict[str, Any]
245
+ إحصائيات خطة المشتريات
246
+ """
247
+ stats = {}
248
+
249
+ # إجمالي العناصر
250
+ stats["total_items"] = len(procurement_plan)
251
+
252
+ # العناصر المحلية والمستوردة
253
+ local_items = [item for item in procurement_plan if item.get("source_type") == "local"]
254
+ import_items = [item for item in procurement_plan if item.get("source_type") == "import"]
255
+
256
+ stats["local_items_count"] = len(local_items)
257
+ stats["import_items_count"] = len(import_items)
258
+
259
+ # نسبة المحتوى المحلي
260
+ if procurement_plan:
261
+ stats["local_content_percentage"] = (len(local_items) / len(procurement_plan)) * 100
262
+ else:
263
+ stats["local_content_percentage"] = 0
264
+
265
+ # إجمالي التكلفة
266
+ total_cost = sum(item.get("estimated_cost", 0) for item in procurement_plan)
267
+ local_cost = sum(item.get("estimated_cost", 0) for item in local_items)
268
+ import_cost = sum(item.get("estimated_cost", 0) for item in import_items)
269
+
270
+ stats["total_cost"] = total_cost
271
+ stats["local_cost"] = local_cost
272
+ stats["import_cost"] = import_cost
273
+
274
+ # نسبة تكلفة المحتوى المحلي
275
+ if total_cost > 0:
276
+ stats["local_cost_percentage"] = (local_cost / total_cost) * 100
277
+ else:
278
+ stats["local_cost_percentage"] = 0
279
+
280
+ # متوسط وقت التوريد
281
+ if procurement_plan:
282
+ avg_lead_time = sum(item.get("lead_time", 0) for item in procurement_plan) / len(procurement_plan)
283
+ stats["average_lead_time"] = avg_lead_time
284
+ else:
285
+ stats["average_lead_time"] = 0
286
+
287
+ # توزيع الموردين حسب المنطقة
288
+ region_counts = defaultdict(int)
289
+ for item in procurement_plan:
290
+ if item.get("source_type") == "local":
291
+ region = item.get("region", "غير محدد")
292
+ region_counts[region] += 1
293
+
294
+ stats["region_distribution"] = dict(region_counts)
295
+
296
+ return stats
297
+
298
+ def _generate_procurement_strategy(self, procurement_plan: List[Dict[str, Any]],
299
+ estimated_local_content: float,
300
+ required_local_content: float) -> Dict[str, Any]:
301
+ """
302
+ إعداد استراتيجية المشتريات
303
+
304
+ المعاملات:
305
+ ----------
306
+ procurement_plan : List[Dict[str, Any]]
307
+ خطة المشتريات
308
+ estimated_local_content : float
309
+ نسبة المحتوى المحلي المقدرة
310
+ required_local_content : float
311
+ نسبة المحتوى المحلي المطلوبة
312
+
313
+ المخرجات:
314
+ --------
315
+ Dict[str, Any]
316
+ استراتيجية المشتريات
317
+ """
318
+ strategy = {}
319
+
320
+ # تحديد النهج العام
321
+ if estimated_local_content >= required_local_content:
322
+ strategy["approach"] = "استراتيجية المشتريات المحلية"
323
+ strategy["description"] = "التركيز على الموردين المحليين لتعزيز المحتوى المحلي والحفاظ على الميزة التنافسية."
324
+ else:
325
+ strategy["approach"] = "استراتيجية تعزيز المحتوى المحلي"
326
+ strategy["description"] = "زيادة الاعتماد على الموردين المحليين واستبدال العناصر المستوردة تدريجياً بمنتجات محلية."
327
+
328
+ # تحديد الجدول الزمني
329
+ if procurement_plan:
330
+ earliest_date = min(item.get("delivery_date", "2099-12-31") for item in procurement_plan)
331
+ latest_date = max(item.get("delivery_date", "2000-01-01") for item in procurement_plan)
332
+
333
+ strategy["timeline"] = {
334
+ "start_date": earliest_date,
335
+ "end_date": latest_date,
336
+ "critical_path": self._identify_critical_path(procurement_plan)
337
+ }
338
+ else:
339
+ strategy["timeline"] = {
340
+ "start_date": "غير محدد",
341
+ "end_date": "غير محدد",
342
+ "critical_path": []
343
+ }
344
+
345
+ # الاعتبارات الرئيسية
346
+ strategy["key_considerations"] = [
347
+ "ضمان الامتثال لمتطلبات المحتوى المحلي",
348
+ "تقليل مخاطر سلسلة الإمداد وتنويع مصادر التوريد",
349
+ "تحسين كفاءة التكلفة والجودة",
350
+ "بناء علاقات طويلة الأمد مع الموردين الرئيسيين"
351
+ ]
352
+
353
+ # خطة عمل محددة
354
+ strategy["action_plan"] = []
355
+
356
+ # التعامل مع العناصر المستوردة
357
+ import_items = [item for item in procurement_plan if item.get("source_type") == "import"]
358
+ if import_items:
359
+ strategy["action_plan"].append(
360
+ "البحث عن موردين محليين بديلين للعناصر المستوردة (" +
361
+ ", ".join([item.get("material", "") for item in import_items[:3]]) +
362
+ (f" وغيرها من {len(import_items) - 3} عناصر" if len(import_items) > 3 else "") +
363
+ ")"
364
+ )
365
+
366
+ # تحسين أوقات التوريد الطويلة
367
+ long_lead_items = [item for item in procurement_plan if item.get("lead_time", 0) > 30]
368
+ if long_lead_items:
369
+ strategy["action_plan"].append(
370
+ "تقليل أوقات التوريد الطويلة للعناصر الحرجة (" +
371
+ ", ".join([item.get("material", "") for item in long_lead_items[:3]]) +
372
+ (f" وغيرها من {len(long_lead_items) - 3} عناصر" if len(long_lead_items) > 3 else "") +
373
+ ")"
374
+ )
375
+
376
+ # التعامل مع العناصر عالية التكلفة
377
+ if procurement_plan:
378
+ avg_cost = sum(item.get("estimated_cost", 0) for item in procurement_plan) / len(procurement_plan)
379
+ high_cost_items = [item for item in procurement_plan if item.get("estimated_cost", 0) > avg_cost * 2]
380
+
381
+ if high_cost_items:
382
+ strategy["action_plan"].append(
383
+ "تحليل بدائل خفض التكلفة للعناصر عالية التكلفة (" +
384
+ ", ".join([item.get("material", "") for item in high_cost_items[:3]]) +
385
+ (f" وغيرها من {len(high_cost_items) - 3} عناصر" if len(high_cost_items) > 3 else "") +
386
+ ")"
387
+ )
388
+
389
+ # إضافة إجراءات عامة
390
+ strategy["action_plan"].extend([
391
+ "توحيد الطلبات من نفس المورد لتحقيق وفورات الحجم",
392
+ "إعداد خطة طوارئ للموردين الرئيسيين",
393
+ "تطوير نظام لتقييم أداء الموردين",
394
+ "توثيق عمليات الشراء وإنشاء قاعدة بيانات سعرية"
395
+ ])
396
+
397
+ return strategy
398
+
399
+ def _identify_critical_path(self, procurement_plan: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
400
+ """
401
+ تحديد المسار الحرج في خطة المشتريات
402
+
403
+ المعاملات:
404
+ ----------
405
+ procurement_plan : List[Dict[str, Any]]
406
+ خطة المشتريات
407
+
408
+ المخرجات:
409
+ --------
410
+ List[Dict[str, Any]]
411
+ المسار الحرج
412
+ """
413
+ # تحديد العناصر الحرجة (أطول وقت توريد، أعلى تكلفة، عناصر مستوردة)
414
+ if not procurement_plan:
415
+ return []
416
+
417
+ # ترتيب العناصر حسب وقت التوريد
418
+ lead_time_sorted = sorted(procurement_plan, key=lambda x: x.get("lead_time", 0), reverse=True)
419
+
420
+ # أخذ أطول 3 عناصر من حيث وقت التوريد
421
+ critical_items = lead_time_sorted[:3]
422
+
423
+ # ترتيب العناصر حسب التكلفة
424
+ cost_sorted = sorted(procurement_plan, key=lambda x: x.get("estimated_cost", 0), reverse=True)
425
+
426
+ # إضافة أعلى 2 عناصر من حيث التكلفة إذا لم تكن موجودة بالفعل
427
+ for item in cost_sorted[:2]:
428
+ if not any(critical.get("material") == item.get("material") for critical in critical_items):
429
+ critical_items.append(item)
430
+
431
+ return [
432
+ {
433
+ "material": item.get("material", ""),
434
+ "supplier": item.get("supplier", ""),
435
+ "lead_time": item.get("lead_time", 0),
436
+ "delivery_date": item.get("delivery_date", ""),
437
+ "reason": "وقت توريد طويل" if item == lead_time_sorted[0] else
438
+ "تكلفة عالية" if item == cost_sorted[0] else
439
+ "عنصر حرج"
440
+ }
441
+ for item in critical_items
442
+ ]
443
+