mirror of
https://github.com/koloideal/DutyLog.git
synced 2026-06-10 02:15:30 +03:00
update
This commit is contained in:
@@ -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:
|
|
||||||
await callback.answer(
|
|
||||||
f"⚠️ Недостаточно часов! У комнаты {room.active_hours} неотработанных ч, а вы пытаетесь отнять {hours} ч",
|
|
||||||
show_alert=True
|
|
||||||
)
|
|
||||||
await dialog_manager.switch_to(AdminMenuSG.room_info)
|
|
||||||
return
|
|
||||||
|
|
||||||
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,
|
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
|
||||||
|
|||||||
-122
@@ -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)
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user