430 lines
18 KiB
Python
430 lines
18 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
📄 Генератор PDF отчета с графиками и заключением
|
|||
|
|
Создает красивый PDF документ с результатами аудита отелей Орловской области
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import psycopg2
|
|||
|
|
from psycopg2.extras import RealDictCursor
|
|||
|
|
from urllib.parse import unquote
|
|||
|
|
import matplotlib.pyplot as plt
|
|||
|
|
import matplotlib
|
|||
|
|
matplotlib.use('Agg') # Для работы без GUI
|
|||
|
|
from reportlab.lib.pagesizes import A4
|
|||
|
|
from reportlab.lib.units import inch
|
|||
|
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Image
|
|||
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|||
|
|
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
|
|||
|
|
from reportlab.lib.colors import HexColor
|
|||
|
|
from reportlab.pdfgen import canvas
|
|||
|
|
from reportlab.pdfbase import pdfmetrics
|
|||
|
|
from reportlab.pdfbase.ttfonts import TTFont
|
|||
|
|
from datetime import datetime
|
|||
|
|
import os
|
|||
|
|
|
|||
|
|
# Регистрируем шрифты с поддержкой кириллицы
|
|||
|
|
pdfmetrics.registerFont(TTFont('DejaVuSans', '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'))
|
|||
|
|
pdfmetrics.registerFont(TTFont('DejaVuSans-Bold', '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'))
|
|||
|
|
pdfmetrics.registerFont(TTFont('DejaVuSans-Oblique', '/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf'))
|
|||
|
|
pdfmetrics.registerFont(TTFont('DejaVuSerif', '/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf'))
|
|||
|
|
pdfmetrics.registerFont(TTFont('DejaVuSerif-Bold', '/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf'))
|
|||
|
|
|
|||
|
|
# Абсолютный путь для временных файлов
|
|||
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||
|
|
|
|||
|
|
# Настройка БД
|
|||
|
|
DB_CONFIG = {
|
|||
|
|
'host': '147.45.189.234',
|
|||
|
|
'port': 5432,
|
|||
|
|
'database': 'default_db',
|
|||
|
|
'user': 'gen_user',
|
|||
|
|
'password': unquote('2~~9_%5EkVsU%3F2%5CS')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
REGION = 'Орловская область'
|
|||
|
|
AUDIT_VERSION = 'v1.0_with_rkn'
|
|||
|
|
|
|||
|
|
# Стили для PDF
|
|||
|
|
styles = getSampleStyleSheet()
|
|||
|
|
|
|||
|
|
# Создаем кастомные стили
|
|||
|
|
title_style = ParagraphStyle(
|
|||
|
|
'CustomTitle',
|
|||
|
|
parent=styles['Heading1'],
|
|||
|
|
fontSize=20,
|
|||
|
|
textColor=HexColor('#1f4788'),
|
|||
|
|
spaceAfter=30,
|
|||
|
|
alignment=TA_CENTER,
|
|||
|
|
fontName='DejaVuSans-Bold'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
heading_style = ParagraphStyle(
|
|||
|
|
'CustomHeading',
|
|||
|
|
parent=styles['Heading2'],
|
|||
|
|
fontSize=14,
|
|||
|
|
textColor=HexColor('#2c5aa0'),
|
|||
|
|
spaceAfter=15,
|
|||
|
|
spaceBefore=20,
|
|||
|
|
fontName='DejaVuSans-Bold'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
subheading_style = ParagraphStyle(
|
|||
|
|
'CustomSubheading',
|
|||
|
|
parent=styles['Heading3'],
|
|||
|
|
fontSize=12,
|
|||
|
|
textColor=HexColor('#4a90e2'),
|
|||
|
|
spaceAfter=10,
|
|||
|
|
spaceBefore=15,
|
|||
|
|
fontName='DejaVuSans-Bold'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
normal_style = ParagraphStyle(
|
|||
|
|
'CustomNormal',
|
|||
|
|
parent=styles['Normal'],
|
|||
|
|
fontSize=11,
|
|||
|
|
textColor=HexColor('#333333'),
|
|||
|
|
alignment=TA_JUSTIFY,
|
|||
|
|
spaceAfter=10,
|
|||
|
|
leading=14,
|
|||
|
|
fontName='DejaVuSans'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
highlight_style = ParagraphStyle(
|
|||
|
|
'CustomHighlight',
|
|||
|
|
parent=normal_style,
|
|||
|
|
textColor=HexColor('#1f4788'),
|
|||
|
|
fontSize=12,
|
|||
|
|
fontName='DejaVuSans-Bold'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def get_database_stats():
|
|||
|
|
"""Получить статистику из БД"""
|
|||
|
|
conn = psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
|
|||
|
|
cur = conn.cursor()
|
|||
|
|
|
|||
|
|
# Статистика из реестра
|
|||
|
|
cur.execute("""
|
|||
|
|
SELECT
|
|||
|
|
COUNT(*) as total_in_registry,
|
|||
|
|
COUNT(CASE WHEN website_address IS NOT NULL AND website_address != '' THEN 1 END) as with_websites,
|
|||
|
|
COUNT(CASE WHEN website_status = 'accessible' THEN 1 END) as accessible_websites
|
|||
|
|
FROM hotel_main
|
|||
|
|
WHERE region_name = %s AND status_name = 'Действует'
|
|||
|
|
""", (REGION,))
|
|||
|
|
registry_stats = cur.fetchone()
|
|||
|
|
|
|||
|
|
# Данные аудита
|
|||
|
|
cur.execute("""
|
|||
|
|
SELECT
|
|||
|
|
har.score_percentage,
|
|||
|
|
har.has_website,
|
|||
|
|
har.criteria_results
|
|||
|
|
FROM hotel_audit_results har
|
|||
|
|
LEFT JOIN hotel_main hm ON hm.id = har.hotel_id
|
|||
|
|
WHERE har.region_name = %s AND har.audit_version = %s AND hm.status_name = 'Действует'
|
|||
|
|
""", (REGION, AUDIT_VERSION))
|
|||
|
|
audit_data = cur.fetchall()
|
|||
|
|
|
|||
|
|
# Анализ критериев
|
|||
|
|
criteria_stats = {}
|
|||
|
|
for hotel in audit_data:
|
|||
|
|
if hotel['criteria_results']:
|
|||
|
|
for criterion in hotel['criteria_results']:
|
|||
|
|
name = criterion.get('criterion_name', 'Неизвестно')
|
|||
|
|
found = criterion.get('found', False)
|
|||
|
|
|
|||
|
|
if name not in criteria_stats:
|
|||
|
|
criteria_stats[name] = {'total': 0, 'found': 0}
|
|||
|
|
|
|||
|
|
criteria_stats[name]['total'] += 1
|
|||
|
|
if found:
|
|||
|
|
criteria_stats[name]['found'] += 1
|
|||
|
|
|
|||
|
|
cur.close()
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
return registry_stats, audit_data, criteria_stats
|
|||
|
|
|
|||
|
|
def create_score_distribution_chart(audit_data, filename):
|
|||
|
|
"""Создать график распределения по баллам"""
|
|||
|
|
scores = [h['score_percentage'] for h in audit_data]
|
|||
|
|
|
|||
|
|
# Категории
|
|||
|
|
ranges = ['0-25%', '26-50%', '51-75%', '76-100%']
|
|||
|
|
counts = [
|
|||
|
|
sum(1 for s in scores if 0 <= s < 26),
|
|||
|
|
sum(1 for s in scores if 26 <= s < 51),
|
|||
|
|
sum(1 for s in scores if 51 <= s < 76),
|
|||
|
|
sum(1 for s in scores if 76 <= s <= 100)
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# Создаем pie chart
|
|||
|
|
fig, ax = plt.subplots(figsize=(10, 8))
|
|||
|
|
colors = ['#ff6b6b', '#ffa726', '#66bb6a', '#42a5f5']
|
|||
|
|
wedges, texts, autotexts = ax.pie(
|
|||
|
|
counts,
|
|||
|
|
labels=ranges,
|
|||
|
|
autopct='%1.1f%%',
|
|||
|
|
colors=colors,
|
|||
|
|
startangle=90,
|
|||
|
|
textprops={'fontsize': 12, 'fontweight': 'bold'}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
ax.set_title('Распределение отелей по баллам соответствия',
|
|||
|
|
fontsize=14, fontweight='bold', pad=20)
|
|||
|
|
|
|||
|
|
plt.tight_layout()
|
|||
|
|
full_path = os.path.join(SCRIPT_DIR, filename)
|
|||
|
|
plt.savefig(full_path, dpi=150, bbox_inches='tight')
|
|||
|
|
plt.close()
|
|||
|
|
print(f"✅ График 1 сохранен: {full_path}")
|
|||
|
|
|
|||
|
|
def create_criteria_chart(criteria_stats, filename):
|
|||
|
|
"""Создать график по критериям (топ-10)"""
|
|||
|
|
sorted_criteria = sorted(criteria_stats.items(),
|
|||
|
|
key=lambda x: x[1]['found']/x[1]['total'] if x[1]['total'] > 0 else 0,
|
|||
|
|
reverse=True)
|
|||
|
|
|
|||
|
|
top_criteria = sorted_criteria[:10]
|
|||
|
|
|
|||
|
|
criteria_names = [name[:40] + '...' if len(name) > 40 else name
|
|||
|
|
for name, _ in top_criteria]
|
|||
|
|
found_counts = [stats['found'] for _, stats in top_criteria]
|
|||
|
|
not_found_counts = [stats['total'] - stats['found'] for _, stats in top_criteria]
|
|||
|
|
|
|||
|
|
fig, ax = plt.subplots(figsize=(14, 8))
|
|||
|
|
|
|||
|
|
x = range(len(criteria_names))
|
|||
|
|
width = 0.6
|
|||
|
|
|
|||
|
|
bars1 = ax.barh(x, found_counts, width, label='Найдено', color='#66bb6a')
|
|||
|
|
bars2 = ax.barh(x, not_found_counts, width, left=found_counts,
|
|||
|
|
label='Не найдено', color='#ff6b6b')
|
|||
|
|
|
|||
|
|
ax.set_yticks(x)
|
|||
|
|
ax.set_yticklabels(criteria_names, fontsize=9)
|
|||
|
|
ax.set_xlabel('Количество отелей', fontweight='bold', fontsize=11)
|
|||
|
|
ax.set_title('Топ-10 критериев: выполнение/невыполнение',
|
|||
|
|
fontsize=14, fontweight='bold', pad=20)
|
|||
|
|
ax.legend(loc='lower right', fontsize=10)
|
|||
|
|
ax.grid(axis='x', alpha=0.3)
|
|||
|
|
|
|||
|
|
plt.tight_layout()
|
|||
|
|
full_path = os.path.join(SCRIPT_DIR, filename)
|
|||
|
|
plt.savefig(full_path, dpi=150, bbox_inches='tight')
|
|||
|
|
plt.close()
|
|||
|
|
print(f"✅ График 2 сохранен: {full_path}")
|
|||
|
|
|
|||
|
|
def create_summary_chart(registry_stats, audit_data, filename):
|
|||
|
|
"""Создать сводный график по статистике"""
|
|||
|
|
# Подготовка данных
|
|||
|
|
total_hotels = len(audit_data)
|
|||
|
|
avg_score = sum(h['score_percentage'] for h in audit_data) / total_hotels if total_hotels > 0 else 0
|
|||
|
|
|
|||
|
|
categories = ['Всего в реестре', 'С сайтами', 'Проведено аудитов', 'Соответствие\n(≥50%)']
|
|||
|
|
values = [
|
|||
|
|
registry_stats['total_in_registry'],
|
|||
|
|
registry_stats['with_websites'],
|
|||
|
|
total_hotels,
|
|||
|
|
sum(1 for h in audit_data if h['score_percentage'] >= 50)
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
fig, ax = plt.subplots(figsize=(10, 6))
|
|||
|
|
bars = ax.bar(categories, values, color=['#4a90e2', '#66bb6a', '#ffa726', '#42a5f5'])
|
|||
|
|
|
|||
|
|
ax.set_title('Общая статистика аудита', fontsize=14, fontweight='bold', pad=20)
|
|||
|
|
ax.set_ylabel('Количество', fontweight='bold')
|
|||
|
|
|
|||
|
|
# Добавляем подписи значений
|
|||
|
|
for bar, val in zip(bars, values):
|
|||
|
|
height = bar.get_height()
|
|||
|
|
ax.text(bar.get_x() + bar.get_width()/2., height,
|
|||
|
|
f'{val}',
|
|||
|
|
ha='center', va='bottom', fontweight='bold')
|
|||
|
|
|
|||
|
|
plt.tight_layout()
|
|||
|
|
full_path = os.path.join(SCRIPT_DIR, filename)
|
|||
|
|
plt.savefig(full_path, dpi=150, bbox_inches='tight')
|
|||
|
|
plt.close()
|
|||
|
|
print(f"✅ График 3 сохранен: {full_path}")
|
|||
|
|
|
|||
|
|
def build_pdf_content(story, registry_stats, audit_data, criteria_stats):
|
|||
|
|
"""Сформировать содержимое PDF"""
|
|||
|
|
|
|||
|
|
# Заголовок
|
|||
|
|
story.append(Spacer(1, 0.5*inch))
|
|||
|
|
story.append(Paragraph("ИТОГИ АУДИТА ПРОЗРАЧНОСТИ", title_style))
|
|||
|
|
story.append(Paragraph("ГОСТИНИЧНОГО СЕКТОРА", title_style))
|
|||
|
|
story.append(Paragraph("Орловской области", title_style))
|
|||
|
|
story.append(Spacer(1, 0.3*inch))
|
|||
|
|
|
|||
|
|
date_str = datetime.now().strftime("%d.%m.%Y")
|
|||
|
|
story.append(Paragraph(f"Дата формирования отчета: {date_str}",
|
|||
|
|
styles['Normal']))
|
|||
|
|
story.append(PageBreak())
|
|||
|
|
|
|||
|
|
# Введение
|
|||
|
|
story.append(Paragraph("ВВЕДЕНИЕ", heading_style))
|
|||
|
|
intro_text = """
|
|||
|
|
<b>Завершен независимый аудит прозрачности и соблюдения требований законодательства
|
|||
|
|
в гостиничной индустрии Орловской области.</b><br/><br/>
|
|||
|
|
|
|||
|
|
Исследование охватило действующие средства размещения, зарегистрированные
|
|||
|
|
в государственном реестре. Проверка осуществлялась по 18 ключевым критериям прозрачности,
|
|||
|
|
включая юридическую идентификацию, контактную информацию, политику конфиденциальности,
|
|||
|
|
реестр РКН, договор-оферту, механизмы обращения граждан, прайс-листы, способы оплаты,
|
|||
|
|
систему бронирования и другие аспекты информационной открытости.
|
|||
|
|
"""
|
|||
|
|
story.append(Paragraph(intro_text, normal_style))
|
|||
|
|
story.append(Spacer(1, 0.2*inch))
|
|||
|
|
|
|||
|
|
# Основные результаты
|
|||
|
|
total_hotels = len(audit_data)
|
|||
|
|
avg_score = sum(h['score_percentage'] for h in audit_data) / total_hotels if total_hotels > 0 else 0
|
|||
|
|
total_with_website = sum(1 for h in audit_data if h['has_website'])
|
|||
|
|
total_compliant = sum(1 for h in audit_data if h['score_percentage'] >= 50)
|
|||
|
|
|
|||
|
|
story.append(Paragraph("ОСНОВНЫЕ РЕЗУЛЬТАТЫ АУДИТА", heading_style))
|
|||
|
|
|
|||
|
|
results_text = f"""
|
|||
|
|
<b>По данным государственного реестра средств размещения:</b><br/>
|
|||
|
|
• Всего действующих объектов размещения: <b>{registry_stats['total_in_registry']}</b><br/>
|
|||
|
|
• Отелей с указанными сайтами в реестре: <b>{registry_stats['with_websites']}</b><br/>
|
|||
|
|
• Доступных сайтов на момент проверки: <b>{registry_stats['accessible_websites']}</b><br/><br/>
|
|||
|
|
|
|||
|
|
<b>В рамках аудита проведена комплексная проверка:</b><br/>
|
|||
|
|
• Обработано действующих отелей: <b>{total_hotels}</b><br/>
|
|||
|
|
• Обладают рабочими сайтами: <b>{total_with_website} ({total_with_website/total_hotels*100:.1f}%)</b><br/>
|
|||
|
|
• Средний балл соответствия требованиям: <b>{avg_score:.1f}%</b><br/>
|
|||
|
|
• Отелей с оценкой ≥50%: <b>{total_compliant} ({total_compliant/total_hotels*100:.1f}%)</b>
|
|||
|
|
"""
|
|||
|
|
story.append(Paragraph(results_text, normal_style))
|
|||
|
|
story.append(Spacer(1, 0.2*inch))
|
|||
|
|
|
|||
|
|
# График распределения
|
|||
|
|
temp_file1 = 'temp_chart1.png'
|
|||
|
|
create_score_distribution_chart(audit_data, temp_file1)
|
|||
|
|
full_path1 = os.path.join(SCRIPT_DIR, temp_file1)
|
|||
|
|
img1 = Image(full_path1, width=6*inch, height=5*inch)
|
|||
|
|
story.append(img1)
|
|||
|
|
story.append(Spacer(1, 0.2*inch))
|
|||
|
|
|
|||
|
|
# Статистика по критериям
|
|||
|
|
story.append(Paragraph("СТАТИСТИКА ПО КРИТЕРИЯМ", heading_style))
|
|||
|
|
|
|||
|
|
sorted_criteria = sorted(criteria_stats.items(),
|
|||
|
|
key=lambda x: x[1]['found']/x[1]['total'] if x[1]['total'] > 0 else 0,
|
|||
|
|
reverse=True)
|
|||
|
|
|
|||
|
|
top_success = sorted_criteria[:5]
|
|||
|
|
top_problems = sorted_criteria[-5:]
|
|||
|
|
|
|||
|
|
# Топ успехов
|
|||
|
|
story.append(Paragraph("Наиболее активно соблюдаются:", subheading_style))
|
|||
|
|
for idx, (name, stats) in enumerate(top_success, 1):
|
|||
|
|
percentage = stats['found'] / stats['total'] * 100 if stats['total'] > 0 else 0
|
|||
|
|
story.append(Paragraph(
|
|||
|
|
f"{idx}. {name}: <b>{percentage:.0f}%</b> ({stats['found']} из {stats['total']})",
|
|||
|
|
normal_style
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
story.append(Spacer(1, 0.2*inch))
|
|||
|
|
|
|||
|
|
# Топ проблем
|
|||
|
|
story.append(Paragraph("Требуют особого внимания:", subheading_style))
|
|||
|
|
for idx, (name, stats) in enumerate(reversed(top_problems), 1):
|
|||
|
|
percentage = stats['found'] / stats['total'] * 100 if stats['total'] > 0 else 0
|
|||
|
|
story.append(Paragraph(
|
|||
|
|
f"{idx}. {name}: <b>{percentage:.0f}%</b> ({stats['found']} из {stats['total']})",
|
|||
|
|
normal_style
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
story.append(Spacer(1, 0.2*inch))
|
|||
|
|
|
|||
|
|
# График по критериям
|
|||
|
|
temp_file2 = 'temp_chart2.png'
|
|||
|
|
create_criteria_chart(criteria_stats, temp_file2)
|
|||
|
|
full_path2 = os.path.join(SCRIPT_DIR, temp_file2)
|
|||
|
|
img2 = Image(full_path2, width=7*inch, height=4*inch)
|
|||
|
|
story.append(img2)
|
|||
|
|
story.append(Spacer(1, 0.2*inch))
|
|||
|
|
|
|||
|
|
# Сводный график
|
|||
|
|
temp_file3 = 'temp_chart3.png'
|
|||
|
|
create_summary_chart(registry_stats, audit_data, temp_file3)
|
|||
|
|
full_path3 = os.path.join(SCRIPT_DIR, temp_file3)
|
|||
|
|
img3 = Image(full_path3, width=5*inch, height=3*inch)
|
|||
|
|
story.append(img3)
|
|||
|
|
story.append(Spacer(1, 0.2*inch))
|
|||
|
|
|
|||
|
|
# Заключение
|
|||
|
|
story.append(PageBreak())
|
|||
|
|
story.append(Paragraph("ЗАКЛЮЧЕНИЕ", heading_style))
|
|||
|
|
|
|||
|
|
conclusion_text = f"""
|
|||
|
|
Аудит показывает, что гостиничный сектор Орловской области активно внедряет
|
|||
|
|
практики прозрачности и соблюдения потребительских прав. Средний балл соответствия
|
|||
|
|
<b>{avg_score:.1f}%</b> указывает на необходимость дальнейшей работы по совершенствованию
|
|||
|
|
доступности информации для гостей и улучшению качества предоставляемых услуг.<br/><br/>
|
|||
|
|
|
|||
|
|
Выявлены области, требующие особого внимания: улучшение политики конфиденциальности
|
|||
|
|
(152-ФЗ), усиление информации о правилах оказания услуг и механизмах решения споров,
|
|||
|
|
расширение контента о доступности для маломобильных групп населения.<br/><br/>
|
|||
|
|
|
|||
|
|
Положительным моментом является высокий процент отелей с рабочими сайтами
|
|||
|
|
({total_with_website/total_hotels*100:.1f}%), что демонстрирует готовность отрасли
|
|||
|
|
к цифровизации и открытости перед потребителем.<br/><br/>
|
|||
|
|
|
|||
|
|
Полученные данные будут направлены владельцам объектов размещения для информирования
|
|||
|
|
о выявленных областях улучшения и передовых практиках.
|
|||
|
|
"""
|
|||
|
|
story.append(Paragraph(conclusion_text, normal_style))
|
|||
|
|
|
|||
|
|
story.append(Spacer(1, 0.3*inch))
|
|||
|
|
story.append(Paragraph(
|
|||
|
|
"<i>Дополнительная информация по результатам аудита доступна в приложенном файле Excel.</i>",
|
|||
|
|
styles['Italic']
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
# Временные файлы будут удалены после создания PDF
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
print(f"📄 СОЗДАНИЕ PDF ОТЧЕТА ПО {REGION}")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
# Получаем данные
|
|||
|
|
print("📊 Загружаем данные из БД...")
|
|||
|
|
registry_stats, audit_data, criteria_stats = get_database_stats()
|
|||
|
|
|
|||
|
|
print(f"✅ Загружено:")
|
|||
|
|
print(f" 🏨 Отелей: {len(audit_data)}")
|
|||
|
|
print(f" 🎯 Критериев: {len(criteria_stats)}")
|
|||
|
|
|
|||
|
|
# Создаем PDF
|
|||
|
|
print("\n📝 Создаем PDF файл...")
|
|||
|
|
filename = f"audit_report_orel_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
|||
|
|
|
|||
|
|
doc = SimpleDocTemplate(filename, pagesize=A4,
|
|||
|
|
rightMargin=72, leftMargin=72,
|
|||
|
|
topMargin=72, bottomMargin=18)
|
|||
|
|
|
|||
|
|
story = []
|
|||
|
|
build_pdf_content(story, registry_stats, audit_data, criteria_stats)
|
|||
|
|
|
|||
|
|
doc.build(story)
|
|||
|
|
|
|||
|
|
# Удаляем временные файлы
|
|||
|
|
for temp_file in ['temp_chart1.png', 'temp_chart2.png', 'temp_chart3.png']:
|
|||
|
|
full_path = os.path.join(SCRIPT_DIR, temp_file)
|
|||
|
|
if os.path.exists(full_path):
|
|||
|
|
os.remove(full_path)
|
|||
|
|
|
|||
|
|
print(f"\n✅ PDF отчет создан: {filename}")
|
|||
|
|
print(f"📊 Включено:")
|
|||
|
|
print(f" 📈 Графиков: 3")
|
|||
|
|
print(f" 📑 Страниц: ~{len([s for s in story if isinstance(s, PageBreak)]) + 1}")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|
|||
|
|
|