Files
hotels/create_horizontal_report.py
Фёдор 309de51744 Add status filter to report generation
- Added ONLY_ACTIVE parameter to filter by hotel status
- Now supports generating reports for 'Действует' (Active) hotels only
- Default: ONLY_ACTIVE = True (Orel region configured)
- Can be easily toggled for all statuses

Changes:
- create_horizontal_report.py: Added status_name filter in SQL query
- Tested on Orel region: 29 active hotels (out of 30 total)
- Average score: 44.1%, RKN registry: 26 hotels
2025-10-27 23:16:29 +03:00

539 lines
23 KiB
Python
Raw 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
"""
📊 УНИВЕРСАЛЬНЫЙ ГЕНЕРАТОР ГОРИЗОНТАЛЬНЫХ ОТЧЁТОВ
Создание Excel отчета в горизонтальном формате для любого региона
Лист 1: Дашборд с графиками и статистикой
Лист 2: Детальная таблица аудита (горизонтальный формат с 18 критериями)
Использование:
Измените переменные REGION и AUDIT_VERSION под нужный регион
"""
import psycopg2
from psycopg2.extras import RealDictCursor
from urllib.parse import unquote
import pandas as pd
import openpyxl
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side, NamedStyle
from openpyxl.chart import BarChart, PieChart, LineChart, Reference
from openpyxl.chart.label import DataLabelList
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.drawing.image import Image
from datetime import datetime
import json
import re
def clean_text_for_excel(text):
"""Очистить текст от недопустимых символов для Excel"""
if text is None:
return ''
text = str(text)
# Удаляем управляющие символы (кроме переноса строки и табуляции)
text = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]', '', text)
return text
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' # Версия аудита
ONLY_ACTIVE = True # Только действующие отели (status_name = 'Действует')
# =======================================
def get_region_data():
"""Получить данные аудита региона"""
conn = psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
cur = conn.cursor()
# Получаем данные аудита с информацией об отелях
status_filter = ""
if ONLY_ACTIVE:
status_filter = "AND hm.status_name = 'Действует'"
cur.execute(f"""
SELECT
har.hotel_id,
har.hotel_name,
har.region_name,
har.website,
har.has_website,
har.total_score,
har.max_score,
har.score_percentage,
har.audit_date,
har.audit_version,
har.criteria_results,
hm.full_name,
hm.website_address,
hm.owner_inn,
hm.owner_ogrn,
hm.addresses,
hm.phone,
hm.email,
hm.website_status,
hm.rkn_registry_status,
hm.rkn_registry_number,
hm.rkn_registry_date,
hm.rkn_checked_at,
hm.register_record,
hm.register_record_date,
hm.owner_full_name,
hm.owner_ogrn,
hm.owner_inn,
hm.phone,
hm.email,
hm.category_name,
hm.registry_url,
hm.status_name
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
{status_filter}
ORDER BY har.score_percentage DESC
""", (REGION, AUDIT_VERSION))
audit_data = cur.fetchall()
# Статистика по критериям (анализируем criteria_results)
criteria_stats = []
if audit_data:
# Собираем статистику по критериям из всех отелей
criteria_counts = {}
total_hotels = len(audit_data)
for hotel in audit_data:
if hotel['criteria_results']:
criteria = hotel['criteria_results']
for criterion in criteria:
name = criterion.get('criterion_name', 'Неизвестно')
found = criterion.get('found', False)
if name not in criteria_counts:
criteria_counts[name] = {'total': 0, 'found': 0}
criteria_counts[name]['total'] += 1
if found:
criteria_counts[name]['found'] += 1
# Преобразуем в список
for name, counts in criteria_counts.items():
percentage = (counts['found'] / counts['total'] * 100) if counts['total'] > 0 else 0
criteria_stats.append({
'criterion_name': name,
'total_checks': counts['total'],
'found_count': counts['found'],
'percentage': percentage
})
# Сортируем по проценту выполнения
criteria_stats.sort(key=lambda x: x['percentage'], reverse=True)
cur.close()
conn.close()
return audit_data, criteria_stats
def create_dashboard_sheet(workbook, audit_data, criteria_stats):
"""Создать лист дашборда"""
ws = workbook.active
ws.title = "📊 Дашборд СПб"
# Стили
header_font = Font(name='Arial', size=14, bold=True, color='FFFFFF')
header_fill = PatternFill(start_color='366092', end_color='366092', fill_type='solid')
subheader_font = Font(name='Arial', size=12, bold=True)
normal_font = Font(name='Arial', size=10)
# Заголовок
ws['A1'] = f"🏛️ ДАШБОРД АУДИТА ОТЕЛЕЙ {REGION.upper()}"
ws['A1'].font = Font(name='Arial', size=16, bold=True, color='366092')
ws['A1'].alignment = Alignment(horizontal='center')
ws.merge_cells('A1:H1')
# Общая статистика
ws['A3'] = f"📈 ОБЩАЯ СТАТИСТИКА ПО {REGION.upper()}"
ws['A3'].font = subheader_font
ws['A3'].fill = PatternFill(start_color='E7E6E6', end_color='E7E6E6', fill_type='solid')
# Подсчитываем статистику
total_hotels = len(audit_data)
total_with_website = sum(1 for h in audit_data if h['has_website'])
total_without_website = total_hotels - total_with_website
total_in_rkn = sum(1 for h in audit_data if h.get('rkn_registry_number'))
total_compliant = sum(1 for h in audit_data if h['score_percentage'] >= 50)
avg_score = sum(h['score_percentage'] for h in audit_data) / total_hotels if total_hotels > 0 else 0
ws['A4'] = f"Всего отелей в {REGION}: {total_hotels}"
ws['A5'] = f"С сайтами: {total_with_website}"
ws['A6'] = f"Без сайтов: {total_without_website}"
ws['A7'] = f"Сайты доступны для анализа: {total_with_website}"
ws['A8'] = f"Сайты недоступны: 0"
ws['A9'] = f"В реестре РКН: {total_in_rkn}"
ws['A10'] = f"Проведено аудитов: {total_hotels}"
ws['A11'] = f"Средний балл (аудит): {avg_score:.1f}%"
for cell in ['A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10', 'A11']:
ws[cell].font = normal_font
# Категория
ws['A13'] = "Категория"
ws['A13'].font = subheader_font
ws['A13'].fill = PatternFill(start_color='E7E6E6', end_color='E7E6E6', fill_type='solid')
ws['A14'] = f"Сайты доступны: {total_with_website}"
ws['B14'] = total_with_website
ws['A15'] = f"Сайты недоступны: 0"
ws['B15'] = 0
ws['A16'] = f"Без сайтов: {total_without_website}"
ws['B16'] = total_without_website
ws['A17'] = f"В реестре РКН: {total_in_rkn}"
ws['B17'] = total_in_rkn
for cell in ['A14', 'A15', 'A16', 'A17']:
ws[cell].font = normal_font
# Статистика по критериям
ws['A19'] = "🎯 СТАТИСТИКА ПО КРИТЕРИЯМ"
ws['A19'].font = subheader_font
ws['A19'].fill = PatternFill(start_color='E7E6E6', end_color='E7E6E6', fill_type='solid')
# Заголовки таблицы критериев
criteria_headers = ['Критерий', 'Найдено', 'Не найдено']
for i, header in enumerate(criteria_headers, 1):
cell = ws.cell(row=20, column=i, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center')
# Данные по критериям
for i, criterion in enumerate(criteria_stats, 21):
not_found = criterion['total_checks'] - criterion['found_count']
ws.cell(row=i, column=1, value=criterion['criterion_name'])
ws.cell(row=i, column=2, value=criterion['found_count'])
ws.cell(row=i, column=3, value=not_found)
# Форматирование
for col in range(1, 4):
ws.cell(row=i, column=col).font = normal_font
ws.cell(row=i, column=col).alignment = Alignment(horizontal='center')
# Распределение по баллам
ws['A40'] = "📊 РАСПРЕДЕЛЕНИЕ ПО БАЛЛАМ"
ws['A40'].font = subheader_font
ws['A40'].fill = PatternFill(start_color='E7E6E6', end_color='E7E6E6', fill_type='solid')
# Заголовки
score_headers = ['Диапазон', 'Количество']
for i, header in enumerate(score_headers, 1):
cell = ws.cell(row=41, column=i, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center')
# Данные по баллам
score_ranges = [
('0-25%', sum(1 for h in audit_data if h['score_percentage'] < 26)),
('26-50%', sum(1 for h in audit_data if 26 <= h['score_percentage'] < 51)),
('51-75%', sum(1 for h in audit_data if 51 <= h['score_percentage'] < 76)),
('76-100%', sum(1 for h in audit_data if h['score_percentage'] >= 76))
]
for i, (range_name, count) in enumerate(score_ranges, 42):
ws.cell(row=i, column=1, value=range_name)
ws.cell(row=i, column=2, value=count)
# Форматирование
for col in range(1, 3):
ws.cell(row=i, column=col).font = normal_font
ws.cell(row=i, column=col).alignment = Alignment(horizontal='center')
# Графики
# Круговой график статуса сайтов
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.height = 10
pie_chart.width = 15
# Добавляем подписи данных
pie_chart.dataLabels = DataLabelList()
pie_chart.dataLabels.showPercent = True
pie_chart.dataLabels.showCategoryName = True
ws.add_chart(pie_chart, "C3")
# Столбчатый график по критериям
chart1 = BarChart()
chart1.title = "Результаты по критериям"
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.width = 20
ws.add_chart(chart1, "C20")
# График распределения по баллам
chart2 = BarChart()
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")
# Настройка ширины колонок
column_widths = [30, 10, 10]
for i, width in enumerate(column_widths, 1):
ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = width
def create_audit_sheet(workbook, audit_data):
"""Создать лист детального аудита в горизонтальном формате"""
ws = workbook.create_sheet("🏨 Аудит отелей")
# Стили
header_font = Font(name='Arial', size=10, bold=True, color='FFFFFF')
header_fill = PatternFill(start_color='366092', end_color='366092', fill_type='solid')
normal_font = Font(name='Arial', size=8)
# Базовые заголовки
base_headers = [
'Отель',
'Дата включения в реестр',
'Владелец',
'ОГРН',
'ИНН',
'Электронная почта владельца',
'Телефон владельца',
'Электронная почта средства размещения',
'Телефон средства размещения',
'Категория объекта',
'Ссылка на запись в реестре',
'Сайт',
'Есть сайт',
'Балл',
'Процент'
]
# Заголовки критериев (по 3 колонки на каждый)
criteria_headers = []
criteria_names = [
"1. Юридическая идентификация и верификация",
"2. Адрес",
"3. Контакты",
"4. Режим работы",
"5. Политика ПДн (152-ФЗ)",
"6. РКН Реестр",
"7. Договор-оферта / Правила оказания услуг",
"8. Рекламации и споры",
"9. Цены/прайс",
"10. Способы оплаты",
"11. Онлайн-оплата",
"12. Онлайн-бронирование",
"13. FAQ",
"14. Доступность для ЛОВЗ",
"15. Партнеры/бренды",
"16. Команда/сотрудники",
"17. Уголок потребителя",
"18. Актуальность документов"
]
for criterion in criteria_names:
criteria_headers.extend([criterion, f"{criterion} URL", f"{criterion} Комментарий"])
# Все заголовки
all_headers = base_headers + criteria_headers
# Записываем заголовки
for i, header in enumerate(all_headers, 1):
cell = ws.cell(row=1, column=i, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
# Данные по отелям
for i, hotel in enumerate(audit_data, 2):
# Базовые данные
ws.cell(row=i, column=1, value=hotel['hotel_name'] or hotel['full_name'])
# Данные реестра
ws.cell(row=i, column=2, value=hotel.get('register_record_date', '')) # Дата включения в реестр
ws.cell(row=i, column=3, value=hotel.get('owner_full_name', '')) # Владелец
ws.cell(row=i, column=4, value=hotel.get('owner_ogrn', '')) # ОГРН
ws.cell(row=i, column=5, value=hotel.get('owner_inn', '')) # ИНН
ws.cell(row=i, column=6, value=hotel.get('email', '')) # Email владельца
ws.cell(row=i, column=7, value=hotel.get('phone', '')) # Телефон владельца
ws.cell(row=i, column=8, value=hotel.get('email', '')) # Email средства размещения (тот же)
ws.cell(row=i, column=9, value=hotel.get('phone', '')) # Телефон средства размещения (тот же)
ws.cell(row=i, column=10, value=hotel.get('category_name', '')) # Категория объекта
# Ссылка на запись в реестре (используем готовую ссылку из БД)
registry_link = hotel.get('registry_url', '')
ws.cell(row=i, column=11, value=registry_link)
# Основные данные
ws.cell(row=i, column=12, value=hotel['website'] or hotel['website_address']) # Сайт
ws.cell(row=i, column=13, value='Да' if hotel['has_website'] else 'Нет') # Есть сайт
ws.cell(row=i, column=14, value=f"{hotel['total_score']}/{hotel['max_score']}") # Балл
ws.cell(row=i, column=15, value=f"{hotel['score_percentage']:.1f}%") # Процент
# Цветовое кодирование процента
percentage = hotel['score_percentage'] or 0
if percentage >= 50:
fill_color = 'C6EFCE' # Зеленый
elif percentage >= 30:
fill_color = 'FFEB9C' # Желтый
else:
fill_color = 'FFC7CE' # Красный
ws.cell(row=i, column=15).fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type='solid')
# Данные по критериям
col_idx = 16 # Начинаем с 16-й колонки (сдвинули на 10 вправо)
if hotel['criteria_results']:
criteria = hotel['criteria_results']
for criterion in criteria:
criterion_id = criterion.get('criterion_id')
# Статус
status = 'Да' if criterion.get('found', False) else 'Нет'
ws.cell(row=i, column=col_idx, value=status)
# URL и Комментарий - специальная обработка для критерия 6 (РКН)
if criterion_id == 6: # РКН Реестр
# URL на реестр РКН
rkn_number = hotel.get('rkn_registry_number', '')
if rkn_number:
url = f"https://rkn.gov.ru/mass-communications/reestr/search/?q={rkn_number}"
else:
url = ""
ws.cell(row=i, column=col_idx + 1, value=url)
# Комментарий: номер РКН + дата
rkn_date = hotel.get('rkn_registry_date', '')
if rkn_number and rkn_date:
comment = f"{rkn_number}\n{rkn_date}"
elif rkn_number:
comment = rkn_number
else:
comment = ""
ws.cell(row=i, column=col_idx + 2, value=comment)
else:
# Обычная обработка для остальных критериев
url = criterion.get('ai_agent', {}).get('url', '')
ws.cell(row=i, column=col_idx + 1, value=clean_text_for_excel(url))
# Используем details вместо quote для коротких комментариев
comment = criterion.get('ai_agent', {}).get('details', '')
if not comment:
# Если details нет, используем quote но обрезаем
comment = criterion.get('ai_agent', {}).get('quote', '')
if len(comment) > 100:
comment = comment[:100] + '...'
ws.cell(row=i, column=col_idx + 2, value=clean_text_for_excel(comment))
# Цветовое кодирование статуса
if criterion.get('found', False):
fill_color = 'C6EFCE' # Зеленый
else:
fill_color = 'FFC7CE' # Красный
ws.cell(row=i, column=col_idx).fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type='solid')
col_idx += 3
# Форматирование всех ячеек строки
for col in range(1, len(all_headers) + 1):
cell = ws.cell(row=i, column=col)
cell.font = normal_font
cell.alignment = Alignment(horizontal='center', vertical='top', wrap_text=True)
# Автоподбор ширины колонок
for column in ws.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if cell.value:
# Учитываем переносы строк
lines = str(cell.value).split('\n')
max_line_length = max(len(line) for line in lines) if lines else 0
max_length = max(max_length, max_line_length)
except:
pass
# Ограничиваем ширину
adjusted_width = min(max_length + 2, 50) # Максимум 50
adjusted_width = max(adjusted_width, 10) # Минимум 10
ws.column_dimensions[column_letter].width = adjusted_width
# Фильтры
ws.auto_filter.ref = f"A1:{openpyxl.utils.get_column_letter(len(all_headers))}{len(audit_data)+1}"
# Заморозка заголовков
ws.freeze_panes = "A2"
def main():
"""Основная функция"""
print(f"🏛️ СОЗДАНИЕ ОТЧЕТА ПО {REGION.upper()} (ГОРИЗОНТАЛЬНЫЙ ФОРМАТ)")
print("=" * 60)
# Получаем данные
print("📊 Загружаем данные из БД...")
audit_data, criteria_stats = get_region_data()
print(f"✅ Загружено:")
print(f" 🏨 Отелей: {len(audit_data)}")
print(f" 🎯 Критериев: {len(criteria_stats)}")
# Создаем Excel файл
print("\n📝 Создаем Excel файл...")
workbook = Workbook()
# Лист дашборда
print("📊 Создаем дашборд...")
create_dashboard_sheet(workbook, audit_data, criteria_stats)
# Лист аудита
print("🏨 Создаем таблицу аудита (горизонтальный формат)...")
create_audit_sheet(workbook, audit_data)
# Сохраняем файл
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"experimental_report_{timestamp}.xlsx"
workbook.save(filename)
print(f"\n✅ Отчет сохранен: {filename}")
print(f"📊 Листы:")
print(f" 📈 Дашборд СПб - графики и статистика")
print(f" 🏨 Аудит отелей - горизонтальный формат (как в CSV)")
if __name__ == "__main__":
main()