Внесение данных из файлов приёма в реестр невостребованных документов и заполнение журнала. Дата внесения — текущая; журнал заполняется чисто (без строк-примеров шаблона). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
3.9 KiB
Python
87 lines
3.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Базовый класс читателя источника."""
|
||
from __future__ import annotations
|
||
|
||
import abc
|
||
import datetime
|
||
from pathlib import Path
|
||
from typing import List
|
||
|
||
from ..config import REGISTER_COLUMNS, SOURCE_NUMBER_COL
|
||
from ..models import CellValue, ReadResult, Record
|
||
|
||
# Позиция колонки «Примечание» (L) в раскладке реестра (A=0 … M=12).
|
||
NOTE_COL = 11
|
||
|
||
|
||
class SourceReader(abc.ABC):
|
||
"""Читает файл приёма и возвращает записи для внесения.
|
||
|
||
По заданию: «внести все строки, начиная со второй». Поэтому пропускается
|
||
только шапка (строка 1), а каждая последующая непустая строка вносится как
|
||
отдельная запись — без фильтрации по № п/п. № п/п в реестре назначается
|
||
заново (сквозной), исходный № отбрасывается.
|
||
|
||
В реестре 13 колонок (A..M). Если в источнике есть данные правее колонки M
|
||
(например, безымянная 14-я колонка), переносить их в отдельную колонку
|
||
некуда — они добавляются в «Примечание» той же строки, а факт фиксируется
|
||
в ``notes`` (информационно).
|
||
|
||
Подклассы реализуют только :meth:`_rows` — выдачу «сырых» строк таблицы
|
||
(список типизированных значений ячеек) первого листа.
|
||
"""
|
||
|
||
def __init__(self, path: Path):
|
||
self.path = Path(path)
|
||
|
||
@abc.abstractmethod
|
||
def _rows(self) -> List[List[CellValue]]:
|
||
"""Сырые строки первого листа, типизированные значения ячеек."""
|
||
|
||
def read(self) -> ReadResult:
|
||
result = ReadResult()
|
||
for index, row in enumerate(self._rows()):
|
||
if index == 0:
|
||
continue # шапка (строка 1)
|
||
if not self._has_content(row):
|
||
continue # пустые строки не вносим
|
||
record = Record(values=list(row[SOURCE_NUMBER_COL + 1: REGISTER_COLUMNS]))
|
||
self._fold_overflow(row, record, result, index + 1)
|
||
result.records.append(record)
|
||
return result
|
||
|
||
def _fold_overflow(self, row: List[CellValue], record: Record,
|
||
result: ReadResult, human_row: int) -> None:
|
||
overflow = [v for i, v in enumerate(row)
|
||
if i >= REGISTER_COLUMNS and v not in (None, "")]
|
||
for value in overflow:
|
||
_merge_cell(record, NOTE_COL, value)
|
||
if overflow:
|
||
result.notes.append(
|
||
"строка %d: данные за колонкой M добавлены в «Примечание»: %s"
|
||
% (human_row, ", ".join(_as_text(v) for v in overflow)))
|
||
|
||
@staticmethod
|
||
def _has_content(row: List[CellValue]) -> bool:
|
||
return any(v not in (None, "") for v in row)
|
||
|
||
|
||
def _merge_cell(record: Record, position: int, value: CellValue) -> None:
|
||
"""Записать значение в колонку записи; занятое поле дополнить через «; »."""
|
||
idx = position - 1
|
||
while len(record.values) <= idx:
|
||
record.values.append(None)
|
||
current = record.values[idx]
|
||
if current in (None, ""):
|
||
record.values[idx] = value
|
||
else:
|
||
record.values[idx] = "%s; %s" % (_as_text(current), _as_text(value))
|
||
|
||
|
||
def _as_text(value: CellValue) -> str:
|
||
if isinstance(value, datetime.date): # покрывает и datetime
|
||
return value.strftime("%d.%m.%Y")
|
||
if isinstance(value, float) and value.is_integer():
|
||
return str(int(value))
|
||
return str(value)
|