From 8b05a2d51247fa3a25f34cbeb886d425052db2d2 Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 1 Mar 2026 15:06:22 +0300 Subject: [PATCH] update --- pyproject.toml | 1 + src/dutylog/application/__main__.py | 2 + .../admin_dialogs/admin_menu_dialog.py | 4 + .../reporting_period_management.py | 216 ++++++++++++++++- .../application/bot/user_dialogs/states.py | 2 + .../database/dao/hours_transactions_dao.py | 35 +++ .../repositories/floors_repository.py | 3 + .../hours_transactions_repository.py | 3 + .../reporting_periods_repository.py | 6 + .../repositories/residents_repository.py | 3 + .../database/repositories/rooms_repository.py | 3 + src/dutylog/infrastructure/ioc.py | 18 ++ src/dutylog/services/__init__.py | 3 + src/dutylog/services/report_service.py | 223 ++++++++++++++++++ uv.lock | 23 ++ 15 files changed, 540 insertions(+), 5 deletions(-) create mode 100644 src/dutylog/services/__init__.py create mode 100644 src/dutylog/services/report_service.py diff --git a/pyproject.toml b/pyproject.toml index 020f9be..0f15d82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "aiogram-dialog>=2.5.0", "asyncpg>=0.31.0", "dishka>=1.8.0", + "openpyxl>=3.1.5", ] [project.scripts] diff --git a/src/dutylog/application/__main__.py b/src/dutylog/application/__main__.py index 05e6d23..dbbc312 100644 --- a/src/dutylog/application/__main__.py +++ b/src/dutylog/application/__main__.py @@ -17,6 +17,7 @@ from dutylog.infrastructure.ioc import ( DatabaseProvider, DAOProvider, RepositoryProvider, + ServiceProvider, ) from dutylog.infrastructure.utils.config import load_config @@ -39,6 +40,7 @@ async def main(): DatabaseProvider(), DAOProvider(), RepositoryProvider(), + ServiceProvider(), ) dp.include_router(user_router) diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py index 9939650..e8d15a6 100644 --- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py @@ -38,6 +38,8 @@ from dutylog.application.bot.user_dialogs.admin_dialogs.rooms_management import from dutylog.application.bot.user_dialogs.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 ( add_hours_select_window, @@ -85,6 +87,8 @@ admin_menu_dialog = Dialog( create_room_confirm_window, reporting_period_window, next_period_confirm_window, + generate_report_select_period_window, + generate_report_confirm_window, statistics_window, broadcast_window, broadcast_confirm_window, diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/reporting_period_management.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/reporting_period_management.py index 354767f..f943275 100644 --- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/reporting_period_management.py +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/reporting_period_management.py @@ -1,15 +1,18 @@ -from aiogram.types import CallbackQuery +from datetime import datetime, timedelta + +from aiogram.types import CallbackQuery, BufferedInputFile from aiogram_dialog import Window, DialogManager from aiogram_dialog.widgets.text import Format, Const -from aiogram_dialog.widgets.kbd import SwitchTo, Button, Row +from aiogram_dialog.widgets.kbd import SwitchTo, Button, Row, Select, ScrollingGroup 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.infrastructure.database.repositories.reporting_periods_repository import ( ReportingPeriodsRepository, ) +from dutylog.infrastructure.utils.datetime import msk_now +from dutylog.services.report_service import ReportService MONTH_NAMES = { @@ -96,7 +99,7 @@ async def on_make_report_click( button: Button, dialog_manager: DialogManager, ): - await callback.answer("⚠️ Функционал в разработке", show_alert=True) + await dialog_manager.switch_to(AdminMenuSG.generate_report_select_period) @inject @@ -173,7 +176,6 @@ reporting_period_window = Window( Const("📊 Сделать отчёт"), id="make_report_btn", on_click=on_make_report_click, - when="has_active", ), SwitchTo( Const("◀️ Назад"), @@ -201,3 +203,207 @@ next_period_confirm_window = Window( state=AdminMenuSG.next_period_confirm, getter=get_next_period_confirm_data, ) + + +@inject +async def get_generate_report_data( + reporting_periods_repository: FromDishka[ReportingPeriodsRepository], + **kwargs, +): + 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() + + if not completed_periods and not active_period: + content = """ +
📊 Генерация отчёта
+ +⚠️ Нет отчётных периодов. + +Создайте отчётный период, чтобы сгенерировать отчёт. +""" + has_periods = False + periods_list = [] + elif not completed_periods and active_period: + content = """ +
📊 Генерация отчёта
+ +Будет сгенерирован отчёт по активному периоду. +""" + has_periods = True + next_day = active_period.start_date + timedelta(days=1) + month_name = MONTH_NAMES[next_day.month] + year = next_day.year + periods_list = [( + f"{month_name} {year} (активный, с {active_period.start_date.strftime('%d.%m.%Y')})", + active_period.id + )] + else: + content = """ +
📊 Генерация отчёта
+ +Выберите период для генерации отчёта: +""" + has_periods = True + periods_list = [] + for period in sorted(completed_periods, key=lambda p: p.start_date, reverse=True): + if period.end_date: + next_day = period.start_date + timedelta(days=1) + month_name = MONTH_NAMES[next_day.month] + year = next_day.year + periods_list.append(( + f"{month_name} {year} ({period.start_date.strftime('%d.%m.%Y')} - {period.end_date.strftime('%d.%m.%Y')})", + period.id + )) + + return { + "content": content, + "has_periods": has_periods, + "periods": periods_list, + } + + +async def on_period_selected( + callback: CallbackQuery, + widget, + dialog_manager: DialogManager, + item_id: str, +): + dialog_manager.dialog_data["selected_period_id"] = int(item_id) + await dialog_manager.switch_to(AdminMenuSG.generate_report_confirm) + + +@inject +async def get_generate_report_confirm_data( + reporting_periods_repository: FromDishka[ReportingPeriodsRepository], + dialog_manager: DialogManager, + **kwargs, +): + period_id = dialog_manager.dialog_data.get("selected_period_id") + if not period_id: + return {"content": "⚠️ Период не выбран"} + + period = await reporting_periods_repository.get_by_id(int(period_id)) + + if period: + next_day = period.start_date + timedelta(days=1) + month_name = MONTH_NAMES[next_day.month] + year = next_day.year + + if period.end_date: + content = f""" +
📊 Подтверждение генерации
+ +Период: {month_name} {year} +Даты: {period.start_date.strftime('%d.%m.%Y')} - {period.end_date.strftime('%d.%m.%Y')} + +Сгенерировать отчёт по начислениям часов за этот период? +""" + else: + content = f""" +
📊 Подтверждение генерации
+ +Период: {month_name} {year} (активный) +Начало: {period.start_date.strftime('%d.%m.%Y')} + +Сгенерировать отчёт по начислениям часов за активный период (до текущей даты)? +""" + else: + content = "⚠️ Период не найден" + + return {"content": content} + + +@inject +async def on_generate_report_confirm( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, + reporting_periods_repository: FromDishka[ReportingPeriodsRepository], + report_service: FromDishka[ReportService], +): + period_id = dialog_manager.dialog_data.get("selected_period_id") + if not period_id: + await callback.answer("⚠️ Период не выбран", show_alert=True) + return + + period = await reporting_periods_repository.get_by_id(int(period_id)) + + if not period: + await callback.answer("⚠️ Период не найден", show_alert=True) + return + + await callback.answer("⏳ Генерирую отчёт...") + + end_date = period.end_date if period.end_date else msk_now() + print(end_date) + + report_file = await report_service.generate_period_report( + period.start_date, end_date + ) + + next_day = period.start_date + timedelta(days=1) + month_name = MONTH_NAMES[next_day.month] + year = next_day.year + filename = f"Отчёт_{month_name}_{year}.xlsx" + + document = BufferedInputFile(report_file.read(), filename=filename) + if callback.message: + await callback.message.answer_document( + document=document, + caption=f"📊 Отчёт по начислениям часов за {month_name} {year}" + ) + + await callback.answer("✅ Отчёт сгенерирован!") + await dialog_manager.switch_to(AdminMenuSG.reporting_period) + + +async def on_generate_report_cancel( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await dialog_manager.switch_to(AdminMenuSG.generate_report_select_period) + + +generate_report_select_period_window = Window( + Format("{content}"), + ScrollingGroup( + Select( + Format("{item[0]}"), + id="period_select", + item_id_getter=lambda x: x[1], + items="periods", + on_click=on_period_selected, + ), + id="periods_scroll", + width=1, + height=5, + when="has_periods", + ), + SwitchTo( + Const("◀️ Назад"), + id="back_to_reporting_period", + state=AdminMenuSG.reporting_period, + ), + state=AdminMenuSG.generate_report_select_period, + getter=get_generate_report_data, +) + +generate_report_confirm_window = Window( + Format("{content}"), + Row( + Button( + Const("✅ Да"), + id="confirm_generate_report", + on_click=on_generate_report_confirm, + ), + Button( + Const("❌ Нет"), + id="cancel_generate_report", + on_click=on_generate_report_cancel, + ), + ), + state=AdminMenuSG.generate_report_confirm, + getter=get_generate_report_confirm_data, +) diff --git a/src/dutylog/application/bot/user_dialogs/states.py b/src/dutylog/application/bot/user_dialogs/states.py index 8147281..a1563bc 100644 --- a/src/dutylog/application/bot/user_dialogs/states.py +++ b/src/dutylog/application/bot/user_dialogs/states.py @@ -42,6 +42,8 @@ class AdminMenuSG(StatesGroup): create_room_confirm = State() reporting_period = State() next_period_confirm = State() + generate_report_select_period = State() + generate_report_confirm = State() statistics = State() broadcast = State() broadcast_confirm = State() diff --git a/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py b/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py index b5b9c9a..2a88e22 100644 --- a/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py +++ b/src/dutylog/infrastructure/database/dao/hours_transactions_dao.py @@ -39,3 +39,38 @@ 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 + + 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()) diff --git a/src/dutylog/infrastructure/database/repositories/floors_repository.py b/src/dutylog/infrastructure/database/repositories/floors_repository.py index f8e2eb6..fa59c6a 100644 --- a/src/dutylog/infrastructure/database/repositories/floors_repository.py +++ b/src/dutylog/infrastructure/database/repositories/floors_repository.py @@ -28,3 +28,6 @@ class FloorsRepository: async def delete_floor(self, floor_id: int) -> None: await self.floors_dao.delete(floor_id) + + async def get_by_id(self, floor_id: int) -> Floor | None: + return await self.get_floor_by_id(floor_id) diff --git a/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py b/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py index d0be656..b0b3e86 100644 --- a/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py +++ b/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py @@ -121,3 +121,6 @@ class HoursTransactionsRepository: self, transaction_id: int ) -> HoursTransaction | None: return await self.transactions_dao.get_by_id(transaction_id) + + async def get_by_period(self, start_date, end_date) -> list[HoursTransaction]: + return await self.transactions_dao.get_by_period(start_date, end_date) diff --git a/src/dutylog/infrastructure/database/repositories/reporting_periods_repository.py b/src/dutylog/infrastructure/database/repositories/reporting_periods_repository.py index 7c5bb49..5712e30 100644 --- a/src/dutylog/infrastructure/database/repositories/reporting_periods_repository.py +++ b/src/dutylog/infrastructure/database/repositories/reporting_periods_repository.py @@ -26,3 +26,9 @@ class ReportingPeriodsRepository: async def delete_period(self, period_id: int) -> None: await self.reporting_periods_dao.delete(period_id) + + async def get_by_id(self, period_id: int) -> ReportingPeriod | None: + return await self.get_period_by_id(period_id) + + async def get_all(self) -> list[ReportingPeriod]: + return await self.get_all_periods() diff --git a/src/dutylog/infrastructure/database/repositories/residents_repository.py b/src/dutylog/infrastructure/database/repositories/residents_repository.py index e7ac9a1..e7ebf26 100644 --- a/src/dutylog/infrastructure/database/repositories/residents_repository.py +++ b/src/dutylog/infrastructure/database/repositories/residents_repository.py @@ -103,3 +103,6 @@ class ResidentsRepository: async def delete_resident(self, resident_id: int) -> None: await self.residents_dao.delete(resident_id) + + async def get_by_id(self, resident_id: int) -> Resident | None: + return await self.get_resident_by_id(resident_id) diff --git a/src/dutylog/infrastructure/database/repositories/rooms_repository.py b/src/dutylog/infrastructure/database/repositories/rooms_repository.py index 414982a..08e7b44 100644 --- a/src/dutylog/infrastructure/database/repositories/rooms_repository.py +++ b/src/dutylog/infrastructure/database/repositories/rooms_repository.py @@ -24,3 +24,6 @@ class RoomsRepository: async def delete_room(self, room_id: int) -> None: await self.rooms_dao.delete(room_id) + + async def get_by_id(self, room_id: int) -> Room | None: + return await self.get_room_by_id(room_id) diff --git a/src/dutylog/infrastructure/ioc.py b/src/dutylog/infrastructure/ioc.py index e001005..c9129d0 100644 --- a/src/dutylog/infrastructure/ioc.py +++ b/src/dutylog/infrastructure/ioc.py @@ -33,6 +33,7 @@ from dutylog.infrastructure.database.repositories.reporting_periods_repository i ReportingPeriodsRepository, ) from dutylog.infrastructure.utils.config import Config, load_config +from dutylog.services.report_service import ReportService class ConfigProvider(Provider): @@ -118,3 +119,20 @@ class RepositoryProvider(Provider): self, reporting_periods_dao: ReportingPeriodsDAO ) -> ReportingPeriodsRepository: return ReportingPeriodsRepository(reporting_periods_dao) + + +class ServiceProvider(Provider): + @provide(scope=Scope.REQUEST) + def get_report_service( + self, + hours_transactions_repository: HoursTransactionsRepository, + residents_repository: ResidentsRepository, + rooms_repository: RoomsRepository, + floors_repository: FloorsRepository, + ) -> ReportService: + return ReportService( + hours_transactions_repository, + residents_repository, + rooms_repository, + floors_repository, + ) diff --git a/src/dutylog/services/__init__.py b/src/dutylog/services/__init__.py new file mode 100644 index 0000000..96cd330 --- /dev/null +++ b/src/dutylog/services/__init__.py @@ -0,0 +1,3 @@ +from dutylog.services.report_service import ReportService + +__all__ = ["ReportService"] diff --git a/src/dutylog/services/report_service.py b/src/dutylog/services/report_service.py new file mode 100644 index 0000000..bee67fe --- /dev/null +++ b/src/dutylog/services/report_service.py @@ -0,0 +1,223 @@ +from datetime import date +from io import BytesIO + +from openpyxl import Workbook +from openpyxl.styles import Alignment, Border, Font, PatternFill, Side + +from dutylog.infrastructure.database.repositories.floors_repository import ( + FloorsRepository, +) +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, +) + + +class ReportService: + def __init__( + self, + hours_transactions_repository: HoursTransactionsRepository, + residents_repository: ResidentsRepository, + rooms_repository: RoomsRepository, + floors_repository: FloorsRepository, + ): + self.hours_transactions_repository = hours_transactions_repository + self.residents_repository = residents_repository + self.rooms_repository = rooms_repository + self.floors_repository = floors_repository + + async def generate_period_report(self, start_date: date, end_date: date) -> BytesIO: + transactions = await self.hours_transactions_repository.get_by_period( + start_date, end_date + ) + + increase_transactions = [ + t for t in transactions if t.transaction_type == "increase" + ] + decrease_transactions = [ + t for t in transactions if t.transaction_type == "decrease" + ] + + wb = Workbook() + ws = wb.active + if ws is None: + raise ValueError("Failed to create worksheet") + ws.title = "Отчет по начислениям" + + header_font = Font(bold=True, size=12, color="FFFFFF") + header_fill = PatternFill( + start_color="4472C4", end_color="4472C4", fill_type="solid" + ) + header_alignment = Alignment(horizontal="center", vertical="center") + + summary_font = Font(bold=True, size=11) + summary_fill = PatternFill( + start_color="E7E6E6", end_color="E7E6E6", fill_type="solid" + ) + + border = Border( + left=Side(style="thin"), + right=Side(style="thin"), + top=Side(style="thin"), + bottom=Side(style="thin"), + ) + + 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") + title_cell = ws.cell(row=1, column=1) + title_cell.font = Font(bold=True, size=14) + title_cell.alignment = Alignment(horizontal="center", vertical="center") + ws.row_dimensions[1].height = 25 + + 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}") + row_num += 1 + + headers = ["Дата", "Резидент", "Комната", "Часы", "Примечание"] + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=row_num, column=col_num) + cell.value = header + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + cell.border = border + + ws.row_dimensions[row_num].height = 20 + row_num += 1 + + total_increase = 0 + + for transaction in increase_transactions: + resident = await self.residents_repository.get_by_id( + transaction.resident_id + ) + if not resident: + continue + + room = await self.rooms_repository.get_by_id(resident.room) + room_number = room.number if room else "—" + + ws.cell( + row=row_num, column=1, value=transaction.created_at.replace(tzinfo=None) + ).border = border + ws.cell(row=row_num, column=1).number_format = "DD.MM.YYYY HH:MM" + ws.cell( + row=row_num, column=2, value=resident.real_name or "Без имени" + ).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=transaction.remark or "—" + ).border = border + + ws.cell(row=row_num, column=1).alignment = Alignment(horizontal="center") + ws.cell(row=row_num, column=3).alignment = Alignment(horizontal="center") + ws.cell(row=row_num, column=4).alignment = Alignment(horizontal="center") + + total_increase += transaction.amount + row_num += 1 + + summary_row = row_num + ws.merge_cells(f"A{summary_row}:C{summary_row}") + summary_cell = ws[f"A{summary_row}"] + summary_cell.value = "Начислено часов:" + summary_cell.font = summary_font + summary_cell.fill = summary_fill + summary_cell.alignment = Alignment(horizontal="right", vertical="center") + summary_cell.border = border + + ws.cell(row=summary_row, column=4, value=total_increase).font = summary_font + ws.cell(row=summary_row, column=4).fill = summary_fill + ws.cell(row=summary_row, column=4).alignment = Alignment(horizontal="center") + ws.cell(row=summary_row, column=4).border = border + + ws.cell(row=summary_row, column=5, value="").fill = summary_fill + ws.cell(row=summary_row, column=5).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}") + row_num += 1 + + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=row_num, column=col_num) + cell.value = header + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + cell.border = border + + ws.row_dimensions[row_num].height = 20 + row_num += 1 + + total_decrease = 0 + + for transaction in decrease_transactions: + resident = await self.residents_repository.get_by_id( + transaction.resident_id + ) + if not resident: + continue + + room = await self.rooms_repository.get_by_id(resident.room) + room_number = room.number if room else "—" + + ws.cell( + row=row_num, column=1, value=transaction.created_at.replace(tzinfo=None) + ).border = border + ws.cell(row=row_num, column=1).number_format = "DD.MM.YYYY HH:MM" + ws.cell( + row=row_num, column=2, value=resident.real_name or "Без имени" + ).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=transaction.remark or "—" + ).border = border + + ws.cell(row=row_num, column=1).alignment = Alignment(horizontal="center") + ws.cell(row=row_num, column=3).alignment = Alignment(horizontal="center") + ws.cell(row=row_num, column=4).alignment = Alignment(horizontal="center") + + total_decrease += transaction.amount + row_num += 1 + + summary_row = row_num + ws.merge_cells(f"A{summary_row}:C{summary_row}") + summary_cell = ws[f"A{summary_row}"] + summary_cell.value = "Списано часов:" + summary_cell.font = summary_font + summary_cell.fill = summary_fill + summary_cell.alignment = Alignment(horizontal="right", vertical="center") + summary_cell.border = border + + ws.cell(row=summary_row, column=4, value=total_decrease).font = summary_font + ws.cell(row=summary_row, column=4).fill = summary_fill + ws.cell(row=summary_row, column=4).alignment = Alignment(horizontal="center") + ws.cell(row=summary_row, column=4).border = border + + ws.cell(row=summary_row, column=5, value="").fill = summary_fill + ws.cell(row=summary_row, column=5).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 + + output = BytesIO() + wb.save(output) + output.seek(0) + + return output diff --git a/uv.lock b/uv.lock index d7dc182..0e29f8a 100644 --- a/uv.lock +++ b/uv.lock @@ -249,6 +249,7 @@ dependencies = [ { name = "aiogram-dialog" }, { name = "asyncpg" }, { name = "dishka" }, + { name = "openpyxl" }, ] [package.dev-dependencies] @@ -265,6 +266,7 @@ requires-dist = [ { name = "aiogram-dialog", specifier = ">=2.5.0" }, { name = "asyncpg", specifier = ">=0.31.0" }, { name = "dishka", specifier = ">=1.8.0" }, + { name = "openpyxl", specifier = ">=3.1.5" }, ] [package.metadata.requires-dev] @@ -275,6 +277,15 @@ dev = [ { name = "watchfiles", specifier = ">=1.1.1" }, ] +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -483,6 +494,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + [[package]] name = "ovld" version = "0.5.14"