This commit is contained in:
2026-03-17 20:12:29 +03:00
parent e796a5bfdd
commit d05f9c3042
14 changed files with 188 additions and 525 deletions
@@ -0,0 +1,46 @@
"""add_per_room_field_and_remove_room_hours_transactions
Revision ID: 4fe7f71301e7
Revises: c7a225e7de2f
Create Date: 2026-03-17 20:00:49.769147
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = '4fe7f71301e7'
down_revision: Union[str, Sequence[str], None] = 'c7a225e7de2f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('hours_transactions', sa.Column('per_room', sa.Boolean(), server_default='false', nullable=False))
op.drop_table('room_hours_transactions')
op.drop_column('rooms', 'inactive_hours')
op.drop_column('rooms', 'active_hours')
def downgrade() -> None:
op.add_column('rooms', sa.Column('active_hours', sa.Integer(), server_default='0', nullable=False))
op.add_column('rooms', sa.Column('inactive_hours', sa.Integer(), server_default='0', nullable=False))
op.create_table('room_hours_transactions',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('room_id', sa.Integer(), nullable=False),
sa.Column('transaction_type', sa.String(length=50), nullable=False),
sa.Column('amount', sa.Integer(), nullable=False),
sa.Column('admin_id', sa.BigInteger(), nullable=True),
sa.Column('remark', sa.String(length=500), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['admin_id'], ['users.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['room_id'], ['rooms.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.drop_column('hours_transactions', 'per_room')
@@ -1,6 +1,6 @@
from aiogram import Bot from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from aiogram.exceptions import TelegramForbiddenError
from aiogram_dialog import Window, DialogManager from aiogram_dialog import Window, DialogManager
from aiogram_dialog.widgets.text import Format, Const from aiogram_dialog.widgets.text import Format, Const
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button, Select, Group from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button, Select, Group
@@ -173,17 +173,11 @@ async def on_add_hours_confirm(
admin = await users_repository.get_user_by_id(admin_id) admin = await users_repository.get_user_by_id(admin_id)
admin_username = f"@{admin.username}" if admin and admin.username else "Администратор" admin_username = f"@{admin.username}" if admin and admin.username else "Администратор"
notification_text = ( notification_text = f"<blockquote> <b>Начислены часы</b></blockquote>\n\n<b>Количество:</b> <code>{hours}</code> ч\n<b>Причина:</b> {remark}\n<b>Администратор:</b> {admin_username}\n\n<b>Всего неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
f"<blockquote> <b>Начислены часы</b></blockquote>\n\n"
f"<b>Количество:</b> <code>{hours}</code> ч\n"
f"<b>Причина:</b> {remark}\n"
f"<b>Администратор:</b> {admin_username}\n\n"
f"<b>Всего неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
)
try: try:
await bot.send_message(resident.user_entity, notification_text) await bot.send_message(resident.user_entity, notification_text)
except TelegramForbiddenError: except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
pass pass
await dialog_manager.switch_to(AdminMenuSG.resident_info) await dialog_manager.switch_to(AdminMenuSG.resident_info)
@@ -227,29 +221,19 @@ async def on_remove_hours_confirm(
admin_username = f"@{admin.username}" if admin and admin.username else "Администратор" admin_username = f"@{admin.username}" if admin and admin.username else "Администратор"
if resident.active_hours == 0: if resident.active_hours == 0:
notification_text = ( notification_text = f"<blockquote>🎉 <b>Поздравляем!</b></blockquote>\n\nВы отработали все часы! Теперь у вас <code>0</code> неотработанных часов.\n\n<b>Списано:</b> <code>{hours}</code> ч\n"
f"<blockquote>🎉 <b>Поздравляем!</b></blockquote>\n\n"
f"Вы отработали все часы! Теперь у вас <code>0</code> неотработанных часов.\n\n"
f"<b>Списано:</b> <code>{hours}</code> ч\n"
)
if remark: if remark:
notification_text += f"<b>Причина:</b> {remark}\n" notification_text += f"<b>Причина:</b> {remark}\n"
notification_text += f"<b>Администратор:</b> {admin_username}" notification_text += f"<b>Администратор:</b> {admin_username}"
else: else:
notification_text = ( notification_text = f"<blockquote> <b>Списаны часы</b></blockquote>\n\n<b>Количество:</b> <code>{hours}</code> ч\n"
f"<blockquote> <b>Списаны часы</b></blockquote>\n\n"
f"<b>Количество:</b> <code>{hours}</code> ч\n"
)
if remark: if remark:
notification_text += f"<b>Причина:</b> {remark}\n" notification_text += f"<b>Причина:</b> {remark}\n"
notification_text += ( notification_text += f"<b>Администратор:</b> {admin_username}\n\n<b>Осталось неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
f"<b>Администратор:</b> {admin_username}\n\n"
f"<b>Осталось неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
)
try: try:
await bot.send_message(resident.user_entity, notification_text) await bot.send_message(resident.user_entity, notification_text)
except TelegramForbiddenError: except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
pass pass
await dialog_manager.switch_to(AdminMenuSG.resident_info) await dialog_manager.switch_to(AdminMenuSG.resident_info)
@@ -1,3 +1,5 @@
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from aiogram_dialog import Window, DialogManager from aiogram_dialog import Window, DialogManager
from aiogram_dialog.widgets.text import Format, Const from aiogram_dialog.widgets.text import Format, Const
@@ -16,8 +18,8 @@ from dutylog.infrastructure.database.repositories.floors_repository import (
from dutylog.infrastructure.database.repositories.residents_repository import ( from dutylog.infrastructure.database.repositories.residents_repository import (
ResidentsRepository, ResidentsRepository,
) )
from dutylog.infrastructure.database.repositories.room_hours_transactions_repository import ( from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
RoomHoursTransactionsRepository, HoursTransactionsRepository,
) )
from dutylog.infrastructure.database.repositories.users_repository import ( from dutylog.infrastructure.database.repositories.users_repository import (
UsersRepository, UsersRepository,
@@ -43,11 +45,7 @@ async def get_rooms_floors_data(
floors_data = [(f"🏢 Этаж {f.number}", f.id) for f in all_floors] floors_data = [(f"🏢 Этаж {f.number}", f.id) for f in all_floors]
content = """ content = "<blockquote>🚪 <b>Комнаты</b></blockquote>\n\nВыберите этаж для просмотра комнат:"
<blockquote>🚪 <b>Комнаты</b></blockquote>
Выберите этаж для просмотра комнат:
"""
return { return {
"content": content, "content": content,
@@ -89,13 +87,7 @@ async def get_rooms_list_data(
rooms_data = [(f"🚪 Комната {r.number}", r.id) for r in rooms] rooms_data = [(f"🚪 Комната {r.number}", r.id) for r in rooms]
content = f""" content = f"<blockquote>🚪 <b>Комнаты на этаже {floor_number}</b></blockquote>\n\n<b>Всего комнат:</b> <code>{len(rooms)}</code>\n\nВыберите комнату для просмотра:"
<blockquote>🚪 <b>Комнаты на этаже {floor_number}</b></blockquote>
<b>Всего комнат:</b> <code>{len(rooms)}</code>
Выберите комнату для просмотра:
"""
return { return {
"content": content, "content": content,
@@ -132,11 +124,7 @@ async def get_create_room_floors_data(
floors_data = [(f"🏢 Этаж {f.number}", f.id) for f in all_floors] floors_data = [(f"🏢 Этаж {f.number}", f.id) for f in all_floors]
content = """ content = "<blockquote> <b>Создание комнаты</b></blockquote>\n\nВыберите этаж для новой комнаты:"
<blockquote> <b>Создание комнаты</b></blockquote>
Выберите этаж для новой комнаты:
"""
return { return {
"content": content, "content": content,
@@ -238,28 +226,22 @@ async def get_room_info_data(
floor = await floors_repository.get_floor_by_id(room.on_floor) floor = await floors_repository.get_floor_by_id(room.on_floor)
floor_number = floor.number if floor else "???" floor_number = floor.number if floor else "???"
# Получаем резидентов комнаты
residents = await residents_repository.get_residents_by_room(room_id) residents = await residents_repository.get_residents_by_room(room_id)
total_active_hours = sum(r.active_hours for r in residents)
total_inactive_hours = sum(r.inactive_hours for r in residents)
residents_info = "" residents_info = ""
if residents: if residents:
residents_info = "\n<b>Проживающие:</b>\n" residents_info = "\n<b>Проживающие:</b>\n"
for resident in residents: for resident in residents:
status = "🟢" if resident.is_busy else "⚪️" status = "🟢" if resident.is_busy else "⚪️"
name = resident.real_name if resident.real_name else "Без имени" name = resident.real_name if resident.real_name else "Без имени"
residents_info += f"{status} {name}\n" residents_info += f"{status} {name} (<code>{resident.active_hours}</code> ч)\n"
else: else:
residents_info = "\n<i>Нет проживающих</i>\n" residents_info = "\n<i>Нет проживающих</i>\n"
info_content = f""" info_content = f"<blockquote>🚪 <b>Информация о комнате</b></blockquote>\n\n<b>Номер:</b> <code>{room.number}</code>\n<b>Этаж:</b> <code>{floor_number}</code>\n{residents_info}\n🟢 <b>Отработанные часы (всего):</b> <code>{total_inactive_hours}</code> ч\n🔴 <b>Неотработанные часы (всего):</b> <code>{total_active_hours}</code> ч"
<blockquote>🚪 <b>Информация о комнате</b></blockquote>
<b>Номер:</b> <code>{room.number}</code>
<b>Этаж:</b> <code>{floor_number}</code>
{residents_info}
🟢 <b>Отработанные часы:</b> <code>{room.inactive_hours}</code> ч
🔴 <b>Неотработанные часы:</b> <code>{room.active_hours}</code> ч
"""
return { return {
"info_content": info_content, "info_content": info_content,
@@ -391,12 +373,11 @@ async def on_room_add_hours_confirm(
callback: CallbackQuery, callback: CallbackQuery,
button: Button, button: Button,
dialog_manager: DialogManager, dialog_manager: DialogManager,
room_transactions_repository: FromDishka[RoomHoursTransactionsRepository], transactions_repository: FromDishka[HoursTransactionsRepository],
residents_repository: FromDishka[ResidentsRepository], residents_repository: FromDishka[ResidentsRepository],
users_repository: FromDishka[UsersRepository], users_repository: FromDishka[UsersRepository],
**kwargs, **kwargs,
): ):
from aiogram import Bot
bot: Bot = dialog_manager.middleware_data.get("bot") bot: Bot = dialog_manager.middleware_data.get("bot")
room_id = dialog_manager.dialog_data.get("selected_room_id") room_id = dialog_manager.dialog_data.get("selected_room_id")
@@ -405,27 +386,26 @@ async def on_room_add_hours_confirm(
admin_id = callback.from_user.id admin_id = callback.from_user.id
if room_id and hours: if room_id and hours:
transaction, _ = await room_transactions_repository.add_hours( results = await transactions_repository.add_hours_to_room(
room_id=room_id, room_id=room_id,
amount=hours, amount=hours,
admin_id=admin_id, admin_id=admin_id,
is_active=True, is_active=True,
remark=remark,
) )
# Отправляем уведомления всем проживающим for transaction, resident in results:
residents = await residents_repository.get_residents_by_room(room_id) if resident and resident.user_entity:
for resident in residents:
if resident.user_entity:
user = await users_repository.get_user_by_id(resident.user_entity) user = await users_repository.get_user_by_id(resident.user_entity)
if user: if user:
try: try:
remark_text = f"\n💬 <i>{remark}</i>" if remark else "" remark_text = f"\n💬 <i>{remark}</i>" if remark else ""
await bot.send_message( await bot.send_message(
user.id, user.id,
f"<blockquote>📢 <b>Уведомление</b></blockquote>\n\n" f"<blockquote> <b>Уведомление</b></blockq uote>\n\n"
f"Вашей комнате начислено <b>+{hours}</b> ч{remark_text}" f"Вашей комнате начислено <b>+{hours}</b> ч{remark_text}"
) )
except Exception: except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
pass pass
await dialog_manager.switch_to(AdminMenuSG.room_info) await dialog_manager.switch_to(AdminMenuSG.room_info)
@@ -436,13 +416,12 @@ async def on_room_remove_hours_confirm(
callback: CallbackQuery, callback: CallbackQuery,
button: Button, button: Button,
dialog_manager: DialogManager, dialog_manager: DialogManager,
room_transactions_repository: FromDishka[RoomHoursTransactionsRepository], transactions_repository: FromDishka[HoursTransactionsRepository],
rooms_repository: FromDishka[RoomsRepository], rooms_repository: FromDishka[RoomsRepository],
residents_repository: FromDishka[ResidentsRepository], residents_repository: FromDishka[ResidentsRepository],
users_repository: FromDishka[UsersRepository], users_repository: FromDishka[UsersRepository],
**kwargs, **kwargs,
): ):
from aiogram import Bot
bot: Bot = dialog_manager.middleware_data.get("bot") bot: Bot = dialog_manager.middleware_data.get("bot")
room_id = dialog_manager.dialog_data.get("selected_room_id") room_id = dialog_manager.dialog_data.get("selected_room_id")
@@ -451,25 +430,26 @@ async def on_room_remove_hours_confirm(
admin_id = callback.from_user.id admin_id = callback.from_user.id
if room_id and hours: if room_id and hours:
room = await rooms_repository.get_room_by_id(room_id) residents = await residents_repository.get_residents_by_room(room_id)
if room and room.active_hours < hours:
for resident in residents:
if resident.active_hours < hours:
await callback.answer( await callback.answer(
f"⚠️ Недостаточно часов! У комнаты {room.active_hours} неотработанных ч, а вы пытаетесь отнять {hours} ч", f"⚠️ Недостаточно часов! У резидента {resident.real_name or 'без имени'} только {resident.active_hours} неотработанных ч, а вы пытаетесь отнять {hours} ч",
show_alert=True show_alert=True
) )
await dialog_manager.switch_to(AdminMenuSG.room_info) await dialog_manager.switch_to(AdminMenuSG.room_info)
return return
await room_transactions_repository.move_hours_to_completed( results = await transactions_repository.remove_hours_from_room(
room_id=room_id, room_id=room_id,
amount=hours, amount=hours,
admin_id=admin_id, admin_id=admin_id,
remark=remark,
) )
# Отправляем уведомления всем проживающим for transaction, resident in results:
residents = await residents_repository.get_residents_by_room(room_id) if resident and resident.user_entity:
for resident in residents:
if resident.user_entity:
user = await users_repository.get_user_by_id(resident.user_entity) user = await users_repository.get_user_by_id(resident.user_entity)
if user: if user:
try: try:
@@ -479,7 +459,7 @@ async def on_room_remove_hours_confirm(
f"<blockquote>📢 <b>Уведомление</b></blockquote>\n\n" f"<blockquote>📢 <b>Уведомление</b></blockquote>\n\n"
f"С вашей комнаты списано <b>-{hours}</b> ч{remark_text}" f"С вашей комнаты списано <b>-{hours}</b> ч{remark_text}"
) )
except Exception: except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
pass pass
await dialog_manager.switch_to(AdminMenuSG.room_info) await dialog_manager.switch_to(AdminMenuSG.room_info)
@@ -538,7 +518,7 @@ async def on_delete_room_cancel(
async def get_room_history_data( async def get_room_history_data(
dialog_manager: DialogManager, dialog_manager: DialogManager,
rooms_repository: FromDishka[RoomsRepository], rooms_repository: FromDishka[RoomsRepository],
room_transactions_repository: FromDishka[RoomHoursTransactionsRepository], transactions_repository: FromDishka[HoursTransactionsRepository],
**kwargs, **kwargs,
): ):
room_id = dialog_manager.dialog_data.get("selected_room_id") room_id = dialog_manager.dialog_data.get("selected_room_id")
@@ -551,25 +531,13 @@ async def get_room_history_data(
if not room: if not room:
return {"history_content": "Ошибка: комната не найдена"} return {"history_content": "Ошибка: комната не найдена"}
transactions = await room_transactions_repository.get_room_history(room_id) transactions = await transactions_repository.get_room_transactions(room_id)
transactions_sorted = sorted(transactions, key=lambda x: x.created_at) last_10 = transactions[:10]
last_10 = transactions_sorted[-10:]
if not last_10: if not last_10:
history_text = f""" history_text = f"<blockquote>📜 <b>История операций</b></blockquote>\n\n<b>Комната:</b> {room.number}\n\n<i>История операций пуста</i>"
<blockquote>📜 <b>История операций</b></blockquote>
<b>Комната:</b> {room.number}
<i>История операций пуста</i>
"""
else: else:
history_text = f""" history_text = f"<blockquote>📜 <b>История операций</b></blockquote>\n\n<b>Комната:</b> {room.number}\n\n"
<blockquote>📜 <b>История операций</b></blockquote>
<b>Комната:</b> {room.number}
"""
for tx in last_10: for tx in last_10:
operation = "Начислено" if tx.transaction_type == "increase" else "Списано" operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
emoji = "+" if tx.transaction_type == "increase" else "" emoji = "+" if tx.transaction_type == "increase" else ""
@@ -577,7 +545,7 @@ async def get_room_history_data(
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None) msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
date_str = msk_time.strftime("%d.%m.%Y %H:%M") date_str = msk_time.strftime("%d.%m.%Y %H:%M")
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else "" remark_text = f"\n💬 <i>{tx.remark}<t/i>" if tx.remark else ""
history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч\n📅 {date_str}{remark_text}</blockquote>\n" history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч\n📅 {date_str}{remark_text}</blockquote>\n"
@@ -15,9 +15,6 @@ from dutylog.infrastructure.database.repositories.rooms_repository import (
from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
HoursTransactionsRepository, HoursTransactionsRepository,
) )
from dutylog.infrastructure.database.repositories.room_hours_transactions_repository import (
RoomHoursTransactionsRepository,
)
from dutylog.infrastructure.utils.datetime import msk_now from dutylog.infrastructure.utils.datetime import msk_now
@@ -27,38 +24,21 @@ async def get_history_data(
residents_repository: FromDishka[ResidentsRepository], residents_repository: FromDishka[ResidentsRepository],
rooms_repository: FromDishka[RoomsRepository], rooms_repository: FromDishka[RoomsRepository],
transactions_repository: FromDishka[HoursTransactionsRepository], transactions_repository: FromDishka[HoursTransactionsRepository],
room_transactions_repository: FromDishka[RoomHoursTransactionsRepository],
**kwargs, **kwargs,
) -> dict[str, str]: ) -> dict[str, str]:
resident = await residents_repository.get_resident_by_user_id(event_from_user.id) resident = await residents_repository.get_resident_by_user_id(event_from_user.id)
if not resident: if not resident:
history_text = """ history_text = "<blockquote>📜 <b>История операций</b></blockquote>\n\n⚠️ <i>Профиль не найден</i>"
<blockquote>📜 <b>История операций</b></blockquote>
⚠️ <i>Профиль не найден</i>
"""
else: else:
# История резидента
transactions = await transactions_repository.get_resident_history(resident.id) transactions = await transactions_repository.get_resident_history(resident.id)
transactions_sorted = sorted(transactions, key=lambda x: x.created_at) transactions_sorted = sorted(transactions, key=lambda x: x.created_at)
last_10 = transactions_sorted[-10:] last_10 = transactions_sorted[-10:]
if not last_10: if not last_10:
history_text = """ history_text = "📜 <b>История операций</b>\n\n<b>👤 Ваши операции:</b>\n<i>История операций пуста</i>\n\n"
📜 <b>История операций</b>
<b>👤 Ваши операции:</b>
<i>История операций пуста</i>
"""
else: else:
history_text = """ history_text = "📜 <b>История операций</b>\n\n<b>👤 Ваши операции:</b>\n\n"
📜 <b>История операций</b>
<b>👤 Ваши операции:</b>
"""
for tx in last_10: for tx in last_10:
operation = "Начислено" if tx.transaction_type == "increase" else "Списано" operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
emoji = "+" if tx.transaction_type == "increase" else "" emoji = "+" if tx.transaction_type == "increase" else ""
@@ -68,35 +48,9 @@ async def get_history_data(
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else "" remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else ""
history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч\n📅 {date_str}{remark_text}</blockquote>\n" room_mark = " 🚪" if tx.per_room else ""
# История комнаты history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч{room_mark}\n📅 {date_str}{remark_text}</blockquote>\n"
room = await rooms_repository.get_room_by_id(resident.room)
if room:
room_transactions = await room_transactions_repository.get_room_history(room.id)
room_transactions_sorted = sorted(room_transactions, key=lambda x: x.created_at)
last_10_room = room_transactions_sorted[-10:]
if not last_10_room:
history_text += """
<b>🚪 Операции комнаты:</b>
<i>История операций пуста</i>
"""
else:
history_text += """
<b>🚪 Операции комнаты:</b>
"""
for tx in last_10_room:
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
emoji = "+" if tx.transaction_type == "increase" else ""
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else ""
history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч\n📅 {date_str}{remark_text}</blockquote>\n"
return {"history_content": history_text} return {"history_content": history_text}
@@ -63,44 +63,20 @@ async def get_main_menu_data(
) )
if not resident: if not resident:
content = f""" content = f"{greeting}\n\n<blockquote>⚠️ <b>Профиль не найден</b></blockquote>\n\nВы еще не привязаны к резиденту.\nОбратитесь к администратору для регистрации."
{greeting}
<blockquote>⚠️ <b>Профиль не найден</b></blockquote>
Вы еще не привязаны к резиденту.
Обратитесь к администратору для регистрации.
"""
has_resident = False has_resident = False
else: else:
room = await rooms_repository.get_room_by_id(resident.room) room = await rooms_repository.get_room_by_id(resident.room)
room_active = room.active_hours if room else 0 room_number = room.number if room else "???"
room_inactive = room.inactive_hours if room else 0
content = f""" room_residents = await residents_repository.get_residents_by_room(resident.room)
{greeting} room_active = sum(r.active_hours for r in room_residents)
room_inactive = sum(r.inactive_hours for r in room_residents)
📊 <i>Статус отработки:</i> content = f"{greeting}\n\n📊 <i>Статус отработки:</i>\n\n<b>👤 Ваши часы:</b>\n<blockquote>✅ Выполнено: <b>{resident.inactive_hours}</b> ч.\n⏳ Осталось: <b>{resident.active_hours}</b> ч.</blockquote>\n\n<b>🚪 Часы комнаты:</b>\n<blockquote>✅ Выполнено: <b>{room_inactive}</b> ч.\n⏳ Осталось: <b>{room_active}</b> ч.</blockquote>\n\n<code>made by kolo</code>"
<b>👤 Ваши часы:</b>
<blockquote>✅ Выполнено: <b>{resident.inactive_hours}</b> ч.
⏳ Осталось: <b>{resident.active_hours}</b> ч.</blockquote>
<b>🚪 Часы комнаты:</b>
<blockquote>✅ Выполнено: <b>{room_inactive}</b> ч.
⏳ Осталось: <b>{room_active}</b> ч.</blockquote>
<code>made by kolo</code>
"""
has_resident = True has_resident = True
else: else:
content = f""" content = f"{greeting}\n\n<blockquote>📋 <b>Панель управления</b></blockquote>\n\nДобро пожаловать в систему учета дежурств!"
{greeting}
<blockquote>📋 <b>Панель управления</b></blockquote>
Добро пожаловать в систему учета дежурств!
"""
has_resident = False has_resident = False
return { return {
@@ -1,60 +0,0 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from dutylog.infrastructure.database.models.room_hours_transaction import RoomHoursTransaction
class RoomHoursTransactionsDAO:
def __init__(self, session: AsyncSession):
self.session = session
async def get_by_id(self, transaction_id: int) -> RoomHoursTransaction | None:
result = await self.session.execute(
select(RoomHoursTransaction).where(RoomHoursTransaction.id == transaction_id)
)
return result.scalar_one_or_none()
async def get_by_room_id(self, room_id: int) -> list[RoomHoursTransaction]:
result = await self.session.execute(
select(RoomHoursTransaction)
.where(RoomHoursTransaction.room_id == room_id)
.order_by(RoomHoursTransaction.created_at.desc())
)
return list(result.scalars().all())
async def get_all(self) -> list[RoomHoursTransaction]:
result = await self.session.execute(select(RoomHoursTransaction))
return list(result.scalars().all())
async def create(self, transaction: RoomHoursTransaction) -> RoomHoursTransaction:
self.session.add(transaction)
await self.session.commit()
await self.session.refresh(transaction)
return transaction
async def get_by_period(self, start_date, end_date) -> list[RoomHoursTransaction]:
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(RoomHoursTransaction)
.where(RoomHoursTransaction.created_at >= start_datetime)
.where(RoomHoursTransaction.created_at <= end_datetime)
.order_by(RoomHoursTransaction.created_at.asc())
)
return list(result.scalars().all())
async def get_by_period(self, start_date, end_date) -> list[RoomHoursTransaction]:
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(RoomHoursTransaction)
.where(RoomHoursTransaction.created_at >= start_datetime)
.where(RoomHoursTransaction.created_at <= end_datetime)
.order_by(RoomHoursTransaction.created_at.asc())
)
return list(result.scalars().all())
@@ -1,10 +1,9 @@
from dutylog.infrastructure.database.models.base import Base from dutylog.infrastructure.database.models.base import Base
from dutylog.infrastructure.database.models.user import User from dutylog.infrastructure.database.models.user import User
from dutylog.infrastructure.database.models.hours_transaction import HoursTransaction from dutylog.infrastructure.database.models.hours_transaction import HoursTransaction
from dutylog.infrastructure.database.models.room_hours_transaction import RoomHoursTransaction
from dutylog.infrastructure.database.models.room import Room from dutylog.infrastructure.database.models.room import Room
from dutylog.infrastructure.database.models.resident import Resident from dutylog.infrastructure.database.models.resident import Resident
from dutylog.infrastructure.database.models.floor import Floor from dutylog.infrastructure.database.models.floor import Floor
from dutylog.infrastructure.database.models.reporting_period import ReportingPeriod from dutylog.infrastructure.database.models.reporting_period import ReportingPeriod
__all__ = ["Base", "User", "HoursTransaction", "RoomHoursTransaction", "Room", "Resident", "Floor", "ReportingPeriod"] __all__ = ["Base", "User", "HoursTransaction", "Room", "Resident", "Floor", "ReportingPeriod"]
@@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from sqlalchemy import BigInteger, Integer, String, DateTime, ForeignKey from sqlalchemy import BigInteger, Boolean, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from dutylog.infrastructure.database.models.base import Base from dutylog.infrastructure.database.models.base import Base
@@ -26,6 +26,9 @@ class HoursTransaction(Base):
BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
) )
remark: Mapped[str | None] = mapped_column(String(500), nullable=True) remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
per_room: Mapped[bool] = mapped_column(
Boolean, default=False, server_default="false"
)
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=msk_now DateTime(timezone=True), default=msk_now
) )
@@ -12,5 +12,3 @@ class Room(Base):
on_floor: Mapped[int] = mapped_column( on_floor: Mapped[int] = mapped_column(
Integer, ForeignKey("floors.id", ondelete="CASCADE"), nullable=False Integer, ForeignKey("floors.id", ondelete="CASCADE"), nullable=False
) )
active_hours: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
inactive_hours: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
@@ -1,25 +0,0 @@
from datetime import datetime
from sqlalchemy import BigInteger, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from dutylog.infrastructure.database.models.base import Base
from dutylog.infrastructure.utils.datetime import msk_now
class RoomHoursTransaction(Base):
__tablename__ = "room_hours_transactions"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
room_id: Mapped[int] = mapped_column(
Integer, ForeignKey("rooms.id", ondelete="CASCADE"), nullable=False
)
transaction_type: Mapped[str] = mapped_column(String(50), nullable=False)
amount: Mapped[int] = mapped_column(Integer, nullable=False)
admin_id: Mapped[int] = mapped_column(
BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=msk_now
)
@@ -25,6 +25,7 @@ class HoursTransactionsRepository:
admin_id: int | None = None, admin_id: int | None = None,
is_active: bool = True, is_active: bool = True,
remark: str | None = None, remark: str | None = None,
per_room: bool = False,
) -> tuple[HoursTransaction, Resident | None]: ) -> tuple[HoursTransaction, Resident | None]:
transaction = HoursTransaction( transaction = HoursTransaction(
resident_id=resident_id, resident_id=resident_id,
@@ -32,6 +33,7 @@ class HoursTransactionsRepository:
amount=amount, amount=amount,
admin_id=admin_id, admin_id=admin_id,
remark=remark, remark=remark,
per_room=per_room,
) )
transaction = await self.transactions_dao.create(transaction) transaction = await self.transactions_dao.create(transaction)
@@ -57,6 +59,7 @@ class HoursTransactionsRepository:
admin_id: int | None = None, admin_id: int | None = None,
is_active: bool = True, is_active: bool = True,
remark: str | None = None, remark: str | None = None,
per_room: bool = False,
) -> tuple[HoursTransaction, Resident | None]: ) -> tuple[HoursTransaction, Resident | None]:
transaction = HoursTransaction( transaction = HoursTransaction(
resident_id=resident_id, resident_id=resident_id,
@@ -64,6 +67,7 @@ class HoursTransactionsRepository:
amount=amount, amount=amount,
admin_id=admin_id, admin_id=admin_id,
remark=remark, remark=remark,
per_room=per_room,
) )
transaction = await self.transactions_dao.create(transaction) transaction = await self.transactions_dao.create(transaction)
@@ -88,6 +92,7 @@ class HoursTransactionsRepository:
amount: int, amount: int,
admin_id: int | None = None, admin_id: int | None = None,
remark: str | None = None, remark: str | None = None,
per_room: bool = False,
) -> tuple[HoursTransaction, Resident | None]: ) -> tuple[HoursTransaction, Resident | None]:
"""Перемещает часы из неотработанных в отработанные""" """Перемещает часы из неотработанных в отработанные"""
transaction = HoursTransaction( transaction = HoursTransaction(
@@ -96,6 +101,7 @@ class HoursTransactionsRepository:
amount=amount, amount=amount,
admin_id=admin_id, admin_id=admin_id,
remark=remark, remark=remark,
per_room=per_room,
) )
transaction = await self.transactions_dao.create(transaction) transaction = await self.transactions_dao.create(transaction)
@@ -124,3 +130,65 @@ class HoursTransactionsRepository:
async def get_by_period(self, start_date, end_date) -> list[HoursTransaction]: async def get_by_period(self, start_date, end_date) -> list[HoursTransaction]:
return await self.transactions_dao.get_by_period(start_date, end_date) return await self.transactions_dao.get_by_period(start_date, end_date)
async def add_hours_to_room(
self,
room_id: int,
amount: int,
admin_id: int | None = None,
is_active: bool = True,
remark: str | None = None,
) -> list[tuple[HoursTransaction, Resident | None]]:
"""Начисляет часы всем резидентам комнаты с флагом per_room=True"""
residents = await self.residents_dao.get_by_room(room_id)
results = []
for resident in residents:
result = await self.add_hours(
resident_id=resident.id,
amount=amount,
admin_id=admin_id,
is_active=is_active,
remark=remark,
per_room=True,
)
results.append(result)
return results
async def remove_hours_from_room(
self,
room_id: int,
amount: int,
admin_id: int | None = None,
remark: str | None = None,
) -> list[tuple[HoursTransaction, Resident | None]]:
"""Списывает часы у всех резидентов комнаты с флагом per_room=True"""
residents = await self.residents_dao.get_by_room(room_id)
results = []
for resident in residents:
result = await self.move_hours_to_completed(
resident_id=resident.id,
amount=amount,
admin_id=admin_id,
remark=remark,
per_room=True,
)
results.append(result)
return results
async def get_room_transactions(self, room_id: int) -> list[HoursTransaction]:
"""Получает все транзакции резидентов комнаты с флагом per_room=True"""
residents = await self.residents_dao.get_by_room(room_id)
all_transactions = []
for resident in residents:
transactions = await self.transactions_dao.get_by_resident_id(resident.id)
room_transactions = [t for t in transactions if t.per_room]
all_transactions.extend(room_transactions)
# Сортируем по дате создания
all_transactions.sort(key=lambda t: t.created_at, reverse=True)
return all_transactions
@@ -1,122 +0,0 @@
from dutylog.infrastructure.database.dao.room_hours_transactions_dao import (
RoomHoursTransactionsDAO,
)
from dutylog.infrastructure.database.dao.rooms_dao import RoomsDAO
from dutylog.infrastructure.database.models.room_hours_transaction import (
RoomHoursTransaction,
)
from dutylog.infrastructure.database.models.hours_transaction import TransactionType
from dutylog.infrastructure.database.models.room import Room
class RoomHoursTransactionsRepository:
def __init__(
self,
transactions_dao: RoomHoursTransactionsDAO,
rooms_dao: RoomsDAO,
):
self.transactions_dao = transactions_dao
self.rooms_dao = rooms_dao
async def add_hours(
self,
room_id: int,
amount: int,
admin_id: int | None = None,
is_active: bool = True,
) -> tuple[RoomHoursTransaction, Room | None]:
transaction = RoomHoursTransaction(
room_id=room_id,
transaction_type=TransactionType.INCREASE.value,
amount=amount,
admin_id=admin_id,
)
transaction = await self.transactions_dao.create(transaction)
room = await self.rooms_dao.get_by_id(room_id)
if room:
if is_active:
new_hours = room.active_hours + amount
room = await self.rooms_dao.update(
room_id, active_hours=new_hours
)
else:
new_hours = room.inactive_hours + amount
room = await self.rooms_dao.update(
room_id, inactive_hours=new_hours
)
return transaction, room
async def remove_hours(
self,
room_id: int,
amount: int,
admin_id: int | None = None,
is_active: bool = True,
) -> tuple[RoomHoursTransaction, Room | None]:
transaction = RoomHoursTransaction(
room_id=room_id,
transaction_type=TransactionType.DECREASE.value,
amount=amount,
admin_id=admin_id,
)
transaction = await self.transactions_dao.create(transaction)
room = await self.rooms_dao.get_by_id(room_id)
if room:
if is_active:
new_hours = max(0, room.active_hours - amount)
room = await self.rooms_dao.update(
room_id, active_hours=new_hours
)
else:
new_hours = max(0, room.inactive_hours - amount)
room = await self.rooms_dao.update(
room_id, inactive_hours=new_hours
)
return transaction, room
async def move_hours_to_completed(
self,
room_id: int,
amount: int,
admin_id: int | None = None,
) -> tuple[RoomHoursTransaction, Room | None]:
"""Перемещает часы из неотработанных в отработанные"""
transaction = RoomHoursTransaction(
room_id=room_id,
transaction_type=TransactionType.DECREASE.value,
amount=amount,
admin_id=admin_id,
)
transaction = await self.transactions_dao.create(transaction)
room = await self.rooms_dao.get_by_id(room_id)
if room:
new_active = max(0, room.active_hours - amount)
new_inactive = room.inactive_hours + amount
room = await self.rooms_dao.update(
room_id,
active_hours=new_active,
inactive_hours=new_inactive
)
return transaction, room
async def get_room_history(self, room_id: int) -> list[RoomHoursTransaction]:
return await self.transactions_dao.get_by_room_id(room_id)
async def get_all_transactions(self) -> list[RoomHoursTransaction]:
return await self.transactions_dao.get_all()
async def get_transaction_by_id(
self, transaction_id: int
) -> RoomHoursTransaction | None:
return await self.transactions_dao.get_by_id(transaction_id)
async def get_by_period(self, start_date, end_date) -> list[RoomHoursTransaction]:
return await self.transactions_dao.get_by_period(start_date, end_date)
async def get_by_period(self, start_date, end_date) -> list[RoomHoursTransaction]:
return await self.transactions_dao.get_by_period(start_date, end_date)
-20
View File
@@ -8,9 +8,6 @@ from dutylog.infrastructure.database.dao.users_dao import UsersDAO
from dutylog.infrastructure.database.dao.hours_transactions_dao import ( from dutylog.infrastructure.database.dao.hours_transactions_dao import (
HoursTransactionsDAO, HoursTransactionsDAO,
) )
from dutylog.infrastructure.database.dao.room_hours_transactions_dao import (
RoomHoursTransactionsDAO,
)
from dutylog.infrastructure.database.dao.rooms_dao import RoomsDAO from dutylog.infrastructure.database.dao.rooms_dao import RoomsDAO
from dutylog.infrastructure.database.dao.residents_dao import ResidentsDAO from dutylog.infrastructure.database.dao.residents_dao import ResidentsDAO
from dutylog.infrastructure.database.dao.floors_dao import FloorsDAO from dutylog.infrastructure.database.dao.floors_dao import FloorsDAO
@@ -23,9 +20,6 @@ from dutylog.infrastructure.database.repositories.users_repository import (
from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
HoursTransactionsRepository, HoursTransactionsRepository,
) )
from dutylog.infrastructure.database.repositories.room_hours_transactions_repository import (
RoomHoursTransactionsRepository,
)
from dutylog.infrastructure.database.repositories.rooms_repository import ( from dutylog.infrastructure.database.repositories.rooms_repository import (
RoomsRepository, RoomsRepository,
) )
@@ -76,10 +70,6 @@ class DAOProvider(Provider):
def get_hours_transactions_dao(self, session: AsyncSession) -> HoursTransactionsDAO: def get_hours_transactions_dao(self, session: AsyncSession) -> HoursTransactionsDAO:
return HoursTransactionsDAO(session) return HoursTransactionsDAO(session)
@provide(scope=Scope.REQUEST)
def get_room_hours_transactions_dao(self, session: AsyncSession) -> RoomHoursTransactionsDAO:
return RoomHoursTransactionsDAO(session)
@provide(scope=Scope.REQUEST) @provide(scope=Scope.REQUEST)
def get_rooms_dao(self, session: AsyncSession) -> RoomsDAO: def get_rooms_dao(self, session: AsyncSession) -> RoomsDAO:
return RoomsDAO(session) return RoomsDAO(session)
@@ -110,14 +100,6 @@ class RepositoryProvider(Provider):
) -> HoursTransactionsRepository: ) -> HoursTransactionsRepository:
return HoursTransactionsRepository(transactions_dao, residents_dao) return HoursTransactionsRepository(transactions_dao, residents_dao)
@provide(scope=Scope.REQUEST)
def get_room_hours_transactions_repository(
self,
transactions_dao: RoomHoursTransactionsDAO,
rooms_dao: RoomsDAO,
) -> RoomHoursTransactionsRepository:
return RoomHoursTransactionsRepository(transactions_dao, rooms_dao)
@provide(scope=Scope.REQUEST) @provide(scope=Scope.REQUEST)
def get_rooms_repository(self, rooms_dao: RoomsDAO) -> RoomsRepository: def get_rooms_repository(self, rooms_dao: RoomsDAO) -> RoomsRepository:
return RoomsRepository(rooms_dao) return RoomsRepository(rooms_dao)
@@ -144,7 +126,6 @@ class ServiceProvider(Provider):
def get_report_service( def get_report_service(
self, self,
hours_transactions_repository: HoursTransactionsRepository, hours_transactions_repository: HoursTransactionsRepository,
room_hours_transactions_repository: RoomHoursTransactionsRepository,
residents_repository: ResidentsRepository, residents_repository: ResidentsRepository,
rooms_repository: RoomsRepository, rooms_repository: RoomsRepository,
floors_repository: FloorsRepository, floors_repository: FloorsRepository,
@@ -152,7 +133,6 @@ class ServiceProvider(Provider):
) -> ReportService: ) -> ReportService:
return ReportService( return ReportService(
hours_transactions_repository, hours_transactions_repository,
room_hours_transactions_repository,
residents_repository, residents_repository,
rooms_repository, rooms_repository,
floors_repository, floors_repository,
+7 -113
View File
@@ -10,9 +10,6 @@ from dutylog.infrastructure.database.repositories.floors_repository import (
from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
HoursTransactionsRepository, HoursTransactionsRepository,
) )
from dutylog.infrastructure.database.repositories.room_hours_transactions_repository import (
RoomHoursTransactionsRepository,
)
from dutylog.infrastructure.database.repositories.residents_repository import ( from dutylog.infrastructure.database.repositories.residents_repository import (
ResidentsRepository, ResidentsRepository,
) )
@@ -30,27 +27,26 @@ class ReportService:
def __init__( def __init__(
self, self,
hours_transactions_repository: HoursTransactionsRepository, hours_transactions_repository: HoursTransactionsRepository,
room_hours_transactions_repository: RoomHoursTransactionsRepository,
residents_repository: ResidentsRepository, residents_repository: ResidentsRepository,
rooms_repository: RoomsRepository, rooms_repository: RoomsRepository,
floors_repository: FloorsRepository, floors_repository: FloorsRepository,
users_repository: UsersRepository, users_repository: UsersRepository,
): ):
self.hours_transactions_repository = hours_transactions_repository self.hours_transactions_repository = hours_transactions_repository
self.room_hours_transactions_repository = room_hours_transactions_repository
self.residents_repository = residents_repository self.residents_repository = residents_repository
self.rooms_repository = rooms_repository self.rooms_repository = rooms_repository
self.floors_repository = floors_repository self.floors_repository = floors_repository
self.users_repository = users_repository self.users_repository = users_repository
async def generate_period_report(self, start_date: date, end_date: date) -> BytesIO: async def generate_period_report(self, start_date: date, end_date: date) -> BytesIO:
resident_transactions = await self.hours_transactions_repository.get_by_period( all_transactions = await self.hours_transactions_repository.get_by_period(
start_date, end_date
)
room_transactions = await self.room_hours_transactions_repository.get_by_period(
start_date, end_date start_date, end_date
) )
# Разделяем транзакции на личные и комнатные
resident_transactions = [t for t in all_transactions if not t.per_room]
room_transactions = [t for t in all_transactions if t.per_room]
resident_increase = [ resident_increase = [
t for t in resident_transactions if t.transaction_type == "increase" t for t in resident_transactions if t.transaction_type == "increase"
] ]
@@ -71,8 +67,8 @@ class ReportService:
await self._create_resident_sheet(wb, "Резиденты - Начисления", resident_increase, start_date, end_date) await self._create_resident_sheet(wb, "Резиденты - Начисления", resident_increase, start_date, end_date)
await self._create_resident_sheet(wb, "Резиденты - Списания", resident_decrease, start_date, end_date) await self._create_resident_sheet(wb, "Резиденты - Списания", resident_decrease, start_date, end_date)
await self._create_room_sheet(wb, "Комнаты - Начисления", room_increase, start_date, end_date) await self._create_resident_sheet(wb, "Комнаты - Начисления", room_increase, start_date, end_date)
await self._create_room_sheet(wb, "Комнаты - Списания", room_decrease, start_date, end_date) await self._create_resident_sheet(wb, "Комнаты - Списания", room_decrease, start_date, end_date)
output = BytesIO() output = BytesIO()
wb.save(output) wb.save(output)
@@ -182,105 +178,3 @@ class ReportService:
ws.column_dimensions["D"].width = 10 ws.column_dimensions["D"].width = 10
ws.column_dimensions["E"].width = 15 ws.column_dimensions["E"].width = 15
ws.column_dimensions["F"].width = 30 ws.column_dimensions["F"].width = 30
async def _create_room_sheet(self, wb: Workbook, title: str, transactions, start_date: date, end_date: date):
ws = wb.create_sheet(title=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"{title} за период {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: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")
ws.row_dimensions[1].height = 25
row_num = 3
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 = 0
for transaction in transactions:
room = await self.rooms_repository.get_by_id(transaction.room_id)
if not room:
continue
floor = await self.floors_repository.get_by_id(room.on_floor)
floor_number = floor.number if floor 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=msk_time).border = border
ws.cell(row=row_num, column=1).number_format = "DD.MM.YYYY HH:MM"
ws.cell(row=row_num, column=2, value=room.number).border = border
ws.cell(row=row_num, column=3, value=floor_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=6, value=transaction.remark or "").border = border
ws.cell(row=row_num, column=1).alignment = Alignment(horizontal="center")
ws.cell(row=row_num, column=2).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 += 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).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.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 = 10
ws.column_dimensions["C"].width = 10
ws.column_dimensions["D"].width = 10
ws.column_dimensions["E"].width = 15
ws.column_dimensions["F"].width = 30