Spaces:
Paused
Paused
| """ | |
| تطبيق وحدة التسعير المتكاملة | |
| """ | |
| 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 | |
| # ملاحظة: نحن لا نستخدم st.set_page_config هنا لأنه يجب أن يكون في ملف app.py الرئيسي فقط | |
| # تحسين المظهر العام باستخدام CSS | |
| st.markdown(""" | |
| <style> | |
| /* ألوان النظام */ | |
| :root { | |
| --primary-color: #1E8A8A; | |
| --primary-light: rgba(30, 138, 138, 0.2); | |
| --secondary-color: #134e4a; | |
| --accent-color: #f0b429; | |
| --background-light: #f8fafc; | |
| --text-color: #334155; | |
| --heading-color: #475569; | |
| --border-color: #e2e8f0; | |
| --success-color: #10b981; | |
| --warning-color: #f59e0b; | |
| --danger-color: #ef4444; | |
| } | |
| /* المظهر العام */ | |
| .main { | |
| background-color: var(--background-light); | |
| color: var(--text-color); | |
| padding: 1rem 1.5rem; | |
| border-radius: 10px; | |
| } | |
| .main .block-container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| /* العناوين والنصوص */ | |
| h1, h2, h3, h4, h5, h6 { | |
| color: var(--heading-color); | |
| font-weight: 700 !important; | |
| } | |
| h1 { | |
| background: linear-gradient(to right, var(--primary-color), var(--secondary-color)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| font-size: 2.5rem !important; | |
| text-align: center; | |
| padding: 0.5rem 0; | |
| margin-bottom: 1rem; | |
| } | |
| h2 { | |
| border-bottom: 2px solid var(--primary-color); | |
| padding-bottom: 0.5rem; | |
| margin-top: 1.5rem; | |
| } | |
| h3 { | |
| color: var(--secondary-color); | |
| font-size: 1.5rem !important; | |
| } | |
| /* خلفية المحتوى */ | |
| .stApp { | |
| background-color: var(--background-light); | |
| } | |
| /* تصميم القوائم والتبويبات */ | |
| .stTabs { | |
| background-color: white; | |
| border-radius: 10px; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.05); | |
| padding: 1rem; | |
| margin-bottom: 1rem; | |
| } | |
| .stTabs [data-baseweb="tab-list"] { | |
| gap: 1rem; | |
| background-color: transparent; | |
| } | |
| .stTabs [data-baseweb="tab"] { | |
| height: 45px; | |
| border-radius: 5px; | |
| padding: 0 20px; | |
| background-color: #f1f5f9; | |
| color: var(--heading-color); | |
| font-weight: 600; | |
| } | |
| .stTabs [aria-selected="true"] { | |
| background-color: var(--primary-color) !important; | |
| color: white !important; | |
| } | |
| /* تنسيق الأزرار */ | |
| .stButton > button { | |
| border-radius: 8px; | |
| padding: 0.5rem 1rem; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| min-height: 45px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| border: none; | |
| } | |
| .stButton > button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.15); | |
| } | |
| /* أزرار متنوعة حسب الوظيفة */ | |
| .primary-btn > button { | |
| background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); | |
| color: white; | |
| } | |
| .success-btn > button { | |
| background: linear-gradient(90deg, #059669, #065f46); | |
| color: white; | |
| } | |
| .warning-btn > button { | |
| background: linear-gradient(90deg, #d97706, #b45309); | |
| color: white; | |
| } | |
| .danger-btn > button { | |
| background: linear-gradient(90deg, #dc2626, #9f1239); | |
| color: white; | |
| } | |
| .info-btn > button { | |
| background: linear-gradient(90deg, #0891b2, #0e7490); | |
| color: white; | |
| } | |
| /* البطاقات والمربعات */ | |
| .stExpander { | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| } | |
| .stExpander > div:first-child { | |
| background-color: white; | |
| padding: 0.75rem; | |
| } | |
| .css-1qg05tj { | |
| border-radius: 8px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| background-color: white; | |
| margin-bottom: 1rem; | |
| } | |
| /* الجداول */ | |
| .dataframe { | |
| border-collapse: collapse; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| margin-bottom: 1rem; | |
| width: 100%; | |
| } | |
| .dataframe th { | |
| background-color: var(--primary-light); | |
| color: var(--primary-color); | |
| font-weight: 600; | |
| text-align: right; | |
| padding: 0.75rem 1rem; | |
| border: none; | |
| } | |
| .dataframe td { | |
| padding: 0.75rem 1rem; | |
| border-bottom: 1px solid var(--border-color); | |
| border-right: none; | |
| border-left: none; | |
| } | |
| .dataframe tr:nth-child(even) { | |
| background-color: rgba(0,0,0,0.02); | |
| } | |
| .dataframe tr:hover { | |
| background-color: rgba(0,0,0,0.05); | |
| } | |
| /* حقول الإدخال */ | |
| .stTextInput > div > div, .stNumberInput > div > div { | |
| border-radius: 8px; | |
| border: 1px solid var(--border-color); | |
| } | |
| .stTextInput > div > div:focus-within, .stNumberInput > div > div:focus-within { | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 1px var(--primary-light); | |
| } | |
| .stSelectbox > div[data-baseweb="select"] { | |
| border-radius: 8px; | |
| } | |
| /* الشريط الجانبي */ | |
| .css-6qob1r.e1fqkh3o3 { | |
| background-color: #134e4a; | |
| background-image: linear-gradient(to bottom, #134e4a, #0f766e); | |
| color: white; | |
| } | |
| .css-6qob1r.e1fqkh3o3 .css-ue6h4q.e1fqkh3o4 { | |
| color: white; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| padding-bottom: 1rem; | |
| margin-bottom: 1rem; | |
| } | |
| .css-6qob1r.e1fqkh3o3 [data-testid="stSidebarNav"] { | |
| padding-top: 1rem; | |
| } | |
| .css-6qob1r.e1fqkh3o3 [data-testid="stSidebarNav"] a { | |
| color: white; | |
| padding: 0.5rem 1rem; | |
| border-radius: 5px; | |
| transition: all 0.3s ease; | |
| } | |
| .css-6qob1r.e1fqkh3o3 [data-testid="stSidebarNav"] a:hover { | |
| background-color: rgba(255,255,255,0.1); | |
| } | |
| /* الرسوم البيانية */ | |
| .js-plotly-plot { | |
| border-radius: 8px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| padding: 1rem; | |
| background-color: white; | |
| } | |
| /* تنسيقات المؤشرات والأرقام */ | |
| [data-testid="stMetricValue"] { | |
| font-size: 1.5rem !important; | |
| font-weight: 700 !important; | |
| color: var(--primary-color); | |
| } | |
| [data-testid="stMetricLabel"] { | |
| font-weight: 600; | |
| } | |
| [data-testid="stMetricDelta"] { | |
| font-size: 0.875rem !important; | |
| font-weight: 600 !important; | |
| } | |
| /* حاويات المحتوى */ | |
| .container-card { | |
| background-color: white; | |
| border-radius: 10px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| padding: 1.5rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| /* تنسيق التنبيهات */ | |
| .stAlert { | |
| border-radius: 8px; | |
| } | |
| /* تنسيق التقدم */ | |
| .stProgress > div > div { | |
| border-radius: 10px; | |
| height: 10px; | |
| } | |
| /* تنسيق التقويم */ | |
| .stDateInput { | |
| border-radius: 8px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # تحسين شكل الأزرار بشكل متقدم | |
| st.markdown(""" | |
| <style> | |
| /* أنماط الأزرار المتقدمة */ | |
| .stButton > button { | |
| border-radius: 10px !important; | |
| padding: 0.6rem 1.5rem !important; | |
| font-weight: 700 !important; | |
| letter-spacing: 0.5px !important; | |
| font-family: 'Almarai', sans-serif !important; | |
| text-align: center !important; | |
| transition: all 0.3s ease !important; | |
| box-shadow: 0 4px 10px rgba(0,0,0,0.1) !important; | |
| display: flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| position: relative !important; | |
| overflow: hidden !important; | |
| border: none !important; | |
| } | |
| .stButton > button:hover { | |
| transform: translateY(-3px) !important; | |
| box-shadow: 0 8px 15px rgba(0,0,0,0.15) !important; | |
| } | |
| .stButton > button:active { | |
| transform: translateY(0) !important; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important; | |
| } | |
| /* أنماط الأزرار الرئيسية */ | |
| .primary-btn > button { | |
| background: linear-gradient(135deg, #1E8A8A, #0F766E) !important; | |
| color: white !important; | |
| } | |
| .primary-btn > button::before { | |
| content: "" !important; | |
| position: absolute !important; | |
| top: 0 !important; | |
| left: -100% !important; | |
| width: 100% !important; | |
| height: 100% !important; | |
| background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.2), rgba(255,255,255,0)) !important; | |
| transform: skewX(-25deg) !important; | |
| transition: all 0.75s ease !important; | |
| } | |
| .primary-btn > button:hover::before { | |
| left: 100% !important; | |
| } | |
| /* أنماط الأزرار الثانوية */ | |
| .secondary-btn > button { | |
| background: linear-gradient(135deg, #475569, #334155) !important; | |
| color: white !important; | |
| } | |
| .secondary-btn > button::before { | |
| content: "" !important; | |
| position: absolute !important; | |
| top: 0 !important; | |
| left: -100% !important; | |
| width: 100% !important; | |
| height: 100% !important; | |
| background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.2), rgba(255,255,255,0)) !important; | |
| transform: skewX(-25deg) !important; | |
| transition: all 0.75s ease !important; | |
| } | |
| .secondary-btn > button:hover::before { | |
| left: 100% !important; | |
| } | |
| /* أنماط أزرار النجاح */ | |
| .success-btn > button { | |
| background: linear-gradient(135deg, #10b981, #059669) !important; | |
| color: white !important; | |
| } | |
| .success-btn > button::before { | |
| content: "" !important; | |
| position: absolute !important; | |
| top: 0 !important; | |
| left: -100% !important; | |
| width: 100% !important; | |
| height: 100% !important; | |
| background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.2), rgba(255,255,255,0)) !important; | |
| transform: skewX(-25deg) !important; | |
| transition: all 0.75s ease !important; | |
| } | |
| .success-btn > button:hover::before { | |
| left: 100% !important; | |
| } | |
| /* أنماط أزرار التحذير */ | |
| .warning-btn > button { | |
| background: linear-gradient(135deg, #f59e0b, #d97706) !important; | |
| color: white !important; | |
| } | |
| .warning-btn > button::before { | |
| content: "" !important; | |
| position: absolute !important; | |
| top: 0 !important; | |
| left: -100% !important; | |
| width: 100% !important; | |
| height: 100% !important; | |
| background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.2), rgba(255,255,255,0)) !important; | |
| transform: skewX(-25deg) !important; | |
| transition: all 0.75s ease !important; | |
| } | |
| .warning-btn > button:hover::before { | |
| left: 100% !important; | |
| } | |
| /* أنماط أزرار الخطر */ | |
| .danger-btn > button { | |
| background: linear-gradient(135deg, #ef4444, #b91c1c) !important; | |
| color: white !important; | |
| } | |
| .danger-btn > button::before { | |
| content: "" !important; | |
| position: absolute !important; | |
| top: 0 !important; | |
| left: -100% !important; | |
| width: 100% !important; | |
| height: 100% !important; | |
| background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.2), rgba(255,255,255,0)) !important; | |
| transform: skewX(-25deg) !important; | |
| transition: all 0.75s ease !important; | |
| } | |
| .danger-btn > button:hover::before { | |
| left: 100% !important; | |
| } | |
| /* أنماط أزرار المعلومات */ | |
| .info-btn > button { | |
| background: linear-gradient(135deg, #3b82f6, #1d4ed8) !important; | |
| color: white !important; | |
| } | |
| .info-btn > button::before { | |
| content: "" !important; | |
| position: absolute !important; | |
| top: 0 !important; | |
| left: -100% !important; | |
| width: 100% !important; | |
| height: 100% !important; | |
| background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.2), rgba(255,255,255,0)) !important; | |
| transform: skewX(-25deg) !important; | |
| transition: all 0.75s ease !important; | |
| } | |
| .info-btn > button:hover::before { | |
| left: 100% !important; | |
| } | |
| /* أنماط الأزرار العضوية */ | |
| .glass-btn > button { | |
| background: rgba(255, 255, 255, 0.2) !important; | |
| backdrop-filter: blur(10px) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.3) !important; | |
| color: var(--primary-color) !important; | |
| } | |
| .glass-btn > button:hover { | |
| background: rgba(255, 255, 255, 0.3) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.4) !important; | |
| } | |
| /* أنماط الأزرار المسطحة */ | |
| .flat-btn > button { | |
| background: transparent !important; | |
| border: 2px solid var(--primary-color) !important; | |
| color: var(--primary-color) !important; | |
| box-shadow: none !important; | |
| } | |
| .flat-btn > button:hover { | |
| background: var(--primary-light) !important; | |
| box-shadow: none !important; | |
| } | |
| /* أنماط أزرار الإجراءات */ | |
| .action-btn > button { | |
| display: flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| gap: 8px !important; | |
| } | |
| /* تنسيق أزرار مع أيقونات */ | |
| .icon-btn > button { | |
| padding: 0.5rem !important; | |
| min-width: 40px !important; | |
| height: 40px !important; | |
| border-radius: 50% !important; | |
| display: flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # وظيفة مساعدة لإنشاء أزرار بتنسيقات مختلفة | |
| def styled_button(label, key, type="primary", on_click=None, args=None, full_width=False, icon=None): | |
| """ | |
| إنشاء زر بتنسيق معين | |
| :param label: نص الزر | |
| :param key: مفتاح الزر الفريد | |
| :param type: نوع التنسيق ('primary', 'secondary', 'success', 'warning', 'danger', 'info', 'glass', 'flat') | |
| :param on_click: الدالة التي سيتم تنفيذها عند النقر | |
| :param args: معاملات الدالة | |
| :param full_width: هل يأخذ الزر العرض كاملاً | |
| :param icon: أيقونة لعرضها قبل النص (emoji أو HTML) | |
| :return: زر مُنسّق | |
| """ | |
| with st.container(): | |
| btn_class = f"{type}-btn" | |
| if icon: | |
| btn_class += " action-btn" | |
| label = f"{icon} {label}" | |
| st.markdown(f'<div class="{btn_class}">', unsafe_allow_html=True) | |
| clicked = st.button(label, key=key, on_click=on_click, args=args, use_container_width=full_width) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return clicked | |
| # وظيفة لإنشاء أزرار أيقونات صغيرة | |
| def icon_button(icon, key, type="primary", on_click=None, args=None, tooltip=""): | |
| """ | |
| إنشاء زر أيقونة صغير | |
| :param icon: الأيقونة (emoji أو HTML) | |
| :param key: مفتاح الزر الفريد | |
| :param type: نوع التنسيق | |
| :param on_click: الدالة التي سيتم تنفيذها عند النقر | |
| :param args: معاملات الدالة | |
| :param tooltip: تلميح عند تمرير المؤشر فوق الزر | |
| :return: زر أيقونة | |
| """ | |
| with st.container(): | |
| st.markdown(f'<div class="{type}-btn icon-btn" title="{tooltip}">', unsafe_allow_html=True) | |
| clicked = st.button(icon, key=key, on_click=on_click, args=args) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return clicked | |
| from modules.pricing.services.standard_pricing import StandardPricing | |
| from modules.pricing.services.unbalanced_pricing import UnbalancedPricing | |
| from modules.pricing.services.local_content_calculator import LocalContentCalculator | |
| from modules.pricing.services.price_prediction import PricePrediction | |
| from modules.pricing.services.construction_cost_calculator import ConstructionCostCalculator | |
| from modules.pricing.services.construction_templates import ConstructionTemplates | |
| 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() | |
| self.construction_calculator = ConstructionCostCalculator() | |
| self.construction_templates = ConstructionTemplates() | |
| 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_item_analysis_tab() | |
| with tabs[2]: | |
| self._render_comprehensive_pricing_tab() | |
| with tabs[3]: | |
| self._render_unbalanced_pricing_tab() | |
| with tabs[4]: | |
| self._render_local_content_tab() | |
| with tabs[5]: | |
| self._render_construction_calculator_tab() | |
| with tabs[6]: | |
| self._render_utilities_tab() | |
| def _render_item_analysis_tab(self): | |
| """عرض تبويب تحليل سعر البند""" | |
| st.markdown("### تحليل سعر البند") | |
| # التحقق من وجود تسعير حالي | |
| if 'current_pricing' not in st.session_state or st.session_state.current_pricing is None: | |
| st.warning("ليس هناك تسعير حالي. يرجى إنشاء تسعير جديد أولاً.") | |
| return | |
| # اختيار البند للتحليل | |
| if 'current_pricing' in st.session_state and st.session_state.current_pricing is not None: | |
| items = st.session_state.current_pricing['items'] | |
| item_options = items['رقم البند'].tolist() | |
| selected_item = st.selectbox("اختر البند للتحليل", item_options, key="item_analysis_selector") | |
| if selected_item: | |
| item_data = items[items['رقم البند'] == selected_item].iloc[0] | |
| st.markdown(f"### تحليل البند: {selected_item}") | |
| st.markdown(f"**وصف البند**: {item_data['وصف البند']}") | |
| st.markdown(f"**الوحدة**: {item_data['الوحدة']}") | |
| st.markdown(f"**الكمية**: {item_data['الكمية']}") | |
| st.markdown(f"**سعر الوحدة**: {item_data['سعر الوحدة']:,.2f} ريال") | |
| # تحليل مكونات السعر | |
| st.markdown("### تحليل مكونات السعر") | |
| # عناصر التكلفة الافتراضية | |
| cost_components = { | |
| 'المواد': 0.6, # 60% من التكلفة | |
| 'العمالة': 0.25, # 25% من التكلفة | |
| 'المعدات': 0.1, # 10% من التكلفة | |
| 'نفقات عامة': 0.05 # 5% من التكلفة | |
| } | |
| # حساب تكلفة كل عنصر | |
| unit_price = item_data['سعر الوحدة'] | |
| component_values = {k: v * unit_price for k, v in cost_components.items()} | |
| # عرض مكونات التكلفة في جدول | |
| components_df = pd.DataFrame({ | |
| 'العنصر': component_values.keys(), | |
| 'نسبة من التكلفة': [f"{v*100:.1f}%" for v in cost_components.values()], | |
| 'القيمة (ريال)': [f"{v:,.2f}" for v in component_values.values()] | |
| }) | |
| st.table(components_df) | |
| # رسم بياني لمكونات التكلفة | |
| fig = px.pie( | |
| names=list(component_values.keys()), | |
| values=list(component_values.values()), | |
| title='توزيع مكونات التكلفة' | |
| ) | |
| st.plotly_chart(fig) | |
| # تحليل تاريخي للأسعار | |
| st.markdown("### تحليل تاريخي للأسعار") | |
| # بيانات تاريخية افتراضية | |
| historical_data = { | |
| 'التاريخ': ['2020-01', '2020-07', '2021-01', '2021-07', '2022-01', '2022-07', '2023-01', '2023-07'], | |
| 'السعر': [ | |
| unit_price * 0.7, | |
| unit_price * 0.75, | |
| unit_price * 0.8, | |
| unit_price * 0.85, | |
| unit_price * 0.9, | |
| unit_price * 0.95, | |
| unit_price, | |
| unit_price * 1.05 | |
| ] | |
| } | |
| hist_df = pd.DataFrame(historical_data) | |
| # رسم بياني للتحليل التاريخي | |
| fig = px.line( | |
| hist_df, | |
| x='التاريخ', | |
| y='السعر', | |
| title='تطور سعر الوحدة عبر الزمن', | |
| markers=True | |
| ) | |
| st.plotly_chart(fig) | |
| # المقارنة مع الأسعار المرجعية | |
| st.markdown("### المقارنة مع الأسعار المرجعية") | |
| # بيانات مرجعية افتراضية | |
| reference_data = { | |
| 'المصدر': ['قاعدة البيانات الداخلية', 'دليل الأسعار الاسترشادي', 'متوسط أسعار السوق', 'أسعار المشاريع المماثلة'], | |
| 'السعر المرجعي': [ | |
| unit_price * 0.95, | |
| unit_price * 1.05, | |
| unit_price * 1.1, | |
| unit_price * 0.9 | |
| ] | |
| } | |
| ref_df = pd.DataFrame(reference_data) | |
| ref_df['الفرق عن السعر الحالي'] = ref_df['السعر المرجعي'] - unit_price | |
| ref_df['نسبة الفرق'] = (ref_df['الفرق عن السعر الحالي'] / unit_price * 100).round(2).astype(str) + '%' | |
| st.table(ref_df) | |
| def _render_new_pricing_tab(self): | |
| """عرض تبويب إنشاء تسعير جديد""" | |
| st.markdown("### إنشاء تسعير جديد") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| tender_name = st.text_input("اسم المناقصة", key="tender_name_input") | |
| client = st.text_input("الجهة المالكة", key="client_input") | |
| pricing_method = st.selectbox("طريقة التسعير", self.pricing_methods, key="pricing_method_selector") | |
| with col2: | |
| tender_number = st.text_input("رقم المناقصة", key="tender_number_input") | |
| location = st.text_input("الموقع", key="location_input") | |
| submission_date = st.date_input("تاريخ التقديم", key="submission_date_input") | |
| # خيارات بيانات البنود | |
| st.markdown("### بيانات البنود") | |
| data_source = st.radio( | |
| "مصدر بيانات البنود", | |
| ["إدخال يدوي", "استيراد من Excel", "استيراد من وحدة تحليل المستندات"], | |
| key="data_source_radio" | |
| ) | |
| if data_source == "إدخال يدوي": | |
| # ضبط 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; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # تهيئة قائمة الوحدات المتاحة | |
| unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"] | |
| # إنشاء بيانات افتراضية إذا لم تكن موجودة | |
| if 'manual_items' not in st.session_state: | |
| # إنشاء DataFrame فارغ | |
| 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 | |
| # عرض واجهة إدخال البنود | |
| st.markdown("### إدخال تفاصيل البنود") | |
| # التحقق من استخدام طريقة الإدخال البسيطة | |
| use_simple_input = st.checkbox("استخدام طريقة الإدخال البسيطة", value=True, key="use_simple_input_checkbox") | |
| if use_simple_input: | |
| # عرض البنود الحالية كجدول للعرض فقط | |
| st.markdown("### جدول البنود الحالية") | |
| st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True) | |
| # إضافة بند جديد | |
| 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_item_id") | |
| new_desc = st.text_area("وصف البند", value="", key="new_item_description") | |
| with col2: | |
| new_unit = st.selectbox("الوحدة", options=unit_options, key="new_item_unit") | |
| new_qty = st.number_input("الكمية", value=0.0, min_value=0.0, format="%.2f", key="new_item_qty") | |
| new_price = st.number_input("سعر الوحدة", value=0.0, min_value=0.0, format="%.2f", key="new_item_price") | |
| new_total = new_qty * new_price | |
| st.info(f"إجمالي البند الجديد: {new_total:,.2f} ريال") | |
| if st.button("إضافة البند", key="add_item_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)] | |
| }) | |
| # إضافة الصف إلى DataFrame | |
| st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True) | |
| st.success("تم إضافة البند بنجاح!") | |
| st.rerun() | |
| else: | |
| st.error("يرجى ملء جميع الحقول المطلوبة: رقم البند، الوصف، والكمية يجب أن تكون أكبر من صفر.") | |
| # تعديل البنود الحالية | |
| st.markdown("### تعديل البنود الحالية") | |
| # تحديد البند المراد تعديله | |
| item_to_edit = 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]}...", | |
| key="item_to_edit_selector" | |
| ) | |
| if item_to_edit: | |
| # الحصول على مؤشر الصف للبند المحدد | |
| idx = st.session_state.manual_items[st.session_state.manual_items['رقم البند'] == item_to_edit].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") | |
| edited_price = st.number_input("سعر الوحدة (تعديل)", value=float(row['سعر الوحدة']), min_value=0.0, format="%.2f", key="edit_price") | |
| edited_total = edited_qty * edited_price | |
| st.info(f"إجمالي البند بعد التعديل: {edited_total:,.2f} ريال") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("حفظ التعديلات", key="save_edit_button"): | |
| # تحديث البند | |
| 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("تم تحديث البند بنجاح!") | |
| st.rerun() | |
| with col2: | |
| if st.button("حذف هذا البند", key="delete_item_button"): | |
| st.session_state.manual_items = st.session_state.manual_items.drop(idx).reset_index(drop=True) | |
| st.warning("تم حذف البند!") | |
| st.rerun() | |
| # المجموع الكلي | |
| total = st.session_state.manual_items['الإجمالي'].sum() | |
| st.metric("المجموع الكلي", f"{total:,.2f} ريال") | |
| # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية | |
| edited_items = st.session_state.manual_items.copy() | |
| else: | |
| # عرض رسالة توضح أن طريقة الإدخال البسيطة هي الأفضل | |
| st.warning("لتجنب مشاكل عدم التوافق في أنواع البيانات، يُفضل استخدام طريقة الإدخال البسيطة.") | |
| # محاولة استخدام المحرر القياسي مع معالجة الأخطاء | |
| try: | |
| # تحويل البيانات إلى الأنواع المناسبة | |
| for col in st.session_state.manual_items.columns: | |
| if col in ['رقم البند', 'وصف البند', 'الوحدة']: | |
| st.session_state.manual_items[col] = st.session_state.manual_items[col].astype(str) | |
| # عرض المحرر (للقراءة فقط) | |
| st.dataframe(st.session_state.manual_items, use_container_width=True, hide_index=True) | |
| # إنشاء نظام تعديل منفصل | |
| st.markdown("### تعديل أسعار الوحدات") | |
| for idx, row in st.session_state.manual_items.iterrows(): | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| st.text(f"{row['رقم البند']}: {row['وصف البند'][:50]}") | |
| with col2: | |
| price = st.number_input( | |
| f"سعر الوحدة ({row['الوحدة']})", | |
| value=float(row['سعر الوحدة']), | |
| min_value=0.0, | |
| key=f"price_{idx}" | |
| ) | |
| # تحديث السعر والإجمالي | |
| st.session_state.manual_items.at[idx, 'سعر الوحدة'] = price | |
| st.session_state.manual_items.at[idx, 'الإجمالي'] = price * row['الكمية'] | |
| # المجموع الكلي | |
| total = st.session_state.manual_items['الإجمالي'].sum() | |
| st.metric("المجموع الكلي", f"{total:,.2f} ريال") | |
| # جعل هذه البيانات متاحة للاستخدام في الخطوات التالية | |
| edited_items = st.session_state.manual_items.copy() | |
| except Exception as e: | |
| st.error(f"حدث خطأ: {str(e)}") | |
| st.info("يرجى استخدام طريقة الإدخال البسيطة لتجنب هذه المشكلة.") | |
| 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({ | |
| 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"], | |
| 'وصف البند': [ | |
| "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", | |
| "توريد وتركيب حديد التسليح للأساسات", | |
| "أعمال العزل المائي للأساسات", | |
| "أعمال الردم والدك للأساسات", | |
| "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", | |
| "توريد وتركيب حديد التسليح للأعمدة", | |
| "أعمال البلوك للجدران" | |
| ], | |
| 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"], | |
| 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 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.0] | |
| }) | |
| st.dataframe(import_items) | |
| if st.button("استيراد البيانات", key="import_excel_button"): | |
| st.session_state.manual_items = import_items.copy() | |
| st.session_state.manual_items_modified = True | |
| st.success("تم استيراد البيانات بنجاح!") | |
| st.rerun() | |
| else: # استيراد من وحدة تحليل المستندات | |
| available_documents = [ | |
| "كراسة شروط مشروع توسعة مستشفى الملك فهد", | |
| "جدول كميات صيانة محطات المياه", | |
| "مخططات إنشاء مدرسة ثانوية" | |
| ] | |
| selected_doc = st.selectbox("اختر المستند", available_documents, key="document_selector") | |
| if styled_button("استيراد البيانات من تحليل المستند", key="import_doc_analysis_button", type="info", icon="📄", full_width=True): | |
| # محاكاة استيراد البيانات | |
| with st.spinner("جاري استيراد البيانات..."): | |
| time.sleep(2) | |
| # إنشاء بيانات افتراضية | |
| doc_items = pd.DataFrame({ | |
| 'رقم البند': ["A1", "A2", "A3", "A4", "A5", "A6", "A7"], | |
| 'وصف البند': [ | |
| "توريد وتركيب أعمال الخرسانة المسلحة للأساسات", | |
| "توريد وتركيب حديد التسليح للأساسات", | |
| "أعمال العزل المائي للأساسات", | |
| "أعمال الردم والدك للأساسات", | |
| "توريد وتركيب أعمال الخرسانة المسلحة للأعمدة", | |
| "توريد وتركيب حديد التسليح للأعمدة", | |
| "أعمال البلوك للجدران" | |
| ], | |
| 'الوحدة': ["م3", "طن", "م2", "م3", "م3", "طن", "م2"], | |
| 'الكمية': [250.0, 25.0, 500.0, 300.0, 120.0, 10.0, 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.0] | |
| }) | |
| st.session_state.manual_items = doc_items.copy() | |
| st.success("تم استيراد البيانات من تحليل المستند بنجاح!") | |
| st.dataframe(doc_items) | |
| # زر بدء التسعير | |
| if styled_button("بدء التسعير", key="start_pricing_button", type="success", icon="✅", full_width=True): | |
| # تحقق من صحة البيانات | |
| if 'manual_items' in st.session_state and not st.session_state.manual_items.empty: | |
| # التأكد من حساب الإجمالي قبل الحفظ | |
| st.session_state.manual_items['الإجمالي'] = st.session_state.manual_items['الكمية'] * st.session_state.manual_items['سعر الوحدة'] | |
| # حفظ بيانات التسعير الحالي | |
| 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['سعر الوحدة'] | |
| # عرض البنود | |
| st.dataframe(items, use_container_width=True, hide_index=True) | |
| # ✅ التوصية الذكية باستخدام OpenAI | |
| with st.expander("🔍 توليد توصية ذكية باستخدام AI"): | |
| if styled_button("توليد توصية ذكية باستخدام AI", key="gen_ai_recommendation_btn", type="secondary", icon="🤖", full_width=True): | |
| import openai | |
| import os | |
| # تهيئة عميل OpenAI - استخدام واجهة الإصدار الجديد فقط (1.0.0 وما فوق) | |
| api_key = os.environ.get("ai") | |
| client = openai.OpenAI(api_key=api_key) | |
| items_df = items.copy() | |
| prompt = f"""قم بتحليل الجدول التالي للبنود في مشروع إنشاء، وقدم توصية ذكية لتحسين التسعير وضمان التوازن المالي. الجدول يحتوي على البنود، الكميات، الأسعار، والإجماليات:\n\n{items_df.to_string(index=False)}\n\nالتوصية:\n""" | |
| try: | |
| with st.spinner("جاري توليد التوصية..."): | |
| # استخدام واجهة OpenAI الجديدة | |
| response = client.chat.completions.create( | |
| model="gpt-4o", # استخدام أحدث نموذج من OpenAI GPT-4o | |
| messages=[ | |
| {"role": "system", "content": "أنت خبير في تسعير مشاريع البناء والبنية التحتية."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| temperature=0.4, | |
| max_tokens=500 | |
| ) | |
| # استخراج محتوى الرسالة من واجهة OpenAI الجديدة | |
| recommendation = response.choices[0].message.content | |
| st.success("تم توليد التوصية بنجاح!") | |
| st.markdown("#### التوصية الذكية:") | |
| st.info(recommendation) | |
| except Exception as e: | |
| st.error(f"حدث خطأ أثناء الاتصال بنموذج OpenAI: {e}") | |
| st.info("يجب التأكد من تثبيت أحدث إصدار من مكتبة OpenAI: `pip install openai --upgrade`") | |
| # واجهة تعديل أسعار الوحدات | |
| st.markdown("### تعديل أسعار الوحدات") | |
| # تقسيم البنود إلى مجموعتين للعرض | |
| col1, col2 = st.columns(2) | |
| half = len(items) // 2 + len(items) % 2 | |
| with col1: | |
| for idx in range(half): | |
| if idx < len(items): | |
| row = items.iloc[idx] | |
| price = st.number_input( | |
| f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})", | |
| value=float(row['سعر الوحدة']), | |
| min_value=0.0, | |
| key=f"price1_{idx}" | |
| ) | |
| items.at[idx, 'سعر الوحدة'] = price | |
| items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية'] | |
| with col2: | |
| for idx in range(half, len(items)): | |
| row = items.iloc[idx] | |
| price = st.number_input( | |
| f"{row['رقم البند']}: {row['وصف البند'][:30]}... ({row['الوحدة']})", | |
| value=float(row['سعر الوحدة']), | |
| min_value=0.0, | |
| key=f"price2_{idx}" | |
| ) | |
| items.at[idx, 'سعر الوحدة'] = price | |
| items.at[idx, 'الإجمالي'] = price * items.at[idx, 'الكمية'] | |
| # حساب وعرض إجماليات التسعير | |
| total_price = 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 = 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 styled_button("حفظ التسعير", key="save_comprehensive_pricing_button", type="primary", icon="💾"): | |
| # تحديث بيانات التسعير الحالي | |
| st.session_state.current_pricing['items'] = items.copy() | |
| st.success("تم حفظ التسعير بنجاح!") | |
| with col2: | |
| if styled_button("تصدير إلى Excel", key="export_to_excel_button", type="info", icon="📊"): | |
| st.success("تم تصدير التسعير إلى Excel بنجاح!") | |
| with col3: | |
| if styled_button("تحليل المخاطر المالية", key="financial_risk_analysis_button", type="warning", icon="⚠️"): | |
| 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)", | |
| "تحميل البنود المؤكدة", | |
| "تخفيض البنود المحتمل زيادتها", | |
| "إستراتيجية مخصصة" | |
| ], | |
| key="pricing_strategy_selector" | |
| ) | |
| # تطبيق الإستراتيجية المختارة | |
| 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("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً من خلال النموذج أدناه.") | |
| # إضافة واجهة لتعديل الأسعار يدوياً | |
| if 'edit_items' not in st.session_state: | |
| st.session_state.edit_items = items.copy() | |
| # عرض نموذج التعديل لكل بند | |
| for index, row in items.iterrows(): | |
| with st.container(): | |
| col1, col2, col3, col4 = st.columns([3, 1, 1, 1]) | |
| with col1: | |
| st.markdown(f"**البند:** {row['وصف البند']}") | |
| with col2: | |
| original_price = st.session_state.current_pricing['items'].iloc[index]['سعر الوحدة'] | |
| new_price = st.number_input( | |
| "سعر الوحدة الجديد", | |
| min_value=0.01, | |
| value=float(row['سعر الوحدة']), | |
| key=f"price_{index}" | |
| ) | |
| items.at[index, 'سعر الوحدة'] = new_price | |
| with col3: | |
| percent_change = ((new_price - original_price) / original_price) * 100 | |
| st.metric( | |
| "نسبة التغيير", | |
| f"{percent_change:.1f}%", | |
| delta=f"{new_price - original_price:.2f}" | |
| ) | |
| with col4: | |
| strategy_options = ['متوازن', 'زيادة', 'نقص'] | |
| current_strategy = row['إستراتيجية التسعير'] | |
| strategy_index = strategy_options.index(current_strategy) if current_strategy in strategy_options else 0 | |
| new_strategy = st.selectbox( | |
| "الإستراتيجية", | |
| options=strategy_options, | |
| index=strategy_index, | |
| key=f"strategy_{index}" | |
| ) | |
| items.at[index, 'إستراتيجية التسعير'] = new_strategy | |
| st.markdown("---") | |
| # حساب الإجمالي بعد التعديل | |
| items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] | |
| # تعيين ألوان للإستراتيجيات وتنسيق الجدول بشكل متقدم | |
| def highlight_row(row): | |
| strategy = row['إستراتيجية التسعير'] | |
| styles = [''] * len(row) | |
| # تطبيق لون خلفية لكل صف حسب الإستراتيجية | |
| if strategy == 'زيادة': | |
| background = 'linear-gradient(90deg, rgba(168, 230, 207, 0.3), rgba(168, 230, 207, 0.1))' | |
| text_color = '#1F7A8C' | |
| elif strategy == 'نقص': | |
| background = 'linear-gradient(90deg, rgba(255, 154, 162, 0.3), rgba(255, 154, 162, 0.1))' | |
| text_color = '#9D2A45' | |
| else: | |
| background = 'linear-gradient(90deg, rgba(220, 237, 255, 0.3), rgba(220, 237, 255, 0.1))' | |
| text_color = '#555555' | |
| # تطبيق النمط على جميع الخلايا في الصف | |
| for i in range(len(styles)): | |
| styles[i] = f'background: {background}; color: {text_color}; border-bottom: 1px solid #ddd;' | |
| # تطبيق نمط خاص على خلية الإستراتيجية | |
| if strategy == 'زيادة': | |
| styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #a8e6cf; color: #007263; font-weight: bold; border-radius: 5px; text-align: center;' | |
| elif strategy == 'نقص': | |
| styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #ff9aa2; color: #9D2A45; font-weight: bold; border-radius: 5px; text-align: center;' | |
| else: | |
| styles[list(row.index).index('إستراتيجية التسعير')] = 'background-color: #dceeff; color: #555555; font-weight: bold; border-radius: 5px; text-align: center;' | |
| # تنسيق عمود السعر | |
| price_idx = list(row.index).index('سعر الوحدة') | |
| styles[price_idx] = styles[price_idx] + 'font-weight: bold;' | |
| # تنسيق عمود الإجمالي | |
| total_idx = list(row.index).index('الإجمالي') | |
| styles[total_idx] = styles[total_idx] + 'font-weight: bold;' | |
| return styles | |
| # عرض الجدول مع تنسيق متقدم | |
| st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>بنود التسعير غير المتزن</h3>", unsafe_allow_html=True) | |
| # تطبيق التنسيق على الجدول | |
| styled_items = items.style.apply(highlight_row, axis=1) | |
| # تنسيق تنسيق الأرقام | |
| styled_items = styled_items.format({ | |
| 'الكمية': '{:,.2f}', | |
| 'سعر الوحدة': '{:,.2f}', | |
| 'الإجمالي': '{:,.2f}' | |
| }) | |
| st.dataframe(styled_items, use_container_width=True, height=None) | |
| # المقارنة بين التسعير المتوازن وغير المتوازن | |
| st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>مقارنة التسعير المتوازن وغير المتوازن</h3>", unsafe_allow_html=True) | |
| original_items = st.session_state.current_pricing['items'].copy() | |
| original_total = original_items['الإجمالي'].sum() | |
| unbalanced_total = items['الإجمالي'].sum() | |
| # عرض بطاقات المقارنة بتصميم متقدم | |
| st.markdown(""" | |
| <style> | |
| .metric-container { | |
| background: linear-gradient(to right, #f1f8ff, #ffffff); | |
| border-radius: 10px; | |
| padding: 15px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
| text-align: center; | |
| border: 1px solid #e6f2ff; | |
| } | |
| .metric-title { | |
| color: #555; | |
| font-size: 0.9em; | |
| margin-bottom: 5px; | |
| } | |
| .metric-value { | |
| color: #1F7A8C; | |
| font-size: 1.8em; | |
| font-weight: bold; | |
| margin: 5px 0; | |
| } | |
| .metric-delta { | |
| font-size: 0.9em; | |
| font-weight: bold; | |
| padding: 3px 8px; | |
| border-radius: 10px; | |
| display: inline-block; | |
| margin-top: 5px; | |
| } | |
| .positive-delta { | |
| background-color: rgba(40, 167, 69, 0.1); | |
| color: #28a745; | |
| } | |
| .negative-delta { | |
| background-color: rgba(220, 53, 69, 0.1); | |
| color: #dc3545; | |
| } | |
| .neutral-delta { | |
| background-color: rgba(108, 117, 125, 0.1); | |
| color: #6c757d; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.markdown(""" | |
| <div class="metric-container"> | |
| <div class="metric-title">إجمالي التسعير المتوازن</div> | |
| <div class="metric-value">{:,.2f} ريال</div> | |
| <div class="metric-delta neutral-delta">التسعير الأصلي</div> | |
| </div> | |
| """.format(original_total), unsafe_allow_html=True) | |
| with col2: | |
| st.markdown(""" | |
| <div class="metric-container"> | |
| <div class="metric-title">إجمالي التسعير غير المتوازن</div> | |
| <div class="metric-value">{:,.2f} ريال</div> | |
| <div class="metric-delta {}">بعد إعادة توزيع الأسعار</div> | |
| </div> | |
| """.format( | |
| unbalanced_total, | |
| "positive-delta" if unbalanced_total > original_total else "negative-delta" if unbalanced_total < original_total else "neutral-delta" | |
| ), unsafe_allow_html=True) | |
| with col3: | |
| diff = unbalanced_total - original_total | |
| delta_percent = diff/original_total*100 if original_total > 0 else 0 | |
| st.markdown(""" | |
| <div class="metric-container"> | |
| <div class="metric-title">الفرق بين التسعيرين</div> | |
| <div class="metric-value">{:,.2f} ريال</div> | |
| <div class="metric-delta {}">نسبة الفرق: {:+.1f}%</div> | |
| </div> | |
| """.format( | |
| diff, | |
| "positive-delta" if diff > 0 else "negative-delta" if diff < 0 else "neutral-delta", | |
| delta_percent | |
| ), unsafe_allow_html=True) | |
| # المعايرة للحفاظ على إجمالي التسعير | |
| if abs(diff) > 1: # إذا كان هناك فرق كبير | |
| if styled_button("معايرة الأسعار للحفاظ على إجمالي التسعير", key="calibrate_prices_button", type="primary", icon="⚖️", full_width=True): | |
| # تعديل الأسعار للحفاظ على إجمالي التكلفة | |
| 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("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>تحليل بصري للتسعير غير المتوازن</h3>", unsafe_allow_html=True) | |
| # إعداد البيانات للرسم البياني | |
| chart_data = pd.DataFrame({ | |
| 'وصف البند': original_items['وصف البند'], | |
| 'التسعير المتوازن': original_items['الإجمالي'], | |
| 'التسعير غير المتوازن': items['الإجمالي'] | |
| }) | |
| # إضافة عمود للنسبة المئوية للتغيير | |
| chart_data['نسبة التغيير'] = (chart_data['التسعير غير المتوازن'] - chart_data['التسعير المتوازن']) / chart_data['التسعير المتوازن'] * 100 | |
| # تحديد لون الأعمدة بناءً على نسبة التغيير | |
| bar_colors = [] | |
| for change in chart_data['نسبة التغيير']: | |
| if change > 5: # زيادة كبيرة | |
| bar_colors.append('#1F7A8C') # أزرق مخضر | |
| elif change > 0: # زيادة صغيرة | |
| bar_colors.append('#81B29A') # أخضر فاتح | |
| elif change > -5: # نقص صغير | |
| bar_colors.append('#F2CC8F') # أصفر | |
| else: # نقص كبير | |
| bar_colors.append('#E07A5F') # أحمر | |
| # التبويب بين مخططات مختلفة للمقارنة | |
| chart_tabs = st.tabs(["مخطط شريطي", "مخطط مقارنة", "مخطط نسبة التغيير"]) | |
| with chart_tabs[0]: # رسم بياني شريطي | |
| # رسم بياني شريطي للمقارنة | |
| fig = go.Figure() | |
| fig.add_trace(go.Bar( | |
| x=chart_data['وصف البند'], | |
| y=chart_data['التسعير المتوازن'], | |
| name='التسعير المتوازن', | |
| marker_color='rgba(55, 83, 109, 0.7)' | |
| )) | |
| fig.add_trace(go.Bar( | |
| x=chart_data['وصف البند'], | |
| y=chart_data['التسعير غير المتوازن'], | |
| name='التسعير غير المتوازن', | |
| marker_color=bar_colors | |
| )) | |
| fig.update_layout( | |
| title='مقارنة بين التسعير المتوازن وغير المتوازن', | |
| xaxis_tickfont_size=14, | |
| yaxis=dict( | |
| title='الإجمالي (ريال)', | |
| titlefont_size=16, | |
| tickfont_size=14, | |
| ), | |
| legend=dict( | |
| x=0.01, | |
| y=0.99, | |
| bgcolor='rgba(255, 255, 255, 0.8)', | |
| bordercolor='rgba(0, 0, 0, 0.1)', | |
| borderwidth=1 | |
| ), | |
| barmode='group', | |
| bargap=0.15, | |
| bargroupgap=0.1, | |
| plot_bgcolor='rgba(240, 249, 255, 0.5)', | |
| margin=dict(t=50, b=50, l=20, r=20) | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| with chart_tabs[1]: # رسم مقارنة | |
| # رسم مقارنة بين التسعيرين | |
| fig = go.Figure() | |
| # إضافة خط للتسعير المتوازن | |
| fig.add_trace(go.Scatter( | |
| x=chart_data['وصف البند'], | |
| y=chart_data['التسعير المتوازن'], | |
| name='التسعير المتوازن', | |
| mode='lines+markers', | |
| line=dict(color='rgb(55, 83, 109)', width=3), | |
| marker=dict(size=10, color='rgb(55, 83, 109)') | |
| )) | |
| # إضافة نقاط للتسعير غير المتوازن | |
| fig.add_trace(go.Scatter( | |
| x=chart_data['وصف البند'], | |
| y=chart_data['التسعير غير المتوازن'], | |
| name='التسعير غير المتوازن', | |
| mode='lines+markers', | |
| line=dict(color='rgb(26, 118, 255)', width=3), | |
| marker=dict( | |
| size=12, | |
| color=bar_colors, | |
| line=dict(width=2, color='white') | |
| ) | |
| )) | |
| # تحديثات التخطيط | |
| fig.update_layout( | |
| title='مقارنة مرئية بين استراتيجيات التسعير', | |
| xaxis_tickfont_size=14, | |
| yaxis=dict( | |
| title='القيمة الإجمالية (ريال)', | |
| titlefont_size=16, | |
| tickfont_size=14, | |
| gridcolor='rgba(200, 200, 200, 0.2)' | |
| ), | |
| legend=dict( | |
| x=0.01, | |
| y=0.99, | |
| bgcolor='rgba(255, 255, 255, 0.8)', | |
| bordercolor='rgba(0, 0, 0, 0.1)', | |
| borderwidth=1 | |
| ), | |
| plot_bgcolor='rgba(240, 249, 255, 0.5)', | |
| margin=dict(t=50, b=50, l=20, r=20) | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| with chart_tabs[2]: # مخطط نسبة التغيير | |
| # مخطط للنسبة المئوية للتغيير | |
| fig = go.Figure() | |
| # إضافة أعمدة لنسبة التغيير مع ألوان مختلفة حسب القيمة | |
| fig.add_trace(go.Bar( | |
| x=chart_data['وصف البند'], | |
| y=chart_data['نسبة التغيير'], | |
| name='نسبة التغيير', | |
| marker_color=bar_colors, | |
| text=[f"{val:.1f}%" for val in chart_data['نسبة التغيير']], | |
| textposition='auto' | |
| )) | |
| # إضافة خط أفقي عند الصفر | |
| fig.add_shape( | |
| type="line", | |
| x0=-0.5, | |
| y0=0, | |
| x1=len(chart_data['وصف البند'])-0.5, | |
| y1=0, | |
| line=dict( | |
| color="black", | |
| width=2, | |
| dash="dash", | |
| ) | |
| ) | |
| # تحديثات التخطيط | |
| fig.update_layout( | |
| title='نسبة التغيير في أسعار البنود (%)', | |
| xaxis_tickfont_size=14, | |
| yaxis=dict( | |
| title='نسبة التغيير (%)', | |
| titlefont_size=16, | |
| tickfont_size=14, | |
| gridcolor='rgba(200, 200, 200, 0.2)', | |
| zeroline=True, | |
| zerolinecolor='black', | |
| zerolinewidth=2 | |
| ), | |
| plot_bgcolor='rgba(240, 249, 255, 0.5)', | |
| margin=dict(t=50, b=50, l=20, r=20) | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # إضافة جدول مع نسب التغيير | |
| st.markdown("#### جدول مفصل بنسب التغيير") | |
| # إعداد بيانات الجدول | |
| table_data = chart_data[['وصف البند', 'التسعير المتوازن', 'التسعير غير المتوازن', 'نسبة التغيير']] | |
| # تنسيق الجدول | |
| def highlight_change(row): | |
| change = row['نسبة التغيير'] | |
| if change > 5: | |
| return ['', '', '', 'background-color: rgba(31, 122, 140, 0.3); color: #1F7A8C; font-weight: bold;'] | |
| elif change > 0: | |
| return ['', '', '', 'background-color: rgba(129, 178, 154, 0.3); color: #2A9D8F; font-weight: bold;'] | |
| elif change > -5: | |
| return ['', '', '', 'background-color: rgba(242, 204, 143, 0.3); color: #BC6C25; font-weight: bold;'] | |
| else: | |
| return ['', '', '', 'background-color: rgba(224, 122, 95, 0.3); color: #AE2012; font-weight: bold;'] | |
| # تطبيق التنسيق | |
| styled_table = table_data.style.apply(highlight_change, axis=1).format({ | |
| 'التسعير المتوازن': '{:,.2f} ريال', | |
| 'التسعير غير المتوازن': '{:,.2f} ريال', | |
| 'نسبة التغيير': '{:+.1f}%' | |
| }) | |
| st.dataframe(styled_table, use_container_width=True) | |
| # قسم لإضافة أو حذف البنود | |
| st.markdown("### إدارة بنود التسعير") | |
| # إضافة بند جديد | |
| with st.expander("إضافة بند جديد"): | |
| col1, col2, col3, col4 = st.columns([3, 1, 1, 1]) | |
| with col1: | |
| new_item_desc = st.text_input("وصف البند", placeholder="أدخل وصف البند الجديد") | |
| with col2: | |
| new_item_unit = st.selectbox( | |
| "الوحدة", | |
| options=["م3", "م2", "متر طولي", "طن", "قطعة", "كجم", "لتر"], | |
| index=0, | |
| key="construction_item_unit" | |
| ) | |
| with col3: | |
| new_item_qty = st.number_input("الكمية", min_value=0.1, value=1.0, format="%.2f") | |
| with col4: | |
| new_item_price = st.number_input("سعر الوحدة", min_value=0.1, value=100.0, format="%.2f") | |
| if st.button("إضافة البند"): | |
| if new_item_desc: | |
| # إنشاء رقم البند الجديد | |
| new_id = f"UB{len(items)+1}" | |
| # إنشاء صف جديد | |
| new_row = pd.DataFrame({ | |
| 'رقم البند': [new_id], | |
| 'وصف البند': [new_item_desc], | |
| 'الوحدة': [new_item_unit], | |
| 'الكمية': [float(new_item_qty)], | |
| 'سعر الوحدة': [float(new_item_price)], | |
| 'الإجمالي': [float(new_item_qty * new_item_price)], | |
| 'إستراتيجية التسعير': ['متوازن'] | |
| }) | |
| # إضافة الصف إلى DataFrame | |
| items = pd.concat([items, new_row], ignore_index=True) | |
| # تحديث الإجمالي | |
| items['الإجمالي'] = items['الكمية'] * items['سعر الوحدة'] | |
| st.success(f"تم إضافة البند \"{new_item_desc}\" بنجاح!") | |
| st.rerun() | |
| else: | |
| st.warning("يرجى إدخال وصف للبند") | |
| # حذف بند | |
| with st.expander("حذف بند"): | |
| if len(items) > 0: | |
| # قائمة بالبنود الحالية | |
| item_options = [f"{row['رقم البند']} - {row['وصف البند']}" for idx, row in items.iterrows()] | |
| selected_item_to_delete = st.selectbox( | |
| "اختر البند المراد حذفه", | |
| options=item_options, | |
| key="item_to_delete_selector" | |
| ) | |
| # استخراج رقم البند | |
| item_id_to_delete = selected_item_to_delete.split(" - ")[0] | |
| if st.button("حذف البند", key="delete_item_button_2"): | |
| # البحث عن البند وحذفه | |
| items = items[items['رقم البند'] != item_id_to_delete] | |
| st.success(f"تم حذف البند {item_id_to_delete} بنجاح!") | |
| st.rerun() | |
| else: | |
| st.info("لا توجد بنود لحذفها") | |
| # أزرار الحفظ والتصدير مع تصميم محسن | |
| st.markdown("<hr style='margin-top: 30px; margin-bottom: 20px; border-top: 1px solid #ddd;'>", unsafe_allow_html=True) | |
| st.markdown("<h3 style='color: #1F7A8C; background: linear-gradient(to right, #f0f9ff, #ffffff); padding: 10px; border-radius: 5px;'>حفظ وتصدير البيانات</h3>", unsafe_allow_html=True) | |
| st.markdown(""" | |
| <style> | |
| .action-card { | |
| background: linear-gradient(135deg, #f8f9fa, #e9ecef); | |
| border-radius: 10px; | |
| padding: 20px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
| border-left: 5px solid #1F7A8C; | |
| transition: all 0.3s ease; | |
| } | |
| .action-card:hover { | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| transform: translateY(-2px); | |
| } | |
| .action-icon { | |
| color: #1F7A8C; | |
| font-size: 24px; | |
| margin-bottom: 10px; | |
| } | |
| .action-title { | |
| color: #333; | |
| font-size: 18px; | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| } | |
| .action-desc { | |
| color: #666; | |
| font-size: 14px; | |
| margin-bottom: 15px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # بطاقة حفظ التسعير | |
| st.markdown(""" | |
| <div class="action-card"> | |
| <div class="action-icon">💾</div> | |
| <div class="action-title">حفظ التسعير غير المتوازن</div> | |
| <div class="action-desc">قم بحفظ التسعير الحالي في المشروع لاستخدامه لاحقاً في التقارير وفي إجمالي التسعير.</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # زر حفظ التسعير غير المتوازن | |
| if st.button("حفظ التسعير غير المتوازن", type="primary", use_container_width=True, key="save_unbalanced_pricing_button"): | |
| st.session_state.current_pricing['items'] = items.copy() | |
| st.session_state.current_pricing['method'] = "التسعير غير المتزن" | |
| st.success("تم حفظ التسعير غير المتوازن بنجاح!") | |
| st.balloons() # إضافة تأثير احتفالي عند الحفظ | |
| with col2: | |
| # بطاقة تصدير التسعير | |
| st.markdown(""" | |
| <div class="action-card"> | |
| <div class="action-icon">📊</div> | |
| <div class="action-title">تصدير البيانات</div> | |
| <div class="action-desc">قم بتصدير جدول التسعير الحالي بصيغة CSV لاستخدامه في برامج أخرى مثل Excel.</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # زر تصدير التسعير | |
| export_button = st.button("تجهيز ملف للتصدير", use_container_width=True, key="prepare_export_file_button") | |
| if export_button: | |
| # تحويل البيانات إلى CSV | |
| csv = items.to_csv(index=False) | |
| st.success("تم تجهيز ملف التصدير بنجاح! يمكنك تنزيله الآن.") | |
| # تقديم البيانات للتنزيل | |
| st.download_button( | |
| label="تنزيل ملف CSV", | |
| data=csv, | |
| file_name="unbalanced_pricing.csv", | |
| mime="text/csv", | |
| use_container_width=True | |
| ) | |
| def _render_construction_calculator_tab(self): | |
| """عرض تبويب حاسبة تكاليف البناء""" | |
| st.markdown("### حاسبة تكاليف البناء المتكاملة") | |
| # شرح حاسبة تكاليف البناء | |
| with st.expander("دليل استخدام حاسبة تكاليف البناء", expanded=False): | |
| st.markdown(""" | |
| **حاسبة تكاليف البناء المتكاملة** هي أداة تساعد في حساب تكاليف البناء بشكل تفصيلي، مع مراعاة جميع عناصر التكلفة: | |
| ### مكونات التكلفة: | |
| 1. **المواد الخام**: جميع المواد المستخدمة في البناء مثل الخرسانة، الحديد، الطوب، الأسمنت، وغيرها. | |
| 2. **العمالة**: تكاليف جميع العمالة المطلوبة بمختلف تخصصاتها. | |
| 3. **المعدات**: تكاليف استخدام أو استئجار المعدات اللازمة للمشروع. | |
| 4. **المصاريف الإدارية**: النفقات العامة والإدارية للمشروع (نسبة من التكلفة المباشرة). | |
| 5. **هامش الربح**: نسبة الربح المضافة على التكلفة. | |
| ### طريقة الاستخدام: | |
| 1. اختر إما حساب تكلفة بند واحد أو حساب تكلفة مشروع كامل. | |
| 2. أدخل بيانات المواد والعمالة والمعدات المستخدمة. | |
| 3. حدد نسب المصاريف الإدارية وهامش الربح. | |
| 4. اضبط عوامل التعديل حسب ظروف المشروع. | |
| 5. احصل على تحليل تفصيلي للتكاليف والسعر النهائي. | |
| ### ميزات الحاسبة: | |
| - قاعدة بيانات مدمجة للأسعار المرجعية للمواد والعمالة والمعدات. | |
| - تحليل نسب مساهمة كل عنصر في التكلفة الإجمالية. | |
| - إمكانية تعديل عوامل التكلفة حسب الموقع والظروف السوقية. | |
| - تصدير النتائج بتنسيقات مختلفة. | |
| - الربط مع وحدة التسعير لنقل النتائج مباشرة إلى جدول التسعير. | |
| - قاعدة بيانات للبنود النموذجية في أعمال المقاولات (مناهل، مواسير، إسفلت، إلخ). | |
| """) | |
| # تبويبات حاسبة التكاليف | |
| calc_tabs = st.tabs(["حساب تكلفة بند", "حساب تكلفة مشروع", "قوائم الأسعار المرجعية", "كتالوج أعمال المقاولات"]) | |
| with calc_tabs[0]: # حساب تكلفة بند | |
| st.markdown("#### حساب تكلفة بند بناء") | |
| # تهيئة بيانات البند الافتراضية إذا لم تكن موجودة | |
| if 'construction_item' not in st.session_state: | |
| st.session_state.construction_item = { | |
| 'وصف_البند': "توريد وصب خرسانة مسلحة للأساسات", | |
| 'الكمية': 25.0, | |
| 'الوحدة': "م3", | |
| 'المواد': [ | |
| {'الاسم': 'خرسانة جاهزة', 'الكمية': 25.0, 'الوحدة': 'م3', 'سعر_الوحدة': 750.0}, | |
| {'الاسم': 'حديد تسليح', 'الكمية': 3.5, 'الوحدة': 'طن', 'سعر_الوحدة': 5500.0} | |
| ], | |
| 'العمالة': [ | |
| {'النوع': 'عامل خرسانة', 'العدد': 6, 'المدة': 1.0, 'سعر_اليوم': 150.0}, | |
| {'النوع': 'حداد مسلح', 'العدد': 4, 'المدة': 3.0, 'سعر_اليوم': 250.0}, | |
| {'النوع': 'نجار مسلح', 'العدد': 4, 'المدة': 3.0, 'سعر_اليوم': 250.0} | |
| ], | |
| 'المعدات': [ | |
| {'النوع': 'مضخة خرسانة', 'العدد': 1.0, 'المدة': 0.5, 'سعر_اليوم': 5000.0}, | |
| {'النوع': 'هزاز خرسانة', 'العدد': 2.0, 'المدة': 1.0, 'سعر_اليوم': 150.0} | |
| ], | |
| 'المصاريف_الإدارية': 0.05, # 5% | |
| 'هامش_الربح': 0.10, # 10% | |
| 'عوامل_التعديل': { | |
| 'location_factor': 1.0, | |
| 'time_factor': 1.0, | |
| 'risk_factor': 1.0, | |
| 'market_factor': 1.0 | |
| } | |
| } | |
| # نموذج إدخال بيانات البند | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.session_state.construction_item['وصف_البند'] = st.text_area( | |
| "وصف البند", | |
| value=st.session_state.construction_item['وصف_البند'], | |
| key="construction_item_description" | |
| ) | |
| with col2: | |
| st.session_state.construction_item['الكمية'] = st.number_input( | |
| "الكمية", | |
| value=st.session_state.construction_item['الكمية'], | |
| min_value=0.1, | |
| format="%.2f" | |
| ) | |
| unit_options = ["م3", "م2", "طن", "متر طولي", "قطعة", "كجم", "لتر"] | |
| st.session_state.construction_item['الوحدة'] = st.selectbox( | |
| "الوحدة", | |
| options=unit_options, | |
| index=unit_options.index(st.session_state.construction_item['الوحدة']) if st.session_state.construction_item['الوحدة'] in unit_options else 0, | |
| key="construction_item_unit_2" | |
| ) | |
| # إدخال تفاصيل المواد | |
| st.markdown("#### تفاصيل المواد") | |
| material_controls = [] | |
| for i, material in enumerate(st.session_state.construction_item['المواد']): | |
| col1, col2, col3, col4, col5 = st.columns([3, 2, 1, 2, 1]) | |
| with col1: | |
| material_name = st.text_input( | |
| "اسم المادة", | |
| value=material['الاسم'], | |
| key=f"material_name_{i}" | |
| ) | |
| with col2: | |
| material_qty = st.number_input( | |
| "الكمية", | |
| value=material['الكمية'], | |
| min_value=0.0, | |
| format="%.2f", | |
| key=f"material_qty_{i}" | |
| ) | |
| with col3: | |
| material_unit = st.selectbox( | |
| "الوحدة", | |
| options=unit_options, | |
| index=unit_options.index(material['الوحدة']) if material['الوحدة'] in unit_options else 0, | |
| key=f"material_unit_{i}" | |
| ) | |
| with col4: | |
| material_price = st.number_input( | |
| "سعر الوحدة", | |
| value=material['سعر_الوحدة'], | |
| min_value=0.0, | |
| format="%.2f", | |
| key=f"material_price_{i}" | |
| ) | |
| with col5: | |
| delete_button = st.button("حذف", key=f"delete_material_{i}") | |
| material_controls.append({ | |
| 'الاسم': material_name, | |
| 'الكمية': material_qty, | |
| 'الوحدة': material_unit, | |
| 'سعر_الوحدة': material_price, | |
| 'delete': delete_button | |
| }) | |
| # إضافة مادة جديدة | |
| if st.button("إضافة مادة جديدة"): | |
| st.session_state.construction_item['المواد'].append({ | |
| 'الاسم': '', | |
| 'الكمية': 0.0, | |
| 'الوحدة': 'م3', | |
| 'سعر_الوحدة': 0.0 | |
| }) | |
| st.rerun() | |
| # تحديث أو حذف المواد | |
| new_materials = [] | |
| for i, control in enumerate(material_controls): | |
| if not control['delete']: | |
| new_materials.append({ | |
| 'الاسم': control['الاسم'], | |
| 'الكمية': control['الكمية'], | |
| 'الوحدة': control['الوحدة'], | |
| 'سعر_الوحدة': control['سعر_الوحدة'] | |
| }) | |
| if len(new_materials) != len(st.session_state.construction_item['المواد']): | |
| st.session_state.construction_item['المواد'] = new_materials | |
| st.rerun() | |
| else: | |
| for i, material in enumerate(new_materials): | |
| st.session_state.construction_item['المواد'][i] = material | |
| # إدخال تفاصيل العمالة | |
| st.markdown("#### تفاصيل العمالة") | |
| labor_controls = [] | |
| for i, labor in enumerate(st.session_state.construction_item['العمالة']): | |
| col1, col2, col3, col4, col5 = st.columns([3, 1, 1, 2, 1]) | |
| with col1: | |
| labor_type = st.text_input( | |
| "نوع العامل", | |
| value=labor['النوع'], | |
| key=f"labor_type_{i}" | |
| ) | |
| with col2: | |
| labor_count = st.number_input( | |
| "العدد", | |
| value=float(labor['العدد']), | |
| min_value=1.0, | |
| step=1.0, | |
| key=f"labor_count_{i}" | |
| ) | |
| with col3: | |
| labor_days = st.number_input( | |
| "المدة (أيام)", | |
| value=labor['المدة'], | |
| min_value=0.1, | |
| format="%.1f", | |
| key=f"labor_days_{i}" | |
| ) | |
| with col4: | |
| labor_daily_rate = st.number_input( | |
| "سعر اليوم", | |
| value=labor['سعر_اليوم'], | |
| min_value=0.0, | |
| format="%.2f", | |
| key=f"labor_daily_rate_{i}" | |
| ) | |
| with col5: | |
| delete_button = st.button("حذف", key=f"delete_labor_{i}") | |
| labor_controls.append({ | |
| 'النوع': labor_type, | |
| 'العدد': labor_count, | |
| 'المدة': labor_days, | |
| 'سعر_اليوم': labor_daily_rate, | |
| 'delete': delete_button | |
| }) | |
| # إضافة عامل جديد | |
| if st.button("إضافة عامل جديد"): | |
| st.session_state.construction_item['العمالة'].append({ | |
| 'النوع': '', | |
| 'العدد': 1.0, | |
| 'المدة': 1.0, | |
| 'سعر_اليوم': 0.0 | |
| }) | |
| st.rerun() | |
| # تحديث أو حذف العمالة | |
| new_labor = [] | |
| for i, control in enumerate(labor_controls): | |
| if not control['delete']: | |
| new_labor.append({ | |
| 'النوع': control['النوع'], | |
| 'العدد': control['العدد'], | |
| 'المدة': control['المدة'], | |
| 'سعر_اليوم': control['سعر_اليوم'] | |
| }) | |
| if len(new_labor) != len(st.session_state.construction_item['العمالة']): | |
| st.session_state.construction_item['العمالة'] = new_labor | |
| st.rerun() | |
| else: | |
| for i, labor in enumerate(new_labor): | |
| st.session_state.construction_item['العمالة'][i] = labor | |
| # إدخال تفاصيل المعدات | |
| st.markdown("#### تفاصيل المعدات") | |
| equipment_controls = [] | |
| for i, equipment in enumerate(st.session_state.construction_item['المعدات']): | |
| col1, col2, col3, col4, col5 = st.columns([3, 1, 1, 2, 1]) | |
| with col1: | |
| equip_type = st.text_input( | |
| "نوع المعدة", | |
| value=equipment['النوع'], | |
| key=f"equip_type_{i}" | |
| ) | |
| with col2: | |
| equip_count = st.number_input( | |
| "العدد", | |
| value=float(equipment['العدد']), | |
| min_value=1.0, | |
| step=1.0, | |
| key=f"equip_count_{i}" | |
| ) | |
| with col3: | |
| equip_days = st.number_input( | |
| "المدة (أيام)", | |
| value=equipment['المدة'], | |
| min_value=0.1, | |
| format="%.1f", | |
| key=f"equip_days_{i}" | |
| ) | |
| with col4: | |
| equip_daily_rate = st.number_input( | |
| "سعر اليوم", | |
| value=equipment['سعر_اليوم'], | |
| min_value=0.0, | |
| format="%.2f", | |
| key=f"equip_daily_rate_{i}" | |
| ) | |
| with col5: | |
| delete_button = st.button("حذف", key=f"delete_equipment_{i}") | |
| equipment_controls.append({ | |
| 'النوع': equip_type, | |
| 'العدد': equip_count, | |
| 'المدة': equip_days, | |
| 'سعر_اليوم': equip_daily_rate, | |
| 'delete': delete_button | |
| }) | |
| # إضافة معدة جديدة | |
| if st.button("إضافة معدة جديدة"): | |
| st.session_state.construction_item['المعدات'].append({ | |
| 'النوع': '', | |
| 'العدد': 1.0, | |
| 'المدة': 1.0, | |
| 'سعر_اليوم': 0.0 | |
| }) | |
| st.rerun() | |
| # تحديث أو حذف المعدات | |
| new_equipment = [] | |
| for i, control in enumerate(equipment_controls): | |
| if not control['delete']: | |
| new_equipment.append({ | |
| 'النوع': control['النوع'], | |
| 'العدد': control['العدد'], | |
| 'المدة': control['المدة'], | |
| 'سعر_اليوم': control['سعر_اليوم'] | |
| }) | |
| if len(new_equipment) != len(st.session_state.construction_item['المعدات']): | |
| st.session_state.construction_item['المعدات'] = new_equipment | |
| st.rerun() | |
| else: | |
| for i, equipment in enumerate(new_equipment): | |
| st.session_state.construction_item['المعدات'][i] = equipment | |
| # النسب والعوامل | |
| st.markdown("#### المصاريف الإدارية وهامش الربح") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.session_state.construction_item['المصاريف_الإدارية'] = st.slider( | |
| "نسبة المصاريف الإدارية (%)", | |
| min_value=0.0, | |
| max_value=20.0, | |
| value=st.session_state.construction_item['المصاريف_الإدارية'] * 100, | |
| step=0.5 | |
| ) / 100 | |
| with col2: | |
| st.session_state.construction_item['هامش_الربح'] = st.slider( | |
| "نسبة هامش الربح (%)", | |
| min_value=0.0, | |
| max_value=30.0, | |
| value=st.session_state.construction_item['هامش_الربح'] * 100, | |
| step=0.5 | |
| ) / 100 | |
| # عوامل التعديل | |
| st.markdown("#### عوامل تعديل التكلفة") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.session_state.construction_item['عوامل_التعديل']['location_factor'] = st.slider( | |
| "معامل الموقع", | |
| min_value=0.5, | |
| max_value=2.0, | |
| value=st.session_state.construction_item['عوامل_التعديل']['location_factor'], | |
| step=0.05, | |
| help="يؤثر في التكلفة حسب صعوبة أو سهولة الوصول للموقع وتوفر الخدمات" | |
| ) | |
| st.session_state.construction_item['عوامل_التعديل']['time_factor'] = st.slider( | |
| "معامل الوقت", | |
| min_value=0.8, | |
| max_value=1.5, | |
| value=st.session_state.construction_item['عوامل_التعديل']['time_factor'], | |
| step=0.05, | |
| help="يؤثر في التكلفة حسب الجدول الزمني للمشروع وضرورة الإسراع في التنفيذ" | |
| ) | |
| with col2: | |
| st.session_state.construction_item['عوامل_التعديل']['risk_factor'] = st.slider( | |
| "معامل المخاطر", | |
| min_value=1.0, | |
| max_value=1.5, | |
| value=st.session_state.construction_item['عوامل_التعديل']['risk_factor'], | |
| step=0.05, | |
| help="يؤثر في التكلفة حسب المخاطر المتوقعة في المشروع" | |
| ) | |
| st.session_state.construction_item['عوامل_التعديل']['market_factor'] = st.slider( | |
| "معامل السوق", | |
| min_value=0.8, | |
| max_value=1.3, | |
| value=st.session_state.construction_item['عوامل_التعديل']['market_factor'], | |
| step=0.05, | |
| help="يؤثر في التكلفة حسب حالة السوق الحالية وتغيرات الأسعار" | |
| ) | |
| # صف أزرار العمليات | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # زر حساب التكلفة | |
| if st.button("حساب تكلفة البند", type="primary"): | |
| with st.spinner("جاري حساب التكلفة..."): | |
| # حساب التكلفة باستخدام الحاسبة | |
| item_cost = self.construction_calculator.calculate_item_cost(st.session_state.construction_item) | |
| st.session_state.item_cost_result = item_cost | |
| with col2: | |
| # زر استيراد بند من كتالوج أعمال المقاولات | |
| if st.button("استيراد بند من الكتالوج"): | |
| # تهيئة session state للاختيار من الكتالوج | |
| if 'show_catalog_selection' not in st.session_state: | |
| st.session_state.show_catalog_selection = True | |
| else: | |
| st.session_state.show_catalog_selection = True | |
| st.rerun() | |
| # عرض واجهة اختيار البند من الكتالوج | |
| if 'show_catalog_selection' in st.session_state and st.session_state.show_catalog_selection: | |
| st.markdown("#### اختيار بند من كتالوج أعمال المقاولات") | |
| # الحصول على فئات البنود | |
| categories = self.construction_templates.get_all_templates()['categories'] | |
| category_options = [f"{cat_data['name']}" for cat_id, cat_data in categories.items()] | |
| category_ids = list(categories.keys()) | |
| selected_category_index = st.selectbox( | |
| "اختر فئة البند", | |
| options=range(len(category_options)), | |
| format_func=lambda i: category_options[i], | |
| key="construction_category_selector" | |
| ) | |
| selected_category_id = category_ids[selected_category_index] | |
| # الحصول على البنود في الفئة المحددة | |
| templates = self.construction_templates.get_templates_by_category(selected_category_id) | |
| if templates: | |
| template_options = [f"{template['name']}" for template in templates] | |
| selected_template_index = st.selectbox( | |
| "اختر البند", | |
| options=range(len(template_options)), | |
| format_func=lambda i: template_options[i], | |
| key="construction_template_selector" | |
| ) | |
| selected_template = templates[selected_template_index] | |
| # عرض تفاصيل البند المحدد | |
| st.markdown(f"**وصف البند**: {selected_template['description']}") | |
| st.markdown(f"**الوحدة**: {selected_template['unit']}") | |
| if st.button("استخدام هذا البند", type="primary"): | |
| # تحويل البند إلى صيغة الحاسبة | |
| template_id = selected_template['id'] | |
| construction_item = self.construction_templates.convert_template_to_item(template_id) | |
| # تحديث بيانات البند في session state | |
| st.session_state.construction_item = construction_item | |
| st.session_state.show_catalog_selection = False | |
| st.rerun() | |
| else: | |
| st.info("لا توجد بنود في هذه الفئة") | |
| if st.button("إلغاء"): | |
| st.session_state.show_catalog_selection = False | |
| st.rerun() | |
| # عرض نتائج الحساب | |
| if 'item_cost_result' in st.session_state: | |
| st.markdown("### نتائج حساب تكلفة البند") | |
| result = st.session_state.item_cost_result | |
| # ملخص البند | |
| st.markdown(f"**البند:** {result['وصف_البند']}") | |
| st.markdown(f"**الكمية:** {result['الكمية']} {result['الوحدة']}") | |
| # المكونات الرئيسية للتكلفة | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric( | |
| "تكلفة المواد", | |
| f"{result['تكاليف_مباشرة']['المواد']['الإجمالي']:,.2f} ريال" | |
| ) | |
| with col2: | |
| st.metric( | |
| "تكلفة العمالة", | |
| f"{result['تكاليف_مباشرة']['العمالة']['الإجمالي']:,.2f} ريال" | |
| ) | |
| with col3: | |
| st.metric( | |
| "تكلفة المعدات", | |
| f"{result['تكاليف_مباشرة']['المعدات']['الإجمالي']:,.2f} ريال" | |
| ) | |
| with col4: | |
| st.metric( | |
| "التكلفة المباشرة", | |
| f"{result['تكاليف_مباشرة']['إجمالي_تكاليف_مباشرة']:,.2f} ريال" | |
| ) | |
| # تفاصيل المصاريف والربح والسعر النهائي | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric( | |
| f"المصاريف الإدارية ({result['مصاريف_إدارية']['نسبة']}%)", | |
| f"{result['مصاريف_إدارية']['قيمة']:,.2f} ريال" | |
| ) | |
| with col2: | |
| st.metric( | |
| f"هامش الربح ({result['هامش_ربح']['نسبة']}%)", | |
| f"{result['هامش_ربح']['قيمة']:,.2f} ريال" | |
| ) | |
| with col3: | |
| st.metric( | |
| "التكلفة الإجمالية", | |
| f"{result['التكلفة_الإجمالية']:,.2f} ريال" | |
| ) | |
| # التكلفة بعد تطبيق عوامل التعديل | |
| adjustment_factor = result['عوامل_التعديل']['المعامل_الإجمالي'] | |
| st.metric( | |
| f"السعر النهائي المعدل (معامل التعديل: {adjustment_factor:.2f})", | |
| f"{result['السعر_المعدل']['إجمالي']:,.2f} ريال", | |
| delta=f"{(adjustment_factor - 1) * 100:.1f}%" | |
| ) | |
| # سعر الوحدة وزر نقل السعر إلى جدول التسعير | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| st.metric( | |
| f"سعر الوحدة ({result['الوحدة']})", | |
| f"{result['السعر_المعدل']['سعر_الوحدة']:,.2f} ريال" | |
| ) | |
| with col2: | |
| if st.button("نقل السعر إلى جدول التسعير", key="transfer_to_pricing"): | |
| if 'manual_items' not in st.session_state: | |
| st.session_state.manual_items = pd.DataFrame(columns=[ | |
| 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي' | |
| ]) | |
| # إنشاء رقم البند الجديد | |
| new_id = f"B{len(st.session_state.manual_items)+1}" | |
| # إنشاء صف جديد | |
| new_row = pd.DataFrame({ | |
| 'رقم البند': [new_id], | |
| 'وصف البند': [result['وصف_البند']], | |
| 'الوحدة': [result['الوحدة']], | |
| 'الكمية': [float(result['الكمية'])], | |
| 'سعر الوحدة': [float(result['السعر_المعدل']['سعر_الوحدة'])], | |
| 'الإجمالي': [float(result['السعر_المعدل']['إجمالي'])] | |
| }) | |
| # إضافة الصف إلى DataFrame | |
| st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True) | |
| # إنشاء حالة التسعير الحالي إذا لم تكن موجودة | |
| if 'current_pricing' not in st.session_state: | |
| st.session_state.current_pricing = { | |
| 'name': 'تسعير جديد', | |
| 'method': 'تسعير مستورد من حاسبة تكاليف البناء', | |
| 'items': st.session_state.manual_items | |
| } | |
| else: | |
| # تحديث البنود في التسعير الحالي | |
| st.session_state.current_pricing['items'] = st.session_state.manual_items | |
| st.success(f"تم نقل البند \"{result['وصف_البند']}\" إلى جدول التسعير بنجاح!") | |
| # تفاصيل التكلفة - المواد | |
| st.markdown("#### تفاصيل تكلفة المواد") | |
| if len(result['تكاليف_مباشرة']['المواد']['التفاصيل']) > 0: | |
| materials_df = pd.DataFrame(result['تكاليف_مباشرة']['المواد']['التفاصيل']) | |
| st.dataframe(materials_df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("لا توجد مواد مضافة") | |
| # تفاصيل التكلفة - العمالة | |
| st.markdown("#### تفاصيل تكلفة العمالة") | |
| if len(result['تكاليف_مباشرة']['العمالة']['التفاصيل']) > 0: | |
| labor_df = pd.DataFrame(result['تكاليف_مباشرة']['العمالة']['التفاصيل']) | |
| st.dataframe(labor_df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("لا توجد عمالة مضافة") | |
| # تفاصيل التكلفة - المعدات | |
| st.markdown("#### تفاصيل تكلفة المعدات") | |
| if len(result['تكاليف_مباشرة']['المعدات']['التفاصيل']) > 0: | |
| equipment_df = pd.DataFrame(result['تكاليف_مباشرة']['المعدات']['التفاصيل']) | |
| st.dataframe(equipment_df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("لا توجد معدات مضافة") | |
| # رسم بياني لتوزيع التكلفة | |
| st.markdown("#### توزيع مكونات التكلفة") | |
| cost_components = { | |
| 'المواد': result['تكاليف_مباشرة']['المواد']['الإجمالي'], | |
| 'العمالة': result['تكاليف_مباشرة']['العمالة']['الإجمالي'], | |
| 'المعدات': result['تكاليف_مباشرة']['المعدات']['الإجمالي'], | |
| 'المصاريف الإدارية': result['مصاريف_إدارية']['قيمة'], | |
| 'هامش الربح': result['هامش_ربح']['قيمة'] | |
| } | |
| colors = ['#2E86C1', '#28B463', '#EB984E', '#8E44AD', '#C0392B'] | |
| fig = px.pie( | |
| values=list(cost_components.values()), | |
| names=list(cost_components.keys()), | |
| title='توزيع مكونات التكلفة', | |
| color_discrete_sequence=colors | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| with calc_tabs[1]: # حساب تكلفة مشروع | |
| st.markdown("#### حساب تكلفة مشروع كامل") | |
| # تهيئة بيانات المشروع الافتراضية إذا لم تكن موجودة | |
| if 'construction_project' not in st.session_state: | |
| st.session_state.construction_project = self.construction_calculator.generate_sample_project_data() | |
| # نموذج إدخال بيانات المشروع | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.session_state.construction_project['اسم_المشروع'] = st.text_input( | |
| "اسم المشروع", | |
| value=st.session_state.construction_project['اسم_المشروع'] | |
| ) | |
| with col2: | |
| st.session_state.construction_project['وصف_المشروع'] = st.text_area( | |
| "وصف المشروع", | |
| value=st.session_state.construction_project['وصف_المشروع'], | |
| height=100, | |
| key="construction_project_description" | |
| ) | |
| # النسب والعوامل الإجمالية للمشروع | |
| st.markdown("#### النسب والعوامل الإجمالية للمشروع") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.session_state.construction_project['المصاريف_الإدارية'] = st.slider( | |
| "نسبة المصاريف الإدارية الإجمالية (%)", | |
| min_value=0.0, | |
| max_value=20.0, | |
| value=st.session_state.construction_project['المصاريف_الإدارية'] * 100, | |
| step=0.5, | |
| key="project_admin_expenses" | |
| ) / 100 | |
| with col2: | |
| st.session_state.construction_project['هامش_الربح'] = st.slider( | |
| "نسبة هامش الربح الإجمالي (%)", | |
| min_value=0.0, | |
| max_value=30.0, | |
| value=st.session_state.construction_project['هامش_الربح'] * 100, | |
| step=0.5, | |
| key="project_profit_margin" | |
| ) / 100 | |
| # عوامل التعديل للمشروع | |
| st.markdown("#### عوامل تعديل التكلفة للمشروع") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.session_state.construction_project['عوامل_التعديل']['location_factor'] = st.slider( | |
| "معامل الموقع", | |
| min_value=0.5, | |
| max_value=2.0, | |
| value=st.session_state.construction_project['عوامل_التعديل']['location_factor'], | |
| step=0.05, | |
| help="يؤثر في التكلفة حسب صعوبة أو سهولة الوصول للموقع وتوفر الخدمات", | |
| key="project_location_factor" | |
| ) | |
| st.session_state.construction_project['عوامل_التعديل']['time_factor'] = st.slider( | |
| "معامل الوقت", | |
| min_value=0.8, | |
| max_value=1.5, | |
| value=st.session_state.construction_project['عوامل_التعديل']['time_factor'], | |
| step=0.05, | |
| help="يؤثر في التكلفة حسب الجدول الزمني للمشروع وضرورة الإسراع في التنفيذ", | |
| key="project_time_factor" | |
| ) | |
| with col2: | |
| st.session_state.construction_project['عوامل_التعديل']['risk_factor'] = st.slider( | |
| "معامل المخاطر", | |
| min_value=1.0, | |
| max_value=1.5, | |
| value=st.session_state.construction_project['عوامل_التعديل']['risk_factor'], | |
| step=0.05, | |
| help="يؤثر في التكلفة حسب المخاطر المتوقعة في المشروع", | |
| key="project_risk_factor" | |
| ) | |
| st.session_state.construction_project['عوامل_التعديل']['market_factor'] = st.slider( | |
| "معامل السوق", | |
| min_value=0.8, | |
| max_value=1.3, | |
| value=st.session_state.construction_project['عوامل_التعديل']['market_factor'], | |
| step=0.05, | |
| help="يؤثر في التكلفة حسب حالة السوق الحالية وتغيرات الأسعار", | |
| key="project_market_factor" | |
| ) | |
| # زر حساب تكلفة المشروع | |
| if st.button("حساب تكلفة المشروع", type="primary"): | |
| with st.spinner("جاري حساب تكلفة المشروع..."): | |
| # حساب التكلفة باستخدام الحاسبة | |
| project_cost = self.construction_calculator.calculate_project_cost(st.session_state.construction_project) | |
| st.session_state.project_cost_result = project_cost | |
| # عرض نتائج حساب المشروع | |
| if 'project_cost_result' in st.session_state: | |
| st.markdown("### نتائج حساب تكلفة المشروع") | |
| result = st.session_state.project_cost_result | |
| # ملخص المشروع | |
| st.markdown(f"**المشروع:** {result['اسم_المشروع']}") | |
| st.markdown(f"**الوصف:** {result['وصف_المشروع']}") | |
| st.markdown(f"**عدد البنود:** {result['عدد_البنود']} بند") | |
| # المكونات الرئيسية للتكلفة | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric( | |
| "إجمالي تكلفة المواد", | |
| f"{result['تكاليف_مباشرة']['المواد']['الإجمالي']:,.2f} ريال", | |
| delta=f"{result['تكاليف_مباشرة']['المواد']['النسبة_المئوية']:.1f}% من التكلفة المباشرة" | |
| ) | |
| with col2: | |
| st.metric( | |
| "إجمالي تكلفة العمالة", | |
| f"{result['تكاليف_مباشرة']['العمالة']['الإجمالي']:,.2f} ريال", | |
| delta=f"{result['تكاليف_مباشرة']['العمالة']['النسبة_المئوية']:.1f}% من التكلفة المباشرة" | |
| ) | |
| with col3: | |
| st.metric( | |
| "إجمالي تكلفة المعدات", | |
| f"{result['تكاليف_مباشرة']['المعدات']['الإجمالي']:,.2f} ريال", | |
| delta=f"{result['تكاليف_مباشرة']['المعدات']['النسبة_المئوية']:.1f}% من التكلفة المباشرة" | |
| ) | |
| # إجمالي التكاليف المباشرة | |
| st.metric( | |
| "إجمالي التكاليف المباشرة", | |
| f"{result['تكاليف_مباشرة']['إجمالي_تكاليف_مباشرة']:,.2f} ريال" | |
| ) | |
| # تفاصيل المصاريف والربح والسعر النهائي | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric( | |
| f"المصاريف الإدارية ({result['مصاريف_إدارية']['نسبة']}%)", | |
| f"{result['مصاريف_إدارية']['قيمة']:,.2f} ريال" | |
| ) | |
| with col2: | |
| st.metric( | |
| f"هامش الربح ({result['هامش_ربح']['نسبة']}%)", | |
| f"{result['هامش_ربح']['قيمة']:,.2f} ريال" | |
| ) | |
| with col3: | |
| st.metric( | |
| "التكلفة الإجمالية", | |
| f"{result['التكلفة_الإجمالية']:,.2f} ريال" | |
| ) | |
| # التكلفة بعد تطبيق عوامل التعديل | |
| adjustment_factor = result['عوامل_التعديل']['المعامل_الإجمالي'] | |
| st.metric( | |
| f"السعر النهائي المعدل (معامل التعديل: {adjustment_factor:.2f})", | |
| f"{result['التكلفة_النهائية_المعدلة']:,.2f} ريال", | |
| delta=f"{(adjustment_factor - 1) * 100:.1f}%" | |
| ) | |
| # رسم بياني لتوزيع التكلفة | |
| st.markdown("#### توزيع مكونات التكلفة") | |
| cost_components = { | |
| 'المواد': result['تكاليف_مباشرة']['المواد']['الإجمالي'], | |
| 'العمالة': result['تكاليف_مباشرة']['العمالة']['الإجمالي'], | |
| 'المعدات': result['تكاليف_مباشرة']['المعدات']['الإجمالي'], | |
| 'المصاريف الإدارية': result['مصاريف_إدارية']['قيمة'], | |
| 'هامش الربح': result['هامش_ربح']['قيمة'] | |
| } | |
| colors = ['#2E86C1', '#28B463', '#EB984E', '#8E44AD', '#C0392B'] | |
| fig = px.pie( | |
| values=list(cost_components.values()), | |
| names=list(cost_components.keys()), | |
| title='توزيع مكونات التكلفة', | |
| color_discrete_sequence=colors | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # جدول تفاصيل البنود | |
| st.markdown("#### تفاصيل بنود المشروع") | |
| items_data = [] | |
| for i, item in enumerate(result['تفاصيل_البنود']): | |
| items_data.append({ | |
| 'رقم': i + 1, | |
| 'الوصف': item['وصف_البند'], | |
| 'الكمية': item['الكمية'], | |
| 'الوحدة': item['الوحدة'], | |
| 'سعر الوحدة': item['سعر_الوحدة'], | |
| 'الإجمالي': item['التكلفة_الإجمالية'], | |
| 'بعد التعديل': item['السعر_المعدل']['إجمالي'] | |
| }) | |
| if len(items_data) > 0: | |
| items_df = pd.DataFrame(items_data) | |
| # تنسيق الجدول لعرض الأرقام بشكل أفضل | |
| def highlight_row(row): | |
| """تنسيق الجدول مع تمييز الصفوف بالتناوب""" | |
| color = '#F0F8FF' if row.name % 2 == 0 else 'white' | |
| return ['background-color: %s' % color] * len(row) | |
| styled_df = items_df.style.apply(highlight_row, axis=1) | |
| styled_df = styled_df.format({ | |
| 'الكمية': '{:,.2f}', | |
| 'سعر الوحدة': '{:,.2f}', | |
| 'الإجمالي': '{:,.2f}', | |
| 'بعد التعديل': '{:,.2f}' | |
| }) | |
| st.dataframe(styled_df, use_container_width=True) | |
| else: | |
| st.info("لا توجد بنود في المشروع") | |
| with calc_tabs[2]: # قوائم الأسعار المرجعية | |
| st.markdown("#### قوائم الأسعار المرجعية") | |
| ref_tabs = st.tabs(["قائمة المواد", "قائمة العمالة", "قائمة المعدات"]) | |
| with ref_tabs[0]: # قائمة المواد | |
| st.markdown("#### قائمة أسعار المواد المرجعية") | |
| # الحصول على قائمة المواد | |
| materials = self.construction_calculator.get_all_rates(item_type='مادة') | |
| if materials and 'المواد' in materials: | |
| # تحويل قاموس المواد إلى DataFrame | |
| materials_list = [] | |
| for name, data in materials['المواد'].items(): | |
| materials_list.append({ | |
| 'اسم المادة': name, | |
| 'الوحدة': data.get('وحدة', ''), | |
| 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), | |
| 'الوصف': data.get('وصف', ''), | |
| 'الفئة': data.get('فئة', '') | |
| }) | |
| if materials_list: | |
| materials_df = pd.DataFrame(materials_list) | |
| # تصفية حسب الفئة | |
| categories = ['الكل'] + sorted(materials_df['الفئة'].unique().tolist()) | |
| selected_category = st.selectbox("تصفية حسب الفئة", categories, key="materials_cat_filter_section1") | |
| if selected_category != 'الكل': | |
| filtered_df = materials_df[materials_df['الفئة'] == selected_category] | |
| else: | |
| filtered_df = materials_df | |
| # تنسيق الجدول | |
| def highlight_materials_row(row): | |
| """تنسيق الجدول مع تمييز الصفوف بالتناوب""" | |
| color = '#F0F8FF' if row.name % 2 == 0 else 'white' | |
| return ['background-color: %s' % color] * len(row) | |
| styled_df = filtered_df.style.apply(highlight_materials_row, axis=1) | |
| styled_df = styled_df.format({ | |
| 'سعر الوحدة': '{:,.2f}' | |
| }) | |
| st.dataframe(styled_df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("لا توجد مواد في قاعدة البيانات") | |
| else: | |
| st.info("لا توجد قائمة مواد متاحة") | |
| with ref_tabs[1]: # قائمة العمالة | |
| st.markdown("#### قائمة أسعار العمالة المرجعية") | |
| # الحصول على قائمة العمالة | |
| labor = self.construction_calculator.get_all_rates(item_type='عمالة') | |
| if labor and 'العمالة' in labor: | |
| # تحويل قاموس العمالة إلى DataFrame | |
| labor_list = [] | |
| for name, data in labor['العمالة'].items(): | |
| labor_list.append({ | |
| 'نوع العامل': name, | |
| 'وحدة الأجر': data.get('وحدة', ''), | |
| 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), | |
| 'الوصف': data.get('وصف', ''), | |
| 'الفئة': data.get('فئة', '') | |
| }) | |
| if labor_list: | |
| labor_df = pd.DataFrame(labor_list) | |
| # تصفية حسب الفئة | |
| categories = ['الكل'] + sorted(labor_df['الفئة'].unique().tolist()) | |
| selected_category = st.selectbox("تصفية حسب الفئة", categories, key="labor_cat_filter_section1") | |
| if selected_category != 'الكل': | |
| filtered_df = labor_df[labor_df['الفئة'] == selected_category] | |
| else: | |
| filtered_df = labor_df | |
| # تنسيق الجدول | |
| def highlight_labor_row(row): | |
| """تنسيق الجدول مع تمييز الصفوف بالتناوب""" | |
| color = '#F0F8FF' if row.name % 2 == 0 else 'white' | |
| return ['background-color: %s' % color] * len(row) | |
| styled_df = filtered_df.style.apply(highlight_labor_row, axis=1) | |
| styled_df = styled_df.format({ | |
| 'سعر الوحدة': '{:,.2f}' | |
| }) | |
| st.dataframe(styled_df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("لا توجد عمالة في قاعدة البيانات") | |
| else: | |
| st.info("لا توجد قائمة عمالة متاحة") | |
| with ref_tabs[2]: # قائمة المعدات | |
| st.markdown("#### قائمة أسعار المعدات المرجعية") | |
| # الحصول على قائمة المعدات | |
| equipment = self.construction_calculator.get_all_rates(item_type='معدة') | |
| if equipment and 'المعدات' in equipment: | |
| # تحويل قاموس المعدات إلى DataFrame | |
| equipment_list = [] | |
| for name, data in equipment['المعدات'].items(): | |
| equipment_list.append({ | |
| 'نوع المعدة': name, | |
| 'وحدة الإيجار': data.get('وحدة', ''), | |
| 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), | |
| 'الوصف': data.get('وصف', ''), | |
| 'الفئة': data.get('فئة', '') | |
| }) | |
| if equipment_list: | |
| equipment_df = pd.DataFrame(equipment_list) | |
| # تصفية حسب الفئة | |
| categories = ['الكل'] + sorted(equipment_df['الفئة'].unique().tolist()) | |
| selected_category = st.selectbox("تصفية حسب الفئة", categories, key="equipment_cat_filter_section1") | |
| if selected_category != 'الكل': | |
| filtered_df = equipment_df[equipment_df['الفئة'] == selected_category] | |
| else: | |
| filtered_df = equipment_df | |
| # تنسيق الجدول | |
| def highlight_equipment_row(row): | |
| """تنسيق الجدول مع تمييز الصفوف بالتناوب""" | |
| color = '#F0F8FF' if row.name % 2 == 0 else 'white' | |
| return ['background-color: %s' % color] * len(row) | |
| styled_df = filtered_df.style.apply(highlight_equipment_row, axis=1) | |
| styled_df = styled_df.format({ | |
| 'سعر الوحدة': '{:,.2f}' | |
| }) | |
| st.dataframe(styled_df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("لا توجد معدات في قاعدة البيانات") | |
| else: | |
| st.info("لا توجد قائمة معدات متاحة") | |
| with calc_tabs[3]: # كتالوج أعمال المقاولات | |
| st.markdown("#### كتالوج أعمال المقاولات") | |
| # شرح كتالوج أعمال المقاولات | |
| with st.expander("معلومات عن كتالوج أعمال المقاولات", expanded=False): | |
| st.markdown(""" | |
| **كتالوج أعمال المقاولات** هو قاعدة بيانات شاملة للبنود النموذجية المستخدمة في مشاريع المقاولات، ويوفر: | |
| - بنود جاهزة لمختلف أنواع الأعمال الإنشائية (خرسانة، مناهل، مواسير، طرق، إلخ). | |
| - تفاصيل دقيقة للمواد والعمالة والمعدات المطلوبة لكل بند. | |
| - تحليل تكلفة تفصيلي يمكن استخدامه مباشرة في عروض الأسعار والمناقصات. | |
| - ربط مباشر مع حاسبة تكاليف البناء وحاسبة التسعير. | |
| **استخدامات الكتالوج:** | |
| 1. استخدام البنود النموذجية مباشرة في التسعير. | |
| 2. تعديل البنود النموذجية لتناسب متطلبات المشروع. | |
| 3. إضافة بنود جديدة إلى الكتالوج للاستخدام المستقبلي. | |
| """) | |
| # الفئات وعرض محتوى الكتالوج | |
| category_col, template_col = st.columns([1, 2]) | |
| with category_col: | |
| st.markdown("### فئات البنود") | |
| # الحصول على فئات البنود | |
| templates = self.construction_templates.get_all_templates() | |
| categories = templates['categories'] | |
| for cat_id, cat_data in categories.items(): | |
| st.markdown(f"#### {cat_data['name']}") | |
| st.markdown(f"{cat_data['description']}") | |
| if st.button(f"عرض بنود {cat_data['name']}", key=f"cat_btn_{cat_id}"): | |
| st.session_state.selected_category = cat_id | |
| st.rerun() | |
| with template_col: | |
| st.markdown("### البنود النموذجية") | |
| selected_category = st.session_state.get("selected_category", list(categories.keys())[0]) | |
| # عرض البنود في الفئة المحددة | |
| cat_templates = self.construction_templates.get_templates_by_category(selected_category) | |
| if cat_templates: | |
| st.markdown(f"#### بنود فئة {categories[selected_category]['name']}") | |
| for template in cat_templates: | |
| with st.expander(template['name'], expanded=False): | |
| st.markdown(f"**الوصف**: {template['description']}") | |
| st.markdown(f"**الوحدة**: {template['unit']}") | |
| # عرض مكونات البند | |
| st.markdown("##### مكونات البند") | |
| # المواد | |
| materials = template['components']['materials'] | |
| if materials: | |
| materials_df = pd.DataFrame(materials) | |
| st.markdown("**المواد:**") | |
| st.dataframe(materials_df, hide_index=True) | |
| # العمالة | |
| labor = template['components']['labor'] | |
| if labor: | |
| labor_df = pd.DataFrame(labor) | |
| st.markdown("**العمالة:**") | |
| st.dataframe(labor_df, hide_index=True) | |
| # المعدات | |
| equipment = template['components']['equipment'] | |
| if equipment: | |
| equipment_df = pd.DataFrame(equipment) | |
| st.markdown("**المعدات:**") | |
| st.dataframe(equipment_df, hide_index=True) | |
| # أزرار العمليات | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("استخدام هذا البند في حاسبة التكاليف", key=f"use_template_{template['id']}"): | |
| # تحويل البند إلى صيغة الحاسبة | |
| construction_item = self.construction_templates.convert_template_to_item(template['id']) | |
| # تحديث بيانات البند في session state | |
| st.session_state.construction_item = construction_item | |
| st.session_state.active_tab = 0 # الانتقال إلى تبويب حساب تكلفة البند | |
| st.rerun() | |
| with col2: | |
| if st.button("إضافة مباشرة إلى جدول التسعير", key=f"add_to_pricing_{template['id']}"): | |
| # تحويل البند إلى صيغة الحاسبة | |
| construction_item = self.construction_templates.convert_template_to_item(template['id']) | |
| # حساب التكلفة | |
| item_cost = self.construction_calculator.calculate_item_cost(construction_item) | |
| # إضافة البند إلى جدول التسعير | |
| if 'manual_items' not in st.session_state: | |
| st.session_state.manual_items = pd.DataFrame(columns=[ | |
| 'رقم البند', 'وصف البند', 'الوحدة', 'الكمية', 'سعر الوحدة', 'الإجمالي' | |
| ]) | |
| # إنشاء رقم البند الجديد | |
| new_id = f"C{len(st.session_state.manual_items)+1}" | |
| # إنشاء صف جديد | |
| new_row = pd.DataFrame({ | |
| 'رقم البند': [new_id], | |
| 'وصف البند': [item_cost['وصف_البند']], | |
| 'الوحدة': [item_cost['الوحدة']], | |
| 'الكمية': [float(item_cost['الكمية'])], | |
| 'سعر الوحدة': [float(item_cost['السعر_المعدل']['سعر_الوحدة'])], | |
| 'الإجمالي': [float(item_cost['السعر_المعدل']['إجمالي'])] | |
| }) | |
| # إضافة الصف إلى DataFrame | |
| st.session_state.manual_items = pd.concat([st.session_state.manual_items, new_row], ignore_index=True) | |
| # إنشاء حالة التسعير الحالي إذا لم تكن موجودة | |
| if 'current_pricing' not in st.session_state: | |
| st.session_state.current_pricing = { | |
| 'name': 'تسعير جديد', | |
| 'method': 'تسعير مستورد من كتالوج المقاولات', | |
| 'items': st.session_state.manual_items | |
| } | |
| else: | |
| # تحديث البنود في التسعير الحالي | |
| st.session_state.current_pricing['items'] = st.session_state.manual_items | |
| st.success(f"تم إضافة البند \"{item_cost['وصف_البند']}\" إلى جدول التسعير بنجاح!") | |
| # إضافة قسم لإضافة بند جديد إلى الكتالوج | |
| st.markdown("### إضافة بند جديد إلى الكتالوج") | |
| if st.button("إضافة البند الحالي إلى الكتالوج"): | |
| if 'item_cost_result' in st.session_state: | |
| # عرض نموذج لإضافة البند إلى الكتالوج | |
| st.session_state.show_add_to_catalog = True | |
| st.rerun() | |
| else: | |
| st.warning("يجب حساب تكلفة البند أولاً في تبويب 'حساب تكلفة بند' قبل إضافته إلى الكتالوج.") | |
| # عرض نموذج إضافة البند إلى الكتالوج | |
| if 'show_add_to_catalog' in st.session_state and st.session_state.show_add_to_catalog: | |
| st.markdown("#### إضافة البند الحالي إلى كتالوج المقاولات") | |
| # اختيار الفئة | |
| category_options = [f"{cat_data['name']}" for cat_id, cat_data in categories.items()] | |
| category_ids = list(categories.keys()) | |
| selected_category_index = st.selectbox( | |
| "اختر فئة البند", | |
| options=range(len(category_options)), | |
| format_func=lambda i: category_options[i], | |
| key="new_template_category" | |
| ) | |
| selected_category_id = category_ids[selected_category_index] | |
| # معلومات البند | |
| item_result = st.session_state.item_cost_result | |
| template_name = st.text_input("اسم البند في الكتالوج", value=item_result['وصف_البند'][:50]) | |
| template_description = st.text_area("وصف البند", value=item_result['وصف_البند'], key="template_item_description") | |
| # الكلمات المفتاحية | |
| tags_input = st.text_input("الكلمات المفتاحية (مفصولة بفواصل)", value="بناء, مقاولات") | |
| tags = [tag.strip() for tag in tags_input.split(",")] | |
| if st.button("إضافة إلى الكتالوج", type="primary"): | |
| # تحويل البند إلى صيغة قالب | |
| template_data = { | |
| "category": selected_category_id, | |
| "name": template_name, | |
| "description": template_description, | |
| "unit": item_result['الوحدة'], | |
| "components": { | |
| "materials": item_result['تكاليف_مباشرة']['المواد']['التفاصيل'], | |
| "labor": item_result['تكاليف_مباشرة']['العمالة']['التفاصيل'], | |
| "equipment": item_result['تكاليف_مباشرة']['المعدات']['التفاصيل'] | |
| }, | |
| "admin_expenses": item_result['مصاريف_إدارية']['نسبة'] / 100, | |
| "profit_margin": item_result['هامش_ربح']['نسبة'] / 100, | |
| "tags": tags | |
| } | |
| # إضافة القالب إلى الكتالوج | |
| template_id = self.construction_templates.add_template(template_data) | |
| st.success(f"تم إضافة البند \"{template_name}\" إلى كتالوج المقاولات بنجاح!") | |
| st.session_state.show_add_to_catalog = False | |
| st.rerun() | |
| if st.button("إلغاء"): | |
| st.session_state.show_add_to_catalog = False | |
| st.rerun() | |
| with ref_tabs[0]: # قائمة المواد | |
| st.markdown("#### قائمة أسعار المواد المرجعية") | |
| # الحصول على قائمة المواد | |
| materials = self.construction_calculator.get_all_rates(item_type='مادة') | |
| if materials and 'المواد' in materials: | |
| # تحويل قاموس المواد إلى DataFrame | |
| materials_list = [] | |
| for name, data in materials['المواد'].items(): | |
| materials_list.append({ | |
| 'اسم المادة': name, | |
| 'الوحدة': data.get('وحدة', ''), | |
| 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), | |
| 'الوصف': data.get('وصف', ''), | |
| 'الفئة': data.get('فئة', '') | |
| }) | |
| if materials_list: | |
| materials_df = pd.DataFrame(materials_list) | |
| # تصفية حسب الفئة | |
| categories = ['الكل'] + sorted(materials_df['الفئة'].unique().tolist()) | |
| selected_category = st.selectbox("تصفية حسب الفئة", categories, key="materials_cat_filter_section2") | |
| if selected_category != 'الكل': | |
| filtered_df = materials_df[materials_df['الفئة'] == selected_category] | |
| else: | |
| filtered_df = materials_df | |
| # تنسيق الجدول | |
| def highlight_materials_row(row): | |
| """تنسيق الجدول مع تمييز الصفوف بالتناوب""" | |
| color = '#F0F8FF' if row.name % 2 == 0 else 'white' | |
| return ['background-color: %s' % color] * len(row) | |
| styled_df = filtered_df.style.apply(highlight_materials_row, axis=1) | |
| styled_df = styled_df.format({ | |
| 'سعر الوحدة': '{:,.2f}' | |
| }) | |
| st.dataframe(styled_df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("لا توجد مواد في قاعدة البيانات") | |
| else: | |
| st.info("لا توجد قائمة مواد متاحة") | |
| with ref_tabs[1]: # قائمة العمالة | |
| st.markdown("#### قائمة أسعار العمالة المرجعية") | |
| # الحصول على قائمة العمالة | |
| labor = self.construction_calculator.get_all_rates(item_type='عمالة') | |
| if labor and 'العمالة' in labor: | |
| # تحويل قاموس العمالة إلى DataFrame | |
| labor_list = [] | |
| for name, data in labor['العمالة'].items(): | |
| labor_list.append({ | |
| 'نوع العامل': name, | |
| 'وحدة الأجر': data.get('وحدة', ''), | |
| 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), | |
| 'الوصف': data.get('وصف', ''), | |
| 'الفئة': data.get('فئة', '') | |
| }) | |
| if labor_list: | |
| labor_df = pd.DataFrame(labor_list) | |
| # تصفية حسب الفئة | |
| categories = ['الكل'] + sorted(labor_df['الفئة'].unique().tolist()) | |
| selected_category = st.selectbox("تصفية حسب الفئة", categories, key="labor_cat_filter_section2") | |
| if selected_category != 'الكل': | |
| filtered_df = labor_df[labor_df['الفئة'] == selected_category] | |
| else: | |
| filtered_df = labor_df | |
| # تنسيق الجدول | |
| def highlight_labor_row(row): | |
| """تنسيق الجدول مع تمييز الصفوف بالتناوب""" | |
| color = '#F0F8FF' if row.name % 2 == 0 else 'white' | |
| return ['background-color: %s' % color] * len(row) | |
| styled_df = filtered_df.style.apply(highlight_labor_row, axis=1) | |
| styled_df = styled_df.format({ | |
| 'سعر الوحدة': '{:,.2f}' | |
| }) | |
| st.dataframe(styled_df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("لا توجد عمالة في قاعدة البيانات") | |
| else: | |
| st.info("لا توجد قائمة عمالة متاحة") | |
| with ref_tabs[2]: # قائمة المعدات | |
| st.markdown("#### قائمة أسعار المعدات المرجعية") | |
| # الحصول على قائمة المعدات | |
| equipment = self.construction_calculator.get_all_rates(item_type='معدة') | |
| if equipment and 'المعدات' in equipment: | |
| # تحويل قاموس المعدات إلى DataFrame | |
| equipment_list = [] | |
| for name, data in equipment['المعدات'].items(): | |
| equipment_list.append({ | |
| 'نوع المعدة': name, | |
| 'وحدة الأجر': data.get('وحدة', ''), | |
| 'سعر الوحدة': data.get('سعر_الوحدة', 0.0), | |
| 'الوصف': data.get('وصف', ''), | |
| 'الفئة': data.get('فئة', '') | |
| }) | |
| if equipment_list: | |
| equipment_df = pd.DataFrame(equipment_list) | |
| # تصفية حسب الفئة | |
| categories = ['الكل'] + sorted(equipment_df['الفئة'].unique().tolist()) | |
| selected_category = st.selectbox("تصفية حسب الفئة", categories, key="equipment_cat_filter_section2") | |
| if selected_category != 'الكل': | |
| filtered_df = equipment_df[equipment_df['الفئة'] == selected_category] | |
| else: | |
| filtered_df = equipment_df | |
| # تنسيق الجدول | |
| def highlight_equipment_row(row): | |
| """تنسيق الجدول مع تمييز الصفوف بالتناوب""" | |
| color = '#F0F8FF' if row.name % 2 == 0 else 'white' | |
| return ['background-color: %s' % color] * len(row) | |
| styled_df = filtered_df.style.apply(highlight_equipment_row, axis=1) | |
| styled_df = styled_df.format({ | |
| 'سعر الوحدة': '{:,.2f}' | |
| }) | |
| st.dataframe(styled_df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("لا توجد معدات في قاعدة البيانات") | |
| else: | |
| st.info("لا توجد قائمة معدات متاحة") | |
| def _render_local_content_tab(self): | |
| """عرض تبويب المحتوى المحلي""" | |
| st.markdown("<h2 style='text-align: center; background: linear-gradient(to right, #1F7A8C, #2A9D8F); color: white; padding: 15px; border-radius: 8px;'>تحليل المحتوى المحلي</h2>", unsafe_allow_html=True) | |
| # التحقق من وجود تسعير حالي | |
| 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. **المنتجات**: المنتجات والمواد المصنعة محلياً. | |
| 2. **الخدمات**: الخدمات المقدمة من شركات محلية. | |
| 3. **القوى العاملة**: العمالة والكوادر الفنية والإدارية المحلية. | |
| ### أهمية المحتوى المحلي: | |
| - تعزيز الاقتصاد المحلي وخلق فرص عمل. | |
| - تحقيق أهداف رؤية 2030 في زيادة المحتوى المحلي. | |
| - التأهل للمشاريع الحكومية التي تتطلب نسبة محتوى محلي محددة. | |
| - الحصول على حوافز وأفضلية في المناقصات الحكومية. | |
| ### متطلبات المحتوى المحلي: | |
| - نسبة المحتوى المحلي للقوى العاملة: 80% | |
| - نسبة المحتوى المحلي للمنتجات: 70% | |
| - نسبة المحتوى المحلي للخدمات: 60% | |
| """) | |
| # عرض لوحة إدخال بيانات المحتوى المحلي | |
| st.markdown("### بيانات المحتوى المحلي") | |
| # التبويبات لأنواع المحتوى المحلي | |
| lc_tabs = st.tabs(["المنتجات", "الخدمات", "القوى العاملة", "التحليل"]) | |
| with lc_tabs[0]: # المنتجات | |
| st.markdown("#### بيانات المنتجات") | |
| # إنشاء بيانات افتراضية للمنتجات إذا لم تكن موجودة | |
| if 'local_content_products' not in st.session_state: | |
| st.session_state.local_content_products = pd.DataFrame({ | |
| 'المنتج': [ | |
| "خرسانة مسلحة", | |
| "حديد تسليح", | |
| "بلوك خرساني", | |
| "عزل مائي", | |
| "دهانات" | |
| ], | |
| 'الكمية': [250, 25, 400, 500, 600], | |
| 'سعر_الوحدة': [1200, 6000, 200, 100, 50], | |
| 'التكلفة_الإجمالية': [300000, 150000, 80000, 50000, 30000], | |
| 'نسبة_المحتوى_المحلي': [0.95, 0.70, 0.98, 0.60, 0.80] | |
| }) | |
| # حساب التكلفة الإجمالية | |
| st.session_state.local_content_products['التكلفة_الإجمالية'] = st.session_state.local_content_products['الكمية'] * st.session_state.local_content_products['سعر_الوحدة'] | |
| # نموذج إضافة منتج جديد | |
| st.markdown("#### إضافة منتج جديد") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| new_product_name = st.text_input("اسم المنتج", key="new_product_name", value="") | |
| with col2: | |
| new_product_quantity = st.number_input("الكمية", key="new_product_quantity", min_value=0, value=0) | |
| with col3: | |
| new_product_price = st.number_input("سعر الوحدة", key="new_product_price", min_value=0, value=0) | |
| with col4: | |
| new_product_local_content = st.slider("نسبة المحتوى المحلي", key="new_product_local_content", min_value=0.0, max_value=1.0, value=0.8, step=0.01, format="%.2f") | |
| if st.button("إضافة المنتج"): | |
| if new_product_name: | |
| # حساب التكلفة الإجمالية | |
| total_cost = new_product_quantity * new_product_price | |
| # إضافة منتج جديد للجدول | |
| new_product = pd.DataFrame({ | |
| 'المنتج': [new_product_name], | |
| 'الكمية': [new_product_quantity], | |
| 'سعر_الوحدة': [new_product_price], | |
| 'التكلفة_الإجمالية': [total_cost], | |
| 'نسبة_المحتوى_المحلي': [new_product_local_content] | |
| }) | |
| # إضافة المنتج الجديد للجدول الحالي | |
| st.session_state.local_content_products = pd.concat([st.session_state.local_content_products, new_product], ignore_index=True) | |
| st.success(f"تم إضافة المنتج {new_product_name} بنجاح!") | |
| else: | |
| st.warning("يرجى إدخال اسم المنتج") | |
| # عرض جدول البنود مع إمكانية التعديل | |
| st.markdown("#### جدول المنتجات") | |
| edited_products = st.data_editor( | |
| st.session_state.local_content_products, | |
| use_container_width=True, | |
| hide_index=True, | |
| key="products_editor" | |
| ) | |
| st.session_state.local_content_products = edited_products | |
| # زر لحذف المنتجات المحددة | |
| if st.button("حذف المنتجات المحددة"): | |
| st.session_state.local_content_products = pd.DataFrame({ | |
| 'المنتج': [], | |
| 'الكمية': [], | |
| 'سعر_الوحدة': [], | |
| 'التكلفة_الإجمالية': [], | |
| 'نسبة_المحتوى_المحلي': [] | |
| }) | |
| st.success("تم حذف جميع المنتجات!") | |
| st.rerun() | |
| # عرض ملخص المنتجات | |
| total_products_cost = edited_products['التكلفة_الإجمالية'].sum() | |
| avg_local_content = (edited_products['التكلفة_الإجمالية'] * edited_products['نسبة_المحتوى_المحلي']).sum() / total_products_cost if total_products_cost > 0 else 0 | |
| st.markdown(f""" | |
| **إجمالي تكلفة المنتجات**: {total_products_cost:,.2f} ريال | |
| **متوسط نسبة المحتوى المحلي للمنتجات**: {avg_local_content*100:.2f}% | |
| **المستهدف**: 70% | |
| **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.7 else "❌ غير ملتزم"} | |
| """) | |
| with lc_tabs[1]: # الخدمات | |
| st.markdown("#### بيانات الخدمات") | |
| # إنشاء بيانات افتراضية للخدمات إذا لم تكن موجودة | |
| if 'local_content_services' not in st.session_state: | |
| st.session_state.local_content_services = pd.DataFrame({ | |
| 'الخدمة': [ | |
| "تصميم معماري", | |
| "إشراف هندسي", | |
| "خدمات نقل", | |
| "خدمات أمن وسلامة", | |
| "صيانة ونظافة" | |
| ], | |
| 'التكلفة': [100000, 120000, 50000, 30000, 20000], | |
| 'نسبة_المحتوى_المحلي': [0.90, 0.85, 0.90, 0.95, 0.95] | |
| }) | |
| # نموذج إضافة خدمة جديدة | |
| st.markdown("#### إضافة خدمة جديدة") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| new_service_name = st.text_input("اسم الخدمة", key="new_service_name", value="") | |
| with col2: | |
| new_service_cost = st.number_input("التكلفة", key="new_service_cost", min_value=0, value=0) | |
| with col3: | |
| new_service_local_content = st.slider("نسبة المحتوى المحلي", key="new_service_local_content", min_value=0.0, max_value=1.0, value=0.8, step=0.01, format="%.2f") | |
| if st.button("إضافة الخدمة"): | |
| if new_service_name: | |
| # إضافة خدمة جديدة للجدول | |
| new_service = pd.DataFrame({ | |
| 'الخدمة': [new_service_name], | |
| 'التكلفة': [new_service_cost], | |
| 'نسبة_المحتوى_المحلي': [new_service_local_content] | |
| }) | |
| # إضافة الخدمة الجديدة للجدول الحالي | |
| st.session_state.local_content_services = pd.concat([st.session_state.local_content_services, new_service], ignore_index=True) | |
| st.success(f"تم إضافة الخدمة {new_service_name} بنجاح!") | |
| else: | |
| st.warning("يرجى إدخال اسم الخدمة") | |
| # عرض جدول الخدمات مع إمكانية التعديل | |
| st.markdown("#### جدول الخدمات") | |
| edited_services = st.data_editor( | |
| st.session_state.local_content_services, | |
| use_container_width=True, | |
| hide_index=True, | |
| key="services_editor" | |
| ) | |
| st.session_state.local_content_services = edited_services | |
| # زر لحذف الخدمات المحددة | |
| if st.button("حذف الخدمات المحددة"): | |
| st.session_state.local_content_services = pd.DataFrame({ | |
| 'الخدمة': [], | |
| 'التكلفة': [], | |
| 'نسبة_المحتوى_المحلي': [] | |
| }) | |
| st.success("تم حذف جميع الخدمات!") | |
| st.rerun() | |
| # عرض ملخص الخدمات | |
| total_services_cost = edited_services['التكلفة'].sum() | |
| avg_local_content = (edited_services['التكلفة'] * edited_services['نسبة_المحتوى_المحلي']).sum() / total_services_cost if total_services_cost > 0 else 0 | |
| st.markdown(f""" | |
| **إجمالي تكلفة الخدمات**: {total_services_cost:,.2f} ريال | |
| **متوسط نسبة المحتوى المحلي للخدمات**: {avg_local_content*100:.2f}% | |
| **المستهدف**: 60% | |
| **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.6 else "❌ غير ملتزم"} | |
| """) | |
| with lc_tabs[2]: # القوى العاملة | |
| st.markdown("#### بيانات القوى العاملة") | |
| # إنشاء بيانات افتراضية للقوى العاملة إذا لم تكن موجودة | |
| if 'local_content_labor' not in st.session_state: | |
| st.session_state.local_content_labor = pd.DataFrame({ | |
| 'فئة_العمالة': [ | |
| "مهندسون", | |
| "فنيون", | |
| "عمال بناء", | |
| "إداريون", | |
| "مشرفون" | |
| ], | |
| 'العدد': [5, 10, 30, 3, 4], | |
| 'الراتب_الشهري': [15000, 8000, 3000, 10000, 12000], | |
| 'المدة_بالأشهر': [12, 12, 12, 12, 12], | |
| 'نسبة_المحتوى_المحلي': [0.75, 0.65, 0.60, 0.90, 0.80] | |
| }) | |
| # حساب التكلفة الإجمالية | |
| st.session_state.local_content_labor['التكلفة_الإجمالية'] = st.session_state.local_content_labor['العدد'] * st.session_state.local_content_labor['الراتب_الشهري'] * st.session_state.local_content_labor['المدة_بالأشهر'] | |
| # نموذج إضافة فئة عمالة جديدة | |
| st.markdown("#### إضافة فئة عمالة جديدة") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| new_labor_category = st.text_input("فئة العمالة", key="new_labor_category", value="") | |
| new_labor_count = st.number_input("العدد", key="new_labor_count", min_value=0, value=0) | |
| with col2: | |
| new_labor_salary = st.number_input("الراتب الشهري", key="new_labor_salary", min_value=0, value=0) | |
| new_labor_months = st.number_input("المدة بالأشهر", key="new_labor_months", min_value=1, value=12) | |
| new_labor_local_content = st.slider("نسبة المحتوى المحلي", key="new_labor_local_content", min_value=0.0, max_value=1.0, value=0.8, step=0.01, format="%.2f") | |
| if st.button("إضافة فئة العمالة"): | |
| if new_labor_category: | |
| # حساب التكلفة الإجمالية | |
| total_cost = new_labor_count * new_labor_salary * new_labor_months | |
| # إضافة فئة عمالة جديدة للجدول | |
| new_labor = pd.DataFrame({ | |
| 'فئة_العمالة': [new_labor_category], | |
| 'العدد': [new_labor_count], | |
| 'الراتب_الشهري': [new_labor_salary], | |
| 'المدة_بالأشهر': [new_labor_months], | |
| 'نسبة_المحتوى_المحلي': [new_labor_local_content], | |
| 'التكلفة_الإجمالية': [total_cost] | |
| }) | |
| # إضافة فئة العمالة الجديدة للجدول الحالي | |
| st.session_state.local_content_labor = pd.concat([st.session_state.local_content_labor, new_labor], ignore_index=True) | |
| st.success(f"تم إضافة فئة العمالة {new_labor_category} بنجاح!") | |
| else: | |
| st.warning("يرجى إدخال اسم فئة العمالة") | |
| # عرض جدول القوى العاملة مع إمكانية التعديل | |
| st.markdown("#### جدول القوى العاملة") | |
| edited_labor = st.data_editor( | |
| st.session_state.local_content_labor, | |
| use_container_width=True, | |
| hide_index=True, | |
| key="labor_editor" | |
| ) | |
| # إعادة حساب التكلفة الإجمالية بعد التعديل | |
| edited_labor['التكلفة_الإجمالية'] = edited_labor['العدد'] * edited_labor['الراتب_الشهري'] * edited_labor['المدة_بالأشهر'] | |
| st.session_state.local_content_labor = edited_labor | |
| # زر لحذف فئات العمالة المحددة | |
| if st.button("حذف فئات العمالة المحددة"): | |
| st.session_state.local_content_labor = pd.DataFrame({ | |
| 'فئة_العمالة': [], | |
| 'العدد': [], | |
| 'الراتب_الشهري': [], | |
| 'المدة_بالأشهر': [], | |
| 'نسبة_المحتوى_المحلي': [], | |
| 'التكلفة_الإجمالية': [] | |
| }) | |
| st.success("تم حذف جميع فئات العمالة!") | |
| st.rerun() | |
| # عرض ملخص القوى العاملة | |
| total_labor_cost = edited_labor['التكلفة_الإجمالية'].sum() | |
| avg_local_content = (edited_labor['التكلفة_الإجمالية'] * edited_labor['نسبة_المحتوى_المحلي']).sum() / total_labor_cost if total_labor_cost > 0 else 0 | |
| st.markdown(f""" | |
| **إجمالي تكلفة القوى العاملة**: {total_labor_cost:,.2f} ريال | |
| **متوسط نسبة المحتوى المحلي للقوى العاملة**: {avg_local_content*100:.2f}% | |
| **المستهدف**: 80% | |
| **الحالة**: {"✅ ملتزم" if avg_local_content >= 0.8 else "❌ غير ملتزم"} | |
| """) | |
| with lc_tabs[3]: # التحليل | |
| st.markdown("#### تحليل المحتوى المحلي") | |
| # حساب المحتوى المحلي الإجمالي | |
| try: | |
| # تجميع بيانات تحليل المحتوى المحلي | |
| products_cost = st.session_state.local_content_products['التكلفة_الإجمالية'].sum() | |
| products_local_content = (st.session_state.local_content_products['التكلفة_الإجمالية'] * st.session_state.local_content_products['نسبة_المحتوى_المحلي']).sum() / products_cost if products_cost > 0 else 0 | |
| services_cost = st.session_state.local_content_services['التكلفة'].sum() | |
| services_local_content = (st.session_state.local_content_services['التكلفة'] * st.session_state.local_content_services['نسبة_المحتوى_المحلي']).sum() / services_cost if services_cost > 0 else 0 | |
| labor_cost = st.session_state.local_content_labor['التكلفة_الإجمالية'].sum() | |
| labor_local_content = (st.session_state.local_content_labor['التكلفة_الإجمالية'] * st.session_state.local_content_labor['نسبة_المحتوى_المحلي']).sum() / labor_cost if labor_cost > 0 else 0 | |
| # حساب الوزن النسبي لكل مكون | |
| total_cost = products_cost + services_cost + labor_cost | |
| products_weight = products_cost / total_cost if total_cost > 0 else 0 | |
| services_weight = services_cost / total_cost if total_cost > 0 else 0 | |
| labor_weight = labor_cost / total_cost if total_cost > 0 else 0 | |
| # حساب المحتوى المحلي الإجمالي | |
| total_local_content = (products_local_content * products_weight) + (services_local_content * services_weight) + (labor_local_content * labor_weight) | |
| # عرض ملخص المحتوى المحلي | |
| st.markdown("### ملخص المحتوى المحلي") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("إجمالي التكاليف", f"{total_cost:,.2f} ريال") | |
| with col2: | |
| st.metric("نسبة المحتوى المحلي الإجمالية", f"{total_local_content*100:.2f}%") | |
| with col3: | |
| target_local_content = 0.7 # 70% | |
| st.metric("الحالة", "ملتزم" if total_local_content >= target_local_content else "غير ملتزم", delta=f"{(total_local_content - target_local_content)*100:.2f}%") | |
| # عرض رسم بياني للمقارنة | |
| st.markdown("### تحليل بصري للمحتوى المحلي") | |
| # رسم بياني شريطي لنسب المحتوى المحلي | |
| categories = ['المنتجات', 'الخدمات', 'القوى العاملة', 'الإجمالي'] | |
| actual_values = [products_local_content * 100, services_local_content * 100, labor_local_content * 100, total_local_content * 100] | |
| target_values = [70, 60, 80, 70] # المستهدفات | |
| # تهيئة البيانات للرسم البياني | |
| chart_data = pd.DataFrame({ | |
| 'الفئة': categories, | |
| 'النسبة الفعلية': actual_values, | |
| 'النسبة المستهدفة': target_values | |
| }) | |
| # رسم بياني شريطي للمقارنة | |
| fig = go.Figure() | |
| fig.add_trace(go.Bar( | |
| x=chart_data['الفئة'], | |
| y=chart_data['النسبة الفعلية'], | |
| name='النسبة الفعلية', | |
| marker_color='rgb(26, 118, 255)' | |
| )) | |
| fig.add_trace(go.Bar( | |
| x=chart_data['الفئة'], | |
| y=chart_data['النسبة المستهدفة'], | |
| name='النسبة المستهدفة', | |
| marker_color='rgb(55, 83, 109)' | |
| )) | |
| 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) | |
| # عرض توصيات لتحسين نسبة المحتوى المحلي | |
| st.markdown("### توصيات لتحسين نسبة المحتوى المحلي") | |
| recommendations = [] | |
| if products_local_content < 0.7: | |
| recommendations.append("- زيادة نسبة المحتوى المحلي للمنتجات من خلال:") | |
| recommendations.append(" - البحث عن موردين محليين للمنتجات ذات النسبة المنخفضة") | |
| recommendations.append(" - استبدال المنتجات المستوردة ببدائل محلية") | |
| recommendations.append(" - التعاون مع المصانع المحلية لتوطين صناعة المنتجات") | |
| if services_local_content < 0.6: | |
| recommendations.append("- زيادة نسبة المحتوى المحلي للخدمات من خلال:") | |
| recommendations.append(" - التعاقد مع شركات خدمات محلية") | |
| recommendations.append(" - تحويل الخدمات المستعان بها من الخارج إلى شركات محلية") | |
| recommendations.append(" - تأهيل الشركات المحلية لتقديم الخدمات المطلوبة") | |
| if labor_local_content < 0.8: | |
| recommendations.append("- زيادة نسبة المحتوى المحلي للقوى العاملة من خلال:") | |
| recommendations.append(" - زيادة توظيف الكوادر المحلية") | |
| recommendations.append(" - تدريب وتأهيل العمالة المحلية") | |
| recommendations.append(" - استبدال العمالة الأجنبية بكوادر محلية تدريجياً") | |
| if total_local_content < 0.7: | |
| recommendations.append("- زيادة نسبة المحتوى المحلي الإجمالية من خلال:") | |
| recommendations.append(" - إعادة توزيع الميزانية لصالح المكونات ذات النسبة العالية من المحتوى المحلي") | |
| recommendations.append(" - وضع خطة مرحلية لزيادة المحتوى المحلي") | |
| recommendations.append(" - التعاون مع اللجنة المحلية لزيادة المحتوى المحلي") | |
| if recommendations: | |
| for rec in recommendations: | |
| st.markdown(rec) | |
| else: | |
| st.success("تهانينا! نسبة المحتوى المحلي متوافقة مع المتطلبات.") | |
| # حساب تأثير المحتوى المحلي على التسعير | |
| st.markdown("### تأثير المحتوى المحلي على التسعير") | |
| # تحديد عامل تعديل السعر بناءً على نسبة المحتوى المحلي | |
| price_adjustment_factor = 1.0 | |
| if total_local_content >= 0.9: | |
| price_adjustment_factor = 0.92 # خصم 8% للمحتوى المحلي العالي جداً | |
| price_discount = "8%" | |
| elif total_local_content >= 0.8: | |
| price_adjustment_factor = 0.94 # خصم 6% للمحتوى المحلي العالي | |
| price_discount = "6%" | |
| elif total_local_content >= 0.7: | |
| price_adjustment_factor = 0.96 # خصم 4% للمحتوى المحلي المتوسط | |
| price_discount = "4%" | |
| elif total_local_content >= 0.6: | |
| price_adjustment_factor = 0.98 # خصم 2% للمحتوى المحلي المنخفض | |
| price_discount = "2%" | |
| else: | |
| price_adjustment_factor = 1.0 # لا خصم | |
| price_discount = "0%" | |
| # عرض تأثير المحتوى المحلي على التسعير | |
| original_total = st.session_state.current_pricing['items']['الإجمالي'].sum() | |
| adjusted_total = original_total * price_adjustment_factor | |
| discount_amount = original_total - adjusted_total | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("إجمالي التسعير الأصلي", f"{original_total:,.2f} ريال") | |
| with col2: | |
| st.metric("نسبة الخصم بسبب المحتوى المحلي", price_discount) | |
| with col3: | |
| st.metric("إجمالي التسعير بعد الخصم", f"{adjusted_total:,.2f} ريال", delta=f"-{discount_amount:,.2f} ريال") | |
| # أزرار العمليات | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("حفظ تحليل المحتوى المحلي"): | |
| # حفظ بيانات المحتوى المحلي في التسعير الحالي | |
| st.session_state.current_pricing['local_content'] = { | |
| 'products': st.session_state.local_content_products.copy(), | |
| 'services': st.session_state.local_content_services.copy(), | |
| 'labor': st.session_state.local_content_labor.copy(), | |
| 'total_local_content': total_local_content, | |
| 'price_adjustment_factor': price_adjustment_factor | |
| } | |
| st.success("تم حفظ تحليل المحتوى المحلي بنجاح!") | |
| with col2: | |
| if st.button("تصدير تقرير المحتوى المحلي"): | |
| st.success("تم تصدير تقرير المحتوى المحلي بنجاح!") | |
| except Exception as e: | |
| st.error(f"حدث خطأ أثناء تحليل المحتوى المحلي: {str(e)}") | |
| st.warning("تأكد من إدخال بيانات المحتوى المحلي بشكل صحيح في التبويبات السابقة.") | |
| def _render_utilities_tab(self): | |
| """عرض تبويب الأدوات المساعدة""" | |
| import json | |
| import copy | |
| from datetime import datetime | |
| st.markdown("## الأدوات المساعدة") | |
| utilities_tab1, utilities_tab2, utilities_tab3, utilities_tab4, utilities_tab5 = st.tabs([ | |
| "الرسوم البيانية المتقدمة", | |
| "استيراد/تصدير الإعدادات", | |
| "النسخ الاحتياطي والاستعادة", | |
| "مقارنة نماذج التسعير", | |
| "إنشاء التقارير" | |
| ]) | |
| with utilities_tab1: | |
| st.markdown("### الرسوم البيانية المتقدمة لتحليل التكلفة") | |
| if 'item_cost_result' in st.session_state: | |
| result = st.session_state.item_cost_result | |
| # تبويب الرسوم البيانية | |
| chart_tab1, chart_tab2, chart_tab3 = st.tabs(["توزيع التكلفة", "مقارنة المكونات", "تأثير العوامل"]) | |
| with chart_tab1: | |
| # رسم بياني دائري لتوزيع التكلفة | |
| fig = go.Figure(data=[go.Pie( | |
| labels=["المواد", "العمالة", "المعدات", "المصاريف الإدارية", "هامش الربح"], | |
| values=[ | |
| result['تكاليف_مباشرة']['المواد']['الإجمالي'], | |
| result['تكاليف_مباشرة']['العمالة']['الإجمالي'], | |
| result['تكاليف_مباشرة']['المعدات']['الإجمالي'], | |
| result['مصاريف_إدارية']['قيمة'], | |
| result['هامش_ربح']['قيمة'] | |
| ], | |
| hole=.3, | |
| marker_colors=['#36a2eb', '#ff6384', '#4bc0c0', '#ffcd56', '#9966ff'] | |
| )]) | |
| fig.update_layout(title_text="توزيع مكونات التكلفة") | |
| st.plotly_chart(fig, use_container_width=True) | |
| with chart_tab2: | |
| # رسم بياني شريطي للمكونات الرئيسية | |
| categories = ["المواد", "العمالة", "المعدات"] | |
| values = [ | |
| result['تكاليف_مباشرة']['المواد']['الإجمالي'], | |
| result['تكاليف_مباشرة']['العمالة']['الإجمالي'], | |
| result['تكاليف_مباشرة']['المعدات']['الإجمالي'] | |
| ] | |
| fig = go.Figure(data=[go.Bar(x=categories, y=values, marker_color=['#36a2eb', '#ff6384', '#4bc0c0'])]) | |
| fig.update_layout(title_text="مقارنة بين المكونات الرئيسية للتكلفة") | |
| st.plotly_chart(fig, use_container_width=True) | |
| with chart_tab3: | |
| # مخطط شريطي يوضح تأثير عوامل التعديل على التكلفة | |
| original_cost = result['التكلفة_الإجمالية'] | |
| final_cost = result['السعر_المعدل']['إجمالي'] | |
| # التحقق من وجود العوامل، وإضافتها بقيم افتراضية إذا كانت غير موجودة | |
| if 'عوامل_التعديل' not in result: | |
| result['عوامل_التعديل'] = {} | |
| # إضافة المفاتيح الناقصة بقيم افتراضية | |
| default_factors = { | |
| 'location_factor': 1.0, | |
| 'time_factor': 1.0, | |
| 'risk_factor': 1.0, | |
| 'market_factor': 1.0 | |
| } | |
| for key, default_value in default_factors.items(): | |
| if key not in result['عوامل_التعديل']: | |
| result['عوامل_التعديل'][key] = default_value | |
| factors = { | |
| "معامل الموقع": result['عوامل_التعديل']['location_factor'], | |
| "معامل الوقت": result['عوامل_التعديل']['time_factor'], | |
| "معامل المخاطر": result['عوامل_التعديل']['risk_factor'], | |
| "معامل السوق": result['عوامل_التعديل']['market_factor'] | |
| } | |
| # حساب القيمة المضافة من كل عامل | |
| factor_effects = {} | |
| for factor_name, factor_value in factors.items(): | |
| factor_effects[factor_name] = original_cost * (factor_value - 1) | |
| fig = go.Figure() | |
| fig.add_trace(go.Waterfall( | |
| name="تأثير العوامل", | |
| orientation="v", | |
| measure=["absolute"] + ["relative"] * len(factor_effects) + ["total"], | |
| x=["التكلفة الأصلية"] + list(factor_effects.keys()) + ["التكلفة النهائية"], | |
| y=[original_cost] + list(factor_effects.values()) + [0], | |
| text=[f"{original_cost:,.2f}"] + [f"{val:,.2f}" for val in factor_effects.values()] + [f"{final_cost:,.2f}"], | |
| connector={"line": {"color": "rgb(63, 63, 63)"}}, | |
| )) | |
| fig.update_layout(title_text="تأثير عوامل التعديل على التكلفة النهائية") | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.warning("لم يتم العثور على بيانات تحليل التكلفة. يرجى إجراء تحليل تكلفة في تبويب 'تحليل سعر البند' أولاً.") | |
| with utilities_tab2: | |
| st.markdown("### استيراد/تصدير إعدادات التسعير") | |
| export_col, import_col = st.columns(2) | |
| with export_col: | |
| st.markdown("#### تصدير الإعدادات الحالية") | |
| if st.button("تصدير إعدادات التسعير", key="export_pricing_settings"): | |
| pricing_settings = { | |
| "construction_item": st.session_state.construction_item if 'construction_item' in st.session_state else None, | |
| "current_pricing": st.session_state.current_pricing if 'current_pricing' in st.session_state else None | |
| } | |
| settings_json = json.dumps(pricing_settings, ensure_ascii=False, indent=2) | |
| st.download_button( | |
| label="تنزيل إعدادات التسعير", | |
| data=settings_json, | |
| file_name="pricing_settings.json", | |
| mime="application/json", | |
| key="download_settings_button" | |
| ) | |
| with import_col: | |
| st.markdown("#### استيراد إعدادات سابقة") | |
| uploaded_file = st.file_uploader("استيراد إعدادات تسعير سابقة", type=["json"], key="upload_pricing_settings") | |
| if uploaded_file is not None: | |
| try: | |
| settings_data = json.loads(uploaded_file.getvalue().decode('utf-8')) | |
| # تحديث بيانات التسعير في الجلسة | |
| if 'construction_item' in settings_data and settings_data['construction_item']: | |
| st.session_state.construction_item = settings_data['construction_item'] | |
| if 'current_pricing' in settings_data and settings_data['current_pricing']: | |
| st.session_state.current_pricing = settings_data['current_pricing'] | |
| st.success("تم استيراد الإعدادات بنجاح!") | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"حدث خطأ أثناء استيراد الإعدادات: {str(e)}") | |
| with utilities_tab3: | |
| st.markdown("### النسخ الاحتياطي والاستعادة") | |
| backup_tab1, backup_tab2 = st.tabs(["إنشاء نسخة احتياطية", "استعادة من نسخة احتياطية"]) | |
| with backup_tab1: | |
| st.markdown("#### إنشاء نسخة احتياطية كاملة") | |
| st.markdown("تقوم هذه العملية بإنشاء نسخة احتياطية كاملة لجميع بيانات التسعير الحالية.") | |
| if st.button("إنشاء نسخة احتياطية كاملة", key="create_full_backup"): | |
| backup_data = { | |
| "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| "construction_item": st.session_state.construction_item if 'construction_item' in st.session_state else None, | |
| "current_pricing": st.session_state.current_pricing if 'current_pricing' in st.session_state else None, | |
| "pricing_models": st.session_state.pricing_models if 'pricing_models' in st.session_state else [], | |
| "manual_items": st.session_state.manual_items.to_dict('records') if 'manual_items' in st.session_state else [] | |
| } | |
| backup_json = json.dumps(backup_data, ensure_ascii=False, indent=2) | |
| filename = f"wahbi_pricing_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" | |
| st.download_button( | |
| label="تنزيل النسخة الاحتياطية", | |
| data=backup_json, | |
| file_name=filename, | |
| mime="application/json", | |
| key="download_backup_button" | |
| ) | |
| st.success("تم إنشاء النسخة الاحتياطية بنجاح!") | |
| with backup_tab2: | |
| st.markdown("#### استعادة من نسخة احتياطية") | |
| st.markdown("يمكنك استعادة بيانات التسعير من نسخة احتياطية سابقة.") | |
| backup_file = st.file_uploader("استعادة من نسخة احتياطية", type=["json"], key="restore_backup_file") | |
| if backup_file is not None: | |
| if st.button("استعادة البيانات", key="restore_from_backup"): | |
| try: | |
| backup_data = json.loads(backup_file.getvalue().decode('utf-8')) | |
| # استعادة البيانات إلى الحالة الحالية | |
| if 'construction_item' in backup_data and backup_data['construction_item']: | |
| st.session_state.construction_item = backup_data['construction_item'] | |
| if 'current_pricing' in backup_data and backup_data['current_pricing']: | |
| st.session_state.current_pricing = backup_data['current_pricing'] | |
| if 'pricing_models' in backup_data: | |
| st.session_state.pricing_models = backup_data['pricing_models'] | |
| if 'manual_items' in backup_data and backup_data['manual_items']: | |
| st.session_state.manual_items = pd.DataFrame(backup_data['manual_items']) | |
| st.success(f"تم استعادة البيانات من النسخة الاحتياطية بنجاح! (تاريخ النسخة: {backup_data.get('timestamp', 'غير معروف')})") | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"حدث خطأ أثناء استعادة البيانات: {str(e)}") | |
| with utilities_tab4: | |
| st.markdown("### مقارنة نماذج التسعير") | |
| st.markdown("هذه الأداة تتيح لك مقارنة نماذج التسعير المختلفة واختيار الأفضل منها.") | |
| # تهيئة قائمة نماذج التسعير إذا لم تكن موجودة | |
| if 'pricing_models' not in st.session_state: | |
| st.session_state.pricing_models = [] | |
| # إضافة النموذج الحالي للمقارنة | |
| if st.button("إضافة النموذج الحالي للمقارنة", key="add_current_model"): | |
| if 'current_pricing' in st.session_state and st.session_state.current_pricing is not None: | |
| model_name = st.session_state.current_pricing.get('name', 'نموذج بدون اسم') | |
| model_data = copy.deepcopy(st.session_state.current_pricing) | |
| # تحقق من عدم وجود نموذج بنفس الاسم | |
| exists = False | |
| for model in st.session_state.pricing_models: | |
| if model.get('name') == model_name: | |
| exists = True | |
| break | |
| if not exists: | |
| st.session_state.pricing_models.append(model_data) | |
| st.success(f"تم إضافة نموذج '{model_name}' للمقارنة!") | |
| else: | |
| st.warning("يوجد نموذج بنفس الاسم في المقارنة بالفعل!") | |
| else: | |
| st.error("لا يوجد نموذج تسعير حالي للإضافة. يرجى إنشاء تسعير جديد أولاً.") | |
| # عرض جدول المقارنة إذا كان هناك نماذج مضافة | |
| if len(st.session_state.pricing_models) > 0: | |
| st.markdown("### جدول مقارنة نماذج التسعير") | |
| comparison_data = [] | |
| for model in st.session_state.pricing_models: | |
| # حساب إجمالي التكلفة | |
| items_df = pd.DataFrame(model.get('items', {})) | |
| total_price = 0 | |
| if not items_df.empty and 'الإجمالي' in items_df.columns: | |
| total_price = items_df['الإجمالي'].sum() | |
| comparison_data.append({ | |
| "اسم النموذج": model.get('name', 'غير معروف'), | |
| "طريقة التسعير": model.get('method', 'غير معروفة'), | |
| "إجمالي التكلفة": f"{total_price:,.2f} ريال", | |
| "عدد البنود": len(items_df) if not items_df.empty else 0, | |
| }) | |
| comparison_df = pd.DataFrame(comparison_data) | |
| st.dataframe(comparison_df, use_container_width=True, hide_index=True) | |
| # عرض رسم بياني للمقارنة | |
| if len(comparison_data) > 1: | |
| st.markdown("### رسم بياني للمقارنة") | |
| # استخراج البيانات للرسم البياني | |
| models = [data["اسم النموذج"] for data in comparison_data] | |
| values = [float(data["إجمالي التكلفة"].replace("ريال", "").replace(",", "")) for data in comparison_data] | |
| # رسم بياني شريطي | |
| fig = go.Figure(data=[ | |
| go.Bar(x=models, y=values, marker_color='rgb(26, 118, 255)') | |
| ]) | |
| fig.update_layout( | |
| title="مقارنة التكلفة الإجمالية بين نماذج التسعير", | |
| xaxis_title="نموذج التسعير", | |
| yaxis_title="التكلفة الإجمالية (ريال)" | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # أزرار إدارة النماذج | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("حذف جميع النماذج", key="clear_comparison"): | |
| st.session_state.pricing_models = [] | |
| st.success("تم مسح جميع نماذج المقارنة!") | |
| st.rerun() | |
| with col2: | |
| # تحديد نموذج للحذف | |
| model_to_delete = st.selectbox( | |
| "اختر نموذج للحذف من المقارنة", | |
| options=[model.get('name', f"نموذج {i+1}") for i, model in enumerate(st.session_state.pricing_models)], | |
| key="model_to_delete" | |
| ) | |
| if st.button("حذف النموذج المحدد", key="delete_selected_model"): | |
| for i, model in enumerate(st.session_state.pricing_models): | |
| if model.get('name') == model_to_delete: | |
| st.session_state.pricing_models.pop(i) | |
| st.warning(f"تم حذف النموذج '{model_to_delete}'") | |
| st.rerun() | |
| break | |
| else: | |
| st.info("لا توجد نماذج للمقارنة حالياً. يرجى إضافة النموذج الحالي للمقارنة أولاً.") | |
| with utilities_tab5: | |
| st.markdown("### إنشاء تقارير التسعير") | |
| st.markdown("يمكنك استخدام هذه الأداة لإنشاء تقارير مفصلة عن التسعير.") | |
| report_type = st.selectbox( | |
| "اختر نوع التقرير", | |
| options=["تقرير ملخص", "تقرير تفصيلي", "تقرير المقارنة"], | |
| key="report_type_selector" | |
| ) | |
| if st.button("إنشاء التقرير", key="generate_report_button"): | |
| if report_type == "تقرير ملخص" and 'current_pricing' in st.session_state and st.session_state.current_pricing is not None: | |
| # إنشاء تقرير ملخص | |
| if isinstance(st.session_state.current_pricing.get('items'), pd.DataFrame): | |
| df = st.session_state.current_pricing['items'].copy() | |
| # حساب الإجماليات | |
| total_price = df['الإجمالي'].sum() if 'الإجمالي' in df.columns else 0 | |
| # تقدير تكاليف المكونات | |
| materials_cost = total_price * 0.6 # تقدير تقريبي للمواد | |
| labor_cost = total_price * 0.25 # تقدير تقريبي للعمالة | |
| equipment_cost = total_price * 0.15 # تقدير تقريبي للمعدات | |
| admin_cost = total_price * 0.05 # تقدير تقريبي للمصاريف الإدارية | |
| profit_margin = total_price * 0.1 # تقدير تقريبي لهامش الربح | |
| final_total = total_price * 1.15 # إجمالي بعد إضافة المصاريف الإدارية وهامش الربح | |
| # إنشاء جدول الملخص | |
| summary = pd.DataFrame({ | |
| "بند التكلفة": ["إجمالي المواد", "إجمالي الأجور", "إجمالي المعدات", "المصاريف الإدارية", "هامش الربح", "الإجمالي النهائي"], | |
| "القيمة": [ | |
| materials_cost, | |
| labor_cost, | |
| equipment_cost, | |
| admin_cost, | |
| profit_margin, | |
| final_total | |
| ] | |
| }) | |
| # تنسيق التقرير | |
| styled_df = summary.style.format({ | |
| "القيمة": "{:,.2f} ريال" | |
| }) | |
| # تحويل الجدول إلى HTML | |
| html = f""" | |
| <html dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>تقرير ملخص التسعير</title> | |
| <style> | |
| body {{ font-family: 'Arial', 'Tahoma', sans-serif; direction: rtl; }} | |
| h1, h2 {{ color: #075e54; text-align: center; }} | |
| table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }} | |
| th {{ background-color: #075e54; color: white; padding: 10px; text-align: right; }} | |
| td {{ padding: 8px; text-align: right; border-bottom: 1px solid #ddd; }} | |
| tr:nth-child(even) {{ background-color: #f9f9f9; }} | |
| .header {{ background-color: #f2f2f2; padding: 20px; margin-bottom: 20px; }} | |
| .footer {{ margin-top: 30px; text-align: center; font-size: 0.8em; color: #666; }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <h1>تقرير ملخص التسعير</h1> | |
| <h2>{st.session_state.current_pricing.get('name', 'تسعير بدون اسم')}</h2> | |
| <p>تاريخ التقرير: {datetime.now().strftime('%Y-%m-%d %H:%M')}</p> | |
| </div> | |
| <h3>ملخص التكاليف</h3> | |
| {styled_df.to_html()} | |
| <h3>البيانات الأساسية</h3> | |
| <ul> | |
| <li>عدد البنود: {len(df)}</li> | |
| <li>طريقة التسعير: {st.session_state.current_pricing.get('method', 'غير محددة')}</li> | |
| </ul> | |
| <div class="footer"> | |
| <p>تم إنشاء هذا التقرير بواسطة نظام وهبي للتحليل المتقدم للمناقصات</p> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| # تقديم زر التنزيل | |
| st.download_button( | |
| label="تنزيل التقرير الملخص", | |
| data=html, | |
| file_name="pricing_summary_report.html", | |
| mime="text/html", | |
| key="download_summary_report" | |
| ) | |
| st.success("تم إنشاء التقرير الملخص بنجاح!") | |
| else: | |
| st.error("تعذر قراءة بيانات التسعير الحالي. يرجى التأكد من وجود تسعير صالح.") | |
| elif report_type == "تقرير تفصيلي" and 'current_pricing' in st.session_state and st.session_state.current_pricing is not None: | |
| # سيتم تنفيذ التقرير التفصيلي | |
| st.info("جاري إعداد التقرير التفصيلي...") | |
| # هنا يمكن تنفيذ كود إنشاء التقرير التفصيلي | |
| st.success("تم إنشاء التقرير التفصيلي. سيتم تطوير هذه الميزة قريباً.") | |
| elif report_type == "تقرير المقارنة" and 'pricing_models' in st.session_state and len(st.session_state.pricing_models) > 0: | |
| # سيتم تنفيذ تقرير المقارنة | |
| st.info("جاري إعداد تقرير المقارنة...") | |
| # هنا يمكن تنفيذ كود إنشاء تقرير المقارنة | |
| st.success("تم إنشاء تقرير المقارنة. سيتم تطوير هذه الميزة قريباً.") | |
| else: | |
| st.error("لا توجد بيانات كافية لإنشاء التقرير المطلوب. يرجى التأكد من وجود تسعير أو نماذج مقارنة.") |