|
""" |
|
وحدة التسعير - التطبيق الرئيسي |
|
""" |
|
|
|
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': 'B-001', |
|
'description': 'توريد وتركيب حديد تسليح', |
|
'unit': 'طن', |
|
'quantity': 15, |
|
'unit_price': 3500, |
|
'total_price': 52500, |
|
'category': 'أعمال حديد' |
|
}, |
|
{ |
|
'id': 5, |
|
'code': 'C-001', |
|
'description': 'توريد وبناء طابوق', |
|
'unit': 'م2', |
|
'quantity': 450, |
|
'unit_price': 120, |
|
'total_price': 54000, |
|
'category': 'أعمال بناء' |
|
} |
|
] |
|
|
|
if 'cost_analysis' not in st.session_state: |
|
st.session_state.cost_analysis = [ |
|
{ |
|
'id': 1, |
|
'category': 'تكاليف مباشرة', |
|
'subcategory': 'مواد', |
|
'description': 'خرسانة', |
|
'amount': 168500, |
|
'percentage': 25.2 |
|
}, |
|
{ |
|
'id': 2, |
|
'category': 'تكاليف مباشرة', |
|
'subcategory': 'مواد', |
|
'description': 'حديد تسليح', |
|
'amount': 52500, |
|
'percentage': 7.8 |
|
}, |
|
{ |
|
'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 |
|
} |
|
] |
|
|
|
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.bar( |
|
direct_cost_df, |
|
x='الفئة الفرعية', |
|
y='المبلغ', |
|
title='توزيع التكاليف المباشرة', |
|
color='الفئة الفرعية', |
|
text_auto='.2s' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("### مقارنة سيناريوهات التسعير") |
|
|
|
|
|
scenarios_df = pd.DataFrame({ |
|
'السيناريو': [item['name'] for item in st.session_state.price_scenarios], |
|
'التكلفة الإجمالية': [item['total_cost'] for item in st.session_state.price_scenarios], |
|
'هامش الربح (%)': [item['profit_margin'] for item in st.session_state.price_scenarios], |
|
'السعر الإجمالي': [item['total_price'] for item in st.session_state.price_scenarios] |
|
}) |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
x=scenarios_df['السيناريو'], |
|
y=scenarios_df['التكلفة الإجمالية'], |
|
name='التكلفة الإجمالية', |
|
marker_color='indianred' |
|
)) |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
x=scenarios_df['السيناريو'], |
|
y=scenarios_df['السعر الإجمالي'], |
|
name='السعر الإجمالي', |
|
marker_color='lightsalmon' |
|
)) |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
x=scenarios_df['السيناريو'], |
|
y=scenarios_df['هامش الربح (%)'] * 10000, |
|
name='هامش الربح (%)', |
|
yaxis='y2', |
|
line=dict(color='royalblue', width=4) |
|
)) |
|
|
|
|
|
fig.update_layout( |
|
title='مقارنة سيناريوهات التسعير', |
|
xaxis_title='السيناريو', |
|
yaxis_title='المبلغ (ريال)', |
|
yaxis2=dict( |
|
title='هامش الربح (%)', |
|
titlefont=dict(color='royalblue'), |
|
tickfont=dict(color='royalblue'), |
|
overlaying='y', |
|
side='right', |
|
range=[0, 20] |
|
), |
|
barmode='group', |
|
legend=dict( |
|
x=0, |
|
y=1.2, |
|
orientation='h' |
|
) |
|
) |
|
|
|
|
|
fig.update_traces( |
|
texttemplate='%{y:,.0f}', |
|
textposition='outside' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("### مؤشرات الأداء الرئيسية") |
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
with col1: |
|
|
|
direct_cost_percentage = (total_direct_cost / total_cost) * 100 |
|
st.metric("نسبة التكاليف المباشرة", f"{direct_cost_percentage:.1f}%") |
|
|
|
with col2: |
|
|
|
indirect_cost_percentage = (total_indirect_cost / total_cost) * 100 |
|
st.metric("نسبة التكاليف غير المباشرة", f"{indirect_cost_percentage:.1f}%") |
|
|
|
with col3: |
|
|
|
profit_margin = (total_profit / total_price) * 100 |
|
st.metric("هامش الربح", f"{profit_margin:.1f}%") |
|
|
|
def _render_bill_of_quantities_tab(self): |
|
"""عرض تبويب جدول الكميات""" |
|
|
|
st.markdown("### جدول الكميات") |
|
|
|
|
|
st.markdown("#### قائمة البنود") |
|
|
|
|
|
boq_df = pd.DataFrame(st.session_state.bill_of_quantities) |
|
|
|
|
|
edited_df = st.data_editor( |
|
boq_df, |
|
column_config={ |
|
"id": st.column_config.NumberColumn("الرقم", disabled=True), |
|
"code": st.column_config.TextColumn("الكود"), |
|
"description": st.column_config.TextColumn("الوصف"), |
|
"unit": st.column_config.SelectboxColumn( |
|
"الوحدة", |
|
options=["م3", "م2", "طن", "كجم", "عدد", "لتر", "متر"] |
|
), |
|
"quantity": st.column_config.NumberColumn("الكمية", min_value=0), |
|
"unit_price": st.column_config.NumberColumn("سعر الوحدة (ريال)", min_value=0, format="%.2f"), |
|
"total_price": st.column_config.NumberColumn("السعر الإجمالي (ريال)", min_value=0, format="%.2f", disabled=True), |
|
"category": st.column_config.SelectboxColumn( |
|
"الفئة", |
|
options=["أعمال ترابية", "أعمال خرسانية", "أعمال حديد", "أعمال بناء", "أعمال تشطيب", "أعمال كهربائية", "أعمال ميكانيكية", "أعمال صحية", "أخرى"] |
|
) |
|
}, |
|
use_container_width=True, |
|
hide_index=True, |
|
num_rows="dynamic" |
|
) |
|
|
|
|
|
for i, row in edited_df.iterrows(): |
|
edited_df.at[i, 'total_price'] = row['quantity'] * row['unit_price'] |
|
|
|
|
|
if not edited_df.equals(boq_df): |
|
st.session_state.bill_of_quantities = edited_df.to_dict('records') |
|
st.success("تم تحديث جدول الكميات بنجاح!") |
|
|
|
|
|
total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities) |
|
st.metric("إجمالي جدول الكميات", f"{total_boq:,.2f} ريال") |
|
|
|
|
|
st.markdown("#### إضافة بند جديد") |
|
|
|
with st.form(key="add_boq_item_form"): |
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
new_code = st.text_input("الكود", key="new_boq_code") |
|
new_description = st.text_area("الوصف", key="new_boq_description") |
|
new_category = st.selectbox( |
|
"الفئة", |
|
["أعمال ترابية", "أعمال خرسانية", "أعمال حديد", "أعمال بناء", "أعمال تشطيب", "أعمال كهربائية", "أعمال ميكانيكية", "أعمال صحية", "أخرى"], |
|
key="new_boq_category" |
|
) |
|
|
|
with col2: |
|
new_unit = st.selectbox( |
|
"الوحدة", |
|
["م3", "م2", "طن", "كجم", "عدد", "لتر", "متر"], |
|
key="new_boq_unit" |
|
) |
|
new_quantity = st.number_input("الكمية", min_value=0.0, key="new_boq_quantity") |
|
new_unit_price = st.number_input("سعر الوحدة (ريال)", min_value=0.0, key="new_boq_unit_price") |
|
|
|
submit_button = st.form_submit_button("إضافة بند") |
|
|
|
if submit_button: |
|
if new_code and new_description: |
|
|
|
new_id = max([item['id'] for item in st.session_state.bill_of_quantities], default=0) + 1 |
|
|
|
|
|
new_total_price = new_quantity * new_unit_price |
|
|
|
|
|
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_code}' بنجاح!") |
|
st.rerun() |
|
else: |
|
st.error("يرجى إدخال الكود والوصف.") |
|
|
|
|
|
st.markdown("#### تحليل جدول الكميات") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
|
|
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()) |
|
}) |
|
|
|
fig = px.pie( |
|
category_df, |
|
values='المبلغ', |
|
names='الفئة', |
|
title='توزيع جدول الكميات حسب الفئة' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
with col2: |
|
|
|
top_items = sorted(st.session_state.bill_of_quantities, key=lambda x: x['total_price'], reverse=True)[:5] |
|
|
|
top_items_df = pd.DataFrame({ |
|
'البند': [item['code'] + ' - ' + item['description'][:20] + '...' for item in top_items], |
|
'القيمة': [item['total_price'] for item in top_items] |
|
}) |
|
|
|
fig = px.bar( |
|
top_items_df, |
|
x='البند', |
|
y='القيمة', |
|
title='أعلى 5 بنود من حيث القيمة', |
|
color='القيمة', |
|
text_auto='.2s' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("#### استيراد وتصدير جدول الكميات") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
if st.button("تصدير جدول الكميات إلى Excel", key="export_boq_button"): |
|
|
|
st.success("تم تصدير جدول الكميات إلى Excel بنجاح!") |
|
|
|
with col2: |
|
uploaded_file = st.file_uploader("استيراد جدول الكميات من Excel", type=["xlsx"], key="import_boq_file") |
|
|
|
if uploaded_file is not None: |
|
if st.button("استيراد البيانات", key="import_boq_button"): |
|
|
|
st.success("تم استيراد جدول الكميات بنجاح!") |
|
|
|
def _render_cost_analysis_tab(self): |
|
"""عرض تبويب تحليل التكاليف""" |
|
|
|
st.markdown("### تحليل التكاليف") |
|
|
|
|
|
st.markdown("#### قائمة التكاليف") |
|
|
|
|
|
cost_df = pd.DataFrame(st.session_state.cost_analysis) |
|
|
|
|
|
edited_df = st.data_editor( |
|
cost_df, |
|
column_config={ |
|
"id": st.column_config.NumberColumn("الرقم", disabled=True), |
|
"category": st.column_config.SelectboxColumn( |
|
"الفئة", |
|
options=["تكاليف مباشرة", "تكاليف غير مباشرة", "أرباح"] |
|
), |
|
"subcategory": st.column_config.TextColumn("الفئة الفرعية"), |
|
"description": st.column_config.TextColumn("الوصف"), |
|
"amount": st.column_config.NumberColumn("المبلغ (ريال)", min_value=0, format="%.2f"), |
|
"percentage": st.column_config.NumberColumn("النسبة (%)", min_value=0, format="%.1f", disabled=True) |
|
}, |
|
use_container_width=True, |
|
hide_index=True, |
|
num_rows="dynamic" |
|
) |
|
|
|
|
|
total_amount = sum(item['amount'] for item in st.session_state.cost_analysis) |
|
|
|
|
|
for i, row in edited_df.iterrows(): |
|
edited_df.at[i, 'percentage'] = (row['amount'] / total_amount) * 100 |
|
|
|
|
|
if not edited_df.equals(cost_df): |
|
st.session_state.cost_analysis = edited_df.to_dict('records') |
|
st.success("تم تحديث تحليل التكاليف بنجاح!") |
|
|
|
|
|
st.metric("إجمالي التكاليف", f"{total_amount:,.2f} ريال") |
|
|
|
|
|
st.markdown("#### إضافة تكلفة جديدة") |
|
|
|
with st.form(key="add_cost_item_form"): |
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
new_category = st.selectbox( |
|
"الفئة", |
|
["تكاليف مباشرة", "تكاليف غير مباشرة", "أرباح"], |
|
key="new_cost_category" |
|
) |
|
new_subcategory = st.text_input("الفئة الفرعية", key="new_cost_subcategory") |
|
|
|
with col2: |
|
new_description = st.text_input("الوصف", key="new_cost_description") |
|
new_amount = st.number_input("المبلغ (ريال)", min_value=0.0, key="new_cost_amount") |
|
|
|
submit_button = st.form_submit_button("إضافة تكلفة") |
|
|
|
if submit_button: |
|
if new_description and new_subcategory: |
|
|
|
new_id = max([item['id'] for item in st.session_state.cost_analysis], default=0) + 1 |
|
|
|
|
|
new_percentage = (new_amount / (total_amount + new_amount)) * 100 |
|
|
|
|
|
st.session_state.cost_analysis.append({ |
|
'id': new_id, |
|
'category': new_category, |
|
'subcategory': new_subcategory, |
|
'description': new_description, |
|
'amount': new_amount, |
|
'percentage': new_percentage |
|
}) |
|
|
|
|
|
new_total = total_amount + new_amount |
|
for item in st.session_state.cost_analysis: |
|
item['percentage'] = (item['amount'] / new_total) * 100 |
|
|
|
st.success(f"تمت إضافة التكلفة '{new_description}' بنجاح!") |
|
st.rerun() |
|
else: |
|
st.error("يرجى إدخال الفئة الفرعية والوصف.") |
|
|
|
|
|
st.markdown("#### تحليل التكاليف") |
|
|
|
|
|
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_df = pd.DataFrame({ |
|
'الفئة': list(category_totals.keys()), |
|
'المبلغ': list(category_totals.values()) |
|
}) |
|
|
|
fig = px.pie( |
|
category_df, |
|
values='المبلغ', |
|
names='الفئة', |
|
title='توزيع التكاليف حسب الفئة', |
|
color_discrete_sequence=px.colors.qualitative.Set3 |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("##### تحليل التكاليف المباشرة") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
|
|
direct_subcategory_totals = {} |
|
|
|
for item in st.session_state.cost_analysis: |
|
if item['category'] == 'تكاليف مباشرة': |
|
subcategory = item['subcategory'] |
|
if subcategory in direct_subcategory_totals: |
|
direct_subcategory_totals[subcategory] += item['amount'] |
|
else: |
|
direct_subcategory_totals[subcategory] = item['amount'] |
|
|
|
direct_subcategory_df = pd.DataFrame({ |
|
'الفئة الفرعية': list(direct_subcategory_totals.keys()), |
|
'المبلغ': list(direct_subcategory_totals.values()) |
|
}) |
|
|
|
fig = px.pie( |
|
direct_subcategory_df, |
|
values='المبلغ', |
|
names='الفئة الفرعية', |
|
title='توزيع التكاليف المباشرة حسب الفئة الفرعية' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
with col2: |
|
|
|
direct_description_totals = {} |
|
|
|
for item in st.session_state.cost_analysis: |
|
if item['category'] == 'تكاليف مباشرة': |
|
description = item['description'] |
|
if description in direct_description_totals: |
|
direct_description_totals[description] += item['amount'] |
|
else: |
|
direct_description_totals[description] = item['amount'] |
|
|
|
direct_description_df = pd.DataFrame({ |
|
'الوصف': list(direct_description_totals.keys()), |
|
'المبلغ': list(direct_description_totals.values()) |
|
}) |
|
|
|
|
|
direct_description_df = direct_description_df.sort_values(by='المبلغ', ascending=False) |
|
|
|
fig = px.bar( |
|
direct_description_df, |
|
x='الوصف', |
|
y='المبلغ', |
|
title='توزيع التكاليف المباشرة حسب البند', |
|
color='المبلغ', |
|
text_auto='.2s' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("##### تحليل التكاليف غير المباشرة") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
|
|
indirect_subcategory_totals = {} |
|
|
|
for item in st.session_state.cost_analysis: |
|
if item['category'] == 'تكاليف غير مباشرة': |
|
subcategory = item['subcategory'] |
|
if subcategory in indirect_subcategory_totals: |
|
indirect_subcategory_totals[subcategory] += item['amount'] |
|
else: |
|
indirect_subcategory_totals[subcategory] = item['amount'] |
|
|
|
indirect_subcategory_df = pd.DataFrame({ |
|
'الفئة الفرعية': list(indirect_subcategory_totals.keys()), |
|
'المبلغ': list(indirect_subcategory_totals.values()) |
|
}) |
|
|
|
fig = px.pie( |
|
indirect_subcategory_df, |
|
values='المبلغ', |
|
names='الفئة الفرعية', |
|
title='توزيع التكاليف غير المباشرة حسب الفئة الفرعية' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
with col2: |
|
|
|
indirect_description_totals = {} |
|
|
|
for item in st.session_state.cost_analysis: |
|
if item['category'] == 'تكاليف غير مباشرة': |
|
description = item['description'] |
|
if description in indirect_description_totals: |
|
indirect_description_totals[description] += item['amount'] |
|
else: |
|
indirect_description_totals[description] = item['amount'] |
|
|
|
indirect_description_df = pd.DataFrame({ |
|
'الوصف': list(indirect_description_totals.keys()), |
|
'المبلغ': list(indirect_description_totals.values()) |
|
}) |
|
|
|
|
|
indirect_description_df = indirect_description_df.sort_values(by='المبلغ', ascending=False) |
|
|
|
fig = px.bar( |
|
indirect_description_df, |
|
x='الوصف', |
|
y='المبلغ', |
|
title='توزيع التكاليف غير المباشرة حسب البند', |
|
color='المبلغ', |
|
text_auto='.2s' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("#### استيراد وتصدير تحليل التكاليف") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
if st.button("تصدير تحليل التكاليف إلى Excel", key="export_cost_button"): |
|
|
|
st.success("تم تصدير تحليل التكاليف إلى Excel بنجاح!") |
|
|
|
with col2: |
|
uploaded_file = st.file_uploader("استيراد تحليل التكاليف من Excel", type=["xlsx"], key="import_cost_file") |
|
|
|
if uploaded_file is not None: |
|
if st.button("استيراد البيانات", key="import_cost_button"): |
|
|
|
st.success("تم استيراد تحليل التكاليف بنجاح!") |
|
|
|
def _render_pricing_scenarios_tab(self): |
|
"""عرض تبويب سيناريوهات التسعير""" |
|
|
|
st.markdown("### سيناريوهات التسعير") |
|
|
|
|
|
st.markdown("#### قائمة السيناريوهات") |
|
|
|
|
|
scenarios_df = pd.DataFrame(st.session_state.price_scenarios) |
|
|
|
|
|
edited_df = st.data_editor( |
|
scenarios_df, |
|
column_config={ |
|
"id": st.column_config.NumberColumn("الرقم", disabled=True), |
|
"name": st.column_config.TextColumn("اسم السيناريو"), |
|
"description": st.column_config.TextColumn("الوصف"), |
|
"total_cost": st.column_config.NumberColumn("إجمالي التكلفة (ريال)", min_value=0, format="%.2f"), |
|
"profit_margin": st.column_config.NumberColumn("هامش الربح (%)", min_value=0, format="%.1f"), |
|
"total_price": st.column_config.NumberColumn("السعر الإجمالي (ريال)", min_value=0, format="%.2f"), |
|
"is_active": st.column_config.CheckboxColumn("نشط") |
|
}, |
|
use_container_width=True, |
|
hide_index=True, |
|
num_rows="dynamic" |
|
) |
|
|
|
|
|
if not edited_df.equals(scenarios_df): |
|
|
|
active_count = sum(edited_df['is_active']) |
|
if active_count != 1: |
|
st.error("يجب أن يكون هناك سيناريو نشط واحد فقط.") |
|
else: |
|
st.session_state.price_scenarios = edited_df.to_dict('records') |
|
st.success("تم تحديث سيناريوهات التسعير بنجاح!") |
|
|
|
|
|
st.markdown("#### إضافة سيناريو جديد") |
|
|
|
with st.form(key="add_scenario_form"): |
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
new_name = st.text_input("اسم السيناريو", key="new_scenario_name") |
|
new_description = st.text_area("الوصف", key="new_scenario_description") |
|
|
|
with col2: |
|
|
|
total_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] != 'أرباح') |
|
|
|
st.number_input("إجمالي التكلفة (ريال)", min_value=0.0, value=total_cost, key="new_scenario_total_cost", disabled=True) |
|
new_profit_margin = st.number_input("هامش الربح (%)", min_value=0.0, max_value=100.0, value=10.0, key="new_scenario_profit_margin") |
|
|
|
|
|
new_profit_amount = total_cost * (new_profit_margin / 100) |
|
new_total_price = total_cost + new_profit_amount |
|
|
|
st.number_input("السعر الإجمالي (ريال)", min_value=0.0, value=new_total_price, key="new_scenario_total_price", disabled=True) |
|
new_is_active = st.checkbox("نشط", key="new_scenario_is_active") |
|
|
|
submit_button = st.form_submit_button("إضافة سيناريو") |
|
|
|
if submit_button: |
|
if new_name: |
|
|
|
if new_is_active: |
|
|
|
for scenario in st.session_state.price_scenarios: |
|
scenario['is_active'] = False |
|
|
|
|
|
new_id = max([item['id'] for item in st.session_state.price_scenarios], default=0) + 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': new_is_active |
|
}) |
|
|
|
st.success(f"تمت إضافة السيناريو '{new_name}' بنجاح!") |
|
st.rerun() |
|
else: |
|
st.error("يرجى إدخال اسم السيناريو.") |
|
|
|
|
|
st.markdown("#### تحليل السيناريوهات") |
|
|
|
|
|
st.markdown("##### مقارنة السيناريوهات") |
|
|
|
|
|
scenarios_comparison_df = pd.DataFrame({ |
|
'السيناريو': [item['name'] for item in st.session_state.price_scenarios], |
|
'التكلفة الإجمالية': [item['total_cost'] for item in st.session_state.price_scenarios], |
|
'هامش الربح (%)': [item['profit_margin'] for item in st.session_state.price_scenarios], |
|
'السعر الإجمالي': [item['total_price'] for item in st.session_state.price_scenarios], |
|
'الحالة': ['نشط' if item['is_active'] else 'غير نشط' for item in st.session_state.price_scenarios] |
|
}) |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
x=scenarios_comparison_df['السيناريو'], |
|
y=scenarios_comparison_df['التكلفة الإجمالية'], |
|
name='التكلفة الإجمالية', |
|
marker_color='indianred' |
|
)) |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
x=scenarios_comparison_df['السيناريو'], |
|
y=scenarios_comparison_df['السعر الإجمالي'], |
|
name='السعر الإجمالي', |
|
marker_color='lightsalmon' |
|
)) |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
x=scenarios_comparison_df['السيناريو'], |
|
y=scenarios_comparison_df['هامش الربح (%)'] * 10000, |
|
name='هامش الربح (%)', |
|
yaxis='y2', |
|
line=dict(color='royalblue', width=4) |
|
)) |
|
|
|
|
|
fig.update_layout( |
|
title='مقارنة سيناريوهات التسعير', |
|
xaxis_title='السيناريو', |
|
yaxis_title='المبلغ (ريال)', |
|
yaxis2=dict( |
|
title='هامش الربح (%)', |
|
titlefont=dict(color='royalblue'), |
|
tickfont=dict(color='royalblue'), |
|
overlaying='y', |
|
side='right', |
|
range=[0, 20] |
|
), |
|
barmode='group', |
|
legend=dict( |
|
x=0, |
|
y=1.2, |
|
orientation='h' |
|
) |
|
) |
|
|
|
|
|
fig.update_traces( |
|
texttemplate='%{y:,.0f}', |
|
textposition='outside' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("##### تحليل تأثير هامش الربح") |
|
|
|
|
|
profit_margins = list(range(0, 21, 2)) |
|
|
|
|
|
total_cost = st.session_state.price_scenarios[0]['total_cost'] |
|
total_prices = [total_cost * (1 + margin / 100) for margin in profit_margins] |
|
|
|
|
|
profit_analysis_df = pd.DataFrame({ |
|
'هامش الربح (%)': profit_margins, |
|
'السعر الإجمالي': total_prices |
|
}) |
|
|
|
|
|
fig = px.line( |
|
profit_analysis_df, |
|
x='هامش الربح (%)', |
|
y='السعر الإجمالي', |
|
title='تأثير هامش الربح على السعر الإجمالي', |
|
markers=True |
|
) |
|
|
|
|
|
fig.update_traces( |
|
texttemplate='%{y:,.0f}', |
|
textposition='top center' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("##### تحليل نقطة التعادل") |
|
|
|
|
|
fixed_costs = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة') |
|
variable_costs = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة') |
|
|
|
|
|
active_scenario = next((item for item in st.session_state.price_scenarios if item['is_active']), None) |
|
if active_scenario: |
|
selling_price = active_scenario['total_price'] |
|
else: |
|
selling_price = st.session_state.price_scenarios[0]['total_price'] |
|
|
|
|
|
if selling_price > variable_costs: |
|
breakeven_point = fixed_costs / (selling_price - variable_costs) |
|
st.metric("نقطة التعادل", f"{breakeven_point:.2f} وحدة") |
|
|
|
|
|
units = list(range(0, int(breakeven_point * 2) + 1, max(1, int(breakeven_point / 10)))) |
|
|
|
total_costs = [fixed_costs + variable_costs * unit for unit in units] |
|
total_revenues = [selling_price * unit for unit in units] |
|
profits = [revenue - cost for revenue, cost in zip(total_revenues, total_costs)] |
|
|
|
breakeven_df = pd.DataFrame({ |
|
'الوحدات': units, |
|
'إجمالي التكاليف': total_costs, |
|
'إجمالي الإيرادات': total_revenues, |
|
'الربح': profits |
|
}) |
|
|
|
fig = go.Figure() |
|
|
|
fig.add_trace(go.Scatter( |
|
x=breakeven_df['الوحدات'], |
|
y=breakeven_df['إجمالي التكاليف'], |
|
name='إجمالي التكاليف', |
|
line=dict(color='red', width=2) |
|
)) |
|
|
|
fig.add_trace(go.Scatter( |
|
x=breakeven_df['الوحدات'], |
|
y=breakeven_df['إجمالي الإيرادات'], |
|
name='إجمالي الإيرادات', |
|
line=dict(color='green', width=2) |
|
)) |
|
|
|
fig.add_trace(go.Scatter( |
|
x=breakeven_df['الوحدات'], |
|
y=breakeven_df['الربح'], |
|
name='الربح', |
|
line=dict(color='blue', width=2) |
|
)) |
|
|
|
|
|
fig.add_vline( |
|
x=breakeven_point, |
|
line_dash="dash", |
|
line_color="black", |
|
annotation_text=f"نقطة التعادل: {breakeven_point:.2f}", |
|
annotation_position="top right" |
|
) |
|
|
|
|
|
fig.add_hline( |
|
y=0, |
|
line_dash="dash", |
|
line_color="gray" |
|
) |
|
|
|
fig.update_layout( |
|
title='تحليل نقطة التعادل', |
|
xaxis_title='الوحدات', |
|
yaxis_title='المبلغ (ريال)' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
else: |
|
st.warning("لا يمكن حساب نقطة التعادل لأن سعر البيع أقل من التكاليف المتغيرة.") |
|
|
|
def _render_competitive_analysis_tab(self): |
|
"""عرض تبويب المقارنة التنافسية""" |
|
|
|
st.markdown("### المقارنة التنافسية") |
|
|
|
|
|
competitors_data = [ |
|
{ |
|
'name': 'شركتنا', |
|
'price': 670000, |
|
'quality': 4.5, |
|
'delivery_time': 180, |
|
'experience': 8, |
|
'local_content': 85 |
|
}, |
|
{ |
|
'name': 'المنافس أ', |
|
'price': 700000, |
|
'quality': 4.2, |
|
'delivery_time': 200, |
|
'experience': 10, |
|
'local_content': 75 |
|
}, |
|
{ |
|
'name': 'المنافس ب', |
|
'price': 650000, |
|
'quality': 3.8, |
|
'delivery_time': 160, |
|
'experience': 5, |
|
'local_content': 90 |
|
}, |
|
{ |
|
'name': 'المنافس ج', |
|
'price': 680000, |
|
'quality': 4.0, |
|
'delivery_time': 190, |
|
'experience': 12, |
|
'local_content': 80 |
|
} |
|
] |
|
|
|
|
|
st.markdown("#### بيانات المنافسين") |
|
|
|
competitors_df = pd.DataFrame(competitors_data) |
|
st.dataframe(competitors_df, use_container_width=True, hide_index=True) |
|
|
|
|
|
st.markdown("#### مقارنة الأسعار") |
|
|
|
fig = px.bar( |
|
competitors_df, |
|
x='name', |
|
y='price', |
|
title='مقارنة الأسعار بين المنافسين', |
|
color='price', |
|
text_auto='.2s' |
|
) |
|
|
|
fig.update_layout( |
|
xaxis_title='المنافس', |
|
yaxis_title='السعر (ريال)' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("#### مقارنة متعددة الأبعاد") |
|
|
|
|
|
categories = ['price', 'quality', 'delivery_time', 'experience', 'local_content'] |
|
|
|
|
|
normalized_data = {} |
|
|
|
for category in categories: |
|
if category == 'price' or category == 'delivery_time': |
|
|
|
min_val = min(item[category] for item in competitors_data) |
|
max_val = max(item[category] for item in competitors_data) |
|
normalized_data[category] = [(max_val - item[category]) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data] |
|
else: |
|
|
|
min_val = min(item[category] for item in competitors_data) |
|
max_val = max(item[category] for item in competitors_data) |
|
normalized_data[category] = [(item[category] - min_val) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data] |
|
|
|
|
|
fig = go.Figure() |
|
|
|
for i, competitor in enumerate(competitors_data): |
|
fig.add_trace(go.Scatterpolar( |
|
r=[normalized_data[category][i] for category in categories], |
|
theta=['السعر', 'الجودة', 'وقت التسليم', 'الخبرة', 'المحتوى المحلي'], |
|
fill='toself', |
|
name=competitor['name'] |
|
)) |
|
|
|
fig.update_layout( |
|
polar=dict( |
|
radialaxis=dict( |
|
visible=True, |
|
range=[0, 1] |
|
) |
|
), |
|
title='مقارنة متعددة الأبعاد بين المنافسين', |
|
showlegend=True |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("#### تحليل نقاط القوة والضعف") |
|
|
|
|
|
our_company = competitors_data[0] |
|
|
|
strengths = [] |
|
weaknesses = [] |
|
|
|
|
|
other_prices = [comp['price'] for comp in competitors_data[1:]] |
|
if our_company['price'] <= min(other_prices): |
|
strengths.append("السعر تنافسي جداً") |
|
elif our_company['price'] <= sum(other_prices) / len(other_prices): |
|
strengths.append("السعر تنافسي") |
|
else: |
|
weaknesses.append("السعر أعلى من المتوسط") |
|
|
|
|
|
other_qualities = [comp['quality'] for comp in competitors_data[1:]] |
|
if our_company['quality'] >= max(other_qualities): |
|
strengths.append("الجودة ممتازة") |
|
elif our_company['quality'] >= sum(other_qualities) / len(other_qualities): |
|
strengths.append("الجودة جيدة") |
|
else: |
|
weaknesses.append("الجودة أقل من المتوسط") |
|
|
|
|
|
other_delivery_times = [comp['delivery_time'] for comp in competitors_data[1:]] |
|
if our_company['delivery_time'] <= min(other_delivery_times): |
|
strengths.append("وقت التسليم سريع جداً") |
|
elif our_company['delivery_time'] <= sum(other_delivery_times) / len(other_delivery_times): |
|
strengths.append("وقت التسليم جيد") |
|
else: |
|
weaknesses.append("وقت التسليم أطول من المتوسط") |
|
|
|
|
|
other_experiences = [comp['experience'] for comp in competitors_data[1:]] |
|
if our_company['experience'] >= max(other_experiences): |
|
strengths.append("خبرة واسعة جداً") |
|
elif our_company['experience'] >= sum(other_experiences) / len(other_experiences): |
|
strengths.append("خبرة جيدة") |
|
else: |
|
weaknesses.append("خبرة أقل من المتوسط") |
|
|
|
|
|
other_local_contents = [comp['local_content'] for comp in competitors_data[1:]] |
|
if our_company['local_content'] >= max(other_local_contents): |
|
strengths.append("محتوى محلي ممتاز") |
|
elif our_company['local_content'] >= sum(other_local_contents) / len(other_local_contents): |
|
strengths.append("محتوى محلي جيد") |
|
else: |
|
weaknesses.append("محتوى محلي أقل من المتوسط") |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
st.markdown("##### نقاط القوة") |
|
for strength in strengths: |
|
st.markdown(f"- {strength}") |
|
|
|
with col2: |
|
st.markdown("##### نقاط الضعف") |
|
for weakness in weaknesses: |
|
st.markdown(f"- {weakness}") |
|
|
|
|
|
st.markdown("#### توصيات للتسعير") |
|
|
|
|
|
recommendations = [] |
|
|
|
|
|
avg_price = sum(comp['price'] for comp in competitors_data) / len(competitors_data) |
|
if our_company['price'] > avg_price: |
|
recommendations.append("النظر في تخفيض السعر للمنافسة بشكل أفضل.") |
|
|
|
|
|
if our_company['quality'] > sum(comp['quality'] for comp in competitors_data[1:]) / len(competitors_data[1:]): |
|
recommendations.append("التأكيد على جودة الخدمات في العروض التسويقية.") |
|
|
|
|
|
if our_company['delivery_time'] < sum(comp['delivery_time'] for comp in competitors_data[1:]) / len(competitors_data[1:]): |
|
recommendations.append("التأكيد على سرعة التسليم كميزة تنافسية.") |
|
|
|
|
|
if our_company['experience'] < max(comp['experience'] for comp in competitors_data[1:]): |
|
recommendations.append("تعزيز فريق العمل بخبرات إضافية.") |
|
|
|
|
|
if our_company['local_content'] > sum(comp['local_content'] for comp in competitors_data[1:]) / len(competitors_data[1:]): |
|
recommendations.append("التأكيد على نسبة المحتوى المحلي العالية في العروض.") |
|
|
|
|
|
recommendations.append("مراجعة هيكل التكاليف بشكل دوري للحفاظ على القدرة التنافسية.") |
|
|
|
|
|
for recommendation in recommendations: |
|
st.markdown(f"- {recommendation}") |
|
|
|
def _render_reports_tab(self): |
|
"""عرض تبويب التقارير""" |
|
|
|
st.markdown("### التقارير") |
|
|
|
|
|
reports = [ |
|
"تقرير جدول الكميات", |
|
"تقرير تحليل التكاليف", |
|
"تقرير سيناريوهات التسعير", |
|
"تقرير المقارنة التنافسية", |
|
"تقرير ملخص التسعير" |
|
] |
|
|
|
|
|
selected_report = st.selectbox("اختر التقرير", reports) |
|
|
|
|
|
export_format = st.radio("صيغة التصدير", ["PDF", "Excel", "Word"]) |
|
|
|
|
|
if st.button("إنشاء التقرير"): |
|
st.success(f"تم إنشاء {selected_report} بصيغة {export_format} بنجاح!") |
|
|
|
|
|
st.markdown("#### نموذج التقرير") |
|
|
|
if selected_report == "تقرير جدول الكميات": |
|
self._render_boq_report() |
|
elif selected_report == "تقرير تحليل التكاليف": |
|
self._render_cost_analysis_report() |
|
elif selected_report == "تقرير سيناريوهات التسعير": |
|
self._render_pricing_scenarios_report() |
|
elif selected_report == "تقرير المقارنة التنافسية": |
|
self._render_competitive_analysis_report() |
|
elif selected_report == "تقرير ملخص التسعير": |
|
self._render_pricing_summary_report() |
|
|
|
def _render_boq_report(self): |
|
"""عرض نموذج تقرير جدول الكميات""" |
|
|
|
st.markdown("### تقرير جدول الكميات") |
|
st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d")) |
|
st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري") |
|
st.markdown("**رقم المناقصة:** T-2024-001") |
|
|
|
st.markdown("#### جدول الكميات") |
|
|
|
|
|
boq_df = pd.DataFrame(st.session_state.bill_of_quantities) |
|
st.dataframe(boq_df, use_container_width=True, hide_index=True) |
|
|
|
|
|
total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities) |
|
st.metric("إجمالي جدول الكميات", f"{total_boq:,.2f} ريال") |
|
|
|
|
|
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()) |
|
}) |
|
|
|
fig = px.pie( |
|
category_df, |
|
values='المبلغ', |
|
names='الفئة', |
|
title='توزيع جدول الكميات حسب الفئة' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
def _render_cost_analysis_report(self): |
|
"""عرض نموذج تقرير تحليل التكاليف""" |
|
|
|
st.markdown("### تقرير تحليل التكاليف") |
|
st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d")) |
|
st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري") |
|
st.markdown("**رقم المناقصة:** T-2024-001") |
|
|
|
st.markdown("#### تحليل التكاليف") |
|
|
|
|
|
cost_df = pd.DataFrame(st.session_state.cost_analysis) |
|
st.dataframe(cost_df, use_container_width=True, hide_index=True) |
|
|
|
|
|
total_cost = sum(item['amount'] for item in st.session_state.cost_analysis) |
|
st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال") |
|
|
|
|
|
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_df = pd.DataFrame({ |
|
'الفئة': list(category_totals.keys()), |
|
'المبلغ': list(category_totals.values()) |
|
}) |
|
|
|
fig = px.pie( |
|
category_df, |
|
values='المبلغ', |
|
names='الفئة', |
|
title='توزيع التكاليف حسب الفئة' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("#### توزيع التكاليف المباشرة") |
|
|
|
|
|
direct_subcategory_totals = {} |
|
|
|
for item in st.session_state.cost_analysis: |
|
if item['category'] == 'تكاليف مباشرة': |
|
subcategory = item['subcategory'] |
|
if subcategory in direct_subcategory_totals: |
|
direct_subcategory_totals[subcategory] += item['amount'] |
|
else: |
|
direct_subcategory_totals[subcategory] = item['amount'] |
|
|
|
direct_subcategory_df = pd.DataFrame({ |
|
'الفئة الفرعية': list(direct_subcategory_totals.keys()), |
|
'المبلغ': list(direct_subcategory_totals.values()) |
|
}) |
|
|
|
fig = px.bar( |
|
direct_subcategory_df, |
|
x='الفئة الفرعية', |
|
y='المبلغ', |
|
title='توزيع التكاليف المباشرة', |
|
color='الفئة الفرعية', |
|
text_auto='.2s' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
def _render_pricing_scenarios_report(self): |
|
"""عرض نموذج تقرير سيناريوهات التسعير""" |
|
|
|
st.markdown("### تقرير سيناريوهات التسعير") |
|
st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d")) |
|
st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري") |
|
st.markdown("**رقم المناقصة:** T-2024-001") |
|
|
|
st.markdown("#### سيناريوهات التسعير") |
|
|
|
|
|
scenarios_df = pd.DataFrame(st.session_state.price_scenarios) |
|
st.dataframe(scenarios_df, use_container_width=True, hide_index=True) |
|
|
|
|
|
active_scenario = next((item for item in st.session_state.price_scenarios if item['is_active']), None) |
|
if active_scenario: |
|
st.markdown(f"**السيناريو النشط:** {active_scenario['name']}") |
|
st.markdown(f"**السعر الإجمالي:** {active_scenario['total_price']:,.2f} ريال") |
|
st.markdown(f"**هامش الربح:** {active_scenario['profit_margin']:.1f}%") |
|
|
|
|
|
st.markdown("#### مقارنة السيناريوهات") |
|
|
|
|
|
scenarios_comparison_df = pd.DataFrame({ |
|
'السيناريو': [item['name'] for item in st.session_state.price_scenarios], |
|
'التكلفة الإجمالية': [item['total_cost'] for item in st.session_state.price_scenarios], |
|
'هامش الربح (%)': [item['profit_margin'] for item in st.session_state.price_scenarios], |
|
'السعر الإجمالي': [item['total_price'] for item in st.session_state.price_scenarios], |
|
'الحالة': ['نشط' if item['is_active'] else 'غير نشط' for item in st.session_state.price_scenarios] |
|
}) |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
x=scenarios_comparison_df['السيناريو'], |
|
y=scenarios_comparison_df['التكلفة الإجمالية'], |
|
name='التكلفة الإجمالية', |
|
marker_color='indianred' |
|
)) |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
x=scenarios_comparison_df['السيناريو'], |
|
y=scenarios_comparison_df['السعر الإجمالي'], |
|
name='السعر الإجمالي', |
|
marker_color='lightsalmon' |
|
)) |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
x=scenarios_comparison_df['السيناريو'], |
|
y=scenarios_comparison_df['هامش الربح (%)'] * 10000, |
|
name='هامش الربح (%)', |
|
yaxis='y2', |
|
line=dict(color='royalblue', width=4) |
|
)) |
|
|
|
|
|
fig.update_layout( |
|
title='مقارنة سيناريوهات التسعير', |
|
xaxis_title='السيناريو', |
|
yaxis_title='المبلغ (ريال)', |
|
yaxis2=dict( |
|
title='هامش الربح (%)', |
|
titlefont=dict(color='royalblue'), |
|
tickfont=dict(color='royalblue'), |
|
overlaying='y', |
|
side='right', |
|
range=[0, 20] |
|
), |
|
barmode='group', |
|
legend=dict( |
|
x=0, |
|
y=1.2, |
|
orientation='h' |
|
) |
|
) |
|
|
|
|
|
fig.update_traces( |
|
texttemplate='%{y:,.0f}', |
|
textposition='outside' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
def _render_competitive_analysis_report(self): |
|
"""عرض نموذج تقرير المقارنة التنافسية""" |
|
|
|
st.markdown("### تقرير المقارنة التنافسية") |
|
st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d")) |
|
st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري") |
|
st.markdown("**رقم المناقصة:** T-2024-001") |
|
|
|
|
|
competitors_data = [ |
|
{ |
|
'name': 'شركتنا', |
|
'price': 670000, |
|
'quality': 4.5, |
|
'delivery_time': 180, |
|
'experience': 8, |
|
'local_content': 85 |
|
}, |
|
{ |
|
'name': 'المنافس أ', |
|
'price': 700000, |
|
'quality': 4.2, |
|
'delivery_time': 200, |
|
'experience': 10, |
|
'local_content': 75 |
|
}, |
|
{ |
|
'name': 'المنافس ب', |
|
'price': 650000, |
|
'quality': 3.8, |
|
'delivery_time': 160, |
|
'experience': 5, |
|
'local_content': 90 |
|
}, |
|
{ |
|
'name': 'المنافس ج', |
|
'price': 680000, |
|
'quality': 4.0, |
|
'delivery_time': 190, |
|
'experience': 12, |
|
'local_content': 80 |
|
} |
|
] |
|
|
|
|
|
st.markdown("#### بيانات المنافسين") |
|
|
|
competitors_df = pd.DataFrame(competitors_data) |
|
st.dataframe(competitors_df, use_container_width=True, hide_index=True) |
|
|
|
|
|
st.markdown("#### مقارنة الأسعار") |
|
|
|
fig = px.bar( |
|
competitors_df, |
|
x='name', |
|
y='price', |
|
title='مقارنة الأسعار بين المنافسين', |
|
color='price', |
|
text_auto='.2s' |
|
) |
|
|
|
fig.update_layout( |
|
xaxis_title='المنافس', |
|
yaxis_title='السعر (ريال)' |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("#### مقارنة متعددة الأبعاد") |
|
|
|
|
|
categories = ['price', 'quality', 'delivery_time', 'experience', 'local_content'] |
|
|
|
|
|
normalized_data = {} |
|
|
|
for category in categories: |
|
if category == 'price' or category == 'delivery_time': |
|
|
|
min_val = min(item[category] for item in competitors_data) |
|
max_val = max(item[category] for item in competitors_data) |
|
normalized_data[category] = [(max_val - item[category]) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data] |
|
else: |
|
|
|
min_val = min(item[category] for item in competitors_data) |
|
max_val = max(item[category] for item in competitors_data) |
|
normalized_data[category] = [(item[category] - min_val) / (max_val - min_val) if max_val != min_val else 0.5 for item in competitors_data] |
|
|
|
|
|
fig = go.Figure() |
|
|
|
for i, competitor in enumerate(competitors_data): |
|
fig.add_trace(go.Scatterpolar( |
|
r=[normalized_data[category][i] for category in categories], |
|
theta=['السعر', 'الجودة', 'وقت التسليم', 'الخبرة', 'المحتوى المحلي'], |
|
fill='toself', |
|
name=competitor['name'] |
|
)) |
|
|
|
fig.update_layout( |
|
polar=dict( |
|
radialaxis=dict( |
|
visible=True, |
|
range=[0, 1] |
|
) |
|
), |
|
title='مقارنة متعددة الأبعاد بين المنافسين', |
|
showlegend=True |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
def _render_pricing_summary_report(self): |
|
"""عرض نموذج تقرير ملخص التسعير""" |
|
|
|
st.markdown("### تقرير ملخص التسعير") |
|
st.markdown("**تاريخ التقرير:** " + time.strftime("%Y-%m-%d")) |
|
st.markdown("**اسم المشروع:** مشروع إنشاء مبنى إداري") |
|
st.markdown("**رقم المناقصة:** T-2024-001") |
|
|
|
|
|
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 |
|
|
|
|
|
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, use_container_width=True, hide_index=True) |
|
|
|
|
|
st.markdown("#### توزيع التكاليف") |
|
|
|
|
|
cost_distribution_df = pd.DataFrame({ |
|
'البند': ['التكاليف المباشرة', 'التكاليف غير المباشرة', 'هامش الربح'], |
|
'المبلغ': [total_direct_cost, total_indirect_cost, total_profit] |
|
}) |
|
|
|
fig = px.pie( |
|
cost_distribution_df, |
|
values='المبلغ', |
|
names='البند', |
|
title='توزيع التكاليف والأرباح', |
|
color_discrete_sequence=px.colors.qualitative.Set3 |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("#### السيناريو النشط") |
|
|
|
active_scenario = next((item for item in st.session_state.price_scenarios if item['is_active']), None) |
|
if active_scenario: |
|
st.markdown(f"**اسم السيناريو:** {active_scenario['name']}") |
|
st.markdown(f"**الوصف:** {active_scenario['description']}") |
|
st.markdown(f"**إجمالي التكلفة:** {active_scenario['total_cost']:,.2f} ريال") |
|
st.markdown(f"**هامش الربح:** {active_scenario['profit_margin']:.1f}%") |
|
st.markdown(f"**السعر الإجمالي:** {active_scenario['total_price']:,.2f} ريال") |
|
|
|
|
|
st.markdown("#### توصيات التسعير") |
|
|
|
st.markdown("- مراجعة هيكل التكاليف بشكل دوري للحفاظ على القدرة التنافسية.") |
|
st.markdown("- التأكيد على جودة الخدمات في العروض التسويقية.") |
|
st.markdown("- التأكيد على نسبة المحتوى المحلي العالية في العروض.") |
|
st.markdown("- مراقبة أسعار المنافسين وتعديل الاستراتيجية التسعيرية عند الحاجة.") |
|
st.markdown("- تحليل نقاط القوة والضعف بشكل مستمر لتحسين العروض المستقبلية.") |
|
|