from datetime import date from io import BytesIO from openpyxl import Workbook from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from dutylog.infrastructure.database.repositories.floors_repository import ( FloorsRepository, ) from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( HoursTransactionsRepository, ) from dutylog.infrastructure.database.repositories.residents_repository import ( ResidentsRepository, ) from dutylog.infrastructure.database.repositories.rooms_repository import ( RoomsRepository, ) class ReportService: def __init__( self, hours_transactions_repository: HoursTransactionsRepository, residents_repository: ResidentsRepository, rooms_repository: RoomsRepository, floors_repository: FloorsRepository, ): self.hours_transactions_repository = hours_transactions_repository self.residents_repository = residents_repository self.rooms_repository = rooms_repository self.floors_repository = floors_repository async def generate_period_report(self, start_date: date, end_date: date) -> BytesIO: transactions = await self.hours_transactions_repository.get_by_period( start_date, end_date ) increase_transactions = [ t for t in transactions if t.transaction_type == "increase" ] decrease_transactions = [ t for t in transactions if t.transaction_type == "decrease" ] wb = Workbook() ws = wb.active if ws is None: raise ValueError("Failed to create worksheet") ws.title = "Отчет по начислениям" header_font = Font(bold=True, size=12, color="FFFFFF") header_fill = PatternFill( start_color="4472C4", end_color="4472C4", fill_type="solid" ) header_alignment = Alignment(horizontal="center", vertical="center") summary_font = Font(bold=True, size=11) summary_fill = PatternFill( start_color="E7E6E6", end_color="E7E6E6", fill_type="solid" ) border = Border( left=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin"), ) title_text = f"Отчет по начислениям часов за период {start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" ws.cell(row=1, column=1, value=title_text) ws.merge_cells("A1:E1") title_cell = ws.cell(row=1, column=1) title_cell.font = Font(bold=True, size=14) title_cell.alignment = Alignment(horizontal="center", vertical="center") ws.row_dimensions[1].height = 25 row_num = 3 ws.cell(row=row_num, column=1, value="НАЧИСЛЕНИЯ") ws.cell(row=row_num, column=1).font = Font(bold=True, size=12) ws.merge_cells(f"A{row_num}:E{row_num}") row_num += 1 headers = ["Дата", "Резидент", "Комната", "Часы", "Примечание"] for col_num, header in enumerate(headers, 1): cell = ws.cell(row=row_num, column=col_num) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = border ws.row_dimensions[row_num].height = 20 row_num += 1 total_increase = 0 for transaction in increase_transactions: resident = await self.residents_repository.get_by_id( transaction.resident_id ) if not resident: continue room = await self.rooms_repository.get_by_id(resident.room) room_number = room.number if room else "—" ws.cell( row=row_num, column=1, value=transaction.created_at.replace(tzinfo=None) ).border = border ws.cell(row=row_num, column=1).number_format = "DD.MM.YYYY HH:MM" ws.cell( row=row_num, column=2, value=resident.real_name or "Без имени" ).border = border ws.cell(row=row_num, column=3, value=room_number).border = border ws.cell(row=row_num, column=4, value=transaction.amount).border = border ws.cell( row=row_num, column=5, value=transaction.remark or "—" ).border = border ws.cell(row=row_num, column=1).alignment = Alignment(horizontal="center") ws.cell(row=row_num, column=3).alignment = Alignment(horizontal="center") ws.cell(row=row_num, column=4).alignment = Alignment(horizontal="center") total_increase += transaction.amount row_num += 1 summary_row = row_num ws.merge_cells(f"A{summary_row}:C{summary_row}") summary_cell = ws[f"A{summary_row}"] summary_cell.value = "Начислено часов:" summary_cell.font = summary_font summary_cell.fill = summary_fill summary_cell.alignment = Alignment(horizontal="right", vertical="center") summary_cell.border = border ws.cell(row=summary_row, column=4, value=total_increase).font = summary_font ws.cell(row=summary_row, column=4).fill = summary_fill ws.cell(row=summary_row, column=4).alignment = Alignment(horizontal="center") ws.cell(row=summary_row, column=4).border = border ws.cell(row=summary_row, column=5, value="").fill = summary_fill ws.cell(row=summary_row, column=5).border = border row_num += 3 ws.cell(row=row_num, column=1, value="СПИСАНИЯ") ws.cell(row=row_num, column=1).font = Font(bold=True, size=12) ws.merge_cells(f"A{row_num}:E{row_num}") row_num += 1 for col_num, header in enumerate(headers, 1): cell = ws.cell(row=row_num, column=col_num) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = border ws.row_dimensions[row_num].height = 20 row_num += 1 total_decrease = 0 for transaction in decrease_transactions: resident = await self.residents_repository.get_by_id( transaction.resident_id ) if not resident: continue room = await self.rooms_repository.get_by_id(resident.room) room_number = room.number if room else "—" ws.cell( row=row_num, column=1, value=transaction.created_at.replace(tzinfo=None) ).border = border ws.cell(row=row_num, column=1).number_format = "DD.MM.YYYY HH:MM" ws.cell( row=row_num, column=2, value=resident.real_name or "Без имени" ).border = border ws.cell(row=row_num, column=3, value=room_number).border = border ws.cell(row=row_num, column=4, value=transaction.amount).border = border ws.cell( row=row_num, column=5, value=transaction.remark or "—" ).border = border ws.cell(row=row_num, column=1).alignment = Alignment(horizontal="center") ws.cell(row=row_num, column=3).alignment = Alignment(horizontal="center") ws.cell(row=row_num, column=4).alignment = Alignment(horizontal="center") total_decrease += transaction.amount row_num += 1 summary_row = row_num ws.merge_cells(f"A{summary_row}:C{summary_row}") summary_cell = ws[f"A{summary_row}"] summary_cell.value = "Списано часов:" summary_cell.font = summary_font summary_cell.fill = summary_fill summary_cell.alignment = Alignment(horizontal="right", vertical="center") summary_cell.border = border ws.cell(row=summary_row, column=4, value=total_decrease).font = summary_font ws.cell(row=summary_row, column=4).fill = summary_fill ws.cell(row=summary_row, column=4).alignment = Alignment(horizontal="center") ws.cell(row=summary_row, column=4).border = border ws.cell(row=summary_row, column=5, value="").fill = summary_fill ws.cell(row=summary_row, column=5).border = border ws.column_dimensions["A"].width = 18 ws.column_dimensions["B"].width = 25 ws.column_dimensions["C"].width = 10 ws.column_dimensions["D"].width = 10 ws.column_dimensions["E"].width = 30 output = BytesIO() wb.save(output) output.seek(0) return output