✨ Добавлен генератор PDF отчетов с графиками и поддержкой кириллицы
- Создан create_pdf_report.py для генерации PDF отчётов - Поддержка кириллицы через DejaVu Sans шрифты - 3 типа графиков: распределение по баллам, топ-10 критериев, общая статистика - Отчёт для Орловской области: 259KB, 5 страниц - Обновлен create_horizontal_report.py
This commit is contained in:
441
audit_report_orel_20251028_003022.pdf
Normal file
441
audit_report_orel_20251028_003022.pdf
Normal file
File diff suppressed because one or more lines are too long
@@ -294,55 +294,61 @@ def create_dashboard_sheet(workbook, audit_data, criteria_stats):
|
||||
ws.cell(row=current_row, column=col).font = normal_font
|
||||
ws.cell(row=current_row, column=col).alignment = Alignment(horizontal='center')
|
||||
|
||||
# Графики
|
||||
# Круговой график статуса сайтов
|
||||
# Графики с динамическими ссылками
|
||||
# 1. Круговой график - распределение отелей по статусу
|
||||
pie_chart = PieChart()
|
||||
pie_chart.title = "Статус сайтов отелей"
|
||||
|
||||
# Данные для пирога: Сайты доступны (4), Сайты недоступны (0), Без сайтов (8), В реестре РКН (10)
|
||||
pie_data = Reference(ws, min_col=2, min_row=14, max_row=17, max_col=2)
|
||||
pie_labels = Reference(ws, min_col=1, min_row=14, max_row=17, max_col=1)
|
||||
pie_chart.add_data(pie_data, titles_from_data=False)
|
||||
pie_chart.set_categories(pie_labels)
|
||||
pie_chart.title = "Распределение по баллам соответствия"
|
||||
pie_chart.height = 10
|
||||
pie_chart.width = 15
|
||||
|
||||
# Добавляем подписи данных
|
||||
# Данные для пирога из таблицы распределения по баллам
|
||||
pie_data = Reference(ws, min_col=2, min_row=score_header_row+1, max_row=score_data_start+len(score_ranges)-1, max_col=2)
|
||||
pie_labels = Reference(ws, min_col=1, min_row=score_header_row+1, max_row=score_data_start+len(score_ranges)-1)
|
||||
pie_chart.add_data(pie_data, titles_from_data=False)
|
||||
pie_chart.set_categories(pie_labels)
|
||||
|
||||
# Подписи данных
|
||||
pie_chart.dataLabels = DataLabelList()
|
||||
pie_chart.dataLabels.showPercent = True
|
||||
pie_chart.dataLabels.showCategoryName = True
|
||||
|
||||
ws.add_chart(pie_chart, "C3")
|
||||
# Добавляем график справа от статистики
|
||||
ws.add_chart(pie_chart, "J3")
|
||||
|
||||
# Столбчатый график по критериям
|
||||
# 2. Столбчатый график - результаты по критериям (топ-10)
|
||||
chart1 = BarChart()
|
||||
chart1.title = "Результаты по критериям"
|
||||
chart1.title = "Топ-10 критериев (найдено / не найдено)"
|
||||
chart1.x_axis.title = "Критерии"
|
||||
chart1.y_axis.title = "Количество отелей"
|
||||
|
||||
data = Reference(ws, min_col=2, min_row=20, max_row=20+len(criteria_stats), max_col=3)
|
||||
cats = Reference(ws, min_col=1, min_row=21, max_row=20+len(criteria_stats))
|
||||
chart1.add_data(data, titles_from_data=False)
|
||||
chart1.set_categories(cats)
|
||||
chart1.height = 10
|
||||
chart1.height = 12
|
||||
chart1.width = 20
|
||||
|
||||
ws.add_chart(chart1, "C20")
|
||||
# Берем топ-10 критериев
|
||||
top_n = min(10, len(criteria_stats))
|
||||
|
||||
# График распределения по баллам
|
||||
data = Reference(ws, min_col=2, min_row=data_start_row, max_row=data_start_row+top_n-1, max_col=3)
|
||||
cats = Reference(ws, min_col=1, min_row=data_start_row, max_row=data_start_row+top_n-1)
|
||||
chart1.add_data(data, titles_from_data=False)
|
||||
chart1.set_categories(cats)
|
||||
|
||||
# Добавляем график справа от таблицы критериев
|
||||
ws.add_chart(chart1, "J20")
|
||||
|
||||
# 3. Столбчатый график распределения по баллам
|
||||
chart2 = BarChart()
|
||||
chart2.title = "Распределение по баллам"
|
||||
chart2.title = "Распределение отелей по баллам"
|
||||
chart2.x_axis.title = "Диапазон баллов"
|
||||
chart2.y_axis.title = "Количество отелей"
|
||||
|
||||
data2 = Reference(ws, min_col=2, min_row=41, max_row=41+len(score_ranges), max_col=2)
|
||||
cats2 = Reference(ws, min_col=1, min_row=42, max_row=41+len(score_ranges))
|
||||
chart2.add_data(data2, titles_from_data=False)
|
||||
chart2.set_categories(cats2)
|
||||
chart2.height = 8
|
||||
chart2.width = 12
|
||||
|
||||
ws.add_chart(chart2, "C40")
|
||||
data2 = Reference(ws, min_col=2, min_row=score_header_row+1, max_row=score_data_start+len(score_ranges)-1, max_col=2)
|
||||
cats2 = Reference(ws, min_col=1, min_row=score_header_row+1, max_row=score_data_start+len(score_ranges)-1)
|
||||
chart2.add_data(data2, titles_from_data=False)
|
||||
chart2.set_categories(cats2)
|
||||
|
||||
# Добавляем график рядом с таблицей распределения
|
||||
ws.add_chart(chart2, "J40")
|
||||
|
||||
# Настройка ширины колонок
|
||||
column_widths = [30, 10, 10]
|
||||
|
||||
429
create_pdf_report.py
Normal file
429
create_pdf_report.py
Normal file
@@ -0,0 +1,429 @@
|
||||
#!/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()
|
||||
|
||||
Reference in New Issue
Block a user