#!/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 = """ Завершен независимый аудит прозрачности и соблюдения требований законодательства в гостиничной индустрии Орловской области.

Исследование охватило действующие средства размещения, зарегистрированные в государственном реестре. Проверка осуществлялась по 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""" По данным государственного реестра средств размещения:
• Всего действующих объектов размещения: {registry_stats['total_in_registry']}
• Отелей с указанными сайтами в реестре: {registry_stats['with_websites']}
• Доступных сайтов на момент проверки: {registry_stats['accessible_websites']}

В рамках аудита проведена комплексная проверка:
• Обработано действующих отелей: {total_hotels}
• Обладают рабочими сайтами: {total_with_website} ({total_with_website/total_hotels*100:.1f}%)
• Средний балл соответствия требованиям: {avg_score:.1f}%
• Отелей с оценкой ≥50%: {total_compliant} ({total_compliant/total_hotels*100:.1f}%) """ 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}: {percentage:.0f}% ({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}: {percentage:.0f}% ({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""" Аудит показывает, что гостиничный сектор Орловской области активно внедряет практики прозрачности и соблюдения потребительских прав. Средний балл соответствия {avg_score:.1f}% указывает на необходимость дальнейшей работы по совершенствованию доступности информации для гостей и улучшению качества предоставляемых услуг.

Выявлены области, требующие особого внимания: улучшение политики конфиденциальности (152-ФЗ), усиление информации о правилах оказания услуг и механизмах решения споров, расширение контента о доступности для маломобильных групп населения.

Положительным моментом является высокий процент отелей с рабочими сайтами ({total_with_website/total_hotels*100:.1f}%), что демонстрирует готовность отрасли к цифровизации и открытости перед потребителем.

Полученные данные будут направлены владельцам объектов размещения для информирования о выявленных областях улучшения и передовых практиках. """ story.append(Paragraph(conclusion_text, normal_style)) story.append(Spacer(1, 0.3*inch)) story.append(Paragraph( "Дополнительная информация по результатам аудита доступна в приложенном файле Excel.", 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()