From c7ad35d343979aeccf2faadc35ae2a9d71b6f4a3 Mon Sep 17 00:00:00 2001 From: kolo Date: Tue, 3 Mar 2026 22:58:04 +0300 Subject: [PATCH] update --- .../dao/room_hours_transactions_dao.py | 27 ++ .../room_hours_transactions_repository.py | 5 + src/dutylog/infrastructure/ioc.py | 2 + src/dutylog/services/report_service.py | 259 ++++++++++-------- 4 files changed, 179 insertions(+), 114 deletions(-) diff --git a/src/dutylog/infrastructure/database/dao/room_hours_transactions_dao.py b/src/dutylog/infrastructure/database/dao/room_hours_transactions_dao.py index 86aebe5..dd7c9dd 100644 --- a/src/dutylog/infrastructure/database/dao/room_hours_transactions_dao.py +++ b/src/dutylog/infrastructure/database/dao/room_hours_transactions_dao.py @@ -31,3 +31,30 @@ class RoomHoursTransactionsDAO: 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/repositories/room_hours_transactions_repository.py b/src/dutylog/infrastructure/database/repositories/room_hours_transactions_repository.py index 52c4aff..290c6b2 100644 --- a/src/dutylog/infrastructure/database/repositories/room_hours_transactions_repository.py +++ b/src/dutylog/infrastructure/database/repositories/room_hours_transactions_repository.py @@ -115,3 +115,8 @@ class RoomHoursTransactionsRepository: 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 e88ec3f..8400e79 100644 --- a/src/dutylog/infrastructure/ioc.py +++ b/src/dutylog/infrastructure/ioc.py @@ -144,6 +144,7 @@ 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, @@ -151,6 +152,7 @@ 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 1440939..6a05553 100644 --- a/src/dutylog/services/report_service.py +++ b/src/dutylog/services/report_service.py @@ -10,6 +10,9 @@ 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, ) @@ -27,34 +30,58 @@ 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: - transactions = await self.hours_transactions_repository.get_by_period( + 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( start_date, end_date ) - increase_transactions = [ - t for t in transactions if t.transaction_type == "increase" + resident_increase = [ + t for t in resident_transactions if t.transaction_type == "increase" ] - decrease_transactions = [ - t for t in transactions if t.transaction_type == "decrease" + resident_decrease = [ + t for t in resident_transactions if t.transaction_type == "decrease" + ] + room_increase = [ + t for t in room_transactions if t.transaction_type == "increase" + ] + room_decrease = [ + t for t in room_transactions if t.transaction_type == "decrease" ] wb = Workbook() - ws = wb.active - if ws is None: - raise ValueError("Failed to create worksheet") - ws.title = "Отчет по начислениям" + + if wb.active: + wb.remove(wb.active) + + 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) + + output = BytesIO() + wb.save(output) + output.seek(0) + + return output + + async def _create_resident_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( @@ -74,7 +101,7 @@ class ReportService: bottom=Side(style="thin"), ) - title_text = f"Отчет по начислениям часов за период {start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" + 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) @@ -83,12 +110,6 @@ class ReportService: ws.row_dimensions[1].height = 25 row_num = 3 - - ws.cell(row=row_num, column=1, value="НАЧИСЛЕНИЯ") - ws.cell(row=row_num, column=1).font = Font(bold=True, size=12) - ws.merge_cells(f"A{row_num}:F{row_num}") - row_num += 1 - headers = ["Дата", "Резидент", "Комната", "Часы", "Админ", "Примечание"] for col_num, header in enumerate(headers, 1): cell = ws.cell(row=row_num, column=col_num) @@ -101,9 +122,8 @@ class ReportService: ws.row_dimensions[row_num].height = 20 row_num += 1 - total_increase = 0 - - for transaction in increase_transactions: + total = 0 + for transaction in transactions: resident = await self.residents_repository.get_by_id( transaction.resident_id ) @@ -121,116 +141,31 @@ class ReportService: 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, 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=resident.real_name or "Без имени" - ).border = border + ws.cell(row=row_num, column=2, value=resident.real_name or "Без имени").border = border ws.cell(row=row_num, column=3, value=room_number).border = border ws.cell(row=row_num, column=4, value=transaction.amount).border = border ws.cell(row=row_num, column=5, value=admin_username).border = border - ws.cell( - row=row_num, column=6, value=transaction.remark or "—" - ).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=3).alignment = Alignment(horizontal="center") ws.cell(row=row_num, column=4).alignment = Alignment(horizontal="center") - total_increase += transaction.amount + 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.value = "Итого часов:" summary_cell.font = summary_font summary_cell.fill = summary_fill summary_cell.alignment = Alignment(horizontal="right", vertical="center") summary_cell.border = border - ws.cell(row=summary_row, column=4, value=total_increase).font = summary_font - ws.cell(row=summary_row, column=4).fill = summary_fill - ws.cell(row=summary_row, column=4).alignment = Alignment(horizontal="center") - ws.cell(row=summary_row, column=4).border = border - - ws.cell(row=summary_row, column=5, value="").fill = summary_fill - ws.cell(row=summary_row, column=5).border = border - - ws.cell(row=summary_row, column=6, value="").fill = summary_fill - ws.cell(row=summary_row, column=6).border = border - - row_num += 3 - - ws.cell(row=row_num, column=1, value="СПИСАНИЯ") - ws.cell(row=row_num, column=1).font = Font(bold=True, size=12) - ws.merge_cells(f"A{row_num}:F{row_num}") - row_num += 1 - - for col_num, header in enumerate(headers, 1): - cell = ws.cell(row=row_num, column=col_num) - cell.value = header - cell.font = header_font - cell.fill = header_fill - cell.alignment = header_alignment - cell.border = border - - ws.row_dimensions[row_num].height = 20 - row_num += 1 - - total_decrease = 0 - - for transaction in decrease_transactions: - resident = await self.residents_repository.get_by_id( - transaction.resident_id - ) - if not resident: - continue - - room = await self.rooms_repository.get_by_id(resident.room) - room_number = room.number if room else "—" - - 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=resident.real_name or "Без имени" - ).border = border - ws.cell(row=row_num, column=3, value=room_number).border = border - ws.cell(row=row_num, column=4, value=transaction.amount).border = border - ws.cell(row=row_num, column=5, value=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=3).alignment = Alignment(horizontal="center") - ws.cell(row=row_num, column=4).alignment = Alignment(horizontal="center") - - total_decrease += transaction.amount - row_num += 1 - - summary_row = row_num - ws.merge_cells(f"A{summary_row}:C{summary_row}") - summary_cell = ws[f"A{summary_row}"] - summary_cell.value = "Списано часов:" - summary_cell.font = summary_font - summary_cell.fill = summary_fill - summary_cell.alignment = Alignment(horizontal="right", vertical="center") - summary_cell.border = border - - ws.cell(row=summary_row, column=4, value=total_decrease).font = summary_font + ws.cell(row=summary_row, column=4, 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 @@ -248,8 +183,104 @@ class ReportService: ws.column_dimensions["E"].width = 15 ws.column_dimensions["F"].width = 30 - output = BytesIO() - wb.save(output) - output.seek(0) + async def _create_room_sheet(self, wb: Workbook, title: str, transactions, start_date: date, end_date: date): + ws = wb.create_sheet(title=title) - return output + 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