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""" +