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']