Spaces:
Sleeping
Sleeping
import streamlit as st | |
import pandas as pd | |
import numpy as np | |
import matplotlib.pyplot as plt | |
import matplotlib.font_manager as fm | |
import arabic_reshaper | |
from bidi.algorithm import get_display | |
import io | |
import base64 | |
from datetime import datetime | |
import random | |
import json | |
import os | |
class PricingApp: | |
"""وحدة التسعير المتكاملة""" | |
def __init__(self): | |
"""تهيئة وحدة التسعير""" | |
# تهيئة البيانات في حالة الجلسة إذا لم تكن موجودة | |
if 'projects' not in st.session_state: | |
st.session_state.projects = [ | |
{ | |
'id': 1, | |
'name': 'مشروع تطوير الطرق الداخلية', | |
'client': 'وزارة النقل', | |
'estimated_value': 5000000, | |
'deadline': '2024-06-30', | |
'status': 'قيد التسعير', | |
'created_at': '2024-01-15', | |
'pricing_type': 'قياسي' | |
}, | |
{ | |
'id': 2, | |
'name': 'مشروع إنشاء مبنى إداري', | |
'client': 'شركة التطوير العقاري', | |
'estimated_value': 12000000, | |
'deadline': '2024-08-15', | |
'status': 'قيد التسعير', | |
'created_at': '2024-02-01', | |
'pricing_type': 'غير متزن' | |
} | |
] | |
if 'current_project' not in st.session_state: | |
st.session_state.current_project = 1 | |
if 'next_project_id' not in st.session_state: | |
st.session_state.next_project_id = len(st.session_state.projects) + 1 | |
if 'show_new_project_form' not in st.session_state: | |
st.session_state.show_new_project_form = False | |
if 'show_edit_project_form' not in st.session_state: | |
st.session_state.show_edit_project_form = False | |
if 'edit_project_id' not in st.session_state: | |
st.session_state.edit_project_id = None | |
if 'boq_items' not in st.session_state: | |
st.session_state.boq_items = [ | |
{ | |
'id': 1, | |
'project_id': 1, | |
'code': 'A-001', | |
'description': 'أعمال الحفر والردم', | |
'unit': 'م3', | |
'quantity': 1500, | |
'unit_price': 45, | |
'total_price': 67500, | |
'resource_type': 'مواد' | |
}, | |
{ | |
'id': 2, | |
'project_id': 1, | |
'code': 'A-002', | |
'description': 'توريد وتركيب طبقة أساس', | |
'unit': 'م2', | |
'quantity': 3000, | |
'unit_price': 85, | |
'total_price': 255000, | |
'resource_type': 'مواد' | |
}, | |
{ | |
'id': 3, | |
'project_id': 1, | |
'code': 'A-003', | |
'description': 'توريد وتركيب خرسانة جاهزة', | |
'unit': 'م3', | |
'quantity': 750, | |
'unit_price': 320, | |
'total_price': 240000, | |
'resource_type': 'مواد' | |
}, | |
{ | |
'id': 4, | |
'project_id': 2, | |
'code': 'B-001', | |
'description': 'أعمال الأساسات', | |
'unit': 'م3', | |
'quantity': 500, | |
'unit_price': 450, | |
'total_price': 225000, | |
'resource_type': 'مواد' | |
}, | |
{ | |
'id': 5, | |
'project_id': 2, | |
'code': 'B-002', | |
'description': 'أعمال الهيكل الخرساني', | |
'unit': 'م3', | |
'quantity': 1200, | |
'unit_price': 550, | |
'total_price': 660000, | |
'resource_type': 'مواد' | |
} | |
] | |
if 'next_boq_item_id' not in st.session_state: | |
st.session_state.next_boq_item_id = len(st.session_state.boq_items) + 1 | |
if 'show_new_boq_item_form' not in st.session_state: | |
st.session_state.show_new_boq_item_form = False | |
if 'show_edit_boq_item_form' not in st.session_state: | |
st.session_state.show_edit_boq_item_form = False | |
if 'edit_boq_item_id' not in st.session_state: | |
st.session_state.edit_boq_item_id = None | |
if 'show_resource_selector' not in st.session_state: | |
st.session_state.show_resource_selector = False | |
if 'selected_resource_type' not in st.session_state: | |
st.session_state.selected_resource_type = "المواد" | |
# تهيئة معامل تعديل التسعير الغير متزن | |
if 'unbalanced_pricing_factors' not in st.session_state: | |
st.session_state.unbalanced_pricing_factors = { | |
'early_items_factor': 1.15, # زيادة أسعار البنود المبكرة بنسبة 15% | |
'late_items_factor': 0.90, # تخفيض أسعار البنود المتأخرة بنسبة 10% | |
'custom_factors': {} # معاملات مخصصة لبنود محددة | |
} | |
# تهيئة حالة حفظ التسعير | |
if 'saved_pricing' not in st.session_state: | |
st.session_state.saved_pricing = [] | |
# تهيئة حالة تحليل سعر البند | |
if 'item_analysis_edited' not in st.session_state: | |
st.session_state.item_analysis_edited = False | |
def render(self): | |
"""طريقة للتوافق مع الواجهة القديمة""" | |
self.run() | |
def run(self): | |
"""تشغيل وحدة التسعير""" | |
st.title("وحدة التسعير المتكاملة") | |
# عرض زر إنشاء تسعير جديد | |
col1, col2, col3 = st.columns([1, 2, 1]) | |
with col2: | |
if st.button("➕ إنشاء تسعير جديد", key="create_new_pricing_btn", type="primary"): | |
st.session_state.show_new_project_form = True | |
# عرض نموذج إنشاء تسعير جديد | |
if st.session_state.show_new_project_form: | |
self._render_new_project_form() | |
# عرض نموذج تعديل المشروع | |
if st.session_state.show_edit_project_form and st.session_state.edit_project_id is not None: | |
self._render_edit_project_form() | |
# عرض قائمة المشاريع | |
self._render_projects_list() | |
# عرض تفاصيل المشروع الحالي | |
if st.session_state.current_project: | |
project_info = self._get_current_project_info() | |
if project_info: | |
self._render_project_info(project_info) | |
# عرض علامات التبويب | |
tab1, tab2, tab3, tab4, tab5 = st.tabs([ | |
"جدول الكميات", | |
"تحليل سعر البند", | |
"تحليل التكلفة", | |
"تحليل الربحية", | |
"استراتيجيات التسعير" | |
]) | |
with tab1: | |
self._render_bill_of_quantities() | |
with tab2: | |
self._render_item_price_analysis() | |
with tab3: | |
self._render_cost_analysis(project_info) | |
with tab4: | |
self._render_profit_margin(project_info) | |
with tab5: | |
self._render_pricing_strategies(project_info) | |
# عرض أزرار التصدير والحفظ | |
self._render_export_save_buttons(project_info) | |
def _render_new_project_form(self): | |
"""عرض نموذج إنشاء مشروع جديد""" | |
st.subheader("إنشاء تسعير جديد") | |
with st.form(key="new_project_form"): | |
name = st.text_input("اسم المشروع", key="new_project_name") | |
client = st.text_input("العميل", key="new_project_client") | |
estimated_value = st.number_input("القيمة التقديرية", min_value=0.0, format="%f", key="new_project_value") | |
deadline = st.date_input("الموعد النهائي", key="new_project_deadline") | |
pricing_type = st.selectbox( | |
"نوع التسعير", | |
["قياسي", "غير متزن", "موجه ربحية", "تنافسي", "استراتيجي"], | |
key="new_project_pricing_type" | |
) | |
col1, col2 = st.columns(2) | |
with col1: | |
submit_button = st.form_submit_button("حفظ") | |
with col2: | |
cancel_button = st.form_submit_button("إلغاء") | |
if submit_button: | |
if name and client and estimated_value > 0: | |
new_project = { | |
'id': st.session_state.next_project_id, | |
'name': name, | |
'client': client, | |
'estimated_value': estimated_value, | |
'deadline': deadline.strftime("%Y-%m-%d"), | |
'status': 'قيد التسعير', | |
'created_at': datetime.now().strftime("%Y-%m-%d"), | |
'pricing_type': pricing_type | |
} | |
st.session_state.projects.append(new_project) | |
st.session_state.current_project = new_project['id'] | |
st.session_state.next_project_id += 1 | |
st.session_state.show_new_project_form = False | |
st.rerun() | |
if cancel_button: | |
st.session_state.show_new_project_form = False | |
st.rerun() | |
def _render_edit_project_form(self): | |
"""عرض نموذج تعديل المشروع""" | |
project = None | |
for p in st.session_state.projects: | |
if p['id'] == st.session_state.edit_project_id: | |
project = p | |
break | |
if not project: | |
st.session_state.show_edit_project_form = False | |
st.rerun() | |
return | |
st.subheader(f"تعديل المشروع: {project['name']}") | |
with st.form(key="edit_project_form"): | |
name = st.text_input("اسم المشروع", value=project['name'], key="edit_project_name") | |
client = st.text_input("العميل", value=project['client'], key="edit_project_client") | |
estimated_value = st.number_input( | |
"القيمة التقديرية", | |
min_value=0.0, | |
value=float(project['estimated_value']), | |
format="%f", | |
key="edit_project_value" | |
) | |
deadline = st.date_input( | |
"الموعد النهائي", | |
value=datetime.strptime(project['deadline'], "%Y-%m-%d").date(), | |
key="edit_project_deadline" | |
) | |
status = st.selectbox( | |
"الحالة", | |
["قيد التسعير", "تم التسعير", "تم التقديم", "فائز", "خاسر"], | |
index=["قيد التسعير", "تم التسعير", "تم التقديم", "فائز", "خاسر"].index(project['status']), | |
key="edit_project_status" | |
) | |
pricing_type = st.selectbox( | |
"نوع التسعير", | |
["قياسي", "غير متزن", "موجه ربحية", "تنافسي", "استراتيجي"], | |
index=["قياسي", "غير متزن", "موجه ربحية", "تنافسي", "استراتيجي"].index(project.get('pricing_type', 'قياسي')), | |
key="edit_project_pricing_type" | |
) | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
submit_button = st.form_submit_button("حفظ") | |
with col2: | |
cancel_button = st.form_submit_button("إلغاء") | |
with col3: | |
delete_button = st.form_submit_button("حذف المشروع", type="primary") | |
if submit_button: | |
if name and client and estimated_value > 0: | |
for i, p in enumerate(st.session_state.projects): | |
if p['id'] == st.session_state.edit_project_id: | |
st.session_state.projects[i]['name'] = name | |
st.session_state.projects[i]['client'] = client | |
st.session_state.projects[i]['estimated_value'] = estimated_value | |
st.session_state.projects[i]['deadline'] = deadline.strftime("%Y-%m-%d") | |
st.session_state.projects[i]['status'] = status | |
st.session_state.projects[i]['pricing_type'] = pricing_type | |
break | |
st.session_state.show_edit_project_form = False | |
st.rerun() | |
if cancel_button: | |
st.session_state.show_edit_project_form = False | |
st.rerun() | |
if delete_button: | |
for i, p in enumerate(st.session_state.projects): | |
if p['id'] == st.session_state.edit_project_id: | |
st.session_state.projects.pop(i) | |
break | |
# حذف بنود جدول الكميات المرتبطة بالمشروع | |
st.session_state.boq_items = [item for item in st.session_state.boq_items if item['project_id'] != st.session_state.edit_project_id] | |
st.session_state.show_edit_project_form = False | |
if st.session_state.projects: | |
st.session_state.current_project = st.session_state.projects[0]['id'] | |
else: | |
st.session_state.current_project = None | |
st.rerun() | |
def _render_projects_list(self): | |
"""عرض قائمة المشاريع""" | |
st.subheader("قائمة المشاريع") | |
if not st.session_state.projects: | |
st.info("لا توجد مشاريع. قم بإنشاء مشروع جديد للبدء.") | |
return | |
# إنشاء DataFrame من قائمة المشاريع | |
df = pd.DataFrame(st.session_state.projects) | |
if len(df) > 0 and 'id' in df.columns: | |
df = df[['id', 'name', 'client', 'estimated_value', 'deadline', 'status', 'pricing_type']] | |
df.columns = ['الرقم', 'اسم المشروع', 'العميل', 'القيمة التقديرية', 'الموعد النهائي', 'الحالة', 'نوع التسعير'] | |
# تنسيق القيمة التقديرية | |
df['القيمة التقديرية'] = df['القيمة التقديرية'].apply(lambda x: f"{x:,.2f} ريال") | |
# عرض الجدول | |
st.dataframe(df, use_container_width=True) | |
# اختيار المشروع | |
col1, col2 = st.columns(2) | |
with col1: | |
project_ids = [p['id'] for p in st.session_state.projects] | |
if st.session_state.current_project in project_ids: | |
current_index = project_ids.index(st.session_state.current_project) | |
else: | |
current_index = 0 if project_ids else None | |
if current_index is not None and project_ids: | |
selected_project_id = st.selectbox( | |
"اختر المشروع", | |
options=project_ids, | |
format_func=lambda x: next((p['name'] for p in st.session_state.projects if p['id'] == x), ""), | |
index=current_index, | |
key="select_project" | |
) | |
if selected_project_id != st.session_state.current_project: | |
st.session_state.current_project = selected_project_id | |
st.rerun() | |
with col2: | |
if st.button("تعديل المشروع", key="edit_project_btn"): | |
st.session_state.edit_project_id = st.session_state.current_project | |
st.session_state.show_edit_project_form = True | |
st.rerun() | |
else: | |
st.info("لا توجد مشاريع. قم بإنشاء مشروع جديد للبدء.") | |
def _get_current_project_info(self): | |
"""الحصول على معلومات المشروع الحالي""" | |
for project in st.session_state.projects: | |
if project['id'] == st.session_state.current_project: | |
return project | |
return None | |
def _render_project_info(self, project): | |
"""عرض معلومات المشروع""" | |
st.header(f"تسعير: {project['name']}") | |
col1, col2, col3, col4 = st.columns(4) | |
with col1: | |
st.metric("العميل", project['client']) | |
with col2: | |
st.metric("القيمة التقديرية", f"{project['estimated_value']:,.2f} ريال") | |
with col3: | |
st.metric("الموعد النهائي", project['deadline']) | |
with col4: | |
st.metric("نوع التسعير", project['pricing_type']) | |
def _render_bill_of_quantities(self): | |
"""عرض جدول الكميات""" | |
st.subheader("جدول الكميات") | |
# زر إضافة بند جديد | |
col1, col2, col3 = st.columns([1, 1, 2]) | |
with col1: | |
if st.button("➕ إضافة بند جديد", key="add_boq_item_btn"): | |
st.session_state.show_new_boq_item_form = True | |
st.session_state.show_resource_selector = False | |
with col2: | |
if st.button("📋 سحب من الموارد", key="add_from_resources_btn"): | |
st.session_state.show_resource_selector = True | |
st.session_state.show_new_boq_item_form = False | |
# عرض نموذج إضافة بند جديد | |
if st.session_state.show_new_boq_item_form: | |
self._render_new_boq_item_form() | |
# عرض نموذج تعديل البند | |
if st.session_state.show_edit_boq_item_form and st.session_state.edit_boq_item_id is not None: | |
self._render_edit_boq_item_form() | |
# عرض محدد الموارد | |
if st.session_state.show_resource_selector: | |
self._render_resource_selector() | |
# الحصول على بنود المشروع الحالي | |
project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] | |
if not project_items: | |
st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") | |
return | |
# إنشاء DataFrame من بنود المشروع | |
df = pd.DataFrame(project_items) | |
df = df[['id', 'code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'resource_type']] | |
# تحويل الجدول إلى جدول قابل للتعديل | |
edited_df = st.data_editor( | |
df, | |
column_config={ | |
"id": st.column_config.Column("الرقم", disabled=True), | |
"code": st.column_config.Column("الكود"), | |
"description": st.column_config.Column("الوصف"), | |
"unit": st.column_config.Column("الوحدة"), | |
"quantity": st.column_config.NumberColumn("الكمية", min_value=0.0, format="%.2f", step=0.1), | |
"unit_price": st.column_config.NumberColumn("سعر الوحدة", min_value=0.0, format="%.2f ريال", step=0.1), | |
"total_price": st.column_config.NumberColumn("السعر الإجمالي", format="%.2f ريال", disabled=True), | |
"resource_type": st.column_config.SelectboxColumn("نوع المورد", options=["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"]) | |
}, | |
use_container_width=True, | |
key="edit_boq_items" | |
) | |
# تحديث البيانات في حالة التعديل | |
if edited_df is not None and not edited_df.equals(df): | |
for i, row in edited_df.iterrows(): | |
item_id = row['id'] | |
for j, item in enumerate(st.session_state.boq_items): | |
if item['id'] == item_id: | |
st.session_state.boq_items[j]['code'] = row['code'] | |
st.session_state.boq_items[j]['description'] = row['description'] | |
st.session_state.boq_items[j]['unit'] = row['unit'] | |
st.session_state.boq_items[j]['quantity'] = row['quantity'] | |
st.session_state.boq_items[j]['unit_price'] = row['unit_price'] | |
st.session_state.boq_items[j]['total_price'] = row['quantity'] * row['unit_price'] | |
st.session_state.boq_items[j]['resource_type'] = row['resource_type'] | |
break | |
st.success("تم تحديث جدول الكميات بنجاح") | |
st.rerun() | |
# حساب المجموع الكلي | |
total_price = sum(item['total_price'] for item in project_items) | |
st.metric("إجمالي جدول الكميات", f"{total_price:,.2f} ريال") | |
# أزرار التصدير والتعديل | |
col1, col2 = st.columns(2) | |
with col1: | |
if st.button("تصدير جدول الكميات", key="export_boq_btn_1"): | |
# إنشاء CSV للتصدير | |
export_df = pd.DataFrame(project_items) | |
export_df = export_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'resource_type']] | |
export_df.columns = ['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي', 'نوع المورد'] | |
csv = export_df.to_csv(index=False) | |
b64 = base64.b64encode(csv.encode()).decode() | |
href = f'<a href="data:file/csv;base64,{b64}" download="boq_{st.session_state.current_project}.csv">تحميل CSV</a>' | |
st.markdown(href, unsafe_allow_html=True) | |
with col2: | |
if len(project_items) > 0: | |
selected_item_id = st.selectbox( | |
"اختر بند للتعديل", | |
options=[item['id'] for item in project_items], | |
format_func=lambda x: next((item['description'] for item in project_items if item['id'] == x), ""), | |
key="select_boq_item" | |
) | |
if st.button("تعديل البند", key="edit_boq_item_btn"): | |
st.session_state.edit_boq_item_id = selected_item_id | |
st.session_state.show_edit_boq_item_form = True | |
st.rerun() | |
def _render_new_boq_item_form(self): | |
"""عرض نموذج إضافة بند جديد""" | |
st.subheader("إضافة بند جديد") | |
with st.form(key="new_boq_item_form"): | |
code = st.text_input("الكود", key="new_boq_item_code") | |
description = st.text_input("الوصف", key="new_boq_item_description") | |
col1, col2 = st.columns(2) | |
with col1: | |
unit = st.text_input("الوحدة", key="new_boq_item_unit") | |
with col2: | |
resource_type = st.selectbox( | |
"نوع المورد", | |
["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"], | |
key="new_boq_item_resource_type" | |
) | |
col3, col4 = st.columns(2) | |
with col3: | |
quantity = st.number_input("الكمية", min_value=0.0, format="%f", key="new_boq_item_quantity") | |
with col4: | |
unit_price = st.number_input("سعر الوحدة", min_value=0.0, format="%f", key="new_boq_item_unit_price") | |
total_price = quantity * unit_price | |
st.metric("السعر الإجمالي", f"{total_price:,.2f} ريال") | |
col5, col6 = st.columns(2) | |
with col5: | |
submit_button = st.form_submit_button("حفظ") | |
with col6: | |
cancel_button = st.form_submit_button("إلغاء") | |
if submit_button: | |
if code and description and unit and quantity > 0 and unit_price > 0: | |
new_item = { | |
'id': st.session_state.next_boq_item_id, | |
'project_id': st.session_state.current_project, | |
'code': code, | |
'description': description, | |
'unit': unit, | |
'quantity': quantity, | |
'unit_price': unit_price, | |
'total_price': total_price, | |
'resource_type': resource_type | |
} | |
st.session_state.boq_items.append(new_item) | |
st.session_state.next_boq_item_id += 1 | |
st.session_state.show_new_boq_item_form = False | |
st.rerun() | |
if cancel_button: | |
st.session_state.show_new_boq_item_form = False | |
st.rerun() | |
def _render_edit_boq_item_form(self): | |
"""عرض نموذج تعديل البند""" | |
item = None | |
for i in st.session_state.boq_items: | |
if i['id'] == st.session_state.edit_boq_item_id: | |
item = i | |
break | |
if not item: | |
st.session_state.show_edit_boq_item_form = False | |
st.rerun() | |
return | |
st.subheader(f"تعديل البند: {item['description']}") | |
with st.form(key="edit_boq_item_form"): | |
code = st.text_input("الكود", value=item['code'], key="edit_boq_item_code") | |
description = st.text_input("الوصف", value=item['description'], key="edit_boq_item_description") | |
col1, col2 = st.columns(2) | |
with col1: | |
unit = st.text_input("الوحدة", value=item['unit'], key="edit_boq_item_unit") | |
with col2: | |
resource_type = st.selectbox( | |
"نوع المورد", | |
["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"], | |
index=["مواد", "عمالة", "معدات", "مقاولين من الباطن", "أخرى"].index(item['resource_type']) if 'resource_type' in item else 0, | |
key="edit_boq_item_resource_type" | |
) | |
col3, col4 = st.columns(2) | |
with col3: | |
quantity = st.number_input("الكمية", min_value=0.0, value=float(item['quantity']), format="%f", key="edit_boq_item_quantity") | |
with col4: | |
unit_price = st.number_input("سعر الوحدة", min_value=0.0, value=float(item['unit_price']), format="%f", key="edit_boq_item_unit_price") | |
total_price = quantity * unit_price | |
st.metric("السعر الإجمالي", f"{total_price:,.2f} ريال") | |
col5, col6, col7 = st.columns(3) | |
with col5: | |
submit_button = st.form_submit_button("حفظ") | |
with col6: | |
cancel_button = st.form_submit_button("إلغاء") | |
with col7: | |
delete_button = st.form_submit_button("حذف البند", type="primary") | |
if submit_button: | |
if code and description and unit and quantity > 0 and unit_price > 0: | |
for i, itm in enumerate(st.session_state.boq_items): | |
if itm['id'] == st.session_state.edit_boq_item_id: | |
st.session_state.boq_items[i]['code'] = code | |
st.session_state.boq_items[i]['description'] = description | |
st.session_state.boq_items[i]['unit'] = unit | |
st.session_state.boq_items[i]['quantity'] = quantity | |
st.session_state.boq_items[i]['unit_price'] = unit_price | |
st.session_state.boq_items[i]['total_price'] = total_price | |
st.session_state.boq_items[i]['resource_type'] = resource_type | |
break | |
st.session_state.show_edit_boq_item_form = False | |
st.rerun() | |
if cancel_button: | |
st.session_state.show_edit_boq_item_form = False | |
st.rerun() | |
if delete_button: | |
for i, itm in enumerate(st.session_state.boq_items): | |
if itm['id'] == st.session_state.edit_boq_item_id: | |
st.session_state.boq_items.pop(i) | |
break | |
st.session_state.show_edit_boq_item_form = False | |
st.rerun() | |
def _render_resource_selector(self): | |
"""عرض محدد الموارد""" | |
st.subheader("سحب من الموارد المسجلة") | |
# اختيار نوع المورد | |
resource_type = st.selectbox( | |
"نوع المورد", | |
["المواد", "العمالة", "المعدات", "المقاولين من الباطن"], | |
index=["المواد", "العمالة", "المعدات", "المقاولين من الباطن"].index(st.session_state.selected_resource_type), | |
key="resource_type_selector" | |
) | |
if resource_type != st.session_state.selected_resource_type: | |
st.session_state.selected_resource_type = resource_type | |
st.rerun() | |
# الحصول على الموارد المناسبة | |
resources = [] | |
if resource_type == "المواد" and 'materials' in st.session_state: | |
resources = st.session_state.materials | |
elif resource_type == "العمالة" and 'labor' in st.session_state: | |
resources = st.session_state.labor | |
elif resource_type == "المعدات" and 'equipment' in st.session_state: | |
resources = st.session_state.equipment | |
elif resource_type == "المقاولين من الباطن" and 'subcontractors' in st.session_state: | |
resources = st.session_state.subcontractors | |
if not resources: | |
st.info(f"لا توجد موارد مسجلة من نوع {resource_type}. يرجى إضافة موارد في وحدة الموارد أولاً.") | |
col1, col2 = st.columns(2) | |
with col2: | |
if st.button("إلغاء", key="cancel_resource_selector"): | |
st.session_state.show_resource_selector = False | |
st.rerun() | |
return | |
# إنشاء DataFrame من الموارد | |
df = pd.DataFrame(resources) | |
if 'id' in df.columns and 'name' in df.columns and 'unit' in df.columns and 'price' in df.columns: | |
df = df[['id', 'name', 'category', 'unit', 'price']] | |
df.columns = ['الرقم', 'الاسم', 'الفئة', 'الوحدة', 'السعر'] | |
# تنسيق السعر | |
df['السعر'] = df['السعر'].apply(lambda x: f"{x:,.2f} ريال") | |
# عرض الجدول | |
st.dataframe(df, use_container_width=True) | |
with st.form(key="add_from_resource_form"): | |
col1, col2 = st.columns(2) | |
with col1: | |
selected_resource_id = st.selectbox( | |
"اختر المورد", | |
options=[r['id'] for r in resources], | |
format_func=lambda x: next((r['name'] for r in resources if r['id'] == x), ""), | |
key="select_resource" | |
) | |
with col2: | |
quantity = st.number_input("الكمية", min_value=0.1, value=1.0, format="%f", key="resource_quantity") | |
# الحصول على المورد المحدد | |
selected_resource = next((r for r in resources if r['id'] == selected_resource_id), None) | |
if selected_resource: | |
total_price = quantity * selected_resource['price'] | |
st.metric("السعر الإجمالي", f"{total_price:,.2f} ريال") | |
col3, col4 = st.columns(2) | |
with col3: | |
submit_button = st.form_submit_button("إضافة إلى جدول الكميات") | |
with col4: | |
cancel_button = st.form_submit_button("إلغاء") | |
if submit_button and selected_resource and quantity > 0: | |
# تحويل نوع المورد إلى الصيغة المناسبة | |
resource_type_map = { | |
"المواد": "مواد", | |
"العمالة": "عمالة", | |
"المعدات": "معدات", | |
"المقاولين من الباطن": "مقاولين من الباطن" | |
} | |
# إنشاء كود فريد | |
resource_code_prefix = { | |
"المواد": "M", | |
"العمالة": "L", | |
"المعدات": "E", | |
"المقاولين من الباطن": "S" | |
} | |
code_prefix = resource_code_prefix.get(resource_type, "X") | |
code = f"{code_prefix}-{selected_resource['id']:03d}" | |
# إضافة البند إلى جدول الكميات | |
new_item = { | |
'id': st.session_state.next_boq_item_id, | |
'project_id': st.session_state.current_project, | |
'code': code, | |
'description': selected_resource['name'], | |
'unit': selected_resource['unit'], | |
'quantity': quantity, | |
'unit_price': selected_resource['price'], | |
'total_price': quantity * selected_resource['price'], | |
'resource_type': resource_type_map.get(resource_type, "أخرى"), | |
'resource_id': selected_resource['id'] | |
} | |
st.session_state.boq_items.append(new_item) | |
st.session_state.next_boq_item_id += 1 | |
st.session_state.show_resource_selector = False | |
st.rerun() | |
if cancel_button: | |
st.session_state.show_resource_selector = False | |
st.rerun() | |
else: | |
st.error("تنسيق بيانات الموارد غير صحيح. يرجى التأكد من وجود الحقول المطلوبة.") | |
if st.button("إلغاء", key="cancel_resource_selector_error"): | |
st.session_state.show_resource_selector = False | |
st.rerun() | |
def _render_item_price_analysis(self): | |
"""عرض تحليل سعر البند مع إمكانية التعديل والحفظ""" | |
st.subheader("تحليل سعر البند") | |
# الحصول على بنود المشروع الحالي | |
project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] | |
if not project_items: | |
st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") | |
return | |
# اختيار البند للتحليل | |
selected_item_id = st.selectbox( | |
"اختر البند للتحليل", | |
options=[item['id'] for item in project_items], | |
format_func=lambda x: next((item['description'] for item in project_items if item['id'] == x), ""), | |
key="select_item_for_analysis" | |
) | |
# الحصول على البند المحدد | |
selected_item = next((item for item in project_items if item['id'] == selected_item_id), None) | |
if not selected_item: | |
st.error("لم يتم العثور على البند المحدد.") | |
return | |
# عرض معلومات البند | |
col1, col2 = st.columns(2) | |
with col1: | |
st.write(f"**البند:** {selected_item['description']}") | |
st.write(f"**الكود:** {selected_item['code']}") | |
st.write(f"**الوحدة:** {selected_item['unit']}") | |
with col2: | |
st.write(f"**الكمية:** {selected_item['quantity']}") | |
st.write(f"**سعر الوحدة:** {selected_item['unit_price']:,.2f} ريال") | |
st.write(f"**السعر الإجمالي:** {selected_item['total_price']:,.2f} ريال") | |
# إنشاء تحليل سعر البند | |
item_analysis = self._generate_item_analysis(selected_item) | |
# عرض تحليل المواد مع إمكانية التعديل | |
self._render_materials_analysis(item_analysis) | |
# عرض تحليل المعدات مع إمكانية التعديل | |
self._render_equipment_analysis(item_analysis) | |
# عرض تحليل العمالة مع إمكانية التعديل | |
self._render_labor_analysis(item_analysis) | |
# عرض تحليل المقاولين من الباطن مع إمكانية التعديل | |
self._render_subcontractors_analysis(item_analysis) | |
# عرض ملخص التكلفة | |
self._render_cost_summary(item_analysis) | |
# زر حفظ التغييرات في جدول الكميات الرئيسي | |
if st.button("حفظ جميع التغييرات في جدول الكميات", key="save_all_changes", type="primary"): | |
# تحديث البند في جدول الكميات | |
self._update_boq_item_from_analysis(selected_item_id, item_analysis) | |
st.success("تم حفظ جميع التغييرات بنجاح في جدول الكميات الرئيسي") | |
st.session_state.item_analysis_edited = False | |
st.rerun() | |
def _generate_item_analysis(self, item): | |
"""إنشاء تحليل سعر البند""" | |
# في هذه النسخة التجريبية، سنقوم بإنشاء بيانات عشوائية للتحليل | |
# في النسخة النهائية، يجب استخدام بيانات حقيقية من قاعدة البيانات | |
unit_price = item['unit_price'] | |
# تقسيم سعر الوحدة إلى مكوناته | |
materials_cost = unit_price * random.uniform(0.4, 0.6) | |
equipment_cost = unit_price * random.uniform(0.1, 0.2) | |
labor_cost = unit_price * random.uniform(0.1, 0.2) | |
overhead_cost = unit_price * random.uniform(0.05, 0.1) | |
profit = unit_price - (materials_cost + equipment_cost + labor_cost + overhead_cost) | |
# إنشاء قائمة المواد | |
materials = [ | |
{ | |
'name': 'خرسانة جاهزة' if 'خرسانة' in item['description'].lower() else 'حديد تسليح' if 'حديد' in item['description'].lower() else 'رمل', | |
'unit': 'م3' if 'خرسانة' in item['description'].lower() else 'طن' if 'حديد' in item['description'].lower() else 'م3', | |
'quantity': random.uniform(0.5, 1.5), | |
'unit_price': materials_cost * 0.6, | |
'total_price': materials_cost * 0.6 * random.uniform(0.5, 1.5) | |
}, | |
{ | |
'name': 'إسمنت', | |
'unit': 'طن', | |
'quantity': random.uniform(0.2, 0.5), | |
'unit_price': materials_cost * 0.3, | |
'total_price': materials_cost * 0.3 * random.uniform(0.2, 0.5) | |
}, | |
{ | |
'name': 'مواد أخرى', | |
'unit': 'مجموعة', | |
'quantity': 1, | |
'unit_price': materials_cost * 0.1, | |
'total_price': materials_cost * 0.1 | |
} | |
] | |
# إنشاء قائمة المعدات | |
equipment = [ | |
{ | |
'name': 'خلاطة خرسانة' if 'خرسانة' in item['description'].lower() else 'حفارة' if 'حفر' in item['description'].lower() else 'رافعة', | |
'unit': 'يوم', | |
'quantity': random.uniform(1, 3), | |
'unit_price': equipment_cost * 0.7, | |
'total_price': equipment_cost * 0.7 * random.uniform(1, 3) | |
}, | |
{ | |
'name': 'معدات أخرى', | |
'unit': 'يوم', | |
'quantity': random.uniform(1, 2), | |
'unit_price': equipment_cost * 0.3, | |
'total_price': equipment_cost * 0.3 * random.uniform(1, 2) | |
} | |
] | |
# إنشاء قائمة العمالة | |
labor = [ | |
{ | |
'name': 'عامل فني', | |
'unit': 'يوم', | |
'quantity': random.uniform(2, 5), | |
'unit_price': labor_cost * 0.6, | |
'total_price': labor_cost * 0.6 * random.uniform(2, 5) | |
}, | |
{ | |
'name': 'عامل عادي', | |
'unit': 'يوم', | |
'quantity': random.uniform(3, 8), | |
'unit_price': labor_cost * 0.4, | |
'total_price': labor_cost * 0.4 * random.uniform(3, 8) | |
} | |
] | |
# إنشاء قائمة المقاولين من الباطن | |
subcontractors = [ | |
{ | |
'name': 'مقاول أعمال خرسانية' if 'خرسانة' in item['description'].lower() else 'مقاول أعمال حفر' if 'حفر' in item['description'].lower() else 'مقاول عام', | |
'unit': 'عقد', | |
'quantity': 1, | |
'unit_price': unit_price * 0.15, | |
'total_price': unit_price * 0.15 | |
} | |
] | |
# حساب إجمالي التكاليف | |
total_materials_cost = sum(material['total_price'] for material in materials) | |
total_equipment_cost = sum(equipment_item['total_price'] for equipment_item in equipment) | |
total_labor_cost = sum(labor_item['total_price'] for labor_item in labor) | |
total_subcontractors_cost = sum(subcontractor['total_price'] for subcontractor in subcontractors) | |
# تعديل الربح ليكون الفرق بين سعر الوحدة وإجمالي التكاليف | |
total_cost = total_materials_cost + total_equipment_cost + total_labor_cost + total_subcontractors_cost + overhead_cost | |
profit = unit_price - total_cost | |
return { | |
'item': item, | |
'materials': materials, | |
'equipment': equipment, | |
'labor': labor, | |
'subcontractors': subcontractors, | |
'total_materials_cost': total_materials_cost, | |
'total_equipment_cost': total_equipment_cost, | |
'total_labor_cost': total_labor_cost, | |
'total_subcontractors_cost': total_subcontractors_cost, | |
'overhead_cost': overhead_cost, | |
'profit': profit, | |
'unit_price': unit_price | |
} | |
def _render_materials_analysis(self, item_analysis): | |
"""عرض تحليل المواد مع إمكانية التعديل""" | |
st.subheader("تحليل المواد") | |
if not item_analysis['materials']: | |
st.info("لا توجد مواد في تحليل هذا البند.") | |
# إضافة زر لإضافة مواد جديدة | |
if st.button("إضافة مواد", key="add_first_material"): | |
item_analysis['materials'] = [{ | |
'name': 'مادة جديدة', | |
'unit': 'وحدة', | |
'quantity': 1.0, | |
'unit_price': 0.0, | |
'total_price': 0.0 | |
}] | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
return | |
# إنشاء DataFrame من قائمة المواد | |
df = pd.DataFrame(item_analysis['materials']) | |
df.columns = ['المادة', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] | |
# تحويل الجدول إلى جدول قابل للتعديل | |
edited_df = st.data_editor( | |
df, | |
use_container_width=True, | |
key="edit_materials_table", | |
column_config={ | |
"المادة": st.column_config.Column("المادة"), | |
"الوحدة": st.column_config.Column("الوحدة"), | |
"الكمية": st.column_config.NumberColumn( | |
"الكمية", | |
min_value=0.0, | |
format="%.2f", | |
step=0.1, | |
), | |
"سعر الوحدة": st.column_config.NumberColumn( | |
"سعر الوحدة", | |
min_value=0.0, | |
format="%.2f ريال", | |
step=0.1, | |
), | |
"السعر الإجمالي": st.column_config.NumberColumn( | |
"السعر الإجمالي", | |
format="%.2f ريال", | |
disabled=True, | |
), | |
}, | |
num_rows="dynamic" | |
) | |
# تحديث البيانات في item_analysis بناءً على التعديلات | |
if edited_df is not None and not edited_df.equals(df): | |
# حذف جميع المواد الحالية | |
item_analysis['materials'] = [] | |
# إضافة المواد المعدلة | |
for i, row in edited_df.iterrows(): | |
# حساب السعر الإجمالي | |
total_price = row['الكمية'] * row['سعر الوحدة'] | |
# إضافة المادة | |
item_analysis['materials'].append({ | |
'name': row['المادة'], | |
'unit': row['الوحدة'], | |
'quantity': row['الكمية'], | |
'unit_price': row['سعر الوحدة'], | |
'total_price': total_price | |
}) | |
# إعادة حساب إجمالي تكلفة المواد | |
item_analysis['total_materials_cost'] = sum(material['total_price'] for material in item_analysis['materials']) | |
# تعيين علامة التعديل | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
# عرض إجمالي تكلفة المواد | |
st.metric("إجمالي تكلفة المواد", f"{item_analysis['total_materials_cost']:,.2f} ريال") | |
# أزرار التحكم | |
col1, col2 = st.columns(2) | |
with col1: | |
if st.button("إضافة مادة جديدة", key="add_material"): | |
item_analysis['materials'].append({ | |
'name': 'مادة جديدة', | |
'unit': 'وحدة', | |
'quantity': 1.0, | |
'unit_price': 0.0, | |
'total_price': 0.0 | |
}) | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
def _render_equipment_analysis(self, item_analysis): | |
"""عرض تحليل المعدات مع إمكانية التعديل""" | |
st.subheader("تحليل المعدات") | |
if not item_analysis['equipment']: | |
st.info("لا توجد معدات في تحليل هذا البند.") | |
# إضافة زر لإضافة معدات جديدة | |
if st.button("إضافة معدات", key="add_first_equipment"): | |
item_analysis['equipment'] = [{ | |
'name': 'معدة جديدة', | |
'unit': 'يوم', | |
'quantity': 1.0, | |
'unit_price': 0.0, | |
'total_price': 0.0 | |
}] | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
return | |
# إنشاء DataFrame من قائمة المعدات | |
df = pd.DataFrame(item_analysis['equipment']) | |
df.columns = ['المعدة', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] | |
# تحويل الجدول إلى جدول قابل للتعديل | |
edited_df = st.data_editor( | |
df, | |
use_container_width=True, | |
key="edit_equipment_table", | |
column_config={ | |
"المعدة": st.column_config.Column("المعدة"), | |
"الوحدة": st.column_config.Column("الوحدة"), | |
"الكمية": st.column_config.NumberColumn( | |
"الكمية", | |
min_value=0.0, | |
format="%.2f", | |
step=0.1, | |
), | |
"سعر الوحدة": st.column_config.NumberColumn( | |
"سعر الوحدة", | |
min_value=0.0, | |
format="%.2f ريال", | |
step=0.1, | |
), | |
"السعر الإجمالي": st.column_config.NumberColumn( | |
"السعر الإجمالي", | |
format="%.2f ريال", | |
disabled=True, | |
), | |
}, | |
num_rows="dynamic" | |
) | |
# تحديث البيانات في item_analysis بناءً على التعديلات | |
if edited_df is not None and not edited_df.equals(df): | |
# حذف جميع المعدات الحالية | |
item_analysis['equipment'] = [] | |
# إضافة المعدات المعدلة | |
for i, row in edited_df.iterrows(): | |
# حساب السعر الإجمالي | |
total_price = row['الكمية'] * row['سعر الوحدة'] | |
# إضافة المعدة | |
item_analysis['equipment'].append({ | |
'name': row['المعدة'], | |
'unit': row['الوحدة'], | |
'quantity': row['الكمية'], | |
'unit_price': row['سعر الوحدة'], | |
'total_price': total_price | |
}) | |
# إعادة حساب إجمالي تكلفة المعدات | |
item_analysis['total_equipment_cost'] = sum(equipment_item['total_price'] for equipment_item in item_analysis['equipment']) | |
# تعيين علامة التعديل | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
# عرض إجمالي تكلفة المعدات | |
st.metric("إجمالي تكلفة المعدات", f"{item_analysis['total_equipment_cost']:,.2f} ريال") | |
# أزرار التحكم | |
col1, col2 = st.columns(2) | |
with col1: | |
if st.button("إضافة معدة جديدة", key="add_equipment"): | |
item_analysis['equipment'].append({ | |
'name': 'معدة جديدة', | |
'unit': 'يوم', | |
'quantity': 1.0, | |
'unit_price': 0.0, | |
'total_price': 0.0 | |
}) | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
def _render_labor_analysis(self, item_analysis): | |
"""عرض تحليل العمالة مع إمكانية التعديل""" | |
st.subheader("تحليل العمالة") | |
if not item_analysis['labor']: | |
st.info("لا توجد عمالة في تحليل هذا البند.") | |
# إضافة زر لإضافة عمالة جديدة | |
if st.button("إضافة عمالة", key="add_first_labor"): | |
item_analysis['labor'] = [{ | |
'name': 'عامل جديد', | |
'unit': 'يوم', | |
'quantity': 1.0, | |
'unit_price': 0.0, | |
'total_price': 0.0 | |
}] | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
return | |
# إنشاء DataFrame من قائمة العمالة | |
df = pd.DataFrame(item_analysis['labor']) | |
df.columns = ['العامل', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] | |
# تحويل الجدول إلى جدول قابل للتعديل | |
edited_df = st.data_editor( | |
df, | |
use_container_width=True, | |
key="edit_labor_table", | |
column_config={ | |
"العامل": st.column_config.Column("العامل"), | |
"الوحدة": st.column_config.Column("الوحدة"), | |
"الكمية": st.column_config.NumberColumn( | |
"الكمية", | |
min_value=0.0, | |
format="%.2f", | |
step=0.1, | |
), | |
"سعر الوحدة": st.column_config.NumberColumn( | |
"سعر الوحدة", | |
min_value=0.0, | |
format="%.2f ريال", | |
step=0.1, | |
), | |
"السعر الإجمالي": st.column_config.NumberColumn( | |
"السعر الإجمالي", | |
format="%.2f ريال", | |
disabled=True, | |
), | |
}, | |
num_rows="dynamic" | |
) | |
# تحديث البيانات في item_analysis بناءً على التعديلات | |
if edited_df is not None and not edited_df.equals(df): | |
# حذف جميع العمالة الحالية | |
item_analysis['labor'] = [] | |
# إضافة العمالة المعدلة | |
for i, row in edited_df.iterrows(): | |
# حساب السعر الإجمالي | |
total_price = row['الكمية'] * row['سعر الوحدة'] | |
# إضافة العامل | |
item_analysis['labor'].append({ | |
'name': row['العامل'], | |
'unit': row['الوحدة'], | |
'quantity': row['الكمية'], | |
'unit_price': row['سعر الوحدة'], | |
'total_price': total_price | |
}) | |
# إعادة حساب إجمالي تكلفة العمالة | |
item_analysis['total_labor_cost'] = sum(labor_item['total_price'] for labor_item in item_analysis['labor']) | |
# تعيين علامة التعديل | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
# عرض إجمالي تكلفة العمالة | |
st.metric("إجمالي تكلفة العمالة", f"{item_analysis['total_labor_cost']:,.2f} ريال") | |
# أزرار التحكم | |
col1, col2 = st.columns(2) | |
with col1: | |
if st.button("إضافة عامل جديد", key="add_labor"): | |
item_analysis['labor'].append({ | |
'name': 'عامل جديد', | |
'unit': 'يوم', | |
'quantity': 1.0, | |
'unit_price': 0.0, | |
'total_price': 0.0 | |
}) | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
def _render_subcontractors_analysis(self, item_analysis): | |
"""عرض تحليل المقاولين من الباطن مع إمكانية التعديل""" | |
st.subheader("تحليل المقاولين من الباطن") | |
# التحقق من وجود مفتاح المقاولين من الباطن في التحليل | |
if 'subcontractors' not in item_analysis: | |
item_analysis['subcontractors'] = [] | |
item_analysis['total_subcontractors_cost'] = 0 | |
if not item_analysis['subcontractors']: | |
st.info("لا يوجد مقاولين من الباطن في تحليل هذا البند.") | |
# إضافة زر لإضافة مقاول جديد | |
if st.button("إضافة مقاول من الباطن", key="add_first_subcontractor"): | |
item_analysis['subcontractors'] = [{ | |
'name': 'مقاول جديد', | |
'unit': 'عقد', | |
'quantity': 1.0, | |
'unit_price': 0.0, | |
'total_price': 0.0 | |
}] | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
return | |
# إنشاء DataFrame من قائمة المقاولين | |
df = pd.DataFrame(item_analysis['subcontractors']) | |
df.columns = ['المقاول', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] | |
# تحويل الجدول إلى جدول قابل للتعديل | |
edited_df = st.data_editor( | |
df, | |
use_container_width=True, | |
key="edit_subcontractors_table", | |
column_config={ | |
"المقاول": st.column_config.Column("المقاول"), | |
"الوحدة": st.column_config.Column("الوحدة"), | |
"الكمية": st.column_config.NumberColumn( | |
"الكمية", | |
min_value=0.0, | |
format="%.2f", | |
step=0.1, | |
), | |
"سعر الوحدة": st.column_config.NumberColumn( | |
"سعر الوحدة", | |
min_value=0.0, | |
format="%.2f ريال", | |
step=0.1, | |
), | |
"السعر الإجمالي": st.column_config.NumberColumn( | |
"السعر الإجمالي", | |
format="%.2f ريال", | |
disabled=True, | |
), | |
}, | |
num_rows="dynamic" | |
) | |
# تحديث البيانات في item_analysis بناءً على التعديلات | |
if edited_df is not None and not edited_df.equals(df): | |
# حذف جميع المقاولين الحاليين | |
item_analysis['subcontractors'] = [] | |
# إضافة المقاولين المعدلين | |
for i, row in edited_df.iterrows(): | |
# حساب السعر الإجمالي | |
total_price = row['الكمية'] * row['سعر الوحدة'] | |
# إضافة المقاول | |
item_analysis['subcontractors'].append({ | |
'name': row['المقاول'], | |
'unit': row['الوحدة'], | |
'quantity': row['الكمية'], | |
'unit_price': row['سعر الوحدة'], | |
'total_price': total_price | |
}) | |
# إعادة حساب إجمالي تكلفة المقاولين | |
item_analysis['total_subcontractors_cost'] = sum(subcontractor['total_price'] for subcontractor in item_analysis['subcontractors']) | |
# تعيين علامة التعديل | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
# عرض إجمالي تكلفة المقاولين | |
st.metric("إجمالي تكلفة المقاولين من الباطن", f"{item_analysis['total_subcontractors_cost']:,.2f} ريال") | |
# أزرار التحكم | |
col1, col2 = st.columns(2) | |
with col1: | |
if st.button("إضافة مقاول جديد", key="add_subcontractor"): | |
item_analysis['subcontractors'].append({ | |
'name': 'مقاول جديد', | |
'unit': 'عقد', | |
'quantity': 1.0, | |
'unit_price': 0.0, | |
'total_price': 0.0 | |
}) | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
def _setup_arabic_fonts(self): | |
"""إعداد الخطوط العربية للرسوم البيانية""" | |
plt.rcParams['font.family'] = 'Arial' | |
plt.rcParams['axes.unicode_minus'] = False | |
def _render_cost_summary(self, item_analysis): | |
"""عرض ملخص التكلفة""" | |
st.subheader("ملخص التكلفة") | |
# إعداد الخطوط العربية | |
self._setup_arabic_fonts() | |
# إنشاء بيانات الرسم البياني | |
# استخدام arabic_reshaper و bidi لمعالجة النص العربي | |
labels_ar = ['المواد', 'المعدات', 'العمالة', 'المقاولين من الباطن', 'المصاريف العمومية', 'الربح'] | |
labels = [get_display(arabic_reshaper.reshape(label)) for label in labels_ar] | |
# التحقق من وجود قيم NaN واستبدالها بأصفار | |
values = [ | |
item_analysis['total_materials_cost'] if not np.isnan(item_analysis['total_materials_cost']) else 0, | |
item_analysis['total_equipment_cost'] if not np.isnan(item_analysis['total_equipment_cost']) else 0, | |
item_analysis['total_labor_cost'] if not np.isnan(item_analysis['total_labor_cost']) else 0, | |
item_analysis['total_subcontractors_cost'] if not np.isnan(item_analysis.get('total_subcontractors_cost', 0)) else 0, | |
item_analysis['overhead_cost'] if not np.isnan(item_analysis['overhead_cost']) else 0, | |
item_analysis['profit'] if not np.isnan(item_analysis['profit']) else 0 | |
] | |
# التحقق من أن جميع القيم موجبة (أو صفر) لتجنب الأخطاء في الرسم البياني | |
values = [max(0, val) for val in values] | |
# إنشاء الرسم البياني | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
# رسم المخطط الدائري | |
wedges, texts, autotexts = ax.pie( | |
values, | |
labels=labels, | |
autopct='%1.1f%%', | |
startangle=90, | |
shadow=True, | |
) | |
# تعديل حجم النص | |
plt.setp(autotexts, size=10, weight="bold") | |
# إضافة العنوان | |
title_ar = 'توزيع تكلفة البند' | |
title = get_display(arabic_reshaper.reshape(title_ar)) | |
ax.set_title(title, fontsize=16) | |
# عرض الرسم البياني | |
st.pyplot(fig) | |
# عرض جدول ملخص التكلفة | |
cost_summary = { | |
'البند': ['المواد', 'المعدات', 'العمالة', 'المقاولين من الباطن', 'المصاريف العمومية', 'الربح', 'الإجمالي'], | |
'التكلفة': [ | |
item_analysis['total_materials_cost'], | |
item_analysis['total_equipment_cost'], | |
item_analysis['total_labor_cost'], | |
item_analysis.get('total_subcontractors_cost', 0), | |
item_analysis['overhead_cost'], | |
item_analysis['profit'], | |
item_analysis['unit_price'] | |
], | |
'النسبة': [ | |
item_analysis['total_materials_cost'] / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, | |
item_analysis['total_equipment_cost'] / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, | |
item_analysis['total_labor_cost'] / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, | |
item_analysis.get('total_subcontractors_cost', 0) / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, | |
item_analysis['overhead_cost'] / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, | |
item_analysis['profit'] / item_analysis['unit_price'] * 100 if item_analysis['unit_price'] > 0 else 0, | |
100 | |
] | |
} | |
df = pd.DataFrame(cost_summary) | |
df.columns = ['البند', 'التكلفة (ريال)', 'النسبة (%)'] | |
# تنسيق الأرقام | |
df['التكلفة (ريال)'] = df['التكلفة (ريال)'].apply(lambda x: f"{x:,.2f}") | |
df['النسبة (%)'] = df['النسبة (%)'].apply(lambda x: f"{x:.2f}%") | |
# عرض الجدول | |
st.dataframe(df, use_container_width=True) | |
# إضافة حقل لتعديل المصاريف العمومية | |
st.subheader("تعديل المصاريف العمومية والربح") | |
col1, col2 = st.columns(2) | |
with col1: | |
new_overhead = st.number_input( | |
"المصاريف العمومية (ريال)", | |
min_value=0.0, | |
value=float(item_analysis['overhead_cost']), | |
step=10.0, | |
format="%.2f", | |
key="edit_overhead_cost" | |
) | |
if new_overhead != item_analysis['overhead_cost']: | |
item_analysis['overhead_cost'] = new_overhead | |
# إعادة حساب الربح | |
total_cost = ( | |
item_analysis['total_materials_cost'] + | |
item_analysis['total_equipment_cost'] + | |
item_analysis['total_labor_cost'] + | |
item_analysis.get('total_subcontractors_cost', 0) + | |
item_analysis['overhead_cost'] | |
) | |
item_analysis['profit'] = item_analysis['unit_price'] - total_cost | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
with col2: | |
new_profit = st.number_input( | |
"الربح (ريال)", | |
min_value=float(-1000000.0), | |
value=float(item_analysis['profit']), | |
step=10.0, | |
format="%.2f", | |
key="edit_profit" | |
) | |
if new_profit != item_analysis['profit']: | |
item_analysis['profit'] = new_profit | |
# إعادة حساب سعر الوحدة | |
total_cost = ( | |
item_analysis['total_materials_cost'] + | |
item_analysis['total_equipment_cost'] + | |
item_analysis['total_labor_cost'] + | |
item_analysis.get('total_subcontractors_cost', 0) + | |
item_analysis['overhead_cost'] | |
) | |
item_analysis['unit_price'] = total_cost + item_analysis['profit'] | |
st.session_state.item_analysis_edited = True | |
st.rerun() | |
def _update_boq_item_from_analysis(self, item_id, item_analysis): | |
"""تحديث البند في جدول الكميات بناءً على التحليل""" | |
# حساب السعر الإجمالي الجديد | |
total_cost = ( | |
item_analysis['total_materials_cost'] + | |
item_analysis['total_equipment_cost'] + | |
item_analysis['total_labor_cost'] + | |
item_analysis.get('total_subcontractors_cost', 0) + | |
item_analysis['overhead_cost'] + | |
item_analysis['profit'] | |
) | |
# تحديث سعر الوحدة والسعر الإجمالي في البند | |
for i, item in enumerate(st.session_state.boq_items): | |
if item['id'] == item_id: | |
# حفظ الكمية الأصلية | |
quantity = item['quantity'] | |
# تحديث سعر الوحدة | |
unit_price = total_cost | |
# تحديث البند | |
st.session_state.boq_items[i]['unit_price'] = unit_price | |
st.session_state.boq_items[i]['total_price'] = unit_price * quantity | |
break | |
def _render_cost_analysis(self, project_info): | |
"""عرض تحليل التكلفة""" | |
st.subheader("تحليل التكلفة") | |
# الحصول على بنود المشروع الحالي | |
project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] | |
if not project_items: | |
st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") | |
return | |
# حساب إجمالي التكلفة | |
total_cost = sum(item['total_price'] for item in project_items) | |
# عرض إجمالي التكلفة | |
st.metric("إجمالي تكلفة المشروع", f"{total_cost:,.2f} ريال") | |
# إنشاء DataFrame من بنود المشروع | |
df = pd.DataFrame(project_items) | |
df = df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price']] | |
df.columns = ['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة', 'السعر الإجمالي'] | |
# تنسيق الأسعار | |
df['سعر الوحدة'] = df['سعر الوحدة'].apply(lambda x: f"{x:,.2f} ريال") | |
df['السعر الإجمالي'] = df['السعر الإجمالي'].apply(lambda x: f"{x:,.2f} ريال") | |
# عرض الجدول | |
st.dataframe(df, use_container_width=True) | |
# إنشاء رسم بياني للتكلفة | |
self._setup_arabic_fonts() | |
# إنشاء بيانات الرسم البياني | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
# تحويل الأسعار إلى أرقام | |
prices = [item['total_price'] for item in project_items] | |
labels = [item['description'] for item in project_items] | |
# تحويل النص العربي | |
labels = [get_display(arabic_reshaper.reshape(label)) for label in labels] | |
# رسم المخطط الشريطي | |
bars = ax.bar(labels, prices) | |
# تدوير التسميات | |
plt.xticks(rotation=45, ha='right') | |
# إضافة العنوان | |
title_ar = 'توزيع تكلفة المشروع حسب البنود' | |
title = get_display(arabic_reshaper.reshape(title_ar)) | |
ax.set_title(title, fontsize=16) | |
# إضافة التسميات | |
ax.set_ylabel(get_display(arabic_reshaper.reshape('التكلفة (ريال)'))) | |
# تنسيق الرسم البياني | |
plt.tight_layout() | |
# عرض الرسم البياني | |
st.pyplot(fig) | |
def _render_profit_margin(self, project_info): | |
"""عرض تحليل الربحية""" | |
st.subheader("تحليل الربحية") | |
# الحصول على بنود المشروع الحالي | |
project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] | |
if not project_items: | |
st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") | |
return | |
# حساب إجمالي التكلفة | |
total_cost = sum(item['total_price'] for item in project_items) | |
# حساب هامش الربح | |
profit_margin = 0.15 # افتراضي 15% | |
profit = total_cost * profit_margin | |
total_with_profit = total_cost + profit | |
# عرض معلومات الربحية | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
st.metric("إجمالي التكلفة", f"{total_cost:,.2f} ريال") | |
with col2: | |
st.metric("هامش الربح", f"{profit_margin:.1%}") | |
with col3: | |
st.metric("إجمالي مع الربح", f"{total_with_profit:,.2f} ريال") | |
# إضافة شريط تمرير لتعديل هامش الربح | |
new_profit_margin = st.slider( | |
"تعديل هامش الربح", | |
min_value=0.0, | |
max_value=0.5, | |
value=profit_margin, | |
step=0.01, | |
format="%.2f", | |
key="profit_margin_slider" | |
) | |
if new_profit_margin != profit_margin: | |
profit_margin = new_profit_margin | |
profit = total_cost * profit_margin | |
total_with_profit = total_cost + profit | |
st.metric("إجمالي مع الربح الجديد", f"{total_with_profit:,.2f} ريال") | |
# إنشاء رسم بياني للربحية | |
self._setup_arabic_fonts() | |
# إنشاء بيانات الرسم البياني | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
# إنشاء المخطط الدائري | |
labels_ar = ['التكلفة', 'الربح'] | |
labels = [get_display(arabic_reshaper.reshape(label)) for label in labels_ar] | |
values = [total_cost, profit] | |
# رسم المخطط الدائري | |
wedges, texts, autotexts = ax.pie( | |
values, | |
labels=labels, | |
autopct='%1.1f%%', | |
startangle=90, | |
shadow=True, | |
colors=['#ff9999', '#66b3ff'] | |
) | |
# تعديل حجم النص | |
plt.setp(autotexts, size=10, weight="bold") | |
# إضافة العنوان | |
title_ar = 'توزيع التكلفة والربح' | |
title = get_display(arabic_reshaper.reshape(title_ar)) | |
ax.set_title(title, fontsize=16) | |
# عرض الرسم البياني | |
st.pyplot(fig) | |
def _render_pricing_strategies(self, project_info): | |
"""عرض استراتيجيات التسعير""" | |
st.subheader("استراتيجيات التسعير") | |
# الحصول على بنود المشروع الحالي | |
project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] | |
if not project_items: | |
st.info("لا توجد بنود في جدول الكميات. قم بإضافة بنود للبدء.") | |
return | |
# عرض استراتيجيات التسعير المختلفة | |
st.write("### استراتيجيات التسعير المتاحة") | |
# استراتيجية التسعير القياسية | |
st.write("#### التسعير القياسي") | |
st.write("في هذه الاستراتيجية، يتم تسعير جميع البنود بناءً على التكلفة الفعلية مع إضافة هامش ربح موحد.") | |
# استراتيجية التسعير الغير متزن | |
st.write("#### التسعير الغير متزن") | |
st.write("في هذه الاستراتيجية، يتم زيادة أسعار البنود المبكرة وتخفيض أسعار البنود المتأخرة للحصول على تدفق نقدي أفضل.") | |
# تعديل معاملات التسعير الغير متزن | |
if project_info['pricing_type'] == 'غير متزن': | |
st.write("### تعديل معاملات التسعير الغير متزن") | |
col1, col2 = st.columns(2) | |
with col1: | |
early_factor = st.slider( | |
"معامل البنود المبكرة", | |
min_value=1.0, | |
max_value=1.5, | |
value=st.session_state.unbalanced_pricing_factors['early_items_factor'], | |
step=0.05, | |
format="%.2f", | |
key="early_factor_slider" | |
) | |
if early_factor != st.session_state.unbalanced_pricing_factors['early_items_factor']: | |
st.session_state.unbalanced_pricing_factors['early_items_factor'] = early_factor | |
with col2: | |
late_factor = st.slider( | |
"معامل البنود المتأخرة", | |
min_value=0.5, | |
max_value=1.0, | |
value=st.session_state.unbalanced_pricing_factors['late_items_factor'], | |
step=0.05, | |
format="%.2f", | |
key="late_factor_slider" | |
) | |
if late_factor != st.session_state.unbalanced_pricing_factors['late_items_factor']: | |
st.session_state.unbalanced_pricing_factors['late_items_factor'] = late_factor | |
# عرض جدول الأسعار المعدلة | |
st.write("### الأسعار المعدلة باستخدام التسعير الغير متزن") | |
# إنشاء نسخة من بنود المشروع | |
unbalanced_items = [] | |
# تقسيم البنود إلى مبكرة ومتأخرة | |
num_items = len(project_items) | |
early_items_count = num_items // 3 | |
late_items_count = num_items // 3 | |
for i, item in enumerate(project_items): | |
unbalanced_item = item.copy() | |
if i < early_items_count: | |
# بند مبكر | |
factor = st.session_state.unbalanced_pricing_factors['early_items_factor'] | |
unbalanced_item['unbalanced_unit_price'] = item['unit_price'] * factor | |
unbalanced_item['unbalanced_total_price'] = unbalanced_item['unbalanced_unit_price'] * item['quantity'] | |
elif i >= num_items - late_items_count: | |
# بند متأخر | |
factor = st.session_state.unbalanced_pricing_factors['late_items_factor'] | |
unbalanced_item['unbalanced_unit_price'] = item['unit_price'] * factor | |
unbalanced_item['unbalanced_total_price'] = unbalanced_item['unbalanced_unit_price'] * item['quantity'] | |
else: | |
# بند وسطي | |
unbalanced_item['unbalanced_unit_price'] = item['unit_price'] | |
unbalanced_item['unbalanced_total_price'] = item['total_price'] | |
unbalanced_items.append(unbalanced_item) | |
# إنشاء DataFrame من البنود المعدلة | |
df = pd.DataFrame(unbalanced_items) | |
df = df[['code', 'description', 'unit', 'quantity', 'unit_price', 'unbalanced_unit_price', 'total_price', 'unbalanced_total_price']] | |
df.columns = ['الكود', 'الوصف', 'الوحدة', 'الكمية', 'سعر الوحدة الأصلي', 'سعر الوحدة المعدل', 'السعر الإجمالي الأصلي', 'السعر الإجمالي المعدل'] | |
# تنسيق الأسعار | |
df['سعر الوحدة الأصلي'] = df['سعر الوحدة الأصلي'].apply(lambda x: f"{x:,.2f} ريال") | |
df['سعر الوحدة المعدل'] = df['سعر الوحدة المعدل'].apply(lambda x: f"{x:,.2f} ريال") | |
df['السعر الإجمالي الأصلي'] = df['السعر الإجمالي الأصلي'].apply(lambda x: f"{x:,.2f} ريال") | |
df['السعر الإجمالي المعدل'] = df['السعر الإجمالي المعدل'].apply(lambda x: f"{x:,.2f} ريال") | |
# عرض الجدول | |
st.dataframe(df, use_container_width=True) | |
# حساب إجمالي الأسعار | |
total_original = sum(item['total_price'] for item in project_items) | |
total_unbalanced = sum(item['unbalanced_total_price'] for item in unbalanced_items) | |
col1, col2 = st.columns(2) | |
with col1: | |
st.metric("إجمالي السعر الأصلي", f"{total_original:,.2f} ريال") | |
with col2: | |
st.metric("إجمالي السعر المعدل", f"{total_unbalanced:,.2f} ريال") | |
# زر تطبيق التسعير الغير متزن | |
if st.button("تطبيق التسعير الغير متزن", key="apply_unbalanced_pricing"): | |
for i, item in enumerate(unbalanced_items): | |
for j, boq_item in enumerate(st.session_state.boq_items): | |
if boq_item['id'] == item['id']: | |
st.session_state.boq_items[j]['unit_price'] = item['unbalanced_unit_price'] | |
st.session_state.boq_items[j]['total_price'] = item['unbalanced_total_price'] | |
break | |
st.success("تم تطبيق التسعير الغير متزن بنجاح") | |
st.rerun() | |
def _render_export_save_buttons(self, project_info): | |
"""عرض أزرار التصدير والحفظ""" | |
st.subheader("تصدير وحفظ التسعير") | |
col1, col2 = st.columns(2) | |
with col1: | |
if st.button("تصدير التسعير كملف Excel", key="export_pricing_excel"): | |
st.info("سيتم تنفيذ هذه الميزة في الإصدار القادم") | |
with col2: | |
if st.button("حفظ التسعير", key="save_pricing"): | |
# حفظ التسعير الحالي | |
project_items = [item for item in st.session_state.boq_items if item['project_id'] == st.session_state.current_project] | |
saved_pricing = { | |
'project_id': project_info['id'], | |
'project_name': project_info['name'], | |
'saved_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
'items': project_items | |
} | |
st.session_state.saved_pricing.append(saved_pricing) | |
# تحديث حالة المشروع | |
for i, p in enumerate(st.session_state.projects): | |
if p['id'] == project_info['id']: | |
st.session_state.projects[i]['status'] = 'تم التسعير' | |
break | |
st.success("تم حفظ التسعير بنجاح") | |
# تشغيل التطبيق | |
if __name__ == "__main__": | |
app = PricingApp() | |
app.run() | |