|
"""
|
|
وحدة التسعير - التطبيق الرئيسي
|
|
"""
|
|
|
|
import streamlit as st
|
|
import pandas as pd
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
import plotly.express as px
|
|
import plotly.graph_objects as go
|
|
from datetime import datetime
|
|
import time
|
|
import io
|
|
import os
|
|
import json
|
|
import base64
|
|
from pathlib import Path
|
|
|
|
class PricingApp:
|
|
"""وحدة التسعير"""
|
|
|
|
def __init__(self):
|
|
"""تهيئة وحدة التسعير"""
|
|
|
|
|
|
if 'bill_of_quantities' not in st.session_state:
|
|
st.session_state.bill_of_quantities = [
|
|
{
|
|
'id': 1,
|
|
'code': 'A-001',
|
|
'description': 'أعمال الحفر والردم',
|
|
'unit': 'م3',
|
|
'quantity': 1500,
|
|
'unit_price': 45,
|
|
'total_price': 67500,
|
|
'category': 'أعمال ترابية'
|
|
},
|
|
{
|
|
'id': 2,
|
|
'code': 'A-002',
|
|
'description': 'توريد وصب خرسانة عادية',
|
|
'unit': 'م3',
|
|
'quantity': 250,
|
|
'unit_price': 350,
|
|
'total_price': 87500,
|
|
'category': 'أعمال خرسانية'
|
|
},
|
|
{
|
|
'id': 3,
|
|
'code': 'A-003',
|
|
'description': 'توريد وصب خرسانة مسلحة للأساسات',
|
|
'unit': 'م3',
|
|
'quantity': 180,
|
|
'unit_price': 450,
|
|
'total_price': 81000,
|
|
'category': 'أعمال خرسانية'
|
|
},
|
|
{
|
|
'id': 4,
|
|
'code': 'A-004',
|
|
'description': 'توريد وصب خرسانة مسلحة للأعمدة',
|
|
'unit': 'م3',
|
|
'quantity': 120,
|
|
'unit_price': 500,
|
|
'total_price': 60000,
|
|
'category': 'أعمال خرسانية'
|
|
},
|
|
{
|
|
'id': 5,
|
|
'code': 'A-005',
|
|
'description': 'توريد وتركيب حديد تسليح',
|
|
'unit': 'طن',
|
|
'quantity': 45,
|
|
'unit_price': 3000,
|
|
'total_price': 135000,
|
|
'category': 'أعمال حديد'
|
|
},
|
|
{
|
|
'id': 6,
|
|
'code': 'A-006',
|
|
'description': 'توريد وبناء طابوق',
|
|
'unit': 'م2',
|
|
'quantity': 1200,
|
|
'unit_price': 45,
|
|
'total_price': 54000,
|
|
'category': 'أعمال بناء'
|
|
},
|
|
{
|
|
'id': 7,
|
|
'code': 'A-007',
|
|
'description': 'أعمال اللياسة والتشطيبات',
|
|
'unit': 'م2',
|
|
'quantity': 2400,
|
|
'unit_price': 35,
|
|
'total_price': 84000,
|
|
'category': 'أعمال تشطيبات'
|
|
},
|
|
{
|
|
'id': 8,
|
|
'code': 'A-008',
|
|
'description': 'أعمال الدهانات',
|
|
'unit': 'م2',
|
|
'quantity': 2400,
|
|
'unit_price': 25,
|
|
'total_price': 60000,
|
|
'category': 'أعمال تشطيبات'
|
|
},
|
|
{
|
|
'id': 9,
|
|
'code': 'A-009',
|
|
'description': 'توريد وتركيب أبواب خشبية',
|
|
'unit': 'عدد',
|
|
'quantity': 24,
|
|
'unit_price': 750,
|
|
'total_price': 18000,
|
|
'category': 'أعمال نجارة'
|
|
},
|
|
{
|
|
'id': 10,
|
|
'code': 'A-010',
|
|
'description': 'توريد وتركيب نوافذ ألمنيوم',
|
|
'unit': 'م2',
|
|
'quantity': 120,
|
|
'unit_price': 350,
|
|
'total_price': 42000,
|
|
'category': 'أعمال ألمنيوم'
|
|
}
|
|
]
|
|
|
|
if 'cost_analysis' not in st.session_state:
|
|
st.session_state.cost_analysis = [
|
|
{
|
|
'id': 1,
|
|
'category': 'تكاليف مباشرة',
|
|
'subcategory': 'مواد',
|
|
'description': 'خرسانة',
|
|
'amount': 120000,
|
|
'percentage': 17.9
|
|
},
|
|
{
|
|
'id': 2,
|
|
'category': 'تكاليف مباشرة',
|
|
'subcategory': 'مواد',
|
|
'description': 'حديد تسليح',
|
|
'amount': 135000,
|
|
'percentage': 20.1
|
|
},
|
|
{
|
|
'id': 3,
|
|
'category': 'تكاليف مباشرة',
|
|
'subcategory': 'مواد',
|
|
'description': 'طابوق',
|
|
'amount': 54000,
|
|
'percentage': 8.1
|
|
},
|
|
{
|
|
'id': 4,
|
|
'category': 'تكاليف مباشرة',
|
|
'subcategory': 'عمالة',
|
|
'description': 'عمالة تنفيذ',
|
|
'amount': 120000,
|
|
'percentage': 17.9
|
|
},
|
|
{
|
|
'id': 5,
|
|
'category': 'تكاليف مباشرة',
|
|
'subcategory': 'معدات',
|
|
'description': 'معدات إنشائية',
|
|
'amount': 85000,
|
|
'percentage': 12.7
|
|
},
|
|
{
|
|
'id': 6,
|
|
'category': 'تكاليف غير مباشرة',
|
|
'subcategory': 'إدارة',
|
|
'description': 'إدارة المشروع',
|
|
'amount': 45000,
|
|
'percentage': 6.7
|
|
},
|
|
{
|
|
'id': 7,
|
|
'category': 'تكاليف غير مباشرة',
|
|
'subcategory': 'إدارة',
|
|
'description': 'إشراف هندسي',
|
|
'amount': 35000,
|
|
'percentage': 5.2
|
|
},
|
|
{
|
|
'id': 8,
|
|
'category': 'تكاليف غير مباشرة',
|
|
'subcategory': 'عامة',
|
|
'description': 'تأمينات وضمانات',
|
|
'amount': 25000,
|
|
'percentage': 3.7
|
|
},
|
|
{
|
|
'id': 9,
|
|
'category': 'تكاليف غير مباشرة',
|
|
'subcategory': 'عامة',
|
|
'description': 'مصاريف إدارية',
|
|
'amount': 30000,
|
|
'percentage': 4.5
|
|
},
|
|
{
|
|
'id': 10,
|
|
'category': 'أرباح',
|
|
'subcategory': 'أرباح',
|
|
'description': 'هامش الربح',
|
|
'amount': 55000,
|
|
'percentage': 8.2
|
|
}
|
|
]
|
|
|
|
if 'price_scenarios' not in st.session_state:
|
|
st.session_state.price_scenarios = [
|
|
{
|
|
'id': 1,
|
|
'name': 'السيناريو الأساسي',
|
|
'description': 'التسعير الأساسي مع هامش ربح 8%',
|
|
'total_cost': 615000,
|
|
'profit_margin': 8.2,
|
|
'total_price': 670000,
|
|
'is_active': True
|
|
},
|
|
{
|
|
'id': 2,
|
|
'name': 'سيناريو تنافسي',
|
|
'description': 'تخفيض هامش الربح للمنافسة',
|
|
'total_cost': 615000,
|
|
'profit_margin': 5.0,
|
|
'total_price': 650000,
|
|
'is_active': False
|
|
},
|
|
{
|
|
'id': 3,
|
|
'name': 'سيناريو مرتفع',
|
|
'description': 'زيادة هامش الربح للمشاريع ذات المخاطر العالية',
|
|
'total_cost': 615000,
|
|
'profit_margin': 12.0,
|
|
'total_price': 700000,
|
|
'is_active': False
|
|
}
|
|
]
|
|
|
|
|
|
if 'competitive_analysis' not in st.session_state:
|
|
st.session_state.competitive_analysis = [
|
|
{
|
|
'id': 1,
|
|
'competitor': 'شركة الإنشاءات المتحدة',
|
|
'project_type': 'مباني سكنية',
|
|
'price_per_sqm': 1800,
|
|
'delivery_time': 12,
|
|
'quality_rating': 4.2,
|
|
'market_share': 15.5
|
|
},
|
|
{
|
|
'id': 2,
|
|
'competitor': 'مجموعة البناء الحديث',
|
|
'project_type': 'مباني سكنية',
|
|
'price_per_sqm': 2100,
|
|
'delivery_time': 10,
|
|
'quality_rating': 4.5,
|
|
'market_share': 18.2
|
|
},
|
|
{
|
|
'id': 3,
|
|
'competitor': 'شركة الإعمار الدولية',
|
|
'project_type': 'مباني سكنية',
|
|
'price_per_sqm': 2300,
|
|
'delivery_time': 14,
|
|
'quality_rating': 4.7,
|
|
'market_share': 22.0
|
|
},
|
|
{
|
|
'id': 4,
|
|
'competitor': 'مؤسسة البناء المتكامل',
|
|
'project_type': 'مباني سكنية',
|
|
'price_per_sqm': 1750,
|
|
'delivery_time': 15,
|
|
'quality_rating': 3.8,
|
|
'market_share': 12.5
|
|
},
|
|
{
|
|
'id': 5,
|
|
'competitor': 'شركتنا',
|
|
'project_type': 'مباني سكنية',
|
|
'price_per_sqm': 1950,
|
|
'delivery_time': 11,
|
|
'quality_rating': 4.4,
|
|
'market_share': 14.8
|
|
}
|
|
]
|
|
|
|
def run(self):
|
|
"""تشغيل وحدة التسعير"""
|
|
|
|
self.render()
|
|
|
|
def render(self):
|
|
"""عرض واجهة وحدة التسعير"""
|
|
|
|
st.markdown("<h1 class='module-title'>وحدة التسعير</h1>", unsafe_allow_html=True)
|
|
|
|
tabs = st.tabs([
|
|
"لوحة التحكم",
|
|
"جدول الكميات",
|
|
"تحليل التكاليف",
|
|
"سيناريوهات التسعير",
|
|
"المقارنة التنافسية",
|
|
"التقارير"
|
|
])
|
|
|
|
with tabs[0]:
|
|
self._render_dashboard_tab()
|
|
|
|
with tabs[1]:
|
|
self._render_bill_of_quantities_tab()
|
|
|
|
with tabs[2]:
|
|
self._render_cost_analysis_tab()
|
|
|
|
with tabs[3]:
|
|
self._render_pricing_scenarios_tab()
|
|
|
|
with tabs[4]:
|
|
self._render_competitive_analysis_tab()
|
|
|
|
with tabs[5]:
|
|
self._render_reports_tab()
|
|
|
|
def _render_dashboard_tab(self):
|
|
"""عرض تبويب لوحة التحكم"""
|
|
|
|
st.markdown("### لوحة تحكم التسعير")
|
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4)
|
|
|
|
|
|
total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
|
|
total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
|
|
total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
|
|
total_cost = total_direct_cost + total_indirect_cost
|
|
total_price = total_cost + total_profit
|
|
|
|
with col1:
|
|
st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.0f} ريال")
|
|
|
|
with col2:
|
|
st.metric("إجمالي التكاليف غير المباشرة", f"{total_indirect_cost:,.0f} ريال")
|
|
|
|
with col3:
|
|
st.metric("إجمالي التكاليف", f"{total_cost:,.0f} ريال")
|
|
|
|
with col4:
|
|
st.metric("السعر الإجمالي", f"{total_price:,.0f} ريال")
|
|
|
|
|
|
st.markdown("### توزيع التكاليف")
|
|
|
|
|
|
cost_categories = {}
|
|
|
|
for item in st.session_state.cost_analysis:
|
|
category = item['category']
|
|
if category in cost_categories:
|
|
cost_categories[category] += item['amount']
|
|
else:
|
|
cost_categories[category] = item['amount']
|
|
|
|
|
|
cost_df = pd.DataFrame({
|
|
'الفئة': list(cost_categories.keys()),
|
|
'المبلغ': list(cost_categories.values())
|
|
})
|
|
|
|
|
|
fig = px.pie(
|
|
cost_df,
|
|
values='المبلغ',
|
|
names='الفئة',
|
|
title='توزيع التكاليف حسب الفئة',
|
|
color_discrete_sequence=px.colors.qualitative.Set3
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### توزيع التكاليف المباشرة")
|
|
|
|
|
|
direct_cost_subcategories = {}
|
|
|
|
for item in st.session_state.cost_analysis:
|
|
if item['category'] == 'تكاليف مباشرة':
|
|
subcategory = item['subcategory']
|
|
if subcategory in direct_cost_subcategories:
|
|
direct_cost_subcategories[subcategory] += item['amount']
|
|
else:
|
|
direct_cost_subcategories[subcategory] = item['amount']
|
|
|
|
|
|
direct_cost_df = pd.DataFrame({
|
|
'الفئة الفرعية': list(direct_cost_subcategories.keys()),
|
|
'المبلغ': list(direct_cost_subcategories.values())
|
|
})
|
|
|
|
|
|
fig = px.pie(
|
|
direct_cost_df,
|
|
values='المبلغ',
|
|
names='الفئة الفرعية',
|
|
title='توزيع التكاليف المباشرة',
|
|
color_discrete_sequence=px.colors.qualitative.Pastel
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### توزيع التكاليف غير المباشرة")
|
|
|
|
|
|
indirect_cost_subcategories = {}
|
|
|
|
for item in st.session_state.cost_analysis:
|
|
if item['category'] == 'تكاليف غير مباشرة':
|
|
subcategory = item['subcategory']
|
|
if subcategory in indirect_cost_subcategories:
|
|
indirect_cost_subcategories[subcategory] += item['amount']
|
|
else:
|
|
indirect_cost_subcategories[subcategory] = item['amount']
|
|
|
|
|
|
indirect_cost_df = pd.DataFrame({
|
|
'الفئة الفرعية': list(indirect_cost_subcategories.keys()),
|
|
'المبلغ': list(indirect_cost_subcategories.values())
|
|
})
|
|
|
|
|
|
fig = px.pie(
|
|
indirect_cost_df,
|
|
values='المبلغ',
|
|
names='الفئة الفرعية',
|
|
title='توزيع التكاليف غير المباشرة',
|
|
color_discrete_sequence=px.colors.qualitative.Pastel1
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
def _render_bill_of_quantities_tab(self):
|
|
"""عرض تبويب جدول الكميات"""
|
|
|
|
st.markdown("### جدول الكميات")
|
|
|
|
|
|
boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
|
|
|
|
|
|
st.dataframe(
|
|
boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
|
|
column_config={
|
|
'code': 'الكود',
|
|
'description': 'الوصف',
|
|
'unit': 'الوحدة',
|
|
'quantity': 'الكمية',
|
|
'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
|
|
'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
|
|
'category': 'الفئة'
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
st.markdown("### إضافة بند جديد")
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
|
with col1:
|
|
new_code = st.text_input("الكود", key="new_boq_code")
|
|
new_description = st.text_input("الوصف", key="new_boq_description")
|
|
new_unit = st.selectbox("الوحدة", ["م3", "م2", "طن", "عدد", "متر طولي"], key="new_boq_unit")
|
|
|
|
with col2:
|
|
new_quantity = st.number_input("الكمية", min_value=0.0, step=1.0, key="new_boq_quantity")
|
|
new_unit_price = st.number_input("سعر الوحدة", min_value=0.0, step=10.0, key="new_boq_unit_price")
|
|
new_category = st.selectbox(
|
|
"الفئة",
|
|
[
|
|
"أعمال ترابية",
|
|
"أعمال خرسانية",
|
|
"أعمال حديد",
|
|
"أعمال بناء",
|
|
"أعمال تشطيبات",
|
|
"أعمال نجارة",
|
|
"أعمال ألمنيوم",
|
|
"أعمال كهربائية",
|
|
"أعمال ميكانيكية",
|
|
"أعمال صحية"
|
|
],
|
|
key="new_boq_category"
|
|
)
|
|
|
|
if st.button("إضافة البند", key="add_boq_item"):
|
|
if new_code and new_description and new_quantity > 0 and new_unit_price > 0:
|
|
|
|
new_total_price = new_quantity * new_unit_price
|
|
|
|
|
|
new_id = max([item['id'] for item in st.session_state.bill_of_quantities]) + 1
|
|
|
|
st.session_state.bill_of_quantities.append({
|
|
'id': new_id,
|
|
'code': new_code,
|
|
'description': new_description,
|
|
'unit': new_unit,
|
|
'quantity': new_quantity,
|
|
'unit_price': new_unit_price,
|
|
'total_price': new_total_price,
|
|
'category': new_category
|
|
})
|
|
|
|
st.success(f"تمت إضافة البند بنجاح: {new_description}")
|
|
|
|
|
|
st.rerun()
|
|
else:
|
|
st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
|
|
|
|
|
|
st.markdown("### ملخص جدول الكميات")
|
|
|
|
|
|
category_totals = {}
|
|
for item in st.session_state.bill_of_quantities:
|
|
category = item['category']
|
|
if category in category_totals:
|
|
category_totals[category] += item['total_price']
|
|
else:
|
|
category_totals[category] = item['total_price']
|
|
|
|
|
|
category_df = pd.DataFrame({
|
|
'الفئة': list(category_totals.keys()),
|
|
'المبلغ': list(category_totals.values())
|
|
})
|
|
|
|
|
|
category_df = category_df.sort_values('المبلغ', ascending=False)
|
|
|
|
|
|
fig = px.bar(
|
|
category_df,
|
|
x='الفئة',
|
|
y='المبلغ',
|
|
title='إجمالي تكلفة البنود حسب الفئة',
|
|
color='الفئة',
|
|
text_auto=True
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities)
|
|
|
|
def _render_cost_analysis_tab(self):
|
|
"""عرض تبويب تحليل التكاليف"""
|
|
|
|
st.markdown("### تحليل التكاليف")
|
|
|
|
|
|
cost_df = pd.DataFrame(st.session_state.cost_analysis)
|
|
|
|
st.dataframe(
|
|
cost_df[['category', 'subcategory', 'description', 'amount', 'percentage']],
|
|
column_config={
|
|
'category': 'الفئة',
|
|
'subcategory': 'الفئة الفرعية',
|
|
'description': 'الوصف',
|
|
'amount': st.column_config.NumberColumn('المبلغ', format='%d ريال'),
|
|
'percentage': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
st.markdown("### إضافة بند تكلفة جديد")
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
|
with col1:
|
|
new_category = st.selectbox(
|
|
"الفئة",
|
|
["تكاليف مباشرة", "تكاليف غير مباشرة", "أرباح"],
|
|
key="new_cost_category"
|
|
)
|
|
|
|
|
|
subcategory_options = []
|
|
if new_category == "تكاليف مباشرة":
|
|
subcategory_options = ["مواد", "عمالة", "معدات"]
|
|
elif new_category == "تكاليف غير مباشرة":
|
|
subcategory_options = ["إدارة", "عامة", "تمويل"]
|
|
else:
|
|
subcategory_options = ["أرباح"]
|
|
|
|
new_subcategory = st.selectbox(
|
|
"الفئة الفرعية",
|
|
subcategory_options,
|
|
key="new_cost_subcategory"
|
|
)
|
|
|
|
new_description = st.text_input("الوصف", key="new_cost_description")
|
|
|
|
with col2:
|
|
new_amount = st.number_input("المبلغ", min_value=0.0, step=1000.0, key="new_cost_amount")
|
|
|
|
|
|
total_cost = sum(item['amount'] for item in st.session_state.cost_analysis)
|
|
|
|
|
|
if total_cost > 0:
|
|
estimated_percentage = (new_amount / total_cost) * 100
|
|
else:
|
|
estimated_percentage = 0
|
|
|
|
st.metric("النسبة المئوية التقديرية", f"{estimated_percentage:.1f}%")
|
|
|
|
if st.button("إضافة بند التكلفة", key="add_cost_item"):
|
|
if new_description and new_amount > 0:
|
|
|
|
new_id = max([item['id'] for item in st.session_state.cost_analysis]) + 1
|
|
|
|
|
|
new_total = total_cost + new_amount
|
|
|
|
|
|
for item in st.session_state.cost_analysis:
|
|
item['percentage'] = (item['amount'] / new_total) * 100
|
|
|
|
|
|
st.session_state.cost_analysis.append({
|
|
'id': new_id,
|
|
'category': new_category,
|
|
'subcategory': new_subcategory,
|
|
'description': new_description,
|
|
'amount': new_amount,
|
|
'percentage': (new_amount / new_total) * 100
|
|
})
|
|
|
|
st.success(f"تمت إضافة بند التكلفة بنجاح: {new_description}")
|
|
|
|
|
|
st.rerun()
|
|
else:
|
|
st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
|
|
|
|
|
|
st.markdown("### تحليل التكاليف حسب الفئة والفئة الفرعية")
|
|
|
|
|
|
cost_by_category_subcategory = {}
|
|
|
|
for item in st.session_state.cost_analysis:
|
|
category = item['category']
|
|
subcategory = item['subcategory']
|
|
key = f"{category} - {subcategory}"
|
|
|
|
if key in cost_by_category_subcategory:
|
|
cost_by_category_subcategory[key] += item['amount']
|
|
else:
|
|
cost_by_category_subcategory[key] = item['amount']
|
|
|
|
|
|
cost_category_subcategory_df = pd.DataFrame({
|
|
'الفئة والفئة الفرعية': list(cost_by_category_subcategory.keys()),
|
|
'المبلغ': list(cost_by_category_subcategory.values())
|
|
})
|
|
|
|
|
|
cost_category_subcategory_df = cost_category_subcategory_df.sort_values('المبلغ', ascending=False)
|
|
|
|
|
|
fig = px.bar(
|
|
cost_category_subcategory_df,
|
|
x='الفئة والفئة الفرعية',
|
|
y='المبلغ',
|
|
title='تحليل التكاليف حسب الفئة والفئة الفرعية',
|
|
color='الفئة والفئة الفرعية',
|
|
text_auto=True
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### تحليل نسب التكاليف")
|
|
|
|
|
|
percentage_df = pd.DataFrame(st.session_state.cost_analysis)
|
|
|
|
fig = px.treemap(
|
|
percentage_df,
|
|
path=['category', 'subcategory', 'description'],
|
|
values='amount',
|
|
title='تحليل هيكل التكاليف',
|
|
color='percentage',
|
|
color_continuous_scale='RdBu',
|
|
color_continuous_midpoint=np.average(percentage_df['percentage'])
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### تحليل اتجاهات التكاليف")
|
|
|
|
|
|
months = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو']
|
|
direct_costs = [510000, 520000, 515000, 525000, 530000, 514000]
|
|
indirect_costs = [130000, 135000, 132000, 138000, 140000, 135000]
|
|
|
|
|
|
trends_df = pd.DataFrame({
|
|
'الشهر': months * 2,
|
|
'نوع التكلفة': ['تكاليف مباشرة'] * 6 + ['تكاليف غير مباشرة'] * 6,
|
|
'المبلغ': direct_costs + indirect_costs
|
|
})
|
|
|
|
|
|
fig = px.line(
|
|
trends_df,
|
|
x='الشهر',
|
|
y='المبلغ',
|
|
color='نوع التكلفة',
|
|
title='اتجاهات التكاليف على مدار الأشهر الستة الماضية',
|
|
markers=True
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
def _render_pricing_scenarios_tab(self):
|
|
"""عرض تبويب سيناريوهات التسعير"""
|
|
|
|
st.markdown("### سيناريوهات التسعير")
|
|
|
|
|
|
scenarios_df = pd.DataFrame(st.session_state.price_scenarios)
|
|
|
|
st.dataframe(
|
|
scenarios_df[['name', 'description', 'total_cost', 'profit_margin', 'total_price', 'is_active']],
|
|
column_config={
|
|
'name': 'اسم السيناريو',
|
|
'description': 'الوصف',
|
|
'total_cost': st.column_config.NumberColumn('إجمالي التكلفة', format='%d ريال'),
|
|
'profit_margin': st.column_config.NumberColumn('هامش الربح', format='%.1f%%'),
|
|
'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
|
|
'is_active': st.column_config.CheckboxColumn('نشط')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
st.markdown("### إنشاء سيناريو جديد")
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
|
|
|
total_cost = sum(item['amount'] for item in st.session_state.cost_analysis
|
|
if item['category'] != 'أرباح')
|
|
|
|
with col1:
|
|
new_name = st.text_input("اسم السيناريو", key="new_scenario_name")
|
|
new_description = st.text_input("وصف السيناريو", key="new_scenario_description")
|
|
|
|
with col2:
|
|
new_profit_margin = st.slider(
|
|
"هامش الربح (%)",
|
|
min_value=0.0,
|
|
max_value=30.0,
|
|
value=10.0,
|
|
step=0.5,
|
|
key="new_scenario_profit_margin"
|
|
)
|
|
|
|
|
|
profit_amount = total_cost * (new_profit_margin / 100)
|
|
new_total_price = total_cost + profit_amount
|
|
|
|
st.metric("إجمالي التكلفة", f"{total_cost:,.0f} ريال")
|
|
st.metric("السعر الإجمالي المقترح", f"{new_total_price:,.0f} ريال")
|
|
|
|
if st.button("إضافة السيناريو", key="add_scenario"):
|
|
if new_name and new_description:
|
|
|
|
new_id = max([item['id'] for item in st.session_state.price_scenarios]) + 1
|
|
|
|
st.session_state.price_scenarios.append({
|
|
'id': new_id,
|
|
'name': new_name,
|
|
'description': new_description,
|
|
'total_cost': total_cost,
|
|
'profit_margin': new_profit_margin,
|
|
'total_price': new_total_price,
|
|
'is_active': False
|
|
})
|
|
|
|
st.success(f"تمت إضافة السيناريو بنجاح: {new_name}")
|
|
|
|
|
|
st.rerun()
|
|
else:
|
|
st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
|
|
|
|
|
|
st.markdown("### مقارنة السيناريوهات")
|
|
|
|
|
|
fig = go.Figure()
|
|
|
|
for scenario in st.session_state.price_scenarios:
|
|
fig.add_trace(go.Bar(
|
|
name=scenario['name'],
|
|
x=['التكلفة', 'الربح', 'السعر الإجمالي'],
|
|
y=[
|
|
scenario['total_cost'],
|
|
scenario['total_price'] - scenario['total_cost'],
|
|
scenario['total_price']
|
|
],
|
|
text=[
|
|
f"{scenario['total_cost']:,.0f}",
|
|
f"{scenario['total_price'] - scenario['total_cost']:,.0f}",
|
|
f"{scenario['total_price']:,.0f}"
|
|
],
|
|
textposition='auto'
|
|
))
|
|
|
|
fig.update_layout(
|
|
title='مقارنة السيناريوهات',
|
|
barmode='group',
|
|
xaxis_title='العنصر',
|
|
yaxis_title='المبلغ (ريال)'
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### تحليل حساسية هامش الربح")
|
|
|
|
|
|
profit_margins = list(range(5, 26, 1))
|
|
total_prices = [total_cost * (1 + margin/100) for margin in profit_margins]
|
|
|
|
|
|
sensitivity_df = pd.DataFrame({
|
|
'هامش الربح (%)': profit_margins,
|
|
'السعر الإجمالي': total_prices
|
|
})
|
|
|
|
|
|
fig = px.line(
|
|
sensitivity_df,
|
|
x='هامش الربح (%)',
|
|
y='السعر الإجمالي',
|
|
title='تحليل حساسية هامش الربح',
|
|
markers=True
|
|
)
|
|
|
|
|
|
competitive_price = 650000
|
|
fig.add_hline(
|
|
y=competitive_price,
|
|
line_dash="dash",
|
|
line_color="red",
|
|
annotation_text="السعر التنافسي",
|
|
annotation_position="bottom right"
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### تفعيل/تعطيل السيناريوهات")
|
|
|
|
for i, scenario in enumerate(st.session_state.price_scenarios):
|
|
col1, col2 = st.columns([4, 1])
|
|
|
|
with col1:
|
|
st.write(f"**{scenario['name']}**: {scenario['description']}")
|
|
|
|
with col2:
|
|
is_active = st.checkbox(
|
|
"تفعيل",
|
|
value=scenario['is_active'],
|
|
key=f"activate_scenario_{scenario['id']}"
|
|
)
|
|
|
|
|
|
if is_active != scenario['is_active']:
|
|
|
|
if is_active:
|
|
for j, other_scenario in enumerate(st.session_state.price_scenarios):
|
|
if j != i:
|
|
other_scenario['is_active'] = False
|
|
|
|
|
|
scenario['is_active'] = is_active
|
|
|
|
|
|
st.rerun()
|
|
|
|
def _render_competitive_analysis_tab(self):
|
|
"""عرض تبويب المقارنة التنافسية"""
|
|
|
|
st.markdown("### المقارنة التنافسية")
|
|
|
|
|
|
competitive_df = pd.DataFrame(st.session_state.competitive_analysis)
|
|
|
|
st.dataframe(
|
|
competitive_df[['competitor', 'project_type', 'price_per_sqm', 'delivery_time', 'quality_rating', 'market_share']],
|
|
column_config={
|
|
'competitor': 'المنافس',
|
|
'project_type': 'نوع المشروع',
|
|
'price_per_sqm': st.column_config.NumberColumn('السعر لكل متر مربع', format='%d ريال'),
|
|
'delivery_time': st.column_config.NumberColumn('مدة التسليم (شهر)', format='%d'),
|
|
'quality_rating': st.column_config.NumberColumn('تقييم الجودة', format='%.1f/5.0'),
|
|
'market_share': st.column_config.NumberColumn('الحصة السوقية', format='%.1f%%')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
st.markdown("### إضافة منافس جديد")
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
|
with col1:
|
|
new_competitor = st.text_input("اسم المنافس", key="new_competitor_name")
|
|
new_project_type = st.selectbox(
|
|
"نوع المشروع",
|
|
["مباني سكنية", "مباني تجارية", "مباني صناعية", "بنية تحتية"],
|
|
key="new_competitor_project_type"
|
|
)
|
|
new_price_per_sqm = st.number_input(
|
|
"السعر لكل متر مربع (ريال)",
|
|
min_value=0,
|
|
step=50,
|
|
key="new_competitor_price"
|
|
)
|
|
|
|
with col2:
|
|
new_delivery_time = st.number_input(
|
|
"مدة التسليم (شهر)",
|
|
min_value=1,
|
|
max_value=36,
|
|
step=1,
|
|
key="new_competitor_delivery"
|
|
)
|
|
new_quality_rating = st.slider(
|
|
"تقييم الجودة",
|
|
min_value=1.0,
|
|
max_value=5.0,
|
|
value=3.5,
|
|
step=0.1,
|
|
key="new_competitor_quality"
|
|
)
|
|
new_market_share = st.number_input(
|
|
"الحصة السوقية (%)",
|
|
min_value=0.0,
|
|
max_value=100.0,
|
|
step=0.5,
|
|
key="new_competitor_market_share"
|
|
)
|
|
|
|
if st.button("إضافة منافس", key="add_competitor"):
|
|
if new_competitor and new_price_per_sqm > 0:
|
|
|
|
new_id = max([item['id'] for item in st.session_state.competitive_analysis]) + 1
|
|
|
|
st.session_state.competitive_analysis.append({
|
|
'id': new_id,
|
|
'competitor': new_competitor,
|
|
'project_type': new_project_type,
|
|
'price_per_sqm': new_price_per_sqm,
|
|
'delivery_time': new_delivery_time,
|
|
'quality_rating': new_quality_rating,
|
|
'market_share': new_market_share
|
|
})
|
|
|
|
st.success(f"تمت إضافة المنافس بنجاح: {new_competitor}")
|
|
|
|
|
|
st.rerun()
|
|
else:
|
|
st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح")
|
|
|
|
|
|
st.markdown("### مقارنة الأسعار")
|
|
|
|
|
|
price_comparison_df = pd.DataFrame(st.session_state.competitive_analysis)
|
|
|
|
|
|
price_comparison_df = price_comparison_df.sort_values('price_per_sqm')
|
|
|
|
|
|
fig = px.bar(
|
|
price_comparison_df,
|
|
x='competitor',
|
|
y='price_per_sqm',
|
|
title='مقارنة الأسعار لكل متر مربع',
|
|
color='competitor',
|
|
text_auto=True
|
|
)
|
|
|
|
|
|
for i, competitor in enumerate(price_comparison_df['competitor']):
|
|
if competitor == 'شركتنا':
|
|
fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(price_comparison_df))]
|
|
break
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### مقارنة الجودة والسعر")
|
|
|
|
|
|
fig = px.scatter(
|
|
price_comparison_df,
|
|
x='price_per_sqm',
|
|
y='quality_rating',
|
|
size='market_share',
|
|
color='competitor',
|
|
title='العلاقة بين السعر والجودة والحصة السوقية',
|
|
labels={
|
|
'price_per_sqm': 'السعر لكل متر مربع (ريال)',
|
|
'quality_rating': 'تقييم الجودة',
|
|
'market_share': 'الحصة السوقية (%)'
|
|
},
|
|
text='competitor'
|
|
)
|
|
|
|
fig.update_traces(textposition='top center')
|
|
|
|
|
|
fig.update_layout(
|
|
shapes=[
|
|
dict(
|
|
type='line',
|
|
x0=min(price_comparison_df['price_per_sqm']),
|
|
y0=min(price_comparison_df['quality_rating']),
|
|
x1=max(price_comparison_df['price_per_sqm']),
|
|
y1=max(price_comparison_df['quality_rating']),
|
|
line=dict(color='gray', dash='dash')
|
|
)
|
|
]
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### مقارنة مدة التسليم")
|
|
|
|
|
|
delivery_comparison_df = price_comparison_df.sort_values('delivery_time')
|
|
|
|
|
|
fig = px.bar(
|
|
delivery_comparison_df,
|
|
x='competitor',
|
|
y='delivery_time',
|
|
title='مقارنة مدة التسليم (شهر)',
|
|
color='competitor',
|
|
text_auto=True
|
|
)
|
|
|
|
|
|
for i, competitor in enumerate(delivery_comparison_df['competitor']):
|
|
if competitor == 'شركتنا':
|
|
fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(delivery_comparison_df))]
|
|
break
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### تحليل الحصة السوقية")
|
|
|
|
|
|
fig = px.pie(
|
|
price_comparison_df,
|
|
values='market_share',
|
|
names='competitor',
|
|
title='توزيع الحصة السوقية',
|
|
hole=0.4
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
def _render_reports_tab(self):
|
|
"""عرض تبويب التقارير"""
|
|
|
|
st.markdown("### تقارير التسعير")
|
|
|
|
|
|
report_type = st.selectbox(
|
|
"اختر نوع التقرير",
|
|
[
|
|
"ملخص التسعير",
|
|
"تقرير جدول الكميات",
|
|
"تقرير تحليل التكاليف",
|
|
"تقرير سيناريوهات التسعير",
|
|
"تقرير المقارنة التنافسية",
|
|
"التقرير الشامل"
|
|
]
|
|
)
|
|
|
|
|
|
col1, col2 = st.columns(2)
|
|
|
|
with col1:
|
|
project_name = st.text_input("اسم المشروع", "مشروع إنشاء مبنى سكني")
|
|
client_name = st.text_input("اسم العميل", "شركة التطوير العقاري")
|
|
|
|
with col2:
|
|
project_location = st.text_input("موقع المشروع", "الرياض، المملكة العربية السعودية")
|
|
report_date = st.date_input("تاريخ التقرير", datetime.now())
|
|
|
|
|
|
if st.button("إنشاء التقرير"):
|
|
st.markdown("### معاينة التقرير")
|
|
|
|
|
|
st.markdown(f"""
|
|
## {report_type}
|
|
**اسم المشروع:** {project_name}
|
|
**اسم العميل:** {client_name}
|
|
**موقع المشروع:** {project_location}
|
|
**تاريخ التقرير:** {report_date.strftime('%Y-%m-%d')}
|
|
""")
|
|
|
|
|
|
if report_type == "ملخص التسعير" or report_type == "التقرير الشامل":
|
|
self._render_pricing_summary_report()
|
|
|
|
if report_type == "تقرير جدول الكميات" or report_type == "التقرير الشامل":
|
|
self._render_boq_report()
|
|
|
|
if report_type == "تقرير تحليل التكاليف" or report_type == "التقرير الشامل":
|
|
self._render_cost_analysis_report()
|
|
|
|
if report_type == "تقرير سيناريوهات التسعير" or report_type == "التقرير الشامل":
|
|
self._render_pricing_scenarios_report()
|
|
|
|
if report_type == "تقرير المقارنة التنافسية" or report_type == "التقرير الشامل":
|
|
self._render_competitive_analysis_report()
|
|
|
|
|
|
st.markdown("### تصدير التقرير")
|
|
|
|
export_format = st.radio(
|
|
"اختر صيغة التصدير",
|
|
["PDF", "Excel", "Word"],
|
|
horizontal=True
|
|
)
|
|
|
|
if st.button("تصدير التقرير"):
|
|
st.success(f"تم تصدير التقرير بصيغة {export_format} بنجاح!")
|
|
|
|
def _render_pricing_summary_report(self):
|
|
"""عرض تقرير ملخص التسعير"""
|
|
|
|
st.markdown("## ملخص التسعير")
|
|
|
|
|
|
total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة')
|
|
total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة')
|
|
total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح')
|
|
total_cost = total_direct_cost + total_indirect_cost
|
|
total_price = total_cost + total_profit
|
|
|
|
|
|
st.markdown("### ملخص التكاليف")
|
|
|
|
summary_data = {
|
|
'البند': ['التكاليف المباشرة', 'التكاليف غير المباشرة', 'إجمالي التكاليف', 'هامش الربح', 'السعر الإجمالي'],
|
|
'المبلغ (ريال)': [total_direct_cost, total_indirect_cost, total_cost, total_profit, total_price],
|
|
'النسبة المئوية': [
|
|
total_direct_cost / total_price * 100,
|
|
total_indirect_cost / total_price * 100,
|
|
total_cost / total_price * 100,
|
|
total_profit / total_price * 100,
|
|
100.0
|
|
]
|
|
}
|
|
|
|
summary_df = pd.DataFrame(summary_data)
|
|
|
|
st.dataframe(
|
|
summary_df,
|
|
column_config={
|
|
'البند': st.column_config.TextColumn('البند'),
|
|
'المبلغ (ريال)': st.column_config.NumberColumn('المبلغ (ريال)', format='%d'),
|
|
'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
fig = px.pie(
|
|
summary_df.iloc[0:3],
|
|
values='المبلغ (ريال)',
|
|
names='البند',
|
|
title='توزيع التكاليف',
|
|
color_discrete_sequence=px.colors.qualitative.Set3
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
fig = px.pie(
|
|
summary_df.iloc[[2, 3]],
|
|
values='المبلغ (ريال)',
|
|
names='البند',
|
|
title='تركيبة السعر الإجمالي',
|
|
color_discrete_sequence=px.colors.qualitative.Pastel
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
def _render_boq_report(self):
|
|
"""عرض تقرير جدول الكميات"""
|
|
|
|
st.markdown("## تقرير جدول الكميات")
|
|
|
|
|
|
boq_df = pd.DataFrame(st.session_state.bill_of_quantities)
|
|
|
|
st.dataframe(
|
|
boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']],
|
|
column_config={
|
|
'code': 'الكود',
|
|
'description': 'الوصف',
|
|
'unit': 'الوحدة',
|
|
'quantity': 'الكمية',
|
|
'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'),
|
|
'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
|
|
'category': 'الفئة'
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
st.markdown("### ملخص جدول الكميات حسب الفئة")
|
|
|
|
|
|
category_totals = {}
|
|
for item in st.session_state.bill_of_quantities:
|
|
category = item['category']
|
|
if category in category_totals:
|
|
category_totals[category] += item['total_price']
|
|
else:
|
|
category_totals[category] = item['total_price']
|
|
|
|
|
|
category_summary_df = pd.DataFrame({
|
|
'الفئة': list(category_totals.keys()),
|
|
'المبلغ الإجمالي': list(category_totals.values())
|
|
})
|
|
|
|
|
|
total_boq = sum(category_totals.values())
|
|
category_summary_df['النسبة المئوية'] = category_summary_df['المبلغ الإجمالي'] / total_boq * 100
|
|
|
|
|
|
category_summary_df = category_summary_df.sort_values('المبلغ الإجمالي', ascending=False)
|
|
|
|
st.dataframe(
|
|
category_summary_df,
|
|
column_config={
|
|
'الفئة': st.column_config.TextColumn('الفئة'),
|
|
'المبلغ الإجمالي': st.column_config.NumberColumn('المبلغ الإجمالي', format='%d ريال'),
|
|
'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
fig = px.pie(
|
|
category_summary_df,
|
|
values='المبلغ الإجمالي',
|
|
names='الفئة',
|
|
title='توزيع تكاليف البنود حسب الفئة',
|
|
color_discrete_sequence=px.colors.qualitative.Set3
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
fig = px.bar(
|
|
category_summary_df,
|
|
x='الفئة',
|
|
y='المبلغ الإجمالي',
|
|
title='إجمالي تكلفة البنود حسب الفئة',
|
|
color='الفئة',
|
|
text_auto=True
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
def _render_cost_analysis_report(self):
|
|
"""عرض تقرير تحليل التكاليف"""
|
|
|
|
st.markdown("## تقرير تحليل التكاليف")
|
|
|
|
|
|
cost_df = pd.DataFrame(st.session_state.cost_analysis)
|
|
|
|
st.dataframe(
|
|
cost_df[['category', 'subcategory', 'description', 'amount', 'percentage']],
|
|
column_config={
|
|
'category': 'الفئة',
|
|
'subcategory': 'الفئة الفرعية',
|
|
'description': 'الوصف',
|
|
'amount': st.column_config.NumberColumn('المبلغ', format='%d ريال'),
|
|
'percentage': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
st.markdown("### ملخص تحليل التكاليف حسب الفئة")
|
|
|
|
|
|
category_totals = {}
|
|
for item in st.session_state.cost_analysis:
|
|
category = item['category']
|
|
if category in category_totals:
|
|
category_totals[category] += item['amount']
|
|
else:
|
|
category_totals[category] = item['amount']
|
|
|
|
|
|
category_summary_df = pd.DataFrame({
|
|
'الفئة': list(category_totals.keys()),
|
|
'المبلغ الإجمالي': list(category_totals.values())
|
|
})
|
|
|
|
|
|
total_cost = sum(category_totals.values())
|
|
category_summary_df['النسبة المئوية'] = category_summary_df['المبلغ الإجمالي'] / total_cost * 100
|
|
|
|
|
|
category_summary_df = category_summary_df.sort_values('المبلغ الإجمالي', ascending=False)
|
|
|
|
st.dataframe(
|
|
category_summary_df,
|
|
column_config={
|
|
'الفئة': st.column_config.TextColumn('الفئة'),
|
|
'المبلغ الإجمالي': st.column_config.NumberColumn('المبلغ الإجمالي', format='%d ريال'),
|
|
'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
fig = px.pie(
|
|
category_summary_df,
|
|
values='المبلغ الإجمالي',
|
|
names='الفئة',
|
|
title='توزيع التكاليف حسب الفئة',
|
|
color_discrete_sequence=px.colors.qualitative.Set3
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### ملخص تحليل التكاليف حسب الفئة والفئة الفرعية")
|
|
|
|
|
|
subcategory_totals = {}
|
|
for item in st.session_state.cost_analysis:
|
|
category = item['category']
|
|
subcategory = item['subcategory']
|
|
key = f"{category} - {subcategory}"
|
|
if key in subcategory_totals:
|
|
subcategory_totals[key] += item['amount']
|
|
else:
|
|
subcategory_totals[key] = item['amount']
|
|
|
|
|
|
subcategory_summary_df = pd.DataFrame({
|
|
'الفئة والفئة الفرعية': list(subcategory_totals.keys()),
|
|
'المبلغ الإجمالي': list(subcategory_totals.values())
|
|
})
|
|
|
|
|
|
subcategory_summary_df['النسبة المئوية'] = subcategory_summary_df['المبلغ الإجمالي'] / total_cost * 100
|
|
|
|
|
|
subcategory_summary_df = subcategory_summary_df.sort_values('المبلغ الإجمالي', ascending=False)
|
|
|
|
st.dataframe(
|
|
subcategory_summary_df,
|
|
column_config={
|
|
'الفئة والفئة الفرعية': st.column_config.TextColumn('الفئة والفئة الفرعية'),
|
|
'المبلغ الإجمالي': st.column_config.NumberColumn('المبلغ الإجمالي', format='%d ريال'),
|
|
'النسبة المئوية': st.column_config.NumberColumn('النسبة المئوية', format='%.1f%%')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
fig = px.bar(
|
|
subcategory_summary_df,
|
|
x='الفئة والفئة الفرعية',
|
|
y='المبلغ الإجمالي',
|
|
title='توزيع التكاليف حسب الفئة والفئة الفرعية',
|
|
color='الفئة والفئة الفرعية',
|
|
text_auto=True
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### تحليل هيكل التكاليف")
|
|
|
|
|
|
fig = px.treemap(
|
|
cost_df,
|
|
path=['category', 'subcategory', 'description'],
|
|
values='amount',
|
|
title='تحليل هيكل التكاليف',
|
|
color='percentage',
|
|
color_continuous_scale='RdBu',
|
|
color_continuous_midpoint=np.average(cost_df['percentage'])
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
def _render_pricing_scenarios_report(self):
|
|
"""عرض تقرير سيناريوهات التسعير"""
|
|
|
|
st.markdown("## تقرير سيناريوهات التسعير")
|
|
|
|
|
|
scenarios_df = pd.DataFrame(st.session_state.price_scenarios)
|
|
|
|
st.dataframe(
|
|
scenarios_df[['name', 'description', 'total_cost', 'profit_margin', 'total_price', 'is_active']],
|
|
column_config={
|
|
'name': 'اسم السيناريو',
|
|
'description': 'الوصف',
|
|
'total_cost': st.column_config.NumberColumn('إجمالي التكلفة', format='%d ريال'),
|
|
'profit_margin': st.column_config.NumberColumn('هامش الربح', format='%.1f%%'),
|
|
'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'),
|
|
'is_active': st.column_config.CheckboxColumn('نشط')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
st.markdown("### مقارنة السيناريوهات")
|
|
|
|
|
|
fig = go.Figure()
|
|
|
|
for scenario in st.session_state.price_scenarios:
|
|
fig.add_trace(go.Bar(
|
|
name=scenario['name'],
|
|
x=['التكلفة', 'الربح', 'السعر الإجمالي'],
|
|
y=[
|
|
scenario['total_cost'],
|
|
scenario['total_price'] - scenario['total_cost'],
|
|
scenario['total_price']
|
|
],
|
|
text=[
|
|
f"{scenario['total_cost']:,.0f}",
|
|
f"{scenario['total_price'] - scenario['total_cost']:,.0f}",
|
|
f"{scenario['total_price']:,.0f}"
|
|
],
|
|
textposition='auto'
|
|
))
|
|
|
|
fig.update_layout(
|
|
title='مقارنة السيناريوهات',
|
|
barmode='group',
|
|
xaxis_title='العنصر',
|
|
yaxis_title='المبلغ (ريال)'
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### مقارنة هوامش الربح")
|
|
|
|
|
|
profit_comparison_df = pd.DataFrame({
|
|
'السيناريو': [scenario['name'] for scenario in st.session_state.price_scenarios],
|
|
'هامش الربح (%)': [scenario['profit_margin'] for scenario in st.session_state.price_scenarios],
|
|
'مبلغ الربح (ريال)': [scenario['total_price'] - scenario['total_cost'] for scenario in st.session_state.price_scenarios]
|
|
})
|
|
|
|
|
|
profit_comparison_df = profit_comparison_df.sort_values('هامش الربح (%)', ascending=False)
|
|
|
|
|
|
fig = px.bar(
|
|
profit_comparison_df,
|
|
x='السيناريو',
|
|
y='هامش الربح (%)',
|
|
title='مقارنة هوامش الربح',
|
|
color='السيناريو',
|
|
text_auto=True
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### تحليل حساسية هامش الربح")
|
|
|
|
|
|
active_scenario = next((s for s in st.session_state.price_scenarios if s['is_active']), st.session_state.price_scenarios[0])
|
|
total_cost = active_scenario['total_cost']
|
|
|
|
|
|
profit_margins = list(range(5, 26, 1))
|
|
total_prices = [total_cost * (1 + margin/100) for margin in profit_margins]
|
|
|
|
|
|
sensitivity_df = pd.DataFrame({
|
|
'هامش الربح (%)': profit_margins,
|
|
'السعر الإجمالي': total_prices
|
|
})
|
|
|
|
|
|
fig = px.line(
|
|
sensitivity_df,
|
|
x='هامش الربح (%)',
|
|
y='السعر الإجمالي',
|
|
title='تحليل حساسية هامش الربح',
|
|
markers=True
|
|
)
|
|
|
|
|
|
competitive_price = 650000
|
|
fig.add_hline(
|
|
y=competitive_price,
|
|
line_dash="dash",
|
|
line_color="red",
|
|
annotation_text="السعر التنافسي",
|
|
annotation_position="bottom right"
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
def _render_competitive_analysis_report(self):
|
|
"""عرض تقرير المقارنة التنافسية"""
|
|
|
|
st.markdown("## تقرير المقارنة التنافسية")
|
|
|
|
|
|
competitive_df = pd.DataFrame(st.session_state.competitive_analysis)
|
|
|
|
st.dataframe(
|
|
competitive_df[['competitor', 'project_type', 'price_per_sqm', 'delivery_time', 'quality_rating', 'market_share']],
|
|
column_config={
|
|
'competitor': 'المنافس',
|
|
'project_type': 'نوع المشروع',
|
|
'price_per_sqm': st.column_config.NumberColumn('السعر لكل متر مربع', format='%d ريال'),
|
|
'delivery_time': st.column_config.NumberColumn('مدة التسليم (شهر)', format='%d'),
|
|
'quality_rating': st.column_config.NumberColumn('تقييم الجودة', format='%.1f/5.0'),
|
|
'market_share': st.column_config.NumberColumn('الحصة السوقية', format='%.1f%%')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
st.markdown("### مقارنة الأسعار")
|
|
|
|
|
|
price_comparison_df = competitive_df.sort_values('price_per_sqm')
|
|
|
|
|
|
fig = px.bar(
|
|
price_comparison_df,
|
|
x='competitor',
|
|
y='price_per_sqm',
|
|
title='مقارنة الأسعار لكل متر مربع',
|
|
color='competitor',
|
|
text_auto=True
|
|
)
|
|
|
|
|
|
for i, competitor in enumerate(price_comparison_df['competitor']):
|
|
if competitor == 'شركتنا':
|
|
fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(price_comparison_df))]
|
|
break
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### مقارنة الجودة والسعر")
|
|
|
|
|
|
fig = px.scatter(
|
|
price_comparison_df,
|
|
x='price_per_sqm',
|
|
y='quality_rating',
|
|
size='market_share',
|
|
color='competitor',
|
|
title='العلاقة بين السعر والجودة والحصة السوقية',
|
|
labels={
|
|
'price_per_sqm': 'السعر لكل متر مربع (ريال)',
|
|
'quality_rating': 'تقييم الجودة',
|
|
'market_share': 'الحصة السوقية (%)'
|
|
},
|
|
text='competitor'
|
|
)
|
|
|
|
fig.update_traces(textposition='top center')
|
|
|
|
|
|
fig.update_layout(
|
|
shapes=[
|
|
dict(
|
|
type='line',
|
|
x0=min(price_comparison_df['price_per_sqm']),
|
|
y0=min(price_comparison_df['quality_rating']),
|
|
x1=max(price_comparison_df['price_per_sqm']),
|
|
y1=max(price_comparison_df['quality_rating']),
|
|
line=dict(color='gray', dash='dash')
|
|
)
|
|
]
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### مقارنة مدة التسليم")
|
|
|
|
|
|
delivery_comparison_df = competitive_df.sort_values('delivery_time')
|
|
|
|
|
|
fig = px.bar(
|
|
delivery_comparison_df,
|
|
x='competitor',
|
|
y='delivery_time',
|
|
title='مقارنة مدة التسليم (شهر)',
|
|
color='competitor',
|
|
text_auto=True
|
|
)
|
|
|
|
|
|
for i, competitor in enumerate(delivery_comparison_df['competitor']):
|
|
if competitor == 'شركتنا':
|
|
fig.data[0].marker.color = ['blue' if x != i else 'red' for x in range(len(delivery_comparison_df))]
|
|
break
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### تحليل الحصة السوقية")
|
|
|
|
|
|
fig = px.pie(
|
|
competitive_df,
|
|
values='market_share',
|
|
names='competitor',
|
|
title='توزيع الحصة السوقية',
|
|
hole=0.4
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
st.markdown("### تحليل الموقع التنافسي")
|
|
|
|
|
|
our_company = next((item for item in st.session_state.competitive_analysis if item['competitor'] == 'شركتنا'), None)
|
|
|
|
if our_company:
|
|
|
|
avg_price = competitive_df['price_per_sqm'].mean()
|
|
avg_delivery = competitive_df['delivery_time'].mean()
|
|
avg_quality = competitive_df['quality_rating'].mean()
|
|
|
|
|
|
comparison_data = {
|
|
'المؤشر': ['السعر لكل متر مربع', 'مدة التسليم', 'تقييم الجودة'],
|
|
'قيمة شركتنا': [our_company['price_per_sqm'], our_company['delivery_time'], our_company['quality_rating']],
|
|
'متوسط السوق': [avg_price, avg_delivery, avg_quality],
|
|
'الفرق (%)': [
|
|
(our_company['price_per_sqm'] - avg_price) / avg_price * 100,
|
|
(our_company['delivery_time'] - avg_delivery) / avg_delivery * 100,
|
|
(our_company['quality_rating'] - avg_quality) / avg_quality * 100
|
|
]
|
|
}
|
|
|
|
comparison_df = pd.DataFrame(comparison_data)
|
|
|
|
st.dataframe(
|
|
comparison_df,
|
|
column_config={
|
|
'المؤشر': st.column_config.TextColumn('المؤشر'),
|
|
'قيمة شركتنا': st.column_config.NumberColumn('قيمة شركتنا'),
|
|
'متوسط السوق': st.column_config.NumberColumn('متوسط السوق'),
|
|
'الفرق (%)': st.column_config.NumberColumn('الفرق (%)', format='%+.1f%%')
|
|
},
|
|
hide_index=True,
|
|
use_container_width=True
|
|
)
|
|
|
|
|
|
|
|
max_price = competitive_df['price_per_sqm'].max()
|
|
min_price = competitive_df['price_per_sqm'].min()
|
|
price_range = max_price - min_price
|
|
|
|
max_delivery = competitive_df['delivery_time'].max()
|
|
min_delivery = competitive_df['delivery_time'].min()
|
|
delivery_range = max_delivery - min_delivery
|
|
|
|
|
|
normalized_price = 100 - ((our_company['price_per_sqm'] - min_price) / price_range * 100) if price_range > 0 else 50
|
|
normalized_delivery = 100 - ((our_company['delivery_time'] - min_delivery) / delivery_range * 100) if delivery_range > 0 else 50
|
|
normalized_quality = (our_company['quality_rating'] / 5) * 100
|
|
normalized_market_share = (our_company['market_share'] / competitive_df['market_share'].max()) * 100
|
|
|
|
|
|
fig = go.Figure()
|
|
|
|
fig.add_trace(go.Scatterpolar(
|
|
r=[normalized_price, normalized_delivery, normalized_quality, normalized_market_share],
|
|
theta=['السعر التنافسي', 'سرعة التسليم', 'الجودة', 'الحصة السوقية'],
|
|
fill='toself',
|
|
name='شركتنا'
|
|
))
|
|
|
|
fig.update_layout(
|
|
polar=dict(
|
|
radialaxis=dict(
|
|
visible=True,
|
|
range=[0, 100]
|
|
)
|
|
),
|
|
title='تحليل الموقع التنافسي لشركتنا'
|
|
)
|
|
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|