diff --git "a/modules/pricing/pricing_app.py" "b/modules/pricing/pricing_app.py" --- "a/modules/pricing/pricing_app.py" +++ "b/modules/pricing/pricing_app.py" @@ -1,1751 +1,1801 @@ import streamlit as st import pandas as pd +import numpy as np import matplotlib.pyplot as plt +import matplotlib.font_manager as fm +import arabic_reshaper +from bidi.algorithm import get_display +import io import base64 from datetime import datetime -import os +import random import json -import csv -import io - -# استيراد الوحدات المخصصة -from data_storage import DataStorage -from export_utils import ( - export_to_excel, export_to_pdf, export_local_content_report, - export_risk_report, get_download_link -) - -# محاولة استيراد المكتبات الإضافية -try: - import arabic_reshaper - from bidi.algorithm import get_display - ARABIC_SUPPORT = True -except ImportError: - ARABIC_SUPPORT = False - st.warning("لم يتم العثور على مكتبات دعم اللغة العربية. بعض الميزات قد لا تعمل بشكل صحيح.") +import os class PricingApp: + """وحدة التسعير المتكاملة""" + def __init__(self): - """تهيئة التطبيق""" - # تهيئة مدير التخزين - self.storage = DataStorage() + """تهيئة وحدة التسعير""" + # تهيئة البيانات في حالة الجلسة إذا لم تكن موجودة + if 'projects' not in st.session_state: + st.session_state.projects = [ + { + 'id': 1, + 'name': 'مشروع تطوير الطرق الداخلية', + 'client': 'وزارة النقل', + 'estimated_value': 5000000, + 'deadline': '2024-06-30', + 'status': 'قيد التسعير', + 'created_at': '2024-01-15', + 'pricing_type': 'قياسي' + }, + { + 'id': 2, + 'name': 'مشروع إنشاء مبنى إداري', + 'client': 'شركة التطوير العقاري', + 'estimated_value': 12000000, + 'deadline': '2024-08-15', + 'status': 'قيد التسعير', + 'created_at': '2024-02-01', + 'pricing_type': 'غير متزن' + } + ] - # تهيئة حالة الجلسة إذا لم تكن موجودة - if 'initialized' not in st.session_state: - self._initialize_session_state() + if 'current_project' not in st.session_state: + st.session_state.current_project = 1 + + if 'next_project_id' not in st.session_state: + st.session_state.next_project_id = len(st.session_state.projects) + 1 + + if 'show_new_project_form' not in st.session_state: + st.session_state.show_new_project_form = False + + if 'show_edit_project_form' not in st.session_state: + st.session_state.show_edit_project_form = False + + if 'edit_project_id' not in st.session_state: + st.session_state.edit_project_id = None + + if 'boq_items' not in st.session_state: + st.session_state.boq_items = [ + { + 'id': 1, + 'project_id': 1, + 'code': 'A-001', + 'description': 'أعمال الحفر والردم', + 'unit': 'م3', + 'quantity': 1500, + 'unit_price': 45, + 'total_price': 67500, + 'resource_type': 'مواد' + }, + { + 'id': 2, + 'project_id': 1, + 'code': 'A-002', + 'description': 'توريد وتركيب طبقة أساس', + 'unit': 'م2', + 'quantity': 3000, + 'unit_price': 85, + 'total_price': 255000, + 'resource_type': 'مواد' + }, + { + 'id': 3, + 'project_id': 1, + 'code': 'A-003', + 'description': 'توريد وتركيب خرسانة جاهزة', + 'unit': 'م3', + 'quantity': 750, + 'unit_price': 320, + 'total_price': 240000, + 'resource_type': 'مواد' + }, + { + 'id': 4, + 'project_id': 2, + 'code': 'B-001', + 'description': 'أعمال الأساسات', + 'unit': 'م3', + 'quantity': 500, + 'unit_price': 450, + 'total_price': 225000, + 'resource_type': 'مواد' + }, + { + 'id': 5, + 'project_id': 2, + 'code': 'B-002', + 'description': 'أعمال الهيكل الخرساني', + 'unit': 'م3', + 'quantity': 1200, + 'unit_price': 550, + 'total_price': 660000, + 'resource_type': 'مواد' + } + ] + + if 'next_boq_item_id' not in st.session_state: + st.session_state.next_boq_item_id = len(st.session_state.boq_items) + 1 + + if 'show_new_boq_item_form' not in st.session_state: + st.session_state.show_new_boq_item_form = False + + if 'show_edit_boq_item_form' not in st.session_state: + st.session_state.show_edit_boq_item_form = False + + if 'edit_boq_item_id' not in st.session_state: + st.session_state.edit_boq_item_id = None + + if 'show_resource_selector' not in st.session_state: + st.session_state.show_resource_selector = False + + if 'selected_resource_type' not in st.session_state: + st.session_state.selected_resource_type = "المواد" + + # تهيئة معامل تعديل التسعير الغير متزن + if 'unbalanced_pricing_factors' not in st.session_state: + st.session_state.unbalanced_pricing_factors = { + 'early_items_factor': 1.15, # زيادة أسعار البنود المبكرة بنسبة 15% + 'late_items_factor': 0.90, # تخفيض أسعار البنود المتأخرة بنسبة 10% + 'custom_factors': {} # معاملات مخصصة لبنود محددة + } + + # تهيئة حالة حفظ التسعير + if 'saved_pricing' not in st.session_state: + st.session_state.saved_pricing = [] + + # تهيئة حالة تحليل سعر البند + if 'item_analysis_edited' not in st.session_state: + st.session_state.item_analysis_edited = False + + def render(self): + """طريقة للتوافق مع الواجهة القديمة""" + self.run() - # تعيين نمط CSS - self._set_css() - - def _initialize_session_state(self): - """تهيئة حالة الجلسة وتحميل البيانات من التخزين""" - # تحميل البيانات من ملفات JSON - projects = self.storage.load_projects() - boq_items = self.storage.load_boq_items() - pricing_history = self.storage.load_pricing_history() - risks = self.storage.load_risks() - - # تعيين حالة الجلسة - st.session_state.projects = projects - st.session_state.boq_items = boq_items - st.session_state.saved_pricing = pricing_history - st.session_state.risks = risks - st.session_state.current_project = None - st.session_state.pricing_step = "project_info" - st.session_state.initialized = True - - def _set_css(self): - """تعيين نمط CSS للتطبيق""" - st.markdown(""" - - """, unsafe_allow_html=True) - def run(self): - """تشغيل التطبيق""" - st.title("نظام تسعير المشاريع") - - # عرض شريط التنقل الجانبي - self._render_sidebar() - - # تحديد الخطوة الحالية - current_step = st.session_state.pricing_step - - # تحديث قائمة الخطوات - steps = { - "project_info": self._render_project_info_step, - "boq": self._render_boq_step, - "cost_analysis": self._render_cost_analysis_step, - "pricing_strategies": self._render_pricing_strategies_step, - "review": self._render_review_step, - "local_content": self._render_local_content_step, - "risk_assessment": self._render_risk_assessment_step - } + """تشغيل وحدة التسعير""" + st.title("وحدة التسعير المتكاملة") - # عرض الخطوة الحالية - if current_step in steps: - steps[current_step]() - else: - st.error(f"خطوة غير معروفة: {current_step}") - - # إضافة طريقة render كبديل لطريقة run للتوافق مع الكود القديم - def render(self): - """طريقة بديلة لتشغيل التطبيق للتوافق مع الكود القديم""" - self.run() - - def _render_sidebar(self): - """عرض شريط التنقل الجانبي""" - with st.sidebar: - st.header("المشاريع") - - # عرض قائمة المشاريع - if st.session_state.projects: - project_names = [p['name'] for p in st.session_state.projects] - project_ids = [p['id'] for p in st.session_state.projects] - - selected_index = 0 - if st.session_state.current_project: - if st.session_state.current_project in project_ids: - selected_index = project_ids.index(st.session_state.current_project) - - selected_project = st.selectbox( - "اختر مشروعًا", - project_names, - index=selected_index - ) + # عرض زر إنشاء تسعير جديد + col1, col2, col3 = st.columns([1, 2, 1]) + with col2: + if st.button("➕ إنشاء تسعير جديد", key="create_new_pricing_btn", type="primary"): + st.session_state.show_new_project_form = True - selected_project_id = project_ids[project_names.index(selected_project)] + # عرض نموذج إنشاء تسعير جديد + if st.session_state.show_new_project_form: + self._render_new_project_form() + + # عرض نموذج تعديل المشروع + if st.session_state.show_edit_project_form and st.session_state.edit_project_id is not None: + self._render_edit_project_form() + + # عرض قائمة المشاريع + self._render_projects_list() + + # عرض تفاصيل المشروع الحالي + if st.session_state.current_project: + project_info = self._get_current_project_info() + if project_info: + self._render_project_info(project_info) - if st.button("فتح المشروع"): - st.session_state.current_project = selected_project_id - st.session_state.pricing_step = "project_info" - st.rerun() + # عرض علامات التبويب + tab1, tab2, tab3, tab4, tab5 = st.tabs([ + "جدول الكميات", + "تحليل سعر البند", + "تحليل التكلفة", + "تحليل الربحية", + "استراتيجيات التسعير" + ]) - if st.button("حذف المشروع"): - self.storage.delete_project(selected_project_id) + with tab1: + self._render_bill_of_quantities() - # تحديث حالة الجلسة - st.session_state.projects = self.storage.load_projects() - st.session_state.boq_items = self.storage.load_boq_items() + with tab2: + self._render_item_price_analysis() - if st.session_state.current_project == selected_project_id: - st.session_state.current_project = None + with tab3: + self._render_cost_analysis(project_info) - st.success(f"تم حذف المشروع: {selected_project}") - st.rerun() - - if st.button("إنشاء مشروع جديد"): - st.session_state.current_project = None - st.session_state.pricing_step = "project_info" - st.rerun() - - # عرض معلومات المشروع الحالي - if st.session_state.current_project: - st.divider() - st.subheader("المشروع الحالي") - - project_info = self._get_current_project_info() - if project_info: - st.write(f"**الاسم:** {project_info['name']}") - st.write(f"**العميل:** {project_info['client']}") - st.write(f"**القيمة التقديرية:** {project_info['estimated_value']:,.2f} ريال") - - # عرض خطوات التسعير - st.divider() - st.subheader("خطوات التسعير") + with tab4: + self._render_profit_margin(project_info) - steps = [ - ("project_info", "معلومات المشروع"), - ("boq", "جدول الكميات"), - ("cost_analysis", "تحليل التكلفة"), - ("pricing_strategies", "استراتيجيات التسعير"), - ("review", "المراجعة النهائية"), - ("local_content", "المحتوى المحلي"), - ("risk_assessment", "تقييم المخاطر") - ] + with tab5: + self._render_pricing_strategies(project_info) - for step_id, step_name in steps: - if st.button(step_name, key=f"nav_{step_id}"): - st.session_state.pricing_step = step_id - st.rerun() - - # عرض معلومات إضافية - st.divider() - st.caption("نظام تسعير المشاريع - الإصدار 2.0") - - def _get_current_project_info(self): - """الحصول على معلومات المشروع الحالي""" - if not st.session_state.current_project: - return None - - for project in st.session_state.projects: - if project['id'] == st.session_state.current_project: - return project - - return None - - def _render_project_info_step(self): - """عرض خطوة معلومات المشروع""" - st.markdown('
', unsafe_allow_html=True) - st.markdown('
الخطوة 1: معلومات المشروع
', unsafe_allow_html=True) - - # التحقق مما إذا كان هناك مشروع حالي - project_info = self._get_current_project_info() - - # إنشاء نموذج إدخال معلومات المشروع - with st.form(key="project_info_form"): - # حقول النموذج - project_name = st.text_input( - "اسم المشروع", - value=project_info['name'] if project_info else "" - ) - - client = st.text_input( - "العميل", - value=project_info['client'] if project_info else "" + # عرض أزرار التصدير والحفظ + self._render_export_save_buttons(project_info) + + def _render_new_project_form(self): + """عرض نموذج إنشاء مشروع جديد""" + st.subheader("إنشاء تسعير جديد") + + with st.form(key="new_project_form"): + name = st.text_input("اسم المشروع", key="new_project_name") + client = st.text_input("العميل", key="new_project_client") + estimated_value = st.number_input("القيمة التقديرية", min_value=0.0, format="%f", key="new_project_value") + deadline = st.date_input("الموعد النهائي", key="new_project_deadline") + pricing_type = st.selectbox( + "نوع التسعير", + ["قياسي", "غير متزن", "موجه ربحية", "تنافسي", "استراتيجي"], + key="new_project_pricing_type" ) - location = st.text_input( - "الموقع", - value=project_info.get('location', "") if project_info else "" - ) + col1, col2 = st.columns(2) + with col1: + submit_button = st.form_submit_button("حفظ") + with col2: + cancel_button = st.form_submit_button("إلغاء") + + if submit_button: + if name and client and estimated_value > 0: + new_project = { + 'id': st.session_state.next_project_id, + 'name': name, + 'client': client, + 'estimated_value': estimated_value, + 'deadline': deadline.strftime("%Y-%m-%d"), + 'status': 'قيد التسعير', + 'created_at': datetime.now().strftime("%Y-%m-%d"), + 'pricing_type': pricing_type + } + + st.session_state.projects.append(new_project) + st.session_state.current_project = new_project['id'] + st.session_state.next_project_id += 1 + st.session_state.show_new_project_form = False + st.rerun() + + if cancel_button: + st.session_state.show_new_project_form = False + st.rerun() - tender_number = st.text_input( - "رقم المناقصة", - value=project_info.get('tender_number', "") if project_info else "" - ) + def _render_edit_project_form(self): + """عرض نموذج تعديل المشروع""" + project = None + for p in st.session_state.projects: + if p['id'] == st.session_state.edit_project_id: + project = p + break + + if not project: + st.session_state.show_edit_project_form = False + st.rerun() + return + st.subheader(f"تعديل المشروع: {project['name']}") + + with st.form(key="edit_project_form"): + name = st.text_input("اسم المشروع", value=project['name'], key="edit_project_name") + client = st.text_input("العميل", value=project['client'], key="edit_project_client") estimated_value = st.number_input( - "القيمة التقديرية (ريال)", - min_value=0.0, - value=float(project_info['estimated_value']) if project_info else 0.0, - step=1000.0 + "القيمة التقديرية", + min_value=0.0, + value=float(project['estimated_value']), + format="%f", + key="edit_project_value" ) - deadline = st.date_input( - "الموعد النهائي", - value=datetime.strptime(project_info['deadline'], "%Y-%m-%d").date() if project_info and 'deadline' in project_info else datetime.now().date() + "الموعد النهائي", + value=datetime.strptime(project['deadline'], "%Y-%m-%d").date(), + key="edit_project_deadline" ) - - contract_duration = st.text_input( - "مدة العقد", - value=project_info.get('contract_duration', "") if project_info else "" + status = st.selectbox( + "الحالة", + ["قيد التسعير", "تم التسعير", "تم التقديم", "فائز", "خاسر"], + index=["قيد التسعير", "تم التسعير", "تم التقديم", "فائز", "خاسر"].index(project['status']), + key="edit_project_status" ) - pricing_type = st.selectbox( "نوع التسعير", - ["قياسي", "تنافسي", "مخصص"], - index=["قياسي", "تنافسي", "مخصص"].index(project_info['pricing_type']) if project_info and 'pricing_type' in project_info else 0 + ["قياسي", "غير متزن", "موجه ربحية", "تنافسي", "استراتيجي"], + index=["قياسي", "غير متزن", "موجه ربحية", "تنافسي", "استراتيجي"].index(project.get('pricing_type', 'قياسي')), + key="edit_project_pricing_type" ) - # زر الإرسال - submit_button = st.form_submit_button("حفظ معلومات المشروع") - - if submit_button: - # التحقق من صحة البيانات - if not project_name or not client or estimated_value <= 0: - st.error("يرجى ملء جميع الحقول المطلوبة") - else: - # إنشاء كائن المشروع - project_data = { - 'name': project_name, - 'client': client, - 'location': location, - 'tender_number': tender_number, - 'estimated_value': estimated_value, - 'deadline': deadline.strftime("%Y-%m-%d"), - 'contract_duration': contract_duration, - 'pricing_type': pricing_type - } - - if project_info: - # تحديث المشروع الحالي - self.storage.update_project(project_info['id'], project_data) - - # تحديث حالة الجلسة - for i, p in enumerate(st.session_state.projects): - if p['id'] == project_info['id']: - st.session_state.projects[i].update(project_data) - break - - st.success(f"تم تحديث معلومات المشروع: {project_name}") - else: - # إنشاء معرف فريد للمشروع - project_id = f"project_{len(st.session_state.projects) + 1}" - project_data['id'] = project_id - - # إضافة المشروع إلى قائمة المشاريع - added_project = self.storage.add_project(project_data) - st.session_state.projects.append(added_project) - st.session_state.current_project = project_id + col1, col2, col3 = st.columns(3) + with col1: + submit_button = st.form_submit_button("حفظ") + with col2: + cancel_button = st.form_submit_button("إلغاء") + with col3: + delete_button = st.form_submit_button("حذف المشروع", type="primary") + + if submit_button: + if name and client and estimated_value > 0: + for i, p in enumerate(st.session_state.projects): + if p['id'] == st.session_state.edit_project_id: + st.session_state.projects[i]['name'] = name + st.session_state.projects[i]['client'] = client + st.session_state.projects[i]['estimated_value'] = estimated_value + st.session_state.projects[i]['deadline'] = deadline.strftime("%Y-%m-%d") + st.session_state.projects[i]['status'] = status + st.session_state.projects[i]['pricing_type'] = pricing_type + break - st.success(f"تم إنشاء مشروع جديد: {project_name}") - - # زر الانتقال إلى الخطوة التالية - if project_info: - if st.button("متابعة إلى جدول الكميات ⬅️", type="primary"): - st.session_state.pricing_step = "boq" + st.session_state.show_edit_project_form = False st.rerun() + + if cancel_button: + st.session_state.show_edit_project_form = False + st.rerun() + + if delete_button: + for i, p in enumerate(st.session_state.projects): + if p['id'] == st.session_state.edit_project_id: + st.session_state.projects.pop(i) + break + + # حذف بنود جدول الكميات المرتبطة بالمشروع + st.session_state.boq_items = [item for item in st.session_state.boq_items if item['project_id'] != st.session_state.edit_project_id] + + st.session_state.show_edit_project_form = False + if st.session_state.projects: + st.session_state.current_project = st.session_state.projects[0]['id'] + else: + st.session_state.current_project = None + + st.rerun() + + def _render_projects_list(self): + """عرض قائمة المشاريع""" + st.subheader("قائمة المشاريع") - st.markdown('
', unsafe_allow_html=True) - - def _render_boq_step(self): - """عرض خطوة جدول الكميات""" - st.markdown('
', unsafe_allow_html=True) - st.markdown('
الخطوة 2: جدول الكميات
', unsafe_allow_html=True) - - project_info = self._get_current_project_info() - if not project_info: - st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") - if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): - st.session_state.pricing_step = "project_info" - st.rerun() + if not st.session_state.projects: + st.info("لا توجد مشاريع. قم بإنشاء مشروع جديد للبدء.") return - - # الحصول على بنود المشروع الحالي - project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] - - # عرض جدول الكميات - if project_items: - st.subheader("جدول الكميات") - # إنشاء DataFrame للعرض - df = pd.DataFrame([ - { - 'الكود': item['code'], - 'الوصف': item['description'], - 'الوحدة': item['unit'], - 'الكمية': item['quantity'], - 'سعر الوحدة': item['unit_price'], - 'السعر الإجمالي': item['total_price'], - 'نوع المورد': item.get('resource_type', '-') - } - for item in project_items - ]) + # إنشاء DataFrame من قائمة المشاريع + df = pd.DataFrame(st.session_state.projects) + if len(df) > 0 and 'id' in df.columns: + df = df[['id', 'name', 'client', 'estimated_value', 'deadline', 'status', 'pricing_type']] + df.columns = ['الرقم', 'اسم المشروع', 'العميل', 'القيمة التقديرية', 'الموعد النهائي', 'الحالة', 'نوع التسعير'] - # عرض الجدول - st.dataframe(df) + # تنسيق القيمة التقديرية + df['القيمة التقديرية'] = df['القيمة التقديرية'].apply(lambda x: f"{x:,.2f} ريال") - # عرض إجمالي التكلفة - total_cost = sum(item['total_price'] for item in project_items) - st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال") - else: - st.info("لا توجد بنود في جدول الكميات. يرجى إضافة بنود باستخدام النموذج أدناه.") - - # نموذج إضافة بند جديد - with st.expander("إضافة بند جديد", expanded=not project_items): - with st.form(key="add_item_form"): - # حقول النموذج - code = st.text_input("الكود") - description = st.text_area("الوصف") - unit = st.text_input("الوحدة") - quantity = st.number_input("الكمية", min_value=0.0, step=0.1) - unit_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, step=0.1) - resource_type = st.selectbox( - "نوع المورد", - ["مواد", "عمالة", "معدات", "خدمات", "أخرى"] - ) - - # حساب السعر الإجمالي - total_price = quantity * unit_price - st.metric("السعر الإجمالي", f"{total_price:,.2f} ريال") - - # زر الإرسال - submit_button = st.form_submit_button("إضافة البند") - - if submit_button: - # التحقق من صحة البيانات - if not code or not description or not unit or quantity <= 0 or unit_price <= 0: - st.error("يرجى ملء جميع الحقول المطلوبة") - else: - # إنشاء كائن البند - item_id = f"item_{len(st.session_state.boq_items) + 1}" - item_data = { - 'id': item_id, - 'project_id': st.session_state.current_project, - 'code': code, - 'description': description, - 'unit': unit, - 'quantity': quantity, - 'unit_price': unit_price, - 'total_price': total_price, - 'resource_type': resource_type - } - - # إضافة البند إلى قائمة البنود - added_item = self.storage.add_boq_item(item_data) - st.session_state.boq_items.append(added_item) - - st.success(f"تم إضافة البند: {code}") - st.rerun() - - # نموذج استيراد بنود من ملف CSV - with st.expander("استيراد من CSV"): - uploaded_file = st.file_uploader("اختر ملف CSV", type="csv") + # عرض الجدول + st.dataframe(df, use_container_width=True) - if uploaded_file is not None: - try: - # قراءة الملف - csv_data = uploaded_file.read().decode('utf-8') - csv_reader = csv.reader(io.StringIO(csv_data)) - - # تخطي الصف الأول (العناوين) - headers = next(csv_reader) + # اختيار المشروع + col1, col2 = st.columns(2) + with col1: + project_ids = [p['id'] for p in st.session_state.projects] + if st.session_state.current_project in project_ids: + current_index = project_ids.index(st.session_state.current_project) + else: + current_index = 0 if project_ids else None - # قراءة البنود - imported_items = [] - for row in csv_reader: - if len(row) >= 6: - code = row[0] - description = row[1] - unit = row[2] - quantity = float(row[3]) - unit_price = float(row[4]) - resource_type = row[5] if len(row) > 5 else "مواد" - - total_price = quantity * unit_price - - item_id = f"item_{len(st.session_state.boq_items) + len(imported_items) + 1}" - item_data = { - 'id': item_id, - 'project_id': st.session_state.current_project, - 'code': code, - 'description': description, - 'unit': unit, - 'quantity': quantity, - 'unit_price': unit_price, - 'total_price': total_price, - 'resource_type': resource_type - } - - imported_items.append(item_data) + if current_index is not None and project_ids: + selected_project_id = st.selectbox( + "اختر المشروع", + options=project_ids, + format_func=lambda x: next((p['name'] for p in st.session_state.projects if p['id'] == x), ""), + index=current_index, + key="select_project" + ) - if imported_items: - if st.button("استيراد البنود", key="import_items_btn"): - # إضافة البنود إلى قائمة البنود - for item_data in imported_items: - added_item = self.storage.add_boq_item(item_data) - st.session_state.boq_items.append(added_item) - - st.success(f"تم استيراد {len(imported_items)} بند") - st.rerun() - else: - st.error("لم يتم العثور على بنود صالحة في الملف") + if selected_project_id != st.session_state.current_project: + st.session_state.current_project = selected_project_id + st.rerun() + + with col2: + if st.button("تعديل المشروع", key="edit_project_btn"): + st.session_state.edit_project_id = st.session_state.current_project + st.session_state.show_edit_project_form = True + st.rerun() + else: + st.info("لا توجد مشاريع. قم بإنشاء مشروع جديد للبدء.") - except Exception as e: - st.error(f"حدث خطأ أثناء استيراد الملف: {str(e)}") + def _get_current_project_info(self): + """الحصول على معلومات المشروع الحالي""" + for project in st.session_state.projects: + if project['id'] == st.session_state.current_project: + return project + return None - # أزرار التنقل بين الخطوات - col1, col2 = st.columns(2) - with col1: - if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info_btn"): - st.session_state.pricing_step = "project_info" - st.rerun() + def _render_project_info(self, project): + """عرض معلومات المشروع""" + st.header(f"تسعير: {project['name']}") + col1, col2, col3, col4 = st.columns(4) + with col1: + st.metric("العميل", project['client']) with col2: - if project_items: - if st.button("متابعة إلى تحليل التكلفة ⬅️", key="continue_to_cost_analysis", type="primary"): - st.session_state.pricing_step = "cost_analysis" - st.rerun() - else: - st.warning("يجب إضافة بند واحد على الأقل للمتابعة") - - st.markdown('
', unsafe_allow_html=True) - - def _render_cost_analysis_step(self): - """عرض خطوة تحليل التكلفة""" - st.markdown('
', unsafe_allow_html=True) - st.markdown('
الخطوة 3: تحليل التكلفة
', unsafe_allow_html=True) - - project_info = self._get_current_project_info() - if not project_info: - st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") - if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): - st.session_state.pricing_step = "project_info" - st.rerun() - return + st.metric("القيمة التقديرية", f"{project['estimated_value']:,.2f} ريال") + with col3: + st.metric("الموعد النهائي", project['deadline']) + with col4: + st.metric("نوع التسعير", project['pricing_type']) + + def _render_bill_of_quantities(self): + """عرض جدول الكميات""" + st.subheader("جدول الكميات") + # زر إضافة بند جديد + col1, col2, col3 = st.columns([1, 1, 2]) + with col1: + if st.button("➕ إضافة بند جديد", key="add_boq_item_btn"): + st.session_state.show_new_boq_item_form = True + st.session_state.show_resource_selector = False + + with col2: + if st.button("📋 سحب من الموارد", key="add_from_resources_btn"): + st.session_state.show_resource_selector = True + st.session_state.show_new_boq_item_form = False + + # عرض نموذج إضافة بند جديد + if st.session_state.show_new_boq_item_form: + self._render_new_boq_item_form() + + # عرض نموذج تعديل البند + if st.session_state.show_edit_boq_item_form and st.session_state.edit_boq_item_id is not None: + self._render_edit_boq_item_form() + + # عرض محدد الموارد + if st.session_state.show_resource_selector: + self._render_resource_selector() + # الحصول على بنود المشروع الحالي project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] if not project_items: - st.warning("لا توجد بنود في جدول الكميات. يرجى العودة إلى الخطوة السابقة لإضافة البنود.") - - col1, col2 = st.columns(2) - with col1: - if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq_btn"): - st.session_state.pricing_step = "boq" - st.rerun() + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") return + + # إنشاء DataFrame من بنود المشروع + df = pd.DataFrame(project_items) + df = df[['id', 'code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'resource_type']] - # تحليل التكلفة - st.subheader("تحليل التكلفة حسب نوع المورد") - - # حساب التكلفة حسب نوع المورد - resource_types = {} - for item in project_items: - resource_type = item.get('resource_type', 'أخرى') - if resource_type in resource_types: - resource_types[resource_type] += item['total_price'] - else: - resource_types[resource_type] = item['total_price'] - - # عرض التكلفة حسب نوع المورد - resource_df = pd.DataFrame({ - 'نوع المورد': list(resource_types.keys()), - 'التكلفة': list(resource_types.values()) - }) - - # إضافة نسبة التكلفة - total_cost = sum(resource_types.values()) - resource_df['النسبة'] = resource_df['التكلفة'] / total_cost * 100 - - # عرض الجدول - st.dataframe(resource_df) - - # عرض الرسم البياني - fig, ax = plt.subplots(figsize=(10, 6)) - ax.pie( - resource_df['التكلفة'], - labels=resource_df['نوع المورد'], - autopct='%1.1f%%', - startangle=90 + # تحويل الجدول إلى جدول قابل للتعديل + edited_df = st.data_editor( + df, + column_config={ + "id": st.column_config.Column("الرقم", disabled=True), + "code": st.column_config.Column("الكود"), + "description": st.column_config.Column("الوصف"), + "unit": st.column_config.Column("الوحدة"), + "quantity": st.column_config.NumberColumn("الكمية", min_value=0.0, format="%.2f", step=0.1), + "unit_price": st.column_config.NumberColumn("سعر الوحدة", min_value=0.0, format="%.2f ريال", step=0.1), + "total_price": st.column_config.NumberColumn("السعر الإجمالي", format="%.2f ريال", disabled=True), + "resource_type": st.column_config.SelectboxColumn("نوع المورد", options=["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"]) + }, + use_container_width=True, + key="edit_boq_items" ) - ax.axis('equal') - st.pyplot(fig) - # عرض إجمالي التكلفة - st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال") + # تحديث البيانات في حالة التعديل + if edited_df is not None and not edited_df.equals(df): + for i, row in edited_df.iterrows(): + item_id = row['id'] + for j, item in enumerate(st.session_state.boq_items): + if item['id'] == item_id: + st.session_state.boq_items[j]['code'] = row['code'] + st.session_state.boq_items[j]['description'] = row['description'] + st.session_state.boq_items[j]['unit'] = row['unit'] + st.session_state.boq_items[j]['quantity'] = row['quantity'] + st.session_state.boq_items[j]['unit_price'] = row['unit_price'] + st.session_state.boq_items[j]['total_price'] = row['quantity'] * row['unit_price'] + st.session_state.boq_items[j]['resource_type'] = row['resource_type'] + break + + st.success("تم تحديث جدول الكميات بنجاح") + st.rerun() - # أزرار التنقل بين الخطوات + # حساب المجموع الكلي + total_price = sum(item['total_price'] for item in project_items) + st.metric("إجمالي جدول الكميات", f"{total_price:,.2f} ريال") + + # أزرار التصدير والتعديل col1, col2 = st.columns(2) with col1: - if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq"): - st.session_state.pricing_step = "boq" - st.rerun() - + if st.button("تصدير جدول الكميات", key="export_boq_btn_1"): + # إنشاء CSV للتصدير + export_df = pd.DataFrame(project_items) + export_df = export_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'resource_type']] + export_df.columns = ['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي', 'نوع المورد'] + + csv = export_df.to_csv(index=False) + b64 = base64.b64encode(csv.encode()).decode() + href = f'تحميل CSV' + st.markdown(href, unsafe_allow_html=True) + with col2: - if st.button("متابعة إلى استراتيجيات التسعير ⬅️", key="continue_to_pricing_strategies", type="primary"): - st.session_state.pricing_step = "pricing_strategies" - st.rerun() + if len(project_items) > 0: + selected_item_id = st.selectbox( + "اختر بند للتعديل", + options=[item['id'] for item in project_items], + format_func=lambda x: next((item['description'] for item in project_items if item['id'] == x), ""), + key="select_boq_item" + ) + + if st.button("تعديل البند", key="edit_boq_item_btn"): + st.session_state.edit_boq_item_id = selected_item_id + st.session_state.show_edit_boq_item_form = True + st.rerun() + + def _render_new_boq_item_form(self): + """عرض نموذج إضافة بند جديد""" + st.subheader("إضافة بند جديد") - st.markdown('
', unsafe_allow_html=True) - - def _render_pricing_strategies_step(self): - """عرض خطوة استراتيجيات التسعير""" - st.markdown('
', unsafe_allow_html=True) - st.markdown('
الخطوة 4: استراتيجيات التسعير
', unsafe_allow_html=True) - - project_info = self._get_current_project_info() - if not project_info: - st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") - if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): - st.session_state.pricing_step = "project_info" + with st.form(key="new_boq_item_form"): + code = st.text_input("الكود", key="new_boq_item_code") + description = st.text_input("الوصف", key="new_boq_item_description") + + col1, col2 = st.columns(2) + with col1: + unit = st.text_input("الوحدة", key="new_boq_item_unit") + with col2: + resource_type = st.selectbox( + "نوع المورد", + ["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"], + key="new_boq_item_resource_type" + ) + + col3, col4 = st.columns(2) + with col3: + quantity = st.number_input("الكمية", min_value=0.0, format="%f", key="new_boq_item_quantity") + with col4: + unit_price = st.number_input("سعر الوحدة", min_value=0.0, format="%f", key="new_boq_item_unit_price") + + total_price = quantity * unit_price + st.metric("السعر الإجمالي", f"{total_price:,.2f} ريال") + + col5, col6 = st.columns(2) + with col5: + submit_button = st.form_submit_button("حفظ") + with col6: + cancel_button = st.form_submit_button("إلغاء") + + if submit_button: + if code and description and unit and quantity > 0 and unit_price > 0: + new_item = { + 'id': st.session_state.next_boq_item_id, + 'project_id': st.session_state.current_project, + 'code': code, + 'description': description, + 'unit': unit, + 'quantity': quantity, + 'unit_price': unit_price, + 'total_price': total_price, + 'resource_type': resource_type + } + + st.session_state.boq_items.append(new_item) + st.session_state.next_boq_item_id += 1 + st.session_state.show_new_boq_item_form = False st.rerun() + + if cancel_button: + st.session_state.show_new_boq_item_form = False + st.rerun() + + def _render_edit_boq_item_form(self): + """عرض نموذج تعديل البند""" + item = None + for i in st.session_state.boq_items: + if i['id'] == st.session_state.edit_boq_item_id: + item = i + break + + if not item: + st.session_state.show_edit_boq_item_form = False + st.rerun() return + + st.subheader(f"تعديل البند: {item['description']}") - # الحصول على بنود المشروع الحالي - project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] - - if not project_items: - st.warning("لا توجد بنود في جدول الكميات. يرجى العودة إلى الخطوة السابقة لإضافة البنود.") + with st.form(key="edit_boq_item_form"): + code = st.text_input("الكود", value=item['code'], key="edit_boq_item_code") + description = st.text_input("الوصف", value=item['description'], key="edit_boq_item_description") col1, col2 = st.columns(2) with col1: - if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq_btn"): - st.session_state.pricing_step = "boq" - st.rerun() - return - - # حساب إجمالي التكلفة - total_cost = sum(item['total_price'] for item in project_items) - - # عرض استراتيجيات التسعير - st.subheader("استراتيجيات التسعير") - - # اختيار استراتيجية التسعير - pricing_strategy = st.selectbox( - "اختر استراتيجية التسعير", - ["قياسي", "تنافسي", "مخصص"], - index=["قياسي", "تنافسي", "مخصص"].index(project_info['pricing_type']) if 'pricing_type' in project_info else 0 + unit = st.text_input("الوحدة", value=item['unit'], key="edit_boq_item_unit") + with col2: + resource_type = st.selectbox( + "نوع المورد", + ["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"], + index=["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"].index(item['resource_type']) if 'resource_type' in item else 0, + key="edit_boq_item_resource_type" + ) + + col3, col4 = st.columns(2) + with col3: + quantity = st.number_input("الكمية", min_value=0.0, value=float(item['quantity']), format="%f", key="edit_boq_item_quantity") + with col4: + unit_price = st.number_input("سعر الوحدة", min_value=0.0, value=float(item['unit_price']), format="%f", key="edit_boq_item_unit_price") + + total_price = quantity * unit_price + st.metric("السعر الإجمالي", f"{total_price:,.2f} ريال") + + col5, col6, col7 = st.columns(3) + with col5: + submit_button = st.form_submit_button("حفظ") + with col6: + cancel_button = st.form_submit_button("إلغاء") + with col7: + delete_button = st.form_submit_button("حذف البند", type="primary") + + if submit_button: + if code and description and unit and quantity > 0 and unit_price > 0: + for i, itm in enumerate(st.session_state.boq_items): + if itm['id'] == st.session_state.edit_boq_item_id: + st.session_state.boq_items[i]['code'] = code + st.session_state.boq_items[i]['description'] = description + st.session_state.boq_items[i]['unit'] = unit + st.session_state.boq_items[i]['quantity'] = quantity + st.session_state.boq_items[i]['unit_price'] = unit_price + st.session_state.boq_items[i]['total_price'] = total_price + st.session_state.boq_items[i]['resource_type'] = resource_type + break + + st.session_state.show_edit_boq_item_form = False + st.rerun() + + if cancel_button: + st.session_state.show_edit_boq_item_form = False + st.rerun() + + if delete_button: + for i, itm in enumerate(st.session_state.boq_items): + if itm['id'] == st.session_state.edit_boq_item_id: + st.session_state.boq_items.pop(i) + break + + st.session_state.show_edit_boq_item_form = False + st.rerun() + + def _render_resource_selector(self): + """عرض محدد الموارد""" + st.subheader("سحب من الموارد المسجلة") + + # اختيار نوع المورد + resource_type = st.selectbox( + "نوع المورد", + ["المواد", "العمالة", "المعدات", "المقاولين من الباطن"], + index=["المواد", "العمالة", "المعدات", "المقاولين من الباطن"].index(st.session_state.selected_resource_type), + key="resource_type_selector" ) - # تعيين نسبة الربح حسب استراتيجية التسعير - if pricing_strategy == "قياسي": - profit_percentage = st.slider("نسبة الربح", min_value=10, max_value=30, value=20, step=1) - st.info("استراتيجية التسعير القياسية تستخدم نسبة ربح ثابتة على إجمالي التكلفة.") - elif pricing_strategy == "تنافسي": - profit_percentage = st.slider("نسبة الربح", min_value=5, max_value=20, value=10, step=1) - st.info("استراتيجية التسعير التنافسية تستخدم نسبة ربح منخفضة لزيادة القدرة التنافسية.") - else: # مخصص - profit_percentage = st.slider("نسبة الربح", min_value=0, max_value=50, value=15, step=1) - st.info("استراتيجية التسعير المخصصة تتيح تحديد نسبة الربح بحرية.") - - # حساب السعر النهائي - profit_amount = total_cost * (profit_percentage / 100) - final_price = total_cost + profit_amount - - # عرض ملخص التسعير - st.subheader("ملخص التسعير") - - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال") - - with col2: - st.metric("مبلغ الربح", f"{profit_amount:,.2f} ريال") - - with col3: - st.metric("السعر النهائي", f"{final_price:,.2f} ريال") - - # زر تطبيق استراتيجية التسعير - if st.button("تطبيق استراتيجية التسعير", key="apply_pricing_strategy"): - # تحديث نوع التسعير في المشروع - self.storage.update_project(project_info['id'], {'pricing_type': pricing_strategy}) + if resource_type != st.session_state.selected_resource_type: + st.session_state.selected_resource_type = resource_type + st.rerun() - # تحديث حالة الجلسة - for i, p in enumerate(st.session_state.projects): - if p['id'] == project_info['id']: - st.session_state.projects[i]['pricing_type'] = pricing_strategy - break + # الحصول على الموارد المناسبة + resources = [] + if resource_type == "المواد" and 'materials' in st.session_state: + resources = st.session_state.materials + elif resource_type == "العمالة" and 'labor' in st.session_state: + resources = st.session_state.labor + elif resource_type == "المعدات" and 'equipment' in st.session_state: + resources = st.session_state.equipment + elif resource_type == "المقاولين من الباطن" and 'subcontractors' in st.session_state: + resources = st.session_state.subcontractors - # حفظ التسعير في سجل التسعير - pricing_data = { - 'project_id': project_info['id'], - 'pricing_strategy': pricing_strategy, - 'profit_percentage': profit_percentage, - 'total_cost': total_cost, - 'profit_amount': profit_amount, - 'final_price': final_price - } + if not resources: + st.info(f"لا توجد موارد مسجلة من نوع {resource_type}. يرجى إضافة موارد في وحدة الموارد أولاً.") + + col1, col2 = st.columns(2) + with col2: + if st.button("إلغاء", key="cancel_resource_selector"): + st.session_state.show_resource_selector = False + st.rerun() + return - self.storage.save_pricing(pricing_data) + # إنشاء DataFrame من الموارد + df = pd.DataFrame(resources) + if 'id' in df.columns and 'name' in df.columns and 'unit' in df.columns and 'price' in df.columns: + df = df[['id', 'name', 'category', 'unit', 'price']] + df.columns = ['الرقم', 'الاسم', 'الفئة', 'الوحدة', 'السعر'] - st.success("تم تطبيق استراتيجية التسعير بنجاح") - - # أزرار التنقل بين الخطوات - col1, col2 = st.columns(2) - with col1: - if st.button("العودة إلى تحليل التكلفة ➡️", key="back_to_cost_analysis"): - st.session_state.pricing_step = "cost_analysis" + # تنسيق السعر + df['السعر'] = df['السعر'].apply(lambda x: f"{x:,.2f} ريال") + + # عرض الجدول + st.dataframe(df, use_container_width=True) + + with st.form(key="add_from_resource_form"): + col1, col2 = st.columns(2) + with col1: + selected_resource_id = st.selectbox( + "اختر المورد", + options=[r['id'] for r in resources], + format_func=lambda x: next((r['name'] for r in resources if r['id'] == x), ""), + key="select_resource" + ) + + with col2: + quantity = st.number_input("الكمية", min_value=0.1, value=1.0, format="%f", key="resource_quantity") + + # الحصول على المورد المحدد + selected_resource = next((r for r in resources if r['id'] == selected_resource_id), None) + if selected_resource: + total_price = quantity * selected_resource['price'] + st.metric("السعر الإجمالي", f"{total_price:,.2f} ريال") + + col3, col4 = st.columns(2) + with col3: + submit_button = st.form_submit_button("إضافة إلى جدول الكميات") + with col4: + cancel_button = st.form_submit_button("إلغاء") + + if submit_button and selected_resource and quantity > 0: + # تحويل نوع المورد إلى الصيغة المناسبة + resource_type_map = { + "المواد": "مواد", + "العمالة": "عمالة", + "المعدات": "معدات", + "المقاولين من الباطن": "مقاولين من الباطن" + } + + # إنشاء كود فريد + resource_code_prefix = { + "المواد": "M", + "العمالة": "L", + "المعدات": "E", + "المقاولين من الباطن": "S" + } + + code_prefix = resource_code_prefix.get(resource_type, "X") + code = f"{code_prefix}-{selected_resource['id']:03d}" + + # إضافة البند إلى جدول الكميات + new_item = { + 'id': st.session_state.next_boq_item_id, + 'project_id': st.session_state.current_project, + 'code': code, + 'description': selected_resource['name'], + 'unit': selected_resource['unit'], + 'quantity': quantity, + 'unit_price': selected_resource['price'], + 'total_price': quantity * selected_resource['price'], + 'resource_type': resource_type_map.get(resource_type, "أخرى"), + 'resource_id': selected_resource['id'] + } + + st.session_state.boq_items.append(new_item) + st.session_state.next_boq_item_id += 1 + st.session_state.show_resource_selector = False st.rerun() - - with col2: - if st.button("متابعة إلى المراجعة ��لنهائية ⬅️", key="continue_to_review", type="primary"): - st.session_state.pricing_step = "review" + + if cancel_button: + st.session_state.show_resource_selector = False st.rerun() - - st.markdown('
', unsafe_allow_html=True) - - def _render_review_step(self): - """عرض خطوة المراجعة النهائية""" - st.markdown('
', unsafe_allow_html=True) - st.markdown('
الخطوة 5: المراجعة النهائية
', unsafe_allow_html=True) - - project_info = self._get_current_project_info() - if not project_info: - st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") - if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): - st.session_state.pricing_step = "project_info" + else: + st.error("تنسيق بيانات الموارد غير صحيح. يرجى التأكد من وجود الحقول المطلوبة.") + + if st.button("إلغاء", key="cancel_resource_selector_error"): + st.session_state.show_resource_selector = False st.rerun() - return + + def _render_item_price_analysis(self): + """عرض تحليل سعر البند مع إمكانية التعديل والحفظ""" + st.subheader("تحليل سعر البند") # الحصول على بنود المشروع الحالي project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] if not project_items: - st.warning("لا توجد بنود في جدول الكميات. يرجى العودة إلى الخطوة السابقة لإضافة البنود.") + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") + return - col1, col2 = st.columns(2) - with col1: - if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq_btn"): - st.session_state.pricing_step = "boq" - st.rerun() + # اختيار البند للتحليل + selected_item_id = st.selectbox( + "اختر البند للتحليل", + options=[item['id'] for item in project_items], + format_func=lambda x: next((item['description'] for item in project_items if item['id'] == x), ""), + key="select_item_for_analysis" + ) + + # الحصول على البند المحدد + selected_item = next((item for item in project_items if item['id'] == selected_item_id), None) + + if not selected_item: + st.error("لم يتم العثور على البند المحدد.") return + + # عرض معلومات البند + col1, col2 = st.columns(2) + with col1: + st.write(f"**البند:** {selected_item['description']}") + st.write(f"**الكود:** {selected_item['code']}") + st.write(f"**الوحدة:** {selected_item['unit']}") + with col2: + st.write(f"**الكمية:** {selected_item['quantity']}") + st.write(f"**سعر الوحدة:** {selected_item['unit_price']:,.2f} ريال") + st.write(f"**السعر الإجمالي:** {selected_item['total_price']:,.2f} ريال") - # الحصول على آخر تسعير للمشروع - pricing_history = self.storage.get_project_pricing_history(st.session_state.current_project) - latest_pricing = pricing_history[-1] if pricing_history else None + # إنشاء تحليل سعر البند + item_analysis = self._generate_item_analysis(selected_item) - # عرض معلومات المشروع - st.subheader("معلومات المشروع") + # عرض تحليل المواد مع إمكانية التعديل + self._render_materials_analysis(item_analysis) - col1, col2 = st.columns(2) + # عرض تحليل المعدات مع إمكانية التعديل + self._render_equipment_analysis(item_analysis) - with col1: - st.write(f"**اسم المشروع:** {project_info['name']}") - st.write(f"**العميل:** {project_info['client']}") - st.write(f"**الموقع:** {project_info.get('location', '-')}") - st.write(f"**رقم المناقصة:** {project_info.get('tender_number', '-')}") + # عرض تحليل العمالة مع إمكانية التعديل + self._render_labor_analysis(item_analysis) - with col2: - st.write(f"**القيمة التقديرية:** {project_info['estimated_value']:,.2f} ريال") - st.write(f"**الموعد النهائي:** {project_info['deadline']}") - st.write(f"**مدة العقد:** {project_info.get('contract_duration', '-')}") - st.write(f"**نوع التسعير:** {project_info['pricing_type']}") + # عرض تحليل المقاولين من الباطن مع إمكانية التعديل + self._render_subcontractors_analysis(item_analysis) - # عرض ملخص التسعير - st.subheader("ملخص التسعير") + # عرض ملخص التكلفة + self._render_cost_summary(item_analysis) - # حساب إجمالي التكلفة - total_cost = sum(item['total_price'] for item in project_items) + # زر حفظ التغييرات في جدول الكميات الرئيسي + if st.button("حفظ جميع التغييرات في جدول الكميات", key="save_all_changes", type="primary"): + # تحديث البند في جدول الكميات + self._update_boq_item_from_analysis(selected_item_id, item_analysis) + st.success("تم حفظ جميع التغييرات بنجاح في جدول الكميات الرئيسي") + st.session_state.item_analysis_edited = False + st.rerun() - if latest_pricing: - profit_percentage = latest_pricing['profit_percentage'] - profit_amount = latest_pricing['profit_amount'] - final_price = latest_pricing['final_price'] - else: - # استخدام قيم افتراضية إذا لم يكن هناك تسعير سابق - profit_percentage = 15 - profit_amount = total_cost * (profit_percentage / 100) - final_price = total_cost + profit_amount + def _generate_item_analysis(self, item): + """إنشاء تحليل سعر البند""" + # في هذه النسخة التجريبية، سنقوم بإنشاء بيانات عشوائية للتحليل + # في النسخة النهائية، يجب استخدام بيانات حقيقية من قاعدة البيانات - col1, col2, col3 = st.columns(3) + unit_price = item['unit_price'] - with col1: - st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال") + # تقسيم سعر الوحدة إلى مكوناته + materials_cost = unit_price * random.uniform(0.4, 0.6) + equipment_cost = unit_price * random.uniform(0.1, 0.2) + labor_cost = unit_price * random.uniform(0.1, 0.2) + overhead_cost = unit_price * random.uniform(0.05, 0.1) + profit = unit_price - (materials_cost + equipment_cost + labor_cost + overhead_cost) - with col2: - st.metric("مبلغ الربح", f"{profit_amount:,.2f} ريال", f"{profit_percentage}%") + # إنشاء قائمة المواد + materials = [ + { + 'name': 'خرسانة جاهزة' if 'خرسانة' in item['description'].lower() else 'حديد تسليح' if 'حديد' in item['description'].lower() else 'رمل', + 'unit': 'م3' if 'خرسانة' in item['description'].lower() else 'طن' if 'حديد' in item['description'].lower() else 'م3', + 'quantity': random.uniform(0.5, 1.5), + 'unit_price': materials_cost * 0.6, + 'total_price': materials_cost * 0.6 * random.uniform(0.5, 1.5) + }, + { + 'name': 'إسمنت', + 'unit': 'طن', + 'quantity': random.uniform(0.2, 0.5), + 'unit_price': materials_cost * 0.3, + 'total_price': materials_cost * 0.3 * random.uniform(0.2, 0.5) + }, + { + 'name': 'مواد أخرى', + 'unit': 'مجموعة', + 'quantity': 1, + 'unit_price': materials_cost * 0.1, + 'total_price': materials_cost * 0.1 + } + ] - with col3: - st.metric("السعر النهائي", f"{final_price:,.2f} ريال") + # إنشاء قائمة المعدات + equipment = [ + { + 'name': 'خلاطة خرسانة' if 'خرسانة' in item['description'].lower() else 'حفارة' if 'حفر' in item['description'].lower() else 'رافعة', + 'unit': 'يوم', + 'quantity': random.uniform(1, 3), + 'unit_price': equipment_cost * 0.7, + 'total_price': equipment_cost * 0.7 * random.uniform(1, 3) + }, + { + 'name': 'معدات أخرى', + 'unit': 'يوم', + 'quantity': random.uniform(1, 2), + 'unit_price': equipment_cost * 0.3, + 'total_price': equipment_cost * 0.3 * random.uniform(1, 2) + } + ] - # عرض جدول الكميات - st.subheader("جدول الكميات") + # إنشاء قائمة العمالة + labor = [ + { + 'name': 'عامل فني', + 'unit': 'يوم', + 'quantity': random.uniform(2, 5), + 'unit_price': labor_cost * 0.6, + 'total_price': labor_cost * 0.6 * random.uniform(2, 5) + }, + { + 'name': 'عامل عادي', + 'unit': 'يوم', + 'quantity': random.uniform(3, 8), + 'unit_price': labor_cost * 0.4, + 'total_price': labor_cost * 0.4 * random.uniform(3, 8) + } + ] - # إنشاء DataFrame للعرض - df = pd.DataFrame([ + # إنشاء قائمة المقاولين من الباطن + subcontractors = [ { - 'الكود': item['code'], - 'الوصف': item['description'], - 'الوحدة': item['unit'], - 'الكمية': item['quantity'], - 'سعر الوحدة': item['unit_price'], - 'السعر الإجمالي': item['total_price'], - 'نوع المورد': item.get('resource_type', '-') + 'name': 'مقاول أعمال خرسانية' if 'خرسانة' in item['description'].lower() else 'مقاول أعمال حفر' if 'حفر' in item['description'].lower() else 'مقاول عام', + 'unit': 'عقد', + 'quantity': 1, + 'unit_price': unit_price * 0.15, + 'total_price': unit_price * 0.15 } - for item in project_items - ]) + ] + + # حساب إجمالي التكاليف + total_materials_cost = sum(material['total_price'] for material in materials) + total_equipment_cost = sum(equipment_item['total_price'] for equipment_item in equipment) + total_labor_cost = sum(labor_item['total_price'] for labor_item in labor) + total_subcontractors_cost = sum(subcontractor['total_price'] for subcontractor in subcontractors) + + # تعديل الربح ليكون الفرق بين سعر الوحدة وإجمالي التكاليف + total_cost = total_materials_cost + total_equipment_cost + total_labor_cost + total_subcontractors_cost + overhead_cost + profit = unit_price - total_cost + + return { + 'item': item, + 'materials': materials, + 'equipment': equipment, + 'labor': labor, + 'subcontractors': subcontractors, + 'total_materials_cost': total_materials_cost, + 'total_equipment_cost': total_equipment_cost, + 'total_labor_cost': total_labor_cost, + 'total_subcontractors_cost': total_subcontractors_cost, + 'overhead_cost': overhead_cost, + 'profit': profit, + 'unit_price': unit_price + } - # عرض الجدول - st.dataframe(df) - - # خيارات التصدير - st.subheader("تصدير البيانات") - - export_options = st.multiselect( - "اختر خيارات التصدير", - [ - "جدول الكميات (CSV)", - "جدول الكميات (Excel)", - "تقرير كامل (PDF)", - "تقرير المحتوى المحلي (Excel)", - "تقرير المخاطر (Excel)" - ], - default=["جدول الكميات (CSV)"], - key="export_options" - ) + def _render_materials_analysis(self, item_analysis): + """عرض تحليل المواد مع إمكانية التعديل""" + st.subheader("تحليل المواد") - if st.button("تصدير البيانات", key="export_data_btn", type="primary"): - if "جدول الكميات (CSV)" in export_options: - # إنشاء ملف CSV للتصدير - csv_buffer = io.StringIO() - csv_writer = csv.writer(csv_buffer) - - # كتابة العناوين - csv_writer.writerow(['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي', 'نوع المورد']) - - # كتابة البيانات - for item in project_items: - csv_writer.writerow([ - item['code'], - item['description'], - item['unit'], - item['quantity'], - item['unit_price'], - item['total_price'], - item.get('resource_type', '-') - ]) - - # تحويل البيانات إلى base64 - b64 = base64.b64encode(csv_buffer.getvalue().encode('utf-8')).decode() - href = f'تحميل CSV' - st.markdown(href, unsafe_allow_html=True) + if not item_analysis['materials']: + st.info("لا توجد مواد في تحليل هذا البند.") - if "جدول الكميات (Excel)" in export_options: - # إنشاء ملف Excel للتصدير - excel_file = export_to_excel( - project_items, - project_info, - f"/tmp/boq_{st.session_state.current_project}.xlsx" - ) - - # عرض رابط التنزيل - href = get_download_link( - excel_file, - "تحميل Excel", - "excel" - ) - st.markdown(href, unsafe_allow_html=True) + # إضافة زر لإضافة مواد جديدة + if st.button("إضافة مواد", key="add_first_material"): + item_analysis['materials'] = [{ + 'name': 'مادة جديدة', + 'unit': 'وحدة', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }] + st.session_state.item_analysis_edited = True + st.rerun() + return - if "تقرير كامل (PDF)" in export_options: - # إنشاء ملف PDF للتصدير - pdf_file = export_to_pdf( - project_items, - project_info, - f"/tmp/report_{st.session_state.current_project}.pdf" - ) - - # عرض رابط التنزيل - href = get_download_link( - pdf_file, - "تحميل PDF", - "pdf" - ) - st.markdown(href, unsafe_allow_html=True) + # إنشاء DataFrame من قائمة المواد + df = pd.DataFrame(item_analysis['materials']) + df.columns = ['المادة', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] + + # تحويل الجدول إلى جدول قابل للتعديل + edited_df = st.data_editor( + df, + use_container_width=True, + key="edit_materials_table", + column_config={ + "المادة": st.column_config.Column("المادة"), + "الوحدة": st.column_config.Column("الوحدة"), + "الكمية": st.column_config.NumberColumn( + "الكمية", + min_value=0.0, + format="%.2f", + step=0.1, + ), + "سعر الوحدة": st.column_config.NumberColumn( + "سعر الوحدة", + min_value=0.0, + format="%.2f ريال", + step=0.1, + ), + "السعر الإجمالي": st.column_config.NumberColumn( + "السعر الإجمالي", + format="%.2f ريال", + disabled=True, + ), + }, + num_rows="dynamic" + ) + + # تحديث البيانات في item_analysis بناءً على التعديلات + if edited_df is not None and not edited_df.equals(df): + # حذف جميع المواد الحالية + item_analysis['materials'] = [] - if "تقرير المحتوى المحلي (Excel)" in export_options: - # إنشاء تقرير المحتوى المحلي - local_content_file = export_local_content_report( - project_items, - project_info, - f"/tmp/local_content_{st.session_state.current_project}.xlsx" - ) + # إضافة المواد المعدلة + for i, row in edited_df.iterrows(): + # حساب السعر الإجمالي + total_price = row['الكمية'] * row['سعر الوحدة'] - # عرض رابط التنزيل - href = get_download_link( - local_content_file, - "تحميل تقرير المحتوى المحلي", - "excel" - ) - st.markdown(href, unsafe_allow_html=True) + # إضافة المادة + item_analysis['materials'].append({ + 'name': row['المادة'], + 'unit': row['الوحدة'], + 'quantity': row['الكمية'], + 'unit_price': row['سعر الوحدة'], + 'total_price': total_price + }) - if "تقرير المخاطر (Excel)" in export_options: - # الحصول على مخاطر المشروع - project_risks = [risk for risk in st.session_state.risks if risk['project_id'] == st.session_state.current_project] - - if project_risks: - # إنشاء تقرير المخاطر - risk_report_file = export_risk_report( - project_risks, - project_info, - total_cost, - f"/tmp/risk_report_{st.session_state.current_project}.xlsx" - ) - - # عرض رابط التنزيل - href = get_download_link( - risk_report_file, - "تحميل تقرير المخاطر", - "excel" - ) - st.markdown(href, unsafe_allow_html=True) - else: - st.warning("لا توجد مخاطر مسجلة للمشروع. لا يمكن إنشاء تقرير المخاطر.") + # إعادة حساب إجمالي تكلفة المواد + item_analysis['total_materials_cost'] = sum(material['total_price'] for material in item_analysis['materials']) + + # تعيين علامة التعديل + st.session_state.item_analysis_edited = True + st.rerun() - # أزرار التنقل بين الخطوات - col1, col2, col3 = st.columns(3) + # عرض إجمالي تكلفة المواد + st.metric("إجمالي تكلفة المواد", f"{item_analysis['total_materials_cost']:,.2f} ريال") + + # أزرار التحكم + col1, col2 = st.columns(2) with col1: - if st.button("العودة إلى استراتيجيات التسعير ➡️", key="back_to_pricing_strategies"): - st.session_state.pricing_step = "pricing_strategies" + if st.button("إضافة مادة جديدة", key="add_material"): + item_analysis['materials'].append({ + 'name': 'مادة جديدة', + 'unit': 'وحدة', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }) + st.session_state.item_analysis_edited = True st.rerun() - with col2: - if st.button("متابعة إلى المحتوى المحلي ⬅️", key="continue_to_local_content", type="primary"): - st.session_state.pricing_step = "local_content" - st.rerun() + def _render_equipment_analysis(self, item_analysis): + """عرض تحليل المعدات مع إمكانية التعديل""" + st.subheader("تحليل المعدات") - with col3: - if st.button("إنهاء وعودة للصفحة الرئيسية", key="go_to_home"): - st.session_state.pricing_step = "project_info" + if not item_analysis['equipment']: + st.info("لا توجد معدات في تحليل هذا البند.") + + # إضافة زر لإضافة معدات جديدة + if st.button("إضافة معدات", key="add_first_equipment"): + item_analysis['equipment'] = [{ + 'name': 'معدة جديدة', + 'unit': 'يوم', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }] + st.session_state.item_analysis_edited = True st.rerun() + return + + # إنشاء DataFrame من قائمة المعدات + df = pd.DataFrame(item_analysis['equipment']) + df.columns = ['المعدة', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] - st.markdown('
', unsafe_allow_html=True) - - def _render_local_content_step(self): - """عرض خطوة احتساب المحتوى المحلي""" - st.markdown('
', unsafe_allow_html=True) - st.markdown('
الخطوة 6: المحتوى المحلي
', unsafe_allow_html=True) - - project_info = self._get_current_project_info() - if not project_info: - st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") - if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): - st.session_state.pricing_step = "project_info" + # تحويل الجدول إلى جدول قابل للتعديل + edited_df = st.data_editor( + df, + use_container_width=True, + key="edit_equipment_table", + column_config={ + "المعدة": st.column_config.Column("المعدة"), + "الوحدة": st.column_config.Column("الوحدة"), + "الكمية": st.column_config.NumberColumn( + "الكمية", + min_value=0.0, + format="%.2f", + step=0.1, + ), + "سعر الوحدة": st.column_config.NumberColumn( + "سعر الوحدة", + min_value=0.0, + format="%.2f ريال", + step=0.1, + ), + "السعر الإجمالي": st.column_config.NumberColumn( + "السعر الإجمالي", + format="%.2f ريال", + disabled=True, + ), + }, + num_rows="dynamic" + ) + + # تحديث البيانات في item_analysis بناءً على التعديلات + if edited_df is not None and not edited_df.equals(df): + # حذف جميع المعدات الحالية + item_analysis['equipment'] = [] + + # إضافة المعدات المعدلة + for i, row in edited_df.iterrows(): + # حساب السعر الإجمالي + total_price = row['الكمية'] * row['سعر الوحدة'] + + # إضافة المعدة + item_analysis['equipment'].append({ + 'name': row['المعدة'], + 'unit': row['الوحدة'], + 'quantity': row['الكمية'], + 'unit_price': row['سعر الوحدة'], + 'total_price': total_price + }) + + # إعادة حساب إجمالي تكلفة المعدات + item_analysis['total_equipment_cost'] = sum(equipment_item['total_price'] for equipment_item in item_analysis['equipment']) + + # تعيين علامة التعديل + st.session_state.item_analysis_edited = True + st.rerun() + + # عرض إجمالي تكلفة المعدات + st.metric("إجمالي تكلفة المعدات", f"{item_analysis['total_equipment_cost']:,.2f} ريال") + + # أزرار التحكم + col1, col2 = st.columns(2) + with col1: + if st.button("إضافة معدة جديدة", key="add_equipment"): + item_analysis['equipment'].append({ + 'name': 'معدة جديدة', + 'unit': 'يوم', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }) + st.session_state.item_analysis_edited = True st.rerun() - return - # الحصول على بنود المشروع الحالي - project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + def _render_labor_analysis(self, item_analysis): + """عرض تحليل العمالة مع إمكانية التعديل""" + st.subheader("تحليل العمالة") - if not project_items: - st.warning("لا توجد بنود في جدول الكميات. يرجى العودة إلى الخطوة السابقة لإضافة البنود.") + if not item_analysis['labor']: + st.info("لا توجد عمالة في تحليل هذا البند.") - col1, col2 = st.columns(2) - with col1: - if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq_btn"): - st.session_state.pricing_step = "boq" - st.rerun() + # إضافة زر لإضافة عمالة جديدة + if st.button("إضافة عمالة", key="add_first_labor"): + item_analysis['labor'] = [{ + 'name': 'عامل جديد', + 'unit': 'يوم', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }] + st.session_state.item_analysis_edited = True + st.rerun() return + + # إنشاء DataFrame من قائمة العمالة + df = pd.DataFrame(item_analysis['labor']) + df.columns = ['العامل', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] - # تعيين النسبة المستهدفة للمحتوى المحلي - local_content_target = st.slider( - "النسبة المستهدفة للمحتوى المحلي (%)", - min_value=0, - max_value=100, - value=project_info.get('local_content_target', 40), - step=5 + # تحويل الجدول إلى جدول قابل للتعديل + edited_df = st.data_editor( + df, + use_container_width=True, + key="edit_labor_table", + column_config={ + "العامل": st.column_config.Column("العامل"), + "الوحدة": st.column_config.Column("الوحدة"), + "الكمية": st.column_config.NumberColumn( + "الكمية", + min_value=0.0, + format="%.2f", + step=0.1, + ), + "سعر الوحدة": st.column_config.NumberColumn( + "سعر الوحدة", + min_value=0.0, + format="%.2f ريال", + step=0.1, + ), + "السعر الإجمالي": st.column_config.NumberColumn( + "السعر الإجمالي", + format="%.2f ريال", + disabled=True, + ), + }, + num_rows="dynamic" ) - # تحديث النسبة المستهدفة في المشروع - if local_content_target != project_info.get('local_content_target', 40): - self.storage.update_project(project_info['id'], {'local_content_target': local_content_target}) + # تحديث البيانات في item_analysis بناءً على التعديلات + if edited_df is not None and not edited_df.equals(df): + # حذف جميع العمالة الحالية + item_analysis['labor'] = [] - # تحديث حالة الجلسة - for i, p in enumerate(st.session_state.projects): - if p['id'] == project_info['id']: - st.session_state.projects[i]['local_content_target'] = local_content_target - break + # إضافة العمالة المعدلة + for i, row in edited_df.iterrows(): + # حساب السعر الإجمالي + total_price = row['الكمية'] * row['سعر الوحدة'] + + # إضافة العامل + item_analysis['labor'].append({ + 'name': row['العامل'], + 'unit': row['الوحدة'], + 'quantity': row['الكمية'], + 'unit_price': row['سعر الوحدة'], + 'total_price': total_price + }) + + # إعادة حساب إجمالي تكلفة العمالة + item_analysis['total_labor_cost'] = sum(labor_item['total_price'] for labor_item in item_analysis['labor']) + + # تعيين علامة التعديل + st.session_state.item_analysis_edited = True + st.rerun() + + # عرض إجمالي تكلفة العمالة + st.metric("إجمالي تكلفة العمالة", f"{item_analysis['total_labor_cost']:,.2f} ريال") + + # أزرار التحكم + col1, col2 = st.columns(2) + with col1: + if st.button("إضافة عامل جديد", key="add_labor"): + item_analysis['labor'].append({ + 'name': 'عامل جديد', + 'unit': 'يوم', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }) + st.session_state.item_analysis_edited = True + st.rerun() + + def _render_subcontractors_analysis(self, item_analysis): + """عرض تحليل المقاولين من الباطن مع إمكانية التعديل""" + st.subheader("تحليل المقاولين من الباطن") - # عرض جدول البنود مع معلومات المحتوى المحلي - st.subheader("تحديث معلومات المحتوى المحلي") + # التحقق من وجود مفتاح المقاولين من الباطن في التحليل + if 'subcontractors' not in item_analysis: + item_analysis['subcontractors'] = [] + item_analysis['total_subcontractors_cost'] = 0 - # إنشاء DataFrame للعرض - df = pd.DataFrame([ - { - 'معرف': item['id'], - 'الكود': item['code'], - 'الوصف': item['description'], - 'نوع المورد': item.get('resource_type', '-'), - 'مورد محلي': item.get('is_local_supplier', False), - 'نسبة المحتوى المحلي (%)': item.get('local_content_percentage', 0), - 'السعر الإجمالي': item['total_price'] - } - for item in project_items - ]) + if not item_analysis['subcontractors']: + st.info("لا يوجد مقاولين من الباطن في تحليل هذا البند.") + + # إضافة زر لإضافة مقاول جديد + if st.button("إضافة مقاول من الباطن", key="add_first_subcontractor"): + item_analysis['subcontractors'] = [{ + 'name': 'مقاول جديد', + 'unit': 'عقد', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }] + st.session_state.item_analysis_edited = True + st.rerun() + return + + # إنشاء DataFrame من قائمة المقاولين + df = pd.DataFrame(item_analysis['subcontractors']) + df.columns = ['المقاول', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] - # عرض الجدول + # تحويل الجدول إلى جدول قابل للتعديل edited_df = st.data_editor( df, + use_container_width=True, + key="edit_subcontractors_table", column_config={ - 'معرف': st.column_config.TextColumn(disabled=True), - 'الكود': st.column_config.TextColumn(disabled=True), - 'الوصف': st.column_config.TextColumn(disabled=True), - 'نوع المورد': st.column_config.TextColumn(disabled=True), - 'مورد محلي': st.column_config.CheckboxColumn( - "مورد محلي", - help="حدد إذا كان المورد محلي" + "المقاول": st.column_config.Column("المقاول"), + "الوحدة": st.column_config.Column("الوحدة"), + "الكمية": st.column_config.NumberColumn( + "الكمية", + min_value=0.0, + format="%.2f", + step=0.1, ), - 'نسبة المحتوى المحلي (%)': st.column_config.NumberColumn( - "نسبة المحتوى المحلي (%)", - min_value=0, - max_value=100, - step=5, - format="%d %%" + "سعر الوحدة": st.column_config.NumberColumn( + "سعر الوحدة", + min_value=0.0, + format="%.2f ريال", + step=0.1, ), - 'السعر الإجمالي': st.column_config.NumberColumn( + "السعر الإجمالي": st.column_config.NumberColumn( "السعر الإجمالي", - format="%,.2f ريال", - disabled=True - ) + format="%.2f ريال", + disabled=True, + ), }, - hide_index=True, - use_container_width=True, - key="local_content_editor" + num_rows="dynamic" ) - # تحديث معلومات المحتوى المحلي - if st.button("حفظ معلومات المحتوى المحلي", key="save_local_content"): + # تحديث البيانات في item_analysis بناءً على التعديلات + if edited_df is not None and not edited_df.equals(df): + # حذف جميع المقاولين الحاليين + item_analysis['subcontractors'] = [] + + # إضافة المقاولين المعدلين for i, row in edited_df.iterrows(): - item_id = row['معرف'] - is_local_supplier = row['مورد محلي'] - local_content_percentage = row['نسبة المحتوى المحلي (%)'] + # حساب السعر الإجمالي + total_price = row['الكمية'] * row['سعر الوحدة'] - # تحديث البند - self.storage.update_boq_item(item_id, { - 'is_local_supplier': is_local_supplier, - 'local_content_percentage': local_content_percentage + # إضافة المقاول + item_analysis['subcontractors'].append({ + 'name': row['المقاول'], + 'unit': row['الوحدة'], + 'quantity': row['الكمية'], + 'unit_price': row['سعر الوحدة'], + 'total_price': total_price }) - - # تحديث حالة الجلسة - for j, item in enumerate(st.session_state.boq_items): - if item['id'] == item_id: - st.session_state.boq_items[j]['is_local_supplier'] = is_local_supplier - st.session_state.boq_items[j]['local_content_percentage'] = local_content_percentage - break - - st.success("تم حفظ معلومات المحتوى المحلي بنجاح") - - # حساب المحتوى المحلي - if st.button("حساب المحتوى المحلي", key="calculate_local_content"): - # تحديث بنود المشروع بعد التعديل - project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] - - # حساب ملخص المحتوى المحلي - local_content_summary = self._calculate_local_content_summary(project_info['id']) - # تحديث ملخص المحتوى المحلي في المشروع - self.storage.update_project(project_info['id'], {'local_content_summary': local_content_summary}) - - # تحديث حالة الجلسة - for i, p in enumerate(st.session_state.projects): - if p['id'] == project_info['id']: - st.session_state.projects[i]['local_content_summary'] = local_content_summary - break + # إعادة حساب إجمالي تكلفة المقاولين + item_analysis['total_subcontractors_cost'] = sum(subcontractor['total_price'] for subcontractor in item_analysis['subcontractors']) - st.success("تم حساب المحتوى المحلي بنجاح") + # تعيين علامة التعديل + st.session_state.item_analysis_edited = True st.rerun() - # عرض ملخص المحتوى المحلي - st.subheader("ملخص المحتوى المحلي") - - # الحصول على ملخص المحتوى المحلي - local_content_summary = project_info.get('local_content_summary', { - 'total_percentage': 0, - 'by_category': { - 'materials': 0, - 'labor': 0, - 'services': 0, - 'equipment': 0 - } - }) - - # عرض النسبة الإجمالية للمحتوى المحلي - total_percentage = local_content_summary.get('total_percentage', 0) + # عرض إجمالي تكلفة المقاولين + st.metric("إجمالي تكلفة المقاولين من الباطن", f"{item_analysis['total_subcontractors_cost']:,.2f} ريال") - # تحديد لون المؤشر حسب النسبة المستهدفة - if total_percentage >= local_content_target: - delta_color = "normal" # أخضر - else: - delta_color = "inverse" # أحمر + # أزرار التحكم + col1, col2 = st.columns(2) + with col1: + if st.button("إضافة مقاول جديد", key="add_subcontractor"): + item_analysis['subcontractors'].append({ + 'name': 'مقاول جديد', + 'unit': 'عقد', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }) + st.session_state.item_analysis_edited = True + st.rerun() + + def _setup_arabic_fonts(self): + """إعداد الخطوط العربية للرسوم البيانية""" + plt.rcParams['font.family'] = 'Arial' + plt.rcParams['axes.unicode_minus'] = False + + def _render_cost_summary(self, item_analysis): + """عرض ملخص التكلفة""" + st.subheader("ملخص التكلفة") + + # إعداد الخطوط العربية + self._setup_arabic_fonts() + + # إنشاء بيانات الرسم البياني + # استخدام arabic_reshaper و bidi لمعالجة النص العربي + labels_ar = ['المواد', 'المعدات', 'العمالة', 'المقاولين من الباطن', 'المصاريف العمومية', 'الربح'] + labels = [get_display(arabic_reshaper.reshape(label)) for label in labels_ar] + + # التحقق من وجود قيم NaN واستبدالها بأصفار + values = [ + item_analysis['total_materials_cost'] if not np.isnan(item_analysis['total_materials_cost']) else 0, + item_analysis['total_equipment_cost'] if not np.isnan(item_analysis['total_equipment_cost']) else 0, + item_analysis['total_labor_cost'] if not np.isnan(item_analysis['total_labor_cost']) else 0, + item_analysis['total_subcontractors_cost'] if not np.isnan(item_analysis.get('total_subcontractors_cost', 0)) else 0, + item_analysis['overhead_cost'] if not np.isnan(item_analysis['overhead_cost']) else 0, + item_analysis['profit'] if not np.isnan(item_analysis['profit']) else 0 + ] + + # التحقق من أن جميع القيم موجبة (أو صفر) لتجنب الأخطاء في الرسم البياني + values = [max(0, val) for val in values] + + # إنشاء الرسم البياني + fig, ax = plt.subplots(figsize=(10, 6)) - st.metric( - "النسبة الإجمالية للمحتوى المحلي", - f"{total_percentage:.1f}%", - f"{total_percentage - local_content_target:.1f}% من المستهدف", - delta_color=delta_color + # رسم المخطط الدائري + wedges, texts, autotexts = ax.pie( + values, + labels=labels, + autopct='%1.1f%%', + startangle=90, + shadow=True, ) - # عرض النسب حسب الفئة - st.subheader("المحتوى المحلي حسب الفئة") + # تعديل حجم النص + plt.setp(autotexts, size=10, weight="bold") - by_category = local_content_summary.get('by_category', {}) + # إضافة العنوان + title_ar = 'توزيع تكلفة البند' + title = get_display(arabic_reshaper.reshape(title_ar)) + ax.set_title(title, fontsize=16) - col1, col2, col3, col4 = st.columns(4) + # عرض الرسم البياني + st.pyplot(fig) - with col1: - st.metric("المواد", f"{by_category.get('materials', 0):.1f}%") + # عرض جدول ملخص التكلفة + cost_summary = { + 'البند': ['المواد', 'المعدات', 'العمالة', 'المقاولين من الباطن', 'المصاريف العمومية', 'الربح', 'الإجمالي'], + 'التكلفة': [ + item_analysis['total_materials_cost'], + item_analysis['total_equipment_cost'], + item_analysis['total_labor_cost'], + item_analysis.get('total_subcontractors_cost', 0), + item_analysis['overhead_cost'], + item_analysis['profit'], + item_analysis['unit_price'] + ], + 'النسبة': [ + item_analysis['total_materials_cost'] / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, + item_analysis['total_equipment_cost'] / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, + item_analysis['total_labor_cost'] / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, + item_analysis.get('total_subcontractors_cost', 0) / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, + item_analysis['overhead_cost'] / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, + item_analysis['profit'] / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, + 100 + ] + } - with col2: - st.metric("العمالة", f"{by_category.get('labor', 0):.1f}%") + df = pd.DataFrame(cost_summary) + df.columns = ['البند', 'التكلفة (ريال)', 'النسبة (%)'] - with col3: - st.metric("الخدمات", f"{by_category.get('services', 0):.1f}%") + # تنسيق الأرقام + df['التكلفة (ريال)'] = df['التكلفة (ريال)'].apply(lambda x: f"{x:,.2f}") + df['النسبة (%)'] = df['النسبة (%)'].apply(lambda x: f"{x:.2f}%") - with col4: - st.metric("المعدات", f"{by_category.get('equipment', 0):.1f}%") + # عرض الجدول + st.dataframe(df, use_container_width=True) - # عرض الرسم البياني - if by_category: - fig, ax = plt.subplots(figsize=(10, 6)) - categories = list(by_category.keys()) - percentages = list(by_category.values()) - - ax.bar(categories, percentages) - ax.set_ylabel('النسبة المئوية (%)') - ax.set_title('المحتوى المحلي حسب الفئة') - ax.axhline(y=local_content_target, color='r', linestyle='-', label=f'المستهدف ({local_content_target}%)') - ax.legend() - - st.pyplot(fig) - - # تصدير تقرير المحتوى المحلي - if st.button("تصدير تقرير المحتوى المحلي", key="export_local_content_report", type="primary"): - # إنشاء تقرير المحتوى المحلي - local_content_file = export_local_content_report( - project_items, - project_info, - f"/tmp/local_content_{st.session_state.current_project}.xlsx" - ) - - # عرض رابط التنزيل - href = get_download_link( - local_content_file, - "تحميل تقرير المحتوى المحلي", - "excel" - ) - st.markdown(href, unsafe_allow_html=True) + # إضافة حقل لتعديل المصاريف العمومية + st.subheader("تعديل المصاريف العمومية والربح") - # أزرار التنقل بين الخطوات col1, col2 = st.columns(2) with col1: - if st.button("العودة إلى المراجعة النهائية ➡️", key="back_to_review"): - st.session_state.pricing_step = "review" + new_overhead = st.number_input( + "المصاريف العمومية (ريال)", + min_value=0.0, + value=float(item_analysis['overhead_cost']), + step=10.0, + format="%.2f", + key="edit_overhead_cost" + ) + + if new_overhead != item_analysis['overhead_cost']: + item_analysis['overhead_cost'] = new_overhead + + # إعادة حساب الربح + total_cost = ( + item_analysis['total_materials_cost'] + + item_analysis['total_equipment_cost'] + + item_analysis['total_labor_cost'] + + item_analysis.get('total_subcontractors_cost', 0) + + item_analysis['overhead_cost'] + ) + + item_analysis['profit'] = item_analysis['unit_price'] - total_cost + st.session_state.item_analysis_edited = True st.rerun() - + with col2: - if st.button("متابعة إلى تقييم المخاطر ⬅️", key="continue_to_risk_assessment", type="primary"): - st.session_state.pricing_step = "risk_assessment" + new_profit = st.number_input( + "الربح (ريال)", + min_value=float(-1000000.0), + value=float(item_analysis['profit']), + step=10.0, + format="%.2f", + key="edit_profit" + ) + + if new_profit != item_analysis['profit']: + item_analysis['profit'] = new_profit + + # إعادة حساب سعر الوحدة + total_cost = ( + item_analysis['total_materials_cost'] + + item_analysis['total_equipment_cost'] + + item_analysis['total_labor_cost'] + + item_analysis.get('total_subcontractors_cost', 0) + + item_analysis['overhead_cost'] + ) + + item_analysis['unit_price'] = total_cost + item_analysis['profit'] + st.session_state.item_analysis_edited = True st.rerun() - st.markdown('
', unsafe_allow_html=True) + def _update_boq_item_from_analysis(self, item_id, item_analysis): + """تحديث البند في جدول الكميات بناءً على التحليل""" + # حساب السعر الإجمالي الجديد + total_cost = ( + item_analysis['total_materials_cost'] + + item_analysis['total_equipment_cost'] + + item_analysis['total_labor_cost'] + + item_analysis.get('total_subcontractors_cost', 0) + + item_analysis['overhead_cost'] + + item_analysis['profit'] + ) + + # تحديث سعر الوحدة والسعر الإجمالي في البند + for i, item in enumerate(st.session_state.boq_items): + if item['id'] == item_id: + # حفظ الكمية الأصلية + quantity = item['quantity'] + + # تحديث سعر الوحدة + unit_price = total_cost + + # تحديث البند + st.session_state.boq_items[i]['unit_price'] = unit_price + st.session_state.boq_items[i]['total_price'] = unit_price * quantity + break - def _calculate_local_content_summary(self, project_id): - """حساب ملخص المحتوى المحلي للمشروع""" - # الحصول على بنود المشروع - project_items = [item for item in st.session_state.boq_items if item['project_id'] == project_id] + def _render_cost_analysis(self, project_info): + """عرض تحليل التكلفة""" + st.subheader("تحليل التكلفة") - if not project_items: - return { - 'total_percentage': 0, - 'by_category': { - 'materials': 0, - 'labor': 0, - 'services': 0, - 'equipment': 0 - } - } + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + if not project_items: + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") + return + # حساب إجمالي التكلفة total_cost = sum(item['total_price'] for item in project_items) - if total_cost == 0: - return { - 'total_percentage': 0, - 'by_category': { - 'materials': 0, - 'labor': 0, - 'services': 0, - 'equipment': 0 - } - } + # عرض إجمالي التكلفة + st.metric("إجمالي تكلفة المشروع", f"{total_cost:,.2f} ريال") - # حساب المحتوى المحلي الإجمالي - local_content_value = sum(item['total_price'] * item.get('local_content_percentage', 0) / 100 for item in project_items) - total_percentage = local_content_value / total_cost * 100 + # إنشاء DataFrame من بنود المشروع + df = pd.DataFrame(project_items) + df = df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price']] + df.columns = ['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] - # حساب المحتوى المحلي حسب الفئة - categories = {} - for category in ['materials', 'labor', 'services', 'equipment']: - category_items = [item for item in project_items if item.get('resource_type', '-').lower() == category] - - if category_items: - category_cost = sum(item['total_price'] for item in category_items) - category_local_content = sum(item['total_price'] * item.get('local_content_percentage', 0) / 100 for item in category_items) - category_percentage = category_local_content / category_cost * 100 if category_cost > 0 else 0 - categories[category] = category_percentage - else: - categories[category] = 0 + # تنسيق الأسعار + df['سعر الوحدة'] = df['سعر الوحدة'].apply(lambda x: f"{x:,.2f} ريال") + df['السعر الإجمالي'] = df['السعر الإجمالي'].apply(lambda x: f"{x:,.2f} ريال") - # إنشاء ملخص المحتوى المحلي - summary = { - 'total_percentage': total_percentage, - 'by_category': categories - } + # عرض الجدول + st.dataframe(df, use_container_width=True) - return summary - - def _render_risk_assessment_step(self): - """عرض خطوة تقييم المخاطر""" - st.markdown('
', unsafe_allow_html=True) - st.markdown('
الخطوة 7: تقييم المخاطر
', unsafe_allow_html=True) - - project_info = self._get_current_project_info() - if not project_info: - st.error("لم يتم العثور على المشروع. الرجاء العودة إلى الخطوة السابقة.") - if st.button("العودة إلى معلومات المشروع ➡️", key="back_to_project_info"): - st.session_state.pricing_step = "project_info" - st.rerun() - return + # إنشاء رسم بياني للتكلفة + self._setup_arabic_fonts() + + # إنشاء بيانات الرسم البياني + fig, ax = plt.subplots(figsize=(10, 6)) + + # تحويل الأسعار إلى أرقام + prices = [item['total_price'] for item in project_items] + labels = [item['description'] for item in project_items] + + # تحويل النص العربي + labels = [get_display(arabic_reshaper.reshape(label)) for label in labels] + + # رسم المخطط الشريطي + bars = ax.bar(labels, prices) + + # تدوير التسميات + plt.xticks(rotation=45, ha='right') + + # إضافة العنوان + title_ar = 'توزيع تكلفة المشروع حسب البنود' + title = get_display(arabic_reshaper.reshape(title_ar)) + ax.set_title(title, fontsize=16) + + # إضافة التسميات + ax.set_ylabel(get_display(arabic_reshaper.reshape('التكلفة (ريال)'))) + + # تنسيق الرسم البياني + plt.tight_layout() + + # عرض الرسم البياني + st.pyplot(fig) + + def _render_profit_margin(self, project_info): + """عرض تحليل الربحية""" + st.subheader("تحليل الربحية") # الحصول على بنود المشروع الحالي project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] if not project_items: - st.warning("لا توجد بنود في جدول الكميات. يرجى العودة إلى الخطوة السابقة لإضافة البنود.") - - col1, col2 = st.columns(2) - with col1: - if st.button("العودة إلى جدول الكميات ➡️", key="back_to_boq_btn"): - st.session_state.pricing_step = "boq" - st.rerun() + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") return - + # حساب إجمالي التكلفة total_cost = sum(item['total_price'] for item in project_items) - # الحصول على مخاطر المشروع الحالي - project_risks = [risk for risk in st.session_state.risks if risk['project_id'] == st.session_state.current_project] - - # استخدام علامات تبويب لتنظيم العرض - tabs = st.tabs(["ملخص المخاطر", "قائمة المخاطر", "إضافة مخاطرة جديدة", "تقرير المخاطر"]) + # حساب هامش الربح + profit_margin = 0.15 # افتراضي 15% + profit = total_cost * profit_margin + total_with_profit = total_cost + profit - with tabs[0]: - # عرض ملخص المخاطر - st.subheader("ملخص المخاطر") - - # حساب ملخص المخاطر - risk_summary = self._calculate_risk_summary(project_info['id'], total_cost) - - # عرض الإحصائيات الرئيسية - col1, col2, col3 = st.columns(3) - - with col1: - st.metric("إجمالي عدد المخاطر", risk_summary['total_risks']) + # عرض معلومات الربحية + col1, col2, col3 = st.columns(3) + with col1: + st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال") + with col2: + st.metric("هامش الربح", f"{profit_margin:.1%}") + with col3: + st.metric("إجمالي مع الربح", f"{total_with_profit:,.2f} ريال") - with col2: - st.metric("احتياطي المخاطر المقترح", f"{risk_summary['risk_contingency']:,.2f} ريال") + # إضافة شريط تمرير لتعديل هامش الربح + new_profit_margin = st.slider( + "تعديل هامش الربح", + min_value=0.0, + max_value=0.5, + value=profit_margin, + step=0.01, + format="%.2f", + key="profit_margin_slider" + ) + + if new_profit_margin != profit_margin: + profit_margin = new_profit_margin + profit = total_cost * profit_margin + total_with_profit = total_cost + profit - with col3: - st.metric("نسبة احتياطي المخاطر", f"{risk_summary['risk_contingency_percentage']:.1f}%") + st.metric("إجمالي مع الربح الجديد", f"{total_with_profit:,.2f} ريال") - # عرض توزيع المخاطر حسب الشدة - st.subheader("توزيع المخاطر حسب الشدة") + # إنشاء رسم بياني للربحية + self._setup_arabic_fonts() + + # إنشاء بيانات الرسم البياني + fig, ax = plt.subplots(figsize=(10, 6)) + + # إنشاء المخطط الدائري + labels_ar = ['التكلفة', 'الربح'] + labels = [get_display(arabic_reshaper.reshape(label)) for label in labels_ar] + + values = [total_cost, profit] + + # رسم المخطط الدائري + wedges, texts, autotexts = ax.pie( + values, + labels=labels, + autopct='%1.1f%%', + startangle=90, + shadow=True, + colors=['#ff9999', '#66b3ff'] + ) + + # تعديل حجم النص + plt.setp(autotexts, size=10, weight="bold") + + # إضافة العنوان + title_ar = 'توزيع التكلفة والربح' + title = get_display(arabic_reshaper.reshape(title_ar)) + ax.set_title(title, fontsize=16) + + # عرض الرسم البياني + st.pyplot(fig) + + def _render_pricing_strategies(self, project_info): + """عرض استراتيجيات التسعير""" + st.subheader("استراتيجيات التسعير") + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + + if not project_items: + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") + return - col1, col2, col3 = st.columns(3) + # عرض استراتيجيات التسعير المختلفة + st.write("### استراتيجيات التسعير المتاحة") + + # استراتيجية التسعير القياسية + st.write("#### التسعير القياسي") + st.write("في هذه الاستراتيجية، يتم تسعير جميع البنود بناءً على التكلفة الفعلية مع إضافة هامش ربح موحد.") + + # استراتيجية التسعير الغير متزن + st.write("#### التسعير الغير متزن") + st.write("في هذه الاستراتيجية، يتم زيادة أسعار البنود المبكرة وتخفيض أسعار البنود المتأخرة للحصول على تدفق نقدي أفضل.") + + # تعديل معاملات التسعير الغير متزن + if project_info['pricing_type'] == 'غير متزن': + st.write("### تعديل معاملات التسعير الغير متزن") + col1, col2 = st.columns(2) with col1: - st.metric("مخاطر عالية", risk_summary['high_risks'], delta_color="inverse") - - with col2: - st.metric("مخاطر متوسطة", risk_summary['medium_risks'], delta_color="off") - - with col3: - st.metric("مخاطر منخفضة", risk_summary['low_risks'], delta_color="normal") - - # عرض رسم بياني للمخاطر - if project_risks: - st.subheader("توزيع المخاطر حسب الفئة") - - # تجميع المخاطر حسب الفئة - category_counts = {} - for risk in project_risks: - category = risk['category'] - if category in category_counts: - category_counts[category] += 1 - else: - category_counts[category] = 1 - - # إنشاء DataFrame للرسم البياني - chart_data = pd.DataFrame({ - "الفئة": list(category_counts.keys()), - "عدد المخاطر": list(category_counts.values()) - }) - - # عرض الرسم البياني - st.bar_chart(chart_data, x="الفئة", y="عدد المخاطر") - - # عرض مصفوفة المخاطر - st.subheader("مصفوفة المخاطر") - - # إنشاء مصفوفة المخاطر - risk_matrix = self._create_risk_matrix(project_risks) - - # عرض المصفوفة - st.dataframe( - risk_matrix, - column_config={ - "": st.column_config.TextColumn(""), - "1": st.column_config.TextColumn("1 - ضئيل"), - "2": st.column_config.TextColumn("2 - منخفض"), - "3": st.column_config.TextColumn("3 - متوسط"), - "4": st.column_config.TextColumn("4 - عالي"), - "5": st.column_config.TextColumn("5 - حرج") - }, - hide_index=True - ) - else: - st.info("لا توجد مخاطر مسجلة للمشروع. يرجى إضافة مخاطر لعرض التحليل.") - - with tabs[1]: - # عرض قائمة المخاطر - st.subheader("قائمة المخاطر") - - if project_risks: - # عرض جدول المخاطر - risk_df = pd.DataFrame([ - { - 'معرف': risk['id'], - 'اسم المخاطرة': risk['name'], - 'الفئة': risk['category'], - 'الاحتمالية': risk['probability'], - 'التأثير': risk['impact'], - 'درجة المخاطرة': risk['risk_score'], - 'التأثير المالي': risk['cost_impact'], - 'التأثير على الجدول': risk['schedule_impact'], - 'الحالة': risk['status'] - } - for risk in project_risks - ]) - - # تنسيق الجدول - st.dataframe( - risk_df, - column_config={ - 'معرف': st.column_config.NumberColumn(disabled=True), - 'اسم المخاطرة': st.column_config.TextColumn(), - 'الفئة': st.column_config.TextColumn(), - 'الاحتمالية': st.column_config.NumberColumn(format="%d"), - 'التأثير': st.column_config.NumberColumn(format="%d"), - 'درجة المخاطرة': st.column_config.NumberColumn(format="%d"), - 'التأثير المالي': st.column_config.NumberColumn(format="%,.2f ريال"), - 'التأثير على الجدول': st.column_config.NumberColumn(format="%d يوم"), - 'الحالة': st.column_config.TextColumn() - }, - hide_index=True, - use_container_width=True - ) - - # عرض تفاصيل المخاطر - st.subheader("تفاصيل المخاطر") - - # اختيار مخاطرة لعرض تفاصيلها - selected_risk_id = st.selectbox( - "اختر مخاطرة لعرض التفاصيل", - options=[risk['id'] for risk in project_risks], - format_func=lambda x: next((risk['name'] for risk in project_risks if risk['id'] == x), ""), - key="selected_risk_id" + early_factor = st.slider( + "معامل البنود المبكرة", + min_value=1.0, + max_value=1.5, + value=st.session_state.unbalanced_pricing_factors['early_items_factor'], + step=0.05, + format="%.2f", + key="early_factor_slider" ) - # عرض تفاصيل المخاطرة المختارة - if selected_risk_id: - selected_risk = next((risk for risk in project_risks if risk['id'] == selected_risk_id), None) + if early_factor != st.session_state.unbalanced_pricing_factors['early_items_factor']: + st.session_state.unbalanced_pricing_factors['early_items_factor'] = early_factor - if selected_risk: - col1, col2 = st.columns(2) - - with col1: - st.markdown(f"**اسم المخاطرة:** {selected_risk['name']}") - st.markdown(f"**الفئة:** {selected_risk['category']}") - st.markdown(f"**الاحتمالية:** {selected_risk['probability']}") - st.markdown(f"**التأثير:** {selected_risk['impact']}") - st.markdown(f"**درجة المخاطرة:** {selected_risk['risk_score']}") - - with col2: - st.markdown(f"**التأثير المالي:** {selected_risk['cost_impact']:,.2f} ريال") - st.markdown(f"**التأثير على الجدول الزمني:** {selected_risk['schedule_impact']} يوم") - st.markdown(f"**الحالة:** {selected_risk['status']}") - st.markdown(f"**تاريخ التحديث:** {selected_risk.get('updated_at', '-')}") - - st.markdown(f"**الوصف:** {selected_risk.get('description', '-')}") - st.markdown(f"**خطة التخفيف:** {selected_risk.get('mitigation_plan', '-')}") - st.markdown(f"**خطة الطوارئ:** {selected_risk.get('contingency_plan', '-')}") - - # أزرار تعديل وحذف المخاطرة - col1, col2 = st.columns(2) - - with col1: - if st.button("تعديل المخاطرة", key="edit_risk_btn"): - st.session_state.edit_risk_id = selected_risk_id - st.rerun() - - with col2: - if st.button("حذف المخاطرة", key="delete_risk_btn"): - # حذف المخاطرة - self.storage.delete_risk(selected_risk_id) - - # تحديث حالة الجلسة - st.session_state.risks = [r for r in st.session_state.risks if r['id'] != selected_risk_id] - - # إعادة حساب ملخص المخاطر - self._calculate_risk_summary(project_info['id'], total_cost) - - st.success(f"تم حذف المخاطرة '{selected_risk['name']}' بنجاح") - st.rerun() + with col2: + late_factor = st.slider( + "معامل البنود المتأخرة", + min_value=0.5, + max_value=1.0, + value=st.session_state.unbalanced_pricing_factors['late_items_factor'], + step=0.05, + format="%.2f", + key="late_factor_slider" + ) - # نموذج تعديل المخاطرة - if 'edit_risk_id' in st.session_state: - edit_risk_id = st.session_state.edit_risk_id - edit_risk = next((risk for risk in project_risks if risk['id'] == edit_risk_id), None) + if late_factor != st.session_state.unbalanced_pricing_factors['late_items_factor']: + st.session_state.unbalanced_pricing_factors['late_items_factor'] = late_factor - if edit_risk: - st.subheader(f"تعديل المخاطرة: {edit_risk['name']}") - - with st.form(key="edit_risk_form"): - # حقول النموذج - risk_name = st.text_input("اسم المخاطرة", value=edit_risk['name']) - risk_description = st.text_area("وصف المخاطرة", value=edit_risk.get('description', '')) - - col1, col2 = st.columns(2) - - with col1: - risk_category = st.selectbox( - "فئة المخاطرة", - options=["مالية", "فنية", "تعاقدية", "تنظيمية", "بيئية", "أمنية", "أخرى"], - index=["مالية", "فنية", "تعاقدية", "تنظيمية", "بيئية", "أمنية", "أخرى"].index(edit_risk['category']) if edit_risk['category'] in ["مالية", "فنية", "تعاقدية", "تنظيمية", "بيئية", "أمنية", "أخرى"] else 0 - ) - - risk_probability = st.slider( - "احتمالية حدوث المخاطرة", - min_value=1, - max_value=5, - value=edit_risk['probability'], - help="1: ضئيلة جداً، 5: شبه مؤكدة" - ) - - risk_impact = st.slider( - "تأثير المخاطرة", - min_value=1, - max_value=5, - value=edit_risk['impact'], - help="1: ضئيل جداً، 5: كارثي" - ) - - with col2: - risk_cost_impact = st.number_input( - "التأثير المالي (ريال)", - min_value=0.0, - value=float(edit_risk['cost_impact']), - step=1000.0 - ) - - risk_schedule_impact = st.number_input( - "التأثير على الجدول الزمني (أيام)", - min_value=0, - value=edit_risk['schedule_impact'], - step=1 - ) - - risk_status = st.selectbox( - "حالة المخاطرة", - options=["نشطة", "مغلقة", "تم التخفيف", "حدثت"], - index=["نشطة", "مغلقة", "تم التخفيف", "حدثت"].index(edit_risk['status']) if edit_risk['status'] in ["نشطة", "مغلقة", "تم التخفيف", "حدثت"] else 0 - ) - - risk_mitigation_plan = st.text_area("خطة التخفيف", value=edit_risk.get('mitigation_plan', '')) - risk_contingency_plan = st.text_area("خطة الطوارئ", value=edit_risk.get('contingency_plan', '')) - - # حساب درجة المخاطرة - risk_score = risk_probability * risk_impact - st.info(f"درجة المخاطرة: {risk_score}") - - # أزرار الإجراءات - col1, col2 = st.columns(2) - - with col1: - cancel_button = st.form_submit_button("إلغاء") - - with col2: - submit_button = st.form_submit_button("حفظ التغييرات") - - if cancel_button: - # إلغاء التعديل - del st.session_state.edit_risk_id - st.rerun() - - if submit_button: - # التحقق من صحة البيانات - if not risk_name: - st.error("يجب إدخال اسم المخاطرة") - else: - # تحديث المخاطرة - updated_risk = { - 'name': risk_name, - 'description': risk_description, - 'category': risk_category, - 'probability': risk_probability, - 'impact': risk_impact, - 'risk_score': risk_score, - 'cost_impact': risk_cost_impact, - 'schedule_impact': risk_schedule_impact, - 'status': risk_status, - 'mitigation_plan': risk_mitigation_plan, - 'contingency_plan': risk_contingency_plan - } - - # تحديث المخاطرة في التخزين - self.storage.update_risk(edit_risk_id, updated_risk) - - # تحديث حالة الجلسة - for i, r in enumerate(st.session_state.risks): - if r['id'] == edit_risk_id: - st.session_state.risks[i].update(updated_risk) - break - - # إعادة حساب ملخص المخاطر - self._calculate_risk_summary(project_info['id'], total_cost) - - # إزالة معرف التعديل - del st.session_state.edit_risk_id - - st.success(f"تم تحديث المخاطرة '{risk_name}' بنجاح") - st.rerun() - else: - st.info("لا توجد مخاطر مسجلة للمشروع. يرجى إضافة مخاطر باستخدام النموذج أدناه.") - - with tabs[2]: - # نموذج إضافة مخاطرة جديدة - st.subheader("إضافة مخاطرة جديدة") + # عرض جدول الأسعار المعدلة + st.write("### الأسعار المعدلة باستخدام التسعير الغير متزن") - with st.form(key="add_risk_form"): - # حقول النموذج - risk_name = st.text_input("اسم المخاطرة") - risk_description = st.text_area("وصف المخاطرة") - - col1, col2 = st.columns(2) - - with col1: - risk_category = st.selectbox( - "فئة المخاطرة", - options=["مالية", "فنية", "تعاقدية", "تنظيمية", "بيئية", "أمنية", "أخرى"] - ) - - risk_probability = st.slider( - "احتمالية حدوث المخاطرة", - min_value=1, - max_value=5, - value=3, - help="1: ضئيلة جداً، 5: شبه مؤكدة" - ) - - risk_impact = st.slider( - "تأثير المخاطرة", - min_value=1, - max_value=5, - value=3, - help="1: ضئيل جداً، 5: كارثي" - ) + # إنشاء نسخة من بنود المشروع + unbalanced_items = [] + + # تقسيم البنود إلى مبكرة ومتأخرة + num_items = len(project_items) + early_items_count = num_items // 3 + late_items_count = num_items // 3 + + for i, item in enumerate(project_items): + unbalanced_item = item.copy() - with col2: - risk_cost_impact = st.number_input( - "التأثير المالي (ريال)", - min_value=0.0, - value=0.0, - step=1000.0 - ) - - risk_schedule_impact = st.number_input( - "التأثير على الجدول الزمني (أيام)", - min_value=0, - value=0, - step=1 - ) + if i < early_items_count: + # بند مبكر + factor = st.session_state.unbalanced_pricing_factors['early_items_factor'] + unbalanced_item['unbalanced_unit_price'] = item['unit_price'] * factor + unbalanced_item['unbalanced_total_price'] = unbalanced_item['unbalanced_unit_price'] * item['quantity'] + elif i >= num_items - late_items_count: + # بند متأخر + factor = st.session_state.unbalanced_pricing_factors['late_items_factor'] + unbalanced_item['unbalanced_unit_price'] = item['unit_price'] * factor + unbalanced_item['unbalanced_total_price'] = unbalanced_item['unbalanced_unit_price'] * item['quantity'] + else: + # بند وسطي + unbalanced_item['unbalanced_unit_price'] = item['unit_price'] + unbalanced_item['unbalanced_total_price'] = item['total_price'] - risk_status = st.selectbox( - "حالة المخاطرة", - options=["نشطة", "مغلقة", "تم التخفيف", "حدثت"], - index=0 - ) - - risk_mitigation_plan = st.text_area("خطة التخفيف") - risk_contingency_plan = st.text_area("خطة الطوارئ") - - # حساب درجة المخاطرة - risk_score = risk_probability * risk_impact - st.info(f"درجة المخاطرة: {risk_score}") + unbalanced_items.append(unbalanced_item) - # زر الإرسال - submit_button = st.form_submit_button("إضافة المخاطرة") - - if submit_button: - # التحقق من صحة البيانات - if not risk_name: - st.error("يجب إدخال اسم المخاطرة") - else: - # إنشاء معرف فريد - risk_id = f"risk_{len(st.session_state.risks) + 1}" - - # إنشاء كائن المخاطرة - new_risk = { - 'id': risk_id, - 'project_id': project_info['id'], - 'name': risk_name, - 'description': risk_description, - 'category': risk_category, - 'probability': risk_probability, - 'impact': risk_impact, - 'risk_score': risk_score, - 'cost_impact': risk_cost_impact, - 'schedule_impact': risk_schedule_impact, - 'status': risk_status, - 'mitigation_plan': risk_mitigation_plan, - 'contingency_plan': risk_contingency_plan - } - - # إضافة المخاطرة إلى التخزين - added_risk = self.storage.add_risk(new_risk) - - # تحديث حالة الجلسة - st.session_state.risks.append(added_risk) - - # إعادة حساب ملخص المخاطر - self._calculate_risk_summary(project_info['id'], total_cost) - - st.success(f"تم إضافة المخاطرة '{risk_name}' بنجاح") - st.rerun() - - with tabs[3]: - # تقرير المخاطر - st.subheader("تقرير المخاطر") + # إنشاء DataFrame من البنود المعدلة + df = pd.DataFrame(unbalanced_items) + df = df[['code', 'description', 'unit', 'quantity', 'unit_price', 'unbalanced_unit_price', 'total_price', 'unbalanced_total_price']] + df.columns = ['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة الأصلي', 'سعر الوحدة المعدل', 'السعر الإجمالي الأصلي', 'السعر الإجمالي المعدل'] + + # تنسيق الأسعار + df['سعر الوحدة الأصلي'] = df['سعر الوحدة الأصلي'].apply(lambda x: f"{x:,.2f} ريال") + df['سعر الوحدة المعدل'] = df['سعر الوحدة المعدل'].apply(lambda x: f"{x:,.2f} ريال") + df['السعر الإجمالي الأصلي'] = df['السعر الإجمالي الأصلي'].apply(lambda x: f"{x:,.2f} ريال") + df['السعر الإجمالي المعدل'] = df['السعر الإجمالي المعدل'].apply(lambda x: f"{x:,.2f} ريال") + + # عرض الجدول + st.dataframe(df, use_container_width=True) + + # حساب إجمالي الأسعار + total_original = sum(item['total_price'] for item in project_items) + total_unbalanced = sum(item['unbalanced_total_price'] for item in unbalanced_items) - if project_risks: - # عرض ملخص التقرير - risk_summary = project_info.get('risk_summary', self._calculate_risk_summary(project_info['id'], total_cost)) + col1, col2 = st.columns(2) + with col1: + st.metric("إجمالي السعر الأصلي", f"{total_original:,.2f} ريال") + with col2: + st.metric("إجمالي السعر المعدل", f"{total_unbalanced:,.2f} ريال") - st.markdown(f""" - ### ملخص تقرير المخاطر لمشروع: {project_info['name']} + # زر تطبيق التسعير الغير متزن + if st.button("تطبيق التسعير الغير متزن", key="apply_unbalanced_pricing"): + for i, item in enumerate(unbalanced_items): + for j, boq_item in enumerate(st.session_state.boq_items): + if boq_item['id'] == item['id']: + st.session_state.boq_items[j]['unit_price'] = item['unbalanced_unit_price'] + st.session_state.boq_items[j]['total_price'] = item['unbalanced_total_price'] + break + + st.success("تم تطبيق التسعير الغير متزن بنجاح") + st.rerun() + + def _render_export_save_buttons(self, project_info): + """عرض أزرار التصدير والحفظ""" + st.subheader("تصدير وحفظ التسعير") + + col1, col2 = st.columns(2) + with col1: + if st.button("تصدير التسعير كملف Excel", key="export_pricing_excel"): + st.info("سيتم تنفيذ هذه الميزة في الإصدار القادم") - - **إجمالي عدد المخاطر:** {risk_summary['total_risks']} - - **المخاطر العالية:** {risk_summary['high_risks']} - - **المخاطر المتوسطة:** {risk_summary['medium_risks']} - - **المخاطر المنخفضة:** {risk_summary['low_risks']} - - **إجمالي التأثير المالي:** {risk_summary['total_cost_impact']:,.2f} ريال - - **إجمالي التأثير على الجدول الزمني:** {risk_summary['total_schedule_impact']} يوم - - **احتياطي المخاطر المقترح:** {risk_summary['risk_contingency']:,.2f} ريال ({risk_summary['risk_contingency_percentage']:.1f}% من التكلفة الإجمالية) - """) + with col2: + if st.button("حفظ التسعير", key="save_pricing"): + # حفظ التسعير الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] - # عرض أعلى المخاطر - st.subheader("أعلى المخاطر") + saved_pricing = { + 'project_id': project_info['id'], + 'project_name': project_info['name'], + 'saved_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + 'items': project_items + } - # ترتيب المخاطر حسب درجة المخاطرة - sorted_risks = sorted(project_risks, key=lambda x: x['risk_score'], reverse=True) + st.session_state.saved_pricing.append(saved_pricing) - for i, risk in enumerate(sorted_risks[:5]): # عرض أعلى 5 مخاطر - risk_color = "🔴" if risk['risk_score'] >= 15 else "🟠" if risk['risk_score'] >= 8 else "🟢" - st.markdown(f""" - {risk_color} **{risk['name']}** (درجة المخاطرة: {risk['risk_score']}) - - **الفئة:** {risk['category']} - - **الاحتمالية:** {risk['probability']}/5, **التأثير:** {risk['impact']}/5 - - **التأثير المالي:** {risk['cost_impact']:,.2f} ريال - - **خطة التخفيف:** {risk.get('mitigation_plan', '-')} - """) + # تحديث حالة المشروع + for i, p in enumerate(st.session_state.projects): + if p['id'] == project_info['id']: + st.session_state.projects[i]['status'] = 'تم التسعير' + break + + st.success("تم حفظ التسعير بنجاح") - # زر تصدير التقرير - if st.button("تصدير تقرير المخاطر", key="export_risk_report_btn", type="primary"): - # إنشاء تقرير المخاطر - risk_report_file = export_risk_report( - project_risks, - project_info, - total_cost, - f"/tmp/risk_report_{st.session_state.current_project}.xlsx" - ) - - # عرض رابط التنزيل - href = get_download_link( - risk_report_file, - "تحميل تقرير المخاطر", - "excel" - ) - st.markdown(href, unsafe_allow_html=True) - else: - st.info("لا توجد مخاطر مسجلة للمشروع. يرجى إضافة مخاطر لإنشاء التقرير.") - - # أزرار التنقل بين الخطوات - col1, col2 = st.columns(2) - with col1: - if st.button("العودة إلى المحتوى المحلي ➡️", key="back_to_local_content"): - st.session_state.pricing_step = "local_content" - st.rerun() - - with col2: - if st.button("إنهاء وعودة للصفحة الرئيسية", key="go_to_home", type="primary"): - st.session_state.pricing_step = "project_info" - st.rerun() - - st.markdown('
', unsafe_allow_html=True) - - def _calculate_risk_summary(self, project_id, total_cost): - """حساب ملخص المخاطر للمشروع""" - # الحصول على مخاطر المشروع - project_risks = [risk for risk in st.session_state.risks if risk['project_id'] == project_id] - - # حساب عدد المخاطر حسب الشدة - high_risks = sum(1 for risk in project_risks if risk['risk_score'] >= 15) - medium_risks = sum(1 for risk in project_risks if 8 <= risk['risk_score'] < 15) - low_risks = sum(1 for risk in project_risks if risk['risk_score'] < 8) - - # حساب إجمالي التأثير المالي والجدول الزمني - total_cost_impact = sum(risk['cost_impact'] for risk in project_risks) - total_schedule_impact = sum(risk['schedule_impact'] for risk in project_risks) - - # حساب احتياطي المخاطر المقترح - # استخدام صيغة بسيطة: مجموع (احتمالية * تأثير مالي) لكل المخاطر - risk_contingency = sum(risk['probability'] / 5 * risk['cost_impact'] for risk in project_risks) - - # حساب نسبة احتياطي المخاطر من التكلفة الإجمالية - risk_contingency_percentage = (risk_contingency / total_cost * 100) if total_cost > 0 else 0 - - # إنشاء ملخص - summary = { - 'total_risks': len(project_risks), - 'high_risks': high_risks, - 'medium_risks': medium_risks, - 'low_risks': low_risks, - 'total_cost_impact': total_cost_impact, - 'total_schedule_impact': total_schedule_impact, - 'risk_contingency': risk_contingency, - 'risk_contingency_percentage': risk_contingency_percentage - } - - # تحديث ملخص المخاطر في المشروع - self.storage.update_project(project_id, {'risk_summary': summary}) - - # تحديث حالة الجلسة - for i, p in enumerate(st.session_state.projects): - if p['id'] == project_id: - st.session_state.projects[i]['risk_summary'] = summary - break - - return summary - - def _create_risk_matrix(self, risks): - """إنشاء مصفوفة المخاطر""" - # إنشاء مصفوفة ف��رغة - matrix = { - "": ["5 - شبه مؤكدة", "4 - محتملة", "3 - ممكنة", "2 - مستبعدة", "1 - نادرة"], - "1": ["", "", "", "", ""], - "2": ["", "", "", "", ""], - "3": ["", "", "", "", ""], - "4": ["", "", "", "", ""], - "5": ["", "", "", "", ""] - } - - # تجميع المخاطر حسب الاحتمالية والتأثير - risk_counts = {} - for risk in risks: - probability = risk['probability'] - impact = risk['impact'] - key = f"{probability}_{impact}" - - if key in risk_counts: - risk_counts[key] += 1 - else: - risk_counts[key] = 1 - - # ملء المصفوفة - for key, count in risk_counts.items(): - probability, impact = map(int, key.split('_')) - row_index = 5 - probability # عكس الترتيب (5 في الأعلى) - col_key = str(impact) - - # تحديد لون الخلية بناءً على درجة المخاطرة - risk_score = probability * impact - if risk_score >= 15: - cell_value = f"🔴 {count}" # أحمر للمخاطر العالية - elif risk_score >= 8: - cell_value = f"🟠 {count}" # برتقالي للمخاطر المتوسطة - else: - cell_value = f"🟢 {count}" # أخضر للمخاطر المنخفضة - - matrix[col_key][row_index] = cell_value - - # تحويل إلى DataFrame - return pd.DataFrame(matrix) - # تشغيل التطبيق if __name__ == "__main__": app = PricingApp()