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,803 +1,472 @@ import streamlit as st import pandas as pd import numpy as np +import random 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 random import json import os +from datetime import datetime class PricingApp: - """وحدة التسعير المتكاملة""" - def __init__(self): - """تهيئة وحدة التسعير""" - # تهيئة البيانات في حالة الجلسة إذا لم تكن موجودة + """تهيئة التطبيق""" + # تعيين عنوان الصفحة + st.set_page_config(page_title="وحدة التسعير المتكاملة", layout="wide") + + # إضافة CSS للتنسيق + self._add_custom_css() + + # تهيئة حالة الجلسة 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': 'غير متزن' - } - ] + st.session_state.projects = self._load_sample_projects() + + if 'current_project_id' not in st.session_state: + st.session_state.current_project_id = None + + if 'current_tab' not in st.session_state: + st.session_state.current_tab = "boq" - 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() - + + def _add_custom_css(self): + """إضافة CSS مخصص للتطبيق""" + st.markdown(""" + + """, unsafe_allow_html=True) + + def _update_total_price(self): + """تحديث السعر الإجمالي تلقائياً عند تغيير الكمية أو سعر الوحدة""" + # سيتم استدعاء هذه الدالة عند تغيير قيمة الكمية أو سعر الوحدة + # يتم تحديث البيانات في الجدول وإعادة حساب المجموع الكلي + st.session_state.item_analysis_edited = True + def run(self): - """تشغيل وحدة التسعير""" + """تشغيل التطبيق""" st.title("وحدة التسعير المتكاملة") - # عرض زر إنشاء تسعير جديد - 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 - - # عرض نموذج إنشاء تسعير جديد - 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() - + # زر إنشاء تسعير جديد + if st.button("➕ إنشاء تسعير جديد", type="primary"): + self._create_new_pricing() + # عرض قائمة المشاريع 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) - - # عرض علامات التبويب - tab1, tab2, tab3, tab4, tab5 = st.tabs([ - "جدول الكميات", - "تحليل سعر البند", - "تحليل التكلفة", - "تحليل الربحية", - "استراتيجيات التسعير" - ]) - - with tab1: - self._render_bill_of_quantities() - - with tab2: - self._render_item_price_analysis() - - with tab3: - self._render_cost_analysis(project_info) - - with tab4: - self._render_profit_margin(project_info) - - with tab5: - self._render_pricing_strategies(project_info) - - # عرض أزرار التصدير والحفظ - 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" - ) - - 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() - - 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['estimated_value']), - format="%f", - key="edit_project_value" - ) - deadline = st.date_input( - "الموعد النهائي", - value=datetime.strptime(project['deadline'], "%Y-%m-%d").date(), - key="edit_project_deadline" - ) - status = st.selectbox( - "الحالة", - ["قيد التسعير", "تم التسعير", "تم التقديم", "فائز", "خاسر"], - index=["قيد التسعير", "تم التسعير", "تم التقديم", "فائز", "خاسر"].index(project['status']), - key="edit_project_status" - ) - pricing_type = st.selectbox( - "نوع التسعير", - ["قياسي", "غير متزن", "موجه ربحية", "تنافسي", "استراتيجي"], - index=["قياسي", "غير متزن", "موجه ربحية", "تنافسي", "استراتيجي"].index(project.get('pricing_type', 'قياسي')), - key="edit_project_pricing_type" - ) - - 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.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() + # إذا تم اختيار مشروع، عرض تفاصيله + if st.session_state.current_project_id is not None: + self._render_project_details() + # عرض التبويبات + self._render_tabs() + + def _load_sample_projects(self): + """تحميل بيانات المشاريع النموذجية""" + return [ + { + 'id': 1, + 'name': 'مشروع تطوير الطرق الداخلية', + 'client': 'وزارة النقل', + 'estimated_value': 5000000.0, + 'deadline': '2024-06-30', + 'pricing_type': 'قياسي', + 'boq': [ + { + 'id': 'A-001', + 'code': 'A-001', + 'description': 'أعمال الحفر والردم', + 'unit': 'م3', + 'quantity': 1500.0, + 'unit_price': 45.0, + 'total_price': 67500.0 + }, + { + 'id': 'A-002', + 'code': 'A-002', + 'description': 'توريد وتركيب بردورات خرسانية', + 'unit': 'م.ط', + 'quantity': 2500.0, + 'unit_price': 85.0, + 'total_price': 212500.0 + }, + { + 'id': 'A-003', + 'code': 'A-003', + 'description': 'أعمال الأسفلت', + 'unit': 'م2', + 'quantity': 10000.0, + 'unit_price': 120.0, + 'total_price': 1200000.0 + } + ] + }, + { + 'id': 2, + 'name': 'مشروع إنشاء مبنى إداري', + 'client': 'شركة التطوير العقاري', + 'estimated_value': 12000000.0, + 'deadline': '2024-08-15', + 'pricing_type': 'قياسي', + 'boq': [ + { + 'id': 'B-001', + 'code': 'B-001', + 'description': 'أعمال الخرسانة المسلحة للأساسات', + 'unit': 'م3', + 'quantity': 750.0, + 'unit_price': 1200.0, + 'total_price': 900000.0 + }, + { + 'id': 'B-002', + 'code': 'B-002', + 'description': 'أعمال البناء بالطوب', + 'unit': 'م2', + 'quantity': 3500.0, + 'unit_price': 180.0, + 'total_price': 630000.0 + }, + { + 'id': 'B-003', + 'code': 'B-003', + 'description': 'أعمال التشطيبات', + 'unit': 'م2', + 'quantity': 5000.0, + 'unit_price': 850.0, + 'total_price': 4250000.0 + } + ] + } + ] + + def _create_new_pricing(self): + """إنشاء تسعير جديد""" + # إنشاء مشروع جديد بقيم افتراضية + new_project = { + 'id': len(st.session_state.projects) + 1, + 'name': f'مشروع جديد {len(st.session_state.projects) + 1}', + 'client': 'عميل جديد', + 'estimated_value': 0.0, + 'deadline': datetime.now().strftime('%Y-%m-%d'), + 'pricing_type': 'قياسي', + 'boq': [] + } + + # إضافة المشروع الجديد إلى قائمة المشاريع + st.session_state.projects.append(new_project) + + # تعيين المشروع الجديد كمشروع حالي + st.session_state.current_project_id = new_project['id'] + + # إعادة تحميل الصفحة + st.rerun() + def _render_projects_list(self): """عرض قائمة المشاريع""" - st.subheader("قائمة المشاريع") + st.header("قائمة المشاريع") - if not st.session_state.projects: - st.info("لا توجد مشاريع. قم بإنشاء مشروع جديد للبدء.") - return - # إنشاء 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 = ['الرقم', 'اسم المشروع', 'العميل', 'القيمة التقديرية', 'الموعد النهائي', 'الحالة', 'نوع التسعير'] - - # تنسيق القيمة التقديرية - df['القيمة التقديرية'] = df['القيمة التقديرية'].apply(lambda x: f"{x:,.2f} ريال") - - # عرض الجدول - st.dataframe(df, use_container_width=True) - - # اختيار المشروع - 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 - - 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 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("لا توجد مشاريع. قم بإنشاء مشروع جديد للبدء.") - - 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 - - def _render_project_info(self, project): - """عرض معلومات المشروع""" - st.header(f"تسعير: {project['name']}") - - col1, col2, col3, col4 = st.columns(4) + projects_data = [] + for i, project in enumerate(st.session_state.projects): + projects_data.append({ + 'الرقم': i, + 'رقم المشروع': project['id'], + 'اسم المشروع': project['name'], + 'العميل': project['client'], + 'القيمة التقديرية': f"{project['estimated_value']:,.2f} ريال", + 'الموعد النهائي': project['deadline'], + 'نوع التسعير': project['pricing_type'] + }) + + df = pd.DataFrame(projects_data) + + # عرض الجدول + st.dataframe(df, use_container_width=True) + + # اختيار المشروع + col1, col2 = st.columns([3, 1]) with col1: - st.metric("العميل", project['client']) - with col2: - st.metric("القيمة التقديرية", f"{project['estimated_value']:,.2f} ريال") - with col3: - st.metric("الموعد النهائي", project['deadline']) - with col4: - st.metric("نوع التسعير", project['pricing_type']) + project_options = [p['name'] for p in st.session_state.projects] + selected_project = st.selectbox("اختر المشروع", project_options, index=0 if project_options else None, key="project_selector") - def _render_bill_of_quantities(self): - """عرض جدول الكميات""" - st.subheader("جدول الكميات") + if selected_project: + selected_project_id = next((p['id'] for p in st.session_state.projects if p['name'] == selected_project), None) + st.session_state.current_project_id = selected_project_id - # زر إضافة بند جديد - 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 st.button("تعديل المشروع"): + # هنا يمكن إضافة منطق تعديل المشروع + pass + + def _render_project_details(self): + """عرض تفاصيل المشروع الحالي""" + # الحصول على المشروع الحالي + current_project = next((p for p in st.session_state.projects if p['id'] == st.session_state.current_project_id), None) - if not project_items: - st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") - return + if current_project: + st.header(f"تسعير: {current_project['name']}") - # إنشاء DataFrame من بنود المشروع - df = pd.DataFrame(project_items) - df = df[['id', 'code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'resource_type']] + # عرض تفاصيل المشروع + col1, col2, col3, col4 = st.columns(4) + with col1: + st.info(f"العميل: {current_project['client']}") + with col2: + st.info(f"القيمة التقديرية: {current_project['estimated_value']:,.2f} ريال") + with col3: + st.info(f"الموعد النهائي: {current_project['deadline']}") + with col4: + st.info(f"نوع التسعير: {current_project['pricing_type']}") + + def _render_tabs(self): + """عرض التب��يبات""" + # إنشاء التبويبات + tabs = ["جدول الكميات", "تحليل سعر البند", "تحليل التكلفة", "تحليل الربحية", "استراتيجيات التسعير"] + + # عرض أزرار التبويبات + cols = st.columns(len(tabs)) + for i, tab in enumerate(tabs): + with cols[i]: + if st.button(tab, key=f"tab_{i}"): + st.session_state.current_tab = tab.lower() + st.rerun() - # تحويل الجدول إلى جدول قابل للتعديل - 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" - ) + # عرض محتوى التبويب الحالي + if st.session_state.current_tab == "جدول الكميات": + self._render_boq() + elif st.session_state.current_tab == "تحليل سعر البند": + self._render_item_price_analysis() + elif st.session_state.current_tab == "تحليل التكلفة": + self._render_cost_analysis() + elif st.session_state.current_tab == "تحليل الربحية": + self._render_profit_analysis() + elif st.session_state.current_tab == "استراتيجيات التسعير": + self._render_pricing_strategies() + + def _render_boq(self): + """عرض جدول الكميات""" + st.header("جدول الكميات") - # تحديث البيانات في حالة التعديل - 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() + # الحصول على المشروع الحالي + current_project = next((p for p in st.session_state.projects if p['id'] == st.session_state.current_project_id), None) - # حساب المجموع الكلي - total_price = sum(item['total_price'] for item in project_items) - st.metric("إجمالي جدول الكميات", f"{total_price:,.2f} ريال") + if not current_project: + st.warning("الرجاء اختيار مشروع أولاً.") + return - # أزرار التصدير والتعديل + # أزرار إضافة بند جديد وسحب من الموارد col1, col2 = st.columns(2) with col1: - 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 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("إضافة بند جديد") - - 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: + if st.button("➕ إضافة بند جديد", key="add_boq_item"): + # إنشاء بند جديد بقيم افتراضية 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 + 'id': f"ITEM-{len(current_project['boq']) + 1}", + 'code': f"ITEM-{len(current_project['boq']) + 1}", + 'description': 'بند جديد', + 'unit': 'وحدة', + 'quantity': 0.0, + 'unit_price': 0.0, + 'total_price': 0.0 } - 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 + # إضافة البند الجديد إلى جدول الكميات + current_project['boq'].append(new_item) 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']}") - 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: - 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 resource_type != st.session_state.selected_resource_type: - st.session_state.selected_resource_type = resource_type - st.rerun() - - # الحصول على الموارد المناسبة - 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 - - 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 - - # إنشاء 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 = ['الرقم', 'الاسم', 'الفئة', 'الوحدة', 'السعر'] - - # تنسيق السعر - df['السعر'] = df['السعر'].apply(lambda x: f"{x:,.2f} ريال") + with col2: + if st.button("📋 سحب من الموارد", key="import_from_resources"): + # هنا يمكن إضافة منطق سحب البنود من الموارد + pass + + # إنشاء DataFrame من جدول الكميات + if current_project['boq']: + boq_data = [] + for i, item in enumerate(current_project['boq']): + boq_data.append({ + 'الرقم': i, + 'رقم البند': item['code'], + 'الوصف': item['description'], + 'الوحدة': item['unit'], + 'الكمية': item['quantity'], + 'سعر الوحدة': item['unit_price'], + 'السعر الإجمالي': item['total_price'], + 'الإجراءات': f' ' + }) - # عرض الجدول - st.dataframe(df, use_container_width=True) + df = pd.DataFrame(boq_data) + + # إضافة صف المجموع + total_price = sum(item['total_price'] for item in current_project['boq']) + total_row = pd.DataFrame([{ + 'الرقم': '', + 'رقم البند': '', + 'الوصف': 'المجموع', + 'الوحدة': '', + 'الكمية': '', + 'سعر الوحدة': '', + 'السعر الإجمالي': total_price, + 'الإجراءات': '' + }]) + + # دمج الجدول مع صف المجموع + df = pd.concat([df, total_row], ignore_index=True) + + # تحويل الجدول إلى جدول قابل للتعديل + edited_df = st.data_editor( + df, + use_container_width=True, + key="edit_boq_table", + column_config={ + "الرقم": st.column_config.Column( + "الرقم", + width="small", + ), + "رقم البند": st.column_config.Column( + "رقم البند", + ), + "الوصف": st.column_config.Column( + "الوصف", + ), + "الوحدة": st.column_config.Column( + "الوحدة", + width="small", + ), + "الكمية": 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 ريال", + ), + "الإجراءات": st.column_config.Column( + "الإجراءات", + width="small", + ), + }, + hide_index=True, + disabled=["الرقم", "السعر الإجمالي", "الإجراءات"], + ) - 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" - ) + # تحديث البيانات في المشروع بناءً على التعديلات + if edited_df is not None and not edited_df.equals(df): + # تحديث بنود جدول الكميات + for i in range(len(edited_df) - 1): # استثناء صف المجموع + row = edited_df.iloc[i] + item_code = row['رقم البند'] - 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() + # البحث عن البند في جدول الكميات + for item in current_project['boq']: + if item['code'] == item_code: + # تحديث البند + item['description'] = row['الوصف'] + item['unit'] = row['الوحدة'] + item['quantity'] = row['الكمية'] + item['unit_price'] = row['سعر الوحدة'] + item['total_price'] = row['الكمية'] * row['سعر الوحدة'] + break - if cancel_button: - st.session_state.show_resource_selector = False st.rerun() else: - st.error("تنسيق بيانات الموارد غير صحيح. يرجى التأكد من وجود الحقول المطلوبة.") - - if st.button("إلغاء", key="cancel_resource_selector_error"): - st.session_state.show_resource_selector = False - st.rerun() - + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود جديدة.") + def _render_item_price_analysis(self): - """عرض تحليل سعر البند مع إمكانية التعديل والحفظ""" - st.subheader("تحليل سعر البند") + """عرض تحليل سعر البند""" + st.header("تحليل سعر البند") - # الحصول على بنود المشروع الحالي - project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + # الحصول على المشروع الحالي + current_project = next((p for p in st.session_state.projects if p['id'] == st.session_state.current_project_id), None) - if not project_items: - st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") + if not current_project or not current_project['boq']: + st.warning("الرجاء اختيار مشروع به بنود في جدول الكميات أولاً.") return - + # اختيار البند للتحليل - 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" - ) + item_options = [f"{item['description']}. الكود: {item['code']}" for item in current_project['boq']] + selected_item_option = st.selectbox("اختر البند للتحليل", item_options, key="item_selector") - # الحصول على البند المحدد - selected_item = next((item for item in project_items if item['id'] == selected_item_id), None) + if not selected_item_option: + st.warning("الرجاء اختيار بند للتحليل.") + return + + # استخراج كود البند من الخيار المحدد + selected_item_code = selected_item_option.split("الكود: ")[1] + + # البحث عن البند في جدول الكميات + selected_item = next((item for item in current_project['boq'] if item['code'] == selected_item_code), None) + selected_item_id = selected_item['id'] if not selected_item: st.error("لم يتم العثور على البند المحدد.") return - - # عرض معلومات البند + + # عرض تفاصيل البند col1, col2 = st.columns(2) with col1: st.write(f"**البند:** {selected_item['description']}") @@ -812,19 +481,19 @@ class PricingApp: item_analysis = self._generate_item_analysis(selected_item) # عرض تحليل المواد مع إمكانية التعديل - self._render_materials_analysis(item_analysis) + self._render_materials_analysis_enhanced(item_analysis) # عرض تحليل المعدات مع إمكانية التعديل - self._render_equipment_analysis(item_analysis) + self._render_equipment_analysis_enhanced(item_analysis) # عرض تحليل العمالة مع إمكانية التعديل - self._render_labor_analysis(item_analysis) + self._render_labor_analysis_enhanced(item_analysis) # عرض تحليل المقاولين من الباطن مع إمكانية التعديل - self._render_subcontractors_analysis(item_analysis) + self._render_subcontractors_analysis_enhanced(item_analysis) # عرض ملخص التكلفة - self._render_cost_summary(item_analysis) + self._render_cost_summary_enhanced(item_analysis) # زر حفظ التغييرات في جدول الكميات الرئيسي if st.button("حفظ جميع التغييرات في جدول الكميات", key="save_all_changes", type="primary"): @@ -833,7 +502,7 @@ class PricingApp: st.success("تم حفظ جميع التغييرات بنجاح في جدول الكميات الرئيسي") st.session_state.item_analysis_edited = False st.rerun() - + def _generate_item_analysis(self, item): """إنشاء تحليل سعر البند""" # في هذه النسخة التجريبية، سنقوم بإنشاء بيانات عشوائية للتحليل @@ -944,9 +613,9 @@ class PricingApp: 'profit': profit, 'unit_price': unit_price } - - def _render_materials_analysis(self, item_analysis): - """عرض تحليل المواد مع إمكانية التعديل""" + + def _render_materials_analysis_enhanced(self, item_analysis): + """عرض تحليل المواد مع إمكانية التعديل (نسخة محسنة)""" st.subheader("تحليل المواد") if not item_analysis['materials']: @@ -964,11 +633,14 @@ class PricingApp: st.session_state.item_analysis_edited = True st.rerun() return - + # إنشاء DataFrame من قائمة المواد df = pd.DataFrame(item_analysis['materials']) df.columns = ['المادة', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] + # إضافة عمود للإجراءات + df['الإجراءات'] = '✏️ 🗑️' + # تحويل الجدول إلى جدول قابل للتعديل edited_df = st.data_editor( df, @@ -982,20 +654,28 @@ class PricingApp: min_value=0.0, format="%.2f", step=0.1, + on_change=self._update_total_price, ), "سعر الوحدة": st.column_config.NumberColumn( "سعر الوحدة", min_value=0.0, format="%.2f ريال", step=0.1, + on_change=self._update_total_price, ), "السعر الإجمالي": st.column_config.NumberColumn( "السعر الإجمالي", format="%.2f ريال", disabled=True, ), + "الإجراءات": st.column_config.Column( + "الإجراءات", + width="small", + disabled=True, + ), }, - num_rows="dynamic" + num_rows="dynamic", + hide_index=True, ) # تحديث البيانات في item_analysis بناءً على التعديلات @@ -1024,13 +704,18 @@ class PricingApp: st.session_state.item_analysis_edited = True st.rerun() - # عرض إجمالي تكلفة المواد - st.metric("إجمالي تكلفة المواد", f"{item_analysis['total_materials_cost']:,.2f} ريال") + # عرض إجمالي تكلفة المواد بتنسيق محسن + st.markdown(f""" +
+
إجمالي تكلفة المواد:
+
{item_analysis['total_materials_cost']:,.2f} ريال
+
+ """, unsafe_allow_html=True) # أزرار التحكم col1, col2 = st.columns(2) with col1: - if st.button("إضافة مادة جديدة", key="add_material"): + if st.button("➕ إضافة مادة جديدة", key="add_material"): item_analysis['materials'].append({ 'name': 'مادة جديدة', 'unit': 'وحدة', @@ -1040,9 +725,9 @@ class PricingApp: }) st.session_state.item_analysis_edited = True st.rerun() - - def _render_equipment_analysis(self, item_analysis): - """عرض تحليل المعدات مع إمكانية التعديل""" + + def _render_equipment_analysis_enhanced(self, item_analysis): + """عرض تحليل المعدات مع إمكانية التعديل (نسخة محسنة)""" st.subheader("تحليل المعدات") if not item_analysis['equipment']: @@ -1060,11 +745,14 @@ class PricingApp: st.session_state.item_analysis_edited = True st.rerun() return - + # إنشاء DataFrame من قائمة المعدات df = pd.DataFrame(item_analysis['equipment']) df.columns = ['المعدة', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] + # إضافة عمود للإجراءات + df['الإجراءات'] = '✏️ 🗑️' + # تحويل الجدول إلى جدول قابل للتعديل edited_df = st.data_editor( df, @@ -1078,20 +766,28 @@ class PricingApp: min_value=0.0, format="%.2f", step=0.1, + on_change=self._update_total_price, ), "سعر الوحدة": st.column_config.NumberColumn( "سعر الوحدة", min_value=0.0, format="%.2f ريال", step=0.1, + on_change=self._update_total_price, ), "السعر الإجمالي": st.column_config.NumberColumn( "السعر الإجمالي", format="%.2f ريال", disabled=True, ), + "الإجراءات": st.column_config.Column( + "الإجراءات", + width="small", + disabled=True, + ), }, - num_rows="dynamic" + num_rows="dynamic", + hide_index=True, ) # تحديث البيانات في item_analysis بناءً على التعديلات @@ -1120,13 +816,18 @@ class PricingApp: st.session_state.item_analysis_edited = True st.rerun() - # عرض إجمالي تكلفة المعدات - st.metric("إجمالي تكلفة المعدات", f"{item_analysis['total_equipment_cost']:,.2f} ريال") + # عرض إجمالي تكلفة المعدات بتنسيق محسن + st.markdown(f""" +
+
إجمالي تكلفة المعدات:
+
{item_analysis['total_equipment_cost']:,.2f} ريال
+
+ """, unsafe_allow_html=True) # أزرار التحكم col1, col2 = st.columns(2) with col1: - if st.button("إضافة معدة جديدة", key="add_equipment"): + if st.button("➕ إضافة معدة جديدة", key="add_equipment"): item_analysis['equipment'].append({ 'name': 'معدة جديدة', 'unit': 'يوم', @@ -1136,9 +837,9 @@ class PricingApp: }) st.session_state.item_analysis_edited = True st.rerun() - - def _render_labor_analysis(self, item_analysis): - """عرض تحليل العمالة مع إمكانية التعديل""" + + def _render_labor_analysis_enhanced(self, item_analysis): + """عرض تحليل العمالة مع إمكانية التعديل (نسخة محسنة)""" st.subheader("تحليل العمالة") if not item_analysis['labor']: @@ -1156,11 +857,14 @@ class PricingApp: st.session_state.item_analysis_edited = True st.rerun() return - + # إنشاء DataFrame من قائمة العمالة df = pd.DataFrame(item_analysis['labor']) df.columns = ['العامل', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] + # إضافة عمود للإجراءات + df['الإجراءات'] = '✏️ 🗑️' + # تحويل الجدول إلى جدول قابل للتعديل edited_df = st.data_editor( df, @@ -1174,20 +878,28 @@ class PricingApp: min_value=0.0, format="%.2f", step=0.1, + on_change=self._update_total_price, ), "سعر الوحدة": st.column_config.NumberColumn( "سعر الوحدة", min_value=0.0, format="%.2f ريال", step=0.1, + on_change=self._update_total_price, ), "السعر الإجمالي": st.column_config.NumberColumn( "السعر الإجمالي", format="%.2f ريال", disabled=True, ), + "الإجراءات": st.column_config.Column( + "الإجراءات", + width="small", + disabled=True, + ), }, - num_rows="dynamic" + num_rows="dynamic", + hide_index=True, ) # تحديث البيانات في item_analysis بناءً على التعديلات @@ -1216,13 +928,18 @@ class PricingApp: st.session_state.item_analysis_edited = True st.rerun() - # عرض إجمالي تكلفة العمالة - st.metric("إجمالي تكلفة العمالة", f"{item_analysis['total_labor_cost']:,.2f} ريال") + # عرض إجمالي تكلفة العمالة بتنسيق محسن + st.markdown(f""" +
+
إجمالي تكلفة العمالة:
+
{item_analysis['total_labor_cost']:,.2f} ريال
+
+ """, unsafe_allow_html=True) # أزرار التحكم col1, col2 = st.columns(2) with col1: - if st.button("إضافة عامل جديد", key="add_labor"): + if st.button("➕ إضافة عامل جديد", key="add_labor"): item_analysis['labor'].append({ 'name': 'عامل جديد', 'unit': 'يوم', @@ -1233,20 +950,15 @@ class PricingApp: st.session_state.item_analysis_edited = True st.rerun() - def _render_subcontractors_analysis(self, item_analysis): - """عرض تحليل المقاولين من الباطن مع إمكانية التعديل""" + def _render_subcontractors_analysis_enhanced(self, item_analysis): + """عرض تحليل المقاولين من الباطن مع إمكانية التعديل (نسخة محسنة)""" st.subheader("تحليل المقاولين من الباطن") - # التحقق من وجود مفتاح المقاولين من الباطن في التحليل - if 'subcontractors' not in item_analysis: - item_analysis['subcontractors'] = [] - item_analysis['total_subcontractors_cost'] = 0 - if not item_analysis['subcontractors']: st.info("لا يوجد مقاولين من الباطن في تحليل هذا البند.") # إضافة زر لإضافة مقاول جديد - if st.button("إضافة مقاول من الباطن", key="add_first_subcontractor"): + if st.button("إضافة مقاول", key="add_first_subcontractor"): item_analysis['subcontractors'] = [{ 'name': 'مقاول جديد', 'unit': 'عقد', @@ -1257,11 +969,14 @@ class PricingApp: st.session_state.item_analysis_edited = True st.rerun() return - + # إنشاء DataFrame من قائمة المقاولين df = pd.DataFrame(item_analysis['subcontractors']) df.columns = ['المقاول', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] + # إضافة عمود للإجراءات + df['الإجراءات'] = '✏️ 🗑️' + # تحويل الجدول إلى جدول قابل للتعديل edited_df = st.data_editor( df, @@ -1275,20 +990,28 @@ class PricingApp: min_value=0.0, format="%.2f", step=0.1, + on_change=self._update_total_price, ), "سعر الوحدة": st.column_config.NumberColumn( "سعر الوحدة", min_value=0.0, format="%.2f ريال", step=0.1, + on_change=self._update_total_price, ), "السعر الإجمالي": st.column_config.NumberColumn( "السعر الإجمالي", format="%.2f ريال", disabled=True, ), + "الإجراءات": st.column_config.Column( + "الإجراءات", + width="small", + disabled=True, + ), }, - num_rows="dynamic" + num_rows="dynamic", + hide_index=True, ) # تحديث البيانات في item_analysis بناءً على التعديلات @@ -1317,13 +1040,18 @@ class PricingApp: st.session_state.item_analysis_edited = True st.rerun() - # عرض إجمالي تكلفة المقاولين - st.metric("إجمالي تكلفة المقاولين من الباطن", f"{item_analysis['total_subcontractors_cost']:,.2f} ريال") + # عرض إجمالي تكلفة المقاولين بتنسيق محسن + st.markdown(f""" +
+
إجمالي تكلفة المقاولين من الباطن:
+
{item_analysis['total_subcontractors_cost']:,.2f} ريال
+
+ """, unsafe_allow_html=True) # أزرار التحكم col1, col2 = st.columns(2) with col1: - if st.button("إضافة مقاول جديد", key="add_subcontractor"): + if st.button("➕ إضافة مقاول جديد", key="add_subcontractor"): item_analysis['subcontractors'].append({ 'name': 'مقاول جديد', 'unit': 'عقد', @@ -1334,118 +1062,58 @@ class PricingApp: 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): - """عرض ملخص التكلفة""" + def _render_cost_summary_enhanced(self, item_analysis): + """عرض ملخص التكلفة (نسخة محسنة)""" st.subheader("ملخص التكلفة") - # إعداد الخطوط العربية - self._setup_arabic_fonts() + # حساب إجمالي التكلفة + 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'] + ) - # إنشاء بيانات الرسم البياني - # استخدام 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 + # إنشاء بيانات ملخص التكلفة + cost_data = [ + {"البند": "تكلفة المواد", "القيمة": item_analysis['total_materials_cost'], "النسبة": item_analysis['total_materials_cost'] / total_cost * 100 if total_cost > 0 else 0}, + {"البند": "تكلفة المعدات", "القيمة": item_analysis['total_equipment_cost'], "النسبة": item_analysis['total_equipment_cost'] / total_cost * 100 if total_cost > 0 else 0}, + {"البند": "تكلفة العمالة", "القيمة": item_analysis['total_labor_cost'], "النسبة": item_analysis['total_labor_cost'] / total_cost * 100 if total_cost > 0 else 0}, + {"البند": "تكلفة المقاولين من الباطن", "القيمة": item_analysis.get('total_subcontractors_cost', 0), "النسبة": item_analysis.get('total_subcontractors_cost', 0) / total_cost * 100 if total_cost > 0 else 0}, + {"البند": "تكاليف غير مباشرة", "القيمة": item_analysis['overhead_cost'], "النسبة": item_analysis['overhead_cost'] / total_cost * 100 if total_cost > 0 else 0}, + {"البند": "إجمالي التكلفة", "القيمة": total_cost, "النسبة": 100.0}, ] - # التحقق من أن جميع القيم موجبة (أو صفر) لتجنب الأخطاء في الرسم البياني - values = [max(0, val) for val in values] + # إنشاء DataFrame من بيانات ملخص التكلفة + df = pd.DataFrame(cost_data) - # إنشاء الرسم البياني - fig, ax = plt.subplots(figsize=(10, 6)) + # تنسيق الأعمدة + df["القيمة (ريال)"] = df["القيمة"].apply(lambda x: f"{x:,.2f}") + df["النسبة (%)"] = df["النسبة"].apply(lambda x: f"{x:.2f}%") - # رسم المخطط الدائري - wedges, texts, autotexts = ax.pie( - values, - labels=labels, - autopct='%1.1f%%', - startangle=90, - shadow=True, - ) + # حذف الأعمدة الأصلية + df = df.drop(columns=["القيمة", "النسبة"]) - # تعديل حجم النص - plt.setp(autotexts, size=10, weight="bold") - - # إضافة العنوان - title_ar = 'توزيع تكلفة البند' - title = get_display(arabic_reshaper.reshape(title_ar)) - ax.set_title(title, fontsize=16) + # عرض الجدول + st.dataframe(df, use_container_width=True, hide_index=True) # عرض الرسم البياني - st.pyplot(fig) + self._render_cost_chart(cost_data[:-1]) # استثناء صف الإجمالي - # عرض جدول ملخص التكلفة - 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 - ] - } - - df = pd.DataFrame(cost_summary) - df.columns = ['البند', 'التكلفة (ريال)', 'النسبة (%)'] - - # تنسيق الأرقام - df['التكلفة (ريال)'] = df['التكلفة (ريال)'].apply(lambda x: f"{x:,.2f}") - df['النسبة (%)'] = df['النسبة (%)'].apply(lambda x: f"{x:.2f}%") - - # عرض الجدول - st.dataframe(df, use_container_width=True) - - # إضافة حقل لتعديل المصاريف العمومية - st.subheader("تعديل المصاريف العمومية والربح") + # عرض تفاصيل الربح + st.subheader("تفاصيل الربح") col1, col2 = st.columns(2) + with col1: - new_overhead = st.number_input( - "المصاريف العمومية (ريال)", - min_value=0.0, - value=float(item_analysis['overhead_cost']), - step=10.0, - format="%.2f", - key="edit_overhead_cost" - ) + st.write(f"**سعر الوحدة:** {item_analysis['unit_price']:,.2f} ريال") + st.write(f"**إجمالي التكلفة:** {total_cost:,.2f} ريال") + st.write(f"**الربح:** {item_analysis['profit']:,.2f} ريال") + st.write(f"**نسبة الربح:** {(item_analysis['profit'] / item_analysis['unit_price'] * 100) if item_analysis['unit_price'] > 0 else 0:.2f}%") - 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'] - ) - + # تحديث الربح إذا تم تعديل التكاليف + if st.session_state.item_analysis_edited: item_analysis['profit'] = item_analysis['unit_price'] - total_cost st.session_state.item_analysis_edited = True st.rerun() @@ -1472,331 +1140,74 @@ class PricingApp: item_analysis['overhead_cost'] ) - item_analysis['unit_price'] = total_cost + item_analysis['profit'] + item_analysis['unit_price'] = total_cost + new_profit st.session_state.item_analysis_edited = True st.rerun() - - 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 _render_cost_analysis(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 - - # حساب إجمالي التكلفة - total_cost = sum(item['total_price'] for item in project_items) - - # عرض إجمالي التكلفة - st.metric("إجمالي تكلفة المشروع", f"{total_cost:,.2f} ريال") - - # إنشاء DataFrame من بنود المشروع - df = pd.DataFrame(project_items) - df = df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price']] - df.columns = ['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] - - # تنسيق الأسعار - df['سعر الوحدة'] = df['سعر الوحدة'].apply(lambda x: f"{x:,.2f} ريال") - df['السعر الإجمالي'] = df['السعر الإجمالي'].apply(lambda x: f"{x:,.2f} ريال") - - # عرض الجدول - st.dataframe(df, use_container_width=True) - - # إنشاء رسم بياني للتكلفة - self._setup_arabic_fonts() - + def _render_cost_chart(self, cost_data): + """عرض رسم بياني للتكاليف""" # إنشاء بيانات الرسم البياني - 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') + labels = [item["البند"] for item in cost_data] + values = [item["القيمة"] for item in cost_data] - # إضافة العنوان - 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) + # تحويل النص العربي ليعرض بشكل صحيح + labels_reshaped = [get_display(arabic_reshaper.reshape(label)) for label in labels] - 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.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") - return - - # حساب إجمالي التكلفة - total_cost = sum(item['total_price'] for item in project_items) - - # حساب هامش الربح - profit_margin = 0.15 # افتراضي 15% - profit = total_cost * profit_margin - total_with_profit = total_cost + profit - - # عرض معلومات الربحية - 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} ريال") - - # إضافة شريط تمرير لتعديل هامش الربح - 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 - - st.metric("إجمالي مع الربح الجديد", f"{total_with_profit:,.2f} ريال") - - # إنشاء رسم بياني للربحية - 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, + labels=labels_reshaped, autopct='%1.1f%%', startangle=90, - shadow=True, - colors=['#ff9999', '#66b3ff'] + shadow=False, ) - # تعديل حجم النص - plt.setp(autotexts, size=10, weight="bold") - - # إضافة العنوان - title_ar = 'توزيع التكلفة والربح' - title = get_display(arabic_reshaper.reshape(title_ar)) - ax.set_title(title, fontsize=16) + # تنسيق الرسم البياني + ax.axis('equal') + plt.title(get_display(arabic_reshaper.reshape('توزيع التكاليف'))) # عرض الرسم البياني st.pyplot(fig) + + def _update_boq_item_from_analysis(self, item_id, item_analysis): + """تحديث البند في جدول الكميات بناءً على التحليل""" + # الحصول على المشروع الحالي + current_project = next((p for p in st.session_state.projects if p['id'] == st.session_state.current_project_id), None) - 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("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") + if not current_project: return - - # عرض استراتيجيات التسعير المختلفة - st.write("### استراتيجيات التسعير المتاحة") - # استراتيجية التسعير القياسية - st.write("#### التسعير القياسي") - st.write("في هذه الاستراتيجية، يتم تسعير جميع البنود بناءً على التكلفة الفعلية مع إضافة هامش ربح موحد.") - - # استراتيجية التسعير الغير متزن - st.write("#### التسعير الغير متزن") - st.write("في هذه الاستراتيجية، يتم زيادة أسعار البنود المبكرة وتخفيض أسعار البنود المتأخرة للحصول على تدفق نقدي أفضل.") - - # تعديل معاملات التسعير الغير متزن - if project_info['pricing_type'] == 'غير متزن': - st.write("### تعديل معاملات التسعير الغير متزن") - - col1, col2 = st.columns(2) - with col1: - 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 early_factor != st.session_state.unbalanced_pricing_factors['early_items_factor']: - st.session_state.unbalanced_pricing_factors['early_items_factor'] = early_factor - - 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 late_factor != st.session_state.unbalanced_pricing_factors['late_items_factor']: - st.session_state.unbalanced_pricing_factors['late_items_factor'] = late_factor - - # عرض جدول الأسعار المعدلة - st.write("### الأسعار المعدلة باستخدام التسعير الغير متزن") - - # إنشاء نسخة من بنود المشروع - 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() - - 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'] - - unbalanced_items.append(unbalanced_item) - - # إنشاء 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) - - col1, col2 = st.columns(2) - with col1: - st.metric("إجمالي السعر الأصلي", f"{total_original:,.2f} ريال") - with col2: - st.metric("إجمالي السعر المعدل", f"{total_unbalanced:,.2f} ريال") - - # زر تطبيق التسعير الغير متزن - 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("سيتم تنفيذ هذه الميزة في الإصدار القادم") - - 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] - - 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 - } - - st.session_state.saved_pricing.append(saved_pricing) - - # تحديث حالة المشروع - for i, p in enumerate(st.session_state.projects): - if p['id'] == project_info['id']: - st.session_state.projects[i]['status'] = 'تم التسعير' - break - - st.success("تم حفظ التسعير بنجاح") - + # البحث عن البند في جدول الكميات + for item in current_project['boq']: + if item['id'] == item_id: + # تحديث سعر الوحدة والسعر الإجمالي + item['unit_price'] = item_analysis['unit_price'] + item['total_price'] = item['quantity'] * item['unit_price'] + break + + def _render_cost_analysis(self): + """عرض تحليل التكلفة""" + st.header("تحليل التكلفة") + st.info("هذه الصفحة قيد التطوير.") + + def _render_profit_analysis(self): + """عرض تحليل الربحية""" + st.header("تحليل الربحية") + st.info("هذه الصفحة قيد التطوير.") + + def _render_pricing_strategies(self): + """عرض استراتيجيات التسعير""" + st.header("استراتيجيات التسعير") + st.info("هذه الصفحة قيد التطوير.") + # تشغيل التطبيق if __name__ == "__main__": app = PricingApp() app.run() + + def _update_total_price(self, widget_value, widget_id): + """تحديث السعر الإجمالي تلقائياً عند تغيير الكمية أو سعر الوحدة""" + # سيتم استدعاء هذه الدالة عند تغيير قيمة الكمية أو سعر الوحدة + # يتم تحديث البيانات في الجدول وإعادة حساب المجموع الكلي + st.session_state.item_analysis_edited = True