"""Выгрузка таблиц экранов в .xlsx (D13) — без БД и сети. Числа и даты пишутся значениями, не текстом: в Excel по ним можно считать. """ from __future__ import annotations import io from collections.abc import Sequence from dataclasses import asdict, is_dataclass from datetime import date, datetime, timedelta, timezone from decimal import Decimal from typing import Any from openpyxl import Workbook from openpyxl.styles import Font from openpyxl.utils import get_column_letter MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" # Время аудита в БД — с часовым поясом, в Excel пояса нет: пишем московское. _MSK = timezone(timedelta(hours=3)) _DATE_FORMAT = "DD.MM.YYYY" _DATETIME_FORMAT = "DD.MM.YYYY HH:MM:SS" _MAX_WIDTH = 60 _SHEET_TITLE_MAX = 31 # ограничение Excel def table_xlsx(rows: Sequence[Any], columns: dict[str, str], sheet: str) -> bytes: """Список dataclass/dict -> .xlsx с одним листом. columns: поле -> заголовок, в том же порядке, что на экране.""" workbook = Workbook() ws = workbook.active ws.title = sheet[:_SHEET_TITLE_MAX] ws.append(list(columns.values())) for cell in ws[1]: cell.font = Font(bold=True) ws.freeze_panes = "A2" widths = [len(title) for title in columns.values()] for row in rows: data = asdict(row) if is_dataclass(row) else dict(row) values = [_cell_value(data.get(name)) for name in columns] ws.append(values) for index, value in enumerate(values): widths[index] = max(widths[index], _width(value)) for column, width in enumerate(widths, start=1): ws.column_dimensions[get_column_letter(column)].width = min( width + 2, _MAX_WIDTH ) for row_cells in ws.iter_rows(min_row=2): for cell in row_cells: if isinstance(cell.value, datetime): cell.number_format = _DATETIME_FORMAT elif isinstance(cell.value, date): cell.number_format = _DATE_FORMAT buffer = io.BytesIO() workbook.save(buffer) return buffer.getvalue() def _cell_value(value: Any) -> Any: if isinstance(value, datetime) and value.tzinfo is not None: return value.astimezone(_MSK).replace(tzinfo=None) return value def _width(value: Any) -> int: if value is None: return 0 if isinstance(value, datetime): return len(_DATETIME_FORMAT) if isinstance(value, date): return len(_DATE_FORMAT) if isinstance(value, Decimal): return len(format(value.normalize(), "f")) return len(str(value))