import os from importlib import resources from typing import List from odf.opendocument import load from odf.text import P from odf.table import Table, TableColumn, TableRow, TableCell from odf.style import Style, TableColumnProperties, TableCellProperties from .base import Formatter from ..types import ReportRow from ..utils import get_month_name_from_range class ODTFormatter(Formatter): """Форматтер для экспорта в ODT.""" def __init__(self, author: str = "", from_date: str = "", to_date: str = ""): """ Инициализирует форматтер с параметрами для шапки отчета. """ self.author = author self.from_date = from_date self.to_date = to_date def format(self, rows: List[ReportRow]) -> "OpenDocument": """ Форматирует данные в объект OpenDocument. """ with ( resources.files("redmine_reporter") .joinpath("templates", "template.odt") .open("rb") as f ): doc = load(f) para_style_name = "Standard" # Заголовок month_name = get_month_name_from_range(self.from_date, self.to_date) header_text = f"{self.author}. Отчет за месяц {month_name}." doc.text.addElement(P(stylename=para_style_name, text=header_text)) doc.text.addElement(P(stylename=para_style_name, text="")) # Стиль ячеек cell_style_name = "TableCellStyle" cell_style = Style(name=cell_style_name, family="table-cell") cell_props = TableCellProperties(padding="0.04in", border="0.05pt solid #000000") cell_style.addElement(cell_props) doc.automaticstyles.addElement(cell_style) # Таблица table = Table(name="Report") column_widths = ["1.56in", "1.63in", "3.93in", "1.56in", "1.43in"] for width in column_widths: col_style = Style(name=f"col_{width}", family="table-column") col_props = TableColumnProperties(columnwidth=width) col_style.addElement(col_props) doc.automaticstyles.addElement(col_style) table.addElement(TableColumn(stylename=col_style)) # Заголовки header_row = TableRow() for text in [ "Наименование Проекта", "Номер версии*", "Задача", "Статус Готовность*", "Затрачено за отчетный период", ]: cell = TableCell(stylename=cell_style_name) cell.addElement(P(stylename=para_style_name, text=text)) header_row.addElement(cell) table.addElement(header_row) projects = {} for r in rows: project = r["project"] version = r["version"] if project not in projects: projects[project] = {} if version not in projects[project]: projects[project][version] = [] projects[project][version].append(r) # Данные с двухуровневой группировкой и объединением ячеек for project, versions in projects.items(): total_project_rows = sum( len(rows_for_version) for rows_for_version in versions.values() ) first_version_in_project = True for version, rows_for_version in versions.items(): row_span_version = len(rows_for_version) first_row_in_version = True for r in rows_for_version: row = TableRow() # Ячейка "Проект" - только в первой строке всего проекта if first_version_in_project and first_row_in_version: cell_project = TableCell(stylename=cell_style_name) cell_project.setAttribute("numberrowsspanned", str(total_project_rows)) p = P(stylename=para_style_name, text=project) # Полное название проекта cell_project.addElement(p) row.addElement(cell_project) # Ячейка "Версия" - только в первой строке каждой версии if first_row_in_version: cell_version = TableCell(stylename=cell_style_name) cell_version.setAttribute("numberrowsspanned", str(row_span_version)) p = P(stylename=para_style_name, text=version) cell_version.addElement(p) row.addElement(cell_version) first_row_in_version = False else: # Пропускаем - уже объединена pass # Остальные колонки task_cell = TableCell(stylename=cell_style_name) task_text = f"{r['issue_id']}. {r['subject']}" p = P(stylename=para_style_name, text=task_text) task_cell.addElement(p) row.addElement(task_cell) status_cell = TableCell(stylename=cell_style_name) p = P(stylename=para_style_name, text=r["status_ru"]) status_cell.addElement(p) row.addElement(status_cell) time_cell = TableCell(stylename=cell_style_name) p = P(stylename=para_style_name, text=r["time_text"]) time_cell.addElement(p) row.addElement(time_cell) table.addElement(row) first_version_in_project = False doc.text.addElement(table) doc.text.addElement(P(stylename=para_style_name, text="")) # Справка for line in [ "«Наименование Проекта» - Имя собственное устройства или программного обеспечения.", "«Номер версии» - Версия в проекте. Опциональное поле.", "«Задача» - Номер по Redmine и формулировка.", "«Статус» - Актуальное состояние задачи на момент отчета. Статусы: закрыто, в работе, ожидание, решена.", "«Готовность» – Опциональное поле в процентах.", "«Затрачено за отчетный период» - в днях или часах.", ]: doc.text.addElement(P(stylename=para_style_name, text=line)) return doc def save(self, rows: List[ReportRow], output_path: str) -> None: """ Сохраняет сформированный документ в файл. """ doc = self.format(rows) doc.save(output_path)