From 7861ad21b101a4360fa1d5de1b1859ea707291b8 Mon Sep 17 00:00:00 2001 From: trevor Date: Wed, 25 Dec 2024 11:28:50 +0900 Subject: [PATCH] statistics and reports fixed --- bot/operations/statistics.py | 127 ++++++++++++++++------------- bot/utils/pdf_report.py | 154 ++++++----------------------------- 2 files changed, 94 insertions(+), 187 deletions(-) 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 - -