|
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: |
|
pricing_app.tabs.append("تحليل أسعار البنود") |
|
|
|
|
|
pricing_app._render_price_analysis_tab = self.render |
|
|
|
|
|
def render_integrated_item_input(): |
|
"""عرض واجهة إدخال البنود مع تحليل السعر المتكامل""" |
|
|
|
|
|
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: |
|
|
|
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_price = unit_price_from_analysis |
|
else: |
|
new_price = st.number_input("سعر الوحدة", value=unit_price_from_analysis if new_qty > 0 else 0.0, min_value=0.0, format="%.2f", key="new_price") |
|
|
|
|
|
new_total = new_qty * new_price |
|
st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال") |
|
|
|
|
|
if not use_calculated_price and new_qty > 0 and unit_price_from_analysis > 0: |
|
price_diff = new_price - unit_price_from_analysis |
|
diff_percentage = (price_diff / unit_price_from_analysis) * 100 |
|
|
|
if abs(diff_percentage) > 5: |
|
if diff_percentage > 0: |
|
st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%") |
|
else: |
|
st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%") |
|
|
|
|
|
if st.button("إضافة البند"): |
|
|
|
if new_id and new_desc and new_qty > 0: |
|
|
|
new_row = pd.DataFrame({ |
|
'رقم البند': [new_id], |
|
'وصف البند': [new_desc], |
|
'الوحدة': [new_unit], |
|
'الكمية': [float(new_qty)], |
|
'سعر الوحدة': [float(new_price)], |
|
'الإجمالي': [float(new_total)] |
|
}) |
|
|
|
|
|
st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True) |
|
|
|
|
|
st.session_state.items_price_analysis[new_id] = st.session_state.new_components.copy() |
|
|
|
|
|
if 'new_components' in st.session_state: |
|
del st.session_state.new_components |
|
|
|
st.success("تم إضافة البند وتحليل السعر بنجاح!") |
|
time.sleep(0.5) |
|
st.rerun() |
|
else: |
|
st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.") |
|
|
|
with tabs[1]: |
|
st.markdown("### تعديل بند حالي مع تحليل السعر") |
|
|
|
|
|
edit_item_id = st.selectbox( |
|
"اختر البند للتعديل", |
|
options=st.session_state.manual_items['رقم البند'].tolist(), |
|
format_func=lambda x: f"{x}: {st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == x]['وصف البند'].values[0][:30]}..." |
|
) |
|
|
|
if edit_item_id: |
|
|
|
idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == edit_item_id].index[0] |
|
row = st.session_state.manual_items.loc[idx] |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
edited_id = st.text_input("رقم البند (تعديل)", value=row['رقم البند'], key="edit_id") |
|
edited_desc = st.text_area("وصف البند (تعديل)", value=row['وصف البند'], key="edit_desc") |
|
|
|
with col2: |
|
edited_unit = st.selectbox( |
|
"الوحدة (تعديل)", |
|
options=unit_options, |
|
index=unit_options.index(row['الوحدة']) if row['الوحدة'] in unit_options else 0, |
|
key="edit_unit" |
|
) |
|
edited_qty = st.number_input("الكمية (تعديل)", value=float(row['الكمية']), min_value=0.0, format="%.2f", key="edit_qty") |
|
|
|
|
|
st.markdown('<div class="pricing-analysis-container">', unsafe_allow_html=True) |
|
st.markdown("#### تحليل سعر البند") |
|
|
|
|
|
if edit_item_id in st.session_state.items_price_analysis: |
|
|
|
components = st.session_state.items_price_analysis[edit_item_id] |
|
else: |
|
|
|
components = pd.DataFrame(columns=[ |
|
'نوع التكلفة', 'الوصف', 'الكمية', 'الوحدة', 'سعر الوحدة', 'الإجمالي' |
|
]) |
|
|
|
|
|
is_concrete = 'خرسان' in row['وصف البند'] |
|
is_steel = 'حديد' in row['وصف البند'] or 'تسليح' in row['وصف البند'] |
|
is_bricks = 'بلوك' in row['وصف البند'] or 'طوب' in row['وصف البند'] |
|
is_paint = 'دهان' in row['وصف البند'] or 'طلاء' in row['وصف البند'] |
|
is_insulation = 'عزل' in row['وصف البند'] |
|
|
|
|
|
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], |
|
'الوحدة': [row['الوحدة'], 'وحدة', 'وحدة', 'وحدة', 'وحدة'], |
|
'سعر الوحدة': [ |
|
row['سعر الوحدة'] * 0.6, |
|
row['سعر الوحدة'] * 0.2, |
|
row['سعر الوحدة'] * 0.1, |
|
row['سعر الوحدة'] * 0.05, |
|
row['سعر الوحدة'] * 0.05 |
|
], |
|
'الإجمالي': [ |
|
row['سعر الوحدة'] * 0.6, |
|
row['سعر الوحدة'] * 0.2, |
|
row['سعر الوحدة'] * 0.1, |
|
row['سعر الوحدة'] * 0.05, |
|
row['سعر الوحدة'] * 0.05 |
|
] |
|
}) |
|
components = pd.concat([components, default_components], ignore_index=True) |
|
|
|
|
|
st.session_state.items_price_analysis[edit_item_id] = components |
|
|
|
|
|
edited_components = st.data_editor( |
|
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.items_price_analysis[edit_item_id] = edited_components |
|
|
|
|
|
total_analysis_price = edited_components['الإجمالي'].sum() |
|
unit_price_from_analysis = total_analysis_price / edited_qty if edited_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, key="use_calc_edit") |
|
|
|
|
|
if use_calculated_price and edited_qty > 0: |
|
edited_price = unit_price_from_analysis |
|
else: |
|
edited_price = st.number_input( |
|
"سعر الوحدة (تعديل)", |
|
value=unit_price_from_analysis if edited_qty > 0 and unit_price_from_analysis > 0 else float(row['سعر الوحدة']), |
|
min_value=0.0, |
|
format="%.2f", |
|
key="edit_price" |
|
) |
|
|
|
|
|
edited_total = edited_qty * edited_price |
|
st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال") |
|
|
|
|
|
if not use_calculated_price and edited_qty > 0 and unit_price_from_analysis > 0: |
|
price_diff = edited_price - unit_price_from_analysis |
|
diff_percentage = (price_diff / unit_price_from_analysis) * 100 |
|
|
|
if abs(diff_percentage) > 5: |
|
if diff_percentage > 0: |
|
st.warning(f"السعر المدخل أعلى من السعر المحسوب بنسبة {diff_percentage:.2f}%") |
|
else: |
|
st.warning(f"السعر المدخل أقل من السعر المحسوب بنسبة {abs(diff_percentage):.2f}%") |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
with col1: |
|
if st.button("حفظ التعديلات", use_container_width=True): |
|
|
|
if edited_id and edited_desc and edited_qty > 0: |
|
|
|
if edited_id != edit_item_id: |
|
|
|
st.session_state.items_price_analysis[edited_id] = st.session_state.items_price_analysis.pop(edit_item_id) |
|
|
|
|
|
st.session_state.manual_items.at[idx, 'رقم البند'] = edited_id |
|
st.session_state.manual_items.at[idx, 'وصف البند'] = edited_desc |
|
st.session_state.manual_items.at[idx, 'الوحدة'] = edited_unit |
|
st.session_state.manual_items.at[idx, 'الكمية'] = edited_qty |
|
st.session_state.manual_items.at[idx, 'سعر الوحدة'] = edited_price |
|
st.session_state.manual_items.at[idx, 'الإجمالي'] = edited_total |
|
|
|
st.success("تم تحديث البند وتحليل السعر بنجاح!") |
|
time.sleep(0.5) |
|
st.rerun() |
|
else: |
|
st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.") |
|
|
|
with col2: |
|
if st.button("استعادة القيم الأصلية", use_container_width=True): |
|
|
|
st.rerun() |
|
|
|
with col3: |
|
if st.button("حذف هذا البند", use_container_width=True): |
|
|
|
if edit_item_id in st.session_state.items_price_analysis: |
|
del st.session_state.items_price_analysis[edit_item_id] |
|
|
|
|
|
st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True) |
|
|
|
st.warning("تم حذف البند وتحليل السعر!") |
|
time.sleep(0.5) |
|
st.rerun() |
|
|