Исправь все ошибки и оптимизируй код - import os
import pandas as pd
from PyQt6.QtWidgets import (
    QApplication,
    QMainWindow,
    QVBoxLayout,
    QHBoxLayout,
    QWidget,
    QPushButton,
    QListWidget,
    QLabel,
    QFileDialog,
    QMessageBox,
    QTextEdit,
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from reportlab.lib.pagesizes import 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
# Убедитесь, что файлы шрифтов находятся в рабочей директории или укажите полный путь
try:
    pdfmetrics.registerFont(TTFont('DejaVuSans', 'DejaVuSans.ttf'))
    pdfmetrics.registerFont(TTFont('DejaVuSans-Bold', 'DejaVuSans-Bold.ttf'))
except Exception as e:
    logging.error(f"Ошибка при регистрации шрифтов: {e}")
    raise

# Установка размера страницы 297x419 мм (A3 формата)
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()
            if data.empty:
                self.log_signal.emit("Нет данных для обработки.")
                return
            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 = [
            "Период с", "Период по", "Код ТТ", "Название ТТ", "Тип оборудования",
            "Наименование проекта", "Наименование оборудования", "Полный период размещения",
            "Ссылка на ПГ", "Зона размещения",
            "Действия ТТ по окончанию размещения", "Код позиции",
            "Наименование позиции", "Загрузка в шт"
        ]
        optional_columns = ["Комментарий", "Место выкладки товара (адрес)"]

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

        # Добавление отсутствующих необязательных столбцов с дефолтными значениями
        for col in optional_columns:
            if col not in data.columns:
                data[col] = "-"
                self.log_signal.emit(f"Информация: столбец '{col}' не найден и был добавлен с дефолтными значениями.")

        # Конвертируем даты, игнорируя ошибки
        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()}"
                )
                # Проверка необходимых столбцов в файле с фотографиями
                required_photo_columns = ["Название проекта", "Путь"]
                missing_photo_columns = [col for col in required_photo_columns if col not in file_data.columns]
                if missing_photo_columns:
                    self.log_signal.emit(f"Ошибка: отсутствуют столбцы {missing_photo_columns} в файле с фотографиями {file_path}.")
                    continue
                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 файла
            safe_tt_name = "".join([c if c.isalnum() or c in (' ', '_', '-') else "_" for c in tt_name])
            pdf_name = f"{tt_code}_{safe_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
            y_position = height - 50  # Начинаем 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:
                    # Проверяем, достаточно ли места для заголовка оборудования
                    if y_position < 100:
                        c.showPage()
                        y_position = height - 50
                        self.draw_text(c, "Администратору необходимо осуществить выкладку товара на ДМП:", 30, y_position, "DejaVuSans", 10)
                        y_position -= 20

                    # Выводим заголовок типа оборудования
                    self.draw_text(c, f"Оборудование: {equipment_type}", 30, y_position, "DejaVuSans-Bold", 10)
                    y_position -= 20

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

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

                        for detail in details:
                            if y_position < 100:
                                c.showPage()
                                y_position = height - 50
                                self.draw_text(c, "Администратору необходимо осуществить выкладку товара на ДМП:", 30, y_position, "DejaVuSans", 10)
                                y_position -= 20
                                self.draw_text(c, f"Оборудование: {equipment_type}", 30, y_position, "DejaVuSans-Bold", 10)
                                y_position -= 20
                                self.draw_text(c, f"Проект: {project}", 30, y_position, "DejaVuSans", 8)
                                y_position -= 20

                            self.draw_text(c, detail, 40, y_position, "DejaVuSans", 8)
                            y_position -= 18

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

                        # Проверка наличия необходимых столбцов
                        if all(col in project_data.columns for col in table_columns):
                            table_data = project_data[table_columns].drop_duplicates().fillna("-")
                            self.draw_text(c, "Товарные позиции:", 40, y_position, "DejaVuSans", 8)
                            y_position -= 20

                            # Определение максимальной ширины таблицы
                            max_table_width = 140 * 2.83465  # 140 мм в пунктах

                            # Определение ширины столбцов
                            col_widths = []
                            for col in table_columns:
                                max_text = max(table_data[col].astype(str).apply(self.string_width, font_name="DejaVuSans", font_size=7).max(),
                                               self.string_width(col, "DejaVuSans-Bold", 7))
                                col_width = max_text + 10  # Добавляем немного отступа
                                col_widths.append(col_width)

                            table_width = sum(col_widths)
                            scaling_factor = 1.0
                            if table_width > max_table_width:
                                scaling_factor = max_table_width / table_width
                                col_widths = [w * scaling_factor for w in col_widths]
                                table_width = max_table_width

                            current_x = 40  # Начальная позиция X для таблицы

                            # Рисуем заголовки таблицы
                            c.setFont("DejaVuSans-Bold", 7)
                            for i, col in enumerate(table_columns):
                                c.drawString(current_x + 2, y_position, col)
                                # Рисуем границу ячейки заголовка
                                c.rect(current_x, y_position - 2, col_widths[i], 12)
                                current_x += col_widths[i]
                            y_position -= 14

                            # Рисуем строки таблицы
                            c.setFont("DejaVuSans", 7)
                            for _, row in table_data.iterrows():
                                current_x = 40
                                for i, col in enumerate(table_columns):
                                    cell_text = str(row[col])
                                    c.drawString(current_x + 2, y_position, cell_text)
                                    # Рисуем границу ячейки
                                    c.rect(current_x, y_position - 2, col_widths[i], 12)
                                    current_x += col_widths[i]
                                y_position -= 14
                                # Проверяем, не вышли ли за пределы страницы
                                if y_position < 100:
                                    c.showPage()
                                    y_position = height - 50

                            # Определяем зону для фотографий (справа от таблицы)
                            photo_zone_x = 40 + table_width + (10 * 2.83465)  # 10 мм отступ
                            photo_zone_width = width - photo_zone_x - (10 * 2.83465)  # 10 мм отступ справа

                            # Получаем все фотографии для текущего проекта
                            photo_rows = photo_data.loc[photo_data['Название проекта'] == project]
                            if not photo_rows.empty:
                                for _, photo_row in photo_rows.iterrows():
                                    photo_path = photo_row['Путь']
                                    if os.path.exists(photo_path):
                                        try:
                                            img = ImageReader(photo_path)
                                            img_width, img_height = img.getSize()
                                            aspect_ratio = img_height / img_width

                                            # Максимальная ширина и высота фотографии
                                            max_photo_width = photo_zone_width - 20  # 10 мм отступ с каждой стороны
                                            max_photo_height = y_position - 50  # Оставляем верхний предел

                                            # Масштабируем изображение, сохраняя соотношение сторон
                                            scale_factor = min(max_photo_width / img_width, max_photo_height / img_height, 1)
                                            display_width = img_width * scale_factor
                                            display_height = img_height * scale_factor

                                            # Проверяем, помещается ли фотография на текущей странице
                                            if y_position - display_height < 50:
                                                c.showPage()
                                                y_position = height - 50

                                            # Рисуем изображение в выделенной зоне
                                            c.drawImage(
                                                img,
                                                photo_zone_x,  # Начальная позиция X
                                                y_position - display_height,  # Начальная позиция Y
                                                width=display_width,
                                                height=display_height,
                                                preserveAspectRatio=True,
                                                mask='auto'
                                            )
                                            y_position -= (display_height + 10)  # Обновляем позицию Y после добавления фото
                                        except Exception as img_e:
                                            self.log_signal.emit(f"Ошибка при добавлении фотографии {photo_path}: {img_e}")
                                    else:
                                        self.log_signal.emit(f"Фотография для проекта {project} не найдена: {photo_path}")
                            else:
                                self.log_signal.emit(f"Фотографии для проекта '{project}' не найдены.")

                        else:
                            self.log_signal.emit(f"Табличные данные отсутствуют для проекта '{project}' или необходимые столбцы отсутствуют.")

                        # Проверяем, не нужно ли начать новую страницу после каждого проекта
                        if y_position < 100:
                            c.showPage()
                            y_position = height - 50

            # Завершение и сохранение PDF
            c.save()
            self.log_signal.emit(f"PDF создан: {pdf_path}")

    def string_width(text, font_name, font_size):
        """Вспомогательная функция для расчета ширины текста."""
        return pdfmetrics.stringWidth(text, font_name, font_size)

    def draw_text(self, canvas_obj, text, x, y, font_name, font_size):
        """Вспомогательный метод для рисования текста."""
        canvas_obj.setFont(font_name, font_size)
        canvas_obj.drawString(x, y, text)


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:
            if folder_path not in self.folder_paths:
                self.folder_paths.append(folder_path)
                self.folder_listbox.addItem(folder_path)
                self.log_info(f"Добавлена папка: {folder_path}")
            else:
                QMessageBox.information(self, "Информация", "Эта папка уже добавлена.")
                self.log_info(f"Попытка добавить уже существующую папку: {folder_path}")

    def remove_folder(self):
        selected_items = self.folder_listbox.selectedItems()
        if not selected_items:
            QMessageBox.information(self, "Информация", "Пожалуйста, выберите папку для удаления.")
            return
        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):
        save_path = QFileDialog.getExistingDirectory(self, "Выберите путь для сохранения PDF")
        if save_path:
            self.save_path = 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
            display_paths = ', '.join([os.path.basename(path) for path in file_paths])
            self.photo_files_label.setText(f"Выбраны файлы: {display_paths}")
            self.log_info(f"Выбраны файлы с фотографиями: {display_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.start_button.setEnabled(False)
        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.finished.connect(self.processing_finished)
        self.pdf_thread.start()
        self.log_info("Начато выполнение процесса генерации PDF.")

    def processing_finished(self):
        self.start_button.setEnabled(True)
        self.log_info("Процесс генерации PDF завершен.")

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

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


if __name__ == "__main__":
    app = QApplication([])
    window = PDFGeneratorApp()
    window.show()
    app.exec()