|
"""
|
|
تطبيق وحدة التسعير المتكاملة
|
|
"""
|
|
|
|
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
|
|
items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
|
|
|
|
for idx in middle_items:
|
|
items.at[idx, 'إستراتيجية التسعير'] = 'متوازن'
|
|
|
|
for idx in late_items:
|
|
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7
|
|
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
|
|
items.at[idx, 'إستراتيجية التسعير'] = 'زيادة'
|
|
|
|
for idx in variable_items:
|
|
if idx < len(items):
|
|
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85
|
|
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
|
|
items.at[idx, 'إستراتيجية التسعير'] = 'نقص'
|
|
|
|
for idx in other_items:
|
|
if idx < len(items):
|
|
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.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(" |