# -*- coding: utf-8 -*- """Запись принятых документов в реестр (.ods) через odfpy. Новые строки дописываются сразу после последней нумерованной строки листа. Стиль каждой ячейки (границы, формат даты/числа) и стиль строки копируются из этой последней строки-образца, поэтому оформление реестра сохраняется. Остальные листы, формулы и стили документа odfpy не трогает. """ from __future__ import annotations import datetime from pathlib import Path from typing import Dict, List, Optional, Tuple from odf.opendocument import load from odf.table import Table, TableCell, TableRow from odf.teletype import extractText from odf.text import P from ..config import REGISTER_COLUMNS from ..models import CellValue, Record # Повторения больше этого — «бесконечный» пустой хвост листа, не данные. _MAX_REPEAT = 200 class RegisterWriter: def __init__(self, path: Path): self.doc = load(str(path)) self._tables: Dict[str, Table] = { t.getAttribute("name"): t for t in self.doc.spreadsheet.getElementsByType(Table) } def has_sheet(self, name: str) -> bool: return name in self._tables def append(self, sheet: str, records: List[Record]) -> int: """Дописать записи на лист. Возвращает число добавленных строк.""" table = self._tables[sheet] template, last_number = self._find_last_numbered_row(table) col_styles = self._column_styles(template) row_style = template.getAttribute("stylename") anchor = template number = last_number for record in records: number += 1 row = self._build_row(record, number, row_style, col_styles) table.insertBefore(row, anchor.nextSibling) anchor = row return len(records) def save(self, path: Path) -> None: self.doc.save(str(path)) # --- поиск строки-образца ------------------------------------------- def _find_last_numbered_row(self, table: Table) -> Tuple[TableRow, int]: last_row: Optional[TableRow] = None last_number = 0 for tr in table.getElementsByType(TableRow): if _row_repeat(tr) > 1: continue # повторяющийся пустой блок cells = tr.getElementsByType(TableCell) if not cells: continue number = _as_int(cells[0]) if number is not None: last_row, last_number = tr, number if last_row is None: raise ValueError("На листе нет нумерованных строк: %s" % table.getAttribute("name")) return last_row, last_number @staticmethod def _column_styles(template: TableRow) -> Dict[int, Optional[str]]: """Карта {индекс колонки 0..12 -> имя стиля ячейки} из образца.""" styles: Dict[int, Optional[str]] = {} col = 0 for cell in template.getElementsByType(TableCell): rep = _col_repeat(cell) style = cell.getAttribute("stylename") for _ in range(rep): if col < REGISTER_COLUMNS: styles[col] = style col += 1 if col >= REGISTER_COLUMNS: break return styles # --- построение строки ---------------------------------------------- def _build_row(self, record: Record, number: int, row_style: Optional[str], col_styles: Dict[int, Optional[str]]) -> TableRow: row = TableRow(stylename=row_style) if row_style else TableRow() for col in range(REGISTER_COLUMNS): style = col_styles.get(col) value = number if col == 0 else record.value(col) row.addElement(_make_cell(value, style)) return row # --- фабрика ячеек по типу значения ------------------------------------- def _make_cell(value: CellValue, style: Optional[str]) -> TableCell: if value is None or value == "": return _cell(style) if isinstance(value, bool): return _text_cell(str(value), style) if isinstance(value, datetime.datetime): return _date_cell(value, style, with_time=True) if isinstance(value, datetime.date): return _date_cell(value, style, with_time=False) if isinstance(value, (int, float)): return _number_cell(value, style) return _text_cell(str(value), style) def _cell(style: Optional[str]) -> TableCell: return TableCell(stylename=style) if style else TableCell() def _text_cell(text: str, style: Optional[str]) -> TableCell: cell = TableCell(valuetype="string", stylename=style) if style \ else TableCell(valuetype="string") for line in text.split("\n"): cell.addElement(P(text=line)) return cell def _number_cell(num, style: Optional[str]) -> TableCell: text = _number_text(num) kw = dict(valuetype="float", value=text) if style: kw["stylename"] = style cell = TableCell(**kw) cell.addElement(P(text=text)) return cell def _date_cell(value, style: Optional[str], with_time: bool) -> TableCell: iso = value.strftime("%Y-%m-%dT%H:%M:%S" if with_time else "%Y-%m-%d") display = value.strftime("%d.%m.%Y") kw = dict(valuetype="date", datevalue=iso) if style: kw["stylename"] = style cell = TableCell(**kw) cell.addElement(P(text=display)) return cell # --- мелкие помощники --------------------------------------------------- def _number_text(num) -> str: if isinstance(num, float) and num.is_integer(): return str(int(num)) return str(num) def _row_repeat(tr: TableRow) -> int: raw = tr.getAttribute("numberrowsrepeated") n = int(raw) if raw else 1 return n if n <= _MAX_REPEAT else _MAX_REPEAT + 1 def _col_repeat(cell: TableCell) -> int: raw = cell.getAttribute("numbercolumnsrepeated") n = int(raw) if raw else 1 return n if n <= _MAX_REPEAT else 1 def _as_int(cell: TableCell) -> Optional[int]: if cell.getAttribute("valuetype") == "float": raw = cell.getAttribute("value") if raw: return int(float(raw)) text = "".join(extractText(p) for p in cell.getElementsByType(P)).strip() return int(text) if text.isdigit() else None