Compare commits
No commits in common. "53165a07c0b8693d6478f8eed6b5671b52ae66f2" and "99114508f7cca55f8894ec066b9d66c9fe2bbf1f" have entirely different histories.
53165a07c0
...
99114508f7
@ -8,7 +8,6 @@ from src.parser.deadline import Deadline
|
|||||||
from src.parser.detect import TableHeader
|
from src.parser.detect import TableHeader
|
||||||
from src.parser.grid import Sheet
|
from src.parser.grid import Sheet
|
||||||
from src.parser.hierarchy import ROOT_LEVEL, BodyRow, Levels, resolve_levels
|
from src.parser.hierarchy import ROOT_LEVEL, BodyRow, Levels, resolve_levels
|
||||||
from src.parser.osv_warnings import unknown_label_warning
|
|
||||||
from src.parser.rules import Rules, first_match, normalize_text
|
from src.parser.rules import Rules, first_match, normalize_text
|
||||||
|
|
||||||
__all__ = ["RowLabel", "column_label", "is_indicator_total", "levels_with_indicators", "read_label", "row_data_type"]
|
__all__ = ["RowLabel", "column_label", "is_indicator_total", "levels_with_indicators", "read_label", "row_data_type"]
|
||||||
@ -47,7 +46,10 @@ def read_label(sheet: Sheet, row: int, text: str, header: TableHeader, rules: Ru
|
|||||||
return RowLabel(indicator=False, data_type="BU")
|
return RowLabel(indicator=False, data_type="BU")
|
||||||
if data_type is not None or any(p.search(label) for p in rules.data_types.indicator_column_untyped):
|
if data_type is not None or any(p.search(label) for p in rules.data_types.indicator_column_untyped):
|
||||||
return RowLabel(indicator=True, data_type=data_type, text=label)
|
return RowLabel(indicator=True, data_type=data_type, text=label)
|
||||||
return RowLabel(indicator=True, data_type=None, text=label, warning=unknown_label_warning(row, label))
|
warning = (
|
||||||
|
f"строка {row}: метка «{label}» в колонке «Показатели» не распознана — строка не участвует в проверках"
|
||||||
|
)
|
||||||
|
return RowLabel(indicator=True, data_type=None, text=label, warning=warning)
|
||||||
data_type = row_data_type(text, rules, frozenset(rules.data_types.currency_codes))
|
data_type = row_data_type(text, rules, frozenset(rules.data_types.currency_codes))
|
||||||
return RowLabel(indicator=data_type is not None, data_type=data_type)
|
return RowLabel(indicator=data_type is not None, data_type=data_type)
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,6 @@ from src.parser.osv_warnings import (
|
|||||||
OSV_NO_DATA_ROWS,
|
OSV_NO_DATA_ROWS,
|
||||||
OSV_ROWS,
|
OSV_ROWS,
|
||||||
OsvNote,
|
OsvNote,
|
||||||
group_row_warnings,
|
|
||||||
hierarchy_note,
|
hierarchy_note,
|
||||||
notes_of,
|
notes_of,
|
||||||
)
|
)
|
||||||
@ -236,7 +235,7 @@ class OsvParser:
|
|||||||
hierarchy_note(levels.strategy),
|
hierarchy_note(levels.strategy),
|
||||||
*notes_of(OSV_HEADER, doc.warnings),
|
*notes_of(OSV_HEADER, doc.warnings),
|
||||||
*([OsvNote(OSV_HEADER, DEFAULT_DATA_TYPE_WARNING)] if defaulted else []),
|
*([OsvNote(OSV_HEADER, DEFAULT_DATA_TYPE_WARNING)] if defaulted else []),
|
||||||
*notes_of(OSV_ROWS, group_row_warnings(body_warnings)),
|
*notes_of(OSV_ROWS, body_warnings),
|
||||||
*notes_of(OSV_ROWS, _by_account_warnings(classified)),
|
*notes_of(OSV_ROWS, _by_account_warnings(classified)),
|
||||||
*notes_of(OSV_INN_ROWS, glue.warnings),
|
*notes_of(OSV_INN_ROWS, glue.warnings),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
страницы» — не код ОСВ, а предупреждение файла `XLS_NO_CODEPAGE` пакета (P4-13, R-2); здесь её нет.
|
страницы» — не код ОСВ, а предупреждение файла `XLS_NO_CODEPAGE` пакета (P4-13, R-2); здесь её нет.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@ -20,9 +19,7 @@ __all__ = [
|
|||||||
"OSV_XLSB_NO_OUTLINE",
|
"OSV_XLSB_NO_OUTLINE",
|
||||||
"HIERARCHY_TEXTS",
|
"HIERARCHY_TEXTS",
|
||||||
"OsvNote",
|
"OsvNote",
|
||||||
"group_row_warnings",
|
|
||||||
"hierarchy_note",
|
"hierarchy_note",
|
||||||
"unknown_label_warning",
|
|
||||||
"notes_of",
|
"notes_of",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -34,10 +31,6 @@ OSV_NO_DATA_ROWS = "OSV_NO_DATA_ROWS"
|
|||||||
OSV_BALANCE_IDENTITY = "OSV_BALANCE_IDENTITY"
|
OSV_BALANCE_IDENTITY = "OSV_BALANCE_IDENTITY"
|
||||||
OSV_PARENT_SUM = "OSV_PARENT_SUM"
|
OSV_PARENT_SUM = "OSV_PARENT_SUM"
|
||||||
OSV_TOTAL = "OSV_TOTAL"
|
OSV_TOTAL = "OSV_TOTAL"
|
||||||
WARNING_ROWS_SHOWN = 20 # сколько номеров строк показывать в сгруппированном предупреждении (как у `checks.py`)
|
|
||||||
_ROW_WARNING = re.compile(r"^строка (\d+): (.*)$", re.DOTALL)
|
|
||||||
_UNKNOWN_LABEL_TAIL = " в колонке «Показатели» не распознана — "
|
|
||||||
_UNKNOWN_LABEL = re.compile(r"^метка «(.*)»" + _UNKNOWN_LABEL_TAIL + r"строка не участвует в проверках$", re.DOTALL)
|
|
||||||
OSV_XLSB_NO_OUTLINE = "OSV_XLSB_NO_OUTLINE" # заметка читателя xlsb: книга дописывает её в каждый документ ОСВ
|
OSV_XLSB_NO_OUTLINE = "OSV_XLSB_NO_OUTLINE" # заметка читателя xlsb: книга дописывает её в каждый документ ОСВ
|
||||||
|
|
||||||
|
|
||||||
@ -65,64 +58,3 @@ def hierarchy_note(strategy: str) -> OsvNote:
|
|||||||
|
|
||||||
def notes_of(code: str, messages: Iterable[str]) -> list[OsvNote]:
|
def notes_of(code: str, messages: Iterable[str]) -> list[OsvNote]:
|
||||||
return [OsvNote(code, message) for message in messages]
|
return [OsvNote(code, message) for message in messages]
|
||||||
|
|
||||||
|
|
||||||
def unknown_label_warning(row: int, label: str) -> str:
|
|
||||||
"""Предупреждение об одной строке с меткой колонки «Показатели», которой нет в словарях."""
|
|
||||||
return f"строка {row}: метка «{label}»{_UNKNOWN_LABEL_TAIL}строка не участвует в проверках"
|
|
||||||
|
|
||||||
|
|
||||||
def _count(number: int) -> str:
|
|
||||||
return f"{number:,}".replace(",", " ")
|
|
||||||
|
|
||||||
|
|
||||||
def _rows_shown(rows: list[int]) -> str:
|
|
||||||
shown = ", ".join(str(number) for number in rows[:WARNING_ROWS_SHOWN])
|
|
||||||
return f"{shown}, …" if len(rows) > WARNING_ROWS_SHOWN else shown
|
|
||||||
|
|
||||||
|
|
||||||
def _plural_rows(number: int) -> str:
|
|
||||||
last_two = number % 100
|
|
||||||
if number % 10 == 1 and last_two != 11: # noqa: PLR2004 - склонение «строка / строки / строк»
|
|
||||||
return "строка"
|
|
||||||
if 2 <= number % 10 <= 4 and not 12 <= last_two <= 14: # noqa: PLR2004
|
|
||||||
return "строки"
|
|
||||||
return "строк"
|
|
||||||
|
|
||||||
|
|
||||||
def group_row_warnings(messages: Iterable[str]) -> list[str]:
|
|
||||||
"""Построчные предупреждения «строка N: <текст>» с одним текстом — в одно с числом и номерами строк (AURORA-1724).
|
|
||||||
|
|
||||||
Одиночное предупреждение остаётся как есть. Нераспознанная метка «Показатели»: «метка «X» в колонке «Показатели»
|
|
||||||
не распознана — 2 128 строк (11, 14, …) не участвуют в проверках»; прочие — «<текст> — строк: N (…)». Номера
|
|
||||||
ограничены `WARNING_ROWS_SHOWN`, порядок групп — по первому появлению; сообщения без номера строки не меняются.
|
|
||||||
"""
|
|
||||||
items = list(messages)
|
|
||||||
groups: dict[str, list[int]] = {}
|
|
||||||
for message in items:
|
|
||||||
found = _ROW_WARNING.match(message)
|
|
||||||
if found:
|
|
||||||
groups.setdefault(found.group(2), []).append(int(found.group(1)))
|
|
||||||
result: list[str] = []
|
|
||||||
done: set[str] = set()
|
|
||||||
for message in items:
|
|
||||||
found = _ROW_WARNING.match(message)
|
|
||||||
if not found:
|
|
||||||
result.append(message)
|
|
||||||
continue
|
|
||||||
text = found.group(2)
|
|
||||||
rows = groups[text]
|
|
||||||
if len(rows) == 1:
|
|
||||||
result.append(message)
|
|
||||||
elif text not in done:
|
|
||||||
done.add(text)
|
|
||||||
result.append(_grouped(text, rows))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _grouped(text: str, rows: list[int]) -> str:
|
|
||||||
label = _UNKNOWN_LABEL.match(text)
|
|
||||||
count = f"{_count(len(rows))} {_plural_rows(len(rows))} ({_rows_shown(rows)})"
|
|
||||||
if label:
|
|
||||||
return f"метка «{label.group(1)}»{_UNKNOWN_LABEL_TAIL}{count} не участвуют в проверках"
|
|
||||||
return f"{text} — строк: {_count(len(rows))} ({_rows_shown(rows)})"
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from src.parser.checks import run_checks
|
|||||||
from src.parser.deadline import Deadline
|
from src.parser.deadline import Deadline
|
||||||
from src.parser.grid import Sheet
|
from src.parser.grid import Sheet
|
||||||
from src.parser.osv import OsvParser
|
from src.parser.osv import OsvParser
|
||||||
from src.parser.osv_warnings import OsvNote, group_row_warnings, hierarchy_note
|
from src.parser.osv_warnings import OsvNote, hierarchy_note
|
||||||
from src.parser.rules import Rules, load_rules
|
from src.parser.rules import Rules, load_rules
|
||||||
|
|
||||||
SUMS = (None, None, 100, 100, None, None)
|
SUMS = (None, None, 100, 100, None, None)
|
||||||
@ -92,68 +92,3 @@ def test_reader_notes_are_appended_last(rules: Rules) -> None:
|
|||||||
|
|
||||||
assert result.notes[-1] == note
|
assert result.notes[-1] == note
|
||||||
assert result.document.warnings[-1].code == "OSV_XLSB_NO_OUTLINE"
|
assert result.document.warnings[-1].code == "OSV_XLSB_NO_OUTLINE"
|
||||||
|
|
||||||
|
|
||||||
def _indicator_rows(label: str, count: int) -> list[list[object]]:
|
|
||||||
rows: list[list[object]] = [
|
|
||||||
[],
|
|
||||||
["Оборотно-сальдовая ведомость по счету: 76"],
|
|
||||||
["Контрагенты"],
|
|
||||||
["за 2025 г."],
|
|
||||||
['ООО "Тест"'],
|
|
||||||
[],
|
|
||||||
[
|
|
||||||
"Субконто",
|
|
||||||
"Показатели",
|
|
||||||
M("Сальдо на начало периода", 2),
|
|
||||||
M("Обороты за период", 2),
|
|
||||||
M("Сальдо на конец периода", 2),
|
|
||||||
],
|
|
||||||
[None, None, "Дебет", "Кредит", "Дебет", "Кредит", "Дебет", "Кредит"],
|
|
||||||
]
|
|
||||||
for number in range(count):
|
|
||||||
rows.append([f"Контрагент {number}", "БУ", 10, None, None, None, 10, None])
|
|
||||||
rows.append([None, label, 5, None, None, None, 5, None])
|
|
||||||
rows.append(["Итого", "БУ", 10 * count, None, None, None, 10 * count, None])
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_indicator_label_gives_one_warning_for_all_rows(rules: Rules) -> None:
|
|
||||||
"""AURORA-1724: неизвестная метка «Показатели» в 30 строках — одно предупреждение с числом и номерами строк."""
|
|
||||||
count = 30
|
|
||||||
result = OsvParser(rules).parse_sheet(sheet_from_rows(_indicator_rows("Неизв.", count)), Deadline(60))
|
|
||||||
texts = [note.message for note in result.notes if note.code == "OSV_ROWS" and "«Неизв.»" in note.message]
|
|
||||||
assert len(texts) == 1, texts
|
|
||||||
assert "30 строк (10, 12, 14," in texts[0]
|
|
||||||
assert "не участвуют в проверках" in texts[0]
|
|
||||||
assert "…" in texts[0] # номера строк ограничены, как у OSV_PARENT_SUM
|
|
||||||
|
|
||||||
|
|
||||||
def test_single_unknown_indicator_label_keeps_row_wording(rules: Rules) -> None:
|
|
||||||
result = OsvParser(rules).parse_sheet(sheet_from_rows(_indicator_rows("Неизв.", 1)), Deadline(60))
|
|
||||||
texts = [note.message for note in result.notes if "«Неизв.»" in note.message]
|
|
||||||
assert texts == ["строка 10: метка «Неизв.» в колонке «Показатели» не распознана — строка не участвует в проверках"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_different_unknown_labels_are_grouped_separately() -> None:
|
|
||||||
messages = [
|
|
||||||
"строка 3: метка «А» в колонке «Показатели» не распознана — строка не участвует в проверках",
|
|
||||||
"строка 4: метка «Б» в колонке «Показатели» не распознана — строка не участвует в проверках",
|
|
||||||
"строка 5: метка «А» в колонке «Показатели» не распознана — строка не участвует в проверках",
|
|
||||||
]
|
|
||||||
assert group_row_warnings(messages) == [
|
|
||||||
"метка «А» в колонке «Показатели» не распознана — 2 строки (3, 5) не участвуют в проверках",
|
|
||||||
"строка 4: метка «Б» в колонке «Показатели» не распознана — строка не участвует в проверках",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_same_text_row_warnings_are_grouped() -> None:
|
|
||||||
messages = [
|
|
||||||
"строка 3: нечисловое значение в колонке сумм: «х»",
|
|
||||||
"строка 4: другое",
|
|
||||||
"строка 5: нечисловое значение в колонке сумм: «х»",
|
|
||||||
]
|
|
||||||
assert group_row_warnings(messages) == [
|
|
||||||
"нечисловое значение в колонке сумм: «х» — строк: 2 (3, 5)",
|
|
||||||
"строка 4: другое",
|
|
||||||
]
|
|
||||||
|
|||||||
@ -551,11 +551,8 @@ design #14).
|
|||||||
1. в таблице есть колонка «Показатели» (заголовок блока по словарю `osv_headers.yaml` — `service_columns.indicator`,
|
1. в таблице есть колонка «Показатели» (заголовок блока по словарю `osv_headers.yaml` — `service_columns.indicator`,
|
||||||
«Показатели», «Показа-тели»), ячейка строки в ней непуста и метка не «БУ»: «НУ» → `data_type = NU`, «Кол.», «Кол. доп.» →
|
«Показатели», «Показа-тели»), ячейка строки в ней непуста и метка не «БУ»: «НУ» → `data_type = NU`, «Кол.», «Кол. доп.» →
|
||||||
QUANTITY, «Вал.» → CURRENCY (словарь `data_types.yaml` — `indicator_column`); «ПР», «ВР» (`indicator_column_untyped`)
|
QUANTITY, «Вал.» → CURRENCY (словарь `data_types.yaml` — `indicator_column`); «ПР», «ВР» (`indicator_column_untyped`)
|
||||||
→ `data_type = null`; метка не из словарей → `data_type = null` и предупреждение `OSV_ROWS`: одно на каждую
|
→ `data_type = null`; метка не из словарей → `data_type = null` и предупреждение `OSV_ROWS` «строка N: метка «X» в
|
||||||
неизвестную метку, а не на каждую строку — «метка «X» в колонке «Показатели» не распознана — 2 128 строк (11, 14, …)
|
колонке «Показатели» не распознана — строка не участвует в проверках»;
|
||||||
не участвуют в проверках» (показаны первые 20 номеров; для единственной строки текст прежний: «строка N: метка «X» в
|
|
||||||
колонке «Показатели» не распознана — строка не участвует в проверках»); так же группируются прочие построчные
|
|
||||||
предупреждения `OSV_ROWS` с одинаковым текстом («строка N: <текст>» → «<текст> — строк: K (…)»; AURORA-1724);
|
|
||||||
2. иначе — текст строки матчит `data_types.yaml` — `row` («Вал.», «Кол.», «Кол-во», «Кол-во, <единица>», «В валюте» → CURRENCY/QUANTITY) или
|
2. иначе — текст строки матчит `data_types.yaml` — `row` («Вал.», «Кол.», «Кол-во», «Кол-во, <единица>», «В валюте» → CURRENCY/QUANTITY) или
|
||||||
равен коду валюты из `currency_codes`.
|
равен коду валюты из `currency_codes`.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user