fix(osv): границы периода по названным месяцам («Январь - Май 2025 г.», один месяц) (T8.I1)

Refs #14

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
This commit is contained in:
Andrey Epifancev 2026-10-06 16:08:26 +04:00
parent 8990ab26fe
commit f279157486
4 changed files with 41 additions and 0 deletions

View File

@ -382,6 +382,10 @@ def _parse_period(text: str, rules: Rules, warnings: list[str]) -> _Period:
quarter = None if rule is None else rule.quarter
is_cumulative = None if rule is None else rule.is_cumulative
start, end = _bounds(year_value, quarter, is_cumulative)
named = rules.periods.bounds_span(text)
if named is not None:
# Месяцы названы в тексте — границы берём из них, а не из квартала словаря («Январь - Май 2025 г.» — не год)
start, end = datetime.date(named.start_year, named.start_month, 1), _month_end(named.end_year, named.end_month)
return _Period(year_value, quarter, is_cumulative, start, end)

View File

@ -286,6 +286,26 @@ class PeriodRules(_Model):
start_year = int(year_text) if year_text is not None else end_year - (1 if start > end else 0)
return MonthRange(start, start_year, end, end_year)
def bounds_span(self, text: str) -> MonthRange | None:
"""Месяцы, которые период называет прямо: диапазон (в т. ч. от января без года начала — «Январь - Май 2025 г.»)
или один месяц («Февраль 2024 г.»); иначе `None`. Нужен только для границ периода: квартал и признак
нарастающего итога по-прежнему берутся из `match`.
"""
span = self.month_range(text)
if span is not None:
return span
found = self.range.search(text)
if found is not None:
start, end = self.month_number(found.group("m1")), self.month_number(found.group("m2"))
if start == 1 and end is not None and found.group("y1") is None:
end_year = int(found.group("y2"))
return MonthRange(1, end_year, end, end_year)
return None
month, year = self.month_number(text), self.year.search(text)
if month is None or year is None:
return None
return MonthRange(month, int(year.group(0)), month, int(year.group(0)))
def match(self, text: str) -> PeriodRule | None:
"""Первое правило, паттерн которого совпал с текстом периода; порядок в YAML — от специфичных к общим."""
for rule in self.rules:

View File

@ -248,6 +248,19 @@ def test_period_bounds_reach_osv_header(rules: Rules) -> None:
)
def test_period_bounds_reach_osv_header_not_from_year_start(rules: Rules) -> None:
title = TITLE.replace("1 квартал", "3 квартал")
rows = [*head(title=title), *mini_osv()[len(head()) :]]
header = only_document(book(rules).parse(xlsx({"ОСВ 60": rows}), "XLSX")).header
assert (header.period_start, header.period_end, header.is_from_year_start) == (
datetime.date(2025, 7, 1),
datetime.date(2025, 9, 30),
False,
)
def test_missing_total_needs_check(rules: Rules) -> None:
"""R-20: нет «Итого» — сверку с корнем выполнить нельзя: `false`, предупреждение, `needs_check`."""
outcome = book(rules).parse(xlsx({"ОСВ 60": mini_osv(total=False)}), "XLSX")

View File

@ -545,6 +545,10 @@ def test_variant2_without_inn_column_leaves_has_inn_unset(rules: Rules) -> None:
("2024 г.", datetime.date(2024, 1, 1), datetime.date(2024, 12, 31), True),
("Октябрь 2024 г. - Сентябрь 2025 г.", datetime.date(2024, 10, 1), datetime.date(2025, 9, 30), False),
("Апрель - Июнь 2025 г.", datetime.date(2025, 4, 1), datetime.date(2025, 6, 30), False),
("Январь - Май 2025 г.", datetime.date(2025, 1, 1), datetime.date(2025, 5, 31), True),
("Январь - Февраль 2024 г.", datetime.date(2024, 1, 1), datetime.date(2024, 2, 29), True),
("Январь - Октябрь 2025 г.", datetime.date(2025, 1, 1), datetime.date(2025, 10, 31), True),
("Февраль 2024 г.", datetime.date(2024, 2, 1), datetime.date(2024, 2, 29), False),
],
)
def test_period_bounds(