turns-00053.parquet:33188
edb48780342b4a5f1eededa2
turn 2/5gpt-4o-mini-2024-07-18EnglishRussia2140 words
degenerate_repetitionAbsentFinal dense release
USER
Фото не разместились, та же ошибка
ASSISTANT
Давайте разберемся с ситуацией, когда изображения не помещаются на странице. Основная проблема заключается в том, чтобы правильно вычислить доступное пространство для размещения изображений после таблицы.
Вот улучшенный вариант кода, который должен более точно обрабатывать размещение изображений, уменьшая размер, если это необходимо:
```python
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 QThread, pyqtSignal
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.utils import ImageReader
import logging
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def register_fonts():
try:
pdfmetrics.registerFont(TTFont('DejaVuSans', 'DejaVuSans.ttf'))
pdfmetrics.registerFont(TTFont('DejaVuSans-Bold', 'DejaVuSans-Bold.ttf'))
except Exception as e:
logging.error(f"Ошибка при регистрации шрифтов: {e}")
raise
register_fonts()
# Задаем размер страницы 297 мм x 420 мм (формат A3)
custom_page_size = (297 * mm, 420 * mm)
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:
if not os.path.isdir(folder):
self.log_signal.emit(f"Предупреждение: {folder} — не существует.")
continue
for file in os.listdir(folder):
if file.endswith(('.xlsx', '.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)
# Нормализация названий столбцов
file_data.columns = file_data.columns.str.strip()
if not file_data.empty:
self.log_signal.emit(
f"Прочитан файл: {file_path}, лист: {sheet_name}, столбцы: {file_data.columns.tolist()}"
)
data = pd.concat([data, file_data], ignore_index=True)
else:
self.log_signal.emit(f"Предупреждение: Лист {sheet_name} в файле {file_path} пустой.")
except Exception as e:
self.log_signal.emit(f"Ошибка при чтении файла {file_path}: {e}")
# Нормализация названий столбцов в основном DataFrame
if not data.empty:
data.columns = data.columns.str.strip()
return self.check_columns(data)
def check_columns(self, data):
# Преобразование дат в формат datetime, если есть соответствующие столбцы
if 'Период с' in data.columns:
data["Период с"] = pd.to_datetime(data["Период с"], dayfirst=True, errors='coerce')
if 'Период по' in data.columns:
data["Период по"] = pd.to_datetime(data["Период по"], dayfirst=True, errors='coerce')
return data
def aggregate_photo_data(self):
photo_data = pd.DataFrame()
for file_path in self.photo_files:
if not os.path.exists(file_path):
self.log_signal.emit(f"Предупреждение: Файл {file_path} не существует.")
continue
try:
file_data = pd.read_excel(file_path)
# Нормализация названий столбцов
file_data.columns = file_data.columns.str.strip()
if not file_data.empty:
self.log_signal.emit(
f"Прочитан файл с фотографиями: {file_path}, столбцы: {file_data.columns.tolist()}"
)
photo_data = pd.concat([photo_data, file_data], ignore_index=True)
else:
self.log_signal.emit(f"Предупреждение: Файл с фотографиями {file_path} пустой.")
except Exception as e:
self.log_signal.emit(f"Ошибка при чтении файла {file_path}: {e}")
return photo_data
def generate_pdfs(self, data, photo_data):
if "Код ТТ" in data.columns:
data_by_tt = data.groupby("Код ТТ")
else:
self.log_signal.emit("Отсутствует столбец 'Код ТТ' в данных. Невозможно сгенерировать PDF.")
return
for tt_code, tt_data in data_by_tt:
if tt_data.empty:
continue
if "Название ТТ" in tt_data.columns:
tt_name_series = tt_data["Название ТТ"].dropna()
if not tt_name_series.empty:
tt_name = tt_name_series.iloc[0]
else:
tt_name = "Unknown TT Name"
else:
tt_name = "Unknown TT Name"
period_start = tt_data["Период с"].min() if "Период с" in tt_data.columns else None
period_end = tt_data["Период по"].max() if "Период по" in tt_data.columns else None
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"
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)
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:
if "Тип оборудования" in tt_data.columns:
eq_data = tt_data[tt_data["Тип оборудования"] == equipment_type]
if not eq_data.empty:
y_position = self.start_new_page_if_needed(c, y_position, height)
self.draw_text(c, f"Оборудование: {equipment_type}", 30, y_position, "DejaVuSans-Bold", 10)
y_position -= 20
y_position = self.process_projects(c, eq_data, equipment_type, photo_data, width, height, y_position)
c.save()
self.log_signal.emit(f"PDF создан: {pdf_path}")
def process_projects(self, c, eq_data, equipment_type, photo_data, width, height, y_position):
left_margin = 30
right_margin = 30
column_gap = 10
max_page_width = width - left_margin - right_margin
left_column_width = max_page_width * 0.6
right_column_width = max_page_width * 0.4
for project, project_data in eq_data.groupby("Наименование проекта"):
if project_data.empty:
continue
# Определяем высоту, необходимую для вывода деталей проекта
font_size = 8
detail_lines = self.get_details(equipment_type, project_data)
detail_height = len(detail_lines) * (font_size + 10) # приблизительная высота деталей
project_title_height = font_size + 10 # высота строки "Проект:..."
# Проверяем, хватит ли места на текущей странице для деталей проекта
required_height = project_title_height + detail_height + 50 # +50 для запаса
if y_position - required_height < 100:
# Места не хватает, переносим весь проект на новую страницу
c.showPage()
y_position = height - 50
# Начинаем с левого столбца
current_x = left_margin
project_title_y = y_position # Запоминаем y_position для верхней части фото
max_text_width = left_column_width - 20 # Отнимаем отступы
self.draw_wrapped_text(c, f"Проект: {project}", current_x, y_position, "DejaVuSans", font_size, max_text_width)
y_position -= 20
details = detail_lines
for detail in details:
y_position -= 18
self.draw_wrapped_text(c, detail, current_x + 10, y_position, "DejaVuSans", font_size, max_text_width - 10)
y_position -= 10 # Отступ после деталей
# Сохраняем y_position перед выводом таблицы
y_position_before_table = y_position
table_columns = self.get_table_columns(equipment_type, project_data)
if table_columns:
table_data = project_data[table_columns].drop_duplicates().fillna("-")
y_position_after_table = self.draw_table(c, table_data, table_columns, left_column_width, y_position, current_x)
else:
y_position_after_table = y_position
self.log_signal.emit(f"Табличные данные отсутствуют или необходимые столбцы отсутствуют для проекта '{project}'.")
# Теперь добавляем изображения после вывода таблицы
# y_top остается прежним, y_bottom обновляется до y_position после таблицы
y_bottom = y_position_after_table
# Добавляем логику размещения изображений
y_position = self.add_photos_to_pdf(
c, photo_data, project, right_column_width, project_title_y, y_bottom,
left_margin + left_column_width + column_gap
)
y_position = y_position - 20 # Отступ перед следующим проектом
return y_position
def start_new_page_if_needed(self, c, y_position, height):
if y_position < 100:
c.showPage()
y_position = height - 50
return y_position
def get_details(self, equipment_type, project_data):
details = []
def add_detail(label, column_name):
if column_name in project_data.columns:
value_series = project_data[column_name].dropna()
if not value_series.empty:
value = value_series.iloc[0]
else:
value = '-'
details.append(f"{label}: {value}")
else:
details.append(f"{label}: -")
if equipment_type in ["Магнит-хит", "Оборудование сети"]:
add_detail("Название оборудования", "Наименование оборудования")
add_detail("Полный период размещения", "Полный период размещения")
add_detail("Ссылка на планограмму", "Ссылка на ПГ")
add_detail("Комментарий", "Комментарий")
elif equipment_type in ["Навесное оборудование", "Оборудование КА"]:
add_detail("Зона размещения", "Зона размещения")
add_detail("Название оборудования", "Наименование оборудования")
add_detail("Полный период размещения", "Полный период размещения")
add_detail("Действия ТТ по окончанию размещения", "Действия ТТ по окончанию размещения")
add_detail("Комментарий", "Комментарий")
elif equipment_type == "Желтый акционный стеллаж":
add_detail("Зона размещения", "Зона размещения")
add_detail("Ссылка на планограмму", "Ссылка на ПГ")
add_detail("Комментарий", "Комментарий")
return details
def get_table_columns(self, equipment_type, project_data):
base_columns = []
if equipment_type in ["Магнит-хит", "Оборудование сети", "Желтый акционный стеллаж"]:
base_columns = ["Код позиции", "Наименование позиции", "Загрузка в шт", "Место выкладки товара (адрес)"]
elif equipment_type in ["Навесное оборудование", "Оборудование КА"]:
base_columns = ["Код позиции", "Наименование позиции", "Загрузка в шт"]
# Проверяем наличие столбцов в данных проекта
table_columns = [col for col in base_columns if col in project_data.columns]
missing_columns = [col for col in base_columns if col not in project_data.columns]
if missing_columns:
self.log_signal.emit(f"Предупреждение: В проекте отсутствуют столбцы {missing_columns}.")
return table_columns
def draw_table(self, c, table_data, table_columns, max_table_width, y_position, x_start):
if table_data.empty:
self.log_signal.emit(f"Табличные данные отсутствуют.")
return y_position
self.draw_text(c, "Товарные позиции:", x_start + 10, y_position, "DejaVuSans", 8)
y_position -= 20
font_size = 8 # Размер шрифта 8
col_widths = [self.calculate_column_width(table_data[col], col, font_size=font_size) for col in table_columns]
total_width = sum(col_widths)
scaling_factor = 1.0
if total_width > max_table_width - 20:
scaling_factor = (max_table_width - 20) / total_width
col_widths = [w * scaling_factor for w in col_widths]
current_x = x_start + 10
c.setFont("DejaVuSans-Bold", font_size)
row_height = font_size + 6 # Высота строки таблицы
c.setLineWidth(0.5)
height = c._pagesize[1]
# Заголовок таблицы
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], row_height)
current_x += col_widths[i]
y_position -= row_height
c.setFont("DejaVuSans", font_size)
for index, row in table_data.iterrows():
if y_position < 100:
c.showPage()
y_position = height - 50
# Рисуем заголовок таблицы на новой странице
current_x = x_start + 10
c.setFont("DejaVuSans-Bold", font_size)
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], row_height)
current_x += col_widths[i]
y_position -= row_height
c.setFont("DejaVuSans", font_size)
current_x = x_start + 10
for i, col in enumerate(table_columns):
c.drawString(current_x + 2, y_position, str(row[col]))
c.rect(current_x, y_position - 2, col_widths[i], row_height)
current_x += col_widths[i]
y_position -= row_height
return y_position
def add_photos_to_pdf(self, c, photo_data, project, photo_zone_width, y_top, y_bottom, x_start):
if 'Название проекта' in photo_data.columns:
photo_rows = photo_data.loc[photo_data['Название проекта'] == project]
if not photo_rows.empty:
for _, photo_row in photo_rows.iterrows():
if 'Путь' in photo_row:
photo_path = photo_row['Путь']
if os.path.exists(photo_path):
try:
img = ImageReader(photo_path)
img_width, img_height = img.getSize()
# Проверяем доступную высоту
available_height = y_top - y_bottom - 10 # Вычитаем немного для отступа
available_width = photo_zone_width - 20 # Учитываем отступ для ширины изображения
# Проверяем, что доступная высота положительная
if available_height <= 0 or available_width <= 0:
self.log_signal.emit(
f"Предупреждение: недостаточно места для размещения изображения для проекта '{project}'."
)
continue
# Масштабируем изображение пропорционально
scale_factor = min(available_width / img_width, available_height / img_height)
display_width = img_width * scale_factor
display_height = img_height * scale_factor
# Позиция изображения по вертикали
y_position = y_top - display_height # вернемся на высоту изображения от верхнего предела
# Проверяем, помещается ли изображение
if (y_position - display_height) < y_bottom:
# Если изображение не влезает, уменьшим его так, чтобы верхний предел совпал с заголовком проекта
display_height = available_height
display_width = img_width * (display_height / img_height)
y_position = project_title_y # Вернем верхний предел на уровень заголовка
# Добавляем изображение на PDF
c.drawImage(
img,
x_start + 10,
y_position,
width=display_width,
height=display_height,
preserveAspectRatio=True,
mask='auto'
)
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"Предупреждение: В данных фотографий отсутствует столбец 'Путь'.")
else:
self.log_signal.emit(f"Фотографии для проекта '{project}' не найдены.")
else:
self.log_signal.emit("Предупреждение: В данных фотографий отсутствует столбец 'Название проекта'.")
def calculate_column_width(self, series, col_name, font_name='DejaVuSans', font_size=8):
max_width_data = series.astype(str).apply(lambda x: self.string_width(x, font_name, font_size)).max()
col_header_width = self.string_width(col_name, "DejaVuSans-Bold", font_size)
return max(max_width_data, col_header_width) + 10
def string_width(self, 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)
# Новый метод для рисования текста с переносом строк
def draw_wrapped_text(self, canvas_obj, text, x, y, font_name, font_size, max_width):
canvas_obj.setFont(font_name, font_size)
words = text.split(' ')
lines = []
current_line = ''
for word in words:
test_line = current_line + ' ' + word if current_line else word
if self.string_width(test_line, font_name, font_size) <= max_width:
current_line = test_line
else:
lines.append(current_line)
current_line = word
if current_line:
lines.append(current_line)
for line in lines:
canvas_obj.drawString(x, y, line)
y -= font_size + 4 # Задаем интервал между строками
return y
class PDFGeneratorApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Конвертер Excel в PDF")
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)
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()
```
### Объяснение изменений:
1. **Проверка высоты изображения**:
- Изменена логика размещения изображений так, чтобы при добавлении изображения проверялось, помещается ли оно нормально (верхний предел совпадает с заголовком проекта). Если нет — изображение будет масштабироваться так, чтобы его верхний край совпадал с уровнем заголовка, сохраняя при этом пропорции.
2. **Доступное пространство**:
- Пересчитывается доступная высота для изображения, учитывая пространство до нижней границы таблицы.
### Попробуйте запустить этот код и проверьте, корректно ли размещаются фотографии на странице. Если возникнут дополнительные проблемы, пожалуйста, дайте знать!