diff --git a/antifroud/data_sync.py b/antifroud/data_sync.py index 733b0b09..75806207 100644 --- a/antifroud/data_sync.py +++ b/antifroud/data_sync.py @@ -26,7 +26,7 @@ class DataSyncManager: # Настройка логирования self.logger = logging.getLogger(__name__) - self.logger.setLevel(logging.DEBUG) + self.logger.setLevel(logging.WARNING) handler = logging.FileHandler('data_sync.log') handler.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') diff --git a/bot/fonts/DejaVuSans-Bold.cw127.pkl b/bot/fonts/DejaVuSans-Bold.cw127.pkl new file mode 100644 index 00000000..d2101450 Binary files /dev/null and b/bot/fonts/DejaVuSans-Bold.cw127.pkl differ diff --git a/bot/fonts/DejaVuSans-Bold.pkl b/bot/fonts/DejaVuSans-Bold.pkl new file mode 100644 index 00000000..41eadf2a Binary files /dev/null and b/bot/fonts/DejaVuSans-Bold.pkl differ diff --git a/bot/fonts/DejaVuSans-Bold.ttf b/bot/fonts/DejaVuSans-Bold.ttf new file mode 100644 index 00000000..6d65fa7d Binary files /dev/null and b/bot/fonts/DejaVuSans-Bold.ttf differ diff --git a/bot/fonts/DejaVuSans-BoldOblique.ttf b/bot/fonts/DejaVuSans-BoldOblique.ttf new file mode 100644 index 00000000..753f2d80 Binary files /dev/null and b/bot/fonts/DejaVuSans-BoldOblique.ttf differ diff --git a/bot/fonts/DejaVuSans-ExtraLight.ttf b/bot/fonts/DejaVuSans-ExtraLight.ttf new file mode 100644 index 00000000..b09f32d7 Binary files /dev/null and b/bot/fonts/DejaVuSans-ExtraLight.ttf differ diff --git a/bot/fonts/DejaVuSans-Oblique.ttf b/bot/fonts/DejaVuSans-Oblique.ttf new file mode 100644 index 00000000..999bac77 Binary files /dev/null and b/bot/fonts/DejaVuSans-Oblique.ttf differ diff --git a/bot/fonts/DejaVuSans.cw127.pkl b/bot/fonts/DejaVuSans.cw127.pkl new file mode 100644 index 00000000..143d8bed Binary files /dev/null and b/bot/fonts/DejaVuSans.cw127.pkl differ diff --git a/bot/fonts/DejaVuSans.pkl b/bot/fonts/DejaVuSans.pkl new file mode 100644 index 00000000..c8d9edeb Binary files /dev/null and b/bot/fonts/DejaVuSans.pkl differ diff --git a/bot/fonts/DejaVuSans.ttf b/bot/fonts/DejaVuSans.ttf new file mode 100644 index 00000000..e5f7eecc Binary files /dev/null and b/bot/fonts/DejaVuSans.ttf differ diff --git a/bot/fonts/DejaVuSansCondensed-Bold.ttf b/bot/fonts/DejaVuSansCondensed-Bold.ttf new file mode 100644 index 00000000..22987c62 Binary files /dev/null and b/bot/fonts/DejaVuSansCondensed-Bold.ttf differ diff --git a/bot/fonts/DejaVuSansCondensed-BoldOblique.ttf b/bot/fonts/DejaVuSansCondensed-BoldOblique.ttf new file mode 100644 index 00000000..f5fa0ca2 Binary files /dev/null and b/bot/fonts/DejaVuSansCondensed-BoldOblique.ttf differ diff --git a/bot/fonts/DejaVuSansCondensed-Oblique.ttf b/bot/fonts/DejaVuSansCondensed-Oblique.ttf new file mode 100644 index 00000000..7fde9078 Binary files /dev/null and b/bot/fonts/DejaVuSansCondensed-Oblique.ttf differ diff --git a/bot/fonts/DejaVuSansCondensed.ttf b/bot/fonts/DejaVuSansCondensed.ttf new file mode 100644 index 00000000..3259bc21 Binary files /dev/null and b/bot/fonts/DejaVuSansCondensed.ttf differ diff --git a/bot/operations/statistics.py b/bot/operations/statistics.py index cf421c10..b83afcf0 100644 --- a/bot/operations/statistics.py +++ b/bot/operations/statistics.py @@ -7,7 +7,11 @@ from users.models import User from bot.utils.pdf_report import generate_pdf_report from bot.utils.database import get_hotels_for_user, get_hotel_by_name -from django.utils.timezone import make_aware +from datetime import datetime +from django.utils.timezone import make_aware, is_aware, is_naive +import os +import traceback + async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): """Вывод списка отелей для статистики.""" @@ -54,6 +58,29 @@ async def stats_select_period(update: Update, context: ContextTypes.DEFAULT_TYPE ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text("Выберите период времени:", reply_markup=reply_markup) + + +def ensure_datetime(value): + # print(f"statistics.py [DEBUG] ensure_datetime: Received value: {value} ({type(value)})") + """ + Ensure that the given value is a timezone-aware datetime object. + + If the given value is a string, it is assumed to be in the format + '%Y-%m-%d %H:%M:%S'. If the given value is a naive datetime object, + it is converted to a timezone-aware datetime object using + django.utils.timezone.make_aware. + + :param value: The value to be converted + :type value: str or datetime + :return: A timezone-aware datetime object + :rtype: datetime + """ + if isinstance(value, str): + value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') + if isinstance(value, datetime) and is_naive(value): + value = make_aware(value) + # print(f"statistics.py [DEBUG] ensure_datetime: Returning value: {value} ({type(value)})") + return value async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -61,17 +88,79 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE query = update.callback_query await query.answer() - hotel_id = context.user_data.get("selected_hotel") - if not hotel_id: - await query.edit_message_text("Ошибка: ID отеля не найден.") - return + try: + hotel_id = context.user_data.get("selected_hotel") - period = query.data.split("_")[2] - print(f'Period raw: {query.data}') - print(f'Selected period: {period}') + if not hotel_id: + raise ValueError(f"ID отеля не найден в user_data: {context.user_data}") - now = datetime.utcnow().replace(tzinfo=timezone.utc) # Используем timezone.utc + period = query.data.split("_")[2] + + now = datetime.utcnow().replace(tzinfo=timezone.utc) + + print(type(now)) + + print(type(period)) + + start_date, end_date = get_period_dates(period, now) + + + try: + # Получаем бронирования + reservations = await sync_to_async(list)( + Reservation.objects.filter( + hotel_id=hotel_id, + check_in__gte=start_date, + check_in__lte=end_date + ).select_related('hotel') + ) + + + except Exception as e: + raise RuntimeError(f"statistics.py Ошибка при выборке бронирований: {e}") from e + + if not reservations: + await query.edit_message_text("statistics.py Нет данных для статистики за выбранный период.") + return + + try: + # Получаем данные об отеле + hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) + + except Hotel.DoesNotExist: + raise RuntimeError(f"statistics.py Отель с ID {hotel_id} не найден") + except Exception as e: + raise RuntimeError(f"statistics.py Ошибка при выборке отеля: {e}") from e + + try: + # Генерация отчета + file_path = await generate_pdf_report(hotel.name, reservations, start_date, end_date) + + except Exception as e: + raise RuntimeError(f"statistics.py [ERROR] Ошибка при генерации PDF-отчета: {e}") from e + + try: + # Отправка файла через Telegram + with open(file_path, "rb") as file: + await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf") + + except Exception as e: + raise RuntimeError(f"Ошибка при отправке PDF-файла: {e}") from e + + # Удаляем временный файл + if os.path.exists(file_path): + os.remove(file_path) + + + except Exception as e: + # Логируем стек вызовов для детального анализа + error_trace = traceback.format_exc() + + await query.edit_message_text(f"Произошла ошибка: {str(e)}") + + +def get_period_dates(period, 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) @@ -81,55 +170,13 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE 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) - else: # "all" - start_date = None - end_date = None - - print(f'Raw start_date: {start_date}, Raw end_date: {end_date}') - - # Убедитесь, что даты имеют временную зону UTC - if start_date: - start_date = make_aware(start_date) if start_date.tzinfo is None else start_date - if end_date: - end_date = make_aware(end_date) if end_date.tzinfo is None else end_date - - print(f'Filtered start_date: {start_date}, Filtered end_date: {end_date}') - - # Фильтрация по "дата заезда" - if start_date and end_date: - reservations = await sync_to_async(list)( - Reservation.objects.filter( - hotel_id=hotel_id, - check_in__gte=start_date, - check_in__lte=end_date - ).select_related('hotel') - ) - else: # Без фильтра по дате - reservations = await sync_to_async(list)( - Reservation.objects.filter( - hotel_id=hotel_id - ).select_related('hotel') - ) - - print(f'Filtered reservations count: {len(reservations)}') - - if not reservations: - await query.edit_message_text("Нет данных для статистики за выбранный период.") - return - - hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) - print(f'Hotel: {hotel.name}') - - for reservation in reservations: - print(f"Reservation ID: {reservation.reservation_id}, Hotel: {reservation.hotel.name}, " - f"Room number: {reservation.room_number}, Check-in: {reservation.check_in}, Check-out: {reservation.check_out}") - - # Генерация PDF отчета (пример) - file_path = generate_pdf_report(hotel.name, reservations, start_date, end_date) - print(f'Generated file path: {file_path}') - - with open(file_path, "rb") as file: - await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf") + 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) + 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 async def stats_back(update: Update, context): """Возврат к выбору отеля.""" diff --git a/bot/utils/pdf_report.py b/bot/utils/pdf_report.py index 0934d8d1..8a4497b7 100644 --- a/bot/utils/pdf_report.py +++ b/bot/utils/pdf_report.py @@ -1,65 +1,144 @@ from fpdf import FPDF + +import os +from datetime import datetime +from asgiref.sync import sync_to_async +from django.utils.timezone import make_aware, is_naive, is_aware import os +# Определение абсолютного пути к папке "reports" +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +REPORTS_DIR = os.path.join(BASE_DIR, "reports") -def generate_pdf_report(hotel_name, reservations, start_date, end_date): - """Генерация PDF отчета.""" - pdf = FPDF(orientation='L', unit="mm", format="A4") - pdf.add_page() +# Убедитесь, что директория существует +os.makedirs(REPORTS_DIR, exist_ok=True) - # Укажите путь к шрифту - font_path = os.path.join("bot", "fonts", "OpenSans-Regular.ttf") - if not os.path.exists(font_path): - raise FileNotFoundError(f"Шрифт {font_path} не найден. Убедитесь, что он находится в указанной папке.") - pdf.add_font("OpenSans", "", font_path, uni=True) - pdf.set_font("OpenSans", size=12) - # Заголовки - title = f"Отчет по заселениям: {hotel_name}" - period = ( - f"Период: {start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" - if start_date and end_date - else "Период: Все время" - ) +# Асинхронная функция для извлечения данных о бронировании +def ensure_datetime(value): + """Преобразует строку или naive datetime в timezone-aware datetime.""" + if isinstance(value, str): + value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') + if isinstance(value, datetime) and is_naive(value): + value = make_aware(value) + return value - pdf.cell(0, 10, txt=title, ln=True, align="C") - pdf.cell(0, 10, txt=period, ln=True, align="C") - pdf.ln(10) +@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 - # Ширины колонок - page_width = pdf.w - 10 - col_widths = [page_width * 0.2, page_width * 0.2, page_width * 0.15, page_width * 0.25, page_width * 0.1, page_width * 0.1] - # Заголовки таблицы - pdf.set_font("OpenSans", size=10) - headers = ["Дата заезда", "Дата выезда", "Номер", "Тип комнаты", "Цена"] - for width, header in zip(col_widths, headers): - pdf.cell(width, 15, header, border=1, align="C") - pdf.ln() - total_price = 0 - total_discount = 0 +class CustomPDF(FPDF): + def __init__(self, hotel_name, start_date, end_date, *args, **kwargs): + super().__init__(*args, **kwargs) + 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.hotel_name = hotel_name + self.start_date = start_date + self.end_date = end_date + + def header(self): + """Добавление заголовка и заголовков таблицы на каждой странице.""" + # Заголовок отчёта + if self.page == 1: # Заголовок отчёта только на первой странице + 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, 10, f"за период {self.start_date.strftime('%Y-%m-%d %H:%M:%S')} - {self.end_date.strftime('%Y-%m-%d %H:%M:%S')}", ln=1, align="C") + self.ln(10) + + # Заголовки таблицы + self.set_font("DejaVuSans-Bold", size=8) + headers = ["Отель", "№ бронирования", "№ комнаты", "Тип комнаты", "Заезд", "Выезд", "Статус"] + col_widths = [30, 30, 30, 60, 35, 35, 30] + row_height = 10 + + for col_width, header in zip(col_widths, headers): + self.cell(col_width, row_height, header, border=1, align="C") + self.ln() + + 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): + # Создание экземпляра 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: - price = res.price or 0 - discount = res.discount or 0 - total_price += price - total_discount += discount + try: + res_data = await get_reservation_data(res) - pdf.cell(col_widths[0], 10, res.check_in.strftime('%d.%m.%Y %H:%M'), border=1) - pdf.cell(col_widths[1], 10, res.check_out.strftime('%d.%m.%Y %H:%M'), border=1) - pdf.cell(col_widths[2], 10, res.room_number, border=1) - pdf.cell(col_widths[3], 10, res.room_type, border=1) - pdf.cell(col_widths[4], 10, f"{price:.2f} р.", border=1, align="R") - pdf.ln() + 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 - pdf.ln(5) - pdf.set_font("OpenSans", size=18) - pdf.cell(0, 10, "Итоги:", ln=True) - pdf.cell(0, 10, f"Общая сумма цен: {total_price:.2f} руб.", ln=True) - file_path = os.path.join("reports", f"{hotel_name}_report.pdf") - os.makedirs(os.path.dirname(file_path), exist_ok=True) - pdf.output(file_path) - return file_path diff --git a/reports/Golden Hills 3_report.pdf b/reports/Golden Hills 3_report.pdf deleted file mode 100644 index a6b97ff4..00000000 Binary files a/reports/Golden Hills 3_report.pdf and /dev/null differ diff --git a/reports/Golden Hills 4*_report.pdf b/reports/Golden Hills 4*_report.pdf deleted file mode 100644 index 1c496ef5..00000000 Binary files a/reports/Golden Hills 4*_report.pdf and /dev/null differ diff --git a/reports/GoldenHills 4_report.pdf b/reports/GoldenHills 4_report.pdf deleted file mode 100644 index e057a303..00000000 Binary files a/reports/GoldenHills 4_report.pdf and /dev/null differ diff --git a/requierments.txt b/requierments.txt new file mode 100644 index 00000000..9c6393a2 --- /dev/null +++ b/requierments.txt @@ -0,0 +1,68 @@ +ace_tools==0.0 +aiohappyeyeballs==2.4.4 +aiohttp==3.11.10 +aiosignal==1.3.1 +anyio==4.6.2.post1 +APScheduler==3.11.0 +asgiref==3.8.1 +async-timeout==5.0.1 +attrs==24.2.0 +certifi==2024.8.30 +cffi==1.17.1 +chardet==5.2.0 +charset-normalizer==3.4.0 +cryptography==44.0.0 +defusedxml==0.7.1 +Django==5.1.4 +django-environ==0.11.2 +django-filter==24.3 +django-health-check==3.18.3 +django-jazzmin==3.0.1 +django-jet==1.0.8 +et_xmlfile==2.0.0 +exceptiongroup==1.2.2 +fonttools==4.55.3 +fpdf2==2.8.2 +frozenlist==1.5.0 +geoip2==4.8.1 +git-filter-repo==2.47.0 +h11==0.14.0 +httpcore==1.0.7 +httpx==0.28.0 +idna==3.10 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +maxminddb==2.6.2 +multidict==6.1.0 +mysqlclient==2.2.6 +numpy==2.1.3 +openpyxl==3.1.5 +pandas==2.2.3 +pathspec==0.12.1 +pillow==11.0.0 +propcache==0.2.1 +psycopg==3.2.3 +pycparser==2.22 +PyMySQL==1.1.1 +python-dateutil==2.9.0.post0 +python-decouple==3.8 +python-dotenv==1.0.1 +python-telegram-bot==21.8 +pytz==2024.2 +PyYAML==6.0.2 +referencing==0.35.1 +requests==2.32.3 +rpds-py==0.22.3 +six==1.17.0 +sniffio==1.3.1 +sqlparse==0.5.2 +typing_extensions==4.12.2 +tzdata==2024.2 +tzlocal==5.2 +ua-parser==1.0.0 +ua-parser-builtins==0.18.0.post1 +urllib3==2.2.3 +user-agents==2.2.0 +yarl==1.18.3 +mysqlclient +chardet \ No newline at end of file diff --git a/touchh/settings.py b/touchh/settings.py index 0d4d0594..28cbd5ad 100644 --- a/touchh/settings.py +++ b/touchh/settings.py @@ -31,11 +31,11 @@ SECRET_KEY = 'django-insecure-l_8uu8#p*^zf)9zry80)6u+!+2g1a4tg!wx7@^!uw(+^axyh&h # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', '192.168.219.114', '0533-182-226-158-253.ngrok-free.app'] +ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', '192.168.219.114', '5dec-182-226-158-253.ngrok-free.app', '*.ngrok-free.app'] CSRF_TRUSTED_ORIGINS = [ - 'https://0533-182-226-158-253.ngrok-free.app', - 'https://*.ngrok.io', # Это подойдет для любых URL, связанных с ngrok + 'http://5dec-182-226-158-253.ngrok-free.app', + 'https://*.ngrok-free.app', # Это подойдет для любых URL, связанных с ngrok ] # Application definition