diff --git a/antifroud/templates/antifroud/admin/external_db_settings_change_form.html b/antifroud/templates/antifroud/admin/external_db_settings_change_form.html index cba4fd0e..e2e3fff6 100644 --- a/antifroud/templates/antifroud/admin/external_db_settings_change_form.html +++ b/antifroud/templates/antifroud/admin/external_db_settings_change_form.html @@ -119,12 +119,12 @@ alert("ID подключения отсутствует."); return; } - fetch(`/admin/antifroud/externaldbsettings/test-connection/?db_id=${dbId}`) + fetch(`/antifroud/externaldbsettings/test-connection/?db_id=${dbId}`) .then(response => response.json()) .then(data => { if (data.status === "success") { document.getElementById("connection-status").innerHTML = `
${data.message}
`; - fetch(`/admin/antifroud/externaldbsettings/fetch-tables/?db_id=${dbId}`) + fetch(`/antifroud/externaldbsettings/fetch-tables/?db_id=${dbId}`) .then(response => response.json()) .then(tableData => { if (tableData.status === "success") { @@ -153,7 +153,7 @@ return; } - fetch(`/admin/antifroud/externaldbsettings/fetch-table-data/?db_id=${dbId}&table_name=${tableName}`) + fetch(`/antifroud/externaldbsettings/fetch-table-data/?db_id=${dbId}&table_name=${tableName}`) .then(response => response.json()) .then(data => { if (data.status === "success") { diff --git a/bot/operations/hotels.py b/bot/operations/hotels.py index 3ec5371c..21c8f0cd 100644 --- a/bot/operations/hotels.py +++ b/bot/operations/hotels.py @@ -78,6 +78,53 @@ async def delete_hotel(update: Update, context): await query.edit_message_text("Отель не найден.") +# async def check_pms(update, context): +# query = update.callback_query + +# try: +# # Получение ID отеля из callback_data +# hotel_id = query.data.split("_")[2] +# logger.debug(f"Hotel ID: {hotel_id}") +# logger.debug(f"Hotel ID type : {type(hotel_id)}") +# # Получение конфигурации отеля и PMS +# hotel = await sync_to_async(Hotel.objects.select_related('pms').get)(id=hotel_id) +# pms_config = hotel.pms + +# if not pms_config: +# await query.edit_message_text("PMS конфигурация не найдена.") +# return + +# # Создаем экземпляр PMSIntegrationManager +# pms_manager = PMSIntegrationManager(hotel_id=hotel_id) +# await pms_manager.load_hotel() +# await sync_to_async(pms_manager.load_plugin)() + +# # Проверяем, какой способ интеграции использовать +# if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data): +# # Плагин поддерживает метод fetch_data +# report = await pms_manager.plugin._fetch_data() + +# else: +# await query.edit_message_text("Подходящий способ интеграции с PMS не найден.") +# return + +# # Формируем сообщение о результатах +# result_message = ( +# f"Интеграция PMS завершена успешно.\n" +# f"Обработано интервалов: {report['processed_intervals']}\n" +# f"Обработано записей: {report['processed_items']}\n" +# f"Ошибки: {len(report['errors'])}" +# ) +# logger.info(f'Result_Message: {result_message}\n Result_meaage_type: {type(result_message)}') +# if report["errors"]: +# result_message += "\n\nСписок ошибок:\n" + "\n".join(report["errors"]) + +# await query.edit_message_text(result_message) +# except Exception as e: +# # Обрабатываем и логируем ошибки +# await query.edit_message_text(f"❌ Ошибка: {str(e)}") + + async def check_pms(update, context): query = update.callback_query @@ -86,6 +133,7 @@ async def check_pms(update, context): hotel_id = query.data.split("_")[2] logger.debug(f"Hotel ID: {hotel_id}") logger.debug(f"Hotel ID type : {type(hotel_id)}") + # Получение конфигурации отеля и PMS hotel = await sync_to_async(Hotel.objects.select_related('pms').get)(id=hotel_id) pms_config = hotel.pms @@ -103,29 +151,37 @@ async def check_pms(update, context): if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data): # Плагин поддерживает метод fetch_data report = await pms_manager.plugin._fetch_data() - + logger.debug(f"REPORT: {report}, TYPE: {type(report)}") else: await query.edit_message_text("Подходящий способ интеграции с PMS не найден.") return + # Проверяем результат выполнения fetch_data + if not report or not isinstance(report, dict): + logger.error(f"Некорректный отчет от fetch_data: {report}") + await query.edit_message_text("Ошибка: Отчет fetch_data отсутствует или имеет некорректный формат.") + return + # Формируем сообщение о результатах result_message = ( f"Интеграция PMS завершена успешно.\n" - f"Обработано интервалов: {report['processed_intervals']}\n" - f"Обработано записей: {report['processed_items']}\n" - f"Ошибки: {len(report['errors'])}" + f"Обработано интервалов: {report.get('processed_intervals', 0)}\n" + f"Обработано записей: {report.get('processed_items', 0)}\n" + f"Ошибки: {len(report.get('errors', []))}" ) - logger.info(f'Result_Message: {result_message}\n Result_meaage_type: {type(result_message)}') - if report["errors"]: + if report.get("errors"): result_message += "\n\nСписок ошибок:\n" + "\n".join(report["errors"]) + logger.info(f"Result_Message: {result_message}") await query.edit_message_text(result_message) except Exception as e: # Обрабатываем и логируем ошибки + logger.error(f"Ошибка в методе check_pms: {str(e)}", exc_info=True) await query.edit_message_text(f"❌ Ошибка: {str(e)}") + async def setup_rooms(update: Update, context): """Настроить номера отеля.""" query = update.callback_query diff --git a/bot/operations/statistics.py b/bot/operations/statistics.py index f71fa729..f049c3f9 100644 --- a/bot/operations/statistics.py +++ b/bot/operations/statistics.py @@ -4,7 +4,8 @@ from telegram.ext import ContextTypes from asgiref.sync import sync_to_async from hotels.models import Reservation, Hotel from users.models import User - +import pytz +from pytz import timezone from bot.utils.pdf_report import generate_pdf_report from bot.utils.database import get_hotels_for_user, get_hotel_by_name from datetime import datetime @@ -13,10 +14,10 @@ import os import traceback import logging from touchh.utils.log import CustomLogger +from django.utils.timezone import localtime +from ..utils.date_utils import ensure_datetime - - -logger = CustomLogger(__name__).get_logger() +logger = CustomLogger(name="Statistics.py", log_level="DEBUG").get_logger() async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): """Вывод списка отелей для статистики.""" @@ -57,40 +58,13 @@ async def stats_select_period(update: Update, context: ContextTypes.DEFAULT_TYPE [InlineKeyboardButton("День", callback_data="stats_period_day")], [InlineKeyboardButton("Неделя", callback_data="stats_period_week")], [InlineKeyboardButton("Месяц", callback_data="stats_period_month")], - [InlineKeyboardButton("Все время", callback_data="stats_period_all")], + [InlineKeyboardButton("Год", callback_data="stats_period_year")], [InlineKeyboardButton("🏠 Главная", callback_data="main_menu")], [InlineKeyboardButton("🔙 Назад", callback_data="statistics")], ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text("Выберите период времени:", reply_markup=reply_markup) - - -def ensure_datetime(value): - """ - Преобразует значение в timezone-aware datetime объект, если это возможно. - - :param value: Значение для преобразования - :type value: str, datetime или другое - :return: timezone-aware datetime - """ - if isinstance(value, datetime): - # Если это объект datetime, проверяем, наивен ли он - return make_aware(value) if is_naive(value) else value - elif isinstance(value, str): - # Если это строка, пытаемся преобразовать в datetime - try: - return make_aware(datetime.strptime(value, '%Y-%m-%d %H:%M:%S')) - except ValueError: - # Если формат не соответствует, пробуем более общую обработку - try: - return make_aware(datetime.fromisoformat(value)) - except ValueError: - # Если не получилось распознать формат - logging.warning(f"Невозможно преобразовать строку в datetime: {value}") - else: - # Если тип неизвестен, просто возвращаем None - logging.warning(f"Получено значение неизвестного типа для преобразования в datetime: {value}") - return None + async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): """Генерация и отправка статистики.""" @@ -100,14 +74,13 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE try: hotel_id = context.user_data.get("selected_hotel") if not hotel_id: - raise ValueError(f"ID отеля не найден в user_data: {context.user_data}") + raise ValueError("ID отеля не найден в user_data") period = query.data.split("_")[2] now = ensure_datetime(datetime.utcnow()) - # Получаем диапазон дат start_date, end_date = get_period_dates(period, now) - + print(type(start_date), type(end_date)) reservations = await sync_to_async(list)( Reservation.objects.filter( hotel_id=hotel_id, @@ -117,7 +90,7 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE ) if not reservations: - await query.edit_message_text("Нет данных для статистики за выбранный период.") + await query.edit_message_text(f"Нет данных для статистики за выбранный период.{start_date} - {end_date}") return hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) @@ -131,30 +104,70 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE os.remove(file_path) except Exception as e: - logging.error(f"Ошибка в generate_statistics: {str(e)}", exc_info=True) - logging.error(f'start_date_type: {type(start_date)}, \n end_date_type: {type(end_date)}\n') - await query.edit_message_text(f"Произошла ошибка: {str(e)}") + logging.error(f"Ошибка в generate_statistics: {e}", exc_info=True) + await query.edit_message_text(f"Произошла ошибка: {e}") -def get_period_dates(period, now): - now = ensure_datetime(now) - if period == "day": - start_date = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) - end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) - elif period == "week": - start_date = (now - timedelta(days=7)).replace(hour=0, minute=0, second=0, microsecond=0) - end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) - elif period == "month": - start_date = (now - timedelta(days=30)).replace(hour=0, minute=0, second=0, microsecond=0) - end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) - elif period == "all": - start_date = (now - timedelta(days=1500)).replace(hour=0, minute=0, second=0, microsecond=0) - end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) +def get_period_dates(period, now=None): + """ + Возвращает диапазон дат (start_date, end_date) для заданного периода. + + :param period: Период (строка: 'today', 'yesterday', 'last_week', 'last_month'). + :param now: Текущая дата/время (опционально). + :return: Кортеж (start_date, end_date). + :raises: ValueError, если период не поддерживается. + """ + if now is None: + now = datetime.now(pytz.UTC) else: - start_date = (now - timedelta(days=1500)).replace(hour=0, minute=0, second=0, microsecond=0) - end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) - return start_date, end_date + now = ensure_datetime(now) # Приведение now к timezone-aware + if period == "today": + start_date = now.replace(hour=0, minute=0, second=0, microsecond=0) + end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) + + elif period == "yesterday": + start_date = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + end_date = (now - timedelta(days=1)).replace(hour=23, minute=59, second=59, microsecond=999999) + + elif period == "week": + # Последняя неделя: с понедельника предыдущей недели до воскресенья + start_date = (now - timedelta(days=now.weekday() + 7)).replace(hour=0, minute=0, second=0, microsecond=0) + end_date = (start_date + timedelta(days=6)).replace(hour=23, minute=59, second=59, microsecond=999999) + + elif period == "month": + # Текущий месяц: с первого дня месяца до текущей даты + start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) + + elif period == "last_month": + # Последний месяц: с первого дня прошлого месяца до последнего дня прошлого месяца + first_day_of_current_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + last_day_of_previous_month = first_day_of_current_month - timedelta(days=1) + start_date = last_day_of_previous_month.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + end_date = last_day_of_previous_month.replace(hour=23, minute=59, second=59, microsecond=999999) + + elif period == "year": + # Текущий год: с первого дня года до текущей даты + start_date = now.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) + end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) + + elif period == "last_year": + # Последний год: с 1 января предыдущего года до 31 декабря предыдущего года + start_date = now.replace(year=now.year - 1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0) + end_date = now.replace(year=now.year - 1, month=12, day=31, hour=23, minute=59, second=59, microsecond=999999) + + else: + raise ValueError(f"Неподдерживаемый период: {period}") + + # Приводим start_date и end_date к timezone-aware, если это не так + if start_date.tzinfo is None: + start_date = start_date.replace(tzinfo=pytz.UTC) + if end_date.tzinfo is None: + end_date = end_date.replace(tzinfo=pytz.UTC) + + # Приведение дат к локальному времени + return localtime(start_date), localtime(end_date) async def stats_back(update: Update, context): """Возврат к выбору отеля.""" diff --git a/bot/utils/pdf_report.py b/bot/utils/pdf_report.py index e0c12909..5f1dc176 100644 --- a/bot/utils/pdf_report.py +++ b/bot/utils/pdf_report.py @@ -1,56 +1,19 @@ from fpdf import FPDF - import os -from datetime import datetime +from datetime import datetime, timedelta from asgiref.sync import sync_to_async -from django.utils.timezone import make_aware, is_naive, is_aware -import os +from django.utils.timezone import make_aware, is_aware, localtime +import pytz +from bot.utils.date_utils import ensure_datetime +from touchh.utils.log import CustomLogger + +logger = CustomLogger(name="CustomPDF Report", log_level="DEBUG").get_logger() # Определение абсолютного пути к папке "reports" BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) REPORTS_DIR = os.path.join(BASE_DIR, "reports") - -# Убедитесь, что директория существует os.makedirs(REPORTS_DIR, exist_ok=True) - - -# Асинхронная функция для извлечения данных о бронировании -def ensure_datetime(value): - """ - Преобразует строку или naive datetime в timezone-aware datetime. - Если значение не удается преобразовать, возвращается None. - """ - if isinstance(value, datetime): - return make_aware(value) if is_naive(value) else value - if isinstance(value, str): - try: - return make_aware(datetime.strptime(value, '%Y-%m-%d %H:%M:%S')) - except ValueError: - print(f"[WARNING] Невозможно преобразовать строку в datetime: {value}") - return None - -# @sync_to_async -# def get_reservation_data(res): -# print(f"[DEBUG] Processing reservation {res.id}") - -# # Убедитесь, что даты являются timezone-aware -# check_in = ensure_datetime(res.check_in) -# check_out = ensure_datetime(res.check_out) - -# result = { -# "hotel_name": res.hotel.name, -# "pms": getattr(res.hotel, 'pms', 'N/A'), -# "reservation_id": res.reservation_id, -# "room_number": res.room_number if res.room_number else "Не указан", -# "room_type": res.room_type, -# "check_in": check_in, -# "check_out": check_out, -# "status": res.status, -# } -# # print(f"[DEBUG] Reservation data: {result}") -# return result - @sync_to_async def get_reservation_data(res): check_in = ensure_datetime(res.check_in) @@ -70,7 +33,8 @@ def get_reservation_data(res): "status": res.status, } - +def sanitize_text(text): + return text.replace("\n", " ").strip() if isinstance(text, str) else text class CustomPDF(FPDF): def __init__(self, hotel_name, start_date, end_date, *args, **kwargs): @@ -78,9 +42,7 @@ class CustomPDF(FPDF): self.font_folder = "bot/fonts/" self.add_font("DejaVuSans-Bold", "", os.path.join(self.font_folder, "DejaVuSans-Bold.ttf"), uni=True) self.add_font("DejaVuSans", "", os.path.join(self.font_folder, "DejaVuSans.ttf"), uni=True) - self.creation_date = datetime.now().strftime("%d.%m.%Y %H:%M:%S") - - # Переданные параметры + self.creation_date = ensure_datetime(datetime.now(pytz.UTC)) self.hotel_name = hotel_name self.start_date = start_date self.end_date = end_date @@ -90,7 +52,6 @@ class CustomPDF(FPDF): self.set_font("DejaVuSans-Bold", size=14) self.cell(0, 10, f"Отчет о бронированиях отеля {self.hotel_name}", ln=1, align="C") self.ln(5) - self.set_font("DejaVuSans", size=10) self.cell( 0, @@ -102,78 +63,21 @@ class CustomPDF(FPDF): self.ln(10) def footer(self): - """Добавление колонтитула внизу страницы.""" self.set_y(-15) self.set_font("DejaVuSans", size=8) self.cell(60, 10, f"Copyright (C) 2024 by Touchh", align="L") self.cell(0, 10, f"Лист {self.page_no()} из {{nb}} / Дата генерации отчета: {self.creation_date}", align="C") - - - - def trim_text_right(self, text, max_width): - """Обрезка текста справа.""" - while self.get_string_width(text) > max_width: - text = text[:-1] - return text + "..." if len(text) > 3 else text - -# async def generate_pdf_report(hotel_name, reservations, start_date, end_date): - -# # Преобразование дат в timezone-aware datetime -# start_date = ensure_datetime(start_date) -# end_date = ensure_datetime(end_date) - -# if not start_date or not end_date: -# raise ValueError(f"Некорректные периоды: start_date={start_date}, end_date={end_date}") - -# # Создание экземпляра PDF с передачей параметров -# pdf = CustomPDF(hotel_name=hotel_name, start_date=start_date, end_date=end_date, orientation="L", unit="mm", format="A4") -# pdf.alias_nb_pages() -# pdf.add_page() # Заголовок отчёта и таблица будут добавлены через методы header и footer - -# # Таблица -# pdf.set_font("DejaVuSans", size=8) -# col_widths = [30, 30, 30, 60, 35, 35, 30] -# row_height = 10 - -# for res in reservations: -# try: -# res_data = await get_reservation_data(res) - -# row_data = [ -# res_data["hotel_name"], -# str(res_data["reservation_id"]), -# res_data["room_number"], -# res_data["room_type"], -# res_data["check_in"].strftime('%Y-%m-%d %H:%M:%S'), -# res_data["check_out"].strftime('%Y-%m-%d %H:%M:%S'), -# res_data["status"], -# ] - -# for col_width, data in zip(col_widths, row_data): -# pdf.cell(col_width, row_height, data, border=1, align="C") -# pdf.ln() -# except Exception as e: -# print(f"pdf_report.py [ERROR] Error processing reservation {res.id}: {e}") - -# # Сохранение PDF -# hotel_name_safe = hotel_name.replace(" ", "_").replace("/", "_") -# start_date_str = start_date.strftime('%Y-%m-%d') -# end_date_str = end_date.strftime('%Y-%m-%d') - -# pdf_output_path = os.path.join(REPORTS_DIR, f"{hotel_name_safe}_report_{start_date_str}-{end_date_str}.pdf") -# pdf.output(pdf_output_path) - -# if not os.path.exists(pdf_output_path): -# raise RuntimeError(f"PDF file was not created at: {pdf_output_path}") - -# return pdf_output_path - async def generate_pdf_report(hotel_name, reservations, start_date, end_date): - # Преобразование дат start_date = ensure_datetime(start_date) end_date = ensure_datetime(end_date) + logger.debug(f"Start_DATE: {start_date} / TYPE: {type(start_date)}") + logger.debug(f"END_DATE: {end_date} / TYPE: {type(end_date)}") + + if not start_date or not end_date: + raise ValueError("Некорректные даты для генерации отчета.") + pdf = CustomPDF(hotel_name=hotel_name, start_date=start_date, end_date=end_date, orientation="L", unit="mm", format="A4") pdf.alias_nb_pages() pdf.add_page() @@ -185,41 +89,31 @@ async def generate_pdf_report(hotel_name, reservations, start_date, end_date): for res in reservations: try: res_data = await get_reservation_data(res) - - # Отладочный вывод - print(f"[DEBUG] Reservation Data: {res_data}") - print(f"[DEBUG] check_in type: {type(res_data['check_in'])}, value: {res_data['check_in']}") - print(f"[DEBUG] check_out type: {type(res_data['check_out'])}, value: {res_data['check_out']}") - - # Проверка и корректировка данных res_data["check_in"] = ensure_datetime(res_data["check_in"]) res_data["check_out"] = ensure_datetime(res_data["check_out"]) row_data = [ - str(res_data["hotel_name"]), - str(res_data["reservation_id"]), - str(res_data["room_number"]), - str(res_data["room_type"]), + sanitize_text(res_data["hotel_name"]), + sanitize_text(str(res_data["reservation_id"])), + sanitize_text(str(res_data["room_number"])), + sanitize_text(str(res_data["room_type"])), res_data["check_in"].strftime('%Y-%m-%d %H:%M:%S'), res_data["check_out"].strftime('%Y-%m-%d %H:%M:%S'), - str(res_data["status"]), + sanitize_text(res_data["status"]), ] - for col_width, data in zip(col_widths, row_data): pdf.cell(col_width, row_height, data, border=1, align="C") pdf.ln() except Exception as e: - print(f"[ERROR] Error processing reservation {res.id}: {e}") - - print("[DEBUG] PDF metadata:", vars(pdf)) + logger.error(f"[ERROR] Error processing reservation {res.id}: {e}") pdf_output_path = os.path.join(REPORTS_DIR, f"{hotel_name.replace(' ', '_')}_report_{start_date.strftime('%Y-%m-%d')}-{end_date.strftime('%Y-%m-%d')}.pdf") + logger.debug(f"PDF output path: {pdf_output_path}") + pdf.output(pdf_output_path) if not os.path.exists(pdf_output_path): raise RuntimeError(f"PDF file was not created at: {pdf_output_path}") return pdf_output_path - - diff --git a/pms_integration/plugins/ecvi_pms.py b/pms_integration/plugins/ecvi_pms.py index 41b14230..7e7fff48 100644 --- a/pms_integration/plugins/ecvi_pms.py +++ b/pms_integration/plugins/ecvi_pms.py @@ -57,7 +57,8 @@ class EcviPMSPlugin(BasePMSPlugin): now = datetime.now() current_date = now.strftime('%Y-%m-%d') yesterday_date = (now - timedelta(days=1)).strftime('%Y-%m-%d') - + processed_items = 0 + errors = [] headers = { "Content-Type": "application/json", } @@ -76,25 +77,48 @@ class EcviPMSPlugin(BasePMSPlugin): return [] # Фильтрация данных - filtered_data = [ - { - 'reservation_id': item.get('task_id'), - 'room_number': item.get('room_name'), - 'room_type': item.get('room_type'), - 'checkin': datetime.strptime(item.get('checkin'), '%Y-%m-%d %H:%M:%S'), - 'checkout': datetime.strptime(item.get('checkout'), '%Y-%m-%d %H:%M:%S'), - 'status': item.get('occupancy') - } for item in data if isinstance(item, dict) and item.get('occupancy') in ['проживание', 'под выезд', 'под заезд'] - ] - - self.logger.debug(f"filtered_data: {filtered_data}") - + filtered_data = [] + for item in data: + try: + if not isinstance(item, dict): + raise ValueError(f"Некорректный формат элемента: {item}") + + reservation_id = item.get('task_id') + if not reservation_id: + raise ValueError("Отсутствует task_id в записи") + + checkin = datetime.strptime(item.get('checkin'), '%Y-%m-%d %H:%M:%S') + checkout = datetime.strptime(item.get('checkout'), '%Y-%m-%d %H:%M:%S') + + filtered_data.append({ + 'reservation_id': reservation_id, + 'room_number': item.get('room_name'), + 'room_type': item.get('room_type'), + 'checkin': checkin, + 'checkout': checkout, + 'status': item.get('occupancy') + }) + processed_items += 1 + except Exception as e: + self.logger.error(f"Ошибка обработки элемента: {e}") + errors.append(str(e)) + # Сохранение данных в базу данных - for item in filtered_data: - await self._save_to_db(item) - - self.logger.debug(f"Данные успешно сохранены.") - return filtered_data + try: + for item in filtered_data: + await self._save_to_db(item) + except Exception as e: + self.logger.error(f"Ошибка сохранения данных в БД: {e}") + errors.append(f"Ошибка сохранения данных в БД: {str(e)}") + + # Формирование отчета + report = { + "processed_intervals": 1, + "processed_items": processed_items, + "errors": errors + } + self.logger.debug(f"Сформированный отчет: {report}") + return report async def _save_to_db(self, item): """ diff --git a/pms_integration/plugins/realtycalendar_pms.py b/pms_integration/plugins/realtycalendar_pms.py index f29fa2cc..d7955ff2 100644 --- a/pms_integration/plugins/realtycalendar_pms.py +++ b/pms_integration/plugins/realtycalendar_pms.py @@ -1,3 +1,194 @@ +# import requests +# import hashlib +# import json +# from .base_plugin import BasePMSPlugin +# from datetime import datetime, timedelta +# from asgiref.sync import sync_to_async +# from touchh.utils.log import CustomLogger +# from hotels.models import Hotel, Reservation +# from app_settings.models import GlobalHotelSettings +# from django.utils import timezone +# class RealtyCalendarPlugin(BasePMSPlugin): +# def __init__(self, config): +# super().__init__(config) +# self.public_key = config.public_key +# self.private_key = config.private_key +# self.api_url = config.url.rstrip("/") +# self.logger = CustomLogger(name="RealtyCalendarPlugin", log_level="DEBUG").get_logger() +# if not self.public_key or not self.private_key: +# raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar") + +# def get_default_parser_settings(self): +# """ +# Возвращает настройки по умолчанию для обработки данных. +# """ +# return { +# "date_format": "%Y-%m-%dT%H:%M:%S", +# "timezone": "UTC" +# } + +# def _get_sorted_keys(self, obj): +# """ +# Возвращает отсортированный по имени список ключей. +# """ +# sorted_keys = sorted(obj.keys()) +# self.logger.debug(f"Отсортированные ключи: {sorted_keys}") +# return sorted_keys + +# def _generate_data_string(self, obj): +# """ +# Формирует строку параметров для подписи. +# """ +# sorted_keys = self._get_sorted_keys(obj) +# string = "".join(f"{key}={obj[key]}" for key in sorted_keys) +# self.logger.debug(f"Сформированная строка данных: {string}") +# return string + self.private_key + +# def _generate_md5(self, string): +# """ +# Генерирует MD5-хеш от строки. +# """ +# md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest() +# self.logger.debug(f"Сформированный MD5-хеш: {md5_hash}") +# return md5_hash + +# def _generate_sign(self, data): +# """ +# Генерирует подпись для данных запроса. +# """ +# data_string = self._generate_data_string(data) +# self.logger.debug(f"Строка для подписи: {data_string}") +# sign = self._generate_md5(data_string) +# self.logger.debug(f"Подпись: {sign}") +# return sign + +# async def _fetch_data(self): +# """ +# Выполняет запрос к API RealtyCalendar для получения данных о бронированиях. +# """ +# self.logger.debug("Начало выполнения функции _fetch_data") +# base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/" +# headers = { +# "Accept": "application/json", +# "Content-Type": "application/json", +# } + +# now = datetime.now() +# data = { +# "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"), +# "end_date": now.strftime("%Y-%m-%d"), +# } +# data["sign"] = self._generate_sign(data) + +# response = requests.post(url=base_url, headers=headers, json=data) +# self.logger.debug(f"Статус ответа: {response.status_code}") + +# if response.status_code != 200: +# self.logger.error(f"Ошибка API: {response.status_code}, {response.text}") +# raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}") + +# try: +# response_data = response.json() +# bookings = response_data.get("bookings", []) +# if not isinstance(bookings, list): +# raise ValueError(f"Ожидался список, но получен {type(bookings)}") +# except Exception as e: +# self.logger.error(f"Ошибка обработки ответа API: {e}") +# raise + +# # Получаем глобальные настройки отеля +# hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) +# hotel_tz = hotel.timezone +# try: +# hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)() +# check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") +# check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") +# except AttributeError: +# # Используем значения по умолчанию, если настроек нет +# check_in_time = "14:00:00" +# check_out_time = "12:00:00" + +# filtered_data = [ +# { +# 'reservation_id': item.get('id'), +# 'checkin': timezone.make_aware( +# datetime.strptime( +# f"{item.get('begin_date')} {check_in_time}", +# "%Y-%m-%d %H:%M:%S" +# ) +# ), +# 'checkout': timezone.make_aware( +# datetime.strptime( +# f"{item.get('end_date')} {check_out_time}", +# "%Y-%m-%d %H:%M:%S" +# ) +# ), +# 'room_number': item.get('apartment_id'), +# 'room_type': item.get('notes', 'Описание отсутствует'), +# 'status': item.get('status') +# } +# for item in bookings +# if isinstance(item, dict) and item.get("status") in ["booked", "request"] +# ] + +# await self._save_to_db(filtered_data) + +# async def _save_to_db(self, data): +# """ +# Сохраняет данные в БД (например, информацию о номере). +# """ +# if not isinstance(data, list): +# self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}") +# return + +# for index, item in enumerate(data, start=1): +# try: +# hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) +# reservation_id = item.get('reservation_id') +# if not reservation_id: +# self.logger.error(f"Пропущена запись {index}: отсутствует 'id'") +# continue + +# existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id) +# existing_reservation = await sync_to_async(existing_reservation.first)() + +# defaults = { +# 'room_number': item['room_number'], +# 'room_type': item['room_type'], +# 'check_in': item['checkin'], +# 'check_out': item['checkout'], +# 'status': item['status'], +# 'hotel': hotel +# } + +# if existing_reservation: +# await sync_to_async(Reservation.objects.update_or_create)( +# reservation_id=reservation_id, defaults=defaults +# ) +# self.logger.debug(f"Резервация {reservation_id} обновлена.") +# else: +# await sync_to_async(Reservation.objects.create)( +# reservation_id=reservation_id, **defaults +# ) +# self.logger.debug(f"Создана новая резервация {reservation_id}") + +# except Exception as e: +# self.logger.error(f"Ошибка при обработке записи {index}: {e}") + +# def validate_plugin(self): +# """ +# Проверка на соответствие требованиям. +# Можно проверить наличие методов или полей. +# """ +# # Проверяем наличие обязательных методов +# required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"] +# for m in required_methods: +# if not hasattr(self, m): +# raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.") +# self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.") +# return True + + import requests import hashlib import json @@ -8,6 +199,7 @@ from touchh.utils.log import CustomLogger from hotels.models import Hotel, Reservation from app_settings.models import GlobalHotelSettings from django.utils import timezone + class RealtyCalendarPlugin(BasePMSPlugin): def __init__(self, config): super().__init__(config) @@ -26,46 +218,123 @@ class RealtyCalendarPlugin(BasePMSPlugin): "date_format": "%Y-%m-%dT%H:%M:%S", "timezone": "UTC" } - def _get_sorted_keys(self, obj): - """ - Возвращает отсортированный по имени список ключей. - """ sorted_keys = sorted(obj.keys()) self.logger.debug(f"Отсортированные ключи: {sorted_keys}") return sorted_keys def _generate_data_string(self, obj): - """ - Формирует строку параметров для подписи. - """ sorted_keys = self._get_sorted_keys(obj) string = "".join(f"{key}={obj[key]}" for key in sorted_keys) self.logger.debug(f"Сформированная строка данных: {string}") return string + self.private_key def _generate_md5(self, string): - """ - Генерирует MD5-хеш от строки. - """ md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest() self.logger.debug(f"Сформированный MD5-хеш: {md5_hash}") return md5_hash def _generate_sign(self, data): - """ - Генерирует подпись для данных запроса. - """ data_string = self._generate_data_string(data) self.logger.debug(f"Строка для подписи: {data_string}") sign = self._generate_md5(data_string) self.logger.debug(f"Подпись: {sign}") return sign + # async def _fetch_data(self): + # self.logger.debug("Начало выполнения функции _fetch_data") + # base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/" + # headers = { + # "Accept": "application/json", + # "Content-Type": "application/json", + # } + + # now = datetime.now() + # data = { + # "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"), + # "end_date": now.strftime("%Y-%m-%d"), + # } + # data["sign"] = self._generate_sign(data) + + # try: + # response = requests.post(url=base_url, headers=headers, json=data) + # self.logger.debug(f"Статус ответа: {response.status_code}") + + # if response.status_code != 200: + # self.logger.error(f"Ошибка API: {response.status_code}, {response.text}") + # raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}") + + # try: + # response_data = response.json() + # bookings = response_data.get("bookings", []) + # # self.logger.debug(f"Полученные данные: {bookings}") + + # if not isinstance(bookings, list): + # self.logger.error(f"Ожидался список, но получен: {type(bookings)}") + # raise ValueError("Некорректный формат данных для bookings") + # except json.JSONDecodeError as e: + # self.logger.error(f"Ошибка декодирования JSON: {e}") + # raise ValueError("Ответ не является корректным JSON.") + # except Exception as e: + # self.logger.error(f"Ошибка обработки ответа API: {e}") + # raise + + # except Exception as e: + # self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}") + # raise + + # try: + # hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) + # hotel_tz = hotel.timezone + # self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}") + + # hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)() + # check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00" + # check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00" + # except Exception as e: + # self.logger.error(f"Ошибка получения настроек отеля: {e}") + # check_in_time, check_out_time = "14:00:00", "12:00:00" + + # filtered_data = [] + # for item in bookings: + # try: + # if not isinstance(item, dict): + # self.logger.error(f"Некорректный формат элемента: {item}") + # continue + + # reservation_id = item.get('id') + # if not reservation_id: + # self.logger.error(f"ID резервации отсутствует: {item}") + # continue + + # begin_date = item.get('begin_date') + # end_date = item.get('end_date') + # if not begin_date or not end_date: + # self.logger.error(f"Отсутствуют даты в записи: {item}") + # continue + + # checkin = timezone.make_aware( + # datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S") + # ) + # checkout = timezone.make_aware( + # datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S") + # ) + + # filtered_data.append({ + # 'reservation_id': reservation_id, + # 'checkin': checkin, + # 'checkout': checkout, + # 'room_number': item.get('apartment_id'), + # 'room_type': item.get('notes', 'Описание отсутствует'), + # 'status': item.get('status') + # }) + # except Exception as e: + # self.logger.error(f"Ошибка обработки элемента: {e}") + + # # self.logger.debug(f"Отфильтрованные данные: {filtered_data}") + # await self._save_to_db(filtered_data) + async def _fetch_data(self): - """ - Выполняет запрос к API RealtyCalendar для получения данных о бронированиях. - """ self.logger.debug("Начало выполнения функции _fetch_data") base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/" headers = { @@ -80,63 +349,114 @@ class RealtyCalendarPlugin(BasePMSPlugin): } data["sign"] = self._generate_sign(data) - response = requests.post(url=base_url, headers=headers, json=data) - self.logger.debug(f"Статус ответа: {response.status_code}") - - if response.status_code != 200: - self.logger.error(f"Ошибка API: {response.status_code}, {response.text}") - raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}") - try: + response = requests.post(url=base_url, headers=headers, json=data) + self.logger.debug(f"Статус ответа: {response.status_code}") + + if response.status_code != 200: + self.logger.error(f"Ошибка API: {response.status_code}, {response.text}") + return { + "processed_intervals": 0, + "processed_items": 0, + "errors": [f"Ошибка API RealtyCalendar: {response.status_code}"] + } + response_data = response.json() bookings = response_data.get("bookings", []) + if not isinstance(bookings, list): - raise ValueError(f"Ожидался список, но получен {type(bookings)}") - except Exception as e: - self.logger.error(f"Ошибка обработки ответа API: {e}") - raise + self.logger.error(f"Ожидался список, но получен: {type(bookings)}") + return { + "processed_intervals": 0, + "processed_items": 0, + "errors": ["Некорректный формат данных для bookings"] + } - # Получаем глобальные настройки отеля - hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) - hotel_tz = hotel.timezone - try: - hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)() - check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") - check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") - except AttributeError: - # Используем значения по умолчанию, если настроек нет - check_in_time = "14:00:00" - check_out_time = "12:00:00" - - filtered_data = [ - { - 'reservation_id': item.get('id'), - 'checkin': timezone.make_aware( - datetime.strptime( - f"{item.get('begin_date')} {check_in_time}", - "%Y-%m-%d %H:%M:%S" - ) - ), - 'checkout': timezone.make_aware( - datetime.strptime( - f"{item.get('end_date')} {check_out_time}", - "%Y-%m-%d %H:%M:%S" - ) - ), - 'room_number': item.get('apartment_id'), - 'room_type': item.get('notes', 'Описание отсутствует'), - 'status': item.get('status') + except json.JSONDecodeError as e: + self.logger.error(f"Ошибка декодирования JSON: {e}") + return { + "processed_intervals": 0, + "processed_items": 0, + "errors": ["Ответ не является корректным JSON."] + } + except Exception as e: + self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}") + return { + "processed_intervals": 0, + "processed_items": 0, + "errors": [str(e)] } - for item in bookings - if isinstance(item, dict) and item.get("status") in ["booked", "request"] - ] - await self._save_to_db(filtered_data) + # Получение настроек отеля + try: + hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) + hotel_tz = hotel.timezone + self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}") + + hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)() + check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00" + check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00" + except Exception as e: + self.logger.error(f"Ошибка получения настроек отеля: {e}") + check_in_time, check_out_time = "14:00:00", "12:00:00" + + # Обработка записей + processed_items = 0 + errors = [] + filtered_data = [] + + for item in bookings: + try: + if not isinstance(item, dict): + raise ValueError(f"Некорректный формат элемента: {item}") + + reservation_id = item.get('id') + if not reservation_id: + raise ValueError(f"ID резервации отсутствует: {item}") + + begin_date = item.get('begin_date') + end_date = item.get('end_date') + if not begin_date or not end_date: + raise ValueError(f"Отсутствуют даты в записи: {item}") + + checkin = timezone.make_aware( + datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S") + ) + checkout = timezone.make_aware( + datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S") + ) + + filtered_data.append({ + 'reservation_id': reservation_id, + 'checkin': checkin, + 'checkout': checkout, + 'room_number': item.get('apartment_id'), + 'room_type': item.get('notes', 'Описание отсутствует'), + 'status': item.get('status') + }) + processed_items += 1 + except Exception as e: + self.logger.error(f"Ошибка обработки элемента: {e}") + errors.append(str(e)) + + # Сохранение в БД + try: + await self._save_to_db(filtered_data) + except Exception as e: + self.logger.error(f"Ошибка сохранения данных в БД: {e}") + errors.append(f"Ошибка сохранения данных в БД: {str(e)}") + + # Формирование отчета + report = { + "processed_intervals": 1, # Пример значения + "processed_items": processed_items, + "errors": errors + } + self.logger.debug(f"Сформированный отчет: {report}") + return report + async def _save_to_db(self, data): - """ - Сохраняет данные в БД (например, информацию о номере). - """ if not isinstance(data, list): self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}") return @@ -165,7 +485,7 @@ class RealtyCalendarPlugin(BasePMSPlugin): await sync_to_async(Reservation.objects.update_or_create)( reservation_id=reservation_id, defaults=defaults ) - self.logger.debug(f"Резервация {reservation_id} обновлена.") + self.logger.debug(f"Резервация {reservation_id} обновлена. ") else: await sync_to_async(Reservation.objects.create)( reservation_id=reservation_id, **defaults @@ -176,11 +496,6 @@ class RealtyCalendarPlugin(BasePMSPlugin): self.logger.error(f"Ошибка при обработке записи {index}: {e}") def validate_plugin(self): - """ - Проверка на соответствие требованиям. - Можно проверить наличие методов или полей. - """ - # Проверяем наличие обязательных методов required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"] for m in required_methods: if not hasattr(self, m):