631 lines
26 KiB
Python
631 lines
26 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
Аудит отелей Чукотки через n8n webhook + сохранение в Excel
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import psycopg2
|
|||
|
|
from psycopg2.extras import RealDictCursor
|
|||
|
|
from urllib.parse import unquote
|
|||
|
|
import requests
|
|||
|
|
import time
|
|||
|
|
import json
|
|||
|
|
from datetime import datetime
|
|||
|
|
import openpyxl
|
|||
|
|
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
|||
|
|
from openpyxl.utils import get_column_letter
|
|||
|
|
from openpyxl.chart import BarChart, PieChart, Reference
|
|||
|
|
from openpyxl.chart.label import DataLabelList
|
|||
|
|
|
|||
|
|
DB_CONFIG = {
|
|||
|
|
'host': '147.45.189.234',
|
|||
|
|
'port': 5432,
|
|||
|
|
'database': 'default_db',
|
|||
|
|
'user': 'gen_user',
|
|||
|
|
'password': unquote('2~~9_%5EkVsU%3F2%5CS')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
WEBHOOK_URL = "https://n8n.clientright.pro/webhook/6be4a7b9-a016-4252-841f-0ebca367914f"
|
|||
|
|
|
|||
|
|
def get_chukotka_hotels():
|
|||
|
|
"""Получить отели Чукотки с chunks и данными РКН"""
|
|||
|
|
conn = psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)
|
|||
|
|
cur = conn.cursor()
|
|||
|
|
|
|||
|
|
cur.execute("""
|
|||
|
|
SELECT DISTINCT
|
|||
|
|
h.id::text AS hotel_id,
|
|||
|
|
h.full_name AS hotel_name,
|
|||
|
|
h.region_name,
|
|||
|
|
h.website_address,
|
|||
|
|
h.rkn_registry_status,
|
|||
|
|
h.rkn_registry_number,
|
|||
|
|
h.rkn_registry_date,
|
|||
|
|
h.rkn_checked_at,
|
|||
|
|
COUNT(hwc.id) AS chunks_count
|
|||
|
|
FROM hotel_main h
|
|||
|
|
INNER JOIN hotel_website_chunks hwc ON hwc.metadata->>'hotel_id' = h.id::text
|
|||
|
|
WHERE h.region_name = 'Чукотский автономный округ'
|
|||
|
|
GROUP BY h.id, h.full_name, h.region_name, h.website_address,
|
|||
|
|
h.rkn_registry_status, h.rkn_registry_number, h.rkn_registry_date, h.rkn_checked_at
|
|||
|
|
ORDER BY h.full_name
|
|||
|
|
""")
|
|||
|
|
|
|||
|
|
hotels = cur.fetchall()
|
|||
|
|
cur.close()
|
|||
|
|
conn.close()
|
|||
|
|
|
|||
|
|
return hotels
|
|||
|
|
|
|||
|
|
def save_audit_to_db(hotel_id: str, hotel_name: str, region: str, audit_result: dict):
|
|||
|
|
"""Сохранить результаты аудита в БД"""
|
|||
|
|
try:
|
|||
|
|
conn = psycopg2.connect(**DB_CONFIG)
|
|||
|
|
cur = conn.cursor()
|
|||
|
|
|
|||
|
|
# Формируем данные для сохранения
|
|||
|
|
criteria_results = audit_result.get('criteria_results', [])
|
|||
|
|
total_score = audit_result.get('found', 0)
|
|||
|
|
max_score = audit_result.get('total_criteria', 17)
|
|||
|
|
score_percentage = audit_result.get('compliance_percentage', 0)
|
|||
|
|
website = audit_result.get('website', '')
|
|||
|
|
has_website = bool(website and website != 'НЕТ САЙТА')
|
|||
|
|
|
|||
|
|
# Добавляем РКН данные в criteria_results для полноты
|
|||
|
|
rkn_criterion = {
|
|||
|
|
'criterion_id': 6,
|
|||
|
|
'criterion_name': 'РКН Реестр',
|
|||
|
|
'found': audit_result.get('rkn_status', '').lower() == 'found',
|
|||
|
|
'rkn_status': audit_result.get('rkn_status'),
|
|||
|
|
'rkn_number': audit_result.get('rkn_number'),
|
|||
|
|
'rkn_date': audit_result.get('rkn_date')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Вставляем РКН критерий на позицию 6 (после критерия 5)
|
|||
|
|
criteria_with_rkn = criteria_results[:5] + [rkn_criterion] + criteria_results[5:]
|
|||
|
|
|
|||
|
|
# Сохраняем в БД (обновляем если уже есть)
|
|||
|
|
cur.execute("""
|
|||
|
|
INSERT INTO hotel_audit_results (
|
|||
|
|
hotel_id, region_name, hotel_name, website, has_website,
|
|||
|
|
criteria_results, total_score, max_score, score_percentage,
|
|||
|
|
audit_version
|
|||
|
|
) VALUES (
|
|||
|
|
%s, %s, %s, %s, %s,
|
|||
|
|
%s, %s, %s, %s,
|
|||
|
|
'v1.0_with_rkn'
|
|||
|
|
)
|
|||
|
|
ON CONFLICT (hotel_id, audit_version)
|
|||
|
|
DO UPDATE SET
|
|||
|
|
region_name = EXCLUDED.region_name,
|
|||
|
|
hotel_name = EXCLUDED.hotel_name,
|
|||
|
|
website = EXCLUDED.website,
|
|||
|
|
has_website = EXCLUDED.has_website,
|
|||
|
|
criteria_results = EXCLUDED.criteria_results,
|
|||
|
|
total_score = EXCLUDED.total_score,
|
|||
|
|
max_score = EXCLUDED.max_score,
|
|||
|
|
score_percentage = EXCLUDED.score_percentage,
|
|||
|
|
audit_date = CURRENT_TIMESTAMP
|
|||
|
|
""", (
|
|||
|
|
hotel_id, region, hotel_name, website, has_website,
|
|||
|
|
json.dumps(criteria_with_rkn, ensure_ascii=False),
|
|||
|
|
total_score, max_score, score_percentage
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
conn.commit()
|
|||
|
|
cur.close()
|
|||
|
|
conn.close()
|
|||
|
|
print(f" 💾 Сохранено в БД")
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" ⚠️ Ошибка сохранения в БД: {e}")
|
|||
|
|
|
|||
|
|
def audit_hotel(hotel_id: str, hotel_name: str) -> dict:
|
|||
|
|
"""Запустить аудит отеля через webhook"""
|
|||
|
|
try:
|
|||
|
|
print(f" 🔍 Аудит: {hotel_name[:50]}...")
|
|||
|
|
|
|||
|
|
response = requests.post(
|
|||
|
|
WEBHOOK_URL,
|
|||
|
|
json={"hotel_id": hotel_id},
|
|||
|
|
timeout=400 # 6+ минут таймаут для обхода Nginx
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if response.status_code == 200:
|
|||
|
|
data = response.json()
|
|||
|
|
print(f" ✅ Готово! Найдено: {data[0]['found']}/{data[0]['total_criteria']}")
|
|||
|
|
return data[0]
|
|||
|
|
else:
|
|||
|
|
print(f" ❌ Ошибка {response.status_code}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
except requests.Timeout:
|
|||
|
|
print(f" ⏱️ Таймаут (>180 сек)")
|
|||
|
|
return None
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" ❌ Ошибка: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def create_excel_report(results: list, filename: str = "audit_chukotka.xlsx"):
|
|||
|
|
"""Создать Excel отчёт в горизонтальном формате"""
|
|||
|
|
|
|||
|
|
wb = openpyxl.Workbook()
|
|||
|
|
ws = wb.active
|
|||
|
|
ws.title = "Аудит"
|
|||
|
|
|
|||
|
|
# Стили
|
|||
|
|
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
|
|||
|
|
header_font = Font(color="FFFFFF", bold=True, size=10)
|
|||
|
|
found_fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
|
|||
|
|
not_found_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
|
|||
|
|
border = Border(
|
|||
|
|
left=Side(style='thin'),
|
|||
|
|
right=Side(style='thin'),
|
|||
|
|
top=Side(style='thin'),
|
|||
|
|
bottom=Side(style='thin')
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ЗАГОЛОВКИ (строка 1)
|
|||
|
|
# Базовые колонки
|
|||
|
|
col = 1
|
|||
|
|
base_headers = ['Отель', 'Сайт', 'Есть сайт', 'Балл', 'Процент']
|
|||
|
|
for header in base_headers:
|
|||
|
|
cell = ws.cell(row=1, column=col, value=header)
|
|||
|
|
cell.fill = header_fill
|
|||
|
|
cell.font = header_font
|
|||
|
|
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
|||
|
|
cell.border = border
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Заголовки критериев (каждый критерий - 3 колонки)
|
|||
|
|
if results and 'criteria_results' in results[0]:
|
|||
|
|
for criterion_idx, criterion in enumerate(results[0]['criteria_results']):
|
|||
|
|
# Вставляем РКН заголовки после критерия 5 (индекс 4)
|
|||
|
|
if criterion_idx == 5: # После критерия 5 (индекс 5 = 6-й критерий)
|
|||
|
|
# Колонки РКН (критерий #6)
|
|||
|
|
rkn_headers = ['6. РКН Реестр', '6. РКН Номер/Дата', '6. РКН Ссылка']
|
|||
|
|
for header in rkn_headers:
|
|||
|
|
cell = ws.cell(row=1, column=col, value=header)
|
|||
|
|
cell.fill = header_fill
|
|||
|
|
cell.font = header_font
|
|||
|
|
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
|||
|
|
cell.border = border
|
|||
|
|
if 'Номер' in header:
|
|||
|
|
ws.column_dimensions[get_column_letter(col)].width = 30
|
|||
|
|
elif 'Ссылка' in header:
|
|||
|
|
ws.column_dimensions[get_column_letter(col)].width = 50
|
|||
|
|
else:
|
|||
|
|
ws.column_dimensions[get_column_letter(col)].width = 20
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
criterion_name = f"{criterion['criterion_id']}. {criterion['criterion_name']}"
|
|||
|
|
|
|||
|
|
# Колонка 1: Статус (ДА/НЕТ)
|
|||
|
|
cell = ws.cell(row=1, column=col, value=criterion_name)
|
|||
|
|
cell.fill = header_fill
|
|||
|
|
cell.font = header_font
|
|||
|
|
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
|||
|
|
cell.border = border
|
|||
|
|
ws.column_dimensions[get_column_letter(col)].width = 35
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Колонка 2: URL
|
|||
|
|
cell = ws.cell(row=1, column=col, value=f"{criterion['criterion_id']}. Апрув URL")
|
|||
|
|
cell.fill = header_fill
|
|||
|
|
cell.font = header_font
|
|||
|
|
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
|||
|
|
cell.border = border
|
|||
|
|
ws.column_dimensions[get_column_letter(col)].width = 40
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Колонка 3: Цитата/Детали
|
|||
|
|
cell = ws.cell(row=1, column=col, value=f"{criterion['criterion_id']}. Комментарий")
|
|||
|
|
cell.fill = header_fill
|
|||
|
|
cell.font = header_font
|
|||
|
|
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
|||
|
|
cell.border = border
|
|||
|
|
ws.column_dimensions[get_column_letter(col)].width = 50
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Высота строки заголовков
|
|||
|
|
ws.row_dimensions[1].height = 40
|
|||
|
|
|
|||
|
|
# ДАННЫЕ (строки 2+)
|
|||
|
|
for row_idx, result in enumerate(results, 2):
|
|||
|
|
col = 1
|
|||
|
|
|
|||
|
|
# Базовые данные
|
|||
|
|
# Колонка A: Название отеля
|
|||
|
|
cell = ws.cell(row=row_idx, column=col, value=result['hotel_name'])
|
|||
|
|
cell.border = border
|
|||
|
|
cell.alignment = Alignment(vertical='top', wrap_text=True)
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Колонка B: Сайт
|
|||
|
|
cell = ws.cell(row=row_idx, column=col, value=result.get('website', 'НЕТ САЙТА'))
|
|||
|
|
cell.border = border
|
|||
|
|
cell.alignment = Alignment(vertical='top')
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Колонка C: Есть сайт
|
|||
|
|
has_website = "Да" if result.get('website') and result.get('website') != 'НЕТ САЙТА' else "Нет"
|
|||
|
|
cell = ws.cell(row=row_idx, column=col, value=has_website)
|
|||
|
|
cell.border = border
|
|||
|
|
cell.alignment = Alignment(horizontal='center', vertical='center')
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Колонка D: Балл (количество найденных)
|
|||
|
|
cell = ws.cell(row=row_idx, column=col, value=result['found'])
|
|||
|
|
cell.border = border
|
|||
|
|
cell.alignment = Alignment(horizontal='center', vertical='center')
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Колонка E: Процент
|
|||
|
|
perc_cell = ws.cell(row=row_idx, column=col, value=f"{result['compliance_percentage']:.1f}%")
|
|||
|
|
perc_cell.border = border
|
|||
|
|
perc_cell.alignment = Alignment(horizontal='center', vertical='center')
|
|||
|
|
if result['compliance_percentage'] >= 70:
|
|||
|
|
perc_cell.fill = found_fill
|
|||
|
|
elif result['compliance_percentage'] < 50:
|
|||
|
|
perc_cell.fill = not_found_fill
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Данные по критериям (каждый критерий - 3 колонки)
|
|||
|
|
for criterion_idx, criterion in enumerate(result.get('criteria_results', [])):
|
|||
|
|
# Вставляем РКН колонки после критерия 5 (индекс 4)
|
|||
|
|
if criterion_idx == 5: # После критерия 5 (индекс 5 = 6-й критерий)
|
|||
|
|
# Колонки РКН (критерий #6)
|
|||
|
|
# Колонка 1: Статус (ДА/НЕТ)
|
|||
|
|
rkn_status = result.get('rkn_status', '')
|
|||
|
|
rkn_in_registry = "ДА" if rkn_status and rkn_status.lower() == 'found' else "НЕТ"
|
|||
|
|
rkn_status_cell = ws.cell(row=row_idx, column=col, value=rkn_in_registry)
|
|||
|
|
rkn_status_cell.border = border
|
|||
|
|
rkn_status_cell.alignment = Alignment(horizontal='center', vertical='center')
|
|||
|
|
if rkn_in_registry == "ДА":
|
|||
|
|
rkn_status_cell.fill = not_found_fill # Красный - плохо если в реестре
|
|||
|
|
else:
|
|||
|
|
rkn_status_cell.fill = found_fill # Зелёный - хорошо если НЕ в реестре
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Колонка 2: Номер и дата
|
|||
|
|
rkn_number = result.get('rkn_number', '')
|
|||
|
|
rkn_date = result.get('rkn_date', '')
|
|||
|
|
rkn_info = f"{rkn_number}\n{rkn_date}" if rkn_number or rkn_date else "-"
|
|||
|
|
cell = ws.cell(row=row_idx, column=col, value=rkn_info)
|
|||
|
|
cell.border = border
|
|||
|
|
cell.alignment = Alignment(vertical='top', wrap_text=True)
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Колонка 3: Ссылка на реестр
|
|||
|
|
rkn_url = f"https://rkn.gov.ru/mass-communications/reestr/search/?q={rkn_number}" if rkn_number else "-"
|
|||
|
|
cell = ws.cell(row=row_idx, column=col, value=rkn_url)
|
|||
|
|
cell.border = border
|
|||
|
|
cell.alignment = Alignment(vertical='top')
|
|||
|
|
col += 1
|
|||
|
|
# Колонка 1: Статус (ДА/НЕТ)
|
|||
|
|
status = "ДА" if criterion['found'] else "НЕТ"
|
|||
|
|
status_cell = ws.cell(row=row_idx, column=col, value=status)
|
|||
|
|
status_cell.border = border
|
|||
|
|
status_cell.alignment = Alignment(horizontal='center', vertical='center')
|
|||
|
|
if criterion['found']:
|
|||
|
|
status_cell.fill = found_fill
|
|||
|
|
else:
|
|||
|
|
status_cell.fill = not_found_fill
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Колонка 2: URL
|
|||
|
|
url = criterion['ai_agent']['url'] if criterion['ai_agent']['url'] else '-'
|
|||
|
|
cell = ws.cell(row=row_idx, column=col, value=url)
|
|||
|
|
cell.border = border
|
|||
|
|
cell.alignment = Alignment(vertical='top')
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Колонка 3: Комментарий/Цитата
|
|||
|
|
if criterion['found']:
|
|||
|
|
# Приоритет: AI детали → AI цитата → Regex извлечение → "Найдено"
|
|||
|
|
comment = ""
|
|||
|
|
|
|||
|
|
# Если AI нашёл - берём его данные
|
|||
|
|
if criterion['ai_agent']['found']:
|
|||
|
|
comment = criterion['ai_agent']['details'] or criterion['ai_agent']['quote']
|
|||
|
|
|
|||
|
|
# Если AI не нашёл, но regex нашёл - берём regex
|
|||
|
|
if not comment or "отсутствует" in comment.lower() or "не найден" in comment.lower():
|
|||
|
|
if criterion['regex']['found'] and criterion['regex']['extracted']:
|
|||
|
|
comment = f"[Regex] {criterion['regex']['extracted']}"
|
|||
|
|
|
|||
|
|
# Если всё ещё пусто
|
|||
|
|
if not comment:
|
|||
|
|
comment = "Найдено"
|
|||
|
|
|
|||
|
|
# Ограничиваем длину
|
|||
|
|
comment = comment[:200] + "..." if len(comment) > 200 else comment
|
|||
|
|
else:
|
|||
|
|
comment = "Не найдено"
|
|||
|
|
|
|||
|
|
cell = ws.cell(row=row_idx, column=col, value=comment)
|
|||
|
|
cell.border = border
|
|||
|
|
cell.alignment = Alignment(vertical='top', wrap_text=True)
|
|||
|
|
col += 1
|
|||
|
|
|
|||
|
|
# Высота строки
|
|||
|
|
ws.row_dimensions[row_idx].height = 50
|
|||
|
|
|
|||
|
|
# Ширина базовых колонок
|
|||
|
|
ws.column_dimensions['A'].width = 40 # Отель
|
|||
|
|
ws.column_dimensions['B'].width = 25 # Сайт
|
|||
|
|
ws.column_dimensions['C'].width = 12 # Есть сайт
|
|||
|
|
ws.column_dimensions['D'].width = 8 # Балл
|
|||
|
|
ws.column_dimensions['E'].width = 10 # Процент
|
|||
|
|
|
|||
|
|
# Закрепить первую строку
|
|||
|
|
ws.freeze_panes = 'A2'
|
|||
|
|
|
|||
|
|
# Создаём дашборд на отдельном листе
|
|||
|
|
create_dashboard(wb, results)
|
|||
|
|
|
|||
|
|
wb.save(filename)
|
|||
|
|
print(f"\n✅ Excel отчёт сохранён: {filename}")
|
|||
|
|
|
|||
|
|
def create_dashboard(wb, results):
|
|||
|
|
"""Создать дашборд с графиками и статистикой"""
|
|||
|
|
|
|||
|
|
# Создаём новый лист для дашборда
|
|||
|
|
ws = wb.create_sheet("📊 Дашборд", 0) # Вставляем первым
|
|||
|
|
|
|||
|
|
# Стили
|
|||
|
|
title_font = Font(size=16, bold=True, color="366092")
|
|||
|
|
header_font = Font(size=12, bold=True, color="FFFFFF")
|
|||
|
|
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
|
|||
|
|
value_font = Font(size=14, bold=True)
|
|||
|
|
green_fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
|
|||
|
|
red_fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
|
|||
|
|
yellow_fill = PatternFill(start_color="FFEB9C", end_color="FFEB9C", fill_type="solid")
|
|||
|
|
|
|||
|
|
# Заголовок
|
|||
|
|
ws['A1'] = '📊 ДАШБОРД АУДИТА ОТЕЛЕЙ'
|
|||
|
|
ws['A1'].font = title_font
|
|||
|
|
ws.merge_cells('A1:F1')
|
|||
|
|
|
|||
|
|
# Общая статистика (строки 3-10)
|
|||
|
|
row = 3
|
|||
|
|
ws[f'A{row}'] = 'ОБЩАЯ СТАТИСТИКА'
|
|||
|
|
ws[f'A{row}'].font = Font(size=14, bold=True)
|
|||
|
|
ws.merge_cells(f'A{row}:B{row}')
|
|||
|
|
row += 1
|
|||
|
|
|
|||
|
|
total_hotels = len(results)
|
|||
|
|
hotels_with_website = sum(1 for r in results if r.get('website') and r.get('website') != 'НЕТ САЙТА')
|
|||
|
|
hotels_without_website = total_hotels - hotels_with_website
|
|||
|
|
|
|||
|
|
# Считаем РКН
|
|||
|
|
hotels_in_rkn = sum(1 for r in results if r.get('rkn_status', '').lower() == 'found')
|
|||
|
|
|
|||
|
|
avg_score = sum(r['compliance_percentage'] for r in results) / total_hotels if total_hotels > 0 else 0
|
|||
|
|
|
|||
|
|
stats = [
|
|||
|
|
('Всего отелей:', total_hotels, None),
|
|||
|
|
('С сайтами:', hotels_with_website, green_fill),
|
|||
|
|
('Без сайтов:', hotels_without_website, red_fill),
|
|||
|
|
('В реестре РКН:', hotels_in_rkn, red_fill if hotels_in_rkn > 0 else green_fill),
|
|||
|
|
('Средний балл:', f"{avg_score:.1f}%", yellow_fill if avg_score < 50 else green_fill),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
for label, value, fill in stats:
|
|||
|
|
ws[f'A{row}'] = label
|
|||
|
|
ws[f'B{row}'] = value
|
|||
|
|
ws[f'B{row}'].font = value_font
|
|||
|
|
if fill:
|
|||
|
|
ws[f'B{row}'].fill = fill
|
|||
|
|
ws[f'B{row}'].alignment = Alignment(horizontal='center')
|
|||
|
|
row += 1
|
|||
|
|
|
|||
|
|
# Данные для графика "Наличие сайтов" (строки 12-15)
|
|||
|
|
row = 12
|
|||
|
|
ws[f'A{row}'] = 'Категория'
|
|||
|
|
ws[f'B{row}'] = 'Количество'
|
|||
|
|
ws[f'A{row}'].fill = header_fill
|
|||
|
|
ws[f'B{row}'].fill = header_fill
|
|||
|
|
ws[f'A{row}'].font = header_font
|
|||
|
|
ws[f'B{row}'].font = header_font
|
|||
|
|
row += 1
|
|||
|
|
|
|||
|
|
ws[f'A{row}'] = 'С сайтами'
|
|||
|
|
ws[f'B{row}'] = hotels_with_website
|
|||
|
|
row += 1
|
|||
|
|
ws[f'A{row}'] = 'Без сайтов'
|
|||
|
|
ws[f'B{row}'] = hotels_without_website
|
|||
|
|
row += 1
|
|||
|
|
ws[f'A{row}'] = 'В реестре РКН'
|
|||
|
|
ws[f'B{row}'] = hotels_in_rkn
|
|||
|
|
|
|||
|
|
# Круговая диаграмма "Наличие сайтов"
|
|||
|
|
pie = PieChart()
|
|||
|
|
labels = Reference(ws, min_col=1, min_row=13, max_row=15)
|
|||
|
|
data = Reference(ws, min_col=2, min_row=12, max_row=15)
|
|||
|
|
pie.add_data(data, titles_from_data=True)
|
|||
|
|
pie.set_categories(labels)
|
|||
|
|
pie.title = "Распределение отелей"
|
|||
|
|
pie.height = 10
|
|||
|
|
pie.width = 15
|
|||
|
|
|
|||
|
|
# Добавляем метки данных
|
|||
|
|
pie.dataLabels = DataLabelList()
|
|||
|
|
pie.dataLabels.showPercent = True
|
|||
|
|
pie.dataLabels.showVal = True
|
|||
|
|
|
|||
|
|
ws.add_chart(pie, "D3")
|
|||
|
|
|
|||
|
|
# Статистика по критериям (строки 18+)
|
|||
|
|
row = 18
|
|||
|
|
ws[f'A{row}'] = 'СТАТИСТИКА ПО КРИТЕРИЯМ'
|
|||
|
|
ws[f'A{row}'].font = Font(size=14, bold=True)
|
|||
|
|
ws.merge_cells(f'A{row}:C{row}')
|
|||
|
|
row += 1
|
|||
|
|
|
|||
|
|
ws[f'A{row}'] = 'Критерий'
|
|||
|
|
ws[f'B{row}'] = 'Найдено'
|
|||
|
|
ws[f'C{row}'] = 'Не найдено'
|
|||
|
|
for col in ['A', 'B', 'C']:
|
|||
|
|
ws[f'{col}{row}'].fill = header_fill
|
|||
|
|
ws[f'{col}{row}'].font = header_font
|
|||
|
|
row += 1
|
|||
|
|
|
|||
|
|
# Собираем статистику по каждому критерию
|
|||
|
|
if results and 'criteria_results' in results[0]:
|
|||
|
|
criteria_stats = []
|
|||
|
|
for criterion in results[0]['criteria_results']:
|
|||
|
|
criterion_id = criterion['criterion_id']
|
|||
|
|
criterion_name = f"{criterion_id}. {criterion['criterion_name'][:30]}"
|
|||
|
|
|
|||
|
|
found_count = sum(1 for r in results
|
|||
|
|
for c in r['criteria_results']
|
|||
|
|
if c['criterion_id'] == criterion_id and c['found'])
|
|||
|
|
not_found_count = total_hotels - found_count
|
|||
|
|
|
|||
|
|
criteria_stats.append((criterion_name, found_count, not_found_count))
|
|||
|
|
|
|||
|
|
# Добавляем РКН как критерий #6
|
|||
|
|
rkn_found = sum(1 for r in results if r.get('rkn_status', '').lower() != 'found') # НЕ в реестре = хорошо
|
|||
|
|
rkn_not_found = total_hotels - rkn_found
|
|||
|
|
|
|||
|
|
# Вставляем РКН на позицию 6
|
|||
|
|
criteria_stats_with_rkn = criteria_stats[:5] + [('6. РКН Реестр (чисто)', rkn_found, rkn_not_found)] + criteria_stats[5:]
|
|||
|
|
|
|||
|
|
start_row = row
|
|||
|
|
for criterion_name, found, not_found in criteria_stats_with_rkn:
|
|||
|
|
ws[f'A{row}'] = criterion_name
|
|||
|
|
ws[f'B{row}'] = found
|
|||
|
|
ws[f'C{row}'] = not_found
|
|||
|
|
row += 1
|
|||
|
|
|
|||
|
|
# Столбчатая диаграмма по критериям
|
|||
|
|
chart = BarChart()
|
|||
|
|
chart.type = "col"
|
|||
|
|
chart.style = 10
|
|||
|
|
chart.title = "Результаты по критериям"
|
|||
|
|
chart.y_axis.title = 'Количество отелей'
|
|||
|
|
chart.x_axis.title = 'Критерии'
|
|||
|
|
|
|||
|
|
data = Reference(ws, min_col=2, min_row=start_row-1, max_row=row-1, max_col=3)
|
|||
|
|
cats = Reference(ws, min_col=1, min_row=start_row, max_row=row-1)
|
|||
|
|
chart.add_data(data, titles_from_data=True)
|
|||
|
|
chart.set_categories(cats)
|
|||
|
|
chart.height = 15
|
|||
|
|
chart.width = 25
|
|||
|
|
|
|||
|
|
ws.add_chart(chart, f"E{start_row}")
|
|||
|
|
|
|||
|
|
# Распределение по баллам (строки row+2)
|
|||
|
|
row += 2
|
|||
|
|
ws[f'A{row}'] = 'РАСПРЕДЕЛЕНИЕ ПО БАЛЛАМ'
|
|||
|
|
ws[f'A{row}'].font = Font(size=14, bold=True)
|
|||
|
|
ws.merge_cells(f'A{row}:B{row}')
|
|||
|
|
row += 1
|
|||
|
|
|
|||
|
|
ws[f'A{row}'] = 'Диапазон'
|
|||
|
|
ws[f'B{row}'] = 'Количество'
|
|||
|
|
ws[f'A{row}'].fill = header_fill
|
|||
|
|
ws[f'B{row}'].fill = header_fill
|
|||
|
|
ws[f'A{row}'].font = header_font
|
|||
|
|
ws[f'B{row}'].font = header_font
|
|||
|
|
row += 1
|
|||
|
|
|
|||
|
|
# Распределение по диапазонам
|
|||
|
|
ranges = [
|
|||
|
|
('0-25%', 0, 25),
|
|||
|
|
('26-50%', 26, 50),
|
|||
|
|
('51-75%', 51, 75),
|
|||
|
|
('76-100%', 76, 100)
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
start_row = row
|
|||
|
|
for range_name, min_val, max_val in ranges:
|
|||
|
|
count = sum(1 for r in results
|
|||
|
|
if min_val <= r['compliance_percentage'] <= max_val)
|
|||
|
|
ws[f'A{row}'] = range_name
|
|||
|
|
ws[f'B{row}'] = count
|
|||
|
|
row += 1
|
|||
|
|
|
|||
|
|
# Столбчатая диаграмма распределения
|
|||
|
|
chart2 = BarChart()
|
|||
|
|
chart2.type = "col"
|
|||
|
|
chart2.style = 11
|
|||
|
|
chart2.title = "Распределение по баллам"
|
|||
|
|
chart2.y_axis.title = 'Количество отелей'
|
|||
|
|
|
|||
|
|
data = Reference(ws, min_col=2, min_row=start_row-1, max_row=row-1)
|
|||
|
|
cats = Reference(ws, min_col=1, min_row=start_row, max_row=row-1)
|
|||
|
|
chart2.add_data(data, titles_from_data=True)
|
|||
|
|
chart2.set_categories(cats)
|
|||
|
|
chart2.height = 10
|
|||
|
|
chart2.width = 15
|
|||
|
|
|
|||
|
|
ws.add_chart(chart2, f"D{start_row-1}")
|
|||
|
|
|
|||
|
|
# Настройка ширины колонок
|
|||
|
|
ws.column_dimensions['A'].width = 35
|
|||
|
|
ws.column_dimensions['B'].width = 15
|
|||
|
|
ws.column_dimensions['C'].width = 15
|
|||
|
|
|
|||
|
|
print(" 📊 Дашборд создан")
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
print("🚀 ЗАПУСК АУДИТА ЧУКОТКИ\n" + "="*60)
|
|||
|
|
|
|||
|
|
# Получаем отели
|
|||
|
|
hotels = get_chukotka_hotels()
|
|||
|
|
print(f"📊 Найдено отелей Чукотки с chunks: {len(hotels)}\n")
|
|||
|
|
|
|||
|
|
if not hotels:
|
|||
|
|
print("❌ Нет отелей для аудита")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Аудитируем
|
|||
|
|
results = []
|
|||
|
|
for idx, hotel in enumerate(hotels, 1):
|
|||
|
|
print(f"\n[{idx}/{len(hotels)}] {hotel['hotel_name']}")
|
|||
|
|
print(f" 🔗 {hotel['website_address']}")
|
|||
|
|
print(f" 📦 Chunks: {hotel['chunks_count']}")
|
|||
|
|
|
|||
|
|
audit_result = audit_hotel(hotel['hotel_id'], hotel['hotel_name'])
|
|||
|
|
|
|||
|
|
if audit_result:
|
|||
|
|
audit_result['website'] = hotel['website_address']
|
|||
|
|
# Добавляем данные РКН
|
|||
|
|
audit_result['rkn_status'] = hotel.get('rkn_registry_status')
|
|||
|
|
audit_result['rkn_number'] = hotel.get('rkn_registry_number')
|
|||
|
|
audit_result['rkn_date'] = hotel.get('rkn_registry_date')
|
|||
|
|
audit_result['rkn_checked_at'] = hotel.get('rkn_checked_at')
|
|||
|
|
|
|||
|
|
# Сохраняем в БД
|
|||
|
|
save_audit_to_db(
|
|||
|
|
hotel['hotel_id'],
|
|||
|
|
hotel['hotel_name'],
|
|||
|
|
hotel['region_name'],
|
|||
|
|
audit_result
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
results.append(audit_result)
|
|||
|
|
|
|||
|
|
# Пауза между запросами
|
|||
|
|
if idx < len(hotels):
|
|||
|
|
time.sleep(2)
|
|||
|
|
else:
|
|||
|
|
print(f" ⚠️ Пропускаем отель")
|
|||
|
|
|
|||
|
|
# Создаём Excel
|
|||
|
|
if results:
|
|||
|
|
print(f"\n📊 ИТОГОВАЯ СТАТИСТИКА\n" + "="*60)
|
|||
|
|
print(f"Обработано отелей: {len(results)}/{len(hotels)}")
|
|||
|
|
avg_compliance = sum(r['compliance_percentage'] for r in results) / len(results)
|
|||
|
|
print(f"Средний % соответствия: {avg_compliance:.1f}%")
|
|||
|
|
|
|||
|
|
filename = f"audit_chukotka_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
|||
|
|
create_excel_report(results, filename)
|
|||
|
|
else:
|
|||
|
|
print("\n❌ Нет результатов для отчёта")
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
main()
|
|||
|
|
|