mirror of
https://github.com/koloideal/DutyLog.git
synced 2026-06-10 02:15:30 +03:00
update
This commit is contained in:
@@ -11,6 +11,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.admin_dialogs import admin_menu_dialog
|
||||
from dutylog.application.bot.creator_dialogs import creator_menu_dialog
|
||||
from dutylog.application.bot.user_dialogs.registration_dialog import registration_dialog
|
||||
from dutylog.infrastructure.ioc import (
|
||||
ConfigProvider,
|
||||
@@ -46,6 +47,7 @@ async def main():
|
||||
dp.include_router(user_router)
|
||||
dp.include_router(main_menu_dialog)
|
||||
dp.include_router(admin_menu_dialog)
|
||||
dp.include_router(creator_menu_dialog)
|
||||
dp.include_router(registration_dialog)
|
||||
|
||||
setup_dialogs(dp)
|
||||
|
||||
@@ -6,7 +6,7 @@ from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dutylog.application.bot.user_dialogs.states import AdminMenuSG
|
||||
from dutylog.application.bot.user_dialogs.states import AdminMenuSG, CreatorMenuSG
|
||||
from dutylog.infrastructure.database.repositories.users_repository import (
|
||||
UsersRepository,
|
||||
)
|
||||
@@ -86,7 +86,7 @@ async def get_admin_menu_data(
|
||||
Выберите действие:
|
||||
"""
|
||||
|
||||
return {"content": content}
|
||||
return {"content": content, "is_creator": is_creator}
|
||||
|
||||
|
||||
@inject
|
||||
@@ -137,6 +137,10 @@ async def on_reporting_period_click(callback, button, dialog_manager):
|
||||
await dialog_manager.switch_to(AdminMenuSG.reporting_period)
|
||||
|
||||
|
||||
async def on_admins_click(callback, button, dialog_manager):
|
||||
await dialog_manager.start(CreatorMenuSG.admins_list)
|
||||
|
||||
|
||||
main_menu_window = Window(
|
||||
Format("{content}"),
|
||||
SwitchTo(
|
||||
@@ -161,6 +165,12 @@ main_menu_window = Window(
|
||||
id="reporting_period_btn",
|
||||
on_click=on_reporting_period_click,
|
||||
),
|
||||
Button(
|
||||
Const("👨💼 Админы"),
|
||||
id="admins_btn",
|
||||
on_click=on_admins_click,
|
||||
when="is_creator",
|
||||
),
|
||||
SwitchTo(
|
||||
Const("📊 Статистика"),
|
||||
id="stats_btn",
|
||||
|
||||
@@ -63,8 +63,6 @@ async def get_reporting_period_data(
|
||||
<b>Дата начала:</b> <code>{start_date.strftime('%d.%m.%Y')}</code>
|
||||
<b>Прошло дней:</b> <code>{days_passed}</code>
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Используйте кнопки ниже для управления периодом.
|
||||
"""
|
||||
has_active = True
|
||||
@@ -74,8 +72,6 @@ async def get_reporting_period_data(
|
||||
|
||||
<b>Статус:</b> ⚪️ Нет активного периода
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Создайте новый отчётный период, чтобы начать учёт дежурств.
|
||||
"""
|
||||
has_active = False
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from dutylog.application.bot.creator_dialogs.creator_menu_dialog import (
|
||||
creator_menu_dialog,
|
||||
)
|
||||
|
||||
__all__ = ["creator_menu_dialog"]
|
||||
@@ -0,0 +1,374 @@
|
||||
from aiogram.types import CallbackQuery
|
||||
from aiogram_dialog import Window, DialogManager
|
||||
from aiogram_dialog.widgets.text import Format, Const
|
||||
from aiogram_dialog.widgets.kbd import SwitchTo, Button, Select, ScrollingGroup, Row
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.aiogram_dialog import inject
|
||||
|
||||
from dutylog.application.bot.user_dialogs.states import CreatorMenuSG, AdminMenuSG
|
||||
from dutylog.infrastructure.database.repositories.users_repository import UsersRepository
|
||||
|
||||
|
||||
@inject
|
||||
async def get_admins_list_data(
|
||||
users_repository: FromDishka[UsersRepository],
|
||||
**kwargs,
|
||||
) -> dict[str, str | list[tuple[str, int]]]:
|
||||
all_users = await users_repository.get_all_users()
|
||||
admins = [u for u in all_users if u.is_admin]
|
||||
|
||||
if not admins:
|
||||
content = """
|
||||
<blockquote>👨💼 <b>Администраторы</b></blockquote>
|
||||
|
||||
⚠️ Нет администраторов в системе.
|
||||
"""
|
||||
admins_list = []
|
||||
else:
|
||||
content = f"""
|
||||
<blockquote>👨💼 <b>Администраторы</b></blockquote>
|
||||
|
||||
Всего администраторов: <code>{len(admins)}</code>
|
||||
|
||||
Выберите администратора для просмотра информации:
|
||||
"""
|
||||
admins_list = []
|
||||
for admin in admins:
|
||||
display_name = f"@{admin.username}" if admin.username else f"ID: {admin.id}"
|
||||
if admin.first_name:
|
||||
display_name = f"{admin.first_name} ({display_name})"
|
||||
admins_list.append((display_name, admin.id))
|
||||
|
||||
return {
|
||||
"content": content,
|
||||
"admins": admins_list,
|
||||
}
|
||||
|
||||
|
||||
async def on_admin_selected(
|
||||
callback: CallbackQuery,
|
||||
widget,
|
||||
dialog_manager: DialogManager,
|
||||
item_id: str,
|
||||
) -> None:
|
||||
dialog_manager.dialog_data["selected_admin_id"] = int(item_id)
|
||||
await dialog_manager.switch_to(CreatorMenuSG.admin_info)
|
||||
|
||||
|
||||
async def on_add_admin_click(
|
||||
callback: CallbackQuery,
|
||||
button: Button,
|
||||
dialog_manager: DialogManager,
|
||||
) -> None:
|
||||
await dialog_manager.switch_to(CreatorMenuSG.add_admin_select_user)
|
||||
|
||||
|
||||
@inject
|
||||
async def get_admin_info_data(
|
||||
users_repository: FromDishka[UsersRepository],
|
||||
dialog_manager: DialogManager,
|
||||
**kwargs,
|
||||
) -> dict[str, str]:
|
||||
admin_id = dialog_manager.dialog_data.get("selected_admin_id")
|
||||
if not admin_id:
|
||||
return {"content": "⚠️ Администратор не выбран"}
|
||||
|
||||
admin = await users_repository.get_user_by_id(int(admin_id))
|
||||
|
||||
if not admin:
|
||||
return {"content": "⚠️ Администратор не найден"}
|
||||
|
||||
username = f"@{admin.username}" if admin.username else "—"
|
||||
first_name = admin.first_name or "—"
|
||||
last_name = admin.last_name or "—"
|
||||
|
||||
content = f"""
|
||||
<blockquote>👨💼 <b>Информация об администраторе</b></blockquote>
|
||||
|
||||
<b>ID:</b> <code>{admin.id}</code>
|
||||
<b>Username:</b> {username}
|
||||
<b>Имя:</b> <code>{first_name}</code>
|
||||
<b>Фамилия:</b> <code>{last_name}</code>
|
||||
|
||||
<b>Дата добавления:</b> <code>{admin.created_at.strftime('%d.%m.%Y %H:%M')}</code>
|
||||
"""
|
||||
|
||||
return {"content": content}
|
||||
|
||||
|
||||
async def on_remove_admin_click(
|
||||
callback: CallbackQuery,
|
||||
button: Button,
|
||||
dialog_manager: DialogManager,
|
||||
) -> None:
|
||||
await dialog_manager.switch_to(CreatorMenuSG.remove_admin_confirm)
|
||||
|
||||
|
||||
@inject
|
||||
async def get_remove_admin_confirm_data(
|
||||
users_repository: FromDishka[UsersRepository],
|
||||
dialog_manager: DialogManager,
|
||||
**kwargs,
|
||||
) -> dict[str, str]:
|
||||
admin_id = dialog_manager.dialog_data.get("selected_admin_id")
|
||||
if not admin_id:
|
||||
return {"content": "⚠️ Администратор не выбран"}
|
||||
|
||||
admin = await users_repository.get_user_by_id(int(admin_id))
|
||||
|
||||
if not admin:
|
||||
return {"content": "⚠️ Администратор не найден"}
|
||||
|
||||
username = f"@{admin.username}" if admin.username else f"ID: {admin.id}"
|
||||
display_name = admin.first_name or username
|
||||
|
||||
content = f"""
|
||||
<blockquote>⚠️ <b>Подтверждение удаления</b></blockquote>
|
||||
|
||||
Вы уверены, что хотите удалить администратора <b>{display_name}</b>?
|
||||
|
||||
Пользователь потеряет права администратора.
|
||||
"""
|
||||
|
||||
return {"content": content}
|
||||
|
||||
|
||||
@inject
|
||||
async def on_remove_admin_confirm(
|
||||
callback: CallbackQuery,
|
||||
button: Button,
|
||||
dialog_manager: DialogManager,
|
||||
users_repository: FromDishka[UsersRepository],
|
||||
) -> None:
|
||||
admin_id = dialog_manager.dialog_data.get("selected_admin_id")
|
||||
if not admin_id:
|
||||
await callback.answer("⚠️ Администратор не выбран", show_alert=True)
|
||||
return
|
||||
|
||||
await users_repository.update_user(int(admin_id), is_admin=False)
|
||||
|
||||
await callback.answer("✅ Администратор удалён!")
|
||||
await dialog_manager.switch_to(CreatorMenuSG.admins_list)
|
||||
|
||||
|
||||
async def on_remove_admin_cancel(
|
||||
callback: CallbackQuery,
|
||||
button: Button,
|
||||
dialog_manager: DialogManager,
|
||||
) -> None:
|
||||
await dialog_manager.switch_to(CreatorMenuSG.admin_info)
|
||||
|
||||
|
||||
@inject
|
||||
async def get_add_admin_select_user_data(
|
||||
users_repository: FromDishka[UsersRepository],
|
||||
**kwargs,
|
||||
) -> dict[str, str | list[tuple[str, int]]]:
|
||||
all_users = await users_repository.get_all_users()
|
||||
non_admin_users = [u for u in all_users if not u.is_admin]
|
||||
|
||||
if not non_admin_users:
|
||||
content = """
|
||||
<blockquote>➕ <b>Добавить администратора</b></blockquote>
|
||||
|
||||
⚠️ Нет пользователей, которых можно сделать администраторами.
|
||||
|
||||
Все пользователи уже являются администраторами.
|
||||
"""
|
||||
users_list = []
|
||||
else:
|
||||
content = f"""
|
||||
<blockquote>➕ <b>Добавить администратора</b></blockquote>
|
||||
|
||||
Всего пользователей: <code>{len(non_admin_users)}</code>
|
||||
|
||||
Выберите пользователя для назначения администратором:
|
||||
"""
|
||||
users_list = []
|
||||
for user in non_admin_users:
|
||||
display_name = f"@{user.username}" if user.username else f"ID: {user.id}"
|
||||
if user.first_name:
|
||||
display_name = f"{user.first_name} ({display_name})"
|
||||
users_list.append((display_name, user.id))
|
||||
|
||||
return {
|
||||
"content": content,
|
||||
"users": users_list,
|
||||
}
|
||||
|
||||
|
||||
async def on_user_selected(
|
||||
callback: CallbackQuery,
|
||||
widget,
|
||||
dialog_manager: DialogManager,
|
||||
item_id: str,
|
||||
) -> None:
|
||||
dialog_manager.dialog_data["selected_user_id"] = int(item_id)
|
||||
await dialog_manager.switch_to(CreatorMenuSG.add_admin_confirm)
|
||||
|
||||
|
||||
@inject
|
||||
async def get_add_admin_confirm_data(
|
||||
users_repository: FromDishka[UsersRepository],
|
||||
dialog_manager: DialogManager,
|
||||
**kwargs,
|
||||
) -> dict[str, str]:
|
||||
user_id = dialog_manager.dialog_data.get("selected_user_id")
|
||||
if not user_id:
|
||||
return {"content": "⚠️ Пользователь не выбран"}
|
||||
|
||||
user = await users_repository.get_user_by_id(int(user_id))
|
||||
|
||||
if not user:
|
||||
return {"content": "⚠️ Пользователь не найден"}
|
||||
|
||||
username = f"@{user.username}" if user.username else f"ID: {user.id}"
|
||||
display_name = user.first_name or username
|
||||
|
||||
content = f"""
|
||||
<blockquote>⚠️ <b>Подтверждение</b></blockquote>
|
||||
|
||||
Вы уверены, что хотите назначить <b>{display_name}</b> администратором?
|
||||
|
||||
Пользователь получит доступ к панели управления.
|
||||
"""
|
||||
|
||||
return {"content": content}
|
||||
|
||||
|
||||
@inject
|
||||
async def on_add_admin_confirm(
|
||||
callback: CallbackQuery,
|
||||
button: Button,
|
||||
dialog_manager: DialogManager,
|
||||
users_repository: FromDishka[UsersRepository],
|
||||
) -> None:
|
||||
user_id = dialog_manager.dialog_data.get("selected_user_id")
|
||||
if not user_id:
|
||||
await callback.answer("⚠️ Пользователь не выбран", show_alert=True)
|
||||
return
|
||||
|
||||
await users_repository.update_user(int(user_id), is_admin=True)
|
||||
|
||||
await callback.answer("✅ Администратор добавлен!")
|
||||
await dialog_manager.switch_to(CreatorMenuSG.admins_list)
|
||||
|
||||
|
||||
async def on_add_admin_cancel(
|
||||
callback: CallbackQuery,
|
||||
button: Button,
|
||||
dialog_manager: DialogManager,
|
||||
) -> None:
|
||||
await dialog_manager.switch_to(CreatorMenuSG.add_admin_select_user)
|
||||
|
||||
|
||||
async def on_back_to_main(
|
||||
callback: CallbackQuery,
|
||||
button: Button,
|
||||
dialog_manager: DialogManager,
|
||||
) -> None:
|
||||
await dialog_manager.done()
|
||||
|
||||
|
||||
admins_list_window = Window(
|
||||
Format("{content}"),
|
||||
ScrollingGroup(
|
||||
Select(
|
||||
Format("{item[0]}"),
|
||||
id="admin_select",
|
||||
item_id_getter=lambda x: x[1],
|
||||
items="admins",
|
||||
on_click=on_admin_selected,
|
||||
),
|
||||
id="admins_scroll",
|
||||
width=1,
|
||||
height=8,
|
||||
),
|
||||
Button(
|
||||
Const("➕ Добавить администратора"),
|
||||
id="add_admin_btn",
|
||||
on_click=on_add_admin_click,
|
||||
),
|
||||
Button(
|
||||
Const("◀️ Назад"),
|
||||
id="back_to_main_from_admins",
|
||||
on_click=on_back_to_main,
|
||||
),
|
||||
state=CreatorMenuSG.admins_list,
|
||||
getter=get_admins_list_data,
|
||||
)
|
||||
|
||||
admin_info_window = Window(
|
||||
Format("{content}"),
|
||||
Button(
|
||||
Const("🗑 Удалить администратора"),
|
||||
id="remove_admin_btn",
|
||||
on_click=on_remove_admin_click,
|
||||
),
|
||||
SwitchTo(
|
||||
Const("◀️ Назад"),
|
||||
id="back_to_admins_list",
|
||||
state=CreatorMenuSG.admins_list,
|
||||
),
|
||||
state=CreatorMenuSG.admin_info,
|
||||
getter=get_admin_info_data,
|
||||
)
|
||||
|
||||
remove_admin_confirm_window = Window(
|
||||
Format("{content}"),
|
||||
Row(
|
||||
Button(
|
||||
Const("✅ Да"),
|
||||
id="confirm_remove_admin",
|
||||
on_click=on_remove_admin_confirm,
|
||||
),
|
||||
Button(
|
||||
Const("❌ Нет"),
|
||||
id="cancel_remove_admin",
|
||||
on_click=on_remove_admin_cancel,
|
||||
),
|
||||
),
|
||||
state=CreatorMenuSG.remove_admin_confirm,
|
||||
getter=get_remove_admin_confirm_data,
|
||||
)
|
||||
|
||||
add_admin_select_user_window = Window(
|
||||
Format("{content}"),
|
||||
ScrollingGroup(
|
||||
Select(
|
||||
Format("{item[0]}"),
|
||||
id="user_select",
|
||||
item_id_getter=lambda x: x[1],
|
||||
items="users",
|
||||
on_click=on_user_selected,
|
||||
),
|
||||
id="users_scroll",
|
||||
width=1,
|
||||
height=8,
|
||||
),
|
||||
SwitchTo(
|
||||
Const("◀️ Назад"),
|
||||
id="back_to_admins_from_add",
|
||||
state=CreatorMenuSG.admins_list,
|
||||
),
|
||||
state=CreatorMenuSG.add_admin_select_user,
|
||||
getter=get_add_admin_select_user_data,
|
||||
)
|
||||
|
||||
add_admin_confirm_window = Window(
|
||||
Format("{content}"),
|
||||
Row(
|
||||
Button(
|
||||
Const("✅ Да"),
|
||||
id="confirm_add_admin",
|
||||
on_click=on_add_admin_confirm,
|
||||
),
|
||||
Button(
|
||||
Const("❌ Нет"),
|
||||
id="cancel_add_admin",
|
||||
on_click=on_add_admin_cancel,
|
||||
),
|
||||
),
|
||||
state=CreatorMenuSG.add_admin_confirm,
|
||||
getter=get_add_admin_confirm_data,
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
from aiogram_dialog import Dialog
|
||||
|
||||
from dutylog.application.bot.creator_dialogs.admins_management import (
|
||||
admins_list_window,
|
||||
admin_info_window,
|
||||
remove_admin_confirm_window,
|
||||
add_admin_select_user_window,
|
||||
add_admin_confirm_window,
|
||||
)
|
||||
|
||||
|
||||
creator_menu_dialog = Dialog(
|
||||
admins_list_window,
|
||||
admin_info_window,
|
||||
remove_admin_confirm_window,
|
||||
add_admin_select_user_window,
|
||||
add_admin_confirm_window,
|
||||
)
|
||||
@@ -116,19 +116,11 @@ async def on_resident_selected(
|
||||
widget: Select,
|
||||
dialog_manager: DialogManager,
|
||||
item_id: str,
|
||||
residents_repository: FromDishka[ResidentsRepository],
|
||||
users_repository: FromDishka[UsersRepository],
|
||||
residents_repository: FromDishka[ResidentsRepository]
|
||||
):
|
||||
user_id = callback.from_user.id
|
||||
resident_id = int(item_id)
|
||||
|
||||
await users_repository.get_or_create_user(
|
||||
user_id=user_id,
|
||||
username=callback.from_user.username,
|
||||
first_name=callback.from_user.first_name,
|
||||
last_name=callback.from_user.last_name,
|
||||
)
|
||||
|
||||
await residents_repository.bind_user_to_resident(resident_id, user_id)
|
||||
|
||||
await callback.answer("✅ Регистрация успешна!")
|
||||
|
||||
@@ -49,6 +49,14 @@ class AdminMenuSG(StatesGroup):
|
||||
broadcast_confirm = State()
|
||||
|
||||
|
||||
class CreatorMenuSG(StatesGroup):
|
||||
admins_list = State()
|
||||
admin_info = State()
|
||||
remove_admin_confirm = State()
|
||||
add_admin_select_user = State()
|
||||
add_admin_confirm = State()
|
||||
|
||||
|
||||
class RegistrationSG(StatesGroup):
|
||||
select_floor = State()
|
||||
select_room = State()
|
||||
|
||||
@@ -29,7 +29,13 @@ async def start_handler(
|
||||
config: FromDishka[Config],
|
||||
):
|
||||
assert message.from_user is not None
|
||||
user = await users_repository.get_user_by_id(message.from_user.id)
|
||||
|
||||
user = await users_repository.get_or_create_user(
|
||||
user_id=message.from_user.id,
|
||||
username=message.from_user.username,
|
||||
first_name=message.from_user.first_name,
|
||||
last_name=message.from_user.last_name,
|
||||
)
|
||||
|
||||
is_creator = message.from_user.id == config.bot.creator_id
|
||||
is_admin = user.is_admin if user else False
|
||||
|
||||
@@ -50,3 +50,6 @@ class UsersRepository:
|
||||
|
||||
async def get_all_users(self) -> list[User]:
|
||||
return await self.users_dao.get_all()
|
||||
|
||||
async def update_user(self, user_id: int, **kwargs) -> User | None:
|
||||
return await self.users_dao.update(user_id, **kwargs)
|
||||
|
||||
Binary file not shown.
@@ -1,79 +0,0 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user