SA-SAJCOAI / modules /pricing /pricing_app.py
EGYADMIN's picture
Update modules/pricing/pricing_app.py
30cad09 verified
import streamlit as st
import pandas as pd
import numpy as np
from datetime import datetime
import time
class PriceAnalysisComponent:
"""مكون تحليل الأسعار للبنود"""
def __init__(self):
"""تهيئة مكون تحليل الأسعار"""
# تهيئة قائمة الوحدات المتاحة
self.unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
# تهيئة فئات التكاليف
self.cost_categories = [
"مواد",
"عمالة",
"معدات",
"مقاولي الباطن",
"مصاريف عامة",
"أرباح"
]
# تهيئة قائمة البنود وتحليل أسعارها
if 'items_price_analysis' not in st.session_state:
st.session_state.items_price_analysis = {}
def render(self):
"""عرض واجهة تحليل الأسعار"""
st.markdown("<h2 class='module-title'>تحليل أسعار البنود</h2>", unsafe_allow_html=True)
# التحقق من وجود بنود في التسعير الحالي
if 'current_pricing' not in st.session_state or 'items' not in st.session_state.current_pricing:
st.warning("ليس هناك بنود للتحليل. يرجى إنشاء تسعير أولاً.")
return
# الحصول على البنود من التسعير الحالي
items = st.session_state.current_pricing['items'].copy()
# عرض قائمة البنود
st.markdown("### قائمة البنود")
st.dataframe(items[['رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي']],
use_container_width=True, hide_index=True)
# اختيار البند لتحليل السعر
selected_item_id = st.selectbox(
"اختر البند لتحليل السعر",
options=items['رقم البند'].tolist(),
format_func=lambda x: f"{x}: {items[items['رقم البند'] == x]['وصف البند'].values[0][:50]}..."
)
if selected_item_id:
# الحصول على البند المحدد
selected_item = items[items['رقم البند'] == selected_item_id].iloc[0]
# عرض تفاصيل البند المختار
col1, col2, col3 = st.columns(3)
with col1:
st.metric("رقم البند", selected_item['رقم البند'])
with col2:
st.metric("الكمية", f"{selected_item['الكمية']} {selected_item['الوحدة']}")
with col3:
st.metric("سعر الوحدة", f"{selected_item['سعر الوحدة']:,.2f} ريال")
st.markdown(f"**وصف البند**: {selected_item['وصف البند']}")
# إنشاء أو تحديث تحليل السعر للبند المحدد
if selected_item_id not in st.session_state.items_price_analysis:
# إنشاء تحليل سعر افتراضي
self._create_default_price_analysis(selected_item_id, selected_item)
# عرض وتحرير تحليل السعر
self._render_price_analysis_editor(selected_item_id, selected_item)
def _create_default_price_analysis(self, item_id, item):
"""إنشاء تحليل سعر افتراضي للبند"""
# إنشاء قائمة مكونات تحليل السعر
components = pd.DataFrame(columns=[
'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
])
# إضافة مكونات افتراضية بناءً على نوع البند
is_concrete = 'خرسان' in item['وصف البند']
is_steel = 'حديد' in item['وصف البند'] or 'تسليح' in item['وصف البند']
is_bricks = 'بلوك' in item['وصف البند'] or 'طوب' in item['وصف البند']
is_paint = 'دهان' in item['وصف البند'] or 'طلاء' in item['وصف البند']
is_insulation = 'عزل' in item['وصف البند']
# إضافة المكونات بناءً على نوع البند
if is_concrete:
# مكونات الخرسانة
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
})
components = pd.concat([components, default_components], ignore_index=True)
elif is_steel:
# مكونات الحديد
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
'الكمية': [1000, 10, 1, 1, 1],
'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
'سعر الوحدة': [4.5, 50, 300, 200, 300],
'الإجمالي': [4500, 500, 300, 200, 300]
})
components = pd.concat([components, default_components], ignore_index=True)
elif is_bricks:
# مكونات البلوك
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
'الكمية': [12.5, 0.02, 1, 1, 1],
'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
'سعر الوحدة': [8, 500, 80, 15, 20],
'الإجمالي': [100, 10, 80, 15, 20]
})
components = pd.concat([components, default_components], ignore_index=True)
elif is_paint:
# مكونات الدهانات
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
'الكمية': [0.4, 0.1, 1, 1, 1],
'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
'سعر الوحدة': [80, 20, 35, 5, 10],
'الإجمالي': [32, 2, 35, 5, 10]
})
components = pd.concat([components, default_components], ignore_index=True)
elif is_insulation:
# مكونات العزل
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
'الكمية': [1.1, 0.2, 1, 1, 1],
'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
'سعر الوحدة': [60, 30, 25, 10, 15],
'الإجمالي': [66, 6, 25, 10, 15]
})
components = pd.concat([components, default_components], ignore_index=True)
else:
# مكونات عامة افتراضية
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
'الوصف': ['مواد أساسية', 'عمالة', 'معدات ومعد مساعدة', 'مصاريف عامة', 'أرباح'],
'الكمية': [1, 1, 1, 1, 1],
'الوحدة': [item['الوحدة'], 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
'سعر الوحدة': [
item['سعر الوحدة'] * 0.6,
item['سعر الوحدة'] * 0.2,
item['سعر الوحدة'] * 0.1,
item['سعر الوحدة'] * 0.05,
item['سعر الوحدة'] * 0.05
],
'الإجمالي': [
item['سعر الوحدة'] * 0.6,
item['سعر الوحدة'] * 0.2,
item['سعر الوحدة'] * 0.1,
item['سعر الوحدة'] * 0.05,
item['سعر الوحدة'] * 0.05
]
})
components = pd.concat([components, default_components], ignore_index=True)
# حفظ تحليل السعر للبند
st.session_state.items_price_analysis[item_id] = components
def _render_price_analysis_editor(self, item_id, item):
"""عرض محرر تحليل السعر للبند"""
st.markdown("### تحليل السعر")
# الحصول على مكونات تحليل السعر
components = st.session_state.items_price_analysis[item_id]
# عرض تحليل السعر في محرر بيانات
st.markdown("#### مكونات السعر")
edited_components = st.data_editor(
components,
use_container_width=True,
hide_index=True,
num_rows="dynamic",
column_config={
'نوع التكلفة': st.column_config.SelectboxColumn(
'نوع التكلفة',
help='فئة التكلفة',
options=self.cost_categories
),
'الوحدة': st.column_config.SelectboxColumn(
'الوحدة',
help='وحدة القياس',
options=self.unit_options + ["وحدة", "ساعة", "يوم"]
),
'الكمية': st.column_config.NumberColumn(
'الكمية',
help='الكمية',
min_value=0.0,
format="%.2f"
),
'سعر الوحدة': st.column_config.NumberColumn(
'سعر الوحدة',
help='سعر الوحدة',
min_value=0.0,
format="%.2f"
),
'الإجمالي': st.column_config.NumberColumn(
'الإجمالي',
help='الإجمالي',
min_value=0.0,
format="%.2f"
)
}
)
# إعادة حساب الإجمالي لكل مكون
edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
# حفظ التعديلات
st.session_state.items_price_analysis[item_id] = edited_components
# حساب إجمالي تحليل السعر
total_analysis_price = edited_components['الإجمالي'].sum()
unit_price_from_analysis = total_analysis_price / item['الكمية'] if item['الكمية'] > 0 else 0
# عرض ملخص تحليل السعر
st.markdown("#### ملخص تحليل السعر")
col1, col2, col3 = st.columns(3)
with col1:
st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
with col2:
st.metric("سعر الوحدة من التحليل", f"{unit_price_from_analysis:,.2f} ريال")
with col3:
# المقارنة مع السعر الأصلي
diff = unit_price_from_analysis - item['سعر الوحدة']
st.metric(
"الفرق عن السعر الأصلي",
f"{diff:,.2f} ريال",
delta=f"{(diff/item['سعر الوحدة']*100) if item['سعر الوحدة'] > 0 else 0:.1f}%"
)
# تحليل توزيع التكاليف حسب الفئة
cost_by_category = edited_components.groupby('نوع التكلفة')['الإجمالي'].sum().reset_index()
# عرض مخطط توزيع التكاليف
st.markdown("#### توزيع التكاليف حسب الفئة")
# عرض توزيع التكاليف في جدول
distribution_df = pd.DataFrame({
'نوع التكلفة': cost_by_category['نوع التكلفة'],
'القيمة': cost_by_category['الإجمالي'],
'النسبة المئوية': (cost_by_category['الإجمالي'] / total_analysis_price * 100).round(2)
})
st.dataframe(
distribution_df,
use_container_width=True,
hide_index=True,
column_config={
'القيمة': st.column_config.NumberColumn(
'القيمة',
help='القيمة',
format="%.2f"
),
'النسبة المئوية': st.column_config.ProgressColumn(
'النسبة المئوية',
help='النسبة المئوية',
format="%.2f%%",
min_value=0,
max_value=100
)
}
)
# أزرار الإجراءات
col1, col2, col3 = st.columns(3)
with col1:
if st.button("تحديث سعر البند", use_container_width=True):
# تحديث سعر البند بناءً على تحليل السعر
items = st.session_state.current_pricing['items'].copy()
item_index = items[items['رقم البند'] == item_id].index[0]
# تحديث سعر الوحدة والإجمالي
items.at[item_index, 'سعر الوحدة'] = unit_price_from_analysis
items.at[item_index, 'الإجمالي'] = unit_price_from_analysis * items.at[item_index, 'الكمية']
# حفظ التعديلات في التسعير الحالي
st.session_state.current_pricing['items'] = items
st.success(f"تم تحديث سعر البند بناءً على تحليل السعر: {unit_price_from_analysis:,.2f} ريال")
time.sleep(0.5)
st.rerun()
with col2:
if st.button("تصدير تحليل السعر", use_container_width=True):
st.success("تم إرسال تحليل السعر للتصدير بنجاح!")
with col3:
if st.button("مسح تحليل السعر", use_container_width=True):
# حذف تحليل السعر للبند
if item_id in st.session_state.items_price_analysis:
del st.session_state.items_price_analysis[item_id]
st.warning("تم مسح تحليل السعر للبند")
time.sleep(0.5)
st.rerun()
def add_to_pricing_app(self, pricing_app):
"""إضافة مكون تحليل الأسعار إلى تطبيق التسعير"""
# إضافة تبويب جديد
if not hasattr(pricing_app, 'tabs'):
pricing_app.tabs = []
if len(pricing_app.tabs) == 4: # إذا كان هناك 4 تبويبات فقط
pricing_app.tabs.append("تحليل أسعار البنود")
# إضافة دالة العرض
pricing_app._render_price_analysis_tab = self.render
def render_integrated_item_input():
"""عرض واجهة إدخال البنود مع تحليل السعر المتكامل"""
# ضبط CSS لتحسين ظهور الواجهة العربية
st.markdown("""
<style>
input, .stTextArea textarea {
direction: rtl;
text-align: right;
font-family: 'Arial', 'Tahoma', sans-serif !important;
}
.stTextInput > div > div > input {
text-align: right;
direction: rtl;
}
.pricing-analysis-container {
border: 1px solid #e0e0e0;
border-radius: 10px;
padding: 10px;
margin-top: 10px;
background-color: #f9f9f9;
}
</style>
""", unsafe_allow_html=True)
# تهيئة قائمة الوحدات المتاحة
unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"]
# تهيئة فئات التكاليف
cost_categories = [
"مواد",
"عمالة",
"معدات",
"مقاولي الباطن",
"مصاريف عامة",
"أرباح"
]
# إنشاء جدول البنود اذا لم يكن موجوداً
if 'manual_items' not in st.session_state:
manual_items = pd.DataFrame(columns=[
'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي'
])
# إضافة بضعة صفوف افتراضية
default_items = pd.DataFrame({
'رقم البند': ["A1", "A2", "A3", "A4", "A5"],
'وصف البند': [
"توريد وتركيب أعمال الخرسانة المسلحة للأساسات",
"توريد وتركيب حديد التسليح للأساسات",
"أعمال العزل المائي للأساسات",
"أعمال الردم والدك للأساسات",
"توريد وتركيب أعمال الخرسانة المسلحة للأعمدة"
],
'الوحدة': ["م3", "طن", "م2", "م3", "م3"],
'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0],
'سعر الوحدة': [0.0, 0.0, 0.0, 0.0, 0.0],
'الإجمالي': [0.0, 0.0, 0.0, 0.0, 0.0]
})
manual_items = pd.concat([manual_items, default_items])
st.session_state.manual_items = manual_items
# إنشاء جدول تحليل الأسعار اذا لم يكن موجوداً
if 'items_price_analysis' not in st.session_state:
st.session_state.items_price_analysis = {}
# عرض واجهة إدخال البنود
st.markdown("### إدخال تفاصيل البنود مع تحليل الأسعار")
# عرض البنود الحالية كجدول للعرض
st.markdown("### جدول البنود الحالية")
st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True)
# التبويبات لإضافة بند جديد أو تعديل بند
tabs = st.tabs(["إضافة بند جديد", "تعديل بند حالي"])
with tabs[0]: # إضافة بند جديد
st.markdown("### إضافة بند جديد مع تحليل السعر")
col1, col2 = st.columns(2)
with col1:
new_id = st.text_input("رقم البند", value=f"A{len(st.session_state.manual_items)+1}", key="new_id")
new_desc = st.text_area("وصف البند", value="", key="new_desc")
with col2:
new_unit = st.selectbox("الوحدة", options=unit_options, key="new_unit")
new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f", key="new_qty")
# إنشاء تحليل السعر للبند الجديد
st.markdown('<div class="pricing-analysis-container">', unsafe_allow_html=True)
st.markdown("#### تحليل سعر البند")
# التعرف التلقائي على نوع البند من الوصف
is_concrete = False
is_steel = False
is_bricks = False
is_paint = False
is_insulation = False
if new_desc:
is_concrete = 'خرسان' in new_desc
is_steel = 'حديد' in new_desc or 'تسليح' in new_desc
is_bricks = 'بلوك' in new_desc or 'طوب' in new_desc
is_paint = 'دهان' in new_desc or 'طلاء' in new_desc
is_insulation = 'عزل' in new_desc
# تلميح للمستخدم عن التعرف التلقائي
if any([is_concrete, is_steel, is_bricks, is_paint, is_insulation]):
detected_type = ""
if is_concrete:
detected_type = "أعمال خرسانة"
elif is_steel:
detected_type = "أعمال حديد"
elif is_bricks:
detected_type = "أعمال بلوك"
elif is_paint:
detected_type = "أعمال دهانات"
elif is_insulation:
detected_type = "أعمال عزل"
st.info(f"تم التعرف تلقائياً على نوع البند: {detected_type}")
# إنشاء مصفوفة فارغة لمكونات البند
if 'new_components' not in st.session_state:
# إنشاء DataFrame فارغ
new_components = pd.DataFrame(columns=[
'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي'
])
# إضافة مكونات افتراضية بناءً على نوع البند
if is_concrete:
# مكونات الخرسانة
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'مواد', 'مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
'الوصف': ['أسمنت', 'رمل', 'حصى', 'عمال وفنيين', 'خلاطات ومعدات صب', 'مصاريف عامة', 'أرباح'],
'الكمية': [350, 0.4, 0.8, 8, 1, 1, 1],
'الوحدة': ['كجم', 'م3', 'م3', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
'سعر الوحدة': [0.5, 100, 120, 50, 500, 100, 150],
'الإجمالي': [175, 40, 96, 400, 500, 100, 150]
})
new_components = pd.concat([new_components, default_components], ignore_index=True)
elif is_steel:
# مكونات الحديد
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
'الوصف': ['حديد التسليح', 'عمال وفنيين', 'معدات ثني وتجهيز الحديد', 'مصاريف عامة', 'أرباح'],
'الكمية': [1000, 10, 1, 1, 1],
'الوحدة': ['كجم', 'ساعة', 'يوم', 'وحدة', 'وحدة'],
'سعر الوحدة': [4.5, 50, 300, 200, 300],
'الإجمالي': [4500, 500, 300, 200, 300]
})
new_components = pd.concat([new_components, default_components], ignore_index=True)
elif is_bricks:
# مكونات البلوك
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
'الوصف': ['بلوك خرساني', 'مونة', 'عمالة بناء', 'مصاريف عامة', 'أرباح'],
'الكمية': [12.5, 0.02, 1, 1, 1],
'الوحدة': ['قطعة', 'م3', 'م2', 'وحدة', 'وحدة'],
'سعر الوحدة': [8, 500, 80, 15, 20],
'الإجمالي': [100, 10, 80, 15, 20]
})
new_components = pd.concat([new_components, default_components], ignore_index=True)
elif is_paint:
# مكونات الدهانات
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
'الوصف': ['دهان', 'مواد تجهيز', 'عمالة دهان', 'مصاريف عامة', 'أرباح'],
'الكمية': [0.4, 0.1, 1, 1, 1],
'الوحدة': ['لتر', 'وحدة', 'م2', 'وحدة', 'وحدة'],
'سعر الوحدة': [80, 20, 35, 5, 10],
'الإجمالي': [32, 2, 35, 5, 10]
})
new_components = pd.concat([new_components, default_components], ignore_index=True)
elif is_insulation:
# مكونات العزل
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'مواد', 'عمالة', 'مصاريف عامة', 'أرباح'],
'الوصف': ['مواد عازلة', 'مواد لاصقة', 'عمالة تركيب', 'مصاريف عامة', 'أرباح'],
'الكمية': [1.1, 0.2, 1, 1, 1],
'الوحدة': ['م2', 'كجم', 'م2', 'وحدة', 'وحدة'],
'سعر الوحدة': [60, 30, 25, 10, 15],
'الإجمالي': [66, 6, 25, 10, 15]
})
new_components = pd.concat([new_components, default_components], ignore_index=True)
else:
# مكونات عامة افتراضية
default_components = pd.DataFrame({
'نوع التكلفة': ['مواد', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
'الوصف': ['مواد أساسية', 'عمالة', 'معدات', 'مصاريف عامة', 'أرباح'],
'الكمية': [1, 1, 1, 1, 1],
'الوحدة': [new_unit if new_unit else 'وحدة', 'وحدة', 'وحدة', 'وحدة', 'وحدة'],
'سعر الوحدة': [100, 50, 30, 20, 20],
'الإجمالي': [100, 50, 30, 20, 20]
})
new_components = pd.concat([new_components, default_components], ignore_index=True)
st.session_state.new_components = new_components
# عرض وتحرير مكونات تحليل السعر
edited_components = st.data_editor(
st.session_state.new_components,
use_container_width=True,
hide_index=True,
num_rows="dynamic",
column_config={
'نوع التكلفة': st.column_config.SelectboxColumn(
'نوع التكلفة',
help='فئة التكلفة',
options=cost_categories
),
'الوحدة': st.column_config.SelectboxColumn(
'الوحدة',
help='وحدة القياس',
options=unit_options + ["وحدة", "ساعة", "يوم"]
),
'الكمية': st.column_config.NumberColumn(
'الكمية',
help='الكمية',
min_value=0.0,
format="%.2f"
),
'سعر الوحدة': st.column_config.NumberColumn(
'سعر الوحدة',
help='سعر الوحدة',
min_value=0.0,
format="%.2f"
),
'الإجمالي': st.column_config.NumberColumn(
'الإجمالي',
help='الإجمالي',
min_value=0.0,
format="%.2f"
)
}
)
# إعادة حساب الإجمالي لكل مكون
edited_components['الإجمالي'] = edited_components['الكمية'] * edited_components['سعر الوحدة']
# حفظ التعديلات
st.session_state.new_components = edited_components
# حساب إجمالي تحليل السعر
total_analysis_price = edited_components['الإجمالي'].sum()
unit_price_from_analysis = total_analysis_price / new_qty if new_qty > 0 else 0
# عرض ملخص تحليل السعر
st.markdown("#### ملخص تحليل السعر")
col1, col2 = st.columns(2)
with col1:
st.metric("إجمالي تكلفة البند من التحليل", f"{total_analysis_price:,.2f} ريال")
with col2:
st.metric("سعر الوحدة المحسوب", f"{unit_price_from_analysis:,.2f} ريال")
st.markdown('</div>', unsafe_allow_html=True)
# استخدام السعر المحسوب
use_calculated_price = st.checkbox("استخدام السعر المحسوب من التحليل", value=True)
# تحديد سعر الوحدة النهائي
if use_calculated_price and new_qty > 0:
new_unit_price = unit_price_from_analysis
else:
new_unit_price = st.number_input