diff --git a/alembic/versions/4fe7f71301e7_add_per_room_field_and_remove_room_.py b/alembic/versions/4fe7f71301e7_add_per_room_field_and_remove_room_.py new file mode 100644 index 0000000..1c2e230 --- /dev/null +++ b/alembic/versions/4fe7f71301e7_add_per_room_field_and_remove_room_.py @@ -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') diff --git a/src/dutylog/application/bot/admin_dialogs/hours_management.py b/src/dutylog/application/bot/admin_dialogs/hours_management.py index 2163328..1c5389e 100644 --- a/src/dutylog/application/bot/admin_dialogs/hours_management.py +++ b/src/dutylog/application/bot/admin_dialogs/hours_management.py @@ -1,6 +1,6 @@ from aiogram import Bot +from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter from aiogram.types import Message, CallbackQuery -from aiogram.exceptions import TelegramForbiddenError from aiogram_dialog import Window, DialogManager from aiogram_dialog.widgets.text import Format, Const 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_username = f"@{admin.username}" if admin and admin.username else "Администратор" - notification_text = ( - f"
➕ Начислены часы\n\n" - f"Количество:
{hours} ч\n"
- f"Причина: {remark}\n"
- f"Администратор: {admin_username}\n\n"
- f"Всего неотработанных часов: {resident.active_hours} ч"
- )
+ notification_text = f"➕ Начислены часы\n\nКоличество:
{hours} ч\nПричина: {remark}\nАдминистратор: {admin_username}\n\nВсего неотработанных часов: {resident.active_hours} ч"
try:
await bot.send_message(resident.user_entity, notification_text)
- except TelegramForbiddenError:
+ except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
pass
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 "Администратор"
if resident.active_hours == 0:
- notification_text = (
- f"🎉 Поздравляем!\n\n" - f"Вы отработали все часы! Теперь у вас
0 неотработанных часов.\n\n"
- f"Списано: {hours} ч\n"
- )
+ notification_text = f"🎉 Поздравляем!\n\nВы отработали все часы! Теперь у вас
0 неотработанных часов.\n\nСписано: {hours} ч\n"
if remark:
notification_text += f"Причина: {remark}\n"
notification_text += f"Администратор: {admin_username}"
else:
- notification_text = (
- f"➖ Списаны часы\n\n" - f"Количество:
{hours} ч\n"
- )
+ notification_text = f"➖ Списаны часы\n\nКоличество:
{hours} ч\n"
if remark:
notification_text += f"Причина: {remark}\n"
- notification_text += (
- f"Администратор: {admin_username}\n\n"
- f"Осталось неотработанных часов: {resident.active_hours} ч"
- )
+ notification_text += f"Администратор: {admin_username}\n\nОсталось неотработанных часов: {resident.active_hours} ч"
try:
await bot.send_message(resident.user_entity, notification_text)
- except TelegramForbiddenError:
+ except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
pass
await dialog_manager.switch_to(AdminMenuSG.resident_info)
diff --git a/src/dutylog/application/bot/admin_dialogs/rooms_management.py b/src/dutylog/application/bot/admin_dialogs/rooms_management.py
index 9ec77eb..0180f13 100644
--- a/src/dutylog/application/bot/admin_dialogs/rooms_management.py
+++ b/src/dutylog/application/bot/admin_dialogs/rooms_management.py
@@ -1,3 +1,5 @@
+from aiogram import Bot
+from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
from aiogram.types import Message, CallbackQuery
from aiogram_dialog import Window, DialogManager
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 (
ResidentsRepository,
)
-from dutylog.infrastructure.database.repositories.room_hours_transactions_repository import (
- RoomHoursTransactionsRepository,
+from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
+ HoursTransactionsRepository,
)
from dutylog.infrastructure.database.repositories.users_repository import (
UsersRepository,
@@ -43,11 +45,7 @@ async def get_rooms_floors_data(
floors_data = [(f"🏢 Этаж {f.number}", f.id) for f in all_floors]
- content = """
-🚪 Комнаты- -Выберите этаж для просмотра комнат: -""" + content = "
🚪 Комнаты\n\nВыберите этаж для просмотра комнат:" return { "content": content, @@ -89,13 +87,7 @@ async def get_rooms_list_data( rooms_data = [(f"🚪 Комната {r.number}", r.id) for r in rooms] - content = f""" -
🚪 Комнаты на этаже {floor_number}- -Всего комнат:
{len(rooms)}
-
-Выберите комнату для просмотра:
-"""
+ content = f"🚪 Комнаты на этаже {floor_number}\n\nВсего комнат:
{len(rooms)}\n\nВыберите комнату для просмотра:"
return {
"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]
- content = """
-➕ Создание комнаты- -Выберите этаж для новой комнаты: -""" + content = "
➕ Создание комнаты\n\nВыберите этаж для новой комнаты:" return { "content": content, @@ -238,28 +226,22 @@ async def get_room_info_data( floor = await floors_repository.get_floor_by_id(room.on_floor) floor_number = floor.number if floor else "???" - # Получаем резидентов комнаты 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 = "" if residents: residents_info = "\nПроживающие:\n" for resident in residents: status = "🟢" if resident.is_busy else "⚪️" name = resident.real_name if resident.real_name else "Без имени" - residents_info += f"{status} {name}\n" + residents_info += f"{status} {name} (
{resident.active_hours} ч)\n"
else:
residents_info = "\nНет проживающих\n"
- info_content = f"""
-🚪 Информация о комнате- -Номер:
{room.number}
-Этаж: {floor_number}
-{residents_info}
-🟢 Отработанные часы: {room.inactive_hours} ч
-🔴 Неотработанные часы: {room.active_hours} ч
-"""
+ info_content = f"🚪 Информация о комнате\n\nНомер:
{room.number}\nЭтаж: {floor_number}\n{residents_info}\n🟢 Отработанные часы (всего): {total_inactive_hours} ч\n🔴 Неотработанные часы (всего): {total_active_hours} ч"
return {
"info_content": info_content,
@@ -391,12 +373,11 @@ async def on_room_add_hours_confirm(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
- room_transactions_repository: FromDishka[RoomHoursTransactionsRepository],
+ transactions_repository: FromDishka[HoursTransactionsRepository],
residents_repository: FromDishka[ResidentsRepository],
users_repository: FromDishka[UsersRepository],
**kwargs,
):
- from aiogram import Bot
bot: Bot = dialog_manager.middleware_data.get("bot")
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
if room_id and hours:
- transaction, _ = await room_transactions_repository.add_hours(
+ results = await transactions_repository.add_hours_to_room(
room_id=room_id,
amount=hours,
admin_id=admin_id,
is_active=True,
+ remark=remark,
)
- # Отправляем уведомления всем проживающим
- residents = await residents_repository.get_residents_by_room(room_id)
- for resident in residents:
- if resident.user_entity:
+ for transaction, resident in results:
+ if resident and resident.user_entity:
user = await users_repository.get_user_by_id(resident.user_entity)
if user:
try:
remark_text = f"\n💬 {remark}" if remark else ""
await bot.send_message(
user.id,
- f"📢 Уведомление\n\n" + f"
� Уведомление\n\n" f"Вашей комнате начислено +{hours} ч{remark_text}" ) - except Exception: + except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter): pass await dialog_manager.switch_to(AdminMenuSG.room_info) @@ -436,13 +416,12 @@ async def on_room_remove_hours_confirm( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, - room_transactions_repository: FromDishka[RoomHoursTransactionsRepository], + transactions_repository: FromDishka[HoursTransactionsRepository], rooms_repository: FromDishka[RoomsRepository], residents_repository: FromDishka[ResidentsRepository], users_repository: FromDishka[UsersRepository], **kwargs, ): - from aiogram import Bot bot: Bot = dialog_manager.middleware_data.get("bot") 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 if room_id and hours: - room = await rooms_repository.get_room_by_id(room_id) - if room and room.active_hours < hours: - await callback.answer( - f"⚠️ Недостаточно часов! У комнаты {room.active_hours} неотработанных ч, а вы пытаетесь отнять {hours} ч", - show_alert=True - ) - await dialog_manager.switch_to(AdminMenuSG.room_info) - return + residents = await residents_repository.get_residents_by_room(room_id) - await room_transactions_repository.move_hours_to_completed( + for resident in residents: + if resident.active_hours < hours: + await callback.answer( + f"⚠️ Недостаточно часов! У резидента {resident.real_name or 'без имени'} только {resident.active_hours} неотработанных ч, а вы пытаетесь отнять {hours} ч", + show_alert=True + ) + await dialog_manager.switch_to(AdminMenuSG.room_info) + return + + results = await transactions_repository.remove_hours_from_room( room_id=room_id, amount=hours, admin_id=admin_id, + remark=remark, ) - # Отправляем уведомления всем проживающим - residents = await residents_repository.get_residents_by_room(room_id) - for resident in residents: - if resident.user_entity: + for transaction, resident in results: + if resident and resident.user_entity: user = await users_repository.get_user_by_id(resident.user_entity) if user: try: @@ -479,7 +459,7 @@ async def on_room_remove_hours_confirm( f"📢 Уведомление\n\n" f"С вашей комнаты списано -{hours} ч{remark_text}" ) - except Exception: + except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter): pass await dialog_manager.switch_to(AdminMenuSG.room_info) @@ -538,7 +518,7 @@ async def on_delete_room_cancel( async def get_room_history_data( dialog_manager: DialogManager, rooms_repository: FromDishka[RoomsRepository], - room_transactions_repository: FromDishka[RoomHoursTransactionsRepository], + transactions_repository: FromDishka[HoursTransactionsRepository], **kwargs, ): room_id = dialog_manager.dialog_data.get("selected_room_id") @@ -551,25 +531,13 @@ async def get_room_history_data( if not room: return {"history_content": "Ошибка: комната не найдена"} - transactions = await room_transactions_repository.get_room_history(room_id) - transactions_sorted = sorted(transactions, key=lambda x: x.created_at) - last_10 = transactions_sorted[-10:] + transactions = await transactions_repository.get_room_transactions(room_id) + last_10 = transactions[:10] if not last_10: - history_text = f""" -📜 История операций- -Комната: {room.number} - -История операций пуста -""" + history_text = f"📜 История операций\n\nКомната: {room.number}\n\nИстория операций пуста" else: - history_text = f""" -📜 История операций- -Комната: {room.number} - -""" + history_text = f"📜 История операций\n\nКомната: {room.number}\n\n" for tx in last_10: operation = "Начислено" 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) date_str = msk_time.strftime("%d.%m.%Y %H:%M") - remark_text = f"\n💬 {tx.remark}" if tx.remark else "" + remark_text = f"\n💬 {tx.remark}" if tx.remark else "" history_text += f" {operation} {emoji}\n" diff --git a/src/dutylog/application/bot/user_dialogs/user_menu/history.py b/src/dutylog/application/bot/user_dialogs/user_menu/history.py index 4872868..786b33e 100644 --- a/src/dutylog/application/bot/user_dialogs/user_menu/history.py +++ b/src/dutylog/application/bot/user_dialogs/user_menu/history.py @@ -15,9 +15,6 @@ from dutylog.infrastructure.database.repositories.rooms_repository import ( from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( HoursTransactionsRepository, ) -from dutylog.infrastructure.database.repositories.room_hours_transactions_repository import ( - RoomHoursTransactionsRepository, -) from dutylog.infrastructure.utils.datetime import msk_now @@ -27,38 +24,21 @@ async def get_history_data( residents_repository: FromDishka[ResidentsRepository], rooms_repository: FromDishka[RoomsRepository], transactions_repository: FromDishka[HoursTransactionsRepository], - room_transactions_repository: FromDishka[RoomHoursTransactionsRepository], **kwargs, ) -> dict[str, str]: resident = await residents_repository.get_resident_by_user_id(event_from_user.id) if not resident: - history_text = """ -{tx.amount}ч\n📅 {date_str}{remark_text}📜 История операций- -⚠️ Профиль не найден -""" + history_text = "📜 История операций\n\n⚠️ Профиль не найден" else: - # История резидента transactions = await transactions_repository.get_resident_history(resident.id) transactions_sorted = sorted(transactions, key=lambda x: x.created_at) last_10 = transactions_sorted[-10:] if not last_10: - history_text = """ -📜 История операций - -👤 Ваши операции: -История операций пуста - -""" + history_text = "📜 История операций\n\n👤 Ваши операции:\nИстория операций пуста\n\n" else: - history_text = """ -📜 История операций - -👤 Ваши операции: - -""" + history_text = "📜 История операций\n\n👤 Ваши операции:\n\n" for tx in last_10: operation = "Начислено" 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💬 {tx.remark}" if tx.remark else "" - history_text += f"{operation} {emoji}\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 += """ -🚪 Операции комнаты: -История операций пуста -""" - else: - history_text += """ -🚪 Операции комнаты: - -""" - 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💬 {tx.remark}" if tx.remark else "" - - history_text += f"{tx.amount}ч\n📅 {date_str}{remark_text}{operation} {emoji}\n" + room_mark = " 🚪" if tx.per_room else "" + + history_text += f"{tx.amount}ч\n📅 {date_str}{remark_text}{operation} {emoji}\n" return {"history_content": history_text} diff --git a/src/dutylog/application/bot/user_dialogs/user_menu/main_menu.py b/src/dutylog/application/bot/user_dialogs/user_menu/main_menu.py index 4202071..b4c60ee 100644 --- a/src/dutylog/application/bot/user_dialogs/user_menu/main_menu.py +++ b/src/dutylog/application/bot/user_dialogs/user_menu/main_menu.py @@ -63,44 +63,20 @@ async def get_main_menu_data( ) if not resident: - content = f""" -{greeting} - -{tx.amount}ч{room_mark}\n📅 {date_str}{remark_text}⚠️ Профиль не найден- -Вы еще не привязаны к резиденту. -Обратитесь к администратору для регистрации. -""" + content = f"{greeting}\n\n⚠️ Профиль не найден\n\nВы еще не привязаны к резиденту.\nОбратитесь к администратору для регистрации." has_resident = False else: room = await rooms_repository.get_room_by_id(resident.room) - room_active = room.active_hours if room else 0 - room_inactive = room.inactive_hours if room else 0 + room_number = room.number if room else "???" - content = f""" -{greeting} - -📊 Статус отработки: - -👤 Ваши часы: -✅ Выполнено: {resident.inactive_hours} ч. -⏳ Осталось: {resident.active_hours} ч.- -🚪 Часы комнаты: -✅ Выполнено: {room_inactive} ч. -⏳ Осталось: {room_active} ч.- -made by kolo-""" + room_residents = await residents_repository.get_residents_by_room(resident.room) + room_active = sum(r.active_hours for r in room_residents) + room_inactive = sum(r.inactive_hours for r in room_residents) + + content = f"{greeting}\n\n📊 Статус отработки:\n\n👤 Ваши часы:\n✅ Выполнено: {resident.inactive_hours} ч.\n⏳ Осталось: {resident.active_hours} ч.\n\n🚪 Часы комнаты:\n✅ Выполнено: {room_inactive} ч.\n⏳ Осталось: {room_active} ч.\n\nmade by kolo" has_resident = True else: - content = f""" -{greeting} - -📋 Панель управления- -Добро пожаловать в систему учета дежурств! -""" + content = f"{greeting}\n\n📋 Панель управления\n\nДобро пожаловать в систему учета дежурств!" has_resident = False return { diff --git a/src/dutylog/infrastructure/database/dao/room_hours_transactions_dao.py b/src/dutylog/infrastructure/database/dao/room_hours_transactions_dao.py deleted file mode 100644 index dd7c9dd..0000000 --- a/src/dutylog/infrastructure/database/dao/room_hours_transactions_dao.py +++ /dev/null @@ -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()) diff --git a/src/dutylog/infrastructure/database/models/__init__.py b/src/dutylog/infrastructure/database/models/__init__.py index e112194..b130e72 100644 --- a/src/dutylog/infrastructure/database/models/__init__.py +++ b/src/dutylog/infrastructure/database/models/__init__.py @@ -1,10 +1,9 @@ from dutylog.infrastructure.database.models.base import Base from dutylog.infrastructure.database.models.user import User 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.resident import Resident from dutylog.infrastructure.database.models.floor import Floor 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"] diff --git a/src/dutylog/infrastructure/database/models/hours_transaction.py b/src/dutylog/infrastructure/database/models/hours_transaction.py index f9a3df7..c7328cb 100644 --- a/src/dutylog/infrastructure/database/models/hours_transaction.py +++ b/src/dutylog/infrastructure/database/models/hours_transaction.py @@ -1,7 +1,7 @@ from datetime import datetime 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 dutylog.infrastructure.database.models.base import Base @@ -26,6 +26,9 @@ class HoursTransaction(Base): BigInteger, ForeignKey("users.id", ondelete="SET NULL"), 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( DateTime(timezone=True), default=msk_now ) diff --git a/src/dutylog/infrastructure/database/models/room.py b/src/dutylog/infrastructure/database/models/room.py index 29f26c2..d19cae6 100644 --- a/src/dutylog/infrastructure/database/models/room.py +++ b/src/dutylog/infrastructure/database/models/room.py @@ -12,5 +12,3 @@ class Room(Base): on_floor: Mapped[int] = mapped_column( 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") diff --git a/src/dutylog/infrastructure/database/models/room_hours_transaction.py b/src/dutylog/infrastructure/database/models/room_hours_transaction.py deleted file mode 100644 index 7ca72d2..0000000 --- a/src/dutylog/infrastructure/database/models/room_hours_transaction.py +++ /dev/null @@ -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 - ) diff --git a/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py b/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py index b0b3e86..cfca573 100644 --- a/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py +++ b/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py @@ -25,6 +25,7 @@ class HoursTransactionsRepository: admin_id: int | None = None, is_active: bool = True, remark: str | None = None, + per_room: bool = False, ) -> tuple[HoursTransaction, Resident | None]: transaction = HoursTransaction( resident_id=resident_id, @@ -32,6 +33,7 @@ class HoursTransactionsRepository: amount=amount, admin_id=admin_id, remark=remark, + per_room=per_room, ) transaction = await self.transactions_dao.create(transaction) @@ -57,6 +59,7 @@ class HoursTransactionsRepository: admin_id: int | None = None, is_active: bool = True, remark: str | None = None, + per_room: bool = False, ) -> tuple[HoursTransaction, Resident | None]: transaction = HoursTransaction( resident_id=resident_id, @@ -64,6 +67,7 @@ class HoursTransactionsRepository: amount=amount, admin_id=admin_id, remark=remark, + per_room=per_room, ) transaction = await self.transactions_dao.create(transaction) @@ -88,6 +92,7 @@ class HoursTransactionsRepository: amount: int, admin_id: int | None = None, remark: str | None = None, + per_room: bool = False, ) -> tuple[HoursTransaction, Resident | None]: """Перемещает часы из неотработанных в отработанные""" transaction = HoursTransaction( @@ -96,6 +101,7 @@ class HoursTransactionsRepository: amount=amount, admin_id=admin_id, remark=remark, + per_room=per_room, ) 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]: 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 diff --git a/src/dutylog/infrastructure/database/repositories/room_hours_transactions_repository.py b/src/dutylog/infrastructure/database/repositories/room_hours_transactions_repository.py deleted file mode 100644 index 290c6b2..0000000 --- a/src/dutylog/infrastructure/database/repositories/room_hours_transactions_repository.py +++ /dev/null @@ -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) diff --git a/src/dutylog/infrastructure/ioc.py b/src/dutylog/infrastructure/ioc.py index 8400e79..902d888 100644 --- a/src/dutylog/infrastructure/ioc.py +++ b/src/dutylog/infrastructure/ioc.py @@ -8,9 +8,6 @@ from dutylog.infrastructure.database.dao.users_dao import UsersDAO from dutylog.infrastructure.database.dao.hours_transactions_dao import ( 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.residents_dao import ResidentsDAO 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 ( HoursTransactionsRepository, ) -from dutylog.infrastructure.database.repositories.room_hours_transactions_repository import ( - RoomHoursTransactionsRepository, -) from dutylog.infrastructure.database.repositories.rooms_repository import ( RoomsRepository, ) @@ -76,10 +70,6 @@ class DAOProvider(Provider): def get_hours_transactions_dao(self, session: AsyncSession) -> HoursTransactionsDAO: return HoursTransactionsDAO(session) - @provide(scope=Scope.REQUEST) - def get_room_hours_transactions_dao(self, session: AsyncSession) -> RoomHoursTransactionsDAO: - return RoomHoursTransactionsDAO(session) - @provide(scope=Scope.REQUEST) def get_rooms_dao(self, session: AsyncSession) -> RoomsDAO: return RoomsDAO(session) @@ -110,14 +100,6 @@ class RepositoryProvider(Provider): ) -> HoursTransactionsRepository: 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) def get_rooms_repository(self, rooms_dao: RoomsDAO) -> RoomsRepository: return RoomsRepository(rooms_dao) @@ -144,7 +126,6 @@ class ServiceProvider(Provider): def get_report_service( self, hours_transactions_repository: HoursTransactionsRepository, - room_hours_transactions_repository: RoomHoursTransactionsRepository, residents_repository: ResidentsRepository, rooms_repository: RoomsRepository, floors_repository: FloorsRepository, @@ -152,7 +133,6 @@ class ServiceProvider(Provider): ) -> ReportService: return ReportService( hours_transactions_repository, - room_hours_transactions_repository, residents_repository, rooms_repository, floors_repository, diff --git a/src/dutylog/services/report_service.py b/src/dutylog/services/report_service.py index 6a05553..14166a7 100644 --- a/src/dutylog/services/report_service.py +++ b/src/dutylog/services/report_service.py @@ -10,9 +10,6 @@ from dutylog.infrastructure.database.repositories.floors_repository import ( from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( HoursTransactionsRepository, ) -from dutylog.infrastructure.database.repositories.room_hours_transactions_repository import ( - RoomHoursTransactionsRepository, -) from dutylog.infrastructure.database.repositories.residents_repository import ( ResidentsRepository, ) @@ -30,27 +27,26 @@ class ReportService: def __init__( self, hours_transactions_repository: HoursTransactionsRepository, - room_hours_transactions_repository: RoomHoursTransactionsRepository, residents_repository: ResidentsRepository, rooms_repository: RoomsRepository, floors_repository: FloorsRepository, users_repository: UsersRepository, ): self.hours_transactions_repository = hours_transactions_repository - self.room_hours_transactions_repository = room_hours_transactions_repository self.residents_repository = residents_repository self.rooms_repository = rooms_repository self.floors_repository = floors_repository self.users_repository = users_repository async def generate_period_report(self, start_date: date, end_date: date) -> BytesIO: - resident_transactions = await self.hours_transactions_repository.get_by_period( - start_date, end_date - ) - room_transactions = await self.room_hours_transactions_repository.get_by_period( + all_transactions = await self.hours_transactions_repository.get_by_period( 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 = [ 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_decrease, start_date, end_date) - await self._create_room_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_increase, start_date, end_date) + await self._create_resident_sheet(wb, "Комнаты - Списания", room_decrease, start_date, end_date) output = BytesIO() wb.save(output) @@ -182,105 +178,3 @@ class ReportService: ws.column_dimensions["D"].width = 10 ws.column_dimensions["E"].width = 15 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