""" معالج ملفات Excel """ import pandas as pd import os import numpy as np import xlsxwriter from datetime import datetime import traceback import config from utils.helpers import create_directory_if_not_exists, get_file_extension, format_number def read_excel_file(file_path, sheet_name=0, header=0, skip_rows=None): """ قراءة ملف Excel المعلمات: file_path: مسار ملف Excel sheet_name: اسم أو رقم الصفحة (افتراضي: 0) header: رقم الصف الذي يحتوي على العناوين (افتراضي: 0) skip_rows: قائمة بأرقام الصفوف للتخطي (افتراضي: None) الإرجاع: DataFrame من البيانات المقروءة """ try: # التحقق من وجود الملف if not os.path.exists(file_path): raise FileNotFoundError(f"الملف غير موجود: {file_path}") # التحقق من امتداد الملف ext = get_file_extension(file_path) if ext not in ['.xlsx', '.xls', '.xlsm']: raise ValueError(f"نوع الملف غير مدعوم: {ext}. يجب أن يكون الملف بامتداد .xlsx أو .xls أو .xlsm") # قراءة الملف df = pd.read_excel( file_path, sheet_name=sheet_name, header=header, skiprows=skip_rows ) return df except Exception as e: error_msg = f"خطأ في قراءة ملف Excel: {str(e)}" print(error_msg) traceback.print_exc() raise Exception(error_msg) def write_excel_file(df, file_path, sheet_name="Sheet1", index=False, freeze_panes=None, column_widths=None, formats=None): """ كتابة DataFrame إلى ملف Excel المعلمات: df: DataFrame المراد كتابته file_path: مسار ملف Excel sheet_name: اسم الصفحة (افتراضي: Sheet1) index: ما إذا كان سيتم تضمين الفهرس (افتراضي: False) freeze_panes: صف وعمود لتجميد الألواح (افتراضي: None) column_widths: قاموس لعرض الأعمدة {column_name: width} formats: قاموس لتنسيقات الأعمدة {column_name: format_function} الإرجاع: True في حالة النجاح """ try: # التأكد من وجود المجلد create_directory_if_not_exists(os.path.dirname(file_path)) # تحديد المكتب والورقة writer = pd.ExcelWriter(file_path, engine='xlsxwriter') df.to_excel(writer, sheet_name=sheet_name, index=index) # الحصول على مرجع لورقة العمل workbook = writer.book worksheet = writer.sheets[sheet_name] # إنشاء تنسيقات مخصصة header_format = workbook.add_format({ 'bold': True, 'bg_color': '#CCCCCC', 'border': 1, 'align': 'center', 'valign': 'vcenter', 'text_wrap': True }) number_format = workbook.add_format({ 'num_format': '#,##0.00', 'align': 'right' }) currency_format = workbook.add_format({ 'num_format': '_-* #,##0.00 [$ريال]_-;-* #,##0.00 [$ريال]_-;_-* "-" [$ريال]_-;_-@_-', 'align': 'right' }) date_format = workbook.add_format({ 'num_format': 'yyyy-mm-dd', 'align': 'center' }) text_format = workbook.add_format({ 'align': 'right', 'text_wrap': True }) # تطبيق التنسيقات على العناوين for col_num, value in enumerate(df.columns.values): worksheet.write(0, col_num + (1 if index else 0), value, header_format) # تعيين حجم الأعمدة if column_widths: for col_name, width in column_widths.items(): if col_name in df.columns: col_idx = df.columns.get_loc(col_name) + (1 if index else 0) worksheet.set_column(col_idx, col_idx, width) else: # ضبط عرض الأعمدة تلقائيًا for col_num, col_name in enumerate(df.columns): max_len = df[col_name].astype(str).map(len).max() col_len = max(max_len, len(str(col_name))) + 2 worksheet.set_column(col_num + (1 if index else 0), col_num + (1 if index else 0), col_len) # تطبيق التنسيقات حسب نوع البيانات for row_num in range(len(df)): for col_num, col_name in enumerate(df.columns): cell_value = df.iloc[row_num, col_num] cell_format = text_format # تحديد التنسيق المناسب بناءً على نوع البيانات if pd.api.types.is_numeric_dtype(df[col_name].dtype): if any(curr in col_name.lower() for curr in ['سعر', 'تكلفة', 'قيمة', 'مبلغ', 'ريال']): cell_format = currency_format else: cell_format = number_format elif pd.api.types.is_datetime64_dtype(df[col_name].dtype): cell_format = date_format # استخدام تنسيق مخصص إذا تم توفيره if formats and col_name in formats: custom_format = formats[col_name] if callable(custom_format): # إذا كان دالة، استدعاها لتطبيق التنسيق cell_value = custom_format(cell_value) else: # إذا كان تنسيق، استخدمه cell_format = custom_format worksheet.write(row_num + 1, col_num + (1 if index else 0), cell_value, cell_format) # تجميد الألواح إذا تم تحديده if freeze_panes: worksheet.freeze_panes(*freeze_panes) # حفظ الملف writer.close() return True except Exception as e: error_msg = f"خطأ في كتابة ملف Excel: {str(e)}" print(error_msg) traceback.print_exc() raise Exception(error_msg) def export_to_excel(data, file_path, sheet_name="Sheet1", customize_func=None): """ تصدير البيانات إلى ملف Excel مع خيارات تخصيص المعلمات: data: DataFrame أو قاموس من DataFrames للتصدير file_path: مسار ملف Excel sheet_name: اسم الصفحة (افتراضي: Sheet1) customize_func: دالة لتخصيص المصنف قبل الحفظ (افتراضي: None) الإرجاع: True في حالة النجاح """ try: # التأكد من وجود المجلد create_directory_if_not_exists(os.path.dirname(file_path)) # إنشاء كائن الكاتب writer = pd.ExcelWriter(file_path, engine='xlsxwriter') # تصدير البيانات if isinstance(data, pd.DataFrame): # إذا كان DataFrame واحد data.to_excel(writer, sheet_name=sheet_name, index=False) elif isinstance(data, dict): # إذا كان قاموس من DataFrames for sheet, df in data.items(): if isinstance(df, pd.DataFrame): df.to_excel(writer, sheet_name=sheet, index=False) else: raise ValueError("البيانات يجب أن تكون DataFrame أو قاموس من DataFrames") # تطبيق التخصيص إذا تم توفيره if customize_func and callable(customize_func): customize_func(writer) # حفظ الملف writer.close() return True except Exception as e: error_msg = f"خطأ في تصدير البيانات إلى Excel: {str(e)}" print(error_msg) traceback.print_exc() raise Exception(error_msg) def read_sheets_from_excel(file_path): """ قراءة جميع صفحات ملف Excel المعلمات: file_path: مسار ملف Excel الإرجاع: قاموس من DataFrames بأسماء الصفحات كمفاتيح """ try: # التحقق من وجود الملف if not os.path.exists(file_path): raise FileNotFoundError(f"الملف غير موجود: {file_path}") # التحقق من امتداد الملف ext = get_file_extension(file_path) if ext not in ['.xlsx', '.xls', '.xlsm']: raise ValueError(f"نوع الملف غير مدعوم: {ext}. يجب أن يكون الملف بامتداد .xlsx أو .xls أو .xlsm") # قراءة جميع الصفحات من الملف excel_file = pd.ExcelFile(file_path) sheets = {} for sheet_name in excel_file.sheet_names: sheets[sheet_name] = pd.read_excel(excel_file, sheet_name=sheet_name) return sheets except Exception as e: error_msg = f"خطأ في قراءة صفحات ملف Excel: {str(e)}" print(error_msg) traceback.print_exc() raise Exception(error_msg) def create_excel_report(data_dict, file_path, formats=None, column_widths=None, title=None, subtitle=None): """ إنشاء تقرير Excel متقدم المعلمات: data_dict: قاموس من DataFrames للتصدير {sheet_name: DataFrame} file_path: مسار ملف Excel formats: قاموس للتنسيقات {sheet_name: {column_name: format}} column_widths: قاموس لعرض الأعمدة {sheet_name: {column_name: width}} title: عنوان التقرير subtitle: العنوان الفرعي للتقرير الإرجاع: True في حالة النجاح """ try: # التأكد من وجود المجلد create_directory_if_not_exists(os.path.dirname(file_path)) # إنشاء كائن الكاتب writer = pd.ExcelWriter(file_path, engine='xlsxwriter') workbook = writer.book # إنشاء التنسيقات العامة header_format = workbook.add_format({ 'bold': True, 'bg_color': '#CCCCCC', 'border': 1, 'align': 'center', 'valign': 'vcenter', 'text_wrap': True }) title_format = workbook.add_format({ 'bold': True, 'font_size': 16, 'align': 'center', 'valign': 'vcenter', 'bg_color': '#E0E0E0', 'border': 2 }) subtitle_format = workbook.add_format({ 'font_size': 12, 'align': 'center', 'valign': 'vcenter', 'bg_color': '#E0E0E0', 'border': 1 }) date_format = workbook.add_format({ 'num_format': 'yyyy-mm-dd', 'align': 'center' }) number_format = workbook.add_format({ 'num_format': '#,##0.00', 'align': 'right' }) currency_format = workbook.add_format({ 'num_format': '_-* #,##0.00 [$ريال]_-;-* #,##0.00 [$ريال]_-;_-* "-" [$ريال]_-;_-@_-', 'align': 'right' }) percent_format = workbook.add_format({ 'num_format': '0.00%', 'align': 'right' }) text_format = workbook.add_format({ 'align': 'right', 'text_wrap': True }) # تصدير البيانات current_row = 0 # إضافة العنوان والعنوان الفرعي إذا تم توفيرهما if title or subtitle: for sheet_name in data_dict.keys(): worksheet = workbook.add_worksheet(sheet_name) current_row = 0 if title: worksheet.merge_range('A1:J1', title, title_format) current_row += 1 if subtitle: worksheet.merge_range(f'A{current_row + 1}:J{current_row + 1}', subtitle, subtitle_format) current_row += 1 # إضافة فاصل current_row += 1 # كتابة البيانات df = data_dict[sheet_name] df.to_excel(writer, sheet_name=sheet_name, startrow=current_row, index=False) # تنسيق العناوين for col_num, value in enumerate(df.columns.values): worksheet.write(current_row, col_num, value, header_format) # تطبيق التنسيقات المخصصة if formats and sheet_name in formats: sheet_formats = formats[sheet_name] for col_name, fmt in sheet_formats.items(): if col_name in df.columns: col_idx = df.columns.get_loc(col_name) for row_num in range(len(df)): cell_value = df.iloc[row_num, col_idx] worksheet.write(row_num + current_row + 1, col_idx, cell_value, fmt) # تعيين عرض الأعمدة if column_widths and sheet_name in column_widths: sheet_widths = column_widths[sheet_name] for col_name, width in sheet_widths.items(): if col_name in df.columns: col_idx = df.columns.get_loc(col_name) worksheet.set_column(col_idx, col_idx, width) else: # ضبط عرض الأعمدة تلقائيًا for col_num, col_name in enumerate(df.columns): max_len = df[col_name].astype(str).map(len).max() col_len = max(max_len, len(str(col_name))) + 2 worksheet.set_column(col_num, col_num, col_len) else: # إذا لم يتم توفير عنوان أو عنوان فرعي، استخدم الطريقة العادية for sheet_name, df in data_dict.items(): df.to_excel(writer, sheet_name=sheet_name, index=False) worksheet = writer.sheets[sheet_name] # تنسيق العناوين for col_num, value in enumerate(df.columns.values): worksheet.write(0, col_num, value, header_format) # تطبيق التنسيقات المخصصة if formats and sheet_name in formats: sheet_formats = formats[sheet_name] for col_name, fmt in sheet_formats.items(): if col_name in df.columns: col_idx = df.columns.get_loc(col_name) for row_num in range(len(df)): cell_value = df.iloc[row_num, col_idx] worksheet.write(row_num + 1, col_idx, cell_value, fmt) # تعيين عرض الأعمدة if column_widths and sheet_name in column_widths: sheet_widths = column_widths[sheet_name] for col_name, width in sheet_widths.items(): if col_name in df.columns: col_idx = df.columns.get_loc(col_name) worksheet.set_column(col_idx, col_idx, width) else: # ضبط عرض الأعمدة تلقائيًا for col_num, col_name in enumerate(df.columns): max_len = df[col_name].astype(str).map(len).max() col_len = max(max_len, len(str(col_name))) + 2 worksheet.set_column(col_num, col_num, col_len) # حفظ الملف writer.close() return True except Exception as e: error_msg = f"خطأ في إنشاء تقرير Excel: {str(e)}" print(error_msg) traceback.print_exc() raise Exception(error_msg) def extract_data_from_excel(file_path, columns_mapping=None, sheet_name=0, header_row=0, data_start_row=1): """ استخراج بيانات منظمة من ملف Excel المعلمات: file_path: مسار ملف Excel columns_mapping: قاموس لتخطيط الأعمدة {اسم_العمود_الجديد: اسم_العمود_الأصلي} sheet_name: اسم أو رقم الصفحة (افتراضي: 0) header_row: رقم صف العناوين (افتراضي: 0) data_start_row: رقم صف بداية البيانات (افتراضي: 1) الإرجاع: DataFrame من البيانات المستخرجة """ try: # قراءة الملف df = pd.read_excel( file_path, sheet_name=sheet_name, header=header_row, skiprows=range(1, data_start_row) if data_start_row > 1 else None ) # تنظيف العناوين (إزالة المسافات الزائدة) df.columns = df.columns.str.strip() # إعادة تسمية الأعمدة إذا تم توفير تخطيط if columns_mapping: # التحقق من وجود جميع الأعمدة المطلوبة missing_columns = [col for col in columns_mapping.values() if col not in df.columns] if missing_columns: raise ValueError(f"الأعمدة التالية غير موجودة في الملف: {', '.join(missing_columns)}") # إعادة تسمية الأعمدة df = df.rename(columns={v: k for k, v in columns_mapping.items()}) # اختيار الأعمدة المطلوبة فقط df = df[list(columns_mapping.keys())] # تنظيف البيانات for col in df.columns: # تحويل الأعمدة النصية if df[col].dtype == 'object': df[col] = df[col].astype(str).str.strip() # محاولة تحويل الأعمدة الرقمية try: df[col] = pd.to_numeric(df[col], errors='ignore') except: pass return df except Exception as e: error_msg = f"خطأ في استخراج البيانات من ملف Excel: {str(e)}" print(error_msg) traceback.print_exc() raise Exception(error_msg)