#!/usr/bin/env python3 """ Создание Excel отчета по Орловской области в горизонтальном формате Лист 1: Дашборд с графиками и статистикой Лист 2: Детальная таблица аудита (горизонтальный формат) """ 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 DB_CONFIG = { 'host': '147.45.189.234', 'port': 5432, 'database': 'default_db', 'user': 'gen_user', 'password': unquote('2~~9_%5EkVsU%3F2%5CS') } def get_orel_data(): """Получить данные аудита Орловской области версии v1.0_with_rkn""" conn = psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor) cur = conn.cursor() # Получаем данные аудита Орловской области с информацией об отелях cur.execute(""" 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 FROM hotel_audit_results har LEFT JOIN hotel_main hm ON hm.id = har.hotel_id WHERE har.region_name = 'Орловская область' AND har.audit_version = 'v1.0_with_rkn' ORDER BY har.score_percentage DESC """) 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'] = "🏛️ ДАШБОРД АУДИТА ОТЕЛЕЙ ОРЛОВСКОЙ ОБЛАСТИ" 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'] = "📈 ОБЩАЯ СТАТИСТИКА ПО ОРЛОВСКОЙ ОБЛАСТИ" 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"Всего отелей в Орловской области: {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['website'] or hotel['website_address']) ws.cell(row=i, column=3, value='Да' if hotel['has_website'] else 'Нет') ws.cell(row=i, column=4, value=f"{hotel['total_score']}/{hotel['max_score']}") ws.cell(row=i, column=5, 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=5).fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type='solid') # Данные по критериям col_idx = 6 # Начинаем с 6-й колонки 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=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=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("🏛️ СОЗДАНИЕ ОТЧЕТА ПО ОРЛОВСКОЙ ОБЛАСТИ (ГОРИЗОНТАЛЬНЫЙ ФОРМАТ)") print("=" * 60) # Получаем данные print("📊 Загружаем данные из БД...") audit_data, criteria_stats = get_orel_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"orel_horizontal_report_{timestamp}.xlsx" workbook.save(filename) print(f"\n✅ Отчет сохранен: {filename}") print(f"📊 Листы:") print(f" 📈 Дашборд Орёл - графики и статистика") print(f" 🏨 Аудит отелей - горизонтальный формат (как в CSV)") if __name__ == "__main__": main()