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,1056 +1,1802 @@ 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): - """تهيئة التطبيق""" - # إضافة CSS للتنسيق - self._add_custom_css() - - # تهيئة حالة الجلسة + """تهيئة وحدة التسعير""" + # تهيئة البيانات في حالة الجلسة إذا لم تكن موجودة if 'projects' not in st.session_state: - 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" + st.session_state.projects = [ + { + 'id': 1, + 'name': 'مشروع تطوير الطرق الداخلية', + 'client': 'وزارة النقل', + 'estimated_value': 5000000, + 'deadline': '2024-06-30', + 'status': 'قيد التسعير', + 'created_at': '2024-01-15', + 'pricing_type': 'قياسي' + }, + { + 'id': 2, + 'name': 'مشروع إنشاء مبنى إداري', + 'client': 'شركة التطوير العقاري', + 'estimated_value': 12000000, + 'deadline': '2024-08-15', + 'status': 'قيد التسعير', + 'created_at': '2024-02-01', + 'pricing_type': 'غير متزن' + } + ] + if '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 _add_custom_css(self): - """إضافة CSS مخصص للتطبيق""" - st.markdown(""" - - """, unsafe_allow_html=True) - - def _update_total_price(self): - """تحديث السعر الإجمالي تلقائياً عند تغيير الكمية أو سعر الوحدة""" - # سيتم استدعاء هذه الدالة عند تغيير قيمة الكمية أو سعر الوحدة - # يتم تحديث البيانات في الجدول وإعادة حساب المجموع الكلي - st.session_state.item_analysis_edited = True - + + def render(self): + """طريقة للتوافق مع الواجهة القديمة""" + self.run() + def run(self): - """تشغيل التطبيق""" + """تشغيل وحدة التسعير""" st.title("وحدة التسعير المتكاملة") - # عرض الشريط العلوي - self._render_top_bar() + # عرض زر إنشاء تسعير جديد + 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() + + # عرض قائمة المشاريع + self._render_projects_list() - # عرض المحتوى حسب التبويب المحدد - if st.session_state.current_tab == "boq": - self._render_bill_of_quantities() - elif st.session_state.current_tab == "item_analysis": - self._render_item_price_analysis() - - def _render_top_bar(self): - """عرض الشريط العلوي""" - cols = st.columns([1, 1, 1, 1, 1]) + # عرض تفاصيل المشروع الحالي + 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 cols[0]: - if st.button("جدول الكميات", use_container_width=True): - st.session_state.current_tab = "boq" + 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 cols[1]: - if st.button("تحليل الأسعار", use_container_width=True): - if st.session_state.current_project_id is not None: - st.session_state.current_tab = "item_analysis" - st.rerun() - else: - st.warning("الرجاء اختيار مشروع أولاً") + 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() + + def _render_projects_list(self): + """عرض قائمة المشاريع""" + st.subheader("قائمة المشاريع") - with cols[4]: - if st.button("إنشاء مشروع جديد", use_container_width=True): - self._create_new_project() - - def _create_new_project(self): - """إنشاء مشروع جديد""" - project_id = f"proj_{len(st.session_state.projects) + 1}" - new_project = { - "id": project_id, - "name": f"مشروع رقم {len(st.session_state.projects) + 1}", - "client": "عميل قياسي", - "date": datetime.now().strftime("%Y-%m-%d"), - "budget": 1000000.00, - "items": [] - } - st.session_state.projects[project_id] = new_project - st.session_state.current_project_id = project_id - st.session_state.current_tab = "boq" - st.rerun() - - def _render_bill_of_quantities(self): - """عرض جدول الكميات""" - st.header("جدول الكميات") - - # اختيار المشروع - project_options = {proj["name"]: proj_id for proj_id, proj in st.session_state.projects.items()} - selected_project_name = st.selectbox( - "اختر المشروع", - options=list(project_options.keys()), - index=0 if st.session_state.current_project_id is None else list(project_options.keys()).index(st.session_state.projects[st.session_state.current_project_id]["name"]) - ) + 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 - st.session_state.current_project_id = project_options[selected_project_name] - current_project = st.session_state.projects[st.session_state.current_project_id] + def _render_project_info(self, project): + """عرض معلومات المشروع""" + st.header(f"تسعير: {project['name']}") - # عرض معلومات المشروع - col1, col2, col3 = st.columns(3) + col1, col2, col3, col4 = st.columns(4) with col1: - st.write(f"**العميل:** {current_project['client']}") + st.metric("العميل", project['client']) with col2: - st.write(f"**التاريخ:** {current_project['date']}") + st.metric("القيمة التقديرية", f"{project['estimated_value']:,.2f} ريال") with col3: - st.write(f"**الميزانية:** {current_project['budget']:,.2f} ريال") + st.metric("الموعد النهائي", project['deadline']) + with col4: + st.metric("نوع التسعير", project['pricing_type']) + + def _render_bill_of_quantities(self): + """عرض جدول الكميات""" + st.subheader("جدول الكميات") - # إضافة بند جديد - with st.expander("إضافة بند جديد"): - with st.form("add_item_form"): - col1, col2 = st.columns(2) - with col1: - new_item_name = st.text_input("اسم البند") - new_item_unit = st.text_input("الوحدة") - with col2: - new_item_quantity = st.number_input("الكمية", min_value=0.0, step=0.1) - new_item_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, step=0.1) + # زر إضافة بند جديد + 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 - submitted = st.form_submit_button("إضافة البند") - if submitted and new_item_name and new_item_unit: - new_item = { - "id": f"item_{len(current_project['items']) + 1}", - "name": new_item_name, - "unit": new_item_unit, - "quantity": float(new_item_quantity), - "unit_price": float(new_item_price), - "total_price": float(new_item_quantity) * float(new_item_price), - "materials": [], - "labor": [], - "equipment": [], - "subcontractors": [] - } - current_project["items"].append(new_item) - st.success("تمت إضافة البند بنجاح") - st.rerun() + 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 current_project["items"]: - items_df = pd.DataFrame([ - { - "م": i + 1, - "اسم البند": item["name"], - "الوحدة": item["unit"], - "الكمية": item["quantity"], - "سعر الوحدة (ريال)": item["unit_price"], - "السعر الإجمالي (ريال)": item["total_price"], - "تحليل": f'تحليل' - } - for i, item in enumerate(current_project["items"]) - ]) - - # إضافة صف المجموع - total_price = sum(item["total_price"] for item in current_project["items"]) - total_row = pd.DataFrame([{ - "م": "", - "اسم البند": "المجموع", - "الوحدة": "", - "الكمية": "", - "سعر الوحدة (ريال)": "", - "السعر الإجمالي (ريال)": total_price, - "تحليل": "" - }]) - - items_df = pd.concat([items_df, total_row], ignore_index=True) - - st.write(items_df.to_html(escape=False, index=False), unsafe_allow_html=True) - - # معالجة النقر على رابط التحليل - for i in range(len(current_project["items"])): - if st.button(f"تحليل البند {i+1}", key=f"analyze_btn_{i}", use_container_width=True): - st.session_state.current_item_index = i - st.session_state.current_tab = "item_analysis" - st.rerun() - else: - st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود جديدة.") - - def _render_item_price_analysis(self): - """عرض تحليل سعر البند""" - if st.session_state.current_project_id is None or "current_item_index" not in st.session_state: - st.warning("الرجاء اختيار مشروع وبند أولاً") + if not project_items: + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") return + + # إنشاء DataFrame من بنود المشروع + df = pd.DataFrame(project_items) + df = df[['id', 'code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'resource_type']] - current_project = st.session_state.projects[st.session_state.current_project_id] - current_item = current_project["items"][st.session_state.current_item_index] + # تحويل الجدول إلى جدول قابل للتعديل + 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" + ) - st.header(f"تحليل سعر البند: {current_item['name']}") + # تحديث البيانات في حالة التعديل + 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() - # معلومات البند - col1, col2, col3, col4 = st.columns(4) + # حساب المجموع الكلي + total_price = sum(item['total_price'] for item in project_items) + st.metric("إجمالي جدول الكميات", f"{total_price:,.2f} ريال") + + # أزرار التصدير والتعديل + col1, col2 = st.columns(2) with col1: - st.write(f"**الوحدة:** {current_item['unit']}") - with col2: - st.write(f"**الكمية:** {current_item['quantity']}") - with col3: - st.write(f"**سعر الوحدة:** {current_item['unit_price']} ريال") - with col4: - st.write(f"**السعر الإجمالي:** {current_item['total_price']} ريال") - - # زر حفظ التغييرات - if st.session_state.item_analysis_edited: - if st.button("حفظ التغييرات", type="primary", use_container_width=True): - # تحديث السعر الإجمالي للبند - materials_total = sum(material.get("total_price", 0) for material in current_item["materials"]) - labor_total = sum(labor.get("total_price", 0) for labor in current_item["labor"]) - equipment_total = sum(equipment.get("total_price", 0) for equipment in current_item["equipment"]) - subcontractors_total = sum(sub.get("total_price", 0) for sub in current_item["subcontractors"]) + 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 = ['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي', 'نوع المورد'] - direct_cost = materials_total + labor_total + equipment_total + subcontractors_total - current_item["unit_price"] = direct_cost / current_item["quantity"] if current_item["quantity"] > 0 else 0 - current_item["total_price"] = current_item["unit_price"] * current_item["quantity"] + csv = export_df.to_csv(index=False) + b64 = base64.b64encode(csv.encode()).decode() + href = f'تحميل CSV' + st.markdown(href, unsafe_allow_html=True) - st.session_state.item_analysis_edited = False - st.success("تم حفظ التغييرات بنجاح") - st.rerun() - - # تحليل المواد - st.subheader("تحليل المواد") - - # إضافة مادة جديدة - with st.expander("إضافة مادة جديدة"): - with st.form("add_material_form"): - col1, col2 = st.columns(2) - with col1: - new_material_name = st.text_input("اسم المادة") - new_material_unit = st.text_input("الوحدة") - with col2: - new_material_quantity = st.number_input("الكمية", min_value=0.0, step=0.1) - new_material_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, step=0.1) + 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" + ) - submitted = st.form_submit_button("إضافة المادة") - if submitted and new_material_name and new_material_unit: - new_material = { - "id": f"material_{len(current_item['materials']) + 1}", - "name": new_material_name, - "unit": new_material_unit, - "quantity": float(new_material_quantity), - "unit_price": float(new_material_price), - "total_price": float(new_material_quantity) * float(new_material_price) - } - current_item["materials"].append(new_material) - st.session_state.item_analysis_edited = True - st.success("تمت إضافة المادة بنجاح") + 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("إضافة بند جديد") - # عرض جدول المواد - if current_item["materials"]: - # تحويل البيانات إلى DataFrame - materials_data = [] - for i, material in enumerate(current_item["materials"]): - materials_data.append({ - "م": i + 1, - "اسم المادة": material["name"], - "الوحدة": material["unit"], - "الكمية": material["quantity"], - "سعر الوحدة (ريال)": material["unit_price"], - "السعر الإجمالي (ريال)": material["total_price"], - "الإجراءات": f' ' - }) + 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") - materials_df = pd.DataFrame(materials_data) - - # إضافة صف المجموع - materials_total = sum(material["total_price"] for material in current_item["materials"]) - materials_df = materials_df.append({ - "م": "", - "اسم المادة": "المجموع", - "الوحدة": "", - "الكمية": "", - "سعر الوحدة (ريال)": "", - "السعر الإجمالي (ريال)": materials_total, - "الإجراءات": "" - }, ignore_index=True) - - # عرض الجدول القابل للتعديل - edited_materials_df = st.data_editor( - materials_df, - column_config={ - "م": st.column_config.NumberColumn("م", width="small"), - "اسم المادة": st.column_config.TextColumn("اسم المادة"), - "الوحدة": st.column_config.TextColumn("الوحدة", width="small"), - "الكمية": st.column_config.NumberColumn("الكمية", format="%.2f", width="small"), - "سعر الوحدة (ريال)": st.column_config.NumberColumn("سعر الوحدة (ريال)", format="%.2f"), - "السعر الإجمالي (ريال)": st.column_config.NumberColumn("السعر الإجمالي (ريال)", format="%.2f"), - "الإجراءات": st.column_config.TextColumn("الإجراءات", width="medium"), - }, - hide_index=True, - key="materials_table", - on_change=self._update_total_price, - disabled=["م", "اسم المادة", "الوحدة", "السعر الإجمالي (��يال)", "الإجراءات"] - ) + 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} ريال") - # تحديث البيانات بعد التعديل - for i, material in enumerate(current_item["materials"]): - if i < len(edited_materials_df) - 1: # تجاهل صف المجموع - material["quantity"] = edited_materials_df.iloc[i]["الكمية"] - material["unit_price"] = edited_materials_df.iloc[i]["سعر الوحدة (ريال)"] - material["total_price"] = material["quantity"] * material["unit_price"] + col5, col6 = st.columns(2) + with col5: + submit_button = st.form_submit_button("حفظ") + with col6: + cancel_button = st.form_submit_button("إلغاء") + + if submit_button: + if code and description and unit and quantity > 0 and unit_price > 0: + new_item = { + 'id': st.session_state.next_boq_item_id, + 'project_id': st.session_state.current_project, + 'code': code, + 'description': description, + 'unit': unit, + 'quantity': quantity, + 'unit_price': unit_price, + 'total_price': total_price, + 'resource_type': resource_type + } + + st.session_state.boq_items.append(new_item) + st.session_state.next_boq_item_id += 1 + st.session_state.show_new_boq_item_form = False + st.rerun() + + if cancel_button: + st.session_state.show_new_boq_item_form = False + st.rerun() - # معالجة أزرار التعديل والحذف - for i in range(len(current_item["materials"])): - col1, col2 = st.columns([1, 1]) - with col1: - if st.button(f"تعديل المادة {i+1}", key=f"edit_material_btn_{i}"): - st.session_state[f"edit_material_{i}"] = True + 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 - with col2: - if st.button(f"حذف المادة {i+1}", key=f"delete_material_btn_{i}"): - st.session_state[f"delete_material_{i}"] = True + 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 st.session_state.get(f"edit_material_{i}", False): - with st.form(f"edit_material_form_{i}"): - material = current_item["materials"][i] - col1, col2 = st.columns(2) - with col1: - material_name = st.text_input("اسم المادة", value=material["name"]) - material_unit = st.text_input("الوحدة", value=material["unit"]) - with col2: - material_quantity = st.number_input("الكمية", min_value=0.0, step=0.1, value=material["quantity"]) - material_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, step=0.1, value=material["unit_price"]) + 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 - col1, col2 = st.columns(2) - with col1: - if st.form_submit_button("حفظ التعديلات"): - material["name"] = material_name - material["unit"] = material_unit - material["quantity"] = float(material_quantity) - material["unit_price"] = float(material_price) - material["total_price"] = material["quantity"] * material["unit_price"] - st.session_state.item_analysis_edited = True - st.session_state[f"edit_material_{i}"] = False - st.rerun() - with col2: - if st.form_submit_button("إلغاء"): - st.session_state[f"edit_material_{i}"] = False - st.rerun() + st.session_state.show_edit_boq_item_form = False + st.rerun() - # تأكيد الحذف - if st.session_state.get(f"delete_material_{i}", False): - st.warning(f"هل أنت متأكد من حذف المادة: {current_item['materials'][i]['name']}؟") - col1, col2 = st.columns(2) - with col1: - if st.button("نعم، حذف", key=f"confirm_delete_material_{i}"): - current_item["materials"].pop(i) - st.session_state.item_analysis_edited = True - st.session_state[f"delete_material_{i}"] = False - st.rerun() - with col2: - if st.button("إلغاء", key=f"cancel_delete_material_{i}"): - st.session_state[f"delete_material_{i}"] = False - st.rerun() - - # عرض المجموع الكلي للمواد - st.markdown(f"
إجمالي تكلفة المواد: {materials_total:,.2f} ريال
", unsafe_allow_html=True) - else: - st.info("لا توجد مواد مضافة لهذا البند") + 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("سحب من الموارد المسجلة") - # تحليل العمالة - st.subheader("تحليل العمالة") + # اختيار نوع المورد + resource_type = st.selectbox( + "نوع المورد", + ["المواد", "العمالة", "المعدات", "المقاولين من الباطن"], + index=["المواد", "العمالة", "المعدات", "المقاولين من الباطن"].index(st.session_state.selected_resource_type), + key="resource_type_selector" + ) - # إضافة عمالة جديدة - with st.expander("إضافة عمالة جديدة"): - with st.form("add_labor_form"): - col1, col2 = st.columns(2) - with col1: - new_labor_name = st.text_input("نوع العمالة") - new_labor_unit = st.text_input("الوحدة", value="يوم") - with col2: - new_labor_quantity = st.number_input("عدد الأيام", min_value=0.0, step=0.5) - new_labor_price = st.number_input("الأجر اليومي (ريال)", min_value=0.0, step=10.0) - - submitted = st.form_submit_button("إضافة العمالة") - if submitted and new_labor_name: - new_labor = { - "id": f"labor_{len(current_item['labor']) + 1}", - "name": new_labor_name, - "unit": new_labor_unit, - "quantity": float(new_labor_quantity), - "unit_price": float(new_labor_price), - "total_price": float(new_labor_quantity) * float(new_labor_price) - } - current_item["labor"].append(new_labor) - st.session_state.item_analysis_edited = True - st.success("تمت إضافة العمالة بنجاح") + 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() - - # عرض جدول العمالة - if current_item["labor"]: - # تحويل البيانات إلى DataFrame - labor_data = [] - for i, labor in enumerate(current_item["labor"]): - labor_data.append({ - "م": i + 1, - "نوع العمالة": labor["name"], - "الوحدة": labor["unit"], - "عدد الأيام": labor["quantity"], - "الأجر اليومي (ريال)": labor["unit_price"], - "الإجمالي (ريال)": labor["total_price"], - "الإجراءات": f' ' - }) + return - labor_df = pd.DataFrame(labor_data) - - # إضافة صف المجموع - labor_total = sum(labor["total_price"] for labor in current_item["labor"]) - labor_df = labor_df.append({ - "م": "", - "نوع العمالة": "المجموع", - "الوحدة": "", - "عدد الأيام": "", - "الأجر اليومي (ريال)": "", - "الإجمالي (ريال)": labor_total, - "الإجراءات": "" - }, ignore_index=True) - - # عرض الجدول القابل للتعديل - edited_labor_df = st.data_editor( - labor_df, - column_config={ - "م": st.column_config.NumberColumn("م", width="small"), - "نوع العمالة": st.column_config.TextColumn("نوع العمالة"), - "الوحدة": st.column_config.TextColumn("الوحدة", width="small"), - "عدد الأيام": st.column_config.NumberColumn("عدد الأيام", format="%.1f", width="small"), - "الأجر اليومي (ريال)": st.column_config.NumberColumn("الأجر اليومي (ريال)", format="%.2f"), - "الإجمالي (ريال)": st.column_config.NumberColumn("الإجمالي (ريال)", format="%.2f"), - "الإجراءات": st.column_config.TextColumn("الإجراءات", width="medium"), - }, - hide_index=True, - key="labor_table", - on_change=self._update_total_price, - disabled=["م", "نوع العمالة", "الوحدة", "الإجمالي (ريال)", "الإجراءات"] - ) + # إنشاء 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 = ['الرقم', 'الاسم', 'الفئة', 'الوحدة', 'السعر'] - # تحديث البيانات بعد التعديل - for i, labor in enumerate(current_item["labor"]): - if i < len(edited_labor_df) - 1: # تجاهل صف المجموع - labor["quantity"] = edited_labor_df.iloc[i]["عدد الأيام"] - labor["unit_price"] = edited_labor_df.iloc[i]["الأجر اليومي (ريال)"] - labor["total_price"] = labor["quantity"] * labor["unit_price"] + # تنسيق السعر + df['السعر'] = df['السعر'].apply(lambda x: f"{x:,.2f} ريال") - # معالجة أزرار التعديل والحذف - for i in range(len(current_item["labor"])): - col1, col2 = st.columns([1, 1]) + # عرض الجدول + st.dataframe(df, use_container_width=True) + + with st.form(key="add_from_resource_form"): + col1, col2 = st.columns(2) with col1: - if st.button(f"تعديل العمالة {i+1}", key=f"edit_labor_btn_{i}"): - st.session_state[f"edit_labor_{i}"] = True - + selected_resource_id = st.selectbox( + "اختر المورد", + options=[r['id'] for r in resources], + format_func=lambda x: next((r['name'] for r in resources if r['id'] == x), ""), + key="select_resource" + ) + with col2: - if st.button(f"حذف العمالة {i+1}", key=f"delete_labor_btn_{i}"): - st.session_state[f"delete_labor_{i}"] = True + 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 = { + "المواد": "مواد", + "العمالة": "عمالة", + "المعدات": "معدات", + "المقاولين من الباطن": "مقاولين من الباطن" + } - # نموذج التعديل - if st.session_state.get(f"edit_labor_{i}", False): - with st.form(f"edit_labor_form_{i}"): - labor = current_item["labor"][i] - col1, col2 = st.columns(2) - with col1: - labor_name = st.text_input("نوع العمالة", value=labor["name"]) - labor_unit = st.text_input("الوحدة", value=labor["unit"]) - with col2: - labor_quantity = st.number_input("عدد الأيام", min_value=0.0, step=0.5, value=labor["quantity"]) - labor_price = st.number_input("الأجر اليومي (ريال)", min_value=0.0, step=10.0, value=labor["unit_price"]) - - col1, col2 = st.columns(2) - with col1: - if st.form_submit_button("حفظ التعديلات"): - labor["name"] = labor_name - labor["unit"] = labor_unit - labor["quantity"] = float(labor_quantity) - labor["unit_price"] = float(labor_price) - labor["total_price"] = labor["quantity"] * labor["unit_price"] - st.session_state.item_analysis_edited = True - st.session_state[f"edit_labor_{i}"] = False - st.rerun() - with col2: - if st.form_submit_button("إلغاء"): - st.session_state[f"edit_labor_{i}"] = False - st.rerun() + # إنشاء كود فريد + 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() - # تأكيد الحذف - if st.session_state.get(f"delete_labor_{i}", False): - st.warning(f"هل أنت متأكد من حذف العمالة: {current_item['labor'][i]['name']}؟") - col1, col2 = st.columns(2) - with col1: - if st.button("نعم، حذف", key=f"confirm_delete_labor_{i}"): - current_item["labor"].pop(i) - st.session_state.item_analysis_edited = True - st.session_state[f"delete_labor_{i}"] = False - st.rerun() - with col2: - if st.button("إلغاء", key=f"cancel_delete_labor_{i}"): - st.session_state[f"delete_labor_{i}"] = False - st.rerun() - - # عرض المجموع الكلي للعمالة - st.markdown(f"
إجمالي تكلفة العمالة: {labor_total:,.2f} ريال
", unsafe_allow_html=True) + if cancel_button: + st.session_state.show_resource_selector = False + st.rerun() else: - st.info("لا توجد عمالة مضافة لهذا البند") + st.error("تنسيق بيانات الموارد غير صحيح. يرجى التأكد من وجود الحقول المطلوبة.") + + if st.button("إلغاء", key="cancel_resource_selector_error"): + st.session_state.show_resource_selector = False + st.rerun() + + def _render_item_price_analysis(self): + """عرض تحليل سعر البند مع إمكانية التعديل والحفظ""" + st.subheader("تحليل سعر البند") - # تحليل المعدات - st.subheader("تحليل المعدات") + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] - # إضافة معدات جديدة - with st.expander("إضافة معدات جديدة"): - with st.form("add_equipment_form"): - col1, col2 = st.columns(2) - with col1: - new_equipment_name = st.text_input("نوع المعدات") - new_equipment_unit = st.text_input("الوحدة", value="يوم") - with col2: - new_equipment_quantity = st.number_input("عدد الأيام", min_value=0.0, step=0.5, key="new_equipment_quantity") - new_equipment_price = st.number_input("التكلفة اليومية (ريال)", min_value=0.0, step=10.0, key="new_equipment_price") + if not project_items: + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") + 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" + ) + + # الحصول على البند المحدد + selected_item = next((item for item in project_items if item['id'] == selected_item_id), None) + + if not selected_item: + st.error("لم يتم العثور على البند المحدد.") + return + + # عرض معلومات البند + col1, col2 = st.columns(2) + with col1: + st.write(f"**البند:** {selected_item['description']}") + st.write(f"**الكود:** {selected_item['code']}") + st.write(f"**الوحدة:** {selected_item['unit']}") + with col2: + st.write(f"**الكمية:** {selected_item['quantity']}") + st.write(f"**سعر الوحدة:** {selected_item['unit_price']:,.2f} ريال") + st.write(f"**السعر الإجمالي:** {selected_item['total_price']:,.2f} ريال") + + # إنشاء تحليل سعر البند + item_analysis = self._generate_item_analysis(selected_item) + + # عرض تحليل المواد مع إمكانية التعديل + self._render_materials_analysis(item_analysis) + + # عرض تحليل المعدات مع إمكانية التعديل + self._render_equipment_analysis(item_analysis) + + # عرض تحليل العمالة مع إمكانية التعديل + self._render_labor_analysis(item_analysis) + + # عرض تحليل المقاولين من الباطن مع إمكانية التعديل + self._render_subcontractors_analysis(item_analysis) + + # عرض ملخص التكلفة + self._render_cost_summary(item_analysis) + + # زر حفظ التغييرات في جدول الكميات الرئيسي + if st.button("حفظ جميع التغييرات في جدول الكميات", key="save_all_changes", type="primary"): + # تحديث البند في جدول الكميات + self._update_boq_item_from_analysis(selected_item_id, item_analysis) + st.success("تم حفظ جميع التغييرات بنجاح في جدول الكميات الرئيسي") + st.session_state.item_analysis_edited = False + st.rerun() + + def _generate_item_analysis(self, item): + """إنشاء تحليل سعر البند""" + # في هذه النسخة التجريبية، سنقوم بإنشاء بيانات عشوائية للتحليل + # في النسخة النهائية، يجب استخدام بيانات حقيقية من قاعدة البيانات + + unit_price = item['unit_price'] + + # تقسيم سعر الوحدة إلى مكوناته + materials_cost = unit_price * random.uniform(0.4, 0.6) + equipment_cost = unit_price * random.uniform(0.1, 0.2) + labor_cost = unit_price * random.uniform(0.1, 0.2) + overhead_cost = unit_price * random.uniform(0.05, 0.1) + profit = unit_price - (materials_cost + equipment_cost + labor_cost + overhead_cost) + + # إنشاء قائمة المواد + materials = [ + { + 'name': 'خرسانة جاهزة' if 'خرسانة' in item['description'].lower() else 'حديد تسليح' if 'حديد' in item['description'].lower() else 'رمل', + 'unit': 'م3' if 'خرسانة' in item['description'].lower() else 'طن' if 'حديد' in item['description'].lower() else 'م3', + 'quantity': random.uniform(0.5, 1.5), + 'unit_price': materials_cost * 0.6, + 'total_price': materials_cost * 0.6 * random.uniform(0.5, 1.5) + }, + { + 'name': 'إسمنت', + 'unit': 'طن', + 'quantity': random.uniform(0.2, 0.5), + 'unit_price': materials_cost * 0.3, + 'total_price': materials_cost * 0.3 * random.uniform(0.2, 0.5) + }, + { + 'name': 'مواد أخرى', + 'unit': 'مجموعة', + 'quantity': 1, + 'unit_price': materials_cost * 0.1, + 'total_price': materials_cost * 0.1 + } + ] + + # إنشاء قائمة المعدات + equipment = [ + { + 'name': 'خلاطة خرسانة' if 'خرسانة' in item['description'].lower() else 'حفارة' if 'حفر' in item['description'].lower() else 'رافعة', + 'unit': 'يوم', + 'quantity': random.uniform(1, 3), + 'unit_price': equipment_cost * 0.7, + 'total_price': equipment_cost * 0.7 * random.uniform(1, 3) + }, + { + 'name': 'معدات أخرى', + 'unit': 'يوم', + 'quantity': random.uniform(1, 2), + 'unit_price': equipment_cost * 0.3, + 'total_price': equipment_cost * 0.3 * random.uniform(1, 2) + } + ] + + # إنشاء قائمة العمالة + labor = [ + { + 'name': 'عامل فني', + 'unit': 'يوم', + 'quantity': random.uniform(2, 5), + 'unit_price': labor_cost * 0.6, + 'total_price': labor_cost * 0.6 * random.uniform(2, 5) + }, + { + 'name': 'عامل عادي', + 'unit': 'يوم', + 'quantity': random.uniform(3, 8), + 'unit_price': labor_cost * 0.4, + 'total_price': labor_cost * 0.4 * random.uniform(3, 8) + } + ] + + # إنشاء قائمة المقاولين من الباطن + subcontractors = [ + { + 'name': 'مقاول أعمال خرسانية' if 'خرسانة' in item['description'].lower() else 'مقاول أعمال حفر' if 'حفر' in item['description'].lower() else 'مقاول عام', + 'unit': 'عقد', + 'quantity': 1, + 'unit_price': unit_price * 0.15, + 'total_price': unit_price * 0.15 + } + ] + + # حساب إجمالي التكاليف + total_materials_cost = sum(material['total_price'] for material in materials) + total_equipment_cost = sum(equipment_item['total_price'] for equipment_item in equipment) + total_labor_cost = sum(labor_item['total_price'] for labor_item in labor) + total_subcontractors_cost = sum(subcontractor['total_price'] for subcontractor in subcontractors) + + # تعديل الربح ليكون الفرق بين سعر الوحدة وإجمالي التكاليف + total_cost = total_materials_cost + total_equipment_cost + total_labor_cost + total_subcontractors_cost + overhead_cost + profit = unit_price - total_cost + + return { + 'item': item, + 'materials': materials, + 'equipment': equipment, + 'labor': labor, + 'subcontractors': subcontractors, + 'total_materials_cost': total_materials_cost, + 'total_equipment_cost': total_equipment_cost, + 'total_labor_cost': total_labor_cost, + 'total_subcontractors_cost': total_subcontractors_cost, + 'overhead_cost': overhead_cost, + 'profit': profit, + 'unit_price': unit_price + } + + def _render_materials_analysis(self, item_analysis): + """عرض تحليل المواد مع إمكانية التعديل""" + st.subheader("تحليل المواد") + + if not item_analysis['materials']: + st.info("لا توجد مواد في تحليل هذا البند.") + + # إضافة زر لإضافة مواد جديدة + if st.button("إضافة مواد", key="add_first_material"): + item_analysis['materials'] = [{ + 'name': 'مادة جديدة', + 'unit': 'وحدة', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }] + st.session_state.item_analysis_edited = True + st.rerun() + return + + # إنشاء DataFrame من قائمة المواد + df = pd.DataFrame(item_analysis['materials']) + df.columns = ['المادة', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] + + # تحويل الجدول إلى جدول قابل للتعديل + edited_df = st.data_editor( + df, + use_container_width=True, + key="edit_materials_table", + column_config={ + "المادة": st.column_config.Column("المادة"), + "الوحدة": st.column_config.Column("الوحدة"), + "الكمية": st.column_config.NumberColumn( + "الكمية", + min_value=0.0, + format="%.2f", + step=0.1, + ), + "سعر الوحدة": st.column_config.NumberColumn( + "سعر الوحدة", + min_value=0.0, + format="%.2f ريال", + step=0.1, + ), + "السعر الإجمالي": st.column_config.NumberColumn( + "السعر الإجمالي", + format="%.2f ريال", + disabled=True, + ), + }, + num_rows="dynamic" + ) + + # تحديث البيانات في item_analysis بناءً على التعديلات + if edited_df is not None and not edited_df.equals(df): + # حذف جميع المواد الحالية + item_analysis['materials'] = [] + + # إضافة المواد المعدلة + for i, row in edited_df.iterrows(): + # حساب السعر الإجمالي + total_price = row['الكمية'] * row['سعر الوحدة'] - submitted = st.form_submit_button("إضافة المعدات") - if submitted and new_equipment_name: - new_equipment = { - "id": f"equipment_{len(current_item['equipment']) + 1}", - "name": new_equipment_name, - "unit": new_equipment_unit, - "quantity": float(new_equipment_quantity), - "unit_price": float(new_equipment_price), - "total_price": float(new_equipment_quantity) * float(new_equipment_price) - } - current_item["equipment"].append(new_equipment) - st.session_state.item_analysis_edited = True - st.success("تمت إضافة المعدات بنجاح") - st.rerun() + # إضافة المادة + item_analysis['materials'].append({ + 'name': row['المادة'], + 'unit': row['الوحدة'], + 'quantity': row['الكمية'], + 'unit_price': row['سعر الوحدة'], + 'total_price': total_price + }) + + # إعادة حساب إجمالي تكلفة المواد + item_analysis['total_materials_cost'] = sum(material['total_price'] for material in item_analysis['materials']) + + # تعيين علامة التعديل + st.session_state.item_analysis_edited = True + st.rerun() - # عرض جدول المعدات - if current_item["equipment"]: - # تحويل البيانات إلى DataFrame - equipment_data = [] - for i, equipment in enumerate(current_item["equipment"]): - equipment_data.append({ - "م": i + 1, - "نوع المعدات": equipment["name"], - "الوحدة": equipment["unit"], - "عدد الأيام": equipment["quantity"], - "التكلفة اليومية (ريال)": equipment["unit_price"], - "الإجمالي (ريال)": equipment["total_price"], - "الإجراءات": f' ' + # عرض إجمالي تكلفة المواد + st.metric("إجمالي تكلفة المواد", f"{item_analysis['total_materials_cost']:,.2f} ريال") + + # أزرار التحكم + col1, col2 = st.columns(2) + with col1: + if st.button("إضافة مادة جديدة", key="add_material"): + item_analysis['materials'].append({ + 'name': 'مادة جديدة', + 'unit': 'وحدة', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 }) + st.session_state.item_analysis_edited = True + st.rerun() + + def _render_equipment_analysis(self, item_analysis): + """عرض تحليل المعدات مع إمكانية التعديل""" + st.subheader("تحليل المعدات") + + if not item_analysis['equipment']: + st.info("لا توجد معدات في تحليل هذا البند.") - equipment_df = pd.DataFrame(equipment_data) - - # إضافة صف المجموع - equipment_total = sum(equipment["total_price"] for equipment in current_item["equipment"]) - equipment_df = equipment_df.append({ - "م": "", - "نوع المعدات": "المجموع", - "الوحدة": "", - "عدد الأيام": "", - "التكلفة اليومية (ريال)": "", - "الإجمالي (ريال)": equipment_total, - "الإجراءات": "" - }, ignore_index=True) - - # عرض الجدول القابل للتعديل - edited_equipment_df = st.data_editor( - equipment_df, - column_config={ - "م": st.column_config.NumberColumn("م", width="small"), - "نوع المعدات": st.column_config.TextColumn("نوع المعدات"), - "الوحدة": st.column_config.TextColumn("الوحدة", width="small"), - "عدد الأيام": st.column_config.NumberColumn("عدد الأيام", format="%.1f", width="small"), - "التكلفة اليومية (ريال)": st.column_config.NumberColumn("التكلفة اليومية (ريال)", format="%.2f"), - "الإجمالي (ريال)": st.column_config.NumberColumn("الإجمالي (ريال)", format="%.2f"), - "الإجراءات": st.column_config.TextColumn("الإجراءات", width="medium"), - }, - hide_index=True, - key="equipment_table", - on_change=self._update_total_price, - disabled=["م", "نوع المعدات", "الوحدة", "الإجمالي (ريال)", "الإجراءات"] - ) + # إضافة زر لإضافة معدات جديدة + if st.button("إضافة معدات", key="add_first_equipment"): + item_analysis['equipment'] = [{ + 'name': 'معدة جديدة', + 'unit': 'يوم', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }] + st.session_state.item_analysis_edited = True + st.rerun() + return - # تحديث البيانات بعد التعديل - for i, equipment in enumerate(current_item["equipment"]): - if i < len(edited_equipment_df) - 1: # تجاهل صف المجموع - equipment["quantity"] = edited_equipment_df.iloc[i]["عدد الأيام"] - equipment["unit_price"] = edited_equipment_df.iloc[i]["التكلفة اليومية (ريال)"] - equipment["total_price"] = equipment["quantity"] * equipment["unit_price"] + # إنشاء DataFrame من قائمة المعدات + df = pd.DataFrame(item_analysis['equipment']) + df.columns = ['المعدة', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] + + # تحويل الجدول إلى جدول قابل للتعديل + edited_df = st.data_editor( + df, + use_container_width=True, + key="edit_equipment_table", + column_config={ + "المعدة": st.column_config.Column("المعدة"), + "الوحدة": st.column_config.Column("الوحدة"), + "الكمية": st.column_config.NumberColumn( + "الكمية", + min_value=0.0, + format="%.2f", + step=0.1, + ), + "سعر الوحدة": st.column_config.NumberColumn( + "سعر الوحدة", + min_value=0.0, + format="%.2f ريال", + step=0.1, + ), + "السعر الإجمالي": st.column_config.NumberColumn( + "السعر الإجمالي", + format="%.2f ريال", + disabled=True, + ), + }, + num_rows="dynamic" + ) + + # تحديث البيانات في item_analysis بناءً على التعديلات + if edited_df is not None and not edited_df.equals(df): + # حذف جميع المعدات الحالية + item_analysis['equipment'] = [] - # معالجة أزرار التعديل والحذف - for i in range(len(current_item["equipment"])): - col1, col2 = st.columns([1, 1]) - with col1: - if st.button(f"تعديل المعدات {i+1}", key=f"edit_equipment_btn_{i}"): - st.session_state[f"edit_equipment_{i}"] = True - - with col2: - if st.button(f"حذف المعدات {i+1}", key=f"delete_equipment_btn_{i}"): - st.session_state[f"delete_equipment_{i}"] = True + # إضافة المعدات المعدلة + for i, row in edited_df.iterrows(): + # حساب السعر الإجمالي + total_price = row['الكمية'] * row['سعر الوحدة'] - # نموذج التعديل - if st.session_state.get(f"edit_equipment_{i}", False): - with st.form(f"edit_equipment_form_{i}"): - equipment = current_item["equipment"][i] - col1, col2 = st.columns(2) - with col1: - equipment_name = st.text_input("نوع المعدات", value=equipment["name"]) - equipment_unit = st.text_input("الوحدة", value=equipment["unit"]) - with col2: - equipment_quantity = st.number_input("عدد الأيام", min_value=0.0, step=0.5, value=equipment["quantity"]) - equipment_price = st.number_input("التكلفة اليومية (ريال)", min_value=0.0, step=10.0, value=equipment["unit_price"]) - - col1, col2 = st.columns(2) - with col1: - if st.form_submit_button("حفظ التعديلات"): - equipment["name"] = equipment_name - equipment["unit"] = equipment_unit - equipment["quantity"] = float(equipment_quantity) - equipment["unit_price"] = float(equipment_price) - equipment["total_price"] = equipment["quantity"] * equipment["unit_price"] - st.session_state.item_analysis_edited = True - st.session_state[f"edit_equipment_{i}"] = False - st.rerun() - with col2: - if st.form_submit_button("إلغاء"): - st.session_state[f"edit_equipment_{i}"] = False - st.rerun() + # إضافة المعدة + item_analysis['equipment'].append({ + 'name': row['المعدة'], + 'unit': row['الوحدة'], + 'quantity': row['الكمية'], + 'unit_price': row['سعر الوحدة'], + 'total_price': total_price + }) + + # إعادة حساب إجمالي تكلفة المعدات + item_analysis['total_equipment_cost'] = sum(equipment_item['total_price'] for equipment_item in item_analysis['equipment']) + + # تعيين علامة التعديل + st.session_state.item_analysis_edited = True + st.rerun() + + # عرض إجمالي تكلفة المعدات + st.metric("إجمالي تكلفة المعدات", f"{item_analysis['total_equipment_cost']:,.2f} ريال") + + # أزرار التحكم + col1, col2 = st.columns(2) + with col1: + if st.button("إضافة معدة جديدة", key="add_equipment"): + item_analysis['equipment'].append({ + 'name': 'معدة جديدة', + 'unit': 'يوم', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }) + st.session_state.item_analysis_edited = True + st.rerun() + + def _render_labor_analysis(self, item_analysis): + """عرض تحليل العمالة مع إمكانية التعديل""" + st.subheader("تحليل العمالة") + + if not item_analysis['labor']: + st.info("لا توجد عمالة في تحليل هذا البند.") + + # إضافة زر لإضافة عمالة جديدة + if st.button("إضافة عمالة", key="add_first_labor"): + item_analysis['labor'] = [{ + 'name': 'عامل جديد', + 'unit': 'يوم', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }] + st.session_state.item_analysis_edited = True + st.rerun() + return + + # إنشاء DataFrame من قائمة العمالة + df = pd.DataFrame(item_analysis['labor']) + df.columns = ['العامل', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] + + # تحويل الجدول إلى جدول قابل للتعديل + edited_df = st.data_editor( + df, + use_container_width=True, + key="edit_labor_table", + column_config={ + "العامل": st.column_config.Column("العامل"), + "الوحدة": st.column_config.Column("الوحدة"), + "الكمية": st.column_config.NumberColumn( + "الكمية", + min_value=0.0, + format="%.2f", + step=0.1, + ), + "سعر الوحدة": st.column_config.NumberColumn( + "سعر الوحدة", + min_value=0.0, + format="%.2f ريال", + step=0.1, + ), + "السعر الإجمالي": st.column_config.NumberColumn( + "السعر الإجمالي", + format="%.2f ريال", + disabled=True, + ), + }, + num_rows="dynamic" + ) + + # تحديث البيانات في item_analysis بناءً على التعديلات + if edited_df is not None and not edited_df.equals(df): + # حذف جميع العمالة الحالية + item_analysis['labor'] = [] + + # إضافة العمالة المعدلة + for i, row in edited_df.iterrows(): + # حساب السعر الإجمالي + total_price = row['الكمية'] * row['سعر الوحدة'] - # تأكيد الحذف - if st.session_state.get(f"delete_equipment_{i}", False): - st.warning(f"هل أنت متأكد من حذف المعدات: {current_item['equipment'][i]['name']}؟") - col1, col2 = st.columns(2) - with col1: - if st.button("نعم، حذف", key=f"confirm_delete_equipment_{i}"): - current_item["equipment"].pop(i) - st.session_state.item_analysis_edited = True - st.session_state[f"delete_equipment_{i}"] = False - st.rerun() - with col2: - if st.button("إلغاء", key=f"cancel_delete_equipment_{i}"): - st.session_state[f"delete_equipment_{i}"] = False - st.rerun() - - # عرض المجموع الكلي للمعدات - st.markdown(f"
إجمالي تكلفة المعدات: {equipment_total:,.2f} ريال
", unsafe_allow_html=True) - else: - st.info("لا توجد معدات مضافة لهذا البند") + # إضافة العامل + item_analysis['labor'].append({ + 'name': row['العامل'], + 'unit': row['الوحدة'], + 'quantity': row['الكمية'], + 'unit_price': row['سعر الوحدة'], + 'total_price': total_price + }) + + # إعادة حساب إجمالي تكلفة العمالة + item_analysis['total_labor_cost'] = sum(labor_item['total_price'] for labor_item in item_analysis['labor']) + + # تعيين علامة التعديل + st.session_state.item_analysis_edited = True + st.rerun() + + # عرض إجمالي تكلفة العمالة + st.metric("إجمالي تكلفة العمالة", f"{item_analysis['total_labor_cost']:,.2f} ريال") - # تحليل المقاولين من الباطن + # أزرار التحكم + col1, col2 = st.columns(2) + with col1: + if st.button("إضافة عامل جديد", key="add_labor"): + item_analysis['labor'].append({ + 'name': 'عامل جديد', + 'unit': 'يوم', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }) + st.session_state.item_analysis_edited = True + st.rerun() + + def _render_subcontractors_analysis(self, item_analysis): + """عرض تحليل المقاولين من الباطن مع إمكانية التعديل""" st.subheader("تحليل المقاولين من الباطن") - # إضافة مقاول جديد - with st.expander("إضافة مقاول من الباطن"): - with st.form("add_subcontractor_form"): - col1, col2 = st.columns(2) - with col1: - new_sub_name = st.text_input("اسم المقاول") - new_sub_work = st.text_input("نوع العمل") - with col2: - new_sub_price = st.number_input("التكلفة الإجمالية (ريال)", min_value=0.0, step=100.0) - - submitted = st.form_submit_button("إضافة مقاول") - if submitted and new_sub_name and new_sub_work: - new_sub = { - "id": f"sub_{len(current_item['subcontractors']) + 1}", - "name": new_sub_name, - "work": new_sub_work, - "quantity": 1, - "unit_price": float(new_sub_price), - "total_price": float(new_sub_price) - } - current_item["subcontractors"].append(new_sub) - st.session_state.item_analysis_edited = True - st.success("تمت إضافة المقاول بنجاح") - st.rerun() + # التحقق من وجود مفتاح المقاولين من الباطن في التحليل + 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"): + item_analysis['subcontractors'] = [{ + 'name': 'مقاول جديد', + 'unit': 'عقد', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }] + st.session_state.item_analysis_edited = True + st.rerun() + return + + # إنشاء DataFrame من قائمة المقاولين + df = pd.DataFrame(item_analysis['subcontractors']) + df.columns = ['المقاول', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] + + # تحويل الجدول إلى جدول قابل للتعديل + edited_df = st.data_editor( + df, + use_container_width=True, + key="edit_subcontractors_table", + column_config={ + "المقاول": st.column_config.Column("المقاول"), + "الوحدة": st.column_config.Column("الوحدة"), + "الكمية": st.column_config.NumberColumn( + "الكمية", + min_value=0.0, + format="%.2f", + step=0.1, + ), + "سعر الوحدة": st.column_config.NumberColumn( + "سعر الوحدة", + min_value=0.0, + format="%.2f ريال", + step=0.1, + ), + "السعر الإجمالي": st.column_config.NumberColumn( + "السعر الإجمالي", + format="%.2f ريال", + disabled=True, + ), + }, + num_rows="dynamic" + ) - # عرض جدول المقاولين - if current_item["subcontractors"]: - # تحويل البيانات إلى DataFrame - subs_data = [] - for i, sub in enumerate(current_item["subcontractors"]): - subs_data.append({ - "م": i + 1, - "اسم المقاول": sub["name"], - "نوع العمل": sub["work"], - "التكلفة الإجمالية (ريال)": sub["total_price"], - "الإجراءات": f' ' + # تحديث البيانات في item_analysis بناءً على التعديلات + if edited_df is not None and not edited_df.equals(df): + # حذف جميع المقاولين الحاليين + item_analysis['subcontractors'] = [] + + # إضافة المقاولين المعدلين + for i, row in edited_df.iterrows(): + # حساب السعر الإجمالي + total_price = row['الكمية'] * row['سعر الوحدة'] + + # إضافة المقاول + item_analysis['subcontractors'].append({ + 'name': row['المقاول'], + 'unit': row['الوحدة'], + 'quantity': row['الكمية'], + 'unit_price': row['سعر الوحدة'], + 'total_price': total_price }) - subs_df = pd.DataFrame(subs_data) - - # إضافة صف المجموع - subs_total = sum(sub["total_price"] for sub in current_item["subcontractors"]) - subs_df = subs_df.append({ - "م": "", - "اسم المقاول": "المجموع", - "نوع العمل": "", - "التكلفة الإجمالية (ريال)": subs_total, - "الإجراءات": "" - }, ignore_index=True) - - # عرض الجدول القابل للتعديل - edited_subs_df = st.data_editor( - subs_df, - column_config={ - "م": st.column_config.NumberColumn("م", width="small"), - "اسم المقاول": st.column_config.TextColumn("اسم المقاول"), - "نوع العمل": st.column_config.TextColumn("نوع العمل"), - "التكلفة الإجمالية (ريال)": st.column_config.NumberColumn("التكلفة الإجمالية (ريال)", format="%.2f"), - "الإجراءات": st.column_config.TextColumn("الإجراءات", width="medium"), - }, - hide_index=True, - key="subs_table", - on_change=self._update_total_price, - disabled=["م", "اسم المقاول", "نوع العمل", "الإجراءات"] + # إعادة حساب إجمالي تكلفة المقاولين + item_analysis['total_subcontractors_cost'] = sum(subcontractor['total_price'] for subcontractor in item_analysis['subcontractors']) + + # تعيين علامة التعديل + st.session_state.item_analysis_edited = True + st.rerun() + + # عرض إجمالي تكلفة المقاولين + st.metric("إجمالي تكلفة المقاولين من الباطن", f"{item_analysis['total_subcontractors_cost']:,.2f} ريال") + + # أزرار التحكم + col1, col2 = st.columns(2) + with col1: + if st.button("إضافة مقاول جديد", key="add_subcontractor"): + item_analysis['subcontractors'].append({ + 'name': 'مقاول جديد', + 'unit': 'عقد', + 'quantity': 1.0, + 'unit_price': 0.0, + 'total_price': 0.0 + }) + st.session_state.item_analysis_edited = True + st.rerun() + + def _setup_arabic_fonts(self): + """إعداد الخطوط العربية للرسوم البيانية""" + plt.rcParams['font.family'] = 'Arial' + plt.rcParams['axes.unicode_minus'] = False + + def _render_cost_summary(self, item_analysis): + """عرض ملخص التكلفة""" + st.subheader("ملخص التكلفة") + + # إعداد الخطوط العربية + self._setup_arabic_fonts() + + # إنشاء بيانات الرسم البياني + # استخدام arabic_reshaper و bidi لمعالجة النص العربي + labels_ar = ['المواد', 'المعدات', 'العمالة', 'المقاولين من الباطن', 'المصاريف العمومية', 'الربح'] + labels = [get_display(arabic_reshaper.reshape(label)) for label in labels_ar] + + # التحقق من وجود قيم NaN واستبدالها بأصفار + values = [ + item_analysis['total_materials_cost'] if not np.isnan(item_analysis['total_materials_cost']) else 0, + item_analysis['total_equipment_cost'] if not np.isnan(item_analysis['total_equipment_cost']) else 0, + item_analysis['total_labor_cost'] if not np.isnan(item_analysis['total_labor_cost']) else 0, + item_analysis['total_subcontractors_cost'] if not np.isnan(item_analysis.get('total_subcontractors_cost', 0)) else 0, + item_analysis['overhead_cost'] if not np.isnan(item_analysis['overhead_cost']) else 0, + item_analysis['profit'] if not np.isnan(item_analysis['profit']) else 0 + ] + + # التحقق من أن جميع القيم موجبة (أو صفر) لتجنب الأخطاء في الرسم البياني + values = [max(0, val) for val in values] + + # إنشاء الرسم البياني + fig, ax = plt.subplots(figsize=(10, 6)) + + # رسم المخطط الدائري + wedges, texts, autotexts = ax.pie( + values, + labels=labels, + autopct='%1.1f%%', + startangle=90, + shadow=True, + ) + + # تعديل حجم النص + plt.setp(autotexts, size=10, weight="bold") + + # إضافة العنوان + title_ar = 'توزيع تكلفة البند' + title = get_display(arabic_reshaper.reshape(title_ar)) + ax.set_title(title, fontsize=16) + + # عرض الرسم البياني + st.pyplot(fig) + + # عرض جدول ملخص التكلفة + 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("تعديل المصاريف العمومية والربح") + + 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" ) - # تحديث البيانات بعد التعديل - for i, sub in enumerate(current_item["subcontractors"]): - if i < len(edited_subs_df) - 1: # تجاهل صف المجموع - sub["total_price"] = edited_subs_df.iloc[i]["التكلفة الإجمالية (ريال)"] - sub["unit_price"] = sub["total_price"] # للمقاولين، سعر الوحدة = السعر الإجمالي + if new_overhead != item_analysis['overhead_cost']: + item_analysis['overhead_cost'] = new_overhead + + # إعادة حساب الربح + total_cost = ( + item_analysis['total_materials_cost'] + + item_analysis['total_equipment_cost'] + + item_analysis['total_labor_cost'] + + item_analysis.get('total_subcontractors_cost', 0) + + item_analysis['overhead_cost'] + ) + + item_analysis['profit'] = item_analysis['unit_price'] - total_cost + st.session_state.item_analysis_edited = True + st.rerun() + + with col2: + new_profit = st.number_input( + "الربح (ريال)", + min_value=float(-1000000.0), + value=float(item_analysis['profit']), + step=10.0, + format="%.2f", + key="edit_profit" + ) - # معالجة أزرار التعديل والحذف - for i in range(len(current_item["subcontractors"])): - col1, col2 = st.columns([1, 1]) - with col1: - if st.button(f"تعديل المقاول {i+1}", key=f"edit_sub_btn_{i}"): - st.session_state[f"edit_sub_{i}"] = True + if new_profit != item_analysis['profit']: + item_analysis['profit'] = new_profit - with col2: - if st.button(f"حذف المقاول {i+1}", key=f"delete_sub_btn_{i}"): - st.session_state[f"delete_sub_{i}"] = True + # إعادة حساب سعر الوحدة + 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.get(f"edit_sub_{i}", False): - with st.form(f"edit_sub_form_{i}"): - sub = current_item["subcontractors"][i] - col1, col2 = st.columns(2) - with col1: - sub_name = st.text_input("اسم المقاول", value=sub["name"]) - sub_work = st.text_input("نوع العمل", value=sub["work"]) - with col2: - sub_price = st.number_input("التكلفة الإجمالية (ريال)", min_value=0.0, step=100.0, value=sub["total_price"]) - - col1, col2 = st.columns(2) - with col1: - if st.form_submit_button("حفظ التعديلات"): - sub["name"] = sub_name - sub["work"] = sub_work - sub["total_price"] = float(sub_price) - sub["unit_price"] = sub["total_price"] - st.session_state.item_analysis_edited = True - st.session_state[f"edit_sub_{i}"] = False - st.rerun() - with col2: - if st.form_submit_button("إلغاء"): - st.session_state[f"edit_sub_{i}"] = False - st.rerun() + item_analysis['unit_price'] = total_cost + item_analysis['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'] - # تأكيد الحذف - if st.session_state.get(f"delete_sub_{i}", False): - st.warning(f"هل أنت متأكد من حذف المقاول: {current_item['subcontractors'][i]['name']}؟") - col1, col2 = st.columns(2) - with col1: - if st.button("نعم، حذف", key=f"confirm_delete_sub_{i}"): - current_item["subcontractors"].pop(i) - st.session_state.item_analysis_edited = True - st.session_state[f"delete_sub_{i}"] = False - st.rerun() - with col2: - if st.button("إلغاء", key=f"cancel_delete_sub_{i}"): - st.session_state[f"delete_sub_{i}"] = False - st.rerun() - - # عرض المجموع الكلي للمقاولين - st.markdown(f"
إجمالي تكلفة المقاولين من الباطن: {subs_total:,.2f} ريال
", unsafe_allow_html=True) - else: - st.info("لا يوجد مقاولون من الباطن مضافون لهذا البند") + # تحديث سعر الوحدة + 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("تحليل التكلفة") - # ملخص التكلفة - 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) - # حساب التكاليف - materials_total = sum(material.get("total_price", 0) for material in current_item["materials"]) - labor_total = sum(labor.get("total_price", 0) for labor in current_item["labor"]) - equipment_total = sum(equipment.get("total_price", 0) for equipment in current_item["equipment"]) - subcontractors_total = sum(sub.get("total_price", 0) for sub in current_item["subcontractors"]) + # عرض إجمالي التكلفة + st.metric("إجمالي تكلفة المشروع", f"{total_cost:,.2f} ريال") - direct_cost = materials_total + labor_total + equipment_total + subcontractors_total - overhead_profit = max(0, current_item["total_price"] - direct_cost) # تجنب القيم السالبة + # إنشاء DataFrame من بنود المشروع + df = pd.DataFrame(project_items) + df = df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price']] + df.columns = ['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] - # عرض ملخص التكلفة - st.markdown("
", unsafe_allow_html=True) + # تنسيق الأسعار + df['سعر الوحدة'] = df['سعر الوحدة'].apply(lambda x: f"{x:,.2f} ريال") + df['السعر الإجمالي'] = df['السعر الإجمالي'].apply(lambda x: f"{x:,.2f} ريال") - col1, col2 = st.columns(2) - with col1: - st.markdown("

تكلفة المواد:

", unsafe_allow_html=True) - st.markdown("

تكلفة العمالة:

", unsafe_allow_html=True) - st.markdown("

تكلفة المعدات:

", unsafe_allow_html=True) - st.markdown("

تكلفة المقاولين من الباطن:

", unsafe_allow_html=True) - st.markdown("

إجمالي التكلفة المباشرة:

", unsafe_allow_html=True) - st.markdown("

المصاريف العمومية والربح:

", unsafe_allow_html=True) - st.markdown("

إجمالي سعر البند:

", unsafe_allow_html=True) + # عرض الجدول + st.dataframe(df, use_container_width=True) + + # إنشاء رسم بياني للتكلفة + self._setup_arabic_fonts() + + # إنشاء بيانات الرسم البياني + fig, ax = plt.subplots(figsize=(10, 6)) + + # تحويل الأسعار إلى أرقام + prices = [item['total_price'] for item in project_items] + labels = [item['description'] for item in project_items] + + # تحويل النص العربي + labels = [get_display(arabic_reshaper.reshape(label)) for label in labels] + + # رسم المخطط الشريطي + bars = ax.bar(labels, prices) + + # تدوير التسميات + plt.xticks(rotation=45, ha='right') + + # إضافة العنوان + title_ar = 'توزيع تكلفة المشروع حسب البنود' + title = get_display(arabic_reshaper.reshape(title_ar)) + ax.set_title(title, fontsize=16) + + # إضافة التسميات + ax.set_ylabel(get_display(arabic_reshaper.reshape('التكلفة (ريال)'))) + + # تنسيق الرسم البياني + plt.tight_layout() + + # عرض الرسم البياني + st.pyplot(fig) + + def _render_profit_margin(self, project_info): + """عرض تحليل الربحية""" + st.subheader("تحليل الربحية") + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + + if not project_items: + st.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.markdown(f"

{materials_total:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

{labor_total:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

{equipment_total:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

{subcontractors_total:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

{direct_cost:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

{overhead_profit:,.2f} ريال

", unsafe_allow_html=True) - st.markdown(f"

{current_item['total_price']:,.2f} ريال

", unsafe_allow_html=True) - - st.markdown("
", unsafe_allow_html=True) - - # رسم بياني للتكاليف - if direct_cost > 0: - st.subheader("توزيع التكاليف") - - # إعداد البيانات للرسم البياني - labels = ['المواد', 'العمالة', 'المعدات', 'المقاولين', 'الربح'] - sizes = [materials_total, labor_total, equipment_total, subcontractors_total, overhead_profit] - colors = ['#ff9999', '#66b3ff', '#99ff99', '#ffcc99', '#c2c2f0'] - - # رسم الرسم البياني - fig, ax = plt.subplots(figsize=(10, 6)) - wedges, texts, autotexts = ax.pie( - sizes, - labels=[get_display(arabic_reshaper.reshape(label)) for label in labels], - autopct='%1.1f%%', - startangle=90, - colors=colors - ) + st.metric("هامش الربح", f"{profit_margin:.1%}") + with col3: + st.metric("إجمالي مع الربح", f"{total_with_profit:,.2f} ريال") - # تنسيق النص - for text in texts: - text.set_fontsize(12) - for autotext in autotexts: - autotext.set_fontsize(10) - autotext.set_color('white') + # إضافة شريط تمرير لتعديل هامش الربح + 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 - ax.axis('equal') - plt.title(get_display(arabic_reshaper.reshape('توزيع تكاليف البند')), fontsize=16) + st.metric("إجمالي مع الربح الجديد", f"{total_with_profit:,.2f} ريال") - st.pyplot(fig) - - def _load_sample_projects(self): - """تحميل مشاريع نموذجية""" - return { - "proj_1": { - "id": "proj_1", - "name": "مشروع إنشاء مبنى سكني", - "client": "شركة الإعمار", - "date": "2025-06-24", - "budget": 901000000.00, - "items": [ - { - "id": "item_1", - "name": "أعمال الحفر", - "unit": "م3", - "quantity": 1500.0, - "unit_price": 35.0, - "total_price": 52500.0, - "materials": [ - { - "id": "material_1", - "name": "رمل", - "unit": "م3", - "quantity": 50.0, - "unit_price": 120.0, - "total_price": 6000.0 - } - ], - "labor": [ - { - "id": "labor_1", - "name": "عامل حفر", - "unit": "يوم", - "quantity": 30.0, - "unit_price": 150.0, - "total_price": 4500.0 - } - ], - "equipment": [ - { - "id": "equipment_1", - "name": "حفارة", - "unit": "يوم", - "quantity": 15.0, - "unit_price": 1200.0, - "total_price": 18000.0 - } - ], - "subcontractors": [] - }, - { - "id": "item_2", - "name": "أعمال الخرسانة", - "unit": "م3", - "quantity": 800.0, - "unit_price": 450.0, - "total_price": 360000.0, - "materials": [ - { - "id": "material_1", - "name": "أسمنت", - "unit": "طن", - "quantity": 120.0, - "unit_price": 600.0, - "total_price": 72000.0 - }, - { - "id": "material_2", - "name": "رمل", - "unit": "م3", - "quantity": 400.0, - "unit_price": 120.0, - "total_price": 48000.0 - }, - { - "id": "material_3", - "name": "حصى", - "unit": "م3", - "quantity": 600.0, - "unit_price": 150.0, - "total_price": 90000.0 - } - ], - "labor": [ - { - "id": "labor_1", - "name": "عامل خرسانة", - "unit": "يوم", - "quantity": 200.0, - "unit_price": 200.0, - "total_price": 40000.0 - }, - { - "id": "labor_2", - "name": "فني خرسانة", - "unit": "يوم", - "quantity": 50.0, - "unit_price": 350.0, - "total_price": 17500.0 - } - ], - "equipment": [ - { - "id": "equipment_1", - "name": "خلاطة خرسانة", - "unit": "يوم", - "quantity": 40.0, - "unit_price": 800.0, - "total_price": 32000.0 - }, - { - "id": "equipment_2", - "name": "مضخة خرسانة", - "unit": "يوم", - "quantity": 20.0, - "unit_price": 1500.0, - "total_price": 30000.0 - } - ], - "subcontractors": [] - }, - { - "id": "item_3", - "name": "أعمال البناء", - "unit": "م2", - "quantity": 2000.0, - "unit_price": 220.0, - "total_price": 440000.0, - "materials": [], - "labor": [], - "equipment": [], - "subcontractors": [ - { - "id": "sub_1", - "name": "مقاول بناء", - "work": "بناء جدران", - "quantity": 1, - "unit_price": 250000.0, - "total_price": 250000.0 - } - ] - } - ] - } - } - -# تشغيل التطبيق عند تنفيذ الملف مباشرة + # إنشاء رسم بياني للربحية + self._setup_arabic_fonts() + + # إنشاء بيانات الرسم البياني + fig, ax = plt.subplots(figsize=(10, 6)) + + # إنشاء المخطط الدائري + labels_ar = ['التكلفة', 'الربح'] + labels = [get_display(arabic_reshaper.reshape(label)) for label in labels_ar] + + values = [total_cost, profit] + + # رسم المخطط الدائري + wedges, texts, autotexts = ax.pie( + values, + labels=labels, + autopct='%1.1f%%', + startangle=90, + shadow=True, + colors=['#ff9999', '#66b3ff'] + ) + + # تعديل حجم النص + plt.setp(autotexts, size=10, weight="bold") + + # إضافة العنوان + title_ar = 'توزيع التكلفة والربح' + title = get_display(arabic_reshaper.reshape(title_ar)) + ax.set_title(title, fontsize=16) + + # عرض الرسم البياني + st.pyplot(fig) + + def _render_pricing_strategies(self, project_info): + """عرض استراتيجيات التسعير""" + st.subheader("استراتيجيات التسعير") + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + + if not project_items: + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") + return + + # عرض استراتيجيات التسعير المختلفة + 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("تم حفظ التسعير بنجاح") + +# تشغيل التطبيق if __name__ == "__main__": app = PricingApp() app.run()