| """ |
| معالج ملفات PDF |
| """ |
|
|
| import os |
| import io |
| import re |
| import PyPDF2 |
| import fitz |
| import pdfplumber |
| import numpy as np |
| from PIL import Image |
| import pytesseract |
| import pandas as pd |
| import traceback |
| from reportlab.lib.pagesizes import A4, landscape |
| from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as ReportLabImage |
| from reportlab.lib import colors |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
| from reportlab.pdfbase import pdfmetrics |
| from reportlab.pdfbase.ttfonts import TTFont |
| import matplotlib.pyplot as plt |
|
|
| from utils.helpers import create_directory_if_not_exists, extract_numbers_from_text |
|
|
| |
| try: |
| font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "Amiri-Regular.ttf") |
| bold_font_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "Amiri-Bold.ttf") |
| |
| if os.path.exists(font_path) and os.path.exists(bold_font_path): |
| pdfmetrics.registerFont(TTFont('Arabic', font_path)) |
| pdfmetrics.registerFont(TTFont('ArabicBold', bold_font_path)) |
| except Exception as e: |
| print(f"تعذر تحميل الخطوط العربية: {str(e)}. سيتم استخدام الخطوط الافتراضية.") |
|
|
|
|
| def extract_text_from_pdf(file_path, method='pymupdf'): |
| """ |
| استخراج النص من ملف PDF |
| |
| المعلمات: |
| file_path: مسار ملف PDF |
| method: طريقة الاستخراج ('pymupdf', 'pypdf2', 'pdfplumber') |
| |
| الإرجاع: |
| نص مستخرج من ملف PDF |
| """ |
| try: |
| |
| if not os.path.exists(file_path): |
| raise FileNotFoundError(f"الملف غير موجود: {file_path}") |
| |
| |
| if method.lower() == 'pymupdf': |
| return _extract_text_with_pymupdf(file_path) |
| elif method.lower() == 'pypdf2': |
| return _extract_text_with_pypdf2(file_path) |
| elif method.lower() == 'pdfplumber': |
| return _extract_text_with_pdfplumber(file_path) |
| else: |
| |
| return _extract_text_with_pymupdf(file_path) |
| |
| except Exception as e: |
| error_msg = f"خطأ في استخراج النص من ملف PDF: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| raise Exception(error_msg) |
|
|
|
|
| def _extract_text_with_pymupdf(file_path): |
| """استخراج النص باستخدام PyMuPDF""" |
| document = fitz.open(file_path) |
| text = "" |
| |
| for page_number in range(len(document)): |
| page = document.load_page(page_number) |
| text += page.get_text("text") + "\n\n" |
| |
| document.close() |
| return text |
|
|
|
|
| def _extract_text_with_pypdf2(file_path): |
| """استخراج النص باستخدام PyPDF2""" |
| with open(file_path, 'rb') as file: |
| reader = PyPDF2.PdfReader(file) |
| text = "" |
| |
| for page_number in range(len(reader.pages)): |
| page = reader.pages[page_number] |
| text += page.extract_text() + "\n\n" |
| |
| return text |
|
|
|
|
| def _extract_text_with_pdfplumber(file_path): |
| """استخراج النص باستخدام pdfplumber""" |
| with pdfplumber.open(file_path) as pdf: |
| text = "" |
| |
| for page in pdf.pages: |
| text += page.extract_text() + "\n\n" |
| |
| return text |
|
|
|
|
| def extract_tables_from_pdf(file_path, page_numbers=None): |
| """ |
| استخراج الجداول من ملف PDF |
| |
| المعلمات: |
| file_path: مسار ملف PDF |
| page_numbers: قائمة بأرقام الصفحات للاستخراج منها (افتراضي: None لجميع الصفحات) |
| |
| الإرجاع: |
| قائمة من DataFrames تمثل الجداول المستخرجة |
| """ |
| try: |
| |
| if not os.path.exists(file_path): |
| raise FileNotFoundError(f"الملف غير موجود: {file_path}") |
| |
| |
| tables = [] |
| |
| with pdfplumber.open(file_path) as pdf: |
| |
| if page_numbers is None: |
| pages_to_extract = range(len(pdf.pages)) |
| else: |
| pages_to_extract = [p-1 for p in page_numbers if 1 <= p <= len(pdf.pages)] |
| |
| |
| for page_idx in pages_to_extract: |
| page = pdf.pages[page_idx] |
| page_tables = page.extract_tables() |
| |
| if page_tables: |
| for table in page_tables: |
| if table: |
| |
| df = pd.DataFrame(table[1:], columns=table[0]) |
| |
| |
| df = df.applymap(lambda x: x.strip() if isinstance(x, str) else x) |
| |
| |
| tables.append(df) |
| |
| return tables |
| |
| except Exception as e: |
| error_msg = f"خطأ في استخراج الجداول من ملف PDF: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| raise Exception(error_msg) |
|
|
|
|
| def extract_images_from_pdf(file_path, output_dir=None, prefix='image'): |
| """ |
| استخراج الصور من ملف PDF |
| |
| المعلمات: |
| file_path: مسار ملف PDF |
| output_dir: دليل الإخراج (افتراضي: None للإرجاع كقائمة من الصور) |
| prefix: بادئة أسماء ملفات الصور |
| |
| الإرجاع: |
| قائمة من مسارات الصور المستخرجة إذا تم تحديد دليل الإخراج، وإلا قائمة من الصور |
| """ |
| try: |
| |
| if not os.path.exists(file_path): |
| raise FileNotFoundError(f"الملف غير موجود: {file_path}") |
| |
| |
| if output_dir: |
| create_directory_if_not_exists(output_dir) |
| |
| |
| document = fitz.open(file_path) |
| images = [] |
| image_paths = [] |
| |
| for page_idx in range(len(document)): |
| page = document.load_page(page_idx) |
| |
| |
| image_list = page.get_images(full=True) |
| |
| for img_idx, img_info in enumerate(image_list): |
| xref = img_info[0] |
| base_image = document.extract_image(xref) |
| image_bytes = base_image["image"] |
| |
| |
| image = Image.open(io.BytesIO(image_bytes)) |
| |
| if output_dir: |
| |
| image_filename = f"{prefix}_{page_idx+1}_{img_idx+1}.{base_image['ext']}" |
| image_path = os.path.join(output_dir, image_filename) |
| image.save(image_path) |
| image_paths.append(image_path) |
| else: |
| |
| images.append(image) |
| |
| document.close() |
| |
| return image_paths if output_dir else images |
| |
| except Exception as e: |
| error_msg = f"خطأ في استخراج الصور من ملف PDF: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| raise Exception(error_msg) |
|
|
|
|
| def extract_text_from_image(image, lang='ara+eng'): |
| """ |
| استخراج النص من صورة باستخدام OCR |
| |
| المعلمات: |
| image: كائن الصورة أو مسار الصورة |
| lang: لغة النص (افتراضي: 'ara+eng' للعربية والإنجليزية) |
| |
| الإرجاع: |
| النص المستخرج من الصورة |
| """ |
| try: |
| |
| if isinstance(image, str): |
| image = Image.open(image) |
| |
| |
| text = pytesseract.image_to_string(image, lang=lang) |
| |
| return text |
| |
| except Exception as e: |
| error_msg = f"خطأ في استخراج النص من الصورة: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| return "" |
|
|
|
|
| def ocr_pdf(file_path, lang='ara+eng'): |
| """ |
| تنفيذ OCR على ملف PDF |
| |
| المعلمات: |
| file_path: مسار ملف PDF |
| lang: لغة النص (افتراضي: 'ara+eng' للعربية والإنجليزية) |
| |
| الإرجاع: |
| النص المستخرج من ملف PDF |
| """ |
| try: |
| |
| if not os.path.exists(file_path): |
| raise FileNotFoundError(f"الملف غير موجود: {file_path}") |
| |
| |
| document = fitz.open(file_path) |
| text = "" |
| |
| for page_idx in range(len(document)): |
| page = document.load_page(page_idx) |
| |
| |
| pix = page.get_pixmap(matrix=fitz.Matrix(300/72, 300/72)) |
| img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) |
| |
| |
| page_text = extract_text_from_image(img, lang=lang) |
| text += page_text + "\n\n" |
| |
| document.close() |
| |
| return text |
| |
| except Exception as e: |
| error_msg = f"خطأ في تنفيذ OCR على ملف PDF: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| raise Exception(error_msg) |
|
|
|
|
| def search_in_pdf(file_path, search_text): |
| """ |
| البحث عن نص في ملف PDF |
| |
| المعلمات: |
| file_path: مسار ملف PDF |
| search_text: النص المراد البحث عنه |
| |
| الإرجاع: |
| قائمة من النتائج {page_number, text_snippet, matched_text} |
| """ |
| try: |
| |
| if not os.path.exists(file_path): |
| raise FileNotFoundError(f"الملف غير موجود: {file_path}") |
| |
| |
| document = fitz.open(file_path) |
| results = [] |
| |
| for page_idx in range(len(document)): |
| page = document.load_page(page_idx) |
| page_text = page.get_text("text") |
| |
| |
| if search_text.lower() in page_text.lower(): |
| |
| lines = page_text.split('\n') |
| for line in lines: |
| if search_text.lower() in line.lower(): |
| results.append({ |
| 'page_number': page_idx + 1, |
| 'text_snippet': line, |
| 'matched_text': search_text |
| }) |
| |
| document.close() |
| |
| return results |
| |
| except Exception as e: |
| error_msg = f"خطأ في البحث في ملف PDF: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| raise Exception(error_msg) |
|
|
|
|
| def extract_quantities_from_pdf(file_path): |
| """ |
| استخراج الكميات من ملف PDF |
| |
| المعلمات: |
| file_path: مسار ملف PDF |
| |
| الإرجاع: |
| DataFrame يحتوي على البنود والكميات المستخرجة |
| """ |
| try: |
| |
| text = extract_text_from_pdf(file_path) |
| tables = extract_tables_from_pdf(file_path) |
| |
| quantities = [] |
| |
| |
| for table in tables: |
| |
| quantity_cols = [col for col in table.columns if any(term in col.lower() for term in ['كمية', 'عدد', 'الكمية'])] |
| unit_cols = [col for col in table.columns if any(term in col.lower() for term in ['وحدة', 'الوحدة'])] |
| item_cols = [col for col in table.columns if any(term in col.lower() for term in ['بند', 'وصف', 'البند', 'العمل'])] |
| |
| if quantity_cols and (unit_cols or item_cols): |
| quantity_col = quantity_cols[0] |
| unit_col = unit_cols[0] if unit_cols else None |
| item_col = item_cols[0] if item_cols else None |
| |
| |
| for _, row in table.iterrows(): |
| if pd.notna(row[quantity_col]) and (item_col is None or pd.notna(row[item_col])): |
| quantity_value = extract_numbers_from_text(row[quantity_col]) |
| quantity = quantity_value[0] if quantity_value else None |
| |
| quantities.append({ |
| 'البند': row[item_col] if item_col else "غير محدد", |
| 'الوحدة': row[unit_col] if unit_col else "غير محدد", |
| 'الكمية': quantity |
| }) |
| |
| |
| lines = text.split('\n') |
| for line in lines: |
| |
| if re.search(r'\d+(?:,\d+)*(?:\.\d+)?', line) and any(unit in line for unit in ['م2', 'م3', 'متر', 'طن', 'كجم', 'عدد']): |
| numbers = extract_numbers_from_text(line) |
| if numbers: |
| |
| unit_match = re.search(r'\b(م2|م3|متر مربع|متر مكعب|م\.ط|طن|كجم|عدد|قطعة)\b', line) |
| unit = unit_match.group(1) if unit_match else "غير محدد" |
| |
| quantities.append({ |
| 'البند': line, |
| 'الوحدة': unit, |
| 'الكمية': numbers[0] |
| }) |
| |
| |
| if quantities: |
| quantities_df = pd.DataFrame(quantities) |
| return quantities_df |
| else: |
| return pd.DataFrame(columns=['البند', 'الوحدة', 'الكمية']) |
| |
| except Exception as e: |
| error_msg = f"خطأ في استخراج الكميات من ملف PDF: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| raise Exception(error_msg) |
|
|
|
|
| def merge_pdfs(input_paths, output_path): |
| """ |
| دمج ملفات PDF متعددة في ملف واحد |
| |
| المعلمات: |
| input_paths: قائمة من مسارات ملفات PDF المراد دمجها |
| output_path: مسار ملف PDF الناتج |
| |
| الإرجاع: |
| True في حالة النجاح |
| """ |
| try: |
| |
| for file_path in input_paths: |
| if not os.path.exists(file_path): |
| raise FileNotFoundError(f"الملف غير موجود: {file_path}") |
| |
| |
| output_dir = os.path.dirname(output_path) |
| create_directory_if_not_exists(output_dir) |
| |
| |
| merger = PyPDF2.PdfMerger() |
| |
| for file_path in input_paths: |
| merger.append(file_path) |
| |
| merger.write(output_path) |
| merger.close() |
| |
| return True |
| |
| except Exception as e: |
| error_msg = f"خطأ في دمج ملفات PDF: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| raise Exception(error_msg) |
|
|
|
|
| def split_pdf(input_path, output_dir, prefix='page'): |
| """ |
| تقسيم ملف PDF إلى ملفات منفصلة لكل صفحة |
| |
| المعلمات: |
| input_path: مسار ملف PDF المراد تقسيمه |
| output_dir: دليل الإخراج |
| prefix: بادئة أسماء ملفات الإخراج |
| |
| الإرجاع: |
| قائمة من مسارات ملفات PDF الناتجة |
| """ |
| try: |
| |
| if not os.path.exists(input_path): |
| raise FileNotFoundError(f"الملف غير موجود: {input_path}") |
| |
| |
| create_directory_if_not_exists(output_dir) |
| |
| |
| with open(input_path, 'rb') as file: |
| reader = PyPDF2.PdfReader(file) |
| output_files = [] |
| |
| |
| for page_idx in range(len(reader.pages)): |
| writer = PyPDF2.PdfWriter() |
| writer.add_page(reader.pages[page_idx]) |
| |
| output_filename = f"{prefix}_{page_idx+1}.pdf" |
| output_path = os.path.join(output_dir, output_filename) |
| |
| with open(output_path, 'wb') as output_file: |
| writer.write(output_file) |
| |
| output_files.append(output_path) |
| |
| return output_files |
| |
| except Exception as e: |
| error_msg = f"خطأ في تقسيم ملف PDF: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| raise Exception(error_msg) |
|
|
|
|
| def export_pricing_to_pdf(data, output_path, title="تحليل الأسعار", description=""): |
| """ |
| تصدير بيانات التسعير إلى ملف PDF |
| |
| المعلمات: |
| data: DataFrame يحتوي على بيانات التسعير |
| output_path: مسار ملف PDF الناتج |
| title: عنوان التقرير |
| description: وصف إضافي للتقرير |
| |
| الإرجاع: |
| مسار ملف PDF الناتج |
| """ |
| try: |
| |
| if data is None or len(data) == 0: |
| raise ValueError("البيانات فارغة") |
| |
| |
| output_dir = os.path.dirname(output_path) |
| create_directory_if_not_exists(output_dir) |
| |
| |
| styles = getSampleStyleSheet() |
| |
| |
| try: |
| |
| if 'Arabic' not in pdfmetrics.getRegisteredFontNames(): |
| pdfmetrics.registerFont(TTFont('Arabic', 'utils/fonts/Amiri-Regular.ttf')) |
| |
| if 'ArabicBold' not in pdfmetrics.getRegisteredFontNames(): |
| pdfmetrics.registerFont(TTFont('ArabicBold', 'utils/fonts/Amiri-Bold.ttf')) |
| |
| |
| title_style = ParagraphStyle( |
| name='ArabicTitleStyle', |
| fontName='ArabicBold', |
| fontSize=16, |
| alignment=1, |
| leading=18 |
| ) |
| |
| normal_style = ParagraphStyle( |
| name='ArabicStyle', |
| fontName='Arabic', |
| fontSize=12, |
| alignment=1, |
| leading=14 |
| ) |
| |
| except: |
| |
| title_style = styles['Title'] |
| normal_style = styles['Normal'] |
| |
| |
| doc = SimpleDocTemplate( |
| output_path, |
| pagesize=landscape(A4), |
| rightMargin=30, |
| leftMargin=30, |
| topMargin=30, |
| bottomMargin=30 |
| ) |
| |
| |
| elements = [] |
| |
| |
| elements.append(Paragraph(title, title_style)) |
| elements.append(Spacer(1, 20)) |
| |
| |
| if description: |
| elements.append(Paragraph(description, normal_style)) |
| elements.append(Spacer(1, 20)) |
| |
| |
| data_list = [data.columns.tolist()] + data.values.tolist() |
| |
| |
| table = Table(data_list, repeatRows=1) |
| |
| |
| table_style = TableStyle([ |
| ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue), |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.black), |
| ('ALIGN', (0, 0), (-1, -1), 'CENTER'), |
| ('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'), |
| ('FONTSIZE', (0, 0), (-1, 0), 12), |
| ('BOTTOMPADDING', (0, 0), (-1, 0), 12), |
| ('BACKGROUND', (0, 1), (-1, -1), colors.white), |
| ('GRID', (0, 0), (-1, -1), 1, colors.black), |
| ('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'), |
| ('FONTSIZE', (0, 1), (-1, -1), 10), |
| ]) |
| |
| |
| for i in range(1, len(data_list)): |
| if i % 2 == 0: |
| table_style.add('BACKGROUND', (0, i), (-1, i), colors.lightgrey) |
| |
| table.setStyle(table_style) |
| elements.append(table) |
| |
| |
| numeric_columns = data.select_dtypes(include=['number']).columns |
| if len(numeric_columns) > 0: |
| elements.append(Spacer(1, 30)) |
| elements.append(Paragraph("ملخص إحصائي", title_style)) |
| elements.append(Spacer(1, 10)) |
| |
| |
| summary = data[numeric_columns].describe().reset_index() |
| summary_list = [['الإحصاءات'] + list(numeric_columns)] + summary.values.tolist() |
| |
| |
| summary_table = Table(summary_list, repeatRows=1) |
| summary_table.setStyle(TableStyle([ |
| ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue), |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.black), |
| ('ALIGN', (0, 0), (-1, -1), 'CENTER'), |
| ('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'), |
| ('FONTSIZE', (0, 0), (-1, 0), 12), |
| ('BOTTOMPADDING', (0, 0), (-1, 0), 12), |
| ('BACKGROUND', (0, 1), (-1, -1), colors.white), |
| ('GRID', (0, 0), (-1, -1), 1, colors.black), |
| ('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'), |
| ('FONTSIZE', (0, 1), (-1, -1), 10), |
| ])) |
| elements.append(summary_table) |
| |
| |
| if len(numeric_columns) > 0 and len(data) > 1: |
| for col in numeric_columns[:2]: |
| plt.figure(figsize=(8, 4)) |
| plt.barh(range(len(data)), data[col]) |
| plt.yticks(range(len(data)), data.iloc[:, 0] if len(data.columns) > 0 else range(len(data))) |
| plt.xlabel(col) |
| plt.title(f"مخطط {col}") |
| plt.tight_layout() |
| |
| |
| img_data = io.BytesIO() |
| plt.savefig(img_data, format='png') |
| img_data.seek(0) |
| plt.close() |
| |
| |
| elements.append(Spacer(1, 20)) |
| img = ReportLabImage(img_data, width=500, height=250) |
| elements.append(img) |
| |
| |
| doc.build(elements) |
| |
| return output_path |
| |
| except Exception as e: |
| error_msg = f"خطأ في تصدير بيانات التسعير إلى PDF: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| raise Exception(error_msg) |
|
|
|
|
| def export_pricing_with_analysis_to_pdf(items_data, analysis_data, output_path, title="تحليل الأسعار مع تحليل المكونات", project_info=None): |
| """ |
| تصدير بيانات التسعير مع تحليل المكونات إلى ملف PDF |
| |
| المعلمات: |
| items_data: DataFrame يحتوي على بيانات البنود |
| analysis_data: قاموس يحتوي على تحليل أسعار البنود {item_id: DataFrame} |
| output_path: مسار ملف PDF الناتج |
| title: عنوان التقرير |
| project_info: معلومات المشروع (قاموس) |
| |
| الإرجاع: |
| مسار ملف PDF الناتج |
| """ |
| try: |
| |
| if items_data is None or len(items_data) == 0: |
| raise ValueError("بيانات البنود فارغة") |
| |
| |
| output_dir = os.path.dirname(output_path) |
| create_directory_if_not_exists(output_dir) |
| |
| |
| styles = getSampleStyleSheet() |
| |
| |
| try: |
| |
| if 'Arabic' not in pdfmetrics.getRegisteredFontNames(): |
| pdfmetrics.registerFont(TTFont('Arabic', 'utils/fonts/Amiri-Regular.ttf')) |
| |
| if 'ArabicBold' not in pdfmetrics.getRegisteredFontNames(): |
| pdfmetrics.registerFont(TTFont('ArabicBold', 'utils/fonts/Amiri-Bold.ttf')) |
| |
| |
| title_style = ParagraphStyle( |
| name='ArabicTitleStyle', |
| fontName='ArabicBold', |
| fontSize=16, |
| alignment=1, |
| leading=18 |
| ) |
| |
| normal_style = ParagraphStyle( |
| name='ArabicStyle', |
| fontName='Arabic', |
| fontSize=12, |
| alignment=1, |
| leading=14 |
| ) |
| |
| subtitle_style = ParagraphStyle( |
| name='ArabicSubtitleStyle', |
| fontName='ArabicBold', |
| fontSize=14, |
| alignment=1, |
| leading=16 |
| ) |
| |
| except: |
| |
| title_style = styles['Title'] |
| normal_style = styles['Normal'] |
| subtitle_style = styles['Heading2'] |
| |
| |
| doc = SimpleDocTemplate( |
| output_path, |
| pagesize=landscape(A4), |
| rightMargin=30, |
| leftMargin=30, |
| topMargin=30, |
| bottomMargin=30 |
| ) |
| |
| |
| elements = [] |
| |
| |
| elements.append(Paragraph(title, title_style)) |
| elements.append(Spacer(1, 20)) |
| |
| |
| if project_info: |
| project_info_text = f"اسم المشروع: {project_info.get('اسم_المشروع', '')} | " |
| project_info_text += f"وصف المشروع: {project_info.get('وصف_المشروع', '')}" |
| elements.append(Paragraph(project_info_text, normal_style)) |
| elements.append(Spacer(1, 20)) |
| |
| |
| elements.append(Paragraph("جدول البنود", subtitle_style)) |
| elements.append(Spacer(1, 10)) |
| |
| |
| items_list = [items_data.columns.tolist()] + items_data.values.tolist() |
| |
| |
| items_table = Table(items_list, repeatRows=1) |
| |
| |
| items_table_style = TableStyle([ |
| ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue), |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.black), |
| ('ALIGN', (0, 0), (-1, -1), 'CENTER'), |
| ('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'), |
| ('FONTSIZE', (0, 0), (-1, 0), 12), |
| ('BOTTOMPADDING', (0, 0), (-1, 0), 12), |
| ('BACKGROUND', (0, 1), (-1, -1), colors.white), |
| ('GRID', (0, 0), (-1, -1), 1, colors.black), |
| ('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'), |
| ('FONTSIZE', (0, 1), (-1, -1), 10), |
| ]) |
| |
| |
| for i in range(1, len(items_list)): |
| if i % 2 == 0: |
| items_table_style.add('BACKGROUND', (0, i), (-1, i), colors.lightgrey) |
| |
| items_table.setStyle(items_table_style) |
| elements.append(items_table) |
| |
| |
| if analysis_data: |
| elements.append(Spacer(1, 30)) |
| elements.append(Paragraph("تحليل مكونات الأسعار", subtitle_style)) |
| |
| for item_id, analysis_df in analysis_data.items(): |
| |
| item_info = items_data[items_data['رقم البند'] == item_id].iloc[0] if 'رقم البند' in items_data.columns and len(items_data[items_data['رقم البند'] == item_id]) > 0 else None |
| |
| if item_info is not None: |
| item_title = f"تحليل مكونات البند: {item_id} - {item_info.get('وصف البند', '')}" |
| else: |
| item_title = f"تحليل مكونات البند: {item_id}" |
| |
| elements.append(Spacer(1, 20)) |
| elements.append(Paragraph(item_title, subtitle_style)) |
| elements.append(Spacer(1, 10)) |
| |
| |
| analysis_list = [analysis_df.columns.tolist()] + analysis_df.values.tolist() |
| |
| |
| analysis_table = Table(analysis_list, repeatRows=1) |
| |
| |
| analysis_table_style = TableStyle([ |
| ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue), |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.black), |
| ('ALIGN', (0, 0), (-1, -1), 'CENTER'), |
| ('FONTNAME', (0, 0), (-1, 0), 'ArabicBold' if 'ArabicBold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'), |
| ('FONTSIZE', (0, 0), (-1, 0), 12), |
| ('BOTTOMPADDING', (0, 0), (-1, 0), 12), |
| ('BACKGROUND', (0, 1), (-1, -1), colors.white), |
| ('GRID', (0, 0), (-1, -1), 1, colors.black), |
| ('FONTNAME', (0, 1), (-1, -1), 'Arabic' if 'Arabic' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'), |
| ('FONTSIZE', (0, 1), (-1, -1), 10), |
| ]) |
| |
| |
| for i in range(1, len(analysis_list)): |
| if i % 2 == 0: |
| analysis_table_style.add('BACKGROUND', (0, i), (-1, i), colors.lightgrey) |
| |
| analysis_table.setStyle(analysis_table_style) |
| elements.append(analysis_table) |
| |
| |
| if 'الإجمالي' in analysis_df.columns and len(analysis_df) > 0: |
| total_analysis_price = analysis_df['الإجمالي'].sum() |
| total_text = f"إجمالي تكلفة البند من التحليل: {total_analysis_price:,.2f} ريال" |
| elements.append(Spacer(1, 10)) |
| elements.append(Paragraph(total_text, normal_style)) |
| |
| |
| doc.build(elements) |
| |
| return output_path |
| |
| except Exception as e: |
| error_msg = f"خطأ في تصدير تحليل الأسعار إلى PDF: {str(e)}" |
| print(error_msg) |
| traceback.print_exc() |
| raise Exception(error_msg) |
|
|