diff --git a/api/src/parser/header.py b/api/src/parser/header.py index a4d90bf..87e0c7e 100644 --- a/api/src/parser/header.py +++ b/api/src/parser/header.py @@ -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) diff --git a/api/src/parser/rules.py b/api/src/parser/rules.py index 977f49a..7062b58 100644 --- a/api/src/parser/rules.py +++ b/api/src/parser/rules.py @@ -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: diff --git a/api/tests/unit/parser/test_book_parser.py b/api/tests/unit/parser/test_book_parser.py index 5b4ffda..07ae8aa 100644 --- a/api/tests/unit/parser/test_book_parser.py +++ b/api/tests/unit/parser/test_book_parser.py @@ -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") diff --git a/api/tests/unit/parser/test_header.py b/api/tests/unit/parser/test_header.py index fdd9dce..7ae7a0c 100644 --- a/api/tests/unit/parser/test_header.py +++ b/api/tests/unit/parser/test_header.py @@ -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(