|
""" |
|
تطبيق وحدة التسعير المتكاملة |
|
""" |
|
|
|
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.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) |
|
:return: زر مُنسّق |
|
""" |
|
|
|
with st.container(): |
|
|
|
col1 = st.columns([1]) |
|
|
|
|
|
display_label = f"{icon} {label}" if icon else label |
|
|
|
|
|
clicked = col1[0].button( |
|
display_label, |
|
key=key, |
|
on_click=on_click, |
|
args=args, |
|
use_container_width=full_width |
|
) |
|
|
|
return clicked |
|
|
|
|
|
def icon_button(icon, key, type="primary", on_click=None, args=None, tooltip=""): |
|
""" |
|
إنشاء زر أيقونة صغير |
|
:param icon: الأيقونة (emoji) |
|
:param key: مفتاح الزر الفريد |
|
:param type: نوع التنسيق |
|
:param on_click: الدالة التي سيتم تنفيذها عند النقر |
|
:param args: معاملات الدالة |
|
:param tooltip: تلميح عند تمرير المؤشر فوق الزر |
|
:return: زر أيقونة |
|
""" |
|
|
|
with st.container(): |
|
|
|
if tooltip: |
|
st.caption(tooltip) |
|
|
|
|
|
clicked = st.button( |
|
icon, |
|
key=key, |
|
on_click=on_click, |
|
args=args, |
|
help=tooltip |
|
) |
|
|
|
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 modules.pricing.services.templates_catalog.templates_catalog import TemplatesCatalog |
|
from utils.excel_handler import export_to_excel |
|
from utils.pdf_handler import export_pricing_to_pdf, export_pricing_with_analysis_to_pdf |
|
from utils.helpers import format_number, format_currency, create_directory_if_not_exists |
|
|
|
|
|
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() |
|
self.templates_catalog = TemplatesCatalog(self.construction_templates) |
|
|
|
def render(self): |
|
"""عرض واجهة وحدة التسعير""" |
|
|
|
|
|
st.title("وحدة التسعير المتكاملة") |
|
|
|
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_templates_catalog_tab() |
|
|
|
with tabs[7]: |
|
self._render_utilities_tab() |
|
|
|
def _render_templates_catalog_tab(self): |
|
"""عرض تبويب كتالوج البنود النموذجية""" |
|
|
|
st.markdown("### كتالوج البنود النموذجية") |
|
|
|
|
|
with st.expander("دليل استخدام كتالوج البنود النموذجية", expanded=False): |
|
st.markdown(""" |
|
**كتالوج البنود النموذجية** هو مكتبة شاملة من البنود الجاهزة لمختلف أنواع الأعمال الإنشائية (خرسانة، حديد، عزل، تشطيبات، إلخ). |
|
|
|
### مميزات الكتالوج: |
|
- تفاصيل دقيقة للمواد والعمالة والمعدات المطلوبة لكل بند. |
|
- تحليل تكلفة تفصيلي يمكن استخدامه مباشرة في عروض الأسعار. |
|
- ربط مباشر مع حاسبة تكاليف البناء وحاسبة الأسعار. |
|
|
|
### كيفية الاستخدام: |
|
- استخدام البنود النموذجية مباشرة في مشاريعك. |
|
- تعديل البنود النموذجية لتناسب متطلبات المشروع. |
|
- إضافة بنود جديدة إلى الكتالوج للاستخدام المستقبلي. |
|
""") |
|
|
|
|
|
self.templates_catalog.render() |
|
|
|
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, |
|
'العمالة': 0.25, |
|
'المعدات': 0.1, |
|
'نفقات عامة': 0.05 |
|
} |
|
|
|
|
|
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 == "إدخال يدوي": |
|
|
|
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: |
|
|
|
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)] |
|
}) |
|
|
|
|
|
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() |
|
|
|
elif data_source == "استيراد من وحدة تحليل المستندات": |
|
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) |
|
|
|
elif data_source == "استيراد من وحدة المشاريع": |
|
|
|
available_projects = [ |
|
"مشروع تطوير طريق الملك عبدالعزيز", |
|
"مشروع إنشاء محطة تحلية المياه بالجبيل", |
|
"مشروع توسعة مستشفى الملك فهد", |
|
"مشروع إنشاء مجمع سكني بالرياض" |
|
] |
|
|
|
selected_project = st.selectbox("اختر المشروع", available_projects, key="project_selector") |
|
|
|
if styled_button("استيراد البيانات من المشروع", key="import_project_data_button", type="primary", icon="🏗️", full_width=True): |
|
|
|
with st.spinner("جاري استيراد بيانات المشروع..."): |
|
time.sleep(1.5) |
|
|
|
|
|
if selected_project == "مشروع تطوير طريق الملك عبدالعزيز": |
|
project_items = pd.DataFrame({ |
|
'رقم البند': ["R1", "R2", "R3", "R4", "R5"], |
|
'وصف البند': [ |
|
"أعمال الحفر والردم للطريق", |
|
"توريد وتنفيذ طبقة الأساس", |
|
"طبقة الأسفلت الأولى", |
|
"طبقة الأسفلت النهائية", |
|
"أعمال الدهانات والعلامات" |
|
], |
|
'الوحدة': ["م3", "م2", "م2", "م2", "م.ط"], |
|
'الكمية': [5000.0, 8000.0, 8000.0, 8000.0, 4000.0], |
|
'سعر الوحدة': [45.0, 120.0, 85.0, 95.0, 25.0], |
|
'الإجمالي': [225000.0, 960000.0, 680000.0, 760000.0, 100000.0] |
|
}) |
|
elif selected_project == "مشروع إنشاء محطة تحلية المياه بالجبيل": |
|
project_items = pd.DataFrame({ |
|
'رقم البند': ["W1", "W2", "W3", "W4", "W5"], |
|
'وصف البند': [ |
|
"أعمال الخرسانة المسلحة للخزانات", |
|
"توريد وتركيب معدات التحلية", |
|
"أعمال التمديدات والأنابيب", |
|
"تشطيبات المباني الإدارية", |
|
"أعمال الكهرباء والتحكم" |
|
], |
|
'الوحدة': ["م3", "قطعة", "م.ط", "م2", "مقطوعية"], |
|
'الكمية': [1800.0, 12.0, 5000.0, 1200.0, 1.0], |
|
'سعر الوحدة': [1200.0, 250000.0, 850.0, 750.0, 1500000.0], |
|
'الإجمالي': [2160000.0, 3000000.0, 4250000.0, 900000.0, 1500000.0] |
|
}) |
|
elif selected_project == "مشروع توسعة مستشفى الملك فهد": |
|
project_items = pd.DataFrame({ |
|
'رقم البند': ["H1", "H2", "H3", "H4", "H5", "H6"], |
|
'وصف البند': [ |
|
"أعمال الهيكل الخرساني", |
|
"أعمال البناء والجدران", |
|
"التشطيبات الداخلية", |
|
"الأنظمة الكهربائية والميكانيكية", |
|
"تجهيزات طبية", |
|
"أعمال الموقع العام" |
|
], |
|
'الوحدة': ["م3", "م2", "م2", "غرفة", "قطعة", "م2"], |
|
'الكمية': [2800.0, 4500.0, 7500.0, 120.0, 45.0, 3000.0], |
|
'سعر الوحدة': [1500.0, 650.0, 1200.0, 35000.0, 80000.0, 350.0], |
|
'الإجمالي': [4200000.0, 2925000.0, 9000000.0, 4200000.0, 3600000.0, 1050000.0] |
|
}) |
|
else: |
|
project_items = pd.DataFrame({ |
|
'رقم البند': ["B1", "B2", "B3", "B4", "B5", "B6", "B7"], |
|
'وصف البند': [ |
|
"الهياكل الخرسانية للفلل", |
|
"الجدران والقواطع الداخلية", |
|
"التشطيبات الخارجية", |
|
"التشطيبات الداخلية", |
|
"أعمال الكهرباء والسباكة", |
|
"الملحقات والحدائق", |
|
"البنية التحتية للموقع" |
|
], |
|
'الوحدة': ["م3", "م2", "م2", "م2", "فيلا", "م2", "مقطوعية"], |
|
'الكمية': [3500.0, 12000.0, 8000.0, 18000.0, 25.0, 5000.0, 1.0], |
|
'سعر الوحدة': [1350.0, 450.0, 380.0, 750.0, 85000.0, 280.0, 2500000.0], |
|
'الإجمالي': [4725000.0, 5400000.0, 3040000.0, 13500000.0, 2125000.0, 1400000.0, 2500000.0] |
|
}) |
|
|
|
st.session_state.manual_items = project_items.copy() |
|
st.success(f"تم استيراد بيانات المشروع '{selected_project}' بنجاح!") |
|
st.dataframe(project_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) |
|
|
|
|
|
|
|
with st.expander("🔍 توليد توصية ذكية باستخدام AI"): |
|
if styled_button("توليد توصية ذكية باستخدام AI", key="gen_ai_recommendation_btn", type="secondary", icon="🤖", full_width=True): |
|
import openai |
|
import os |
|
|
|
|
|
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("جاري توليد التوصية..."): |
|
|
|
response = client.chat.completions.create( |
|
model="gpt-4o", |
|
messages=[ |
|
{"role": "system", "content": "أنت خبير في تسعير مشاريع البناء والبنية التحتية."}, |
|
{"role": "user", "content": prompt} |
|
], |
|
temperature=0.4, |
|
max_tokens=500 |
|
) |
|
|
|
|
|
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="📊"): |
|
try: |
|
|
|
export_dir = "exports" |
|
create_directory_if_not_exists(export_dir) |
|
|
|
|
|
file_path = os.path.join(export_dir, f"تسعير_{tender_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx") |
|
export_to_excel(items, file_path, tender_name) |
|
|
|
st.success(f"تم تصدير التسعير إلى Excel بنجاح! مسار الملف: {file_path}") |
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}") |
|
|
|
if styled_button("تصدير إلى PDF", key="export_to_pdf_button", type="info", icon="📄"): |
|
try: |
|
|
|
export_dir = "exports" |
|
create_directory_if_not_exists(export_dir) |
|
|
|
|
|
file_path = os.path.join(export_dir, f"تسعير_{tender_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf") |
|
|
|
|
|
if 'items_price_analysis' in st.session_state and st.session_state.items_price_analysis: |
|
|
|
project_info = { |
|
'اسم_المشروع': tender_name, |
|
'وصف_المشروع': f"مناقصة رقم: {tender_number} - الجهة المالكة: {client} - الموقع: {location}" |
|
} |
|
|
|
|
|
export_pricing_with_analysis_to_pdf( |
|
items, |
|
st.session_state.items_price_analysis, |
|
file_path, |
|
title=f"تسعير مناقصة: {tender_name}", |
|
project_info=project_info |
|
) |
|
else: |
|
|
|
export_pricing_to_pdf( |
|
items, |
|
file_path, |
|
title=f"تسعير مناقصة: {tender_name}", |
|
description=f"مناقصة رقم: {tender_number} - الجهة المالكة: {client} - الموقع: {location}" |
|
) |
|
|
|
st.success(f"تم تصدير التسعير إلى PDF بنجاح! مسار الملف: {file_path}") |
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء تصدير البيانات: {str(e)}") |
|
|
|
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 |
|
items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' |
|
|
|
for idx in middle_items: |
|
items.at[idx, 'إستراتيجية التسعير'] = 'متوازن' |
|
|
|
for idx in late_items: |
|
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 |
|
items.at[idx, 'إستراتيجية التسعير'] = 'نقص' |
|
|
|
elif strategy == "تحميل البنود المؤكدة": |
|
|
|
confirmed_items = [0, 2, 4] |
|
variable_items = [idx for idx in range(len(items)) if idx not in confirmed_items] |
|
|
|
|
|
for idx in confirmed_items: |
|
if idx < len(items): |
|
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.25 |
|
items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' |
|
|
|
for idx in variable_items: |
|
if idx < len(items): |
|
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.85 |
|
items.at[idx, 'إستراتيجية التسعير'] = 'نقص' |
|
|
|
elif strategy == "تخفيض البنود المحتمل زيادتها": |
|
|
|
variable_items = [1, 3] |
|
other_items = [idx for idx in range(len(items)) if idx not in variable_items] |
|
|
|
|
|
for idx in variable_items: |
|
if idx < len(items): |
|
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 0.7 |
|
items.at[idx, 'إستراتيجية التسعير'] = 'نقص' |
|
|
|
for idx in other_items: |
|
if idx < len(items): |
|
items.at[idx, 'سعر الوحدة'] = items.at[idx, 'سعر الوحدة'] * 1.15 |
|
items.at[idx, 'إستراتيجية التسعير'] = 'زيادة' |
|
|
|
else: |
|
st.markdown("### تعديل أسعار البنود يدوياً") |
|
st.markdown("قم بتعديل أسعار البنود وإستراتيجية التسعير يدوياً من خلال النموذج أدناه.") |
|
|
|
|
|
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.subheader("بنود التسعير غير المتزن") |
|
|
|
|
|
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.subheader("مقارنة التسعير المتوازن وغير المتوازن") |
|
|
|
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.subheader("تحليل بصري للتسعير غير المتوازن") |
|
|
|
|
|
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)], |
|
'إستراتيجية التسعير': ['متوازن'] |
|
}) |
|
|
|
|
|
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.divider() |
|
st.subheader("حفظ وتصدير البيانات") |
|
|
|
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 = 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): |
|
"""عرض تبويب حاسبة تكاليف البناء المتكاملة""" |
|
|
|
|
|
from .construction_calculator import render_construction_calculator |
|
|
|
st.markdown("### حاسبة تكاليف البناء المتكاملة") |
|
|
|
|
|
with st.expander("دليل استخدام حاسبة تكاليف البناء", expanded=False): |
|
st.markdown(""" |
|
**حاسبة تكاليف البناء المتكاملة** هي أداة تساعد في حساب تكاليف البناء بشكل تفصيلي، مع مراعاة جميع عناصر التكلفة: |
|
|
|
### مكونات التكلفة: |
|
- المواد الخام |
|
- العمالة |
|
- المعدات |
|
- المصاريف الإدارية |
|
- هامش الربح |
|
|
|
### كيفية الاستخدام: |
|
1. اختر إما حساب تكلفة بند واحد أو حساب تكلفة مشروع. |
|
2. أدخل بيانات المواد والعمالة والمعدات. |
|
3. حدد نسب المصاريف الإدارية وهامش الربح. |
|
4. اضبط عوامل التعديل حسب ظروف المشروع. |
|
5. احصل على تحليل تفصيلي للتكاليف والسعر النهائي. |
|
|
|
### مميزات الحاسبة: |
|
- قاعدة بيانات مدمجة للأسعار المرجعية للمواد والعمالة. |
|
- تحليل نسب مساهمة كل عنصر في التكلفة. |
|
- إمكانية تعديل عوامل التكلفة حسب الموقع والظروف. |
|
- تصدير النتائج بتنسيقات متعددة. |
|
- الربط مع وحدة التسعير لنقل النتائج مباشرة إلى جدول البنود. |
|
- قاعدة بيانات للبنود النموذجية في أعمال المقاولات. |
|
""") |
|
|
|
render_construction_calculator() |
|
|
|
|
|
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, |
|
'هامش_الربح': 0.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("استيراد بند من الكتالوج"): |
|
|
|
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) |
|
|
|
|
|
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['السعر_المعدل']['إجمالي'])] |
|
}) |
|
|
|
|
|
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: |
|
|
|
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: |
|
|
|
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: |
|
|
|
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']) |
|
|
|
|
|
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['السعر_المعدل']['إجمالي'])] |
|
}) |
|
|
|
|
|
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("إلغاء", key="cancel_add_to_catalog"): |
|
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: |
|
|
|
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: |
|
|
|
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: |
|
|
|
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 |
|
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 |
|
price_discount = "8%" |
|
elif total_local_content >= 0.8: |
|
price_adjustment_factor = 0.94 |
|
price_discount = "6%" |
|
elif total_local_content >= 0.7: |
|
price_adjustment_factor = 0.96 |
|
price_discount = "4%" |
|
elif total_local_content >= 0.6: |
|
price_adjustment_factor = 0.98 |
|
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 = 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("لا توجد بيانات كافية لإنشاء التقرير المطلوب. يرجى التأكد من وجود تسعير أو نماذج مقارنة.") |