Spaces:
Paused
Paused
| 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() | |