import os
import pandas as pd
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QListWidget, QLabel, QFileDialog, QMessageBox, QTextEdit
from PyQt6.QtGui import QFontDatabase, QFont
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from reportlab.lib.pagesizes import landscape, A4
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.utils import ImageReader
from datetime import datetime
import logging

# Настройка логирования
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# Загрузка шрифтов DejaVuSans и DejaVuSans-Bold
pdfmetrics.registerFont(TTFont('DejaVuSans', 'DejaVuSans.ttf'))
pdfmetrics.registerFont(TTFont('DejaVuSans-Bold', 'DejaVuSans-Bold.ttf'))

# Установка размера страницы 297x419 мм
custom_page_size = (297 * 2.83465, 419 * 2.83465)  # Перевод мм в точки (1 мм = 2.83465 точки)

class PDFGeneratorThread(QThread):
    log_signal = pyqtSignal(str)

    def __init__(self, folder_paths, save_path, photo_files):
        super().__init__()
        self.folder_paths = folder_paths
        self.save_path = save_path
        self.photo_files = photo_files

    def run(self):
        try:
            data = self.aggregate_data()
            photo_data = self.aggregate_photo_data()
            self.generate_pdfs(data, photo_data)
        except Exception as e:
            self.log_signal.emit(f"Ошибка при выполнении: {e}")

    def aggregate_data(self):
        data = pd.DataFrame()
        for folder in self.folder_paths:
            for file in os.listdir(folder):
                if file.endswith(".xlsx") or file.endswith(".xls"):
                    file_path = os.path.join(folder, file)
                    try:
                        xls = pd.ExcelFile(file_path)
                        for sheet_name in xls.sheet_names:
                            file_data = pd.read_excel(file_path, sheet_name=sheet_name)
                            self.log_signal.emit(f"Прочитан файл: {file_path}, лист: {sheet_name}, столбцы: {file_data.columns.tolist()}")
                            data = pd.concat([data, file_data], ignore_index=True)
                    except Exception as e:
                        self.log_signal.emit(f"Ошибка при чтении файла {file_path}: {e}")

        # Проверка имен столбцов
        required_columns = ["Период с", "Период по"]
        for col in required_columns:
            if col not in data.columns:
                self.log_signal.emit(f"Ошибка: столбец '{col}' не найден в данных.")
                return pd.DataFrame()

        # Конвертируем даты, игнорируя ошибки
        data["Период с"] = pd.to_datetime(data["Период с"], format="%d.%m.%Y", errors='coerce')
        data["Период по"] = pd.to_datetime(data["Период по"], format="%d.%m.%Y", errors='coerce')
        return data

    def aggregate_photo_data(self):
        photo_data = pd.DataFrame()
        for file_path in self.photo_files:
            try:
                file_data = pd.read_excel(file_path)
                self.log_signal.emit(f"Прочитан файл с фотографиями: {file_path}, столбцы: {file_data.columns.tolist()}")
                photo_data = pd.concat([photo_data, file_data], ignore_index=True)
            except Exception as e:
                self.log_signal.emit(f"Ошибка при чтении файла {file_path}: {e}")
        return photo_data

    def generate_pdfs(self, data, photo_data):
        # Группируем данные по торговым точкам
        data_by_tt = data.groupby("Код ТТ")
        for tt_code, tt_data in data_by_tt:
            tt_name = tt_data["Название ТТ"].iloc[0]
            period_start = tt_data["Период с"].min()
            period_end = tt_data["Период по"].max()

            # Преобразуем даты в строки для имени файла
            period_start_str = period_start.strftime('%Y-%m-%d') if pd.notnull(period_start) else "unknown_start"
            period_end_str = period_end.strftime('%Y-%m-%d') if pd.notnull(period_end) else "unknown_end"

            # Имя PDF файла
            pdf_name = f"{tt_code}_{tt_name}_{period_start_str}_{period_end_str}.pdf"
            pdf_path = os.path.join(self.save_path, pdf_name)

            # Создание PDF
            c = canvas.Canvas(pdf_path, pagesize=custom_page_size)
            width, height = custom_page_size

            # Заголовок
            y_position = height - 50
            self.draw_text(c, "Администратору необходимо осуществить выкладку товара на ДМП:", 30, y_position, "DejaVuSans", 10)
            y_position -= 20

            # Группируем данные по типу оборудования
            equipment_types = ["Магнит-хит", "Оборудование КА", "Навесное оборудование", "Оборудование сети", "Желтый акционный стеллаж"]
            for equipment_type in equipment_types:
                eq_data = tt_data[tt_data["Тип оборудования"] == equipment_type]
                if not eq_data.empty:
                    # Выводим заголовок типа оборудования
                    self.draw_text(c, f"Оборудование: {equipment_type}", 30, y_position, "DejaVuSans-Bold", 10)
                    y_position -= 20

                    # Основные данные по каждому типу оборудования
                    for project, project_data in eq_data.groupby("Наименование проекта"):
                        project_start_y = y_position
                        self.draw_text(c, f"Проект: {project}", 30, y_position, "DejaVuSans", 8)
                        y_position -= 20

                        if equipment_type == "Магнит-хит" or equipment_type == "Оборудование сети":
                            self.draw_text(c, f"Название оборудования: {project_data['Наименование оборудования'].iloc[0]}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                            self.draw_text(c, f"Полный период размещения: {project_data['Полный период размещения'].iloc[0]}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                            self.draw_text(c, f"Ссылка на планограмму: {project_data['Ссылка на ПГ'].iloc[0] if pd.notnull(project_data['Ссылка на ПГ'].iloc[0]) else '-'}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                            self.draw_text(c, f"Комментарий: {project_data['Комментарий'].iloc[0] if 'Комментарий' in project_data.columns and pd.notnull(project_data['Комментарий'].iloc[0]) else '-'}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                        elif equipment_type == "Навесное оборудование" or equipment_type == "Оборудование КА":
                            self.draw_text(c, f"Зона размещения: {project_data['Зона размещения'].iloc[0] if pd.notnull(project_data['Зона размещения'].iloc[0]) else '-'}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                            self.draw_text(c, f"Название оборудования: {project_data['Наименование оборудования'].iloc[0]}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                            self.draw_text(c, f"Полный период размещения: {project_data['Полный период размещения'].iloc[0]}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                            self.draw_text(c, f"Действия ТТ по окончанию размещения: {project_data['Действия ТТ по окончанию размещения'].iloc[0]}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                            self.draw_text(c, f"Комментарий: {project_data['Комментарий'].iloc[0] if 'Комментарий' in project_data.columns and pd.notnull(project_data['Комментарий'].iloc[0]) else '-'}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                        elif equipment_type == "Желтый акционный стеллаж":
                            self.draw_text(c, f"Зона размещения: {project_data['Зона размещения'].iloc[0]}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                            self.draw_text(c, f"Ссылка на планограмму: {project_data['Ссылка на ПГ'].iloc[0] if pd.notnull(project_data['Ссылка на ПГ'].iloc[0]) else '-'}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20
                            self.draw_text(c, f"Комментарий: {project_data['Комментарий'].iloc[0] if 'Комментарий' in project_data.columns and pd.notnull(project_data['Комментарий'].iloc[0]) else '-'}", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20

                        # Создание таблицы с уникальными данными
                        if equipment_type == "Магнит-хит" or equipment_type == "Оборудование сети" or equipment_type == "Желтый акционный стеллаж":
                            table_columns = ["Код позиции", "Наименование позиции", "Загрузка в шт", "Место выкладки товара (адрес)"]
                        elif equipment_type == "Навесное оборудование" or equipment_type == "Оборудование КА":
                            table_columns = ["Код позиции", "Наименование позиции", "Загрузка в шт"]

                        if all(col in project_data.columns for col in table_columns):
                            table_data = project_data[table_columns].drop_duplicates()
                            table_data = table_data.fillna("-")
                            self.draw_text(c, "Товарные позиции:", 30, y_position, "DejaVuSans", 8)
                            y_position -= 20

                            # Выделение всех границ таблицы
                            col_widths = [max(table_data[col].astype(str).apply(len).max() * 6, 50) for col in table_columns]
                            row_height = 15
                            table_width = sum(col_widths)
                            table_height = len(table_data) * row_height

                            # Рисуем таблицу
                            for i, row in enumerate(table_data.itertuples()):
                                if y_position - (i + 1) * row_height < 50:
                                    c.showPage()
                                    y_position = height - 50 - 10  # Добавляем отступ в 10 мм
                                for j, value in enumerate(row[1:]):
                                    c.rect(30 + sum(col_widths[:j]), y_position - (i + 1) * row_height, col_widths[j], row_height)
                                    c.setFont("DejaVuSans", 7)
                                    c.drawString(35 + sum(col_widths[:j]), y_position - (i + 1) * row_height + 3, str(value))
                            y_position -= table_height

                            # Добавление фотографии оборудования
                            photo_rows = photo_data.loc[photo_data['Название проекта'] == project]
                            if not photo_rows.empty:
                                photo_path = photo_rows['Путь'].iloc[0]
                                if os.path.exists(photo_path):
                                    img = ImageReader(photo_path)
                                    img_width, img_height = img.getSize()
                                    aspect_ratio = img_height / img_width

                                    # Ограничиваем размер фотографии
                                    max_photo_height = project_start_y - y_position - 20  # Расстояние от первой строки проекта до "Проект:"
                                    max_photo_width = width - table_width - 40  # 10 мм отступ с каждой стороны

                                    # Уменьшаем размер фотографии, если она не помещается
                                    if img_width > max_photo_width or img_height > max_photo_height:
                                        scale_factor = min(max_photo_width / img_width, max_photo_height / img_height)
                                        img_width *= scale_factor
                                        img_height *= scale_factor

                                    # Проверка на перекрытие фотографий
                                    if y_position - img_height < 50:
                                        c.showPage()
                                        y_position = height - 50 - 10  # Добавляем отступ в 10 мм

                                    # Размещение фотографии справа от текста и таблиц
                                    photo_y_position = project_start_y - img_height
                                    c.drawImage(img, width - img_width - 30, photo_y_position, width=img_width, height=img_height)
                                    y_position -= img_height + 10  # Добавляем отступ между фотографиями
                                else:
                                    self.log_signal.emit(f"Фотография для проекта {project} не найдена.")
                            else:
                                self.log_signal.emit(f"Фотография для проекта {project} не найдена.")

                        # Проверка на необходимость создания новой страницы
                        if y_position < 100:
                            c.showPage()
                            y_position = height - 50 - 10  # Добавляем отступ в 10 мм

            c.save()
            self.log_signal.emit(f"Создан PDF файл: {pdf_path}")

        self.log_signal.emit("PDF файлы успешно созданы!")

    def draw_text(self, canvas, text, x, y, font_name, font_size):
        canvas.setFont(font_name, font_size)
        canvas.drawString(x, y, text)

    def wrap_text(self, canvas, text, max_width):
        """Разбивает текст на строки, если он превышает максимальную ширину."""
        words = text.split()
        lines = []
        current_line = ""
        for word in words:
            test_line = current_line + word + " "
            width = canvas.stringWidth(test_line, "DejaVuSans", 8)
            if width > max_width:
                lines.append(current_line.strip())
                current_line = word + " "
            else:
                current_line = test_line
        lines.append(current_line.strip())
        return lines

class PDFGeneratorApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Excel to PDF Converter")
        self.folder_paths = []
        self.save_path = ""
        self.photo_files = []

        # Основной виджет
        main_widget = QWidget()
        self.setCentralWidget(main_widget)

        # Основной макет
        main_layout = QVBoxLayout()
        main_widget.setLayout(main_layout)

        # Список для отображения выбранных путей к папкам
        self.folder_listbox = QListWidget()
        main_layout.addWidget(self.folder_listbox)

        # Кнопки для управления папками
        folder_buttons_layout = QHBoxLayout()
        main_layout.addLayout(folder_buttons_layout)

        self.add_folder_button = QPushButton("Добавить папку")
        self.add_folder_button.clicked.connect(self.add_folder)
        folder_buttons_layout.addWidget(self.add_folder_button)

        self.remove_folder_button = QPushButton("Удалить папку")
        self.remove_folder_button.clicked.connect(self.remove_folder)
        folder_buttons_layout.addWidget(self.remove_folder_button)

        # Кнопка для выбора пути сохранения
        self.save_path_button = QPushButton("Выбрать путь для сохранения PDF")
        self.save_path_button.clicked.connect(self.choose_save_path)
        main_layout.addWidget(self.save_path_button)

        # Поле для отображения пути для сохранения PDF
        self.save_path_label = QLabel("Путь для сохранения PDF не выбран")
        main_layout.addWidget(self.save_path_label)

        # Кнопка для выбора файлов с фотографиями
        self.photo_files_button = QPushButton("Выбрать файлы с фотографиями")
        self.photo_files_button.clicked.connect(self.choose_photo_files)
        main_layout.addWidget(self.photo_files_button)

        # Поле для отображения пути к файлам с фотографиями
        self.photo_files_label = QLabel("Файлы с фотографиями не выбраны")
        main_layout.addWidget(self.photo_files_label)

        # Кнопка для запуска программы
        self.start_button = QPushButton("Запуск")
        self.start_button.clicked.connect(self.start_processing)
        main_layout.addWidget(self.start_button)

        # Кнопка для закрытия программы
        self.close_button = QPushButton("Закрыть")
        self.close_button.clicked.connect(self.close)
        main_layout.addWidget(self.close_button)

        # Поле для вывода логов
        self.log_text_edit = QTextEdit()
        self.log_text_edit.setReadOnly(True)
        main_layout.addWidget(self.log_text_edit)

        # Применение стилей
        self.apply_styles()

    def apply_styles(self):
        stylesheet = """
        QMainWindow {
            background-color: #f0f0f0;
        }
        QPushButton {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            font-size: 14px;
            margin: 5px;
        }
        QPushButton:hover {
            background-color: #45a049;
        }
        QListWidget {
            background-color: white;
            border: 1px solid #ddd;
            border-radius: 5px;
            padding: 10px;
            font-size: 14px;
        }
        QLabel {
            font-size: 14px;
            color: #333;
        }
        QTextEdit {
            background-color: white;
            border: 1px solid #ddd;
            border-radius: 5px;
            padding: 10px;
            font-size: 12px;
        }
        """
        self.setStyleSheet(stylesheet)

    def add_folder(self):
        folder_path = QFileDialog.getExistingDirectory(self, "Выберите папку")
        if folder_path:
            self.folder_paths.append(folder_path)
            self.folder_listbox.addItem(folder_path)
            self.log_info(f"Добавлена папка: {folder_path}")

    def remove_folder(self):
        selected_items = self.folder_listbox.selectedItems()
        for item in selected_items:
            self.folder_paths.remove(item.text())
            self.folder_listbox.takeItem(self.folder_listbox.row(item))
            self.log_info(f"Удалена папка: {item.text()}")

    def choose_save_path(self):
        self.save_path = QFileDialog.getExistingDirectory(self, "Выберите путь для сохранения PDF")
        if self.save_path:
            self.save_path_label.setText(f"Сохранение в: {self.save_path}")
            self.log_info(f"Выбран путь для сохранения PDF: {self.save_path}")

    def choose_photo_files(self):
        file_paths, _ = QFileDialog.getOpenFileNames(self, "Выберите файлы с фотографиями", "", "Excel files (*.xlsx *.xls)")
        if file_paths:
            self.photo_files = file_paths
            self.photo_files_label.setText(f"Выбраны файлы: {', '.join(file_paths)}")
            self.log_info(f"Выбраны файлы с фотографиями: {', '.join(file_paths)}")

    def start_processing(self):
        if not self.folder_paths:
            QMessageBox.warning(self, "Ошибка", "Пожалуйста, выберите хотя бы одну папку с файлами Excel.")
            self.log_error("Ошибка: не выбрана ни одна папка с файлами Excel.")
            return
        if not self.save_path:
            QMessageBox.warning(self, "Ошибка", "Пожалуйста, выберите путь для сохранения PDF файлов.")
            self.log_error("Ошибка: не выбран путь для сохранения PDF файлов.")
            return
        if not self.photo_files:
            QMessageBox.warning(self, "Ошибка", "Пожалуйста, выберите файлы с фотографиями.")
            self.log_error("Ошибка: не выбраны файлы с фотографиями.")
            return

        self.pdf_thread = PDFGeneratorThread(self.folder_paths, self.save_path, self.photo_files)
        self.pdf_thread.log_signal.connect(self.log_info)
        self.pdf_thread.start()

    def log_info(self, message):
        self.log_text_edit.append(f"INFO: {message}")

    def log_error(self, message):
        self.log_text_edit.append(f"ERROR: {message}")

if __name__ == "__main__":
    app = QApplication([])
    window = PDFGeneratorApp()
    window.show()
    app.exec()
Давай изменим логику расположения таблиц и фото
1. Таблицы должны занимать место по горизонтали страницы в соответствии с шириной текста в ячейках таблицы. Размер страницы установить на  297x419 мм
2. Для расположения фото должна быть следующая логика - Оставшееся место справа от таблицы по каждому проекту должно использоваться для размещения фото. Необходимо представить это место как прямоугольную зону которая начинается сверху со строки с названием проекта, слева с 10 мм отступа от таблицы, снизу 10 мм отступ от конца первой строки следующего проекта или конца листа, справа 10 мм отступ от края листа