Приём документов → реестр + журнал
Внесение данных из файлов приёма в реестр невостребованных документов и заполнение журнала. Дата внесения — текущая; журнал заполняется чисто (без строк-примеров шаблона). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
86
priem_register/readers/ods_reader.py
Normal file
86
priem_register/readers/ods_reader.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Читатель .ods (odfpy). Разворачивает повторяющиеся ячейки/строки."""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import List
|
||||
|
||||
from odf.opendocument import load
|
||||
from odf.table import Table, TableCell, TableRow
|
||||
from odf.teletype import extractText
|
||||
from odf.text import P
|
||||
|
||||
from ..models import CellValue
|
||||
from .base import SourceReader
|
||||
|
||||
# Пределы на «бесконечные» повторения пустого хвоста таблицы.
|
||||
_MAX_COL_REPEAT = 200
|
||||
_MAX_ROW_REPEAT = 200
|
||||
|
||||
|
||||
class OdsReader(SourceReader):
|
||||
def _rows(self) -> List[List[CellValue]]:
|
||||
doc = load(str(self.path))
|
||||
table = doc.spreadsheet.getElementsByType(Table)[0]
|
||||
rows: List[List[CellValue]] = []
|
||||
for tr in table.getElementsByType(TableRow):
|
||||
row = self._row_values(tr)
|
||||
row_rep = _repeat(tr, "numberrowsrepeated", _MAX_ROW_REPEAT)
|
||||
for _ in range(row_rep):
|
||||
rows.append(list(row))
|
||||
_trim_trailing_empty(rows)
|
||||
return rows
|
||||
|
||||
@staticmethod
|
||||
def _row_values(tr: TableRow) -> List[CellValue]:
|
||||
values: List[CellValue] = []
|
||||
for cell in tr.getElementsByType(TableCell):
|
||||
col_rep = _repeat(cell, "numbercolumnsrepeated", _MAX_COL_REPEAT)
|
||||
value = _cell_value(cell)
|
||||
values.extend([value] * col_rep)
|
||||
return values
|
||||
|
||||
|
||||
def _repeat(element, attr: str, limit: int) -> int:
|
||||
raw = element.getAttribute(attr)
|
||||
n = int(raw) if raw else 1
|
||||
# огромные повторения — это пустой хвост листа, не размножаем
|
||||
return n if n <= limit else 1
|
||||
|
||||
|
||||
def _cell_value(cell: TableCell) -> CellValue:
|
||||
vtype = cell.getAttribute("valuetype")
|
||||
if vtype in ("float", "currency", "percentage"):
|
||||
raw = cell.getAttribute("value")
|
||||
if raw is not None:
|
||||
num = float(raw)
|
||||
return int(num) if num.is_integer() else num
|
||||
if vtype == "date":
|
||||
raw = cell.getAttribute("datevalue")
|
||||
if raw:
|
||||
return _parse_date(raw)
|
||||
text = _cell_text(cell).strip()
|
||||
return text or None
|
||||
|
||||
|
||||
def _cell_text(cell: TableCell) -> str:
|
||||
# extractText корректно собирает текст из <text:span>, <text:s> (кратные
|
||||
# пробелы), <text:tab> и <text:line-break> — ручной обход childNodes их терял.
|
||||
return "\n".join(extractText(p) for p in cell.getElementsByType(P))
|
||||
|
||||
|
||||
def _parse_date(raw: str):
|
||||
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"):
|
||||
try:
|
||||
dt = datetime.datetime.strptime(raw, fmt)
|
||||
if dt.hour == dt.minute == dt.second == 0:
|
||||
return dt.date()
|
||||
return dt
|
||||
except ValueError:
|
||||
continue
|
||||
return raw
|
||||
|
||||
|
||||
def _trim_trailing_empty(rows: List[List[CellValue]]) -> None:
|
||||
while rows and all(v in (None, "") for v in rows[-1]):
|
||||
rows.pop()
|
||||
Reference in New Issue
Block a user