|
""" |
|
وحدة التسعير - التطبيق الرئيسي |
|
""" |
|
|
|
import streamlit as st |
|
import pandas as pd |
|
import numpy as np |
|
import matplotlib.pyplot as plt |
|
import plotly.express as px |
|
import plotly.graph_objects as go |
|
from datetime import datetime |
|
import time |
|
import io |
|
import os |
|
import json |
|
import base64 |
|
from pathlib import Path |
|
|
|
class PricingApp: |
|
"""وحدة التسعير""" |
|
|
|
def __init__(self): |
|
"""تهيئة وحدة التسعير""" |
|
|
|
|
|
if 'bill_of_quantities' not in st.session_state: |
|
st.session_state.bill_of_quantities = [ |
|
{ |
|
'id': 1, |
|
'code': 'A-001', |
|
'description': 'أعمال الحفر والردم', |
|
'unit': 'م3', |
|
'quantity': 1500, |
|
'unit_price': 45, |
|
'total_price': 67500, |
|
'category': 'أعمال ترابية' |
|
}, |
|
{ |
|
'id': 2, |
|
'code': 'A-002', |
|
'description': 'توريد وصب خرسانة عادية', |
|
'unit': 'م3', |
|
'quantity': 250, |
|
'unit_price': 350, |
|
'total_price': 87500, |
|
'category': 'أعمال خرسانية' |
|
}, |
|
{ |
|
'id': 3, |
|
'code': 'A-003', |
|
'description': 'توريد وصب خرسانة مسلحة للأساسات', |
|
'unit': 'م3', |
|
'quantity': 180, |
|
'unit_price': 450, |
|
'total_price': 81000, |
|
'category': 'أعمال خرسانية' |
|
}, |
|
{ |
|
'id': 4, |
|
'code': 'A-004', |
|
'description': 'توريد وصب خرسانة مسلحة للأعمدة', |
|
'unit': 'م3', |
|
'quantity': 120, |
|
'unit_price': 500, |
|
'total_price': 60000, |
|
'category': 'أعمال خرسانية' |
|
}, |
|
{ |
|
'id': 5, |
|
'code': 'A-005', |
|
'description': 'توريد وتركيب حديد تسليح', |
|
'unit': 'طن', |
|
'quantity': 45, |
|
'unit_price': 3000, |
|
'total_price': 135000, |
|
'category': 'أعمال حديد' |
|
}, |
|
{ |
|
'id': 6, |
|
'code': 'A-006', |
|
'description': 'توريد وبناء طابوق', |
|
'unit': 'م2', |
|
'quantity': 1200, |
|
'unit_price': 45, |
|
'total_price': 54000, |
|
'category': 'أعمال بناء' |
|
}, |
|
{ |
|
'id': 7, |
|
'code': 'A-007', |
|
'description': 'أعمال اللياسة والتشطيبات', |
|
'unit': 'م2', |
|
'quantity': 2400, |
|
'unit_price': 35, |
|
'total_price': 84000, |
|
'category': 'أعمال تشطيبات' |
|
}, |
|
{ |
|
'id': 8, |
|
'code': 'A-008', |
|
'description': 'أعمال الدهانات', |
|
'unit': 'م2', |
|
'quantity': 2400, |
|
'unit_price': 25, |
|
'total_price': 60000, |
|
'category': 'أعمال تشطيبات' |
|
}, |
|
{ |
|
'id': 9, |
|
'code': 'A-009', |
|
'description': 'توريد وتركيب أبواب خشبية', |
|
'unit': 'عدد', |
|
'quantity': 24, |
|
'unit_price': 750, |
|
'total_price': 18000, |
|
'category': 'أعمال نجارة' |
|
}, |
|
{ |
|
'id': 10, |
|
'code': 'A-010', |
|
'description': 'توريد وتركيب نوافذ ألمنيوم', |
|
'unit': 'م2', |
|
'quantity': 120, |
|
'unit_price': 350, |
|
'total_price': 42000, |
|
'category': 'أعمال ألمنيوم' |
|
} |
|
] |
|
|
|
if 'cost_analysis' not in st.session_state: |
|
st.session_state.cost_analysis = [ |
|
{ |
|
'id': 1, |
|
'category': 'تكاليف مباشرة', |
|
'subcategory': 'مواد', |
|
'description': 'خرسانة', |
|
'amount': 120000, |
|
'percentage': 17.9 |
|
}, |
|
{ |
|
'id': 2, |
|
'category': 'تكاليف مباشرة', |
|
'subcategory': 'مواد', |
|
'description': 'حديد تسليح', |
|
'amount': 135000, |
|
'percentage': 20.1 |
|
}, |
|
{ |
|
'id': 3, |
|
'category': 'تكاليف مباشرة', |
|
'subcategory': 'مواد', |
|
'description': 'طابوق', |
|
'amount': 54000, |
|
'percentage': 8.1 |
|
}, |
|
{ |
|
'id': 4, |
|
'category': 'تكاليف مباشرة', |
|
'subcategory': 'عمالة', |
|
'description': 'عمالة تنفيذ', |
|
'amount': 120000, |
|
'percentage': 17.9 |
|
}, |
|
{ |
|
'id': 5, |
|
'category': 'تكاليف مباشرة', |
|
'subcategory': 'معدات', |
|
'description': 'معدات إنشائية', |
|
'amount': 85000, |
|
'percentage': 12.7 |
|
}, |
|
{ |
|
'id': 6, |
|
'category': 'تكاليف غير مباشرة', |
|
'subcategory': 'إدارة', |
|
'description': 'إدارة المشروع', |
|
'amount': 45000, |
|
'percentage': 6.7 |
|
}, |
|
{ |
|
'id': 7, |
|
'category': 'تكاليف غير مباشرة', |
|
'subcategory': 'إدارة', |
|
'description': 'إشراف هندسي', |
|
'amount': 35000, |
|
'percentage': 5.2 |
|
}, |
|
{ |
|
'id': 8, |
|
'category': 'تكاليف غير مباشرة', |
|
'subcategory': 'عامة', |
|
'description': 'تأمينات وضمانات', |
|
'amount': 25000, |
|
'percentage': 3.7 |
|
}, |
|
{ |
|
'id': 9, |
|
'category': 'تكاليف غير مباشرة', |
|
'subcategory': 'عامة', |
|
'description': 'مصاريف إدارية', |
|
'amount': 30000, |
|
'percentage': 4.5 |
|
}, |
|
{ |
|
'id': 10, |
|
'category': 'أرباح', |
|
'subcategory': 'أرباح', |
|
'description': 'هامش الربح', |
|
'amount': 55000, |
|
'percentage': 8.2 |
|
} |
|
] |
|
|
|
if 'price_scenarios' not in st.session_state: |
|
st.session_state.price_scenarios = [ |
|
{ |
|
'id': 1, |
|
'name': 'السيناريو الأساسي', |
|
'description': 'التسعير الأساسي مع هامش ربح 8%', |
|
'total_cost': 615000, |
|
'profit_margin': 8.2, |
|
'total_price': 670000, |
|
'is_active': True |
|
}, |
|
{ |
|
'id': 2, |
|
'name': 'سيناريو تنافسي', |
|
'description': 'تخفيض هامش الربح للمنافسة', |
|
'total_cost': 615000, |
|
'profit_margin': 5.0, |
|
'total_price': 650000, |
|
'is_active': False |
|
}, |
|
{ |
|
'id': 3, |
|
'name': 'سيناريو مرتفع', |
|
'description': 'زيادة هامش الربح للمشاريع ذات المخاطر العالية', |
|
'total_cost': 615000, |
|
'profit_margin': 12.0, |
|
'total_price': 700000, |
|
'is_active': False |
|
} |
|
] |
|
|
|
def run(self): |
|
"""تشغيل وحدة التسعير""" |
|
|
|
self.render() |
|
|
|
def render(self): |
|
"""عرض واجهة وحدة التسعير""" |
|
|
|
st.markdown("<h1 class='module-title'>وحدة التسعير</h1>", unsafe_allow_html=True) |
|
|
|
tabs = st.tabs([ |
|
"لوحة التحكم", |
|
"جدول الكميات", |
|
"تحليل التكاليف", |
|
"سيناريوهات التسعير", |
|
"المقارنة التنافسية", |
|
"التقارير" |
|
]) |
|
|
|
with tabs[0]: |
|
self._render_dashboard_tab() |
|
|
|
with tabs[1]: |
|
self._render_bill_of_quantities_tab() |
|
|
|
with tabs[2]: |
|
self._render_cost_analysis_tab() |
|
|
|
with tabs[3]: |
|
self._render_pricing_scenarios_tab() |
|
|
|
with tabs[4]: |
|
self._render_competitive_analysis_tab() |
|
|
|
with tabs[5]: |
|
self._render_reports_tab() |
|
|
|
def _render_dashboard_tab(self): |
|
"""عرض تبويب لوحة التحكم""" |
|
|
|
st.markdown("### لوحة تحكم التسعير") |
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
|
|
total_direct_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف مباشرة') |
|
total_indirect_cost = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'تكاليف غير مباشرة') |
|
total_profit = sum(item['amount'] for item in st.session_state.cost_analysis if item['category'] == 'أرباح') |
|
total_cost = total_direct_cost + total_indirect_cost |
|
total_price = total_cost + total_profit |
|
|
|
with col1: |
|
st.metric("إجمالي التكاليف المباشرة", f"{total_direct_cost:,.0f} ريال") |
|
|
|
with col2: |
|
st.metric("إجمالي التكاليف غير المباشرة", f"{total_indirect_cost:,.0f} ريال") |
|
|
|
with col3: |
|
st.metric("إجمالي التكاليف", f"{total_cost:,.0f} ريال") |
|
|
|
with col4: |
|
st.metric("السعر الإجمالي", f"{total_price:,.0f} ريال") |
|
|
|
|
|
st.markdown("### توزيع التكاليف") |
|
|
|
|
|
cost_categories = {} |
|
|
|
for item in st.session_state.cost_analysis: |
|
category = item['category'] |
|
if category in cost_categories: |
|
cost_categories[category] += item['amount'] |
|
else: |
|
cost_categories[category] = item['amount'] |
|
|
|
|
|
cost_df = pd.DataFrame({ |
|
'الفئة': list(cost_categories.keys()), |
|
'المبلغ': list(cost_categories.values()) |
|
}) |
|
|
|
|
|
fig = px.pie( |
|
cost_df, |
|
values='المبلغ', |
|
names='الفئة', |
|
title='توزيع التكاليف حسب الفئة', |
|
color_discrete_sequence=px.colors.qualitative.Set3 |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("### توزيع التكاليف المباشرة") |
|
|
|
|
|
direct_cost_subcategories = {} |
|
|
|
for item in st.session_state.cost_analysis: |
|
if item['category'] == 'تكاليف مباشرة': |
|
subcategory = item['subcategory'] |
|
if subcategory in direct_cost_subcategories: |
|
direct_cost_subcategories[subcategory] += item['amount'] |
|
else: |
|
direct_cost_subcategories[subcategory] = item['amount'] |
|
|
|
|
|
direct_cost_df = pd.DataFrame({ |
|
'الفئة الفرعية': list(direct_cost_subcategories.keys()), |
|
'المبلغ': list(direct_cost_subcategories.values()) |
|
}) |
|
|
|
|
|
fig = px.pie( |
|
direct_cost_df, |
|
values='المبلغ', |
|
names='الفئة الفرعية', |
|
title='توزيع التكاليف المباشرة', |
|
color_discrete_sequence=px.colors.qualitative.Pastel |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("### توزيع التكاليف غير المباشرة") |
|
|
|
|
|
indirect_cost_subcategories = {} |
|
|
|
for item in st.session_state.cost_analysis: |
|
if item['category'] == 'تكاليف غير مباشرة': |
|
subcategory = item['subcategory'] |
|
if subcategory in indirect_cost_subcategories: |
|
indirect_cost_subcategories[subcategory] += item['amount'] |
|
else: |
|
indirect_cost_subcategories[subcategory] = item['amount'] |
|
|
|
|
|
indirect_cost_df = pd.DataFrame({ |
|
'الفئة الفرعية': list(indirect_cost_subcategories.keys()), |
|
'المبلغ': list(indirect_cost_subcategories.values()) |
|
}) |
|
|
|
|
|
fig = px.pie( |
|
indirect_cost_df, |
|
values='المبلغ', |
|
names='الفئة الفرعية', |
|
title='توزيع التكاليف غير المباشرة', |
|
color_discrete_sequence=px.colors.qualitative.Pastel1 |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
def _render_bill_of_quantities_tab(self): |
|
"""عرض تبويب جدول الكميات""" |
|
|
|
st.markdown("### جدول الكميات") |
|
|
|
|
|
boq_df = pd.DataFrame(st.session_state.bill_of_quantities) |
|
|
|
|
|
st.dataframe( |
|
boq_df[['code', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'category']], |
|
column_config={ |
|
'code': 'الكود', |
|
'description': 'الوصف', |
|
'unit': 'الوحدة', |
|
'quantity': 'الكمية', |
|
'unit_price': st.column_config.NumberColumn('سعر الوحدة', format='%d ريال'), |
|
'total_price': st.column_config.NumberColumn('السعر الإجمالي', format='%d ريال'), |
|
'category': 'الفئة' |
|
}, |
|
hide_index=True, |
|
use_container_width=True |
|
) |
|
|
|
|
|
st.markdown("### إضافة بند جديد") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
new_code = st.text_input("الكود", key="new_boq_code") |
|
new_description = st.text_input("الوصف", key="new_boq_description") |
|
new_unit = st.selectbox("الوحدة", ["م3", "م2", "طن", "عدد", "متر طولي"], key="new_boq_unit") |
|
|
|
with col2: |
|
new_quantity = st.number_input("الكمية", min_value=0.0, step=1.0, key="new_boq_quantity") |
|
new_unit_price = st.number_input("سعر الوحدة", min_value=0.0, step=10.0, key="new_boq_unit_price") |
|
new_category = st.selectbox( |
|
"الفئة", |
|
[ |
|
"أعمال ترابية", |
|
"أعمال خرسانية", |
|
"أعمال حديد", |
|
"أعمال بناء", |
|
"أعمال تشطيبات", |
|
"أعمال نجارة", |
|
"أعمال ألمنيوم", |
|
"أعمال كهربائية", |
|
"أعمال ميكانيكية", |
|
"أعمال صحية" |
|
], |
|
key="new_boq_category" |
|
) |
|
|
|
if st.button("إضافة البند", key="add_boq_item"): |
|
if new_code and new_description and new_quantity > 0 and new_unit_price > 0: |
|
|
|
new_total_price = new_quantity * new_unit_price |
|
|
|
|
|
new_id = max([item['id'] for item in st.session_state.bill_of_quantities]) + 1 |
|
|
|
st.session_state.bill_of_quantities.append({ |
|
'id': new_id, |
|
'code': new_code, |
|
'description': new_description, |
|
'unit': new_unit, |
|
'quantity': new_quantity, |
|
'unit_price': new_unit_price, |
|
'total_price': new_total_price, |
|
'category': new_category |
|
}) |
|
|
|
st.success(f"تمت إضافة البند بنجاح: {new_description}") |
|
|
|
|
|
st.rerun() |
|
else: |
|
st.error("يرجى إدخال جميع البيانات المطلوبة بشكل صحيح") |
|
|
|
|
|
st.markdown("### ملخص جدول الكميات") |
|
|
|
|
|
category_totals = {} |
|
for item in st.session_state.bill_of_quantities: |
|
category = item['category'] |
|
if category in category_totals: |
|
category_totals[category] += item['total_price'] |
|
else: |
|
category_totals[category] = item['total_price'] |
|
|
|
|
|
category_df = pd.DataFrame({ |
|
'الفئة': list(category_totals.keys()), |
|
'المبلغ': list(category_totals.values()) |
|
}) |
|
|
|
|
|
category_df = category_df.sort_values('المبلغ', ascending=False) |
|
|
|
|
|
fig = px.bar( |
|
category_df, |
|
x='الفئة', |
|
y='المبلغ', |
|
title='إجمالي تكلفة البنود حسب الفئة', |
|
color='الفئة', |
|
text_auto=True |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
total_boq = sum(item['total_price'] for item in st.session_state.bill_of_quantities) |
|
|
|
|
|
def _render_cost_analysis_tab(self): |
|
"""عرض تبويب تحليل التكاليف""" |
|
|
|
st.markdown("### تحليل التكاليف") |
|
|
|
st.info("تبويب تحليل التكاليف قيد التطوير") |
|
|
|
def _render_pricing_scenarios_tab(self): |
|
"""عرض تبويب سيناريوهات التسعير""" |
|
|
|
st.markdown("### سيناريوهات التسعير") |
|
|
|
st.info("تبويب سيناريوهات التسعير قيد التطوير") |
|
|
|
def _render_competitive_analysis_tab(self): |
|
"""عرض تبويب المقارنة التنافسية""" |
|
|
|
st.markdown("### المقارنة التنافسية") |
|
|
|
st.info("تبويب المقارنة التنافسية قيد التطوير") |
|
|
|
def _render_reports_tab(self): |
|
"""عرض تبويب التقارير""" |
|
|
|
st.markdown("### التقارير") |
|
|
|
st.info("تبويب التقارير قيد التطوير") |
|
|