Spaces:
Sleeping
Sleeping
""" | |
تطبيق وحدة التسعير المتكاملة | |
""" | |
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 random | |
import os | |
import time | |
import io | |
from modules.pricing.services.standard_pricing import StandardPricing | |
from modules.pricing.services.unbalanced_pricing import UnbalancedPricing | |
from modules.pricing.services.local_content import LocalContentCalculator | |
from modules.pricing.services.price_prediction import PricePrediction | |
from utils.excel_handler import export_to_excel | |
from utils.helpers import format_number, format_currency | |
class PricingApp: | |
"""وحدة التسعير المتكاملة""" | |
def __init__(self): | |
self.pricing_methods = [ | |
"التسعير القياسي", | |
"التسعير غير المتزن", | |
"التسعير التنافسي", | |
"التسعير الموجه بالربحية" | |
] | |
# تهيئة خدمات التسعير | |
self.standard_pricing = StandardPricing() | |
self.unbalanced_pricing = UnbalancedPricing() | |
self.local_content = LocalContentCalculator() | |
self.price_prediction = PricePrediction() | |
def render(self): | |
"""عرض واجهة وحدة التسعير""" | |
st.markdown("<h1 class='module-title'>وحدة التسعير المتكاملة</h1>", unsafe_allow_html=True) | |
tabs = st.tabs([ | |
"إنشاء تسعير جديد", | |
"نموذج التسعير الشامل", | |
"التسعير غير المتزن", | |
"المحتوى المحلي" | |
]) | |
with tabs[0]: | |
self._render_new_pricing_tab() | |
with tabs[1]: | |
self._render_comprehensive_pricing_tab() | |
with tabs[2]: | |
self._render_unbalanced_pricing_tab() | |
with tabs[3]: | |
self._render_local_content_tab() | |
def _render_new_pricing_tab(self): | |
"""عرض تبويب إنشاء تسعير جديد""" | |
st.markdown("### إنشاء تسعير جديد") | |
col1, col2 = st.columns(2) | |
with col1: | |
tender_name = st.text_input("اسم المناقصة") | |
client = st.text_input("الجهة المالكة") | |
pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods) | |
with col2: | |
tender_number = st.text_input("رقم المناقصة") | |
location = st.text_input("الموقع") | |
submission_date = st.date_input("تاريخ التقديم") | |
# خيارات بيانات البنود | |
st.markdown("### بيانات البنود") | |
data_source = st.radio( | |
"مصدر بيانات البنود", | |
["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات"] | |
) | |
if data_source == "إدخال يدوي": | |
# إنشاء بيانات افتراضية | |
if 'manual_items' not in st.session_state: | |
st.session_state.manual_items = pd.DataFrame({ | |
'رقم البند': [f"A{i}" for i in range(1, 6)], | |
'وصف البند': [ | |
"توريد وتركيب أعمال الخرسانة المسلحة للأساسات", | |
"توريد وتركيب حديد التسليح للأساسات", | |
"أعمال العزل المائي للأساسات", | |
"أعمال الردم والدك للأساسات", | |
"توريد وتركيب أعمال الخرسانة المسلحة للأعمدة" | |
], | |
'الوحدة': ["م3", "طن", "م2", "م3", "م3"], | |
'الكمية': [250, 25, 500, 300, 120], | |
'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0], | |
'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0] | |
}) | |
# عرض جدول البنود مع إمكانية التعديل | |
edited_items = st.data_editor( | |
st.session_state.manual_items, | |
use_container_width=True, | |
hide_index=True, | |
num_rows="dynamic" | |
) | |
st.session_state.manual_items = edited_items | |
elif data_source == "استيراد من Excel": | |
uploaded_file = st.file_uploader("رفع ملف Excel", type=["xlsx", "xls"]) | |
if uploaded_file is not None: | |
st.success("تم رفع الملف بنجاح") | |
# محاكاة قراءة الملف | |
st.markdown("### معاينة البيانات المستوردة") | |
# إنشاء بيانات افتراضية | |
import_items = pd.DataFrame({ | |
'رقم البند': [f"A{i}" for i in range(1, 8)], | |
'وصف البند': [ | |
"توريد وتركيب أعمال الخرسانة المسلحة للأساسات", | |
"توريد وتركيب حديد التسليح للأساسات", | |
"أعمال العزل المائي للأساسات", | |
"أعمال الردم والدك للأساسات", | |
"توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", | |
"توريد وتركيب حديد التسليح للأعمدة", | |
"أعمال البلوك للجدران" | |
], | |
'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"], | |
'الكمية': [250, 25, 500, 300, 120, 10, 400], | |
'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], | |
'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] | |
}) | |
st.dataframe(import_items) | |
if st.button("استيراد البيانات"): | |
st.session_state.manual_items = import_items.copy() | |
st.session_state.manual_items_modified = True | |
st.success("تم استيراد البيانات بنجاح!") | |
else: # استيراد من وحدة تحليل المستندات | |
available_documents = [ | |
"كراسة شروط مشروع توسعة مستشفى الملك فهد", | |
"جدول كميات صيانة محطات المياه", | |
"مخططات إنشاء مدرسة ثانوية" | |
] | |
selected_doc = st.selectbox("اختر المستند", available_documents) | |
if st.button("استيراد البيانات من تحليل المستند"): | |
# محاكاة استيراد البيانات | |
with st.spinner("جاري استيراد البيانات..."): | |
time.sleep(2) | |
# إنشاء بيانات افتراضية | |
doc_items = pd.DataFrame({ | |
'رقم البند': [f"A{i}" for i in range(1, 8)], | |
'وصف البند': [ | |
"توريد وتركيب أعمال الخرسانة المسلحة للأساسات", | |
"توريد وتركيب حديد التسليح للأساسات", | |
"أعمال العزل المائي للأساسات", | |
"أعمال الردم والدك للأساسات", | |
"توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", | |
"توريد وتركيب حديد التسليح للأعمدة", | |
"أعمال البلوك للجدران" | |
], | |
'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"], | |
'الكمية': [250, 25, 500, 300, 120, 10, 400], | |
'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], | |
'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] | |
}) | |
st.session_state.manual_items = doc_items.copy() | |
st.success("تم استيراد البيانات من تحليل المستند بنجاح!") | |
st.dataframe(doc_items) | |
# زر بدء التسعير | |
if st.button("بدء التسعير"): | |
# تحقق من صحة البيانات | |
if 'manual_items' in st.session_state and not st.session_state.manual_items.empty: | |
# حفظ بيانات التسعير الحالي | |
st.session_state.current_pricing = { | |
'name': tender_name, | |
'number': tender_number, | |
'client': client, | |
'location': location, | |
'method': pricing_method, | |
'submission_date': submission_date, | |
'items': st.session_state.manual_items.copy(), | |
'status': 'جديد', | |
'created_at': datetime.now() | |
} | |
# الانتقال إلى تبويب نموذج التسعير الشامل | |
st.success("تم إنشاء التسعير بنجاح! يمكنك الانتقال إلى نموذج التسعير الشامل.") | |
else: | |
st.error("يرجى إدخال بيانات البنود أولاً.") | |
def _render_comprehensive_pricing_tab(self): | |
"""عرض تبويب نموذج التسعير الشامل""" | |
st.markdown("### نموذج التسعير الشامل") | |
# التحقق من وجود تسعير حالي | |
if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: | |
st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") | |
return | |
# عرض معلومات التسعير الحالي | |
pricing = st.session_state.current_pricing | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
st.metric("اسم المناقصة", pricing['name']) | |
st.metric("الجهة المالكة", pricing['client']) | |
with col2: | |
st.metric("رقم المناقصة", pricing['number']) | |
st.metric("تاريخ التقديم", pricing['submission_date'].strftime("%Y-%m-%d")) | |
with col3: | |
st.metric("طريقة التسعير", pricing['method']) | |
st.metric("الموقع", pricing['location']) | |
# عرض البنود والتسعير | |
st.markdown("### بنود التسعير") | |
items = pricing['items'].copy() | |
# إضافة أسعار الوحدة للمحاكاة | |
if 'سعر الوحدة' in items.columns and (items['سعر الوحدة'] == 0).all(): | |
items['سعر الوحدة'] = [ | |
round(random.uniform(1000, 3000), 2), # الخرسانة | |
round(random.uniform(5000, 7000), 2), # الحديد | |
round(random.uniform(100, 200), 2), # العزل | |
round(random.uniform(50, 100), 2), # الردم | |
round(random.uniform(1200, 3500), 2), # الخرسانة للأعمدة | |
] | |
if len(items) > 5: | |
for i in range(5, len(items)): | |
items.at[i, 'سعر الوحدة'] = round(random.uniform(500, 5000), 2) | |
# حساب الإجمالي | |
items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] | |
# عرض الجدول مع إمكانية التعديل | |
edited_items = st.data_editor( | |
items, | |
use_container_width=True, | |
hide_index=True, | |
disabled=('رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'الإجمالي') | |
) | |
# حساب الإجمالي بعد التعديل | |
edited_items['الإجمالي'] = edited_items['الكمية'] * edited_items['سعر الوحدة'] | |
st.session_state.current_pricing['items'] = edited_items | |
# حساب وعرض إجماليات التسعير | |
total_price = edited_items['الإجمالي'].sum() | |
st.markdown("### إجماليات التسعير") | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
st.metric("إجمالي التكاليف المباشرة", f"{total_price:,.2f} ريال") | |
with col2: | |
overhead_percentage = st.slider("نسبة المصاريف العامة والأرباح (%)", 5, 30, 15) | |
overhead_value = total_price * overhead_percentage / 100 | |
st.metric("المصاريف العامة والأرباح", f"{overhead_value:,.2f} ريال") | |
with col3: | |
grand_total = total_price + overhead_value | |
st.metric("الإجمالي النهائي", f"{grand_total:,.2f} ريال") | |
# رسم بياني لتوزيع التكاليف | |
st.markdown("### تحليل التكاليف") | |
# حساب النسب المئوية لكل بند | |
pie_data = edited_items.copy() | |
pie_data['نسبة من إجمالي التكاليف'] = pie_data['الإجمالي'] / total_price * 100 | |
fig = px.pie( | |
pie_data, | |
values='نسبة من إجمالي التكاليف', | |
names='وصف البند', | |
title='توزيع التكاليف حسب البنود', | |
hole=0.4 | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
# أزرار العمليات | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
if st.button("حفظ التسعير"): | |
st.success("تم حفظ التسعير بنجاح!") | |
with col2: | |
if st.button("تصدير إلى Excel"): | |
st.success("تم تصدير التسعير إلى Excel بنجاح!") | |
with col3: | |
if st.button("تحليل المخاطر المالية"): | |
st.success("تم إرسال الطلب إلى وحدة تحليل المخاطر!") | |
def _render_unbalanced_pricing_tab(self): | |
"""عرض تبويب التسعير غير المتزن""" | |
st.markdown("### التسعير غير المتزن") | |
# التحقق من وجود تسعير حالي | |
if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: | |
st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") | |
return | |
# شرح التسعير غير المتزن | |
with st.expander("ما هو التسعير غير المتزن؟", expanded=False): | |
st.markdown(""" | |
**التسعير غير المتزن** هو استراتيجية تسعير تقوم على توزيع التكاليف بين بنود المناقصة بشكل غير متساوٍ، مع الحفاظ على إجمالي قيمة العطاء. | |
### استراتيجيات التسعير غير المتزن: | |
1. **التحميل الأمامي (Front Loading)**: زيادة أسعار البنود المبكرة في المشروع للحصول على تدفق نقدي أفضل في بداية المشروع. | |
2. **التحميل الخلفي (Back Loading)**: زيادة أسعار البنود المتأخرة في المشروع. | |
3. **تحميل البنود المؤكدة**: زيادة أسعار البنود التي من المؤكد تنفيذها بالكميات المحددة. | |
4. **تخفيض أسعار البنود المحتملة**: تخفيض أسعار البنود التي قد تزيد كمياتها أثناء التنفيذ. | |
### مزايا التسعير غير المتزن: | |
- تحسين التدفق النقدي للمشروع. | |
- تعظيم الربحية في حالة التغييرات والأوامر التغييرية. | |
- زيادة فرص الفوز بالمناقصة. | |
### مخاطر التسعير غير المتزن: | |
- قد يتم رفض العطاء إذا كان عدم التوازن واضحاً. | |
- قد تتأثر السمعة سلباً إذا تم استخدامه بشكل مفرط. | |
- قد يؤدي إلى خسائر إذا لم يتم تنفيذ البنود ذات الأسعار العالية. | |
""") | |
# عرض بنود التسعير الحالي | |
items = st.session_state.current_pricing['items'].copy() | |
# إضافة عمود إستراتيجية التسعير | |
if 'إستراتيجية التسعير' not in items.columns: | |
items['إستراتيجية التسعير'] = 'متوازن' | |
st.markdown("### إستراتيجية التسعير غير المتزن") | |
# اختيار الإستراتيجية | |
strategy = st.selectbox( | |
"اختر إستراتيجية التسعير", | |
[ | |
"تحميل أمامي (Front Loading)", | |
"تحميل البنود المؤكدة", | |
"تخفيض البنود المحتمل زيادتها", | |
"إستراتيجية مخصصة" | |
] | |
) | |
# تطبيق الإستراتيجية المختارة | |
if strategy == "تحميل أمامي (Front Loading)": | |
# محاكاة تحميل أمامي | |
items_count = len(items) | |
early_items = items.iloc[:items_count//3].index | |
middle_items = items.iloc[items_count//3:2*items_count//3].index | |
late_items = items.iloc[2*items_count//3:].index | |
# تطبيق الزيادة والنقصان | |
for idx in early_items: | |
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.3 # زيادة 30% | |
items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' | |
for idx in middle_items: | |
items.at[idx, 'إستراتيجية التسعير'] = 'متوازن' | |
for idx in late_items: | |
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30% | |
items.at[idx, 'إستراتيجية التسعير'] = 'نقص' | |
elif strategy == "تحميل البنود المؤكدة": | |
# محاكاة - اعتبار بعض البنود مؤكدة | |
confirmed_items = [0, 2, 4] # الأصفار-مستندة | |
variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items] | |
# تطبيق الزيادة والنقصان | |
for idx in confirmed_items: | |
if idx < len(items): | |
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 # زيادة 25% | |
items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' | |
for idx in variable_items: | |
if idx < len(items): | |
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 # نقص 15% | |
items.at[idx, 'إستراتيجية التسعير'] = 'نقص' | |
elif strategy == "تخفيض البنود المحتمل زيادتها": | |
# محاكاة - اعتبار بعض البنود محتمل زيادتها | |
variable_items = [1, 3] # الأصفار-مستندة | |
other_items = [idx for idx in range(len(items)) if idx not in variable_items] | |
# تطبيق الزيادة والنقصان | |
for idx in variable_items: | |
if idx < len(items): | |
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 # نقص 30% | |
items.at[idx, 'إستراتيجية التسعير'] = 'نقص' | |
for idx in other_items: | |
if idx < len(items): | |
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 # زيادة 15% | |
items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' | |
else: # إستراتيجية مخصصة | |
st.markdown("### تعديل أسعار البنود يدوياً") | |
st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً.") | |
# حساب الإجمالي بعد التعديل | |
items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] | |
# تعيين ألوان للإستراتيجيات | |
def highlight_strategy(val): | |
if val == 'زيادة': | |
return 'background-color: #a8e6cf' | |
elif val == 'نقص': | |
return 'background-color: #ff9aa2' | |
return '' | |
# عرض الجدول مع تنسيق | |
st.markdown("### بنود التسعير غير المتزن") | |
styled_items = items.style.applymap(highlight_strategy, subset=['إستراتيجية التسعير']) | |
st.dataframe(styled_items, use_container_width=True) | |
# المقارنة بين التسعير المتوازن وغير المتوازن | |
st.markdown("### مقارنة التسعير المتوازن وغير المتوازن") | |
original_items = st.session_state.current_pricing['items'].copy() | |
original_total = original_items['الإجمالي'].sum() | |
unbalanced_total = items['الإجمالي'].sum() | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
st.metric("إجمالي التسعير المتوازن", f"{original_total:,.2f} ريال") | |
with col2: | |
st.metric("إجمالي التسعير غير المتوازن", f"{unbalanced_total:,.2f} ريال") | |
with col3: | |
diff = unbalanced_total - original_total | |
st.metric("الفرق", f"{diff:,.2f} ريال", delta=f"{diff/original_total*100:.1f}%") | |
# المعايرة للحفاظ على إجمالي التسعير | |
if abs(diff) > 1: # إذا كان هناك فرق كبير | |
if st.button("معايرة الأسعار للحفاظ على إجمالي التسعير"): | |
# تعديل الأسعار للحفاظ على إجمالي التكلفة | |
adjustment_factor = original_total / unbalanced_total | |
items['سعر الوحدة'] = items['سعر الوحدة'] * adjustment_factor | |
items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] | |
st.success(f"تم تعديل الأسعار للحفاظ على إجمالي التسعير الأصلي ({original_total:,.2f} ريال)") | |
st.dataframe(items, use_container_width=True) | |
# رسم بياني للمقارنة | |
st.markdown("### تحليل بصري للتسعير غير المتوازن") | |
# إعداد البيانات للرسم البياني | |
chart_data = pd.DataFrame({ | |
'وصف البند': original_items['وصف البند'], | |
'التسعير المتوازن': original_items['الإجمالي'], | |
'التسعير غير المتوازن': items['الإجمالي'] | |
}) | |
# رسم بياني شريطي للمقارنة | |
fig = go.Figure() | |
fig.add_trace(go.Bar( | |
x=chart_data['وصف البند'], | |
y=chart_data['التسعير المتوازن'], | |
name='التسعير المتوازن', | |
marker_color='rgb(55, 83, 109)' | |
)) | |
fig.add_trace(go.Bar( | |
x=chart_data['وصف البند'], | |
y=chart_data['التسعير غير المتوازن'], | |
name='التسعير غير المتوازن', | |
marker_color='rgb(26, 118, 255)' | |
)) | |
fig.update_layout( | |
title='مقارنة بين التسعير المتوازن وغير المتوازن', | |
xaxis_tickfont_size=14, | |
yaxis=dict( | |
title='الإجمالي (ريال)', | |
titlefont_size=16, | |
tickfont_size=14, | |
), | |
legend=dict( | |
x=0, | |
y=1.0, | |
bgcolor='rgba(255, 255, 255, 0)', | |
bordercolor='rgba(255, 255, 255, 0)' | |
), | |
barmode='group', | |
bargap=0.15, | |
bargroupgap=0.1 | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
# زر حفظ التسعير غير المتوازن | |
if st.button(" |