Files
hotels/create_pdf_report.py
Фёдор 54e2206234 Добавлен генератор PDF отчетов с графиками и поддержкой кириллицы
- Создан create_pdf_report.py для генерации PDF отчётов
- Поддержка кириллицы через DejaVu Sans шрифты
- 3 типа графиков: распределение по баллам, топ-10 критериев, общая статистика
- Отчёт для Орловской области: 259KB, 5 страниц
- Обновлен create_horizontal_report.py
2025-10-28 00:33:33 +03:00

430 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()