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,185 +1,155 @@ import streamlit as st import pandas as pd import numpy as np +import matplotlib.pyplot as plt +import matplotlib.font_manager as fm import io import base64 -import datetime +from datetime import datetime +import random import json -import os -from PIL import Image -import matplotlib.pyplot as plt -import seaborn as sns class PricingApp: + """وحدة التسعير المتكاملة""" + def __init__(self): - """ - تهيئة وحدة التسعير المحسنة - """ - self.initialize_session_state() - - def initialize_session_state(self): - """ - تهيئة متغيرات حالة الجلسة - """ + """تهيئة وحدة التسعير""" + # تهيئة البيانات في حالة الجلسة إذا لم تكن موجودة if 'projects' not in st.session_state: st.session_state.projects = [ { - "id": 1, - "name": "مشروع تطوير البنية التحتية", - "client": "وزارة الإسكان", - "estimated_value": 5000000, - "deadline": "2023-12-31", - "status": "قيد التنفيذ", - "completion": 35, - "profit_margin": 15 + '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": 8500000, - "deadline": "2024-06-30", - "status": "قيد التخطيط", - "completion": 0, - "profit_margin": 18 + '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 = None + st.session_state.current_project = 1 - if 'bill_of_quantities' not in st.session_state: - st.session_state.bill_of_quantities = [ - { - "project_id": 1, - "items": [ - {"id": 1, "description": "حفر أساسات", "unit": "م³", "quantity": 250, "unit_price": 120, "total_price": 30000}, - {"id": 2, "description": "صب خرسانة", "unit": "م³", "quantity": 180, "unit_price": 350, "total_price": 63000}, - {"id": 3, "description": "توريد وتركيب حديد تسليح", "unit": "طن", "quantity": 25, "unit_price": 5000, "total_price": 125000} - ] - }, - { - "project_id": 2, - "items": [ - {"id": 1, "description": "أعمال حفر", "unit": "م³", "quantity": 500, "unit_price": 100, "total_price": 50000}, - {"id": 2, "description": "أعمال خرسانة", "unit": "م³", "quantity": 300, "unit_price": 400, "total_price": 120000} - ] - } - ] + if 'next_project_id' not in st.session_state: + st.session_state.next_project_id = len(st.session_state.projects) + 1 - if 'materials_catalog' not in st.session_state: - st.session_state.materials_catalog = [ - {"id": 1, "name": "أسمنت بورتلاندي", "unit": "طن", "price": 400, "category": "مواد بناء أساسية"}, - {"id": 2, "name": "حديد تسليح 8 مم", "unit": "طن", "price": 4800, "category": "حديد تسليح"}, - {"id": 3, "name": "حديد تسليح 10 مم", "unit": "طن", "price": 4700, "category": "حديد تسليح"}, - {"id": 4, "name": "حديد تسليح 12 مم", "unit": "طن", "price": 4600, "category": "حديد تسليح"}, - {"id": 5, "name": "رمل", "unit": "م³", "price": 80, "category": "مواد بناء أساسية"}, - {"id": 6, "name": "زلط", "unit": "م³", "price": 150, "category": "مواد بناء أساسية"}, - {"id": 7, "name": "طوب أحمر", "unit": "1000 طوبة", "price": 800, "category": "مواد بناء أساسية"}, - {"id": 8, "name": "طوب أسمنتي", "unit": "1000 طوبة", "price": 1200, "category": "مواد بناء أساسية"}, - {"id": 9, "name": "خشب", "unit": "م³", "price": 3500, "category": "مواد نجارة"}, - {"id": 10, "name": "دهان بلاستيك", "unit": "برميل", "price": 500, "category": "دهانات"} - ] + if 'show_new_project_form' not in st.session_state: + st.session_state.show_new_project_form = False - if 'equipment_catalog' not in st.session_state: - st.session_state.equipment_catalog = [ - {"id": 1, "name": "حفار", "daily_rate": 1500, "category": "معدات حفر"}, - {"id": 2, "name": "لودر", "daily_rate": 1200, "category": "معدات حفر"}, - {"id": 3, "name": "خلاطة خرسانة", "daily_rate": 800, "category": "معدات خرسانة"}, - {"id": 4, "name": "هزاز خرسانة", "daily_rate": 200, "category": "معدات خرسانة"}, - {"id": 5, "name": "شاحنة نقل", "daily_rate": 1000, "category": "معدات نقل"}, - {"id": 6, "name": "رافعة برجية", "daily_rate": 2500, "category": "معدات رفع"}, - {"id": 7, "name": "مضخة خرسانة", "daily_rate": 1800, "category": "معدات خرسانة"}, - {"id": 8, "name": "مولد كهرباء", "daily_rate": 600, "category": "معدات كهربائية"}, - {"id": 9, "name": "كمبروسر هواء", "daily_rate": 400, "category": "معدات متنوعة"}, - {"id": 10, "name": "معدات يدوية", "daily_rate": 200, "category": "معدات متنوعة"} - ] + if 'show_edit_project_form' not in st.session_state: + st.session_state.show_edit_project_form = False - if 'labor_rates' not in st.session_state: - st.session_state.labor_rates = [ - {"id": 1, "role": "عامل عادي", "daily_rate": 150, "category": "عمالة عادية"}, - {"id": 2, "role": "نجار", "daily_rate": 250, "category": "عمالة ماهرة"}, - {"id": 3, "role": "حداد", "daily_rate": 250, "category": "عمالة ماهرة"}, - {"id": 4, "role": "مبيض محارة", "daily_rate": 230, "category": "عمالة ماهرة"}, - {"id": 5, "role": "سباك", "daily_rate": 270, "category": "عمالة ماهرة"}, - {"id": 6, "role": "كهربائي", "daily_rate": 280, "category": "عمالة ماهرة"}, - {"id": 7, "role": "مشغل معدات", "daily_rate": 300, "category": "عمالة ماهرة"}, - {"id": 8, "role": "مهندس موقع", "daily_rate": 600, "category": "مهندسين"}, - {"id": 9, "role": "مهندس مدني", "daily_rate": 700, "category": "مهندسين"}, - {"id": 10, "role": "مدير مشروع", "daily_rate": 1000, "category": "إدارة"} - ] + if 'edit_project_id' not in st.session_state: + st.session_state.edit_project_id = None - if 'item_analysis' not in st.session_state: - st.session_state.item_analysis = [ + if 'boq_items' not in st.session_state: + st.session_state.boq_items = [ { - "project_id": 1, - "item_id": 1, - "materials": [ - {"material_id": 5, "quantity": 0.2, "cost": 16}, - ], - "equipment": [ - {"equipment_id": 1, "days": 0.05, "cost": 75}, - {"equipment_id": 2, "days": 0.05, "cost": 60} - ], - "labor": [ - {"labor_id": 1, "days": 0.1, "cost": 15}, - {"labor_id": 7, "days": 0.05, "cost": 15} - ], - "overhead": 10, - "profit": 15, - "total_cost": 120 + 'id': 1, + 'project_id': 1, + 'code': 'A-001', + 'description': 'أعمال الحفر والردم', + 'unit': 'م3', + 'quantity': 1500, + 'unit_price': 45, + 'total_price': 67500, + 'resource_type': 'مواد' }, { - "project_id": 1, - "item_id": 2, - "materials": [ - {"material_id": 1, "quantity": 0.3, "cost": 120}, - {"material_id": 5, "quantity": 0.4, "cost": 32}, - {"material_id": 6, "quantity": 0.8, "cost": 120} - ], - "equipment": [ - {"equipment_id": 3, "days": 0.05, "cost": 40}, - {"equipment_id": 4, "days": 0.05, "cost": 10}, - {"equipment_id": 7, "days": 0.02, "cost": 36} - ], - "labor": [ - {"labor_id": 1, "days": 0.2, "cost": 30}, - {"labor_id": 7, "days": 0.05, "cost": 15} - ], - "overhead": 12, - "profit": 15, - "total_cost": 350 + '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 'show_new_project_form' not in st.session_state: - st.session_state.show_new_project_form = False + 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_item_analysis' not in st.session_state: - st.session_state.show_item_analysis = False + if 'show_edit_boq_item_form' not in st.session_state: + st.session_state.show_edit_boq_item_form = False - if 'current_item' not in st.session_state: - st.session_state.current_item = None + if 'edit_boq_item_id' not in st.session_state: + st.session_state.edit_boq_item_id = None - if 'show_add_item_form' not in st.session_state: - st.session_state.show_add_item_form = False + 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': {} # معاملات مخصصة لبنود محددة + } def render(self): - """ - طريقة للتوافق مع الواجهة القديمة - تقوم باستدعاء طريقة run - """ + """طريقة للتوافق مع الواجهة القديمة""" self.run() def run(self): - """ - تشغيل وحدة التسعير المحسنة - """ + """تشغيل وحدة التسعير""" st.title("وحدة التسعير المتكاملة") - # إضافة زر إنشاء تسعير جديد في أعلى الصفحة + # عرض زر إنشاء تسعير جديد col1, col2, col3 = st.columns([1, 2, 1]) - with col1: + with col2: if st.button("➕ إنشاء تسعير جديد", key="create_new_pricing_btn", type="primary"): st.session_state.show_new_project_form = True @@ -187,36 +157,57 @@ class PricingApp: if st.session_state.show_new_project_form: self._render_new_project_form() + # عرض نموذج تعديل المشروع + if st.session_state.show_edit_project_form and st.session_state.edit_project_id is not None: + self._render_edit_project_form() + # عرض قائمة المشاريع self._render_projects_list() - # عرض تفاصيل المشروع المحدد + # عرض تفاصيل المشروع الحالي if st.session_state.current_project: - project_info = next((p for p in st.session_state.projects if p["id"] == st.session_state.current_project), None) + project_info = self._get_current_project_info() if project_info: - self._render_project_details(project_info) - - # عرض تحليل الربحية - self._render_profit_margin(project_info) + self._render_project_info(project_info) - # عرض جدول الكميات - self._render_bill_of_quantities() + # عرض علامات التبويب + tab1, tab2, tab3, tab4, tab5 = st.tabs([ + "جدول الكميات", + "تحليل سعر البند", + "تحليل التكلفة", + "تحليل الربحية", + "استراتيجيات التسعير" + ]) - # عرض تحليل سعر البند - if st.session_state.show_item_analysis and st.session_state.current_item: + 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) + def _render_new_project_form(self): - """ - عرض نموذج إنشاء مشروع جديد - """ + """عرض نموذج إنشاء مشروع جديد""" st.subheader("إنشاء تسعير جديد") with st.form(key="new_project_form"): - project_name = st.text_input("اسم المشروع", key="new_project_name") + name = st.text_input("اسم المشروع", key="new_project_name") client = st.text_input("العميل", key="new_project_client") - estimated_value = st.number_input("القيمة التقديرية", min_value=0, key="new_project_value") + 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: @@ -224,711 +215,1345 @@ class PricingApp: with col2: cancel_button = st.form_submit_button("إلغاء") - if submit_button and project_name: - # إنشاء مشروع جديد - new_project_id = max([p["id"] for p in st.session_state.projects], default=0) + 1 - new_project = { - "id": new_project_id, - "name": project_name, - "client": client, - "estimated_value": estimated_value, - "deadline": deadline.strftime("%Y-%m-%d"), - "status": "قيد التخطيط", - "completion": 0, - "profit_margin": 15 - } - - # إضافة المشروع الجديد - st.session_state.projects.append(new_project) - - # إنشاء جدول كميات فارغ للمشروع الجديد - st.session_state.bill_of_quantities.append({ - "project_id": new_project_id, - "items": [] - }) - - # تحديد المشروع الحالي - st.session_state.current_project = new_project_id - - # إخفاء النموذج + 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_new_project_form = False + 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("قائمة المشاريع") - # إنشاء جدول المشاريع - projects_df = pd.DataFrame(st.session_state.projects) - - # تنسيق الجدول - if not projects_df.empty: - projects_df = projects_df[["id", "name", "client", "estimated_value", "deadline", "status", "completion"]] - projects_df.columns = ["الرقم", "اسم المشروع", "العميل", "القيمة التقديرية", "الموعد النهائي", "الحالة", "نسبة الإنجاز %"] + if not st.session_state.projects: + st.info("لا توجد مشاريع. قم بإنشاء مشروع جديد للبدء.") + return - # عرض الجدول - st.dataframe(projects_df, use_container_width=True) - - # اختيار مشروع - selected_project = st.selectbox( - "اختر مشروع للتسعير", - options=st.session_state.projects, - format_func=lambda x: x["name"], - key="project_selector" + # إنشاء DataFrame من قائمة المشاريع + df = pd.DataFrame(st.session_state.projects) + 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: + selected_project_id = st.selectbox( + "اختر المشروع", + options=[p['id'] for p in st.session_state.projects], + format_func=lambda x: next((p['name'] for p in st.session_state.projects if p['id'] == x), ""), + index=[p['id'] for p in st.session_state.projects].index(st.session_state.current_project) if st.session_state.current_project in [p['id'] for p in st.session_state.projects] else 0, + key="select_project" ) - if selected_project: - st.session_state.current_project = selected_project["id"] - else: - st.info("لا توجد مشاريع. قم بإنشاء مشروع جديد.") - - def _render_project_details(self, project_info): - """ - عرض تفاصيل المشروع - """ - st.subheader(f"تفاصيل المشروع: {project_info['name']}") + 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() + + def _get_current_project_info(self): + """الحصول على معلومات المشروع الحالي""" + for project in st.session_state.projects: + if project['id'] == st.session_state.current_project: + return project + return None - col1, col2, col3 = st.columns(3) + def _render_project_info(self, project): + """عرض معلومات المشروع""" + st.header(f"تسعير: {project['name']}") + + col1, col2, col3, col4 = st.columns(4) with col1: - st.metric("العميل", project_info["client"]) - st.metric("الحالة", project_info["status"]) + st.metric("العميل", project['client']) with col2: - st.metric("القيمة التقديرية", f"{project_info['estimated_value']:,} ريال") - st.metric("نسبة الإنجاز", f"{project_info['completion']}%") + st.metric("القيمة التقديرية", f"{project['estimated_value']:,.2f} ريال") with col3: - st.metric("الموعد النهائي", project_info["deadline"]) - st.metric("هامش الربح", f"{project_info['profit_margin']}%") + st.metric("الموعد النهائي", project['deadline']) + with col4: + st.metric("نوع التسعير", project['pricing_type']) - def _render_profit_margin(self, project_info): - """ - عرض تحليل الربحية - """ - st.subheader("تحليل الربحية") - - col1, col2 = st.columns([2, 3]) + def _render_bill_of_quantities(self): + """عرض جدول الكميات""" + st.subheader("جدول الكميات") + # زر إضافة بند جديد + col1, col2, col3 = st.columns([1, 1, 2]) with col1: - # تحليل حساسية هامش الربح - profit_margin = project_info["profit_margin"] - - # إنشاء بيانات تحليل الحساسية - sensitivity_data = [] - for adjustment in [-5, -2, 0, 2, 5, 10]: - adjusted_margin = profit_margin + adjustment - if adjusted_margin < 0: - continue - - project_value = project_info["estimated_value"] - cost = project_value / (1 + adjusted_margin/100) - profit = project_value - cost + if st.button("➕ إضافة بند جديد", key="add_boq_item_btn"): + st.session_state.show_new_boq_item_form = True + st.session_state.show_resource_selector = False - sensitivity_data.append({ - "نسبة الربح المعدلة": adjusted_margin, - "التكلفة": cost, - "الربح": profit, - "قيمة المشروع": project_value - }) - - # إنشاء DataFrame - sensitivity_df = pd.DataFrame(sensitivity_data) - - # تنسيق الأعمدة - formatted_df = sensitivity_df.copy() - formatted_df["نسبة الربح المعدلة"] = formatted_df["نسبة الربح المعدلة"].apply(lambda x: f"{x}%") - formatted_df["التكلفة"] = formatted_df["التكلفة"].apply(lambda x: f"{x:,.2f} ريال") - formatted_df["الربح"] = formatted_df["الربح"].apply(lambda x: f"{x:,.2f} ريال") - formatted_df["قيمة المشروع"] = formatted_df["قيمة المشروع"].apply(lambda x: f"{x:,.2f} ريال") - - # عرض الجدول - st.dataframe(formatted_df, use_container_width=True) - with col2: - # رسم بياني لتحليل الربحية - fig, ax = plt.subplots(figsize=(10, 6)) - - # إعداد البيانات للرسم البياني - margins = [row["نسبة الربح المعدلة"] for row in sensitivity_data] - profits = [row["الربح"] for row in sensitivity_data] - costs = [row["التكلفة"] for row in sensitivity_data] - - # رسم الأعمدة - x = np.arange(len(margins)) - width = 0.35 + 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() - ax.bar(x - width/2, costs, width, label='التكلفة', color='#3498db') - ax.bar(x + width/2, profits, width, label='الربح', color='#2ecc71') + # عرض نموذج تعديل البند + 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() - # إضافة التسميات - ax.set_xlabel('نسبة الربح %') - ax.set_ylabel('القيمة (ريال)') - ax.set_title('تحليل الربحية حسب نسبة الربح') - ax.set_xticks(x) - ax.set_xticklabels([f"{m}%" for m in margins]) - ax.legend() + # عرض محدد الموارد + if st.session_state.show_resource_selector: + self._render_resource_selector() - # عرض الرسم البياني - st.pyplot(fig) + # الحصول على بنود المشروع الحالي + 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 - def _render_bill_of_quantities(self): - """ - عرض جدول الكميات - """ - st.subheader("جدول الكميات") + # إنشاء DataFrame من بنود المشروع + df = pd.DataFrame(project_items) + df = df[['id', 'code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'resource_type']] + df.columns = ['الرقم', 'الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي', 'نوع المورد'] - # الحصول على جدول الكميات للمشروع الحالي - boq = next((b for b in st.session_state.bill_of_quantities if b["project_id"] == st.session_state.current_project), None) + # تنسيق الأسعار + df['سعر الوحدة'] = df['سعر الوحدة'].apply(lambda x: f"{x:,.2f} ريال") + df['السعر الإجمالي'] = df['السعر الإجمالي'].apply(lambda x: f"{x:,.2f} ريال") - if boq and boq["items"]: - # إنشاء DataFrame - boq_df = pd.DataFrame(boq["items"]) - boq_df.columns = ["الرقم", "البند", "الوحدة", "الكمية", "سعر الوحدة", "الإجمالي"] - - # إضافة زر لإضافة بند جديد - col1, col2, col3 = st.columns([1, 2, 1]) - with col1: - if st.button("➕ إضافة بند جديد", key="add_new_item_btn"): - st.session_state.show_add_item_form = True - - # عرض نموذج إضافة بند جديد - if st.session_state.show_add_item_form: - self._render_add_item_form() - - # عرض الجدول - st.dataframe(boq_df, use_container_width=True) - - # حساب الإجمالي - total = sum(item["total_price"] for item in boq["items"]) - st.metric("إجمالي جدول الكميات", f"{total:,} ريال") - - # تصدير جدول الكميات + # عرض الجدول + st.dataframe(df, use_container_width=True) + + # حساب المجموع الكلي + total_price = sum(item['total_price'] for item in project_items) + st.metric("إجمالي جدول الكميات", f"{total_price:,.2f} ريال") + + # أزرار التصدير والتعديل + col1, col2 = st.columns(2) + with col1: if st.button("تصدير", key="export_boq_btn_1"): - # إنشاء ملف Excel - output = io.BytesIO() - with pd.ExcelWriter(output, engine='openpyxl') as writer: - boq_df.to_excel(writer, index=False, sheet_name='جدول الكميات') - - # تنزيل الملف - b64 = base64.b64encode(output.getvalue()).decode() - href = f'تنزيل ملف Excel' + # إنشاء 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) - # اختيار بند للتحليل - st.subheader("تحليل سعر البند") - selected_item = st.selectbox( - "اختر بند للتحليل", - options=boq["items"], - format_func=lambda x: x["description"], - key="item_selector" + with col2: + 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 selected_item: - st.session_state.current_item = selected_item["id"] - st.session_state.show_item_analysis = True - - # عرض زر لتحليل سعر البند - if st.button("تحليل سعر البند", key="analyze_item_btn"): - self._render_item_price_analysis() - else: - st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود جديدة.") - - # إضافة زر لإضافة بند جديد - if st.button("➕ إضافة بند جديد", key="add_first_item_btn"): - st.session_state.show_add_item_form = True - - # عرض نموذج إضافة بند جديد - if st.session_state.show_add_item_form: - self._render_add_item_form() + 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_add_item_form(self): - """ - عرض نموذج إضافة بند جديد - """ + def _render_new_boq_item_form(self): + """عرض نموذج إضافة بند جديد""" st.subheader("إضافة بند جديد") - with st.form(key="add_item_form"): - description = st.text_input("وصف البند", key="new_item_description") + 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, col3 = st.columns(3) + col1, col2 = st.columns(2) with col1: - unit = st.selectbox( - "الوحدة", - options=["م³", "م²", "م.ط", "طن", "كجم", "عدد", "مقطوعية"], - key="new_item_unit" - ) + unit = st.text_input("الوحدة", key="new_boq_item_unit") with col2: - quantity = st.number_input("الكمية", min_value=0.0, key="new_item_quantity") + resource_type = st.selectbox( + "نوع المورد", + ["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"], + key="new_boq_item_resource_type" + ) + + col3, col4 = st.columns(2) with col3: - unit_price = st.number_input("سعر الوحدة", min_value=0.0, key="new_item_unit_price") + 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:,} ريال") + st.metric("السعر الإجمالي", f"{total_price:,.2f} ريال") - col1, col2 = st.columns(2) - with col1: - submit_button = st.form_submit_button("إضافة") - with col2: + col5, col6 = st.columns(2) + with col5: + submit_button = st.form_submit_button("حفظ") + with col6: cancel_button = st.form_submit_button("إلغاء") - if submit_button and description and quantity > 0 and unit_price > 0: - # الحصول على جدول الكميات للمشروع الحالي - boq = next((b for b in st.session_state.bill_of_quantities if b["project_id"] == st.session_state.current_project), None) - - if boq: - # إنشاء معرف جديد للبند - new_item_id = max([item["id"] for item in boq["items"]], default=0) + 1 - - # إضافة البند الجديد + if submit_button: + if code and description and unit and quantity > 0 and unit_price > 0: new_item = { - "id": new_item_id, - "description": description, - "unit": unit, - "quantity": quantity, - "unit_price": unit_price, - "total_price": total_price + '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 } - boq["items"].append(new_item) - - # إخفاء النموذج - st.session_state.show_add_item_form = False - - # إعادة تحميل الصفحة + 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_add_item_form = False + st.session_state.show_new_boq_item_form = False st.rerun() - def _render_item_price_analysis(self): - """ - عرض تحليل سعر البند - """ - if not st.session_state.current_item: - return - - st.subheader("تحليل سعر البند") - - # الحصول على البند الحالي - boq = next((b for b in st.session_state.bill_of_quantities if b["project_id"] == st.session_state.current_project), None) - if not boq: - return - - item = next((i for i in boq["items"] if i["id"] == st.session_state.current_item), None) + 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.write(f"**البند:** {item['description']}") - st.write(f"**الوحدة:** {item['unit']}") - st.write(f"**سعر الوحدة:** {item['unit_price']} ريال") - - # البحث عن تحليل سعر البند - item_analysis = next((a for a in st.session_state.item_analysis - if a["project_id"] == st.session_state.current_project and a["item_id"] == st.session_state.current_item), None) - - # إذا لم يكن هناك تحليل، إنشاء تحليل جديد - if not item_analysis: - # إنشاء تحليل افتراضي - item_analysis = { - "project_id": st.session_state.current_project, - "item_id": st.session_state.current_item, - "materials": [], - "equipment": [], - "labor": [], - "overhead": 10, - "profit": 15, - "total_cost": item["unit_price"] - } - - # إضافة التحليل الجديد - st.session_state.item_analysis.append(item_analysis) - - # عرض تحليل سعر البند - tab1, tab2, tab3, tab4 = st.tabs(["المواد", "المعدات", "العمالة", "ملخص التكلفة"]) + st.subheader(f"تعديل البند: {item['description']}") - with tab1: - self._render_materials_analysis(item_analysis) - - with tab2: - self._render_equipment_analysis(item_analysis) - - with tab3: - self._render_labor_analysis(item_analysis) - - with tab4: - self._render_cost_summary(item_analysis) - - def _render_materials_analysis(self, item_analysis): - """ - عرض تحليل المواد - """ - st.subheader("تحليل المواد") - - # عرض المواد الحالية - if item_analysis["materials"]: - materials_data = [] - for material_item in item_analysis["materials"]: - material = next((m for m in st.session_state.materials_catalog if m["id"] == material_item["material_id"]), None) - if material: - materials_data.append({ - "المادة": material["name"], - "الوحدة": material["unit"], - "الكمية": material_item["quantity"], - "سعر الوحدة": material["price"], - "التكلفة": material_item["cost"] - }) - - # إنشاء DataFrame - materials_df = pd.DataFrame(materials_data) - - # عرض الجدول - st.dataframe(materials_df, use_container_width=True) + 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") - # حساب إجمالي تكلفة المواد - total_materials_cost = sum(material_item["cost"] for material_item in item_analysis["materials"]) - st.metric("إجمالي تكلفة المواد", f"{total_materials_cost:,} ريال") - else: - st.info("لا توجد مواد مضافة لهذا البند.") - - # إضافة مادة جديدة - st.subheader("إضافة مادة جديدة") - - with st.form(key="add_material_form"): - # اختيار المادة - material = st.selectbox( - "المادة", - options=st.session_state.materials_catalog, - format_func=lambda x: f"{x['name']} ({x['unit']})", - key="new_material" - ) - - # الكمية - quantity = st.number_input("الكمية", min_value=0.0, key="new_material_quantity") - - # حساب التكلفة - if material and quantity > 0: - cost = material["price"] * quantity - st.metric("التكلفة", f"{cost:,} ريال") - else: - cost = 0 - - # أزرار الإضافة والإلغاء col1, col2 = st.columns(2) with col1: - submit_button = st.form_submit_button("إضافة") + unit = st.text_input("الوحدة", value=item['unit'], key="edit_boq_item_unit") with col2: - cancel_button = st.form_submit_button("إلغاء") + resource_type = st.selectbox( + "نوع المورد", + ["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"], + index=["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"].index(item['resource_type']) if 'resource_type' in item else 0, + key="edit_boq_item_resource_type" + ) - if submit_button and material and quantity > 0: - # إضافة المادة الجديدة - new_material = { - "material_id": material["id"], - "quantity": quantity, - "cost": cost - } - - # إضافة المادة إلى التحليل - item_analysis["materials"].append(new_material) - - # تحديث إجمالي التكلفة - self._update_total_cost(item_analysis) + 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() - def _render_equipment_analysis(self, item_analysis): - """ - عرض تحليل المعدات - """ - st.subheader("تحليل المعدات") - - # عرض المعدات الحالية - if item_analysis["equipment"]: - equipment_data = [] - for equipment_item in item_analysis["equipment"]: - equipment = next((e for e in st.session_state.equipment_catalog if e["id"] == equipment_item["equipment_id"]), None) - if equipment: - equipment_data.append({ - "المعدة": equipment["name"], - "عدد الأيام": equipment_item["days"], - "التكلفة اليومية": equipment["daily_rate"], - "التكلفة": equipment_item["cost"] - }) + 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 - # إنشاء DataFrame - equipment_df = pd.DataFrame(equipment_data) + st.session_state.show_edit_boq_item_form = False + st.rerun() - # عرض الجدول - st.dataframe(equipment_df, use_container_width=True) + 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() - # حساب إجمالي تكلفة المعدات - total_equipment_cost = sum(equipment_item["cost"] for equipment_item in item_analysis["equipment"]) - st.metric("إجمالي تكلفة المعدات", f"{total_equipment_cost:,} ريال") - else: - st.info("لا توجد معدات مضافة لهذا البند.") - - # إضافة معدة جديدة - st.subheader("إضافة معدة جديدة") - - with st.form(key="add_equipment_form"): - # اختيار المعدة - equipment = st.selectbox( - "المعدة", - options=st.session_state.equipment_catalog, - format_func=lambda x: f"{x['name']} ({x['daily_rate']} ريال/يوم)", - key="new_equipment" - ) + # الحصول على الموارد المناسبة + 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 - # عدد الأيام - days = st.number_input("عدد الأيام", min_value=0.0, key="new_equipment_days") + if not resources: + st.info(f"لا توجد موارد مسجلة من نوع {resource_type}. يرجى إضافة موارد في وحدة الموارد أولاً.") - # حساب التكلفة - if equipment and days > 0: - cost = equipment["daily_rate"] * days - st.metric("التكلفة", f"{cost:,} ريال") - else: - cost = 0 - - # أزرار الإضافة والإلغاء col1, col2 = st.columns(2) - with col1: - submit_button = st.form_submit_button("إضافة") with col2: - cancel_button = st.form_submit_button("إلغاء") - - if submit_button and equipment and days > 0: - # إضافة المعدة الجديدة - new_equipment = { - "equipment_id": equipment["id"], - "days": days, - "cost": cost - } - - # إضافة المعدة إلى التحليل - item_analysis["equipment"].append(new_equipment) + if st.button("إلغاء", key="cancel_resource_selector"): + st.session_state.show_resource_selector = False + st.rerun() + return - # تحديث إجمالي التكلفة - self._update_total_cost(item_analysis) + # إنشاء DataFrame من الموارد + df = pd.DataFrame(resources) + if 'id' in df.columns and 'name' in df.columns and 'unit' in df.columns and 'price' in df.columns: + df = df[['id', 'name', 'category', 'unit', 'price']] + df.columns = ['الرقم', 'الاسم', 'الفئة', 'الوحدة', 'السعر'] - # إعادة تحميل الصفحة - st.rerun() - - def _render_labor_analysis(self, item_analysis): - """ - عرض تحليل العمالة - """ - st.subheader("تحليل العمالة") - - # عرض العمالة الحالية - if item_analysis["labor"]: - labor_data = [] - for labor_item in item_analysis["labor"]: - labor = next((l for l in st.session_state.labor_rates if l["id"] == labor_item["labor_id"]), None) - if labor: - labor_data.append({ - "العامل": labor["role"], - "عدد الأيام": labor_item["days"], - "التكلفة اليومية": labor["daily_rate"], - "التكلفة": labor_item["cost"] - }) - - # إنشاء DataFrame - labor_df = pd.DataFrame(labor_data) + # تنسيق السعر + df['السعر'] = df['السعر'].apply(lambda x: f"{x:,.2f} ريال") # عرض الجدول - st.dataframe(labor_df, use_container_width=True) - - # حساب إجمالي تكلفة العمالة - total_labor_cost = sum(labor_item["cost"] for labor_item in item_analysis["labor"]) - st.metric("إجمالي تكلفة العمالة", f"{total_labor_cost:,} ريال") + st.dataframe(df, use_container_width=True) + + with st.form(key="add_from_resource_form"): + col1, col2 = st.columns(2) + with col1: + selected_resource_id = st.selectbox( + "اختر المورد", + options=[r['id'] for r in resources], + format_func=lambda x: next((r['name'] for r in resources if r['id'] == x), ""), + key="select_resource" + ) + + with col2: + quantity = st.number_input("الكمية", min_value=0.1, value=1.0, format="%f", key="resource_quantity") + + # الحصول على المورد المحدد + selected_resource = next((r for r in resources if r['id'] == selected_resource_id), None) + if selected_resource: + total_price = quantity * selected_resource['price'] + st.metric("السعر الإجمالي", f"{total_price:,.2f} ريال") + + col3, col4 = st.columns(2) + with col3: + submit_button = st.form_submit_button("إضافة إلى جدول الكميات") + with col4: + cancel_button = st.form_submit_button("إلغاء") + + if submit_button and selected_resource and quantity > 0: + # تحويل نوع المورد إلى الصيغة المناسبة + resource_type_map = { + "المواد": "مواد", + "العمالة": "عمالة", + "المعدات": "معدات", + "المقاولين من الباطن": "مقاولين من الباطن" + } + + # إنشاء كود فريد + resource_code_prefix = { + "المواد": "M", + "العمالة": "L", + "المعدات": "E", + "المقاولين من الباطن": "S" + } + + code_prefix = resource_code_prefix.get(resource_type, "X") + code = f"{code_prefix}-{selected_resource['id']:03d}" + + # إضافة البند إلى جدول الكميات + new_item = { + 'id': st.session_state.next_boq_item_id, + 'project_id': st.session_state.current_project, + 'code': code, + 'description': selected_resource['name'], + 'unit': selected_resource['unit'], + 'quantity': quantity, + 'unit_price': selected_resource['price'], + 'total_price': quantity * selected_resource['price'], + 'resource_type': resource_type_map.get(resource_type, "أخرى"), + 'resource_id': selected_resource['id'] + } + + st.session_state.boq_items.append(new_item) + st.session_state.next_boq_item_id += 1 + st.session_state.show_resource_selector = False + st.rerun() + + if cancel_button: + st.session_state.show_resource_selector = False + st.rerun() else: - st.info("لا توجد عمالة مضافة لهذا البند.") - - # إضافة عامل جديد - st.subheader("إضافة عامل جديد") - - with st.form(key="add_labor_form"): - # اختيار العامل - labor = st.selectbox( - "العامل", - options=st.session_state.labor_rates, - format_func=lambda x: f"{x['role']} ({x['daily_rate']} ريال/يوم)", - key="new_labor" - ) - - # عدد الأيام - days = st.number_input("عدد الأيام", min_value=0.0, key="new_labor_days") + st.error("تنسيق بيانات الموارد غير صحيح. يرجى ال��أكد من وجود الحقول المطلوبة.") - # حساب التكلفة - if labor and days > 0: - cost = labor["daily_rate"] * days - st.metric("التكلفة", f"{cost:,} ريال") - else: - cost = 0 - - # أزرار الإضافة والإلغاء - col1, col2 = st.columns(2) - with col1: - submit_button = st.form_submit_button("إضافة") - with col2: - cancel_button = st.form_submit_button("إلغاء") + if st.button("إلغاء", key="cancel_resource_selector_error"): + st.session_state.show_resource_selector = False + st.rerun() - if submit_button and labor and days > 0: - # إضافة العامل الجديد - new_labor = { - "labor_id": labor["id"], - "days": days, - "cost": cost - } + def _render_item_price_analysis(self): + """عرض تحليل سعر البند""" + st.subheader("تحليل سعر البند") + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + + if not project_items: + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") + return - # إضافة العامل إلى التحليل - item_analysis["labor"].append(new_labor) + # اختيار البند للتحليل + 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 - # تحديث إجمالي التكلفة - self._update_total_cost(item_analysis) + # عرض معلومات البند + st.write(f"**البند:** {selected_item['description']}") + st.write(f"**الكود:** {selected_item['code']}") + st.write(f"**الوحدة:** {selected_item['unit']}") + 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_cost_summary(item_analysis) + + 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) + } + ] + + # حساب إجمالي التكاليف + 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_cost = total_materials_cost + total_equipment_cost + total_labor_cost + overhead_cost + profit = unit_price - total_cost + + return { + 'item': item, + 'materials': materials, + 'equipment': equipment, + 'labor': labor, + 'total_materials_cost': total_materials_cost, + 'total_equipment_cost': total_equipment_cost, + 'total_labor_cost': total_labor_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("لا توجد مواد في تحليل هذا البند.") + return - # إعادة تحميل الصفحة - st.rerun() + # إنشاء DataFrame من قائمة المواد + df = pd.DataFrame(item_analysis['materials']) + 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.metric("إجمالي تكلفة المواد", f"{item_analysis['total_materials_cost']:,.2f} ريال") + + def _render_equipment_analysis(self, item_analysis): + """عرض تحليل المعدات""" + st.subheader("تحليل المعدات") + + if not item_analysis['equipment']: + st.info("لا توجد معدات في تحليل هذا البند.") + return + + # إنشاء DataFrame من قائمة المعدات + df = pd.DataFrame(item_analysis['equipment']) + 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.metric("إجمالي تكلفة المعدات", f"{item_analysis['total_equipment_cost']:,.2f} ريال") + + def _render_labor_analysis(self, item_analysis): + """عرض تحليل العمالة""" + st.subheader("تحليل العمالة") + + if not item_analysis['labor']: + st.info("لا توجد عمالة في تحليل هذا البند.") + return + # إنشاء DataFrame من قائمة العمالة + df = pd.DataFrame(item_analysis['labor']) + 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.metric("إجمالي تكلفة العمالة", f"{item_analysis['total_labor_cost']:,.2f} ريال") + + 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("ملخص التكلفة") - # حساب التكاليف - materials_cost = sum(material["cost"] for material in item_analysis["materials"]) - equipment_cost = sum(equipment["cost"] for equipment in item_analysis["equipment"]) - labor_cost = sum(labor["cost"] for labor in item_analysis["labor"]) - direct_cost = materials_cost + equipment_cost + labor_cost + # إعداد الخطوط العربية + self._setup_arabic_fonts() - overhead_cost = direct_cost * (item_analysis["overhead"] / 100) - subtotal = direct_cost + overhead_cost - profit_cost = subtotal * (item_analysis["profit"] / 100) - total_cost = subtotal + profit_cost + # إنشاء بيانات الرسم البياني + labels = ['المواد', 'المعدات', 'العمالة', 'المصاريف العمومية', 'الربح'] - # عرض ملخص التكلفة - cost_data = [ - {"البند": "تكلفة المواد", "القيمة": materials_cost, "النسبة": materials_cost / total_cost * 100 if total_cost > 0 else 0}, - {"البند": "تكلفة المعدات", "القيمة": equipment_cost, "النسبة": equipment_cost / total_cost * 100 if total_cost > 0 else 0}, - {"البند": "تكلفة العمالة", "القيمة": labor_cost, "النسبة": labor_cost / total_cost * 100 if total_cost > 0 else 0}, - {"البند": "التكلفة المباشرة", "القيمة": direct_cost, "النسبة": direct_cost / total_cost * 100 if total_cost > 0 else 0}, - {"البند": f"المصاريف العمومية ({item_analysis['overhead']}%)", "القيمة": overhead_cost, "النسبة": overhead_cost / total_cost * 100 if total_cost > 0 else 0}, - {"البند": "الإجمالي الفرعي", "القيمة": subtotal, "النسبة": subtotal / total_cost * 100 if total_cost > 0 else 0}, - {"البند": f"الربح ({item_analysis['profit']}%)", "القيمة": profit_cost, "النسبة": profit_cost / total_cost * 100 if total_cost > 0 else 0}, - {"البند": "إجمالي التكلفة", "القيمة": total_cost, "النسبة": 100} + # التحقق من وجود قيم 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['overhead_cost'] if not np.isnan(item_analysis['overhead_cost']) else 0, + item_analysis['profit'] if not np.isnan(item_analysis['profit']) else 0 ] - # إنشاء DataFrame - cost_df = pd.DataFrame(cost_data) + # التحقق من أن جميع القيم موجبة (أو صفر) لتجنب الأخطاء في الرسم البياني + values = [max(0, val) for val in values] - # تنسيق الأعمدة - formatted_df = cost_df.copy() - formatted_df["القيمة"] = formatted_df["القيمة"].apply(lambda x: f"{x:,.2f} ريال") - formatted_df["النسبة"] = formatted_df["النسبة"].apply(lambda x: f"{x:.2f}%") + # إنشاء الرسم البياني + 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") + + # إضافة العنوان + ax.set_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['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['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(formatted_df, use_container_width=True) + st.dataframe(df, use_container_width=True) + + def _render_cost_analysis(self, project_info): + """عرض تحليل التكلفة""" + st.subheader("تحليل التكلفة") + + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] - # رسم بياني للتكلفة + if not project_items: + st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") + return + + # حساب إجمالي تكلفة المشروع + total_cost = sum(item['total_price'] for item in project_items) + + # عرض إجمالي تكلفة المشروع + st.metric("إجمالي تكلفة المشروع", f"{total_cost:,.2f} ريال") + + # إنشاء تحليل التكلفة حسب نوع المورد + cost_by_resource_type = {} + for item in project_items: + resource_type = item.get('resource_type', 'أخرى') + if resource_type not in cost_by_resource_type: + cost_by_resource_type[resource_type] = 0 + cost_by_resource_type[resource_type] += item['total_price'] + + # إعداد الخطوط العربية + self._setup_arabic_fonts() + + # إنشاء الرسم البياني fig, ax = plt.subplots(figsize=(10, 6)) - # إعداد البيانات للرسم البياني - labels = ["المواد", "المعدات", "العمالة", "المصاريف العمومية", "الربح"] - sizes = [materials_cost, equipment_cost, labor_cost, overhead_cost, profit_cost] - colors = ['#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6'] + # التحقق من وجود قيم NaN واستبدالها بأصفار + labels = list(cost_by_resource_type.keys()) + values = [cost_by_resource_type[label] if not np.isnan(cost_by_resource_type[label]) else 0 for label in labels] + + # التحقق من أن جميع القيم موجبة (أو صفر) لتجنب الأخطاء في الرسم البياني + values = [max(0, val) for val in values] - # رسم الدائرة + # رسم المخطط الدائري wedges, texts, autotexts = ax.pie( - sizes, - labels=labels, - colors=colors, + values, + labels=labels, autopct='%1.1f%%', startangle=90, - wedgeprops={'edgecolor': 'w', 'linewidth': 1} + shadow=True, ) - # تنسيق النص - for text in texts: - text.set_fontsize(12) - for autotext in autotexts: - autotext.set_fontsize(10) - autotext.set_color('white') - + # تعديل حجم النص + plt.setp(autotexts, size=10, weight="bold") + # إضافة العنوان - ax.set_title('توزيع التكلفة') + ax.set_title('توزيع التكلفة حسب نوع المورد', fontsize=16) # عرض الرسم البياني st.pyplot(fig) - # تعديل نسب المصاريف العمومية والربح - col1, col2 = st.columns(2) + # عرض جدول تحليل التكلفة + cost_analysis = { + 'نوع المورد': list(cost_by_resource_type.keys()), + 'التكلفة': list(cost_by_resource_type.values()), + 'النسبة': [cost / total_cost * 100 if total_cost > 0 else 0 for cost in cost_by_resource_type.values()] + } + + df = pd.DataFrame(cost_analysis) + 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) + + 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 = st.slider( + "نسبة الربح (%)", + min_value=0.0, + max_value=50.0, + value=15.0, + step=0.5, + key="profit_margin_slider" + ) + + # عرض شريط تمرير لتعديل نسبة المصاريف العمومية + overhead_percentage = st.slider( + "نسبة المصاريف العمومية (%)", + min_value=0.0, + max_value=30.0, + value=10.0, + step=0.5, + key="overhead_percentage_slider" + ) + + # حساب قيمة المصاريف العمومية + overhead_cost = total_cost * (overhead_percentage / 100) + + # حساب قيمة الربح + profit_amount = (total_cost + overhead_cost) * (profit_margin / 100) + + # حساب السعر النهائي + final_price = total_cost + overhead_cost + profit_amount + + # عرض النتائج + col1, col2, col3 = st.columns(3) with col1: - overhead = st.slider( - "نسبة المصاريف العمومية (%)", - min_value=0, - max_value=30, - value=item_analysis["overhead"], - key="overhead_slider" - ) + st.metric("إجمالي التكلفة المباشرة", f"{total_cost:,.2f} ريال") with col2: - profit = st.slider( - "نسبة الربح (%)", - min_value=0, - max_value=50, - value=item_analysis["profit"], - key="profit_slider" + st.metric("المصاريف العمومية", f"{overhead_cost:,.2f} ريال") + with col3: + st.metric("قيمة الربح", f"{profit_amount:,.2f} ريال") + + st.metric("السعر النهائي للمشروع", f"{final_price:,.2f} ريال") + + # إنشاء تحليل حساسية الربحية + st.subheader("تحليل حساسية الربحية") + + # إنشاء مصفوفة نسب الربح والمصاريف العمومية + profit_margins = [profit_margin - 5, profit_margin, profit_margin + 5] + overhead_percentages = [overhead_percentage - 2, overhead_percentage, overhead_percentage + 2] + + # التأكد من أن جميع القيم موجبة + profit_margins = [max(0, margin) for margin in profit_margins] + overhead_percentages = [max(0, percentage) for percentage in overhead_percentages] + + # إنشاء بيانات تحليل الحساسية + sensitivity_data = [] + + for p_margin in profit_margins: + for o_percentage in overhead_percentages: + o_cost = total_cost * (o_percentage / 100) + p_amount = (total_cost + o_cost) * (p_margin / 100) + f_price = total_cost + o_cost + p_amount + + sensitivity_data.append({ + "نسبة الربح": p_margin, + "نسبة المصاريف العمومية": o_percentage, + "السعر النهائي": f_price, + "نسبة الربح المعدلة": p_amount / f_price * 100 if f_price > 0 else 0 + }) + + # إنشاء DataFrame من بيانات تحليل الحساسية + df = pd.DataFrame(sensitivity_data) + + # إنشاء جدول تحليل الحساسية + pivot_table = pd.pivot_table( + df, + values="السعر النهائي", + index="نسبة الربح", + columns="نسبة المصاريف العمومية", + aggfunc="first" + ) + + # تنسيق الأرقام + pivot_table = pivot_table.applymap(lambda x: f"{x:,.2f} ريال") + + # عرض جدول تحليل الحساسية + st.write("**جدول تحليل الحساسية (السعر النهائي)**") + st.dataframe(pivot_table, use_container_width=True) + + # إنشاء جدول نسبة الربح المعدلة + profit_pivot = pd.pivot_table( + df, + values="نسبة الربح المعدلة", + index="نسبة الربح", + columns="نسبة المصاريف العمومية", + aggfunc="first" + ) + + # تنسيق الأرقام + profit_pivot = profit_pivot.applymap(lambda x: f"{x:.2f}%" if isinstance(x, (int, float)) else str(x)) + + # عرض جدول نسبة الربح المعدلة + st.write("**جدول تحليل الحساسية (نسبة الربح المعدلة)**") + st.dataframe(profit_pivot, use_container_width=True) + + # إنشاء رسم بياني لتحليل الحساسية + self._setup_arabic_fonts() + + fig, ax = plt.subplots(figsize=(10, 6)) + + # إنشاء بيانات الرسم البياني + x = [str(margin) + "%" for margin in df["نسبة الربح"]] + y = df["السعر النهائي"] + hue = [str(percentage) + "%" for percentage in df["نسبة المصاريف العمومية"]] + + # إنشاء قاموس للألوان + colors = { + str(overhead_percentages[0]) + "%": "blue", + str(overhead_percentages[1]) + "%": "green", + str(overhead_percentages[2]) + "%": "red" + } + + # رسم الخطوط + for percentage in set(hue): + mask = [h == percentage for h in hue] + ax.plot( + [x[i] for i in range(len(x)) if mask[i]], + [y[i] for i in range(len(y)) if mask[i]], + marker='o', + label=percentage, + color=colors.get(percentage, "gray") ) - # تحديث النسب إذا تغيرت - if overhead != item_analysis["overhead"] or profit != item_analysis["profit"]: - item_analysis["overhead"] = overhead - item_analysis["profit"] = profit + # إضافة التسميات + ax.set_xlabel('نسبة الربح', fontsize=12) + ax.set_ylabel('السعر النهائي (ريال)', fontsize=12) + ax.set_title('تحليل حساسية الربحية', fontsize=16) + ax.legend(title='نسبة المصاريف العمومية') + ax.grid(True) + + # تنسيق المحور Y + ax.get_yaxis().set_major_formatter( + plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))) + ) + + # عرض الرسم البياني + 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 - # تحديث إجمالي التكلفة - self._update_total_cost(item_analysis) + # اختيار استراتيجية التسعير + pricing_strategy = st.selectbox( + "اختر استراتيجية التسعير", + ["تسعير قياسي", "تسعير غير متزن", "تسعير موجه ربحية", "تسعير تنافسي", "تسعير استراتيجي"], + key="pricing_strategy_selector" + ) + + # عرض وصف استراتيجية التسعير + if pricing_strategy == "تسعير قياسي": + st.info(""" + **التسعير القياسي**: يعتمد على تحليل التكلفة الفعلية لكل بند مع إضافة نسبة ربح ثابتة على جميع البنود. + + **المميزات**: + - سهل التطبيق والفهم + - عادل ومتوازن لجميع البنود + - يعكس التكلفة الحقيقية للمشروع + + **العيوب**: + - لا يأخذ في الاعتبار التدفق النقدي + - قد لا يكون تنافسياً في بعض الحالات + """) + elif pricing_strategy == "تسعير غير متزن": + st.info(""" + **التسعير غير المتزن**: يتضمن زيادة أسعار البنود المبكرة في المشروع وتخفيض أسعار البنود المتأخرة. + + **المميزات**: + - يحسن التدفق النقدي في بداية المشروع + - يقلل من مخاطر التمويل + - يمكن أن يزيد من الربحية الإجمالية + + **العيوب**: + - قد يكون مرفوضاً من قبل بعض العملاء + - يتطلب تحليلاً دقيقاً لجدول المشروع + - قد يؤدي إلى مخاطر إذا لم يكتمل المشروع + """) + + # عرض أدوات التحكم في التسعير الغير متزن + st.subheader("إعدادات التسعير الغير متزن") - # إعادة تحميل الصفحة - st.rerun() + col1, col2 = st.columns(2) + with col1: + early_items_factor = st.slider( + "معامل زيادة البنود المبكرة", + min_value=1.0, + max_value=2.0, + value=st.session_state.unbalanced_pricing_factors.get('early_items_factor', 1.15), + step=0.01, + key="early_items_factor_slider" + ) + with col2: + late_items_factor = st.slider( + "معامل تخفيض البنود المتأخرة", + min_value=0.5, + max_value=1.0, + value=st.session_state.unbalanced_pricing_factors.get('late_items_factor', 0.90), + step=0.01, + key="late_items_factor_slider" + ) + + # تحديث معاملات التسعير الغير متزن + st.session_state.unbalanced_pricing_factors['early_items_factor'] = early_items_factor + st.session_state.unbalanced_pricing_factors['late_items_factor'] = late_items_factor + + # عرض معاملات مخصصة للبنود + st.subheader("معاملات مخصصة للبنود") + + # إنشاء DataFrame من بنود المشروع + df = pd.DataFrame(project_items) + df = df[['id', 'code', 'description', 'unit_price', 'total_price']] + df.columns = ['الرقم', 'الكود', 'الوصف', 'سعر الوحدة', 'السعر الإجمالي'] - # تحديث سعر البند - if st.button("تحديث سعر البند", key="update_item_price_btn"): - # الحصول على البند الحالي - boq = next((b for b in st.session_state.bill_of_quantities if b["project_id"] == st.session_state.current_project), None) - if boq: - item = next((i for i in boq["items"] if i["id"] == st.session_state.current_item), None) - if item: - # تحديث سعر الوحدة - item["unit_price"] = total_cost - item["total_price"] = total_cost * item["quantity"] + # تنسيق الأسعار + df['سعر الوحدة'] = df['سعر الوحدة'].apply(lambda x: f"{x:,.2f} ريال") + df['السعر الإجمالي'] = df['السعر الإجمالي'].apply(lambda x: f"{x:,.2f} ريال") + + # عرض الجدول + st.dataframe(df, use_container_width=True) + + # اختيار البند لتعديل معامله + 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_custom_factor" + ) + + # الحصول على البند المحدد + selected_item = next((item for item in project_items if item['id'] == selected_item_id), None) + + if selected_item: + # الحصول على المعامل الحالي للبند + current_factor = st.session_state.unbalanced_pricing_factors.get('custom_factors', {}).get(str(selected_item_id), 1.0) + + # عرض شريط تمرير لتعديل معامل البند + custom_factor = st.slider( + f"معامل البند: {selected_item['description']}", + min_value=0.5, + max_value=2.0, + value=current_factor, + step=0.01, + key="custom_factor_slider" + ) + + # تحديث معامل البند + if 'custom_factors' not in st.session_state.unbalanced_pricing_factors: + st.session_state.unbalanced_pricing_factors['custom_factors'] = {} - # إعادة تحميل الصفحة - st.rerun() + st.session_state.unbalanced_pricing_factors['custom_factors'][str(selected_item_id)] = custom_factor + + # عرض السعر الأصلي والسعر المعدل + original_price = selected_item['unit_price'] + adjusted_price = original_price * custom_factor + + col3, col4 = st.columns(2) + with col3: + st.metric("السعر الأصلي", f"{original_price:,.2f} ريال") + with col4: + st.metric("السعر المعدل", f"{adjusted_price:,.2f} ريال", delta=f"{(custom_factor - 1) * 100:.1f}%") - def _update_total_cost(self, item_analysis): - """ - تحديث إجمالي التكلفة - """ - # حساب التكاليف - materials_cost = sum(material["cost"] for material in item_analysis["materials"]) - equipment_cost = sum(equipment["cost"] for equipment in item_analysis["equipment"]) - labor_cost = sum(labor["cost"] for labor in item_analysis["labor"]) - direct_cost = materials_cost + equipment_cost + labor_cost - - overhead_cost = direct_cost * (item_analysis["overhead"] / 100) - subtotal = direct_cost + overhead_cost - profit_cost = subtotal * (item_analysis["profit"] / 100) - total_cost = subtotal + profit_cost - - # تحديث إجمالي التكلفة - item_analysis["total_cost"] = total_cost + # عرض زر تطبيق التسعير الغير متزن + if st.button("تطبيق التسعير الغير متزن", key="apply_unbalanced_pricing_btn"): + # تطبيق التسعير الغير متزن على جميع البنود + self._apply_unbalanced_pricing() + st.success("تم تطبيق التسعير الغير متزن بنجاح!") + st.rerun() + + elif pricing_strategy == "تسعير موجه ربحية": + st.info(""" + **التسعير الموجه ربحية**: يركز على تحقيق أقصى ربح ممكن من خلال زيادة أسعار البنود ذات التكلفة المنخفضة والمخاطر القليلة. + + **المميزات**: + - يزيد من الربحية الإجمالية للمشروع + - يستفيد من البنود ذات هامش الربح العالي + - يمكن أن يعوض الخسائر في البنود الأخرى + + **العيوب**: + - قد يؤدي إلى أسعار غير تنافسية في بعض البنود + - يتطلب تحليلاً دقيقاً للتكاليف والمخاطر + - قد يكون مرفوضاً من قبل بعض العملاء + """) + elif pricing_strategy == "تسعير تنافسي": + st.info(""" + **التسعير التنافسي**: يهدف إلى تقديم أقل سعر ممكن للفوز بالمناقصة مع الحفاظ على هامش ربح مقبول. + + **المميزات**: + - يزيد من فرص الفوز بالمناقصة + - يمكن أن يفتح أسواقاً جديدة + - يمكن أن يبني علاقات طويلة المدى مع العملاء + + **العيوب**: + - يقلل من هامش الربح + - قد يؤدي إلى خسائر إذا لم يتم تقدير التكاليف بدقة + - قد يؤدي إلى منافسة سعرية غير مستدامة + """) + elif pricing_strategy == "تسعير استراتيجي": + st.info(""" + **التسعير الاستراتيجي**: يأخذ في الاعتبار العلاقة طويلة المدى مع العميل والمشاريع المستقبلية المحتملة. + + **المميزات**: + - يبني علاقات طويلة المدى مع العملاء + - يمكن أن يؤدي إلى مشاريع مستقبلية أكثر ربحية + - يمكن أن يفتح أسواقاً جديدة + + **العيوب**: + - قد يقلل من الربحية في المشروع الحالي + - يتطلب تحليلاً دقيقاً للسوق والمنافسين + - قد لا يكون مناسباً لجميع المشاريع + """) + + # عرض مقارنة بين استراتيجيات التسعير + st.subheader("مقارنة بين استراتيجيات التسعير") + + # حساب إجمالي تكلفة المشروع + total_cost = sum(item['total_price'] for item in project_items) + + # حساب السعر النهائي لكل استراتيجية + standard_price = total_cost * 1.25 # تسعير قياسي: تكلفة + 25% + unbalanced_price = total_cost * 1.25 # تسعير غير متزن: نفس السعر الإجمالي ولكن بتوزيع مختلف + profit_oriented_price = total_cost * 1.35 # تسعير موجه ربحية: تكلفة + 35% + competitive_price = total_cost * 1.15 # تسعير تنافسي: تكلفة + 15% + strategic_price = total_cost * 1.20 # تسعير استراتيجي: تكلفة + 20% + + # إنشاء بيانات المقارنة + comparison_data = { + 'استراتيجية التسعير': ['تسعير قياسي', 'تسعير غير متزن', 'تسعير موجه ربحية', 'تسعير تنافسي', 'تسعير استراتيجي'], + 'السعر النهائي': [standard_price, unbalanced_price, profit_oriented_price, competitive_price, strategic_price], + 'نسبة الزيادة عن التكلفة': [25, 25, 35, 15, 20], + 'التدفق النقدي': ['متوسط', 'جيد', 'متوسط', 'ضعيف', 'متوسط'], + 'فرص الفوز': ['متوسطة', 'متوسطة', 'منخفضة', 'عالية', 'متوسطة'] + } + + # إنشاء DataFrame من بيانات المقارنة + df = pd.DataFrame(comparison_data) + + # تنسيق الأسعار + df['السعر النهائي'] = df['السعر النهائي'].apply(lambda x: f"{x:,.2f} ريال") + df['نسبة الزيادة عن التكلفة'] = df['نسبة الزيادة عن التكلفة'].apply(lambda x: f"{x}%") + + # عرض الجدول + st.dataframe(df, use_container_width=True) + + # إنشاء رسم بياني للمقارنة + self._setup_arabic_fonts() + + fig, ax = plt.subplots(figsize=(10, 6)) + + # إنشاء بيانات الرسم البياني + strategies = comparison_data['استراتيجية التسعير'] + prices = [standard_price, unbalanced_price, profit_oriented_price, competitive_price, strategic_price] + + # رسم الأعمدة + bars = ax.bar(strategies, prices, color=['blue', 'green', 'red', 'purple', 'orange']) + + # إضافة التسميات + ax.set_xlabel('استراتيجية التسعير', fontsize=12) + ax.set_ylabel('السعر النهائي (ريال)', fontsize=12) + ax.set_title('مقارنة بين استراتيجيات التسعير', fontsize=16) + + # تنسيق المحور Y + ax.get_yaxis().set_major_formatter( + plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))) + ) + + # إضافة قيم الأعمدة + for bar in bars: + height = bar.get_height() + ax.text( + bar.get_x() + bar.get_width() / 2., + height * 1.01, + f"{height:,.0f}", + ha='center', + va='bottom', + rotation=0, + fontsize=8 + ) + + # تدوير تسميات المحور X + plt.xticks(rotation=45, ha='right') + + # ضبط التخطيط + plt.tight_layout() + + # عرض الرسم البياني + st.pyplot(fig) + + def _apply_unbalanced_pricing(self): + """تطبيق التسعير الغير متزن على بنود المشروع""" + # الحصول على بنود المشروع الحالي + project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] + + if not project_items: + return + + # الحصول على معاملات التسعير الغير متزن + early_items_factor = st.session_state.unbalanced_pricing_factors.get('early_items_factor', 1.15) + late_items_factor = st.session_state.unbalanced_pricing_factors.get('late_items_factor', 0.90) + custom_factors = st.session_state.unbalanced_pricing_factors.get('custom_factors', {}) + + # حساب إجمالي تكلفة المشروع قبل التعديل + total_price_before = sum(item['total_price'] for item in project_items) + + # تقسيم البنود إلى مبكرة ومتأخرة + num_items = len(project_items) + early_items = project_items[:num_items // 3] + late_items = project_items[num_items * 2 // 3:] + middle_items = project_items[num_items // 3:num_items * 2 // 3] + + # تطبيق المعاملات على البنود + for i, item in enumerate(st.session_state.boq_items): + if item['project_id'] != st.session_state.current_project: + continue + + # التحقق من وجود معامل مخصص للبند + if str(item['id']) in custom_factors: + factor = custom_factors[str(item['id'])] + # تطبيق معامل البنود المبكرة + elif item in early_items: + factor = early_items_factor + # تطبيق معامل البنود المتأخرة + elif item in late_items: + factor = late_items_factor + # البنود الوسطى تبقى كما هي + else: + factor = 1.0 + + # تعديل سعر الوحدة والسعر الإجمالي + for j, original_item in enumerate(st.session_state.boq_items): + if original_item['id'] == item['id']: + original_unit_price = original_item['unit_price'] / factor if factor != 0 else original_item['unit_price'] + st.session_state.boq_items[j]['unit_price'] = original_unit_price * factor + st.session_state.boq_items[j]['total_price'] = original_item['quantity'] * st.session_state.boq_items[j]['unit_price'] + break + + # حساب إجمالي تكلفة المشروع بعد التعديل + total_price_after = sum(item['total_price'] for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project) + + # تعديل الأسعار للحفاظ على نفس إجمالي التكلفة + if total_price_after > 0: + adjustment_factor = total_price_before / total_price_after + + for i, item in enumerate(st.session_state.boq_items): + if item['project_id'] == st.session_state.current_project: + st.session_state.boq_items[i]['unit_price'] *= adjustment_factor + st.session_state.boq_items[i]['total_price'] = item['quantity'] * st.session_state.boq_items[i]['unit_price']