|
""" |
|
معالج ملفات 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) |
|
|