From 1110d89bb0e14f1ccbeed7866675a12f19109f85 Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 1 Mar 2026 15:32:25 +0300 Subject: [PATCH] update --- src/dutylog/application/__main__.py | 2 +- .../application/bot/admin_dialogs/__init__.py | 5 ++ .../admin_dialogs/admin_menu_dialog.py | 16 ++-- .../admin_dialogs/broadcast.py | 0 .../admin_dialogs/floors_management.py | 0 .../admin_dialogs/hours_management.py | 0 .../admin_dialogs/main_menu.py | 0 .../reporting_period_management.py | 27 +++--- .../admin_dialogs/residents_filter.py | 0 .../admin_dialogs/residents_management.py | 0 .../admin_dialogs/rooms_management.py | 0 .../user_dialogs/admin_dialogs/__init__.py | 5 -- .../database/dao/hours_transactions_dao.py | 21 ----- src/dutylog/infrastructure/ioc.py | 2 + src/dutylog/services/report_service.py | 52 +++++++++--- test_report_2026-03-01_2026-03-01.xlsx | Bin 0 -> 5881 bytes test_report_generation.py | 79 ++++++++++++++++++ 17 files changed, 150 insertions(+), 59 deletions(-) create mode 100644 src/dutylog/application/bot/admin_dialogs/__init__.py rename src/dutylog/application/bot/{user_dialogs => }/admin_dialogs/admin_menu_dialog.py (78%) rename src/dutylog/application/bot/{user_dialogs => }/admin_dialogs/broadcast.py (100%) rename src/dutylog/application/bot/{user_dialogs => }/admin_dialogs/floors_management.py (100%) rename src/dutylog/application/bot/{user_dialogs => }/admin_dialogs/hours_management.py (100%) rename src/dutylog/application/bot/{user_dialogs => }/admin_dialogs/main_menu.py (100%) rename src/dutylog/application/bot/{user_dialogs => }/admin_dialogs/reporting_period_management.py (98%) rename src/dutylog/application/bot/{user_dialogs => }/admin_dialogs/residents_filter.py (100%) rename src/dutylog/application/bot/{user_dialogs => }/admin_dialogs/residents_management.py (100%) rename src/dutylog/application/bot/{user_dialogs => }/admin_dialogs/rooms_management.py (100%) delete mode 100644 src/dutylog/application/bot/user_dialogs/admin_dialogs/__init__.py create mode 100644 test_report_2026-03-01_2026-03-01.xlsx create mode 100644 test_report_generation.py diff --git a/src/dutylog/application/__main__.py b/src/dutylog/application/__main__.py index dbbc312..606b0dd 100644 --- a/src/dutylog/application/__main__.py +++ b/src/dutylog/application/__main__.py @@ -10,7 +10,7 @@ from dishka.integrations.aiogram import setup_dishka from dutylog.application.bot.user_handlers import router as user_router from dutylog.application.bot.user_dialogs import main_menu_dialog -from dutylog.application.bot.user_dialogs.admin_dialogs import admin_menu_dialog +from dutylog.application.bot.admin_dialogs import admin_menu_dialog from dutylog.application.bot.user_dialogs.registration_dialog import registration_dialog from dutylog.infrastructure.ioc import ( ConfigProvider, diff --git a/src/dutylog/application/bot/admin_dialogs/__init__.py b/src/dutylog/application/bot/admin_dialogs/__init__.py new file mode 100644 index 0000000..7dc8fb0 --- /dev/null +++ b/src/dutylog/application/bot/admin_dialogs/__init__.py @@ -0,0 +1,5 @@ +from dutylog.application.bot.admin_dialogs.admin_menu_dialog import ( + admin_menu_dialog, +) + +__all__ = ["admin_menu_dialog"] diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py b/src/dutylog/application/bot/admin_dialogs/admin_menu_dialog.py similarity index 78% rename from src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py rename to src/dutylog/application/bot/admin_dialogs/admin_menu_dialog.py index e8d15a6..50f119d 100644 --- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py +++ b/src/dutylog/application/bot/admin_dialogs/admin_menu_dialog.py @@ -1,10 +1,10 @@ from aiogram_dialog import Dialog -from dutylog.application.bot.user_dialogs.admin_dialogs.main_menu import ( +from dutylog.application.bot.admin_dialogs.main_menu import ( main_menu_window, statistics_window, ) -from dutylog.application.bot.user_dialogs.admin_dialogs.residents_management import ( +from dutylog.application.bot.admin_dialogs.residents_management import ( residents_list_window, resident_info_window, resident_logout_confirm_window, @@ -16,18 +16,18 @@ from dutylog.application.bot.user_dialogs.admin_dialogs.residents_management imp search_input_window, search_results_window, ) -from dutylog.application.bot.user_dialogs.admin_dialogs.residents_filter import ( +from dutylog.application.bot.admin_dialogs.residents_filter import ( filter_select_window, filter_hours_input_window, filtered_results_window, ) -from dutylog.application.bot.user_dialogs.admin_dialogs.floors_management import ( +from dutylog.application.bot.admin_dialogs.floors_management import ( floors_list_window, floor_delete_confirm_window, create_floor_input_window, create_floor_confirm_window, ) -from dutylog.application.bot.user_dialogs.admin_dialogs.rooms_management import ( +from dutylog.application.bot.admin_dialogs.rooms_management import ( rooms_select_floor_window, rooms_list_window, room_delete_confirm_window, @@ -35,13 +35,13 @@ from dutylog.application.bot.user_dialogs.admin_dialogs.rooms_management import create_room_input_window, create_room_confirm_window, ) -from dutylog.application.bot.user_dialogs.admin_dialogs.reporting_period_management import ( +from dutylog.application.bot.admin_dialogs.reporting_period_management import ( reporting_period_window, next_period_confirm_window, generate_report_select_period_window, generate_report_confirm_window, ) -from dutylog.application.bot.user_dialogs.admin_dialogs.hours_management import ( +from dutylog.application.bot.admin_dialogs.hours_management import ( add_hours_select_window, remove_hours_select_window, add_hours_custom_window, @@ -51,7 +51,7 @@ from dutylog.application.bot.user_dialogs.admin_dialogs.hours_management import add_hours_confirm_window, remove_hours_confirm_window, ) -from dutylog.application.bot.user_dialogs.admin_dialogs.broadcast import ( +from dutylog.application.bot.admin_dialogs.broadcast import ( broadcast_window, broadcast_confirm_window, ) diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/broadcast.py b/src/dutylog/application/bot/admin_dialogs/broadcast.py similarity index 100% rename from src/dutylog/application/bot/user_dialogs/admin_dialogs/broadcast.py rename to src/dutylog/application/bot/admin_dialogs/broadcast.py diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/floors_management.py b/src/dutylog/application/bot/admin_dialogs/floors_management.py similarity index 100% rename from src/dutylog/application/bot/user_dialogs/admin_dialogs/floors_management.py rename to src/dutylog/application/bot/admin_dialogs/floors_management.py diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/hours_management.py b/src/dutylog/application/bot/admin_dialogs/hours_management.py similarity index 100% rename from src/dutylog/application/bot/user_dialogs/admin_dialogs/hours_management.py rename to src/dutylog/application/bot/admin_dialogs/hours_management.py diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/main_menu.py b/src/dutylog/application/bot/admin_dialogs/main_menu.py similarity index 100% rename from src/dutylog/application/bot/user_dialogs/admin_dialogs/main_menu.py rename to src/dutylog/application/bot/admin_dialogs/main_menu.py diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/reporting_period_management.py b/src/dutylog/application/bot/admin_dialogs/reporting_period_management.py similarity index 98% rename from src/dutylog/application/bot/user_dialogs/admin_dialogs/reporting_period_management.py rename to src/dutylog/application/bot/admin_dialogs/reporting_period_management.py index f943275..e287736 100644 --- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/reporting_period_management.py +++ b/src/dutylog/application/bot/admin_dialogs/reporting_period_management.py @@ -35,7 +35,7 @@ async def on_reporting_period_click( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, -): +) -> None: await dialog_manager.switch_to(AdminMenuSG.reporting_period) @@ -43,7 +43,7 @@ async def on_reporting_period_click( async def get_reporting_period_data( reporting_periods_repository: FromDishka[ReportingPeriodsRepository], **kwargs, -): +) -> dict[str, bool | str]: active_period = await reporting_periods_repository.get_active_period() if active_period: @@ -90,7 +90,7 @@ async def on_next_period_click( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, -): +) -> None: await dialog_manager.switch_to(AdminMenuSG.next_period_confirm) @@ -98,7 +98,7 @@ async def on_make_report_click( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, -): +) -> None: await dialog_manager.switch_to(AdminMenuSG.generate_report_select_period) @@ -106,7 +106,7 @@ async def on_make_report_click( async def get_next_period_confirm_data( reporting_periods_repository: FromDishka[ReportingPeriodsRepository], **kwargs, -): +) -> dict[str, str]: active_period = await reporting_periods_repository.get_active_period() if active_period: @@ -144,7 +144,7 @@ async def on_next_period_confirm( button: Button, dialog_manager: DialogManager, reporting_periods_repository: FromDishka[ReportingPeriodsRepository], -): +) -> None: active_period = await reporting_periods_repository.get_active_period() current_date = datetime.now().date() @@ -161,7 +161,7 @@ async def on_next_period_cancel( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, -): +) -> None: await dialog_manager.switch_to(AdminMenuSG.reporting_period) @@ -209,7 +209,7 @@ next_period_confirm_window = Window( async def get_generate_report_data( reporting_periods_repository: FromDishka[ReportingPeriodsRepository], **kwargs, -): +) -> dict[str, bool | str | list[tuple[str, int]]]: all_periods = await reporting_periods_repository.get_all() completed_periods = [p for p in all_periods if p.end_date is not None] active_period = await reporting_periods_repository.get_active_period() @@ -268,7 +268,7 @@ async def on_period_selected( widget, dialog_manager: DialogManager, item_id: str, -): +) -> None: dialog_manager.dialog_data["selected_period_id"] = int(item_id) await dialog_manager.switch_to(AdminMenuSG.generate_report_confirm) @@ -278,7 +278,7 @@ async def get_generate_report_confirm_data( reporting_periods_repository: FromDishka[ReportingPeriodsRepository], dialog_manager: DialogManager, **kwargs, -): +) -> dict[str, str]: period_id = dialog_manager.dialog_data.get("selected_period_id") if not period_id: return {"content": "⚠️ Период не выбран"} @@ -321,7 +321,7 @@ async def on_generate_report_confirm( dialog_manager: DialogManager, reporting_periods_repository: FromDishka[ReportingPeriodsRepository], report_service: FromDishka[ReportService], -): +) -> None: period_id = dialog_manager.dialog_data.get("selected_period_id") if not period_id: await callback.answer("⚠️ Период не выбран", show_alert=True) @@ -335,8 +335,7 @@ async def on_generate_report_confirm( await callback.answer("⏳ Генерирую отчёт...") - end_date = period.end_date if period.end_date else msk_now() - print(end_date) + end_date = period.end_date if period.end_date else msk_now().date() report_file = await report_service.generate_period_report( period.start_date, end_date @@ -362,7 +361,7 @@ async def on_generate_report_cancel( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, -): +) -> None: await dialog_manager.switch_to(AdminMenuSG.generate_report_select_period) diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_filter.py b/src/dutylog/application/bot/admin_dialogs/residents_filter.py similarity index 100% rename from src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_filter.py rename to src/dutylog/application/bot/admin_dialogs/residents_filter.py diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_management.py b/src/dutylog/application/bot/admin_dialogs/residents_management.py similarity index 100% rename from src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_management.py rename to src/dutylog/application/bot/admin_dialogs/residents_management.py diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/rooms_management.py b/src/dutylog/application/bot/admin_dialogs/rooms_management.py similarity index 100% rename from src/dutylog/application/bot/user_dialogs/admin_dialogs/rooms_management.py rename to src/dutylog/application/bot/admin_dialogs/rooms_management.py diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/__init__.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/__init__.py deleted file mode 100644 index 73f79e8..0000000 --- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from dutylog.application.bot.user_dialogs.admin_dialogs.admin_menu_dialog import ( - admin_menu_dialog, -) - -__all__ = ["admin_menu_dialog"] diff --git a/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py b/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py index 2a88e22..e95fec1 100644 --- a/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py +++ b/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py @@ -39,27 +39,6 @@ class HoursTransactionsDAO: delete(HoursTransaction).where(HoursTransaction.id == transaction_id) ) await self.session.commit() - async def delete(self, transaction_id: int) -> None: - await self.session.execute( - delete(HoursTransaction).where(HoursTransaction.id == transaction_id) - ) - await self.session.commit() - - async def get_by_period(self, start_date, end_date) -> list[HoursTransaction]: - """Получает транзакции за период""" - from datetime import datetime, time - - start_datetime = datetime.combine(start_date, time.min) - end_datetime = datetime.combine(end_date, time.max) - - result = await self.session.execute( - select(HoursTransaction) - .where(HoursTransaction.created_at >= start_datetime) - .where(HoursTransaction.created_at <= end_datetime) - .order_by(HoursTransaction.created_at.asc()) - ) - return list(result.scalars().all()) - async def get_by_period(self, start_date, end_date) -> list[HoursTransaction]: from datetime import datetime, time diff --git a/src/dutylog/infrastructure/ioc.py b/src/dutylog/infrastructure/ioc.py index c9129d0..902d888 100644 --- a/src/dutylog/infrastructure/ioc.py +++ b/src/dutylog/infrastructure/ioc.py @@ -129,10 +129,12 @@ class ServiceProvider(Provider): residents_repository: ResidentsRepository, rooms_repository: RoomsRepository, floors_repository: FloorsRepository, + users_repository: UsersRepository, ) -> ReportService: return ReportService( hours_transactions_repository, residents_repository, rooms_repository, floors_repository, + users_repository, ) diff --git a/src/dutylog/services/report_service.py b/src/dutylog/services/report_service.py index bee67fe..1440939 100644 --- a/src/dutylog/services/report_service.py +++ b/src/dutylog/services/report_service.py @@ -1,4 +1,4 @@ -from datetime import date +from datetime import date, timedelta, timezone from io import BytesIO from openpyxl import Workbook @@ -16,6 +16,11 @@ from dutylog.infrastructure.database.repositories.residents_repository import ( from dutylog.infrastructure.database.repositories.rooms_repository import ( RoomsRepository, ) +from dutylog.infrastructure.database.repositories.users_repository import ( + UsersRepository, +) + +MSK_TZ = timezone(timedelta(hours=3)) class ReportService: @@ -25,11 +30,13 @@ class ReportService: residents_repository: ResidentsRepository, rooms_repository: RoomsRepository, floors_repository: FloorsRepository, + users_repository: UsersRepository, ): self.hours_transactions_repository = hours_transactions_repository self.residents_repository = residents_repository self.rooms_repository = rooms_repository self.floors_repository = floors_repository + self.users_repository = users_repository async def generate_period_report(self, start_date: date, end_date: date) -> BytesIO: transactions = await self.hours_transactions_repository.get_by_period( @@ -69,7 +76,7 @@ class ReportService: title_text = f"Отчет по начислениям часов за период {start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" ws.cell(row=1, column=1, value=title_text) - ws.merge_cells("A1:E1") + ws.merge_cells("A1:F1") title_cell = ws.cell(row=1, column=1) title_cell.font = Font(bold=True, size=14) title_cell.alignment = Alignment(horizontal="center", vertical="center") @@ -79,10 +86,10 @@ class ReportService: ws.cell(row=row_num, column=1, value="НАЧИСЛЕНИЯ") ws.cell(row=row_num, column=1).font = Font(bold=True, size=12) - ws.merge_cells(f"A{row_num}:E{row_num}") + ws.merge_cells(f"A{row_num}:F{row_num}") row_num += 1 - headers = ["Дата", "Резидент", "Комната", "Часы", "Примечание"] + headers = ["Дата", "Резидент", "Комната", "Часы", "Админ", "Примечание"] for col_num, header in enumerate(headers, 1): cell = ws.cell(row=row_num, column=col_num) cell.value = header @@ -105,9 +112,17 @@ class ReportService: room = await self.rooms_repository.get_by_id(resident.room) room_number = room.number if room else "—" + + admin_username = "—" + if transaction.admin_id: + admin = await self.users_repository.get_user_by_id(transaction.admin_id) + if admin and admin.username: + admin_username = f"@{admin.username}" + + msk_time = transaction.created_at.astimezone(MSK_TZ).replace(tzinfo=None) ws.cell( - row=row_num, column=1, value=transaction.created_at.replace(tzinfo=None) + row=row_num, column=1, value=msk_time ).border = border ws.cell(row=row_num, column=1).number_format = "DD.MM.YYYY HH:MM" ws.cell( @@ -115,8 +130,9 @@ class ReportService: ).border = border ws.cell(row=row_num, column=3, value=room_number).border = border ws.cell(row=row_num, column=4, value=transaction.amount).border = border + ws.cell(row=row_num, column=5, value=admin_username).border = border ws.cell( - row=row_num, column=5, value=transaction.remark or "—" + row=row_num, column=6, value=transaction.remark or "—" ).border = border ws.cell(row=row_num, column=1).alignment = Alignment(horizontal="center") @@ -142,12 +158,15 @@ class ReportService: ws.cell(row=summary_row, column=5, value="").fill = summary_fill ws.cell(row=summary_row, column=5).border = border + + ws.cell(row=summary_row, column=6, value="").fill = summary_fill + ws.cell(row=summary_row, column=6).border = border row_num += 3 ws.cell(row=row_num, column=1, value="СПИСАНИЯ") ws.cell(row=row_num, column=1).font = Font(bold=True, size=12) - ws.merge_cells(f"A{row_num}:E{row_num}") + ws.merge_cells(f"A{row_num}:F{row_num}") row_num += 1 for col_num, header in enumerate(headers, 1): @@ -172,9 +191,17 @@ class ReportService: room = await self.rooms_repository.get_by_id(resident.room) room_number = room.number if room else "—" + + admin_username = "—" + if transaction.admin_id: + admin = await self.users_repository.get_user_by_id(transaction.admin_id) + if admin and admin.username: + admin_username = f"@{admin.username}" + + msk_time = transaction.created_at.astimezone(MSK_TZ).replace(tzinfo=None) ws.cell( - row=row_num, column=1, value=transaction.created_at.replace(tzinfo=None) + row=row_num, column=1, value=msk_time ).border = border ws.cell(row=row_num, column=1).number_format = "DD.MM.YYYY HH:MM" ws.cell( @@ -182,8 +209,9 @@ class ReportService: ).border = border ws.cell(row=row_num, column=3, value=room_number).border = border ws.cell(row=row_num, column=4, value=transaction.amount).border = border + ws.cell(row=row_num, column=5, value=admin_username).border = border ws.cell( - row=row_num, column=5, value=transaction.remark or "—" + row=row_num, column=6, value=transaction.remark or "—" ).border = border ws.cell(row=row_num, column=1).alignment = Alignment(horizontal="center") @@ -209,12 +237,16 @@ class ReportService: ws.cell(row=summary_row, column=5, value="").fill = summary_fill ws.cell(row=summary_row, column=5).border = border + + ws.cell(row=summary_row, column=6, value="").fill = summary_fill + ws.cell(row=summary_row, column=6).border = border ws.column_dimensions["A"].width = 18 ws.column_dimensions["B"].width = 25 ws.column_dimensions["C"].width = 10 ws.column_dimensions["D"].width = 10 - ws.column_dimensions["E"].width = 30 + ws.column_dimensions["E"].width = 15 + ws.column_dimensions["F"].width = 30 output = BytesIO() wb.save(output) diff --git a/test_report_2026-03-01_2026-03-01.xlsx b/test_report_2026-03-01_2026-03-01.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..47a9a97a7dd0e4e279045ccf4e57233514008e46 GIT binary patch literal 5881 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 S69B-yx}~qEzu?zG0{kDCcUbHI literal 0 HcmV?d00001 diff --git a/test_report_generation.py b/test_report_generation.py new file mode 100644 index 0000000..fbd57c8 --- /dev/null +++ b/test_report_generation.py @@ -0,0 +1,79 @@ +import asyncio +from datetime import date + +from dutylog.infrastructure.database.config import create_engine, create_session_maker +from dutylog.infrastructure.database.dao.hours_transactions_dao import HoursTransactionsDAO +from dutylog.infrastructure.database.dao.residents_dao import ResidentsDAO +from dutylog.infrastructure.database.dao.rooms_dao import RoomsDAO +from dutylog.infrastructure.database.dao.floors_dao import FloorsDAO +from dutylog.infrastructure.database.dao.reporting_periods_dao import ReportingPeriodsDAO +from dutylog.infrastructure.database.dao.users_dao import UsersDAO +from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( + HoursTransactionsRepository, +) +from dutylog.infrastructure.database.repositories.residents_repository import ResidentsRepository +from dutylog.infrastructure.database.repositories.rooms_repository import RoomsRepository +from dutylog.infrastructure.database.repositories.floors_repository import FloorsRepository +from dutylog.infrastructure.database.repositories.reporting_periods_repository import ( + ReportingPeriodsRepository, +) +from dutylog.infrastructure.database.repositories.users_repository import UsersRepository +from dutylog.infrastructure.utils.config import load_config +from dutylog.services.report_service import ReportService + + +async def test_report_generation() -> None: + config = load_config() + engine = create_engine(config.database.url) + session_maker = create_session_maker(engine) + + async with session_maker() as session: + hours_transactions_dao = HoursTransactionsDAO(session) + residents_dao = ResidentsDAO(session) + rooms_dao = RoomsDAO(session) + floors_dao = FloorsDAO(session) + reporting_periods_dao = ReportingPeriodsDAO(session) + users_dao = UsersDAO(session) + + hours_transactions_repository = HoursTransactionsRepository( + hours_transactions_dao, residents_dao + ) + residents_repository = ResidentsRepository(residents_dao) + rooms_repository = RoomsRepository(rooms_dao) + floors_repository = FloorsRepository(floors_dao) + reporting_periods_repository = ReportingPeriodsRepository(reporting_periods_dao) + users_repository = UsersRepository(users_dao) + + report_service = ReportService( + hours_transactions_repository, + residents_repository, + rooms_repository, + floors_repository, + users_repository, + ) + + active_period = await reporting_periods_repository.get_active_period() + + if not active_period: + print("⚠️ Нет активного периода") + return + + print(f"📅 Активный период: с {active_period.start_date}") + + end_date = active_period.end_date if active_period.end_date else date.today() + + print(f"📊 Генерирую отчёт за период {active_period.start_date} - {end_date}") + + report_file = await report_service.generate_period_report( + active_period.start_date, end_date + ) + + filename = f"test_report_{active_period.start_date}_{end_date}.xlsx" + with open(filename, "wb") as f: + f.write(report_file.read()) + + print(f"✅ Отчёт сохранён в файл: {filename}") + + +if __name__ == "__main__": + asyncio.run(test_report_generation())