# Design: Срез 2b — парсер ББ и ОФР из Excel ## Context - Точка подключения прежняя: `get_parser(settings)` → `Parser.parse(data, file_type) -> ParseOutcome`; воркер (`core/worker.py`) вызывает парсер в `asyncio.to_thread`, собирает конверт и пишет `results/{file_id}/{job_id}/result.json`. `ParseOutcome` требует непустые `documents` при `done|needs_check`. - Сейчас `OsvParser.parse` делает и оркестрацию книги (лимит, чтение, дедлайн, `classify_sheet`, цикл по листам), и конвейер листа ОСВ (`parse_sheet`). Листы `SheetKind.STATEMENT` пропускаются с предупреждением. - Контракт v1.1 уже содержит `statementDocument{document_type BALANCE|INCOME|OTHER, locator, header{form_name, organization_name, inn, report_date_text, report_date, units, okud, okpo, okopf, okfs, address, *_confidence}, columns[{column_index, label, period_date}], lines[{line_code, line_name, *_confidence, values[{column_index, amount, string_value, confidence, is_recognized}]}]}` — поля «как у OCR». Табличный путь заполняет `confidence = 100`, `is_recognized = true` (§12). - Реальные файлы (`../fixtures/statements-xlsx/**`, 15 файлов + 4 шаблона), три раскладки: 1. **1С-xls постранично** (Унагранде Компани, Унагранде Групп): один файл = одна страница формы; на странице 1 титульный блок (организация, ИНН, ОКПО, ОКВЭД, ОКОПФ/ОКФС, единица «в тыс. рублей», ОКЕИ 384, адрес, аудитор с **своим** ИНН), заголовок таблицы «Пояснения | Наименование показателя | Код | На 31 декабря 2024 г. | … 2023 | … 2022»; на странице 2 только «Форма 0710001 с.2» и та же строка заголовка таблицы. Под строками 1110/1150/1540 — пятизначные подстроки 11101, 11102 (1С «в том числе» без слов), есть строки без кода и без имени с нулями. Значения — рубли с копейками при единице «тыс. руб.». 2. **Многолистовая xlsx ФСБУ 4/2023** (НПЗ): ББ на 5 листах, ОФР на 3; на первом листе титул «Бухгалтерская отчетность за январь - сентябрь 2025 г.» и ниже заголовок «Бухгалтерский баланс | на 30 сентября 2025 г. | 0710001»; листы 2–5 начинаются сразу со строк («в том числе:», 11901, 1100, …) без заголовка; есть строка 1105 «Гудвил», подстроки 12301…12314, 23201…, в ОФР строка без кода с суммами («Реализация иного имущества…»). 3. **`TDSheet`** (Межениновская): вся форма на одном листе, заголовок «БУХГАЛТЕРСКИЙ БАЛАНС / за 9 месяцев 2025 г.», дата тройкой «30 | 9 | 2025» (числа, не строки), колонки «Наименование показателя | Коды | За 9 месяцев\n2025 года | На 31 декабря\n2024 года | …», под шапкой строка нумерации «1 | 2 | 3 | 4 | 5», значения — тысячи с дробной частью. 4. **Шаблоны** 66н и ФСБУ 4/2023: заголовок таблицы разнесён на две строки («На» / «20__ г.»), коды есть, сумм нет, лист содержит обе страницы формы с маркером «Форма 0710001 с. 2» посередине; в шаблоне ФСБУ строка «АКТИВ» и код 1110 стоят в одной строке из-за объединений (образец формы, не данные). - Параллельно в ветках issues #9/#10/#11 правятся модули ОСВ (`header.py`, `classify.py`, `checks.py`, `hierarchy.py`, словари ОСВ, golden ОСВ). Этот change их не трогает. ## Goals / Non-Goals **Goals:** все 15 реальных файлов разбираются без правил под файл, организацию, имя листа или номер строки; полные формы (НПЗ, Межениновская) дают `done` с сошедшимися жёсткими соотношениями §7; постраничные xls дают документы с данными и честный `needs_check` для страниц без шапки; формулы §7 и синонимы — данные; конвейер форм изолирован от ОСВ так, что параллельные правки ОСВ не конфликтуют; контракт растёт только MINOR-ом. **Non-Goals:** см. proposal. ## Decisions ### D-1. `BookParser` — оркестратор книги, `OsvParser` — конвейер листа `book.py`: `BookParser(osv: OsvParser, statements: StatementParser, rules: Rules, max_bytes, timeout_seconds, clock)`. `parse()` повторяет нынешний `OsvParser.parse`/`_parse_book`: лимит `PARSE_MAX_BYTES` → `read_book` → `Deadline` → для каждого листа `classify_sheet` → `OSV` в `osv.parse_sheet`, `STATEMENT`/`STATEMENT_CONTINUATION` накапливаются и передаются `statements.parse_sheets(sheets_in_order, deadline)` **одним вызовом** (склейка продолжений требует видеть соседние листы), `OTHER` → предупреждение. Документы книги — ОСВ в порядке листов, затем формы в порядке листов. Статус `done`, если все документы `ok`; ни одного документа ОСВ или формы → `InvalidDocumentError` («в книге нет листов ОСВ или форм»). `OsvParser` теряет `parse`/`_parse_book`, оставляет `parse_sheet` и свойства; `get_parser` собирает `BookParser`. `NullParser` без изменений. *Альтернативы:* ветка внутри `osv.py` (смешение доменов, конфликты с #9–#11); два `Parser`-а с композитом (книга читается дважды, дедлайн и лимиты делятся). *Цена ошибки:* `BookParser` тонкий, переносится. ### D-2. Классификация листа — три вида для форм, признаки по области до таблицы `detect.classify_sheet(sheet, rules) -> SheetKind` расширяется значением `STATEMENT_CONTINUATION`. Порядок проверки прежний: признак ОСВ → `OSV`; иначе признак формы (заголовок формы или ОКУД из `statement_headers.yaml`) в любой строке **до** первой строки, похожей на заголовок таблицы форм («Код»/«Коды» и справа ячейки-периоды) либо до первой строки с кодом строки формы → `STATEMENT`; иначе маркер продолжения («Форма 0710001 с.2» / «с. 2») **или** строка заголовка таблицы форм без заголовка формы **или** (без заголовка таблицы) колонка, где ≥ 60 % непустых значений — коды форм, → `STATEMENT_CONTINUATION`; иначе `OTHER`. Ограничение «первые N строк» для признаков формы снимается: у НПЗ заголовок формы стоит после титульного блока. Признак ОСВ по-прежнему ищется там же, где сейчас (модуль ОСВ не меняется). ### D-3. Тип формы и разрез листа `statements/detect.py`: `find_forms(sheet, rules) -> list[FormBlock(kind: BALANCE|INCOME|DISCLOSURE, title_row, okud, table_header_row, first_data_row, last_row)]`. Тип по заголовку/ОКУД из `statement_headers.yaml`: «Бухгалтерский баланс» / 0710001 → BALANCE; «Отчет о финансовых результатах», «Отчет о прибылях и убытках» / 0710002 → INCOME; «Пояснения к бухгалтерскому балансу», «Расшифровка», «Отчет об изменениях капитала» (0710003), «Отчет о движении денежных средств» (0710004) и прочие ОКУД 0710xxx → DISCLOSURE. Титул «Бухгалтерская отчетность» — не тип: тип берёт ближайший следующий заголовок. Два заголовка форм на листе → два блока, второй начинается со своего заголовка (§12 п. 1). Если ОКУД и текст заголовка противоречат — предупреждение, тип по ОКУД. DISCLOSURE → документ с `locator`, шапкой (если нашлась) и пустыми `columns`/`lines`; на статус книги не влияет. ### D-4. Заголовок таблицы, колонка кодов, колонка наименований, колонки-периоды `statements/columns.py`. Строка заголовка таблицы — первая после заголовка формы строка, где есть ячейка «Код»/«Коды» (синонимы в словаре) и правее неё хотя бы одна ячейка, матчащая период из `statement_periods.yaml`; при отсутствии слова «Код» — строка, под которой начинается колонка кодов. Заголовок, разнесённый на две строки (шаблоны: «На» + «20__ г.»; Межениновская: перенос строки внутри ячейки), склеивается: текст ячейки и текст ячейки той же колонки в следующей строке, если та строка не содержит кодов и сумм. Строка нумерации колонок («1 2 3 4 5» подряд) пропускается. Колонка кодов: среди колонок между наименованием и первой колонкой-периодом та, где ≥ 60 % непустых значений в блоке — коды форм (`^\d{4}$` или `^\d{5}$` в диапазонах 1100–1799 / 2100–2999 и их пятизначных расширениях, плюс 5xxx для расшифровок); значения читаются как текст (числа 1110.0 → «1110»). Нет такой колонки → документ без строк, `needs_check`, предупреждение «колонка кодов не найдена» (решение владельца 06.10.2026, вопрос 6). Колонка наименований — ближайшая левее кодов колонка с преобладанием текста; колонка «Пояснения» исключается по заголовку и по тому, что её значения — пустые или короткие (≤ 3 символов). Колонки-периоды — все ячейки заголовка правее кодов с распознанным периодом, по порядку слева направо: `column_index` 0, 1, 2…; `period_date` — дата («На 31 декабря 2024 г.», «31.12.2024») или конец периода («За 9 месяцев 2025 г.» → 2025-09-30, «За январь - сентябрь 2025 г.» → 2025-09-30, «За 2024 г.» → 2024-12-31). Ячейки заголовка правее кодов без периода («Изменение», «%») не становятся колонками, их значения не читаются, одно предупреждение на документ. `is_current` в контракте нет — ядро берёт самую позднюю дату. ### D-5. Шапка документа `statements/header.py`: область — от начала блока формы до строки заголовка таблицы; для первого блока на листе — от начала листа (титульный блок НПЗ над заголовком формы входит в шапку баланса); шапка другого листа MUST NOT использоваться (у ОФР НПЗ нет метки «ИНН» → `inn = null`). Метки и регулярки — в `statement_headers.yaml`; значение метки — ближайшая непустая ячейка правее в той же строке, иначе первая непустая ячейка следующей строки (адрес, имя организации под меткой). Особые правила: `inn` — 10 или 12 цифр у метки «ИНН», строки после метки «аудитор» исключаются; `organization_name` — после «Организация», а если справа пусто и строкой выше есть текст с признаком ОПФ (ООО/АО/ПАО/ЗАО/кавычки) — он (НПЗ); `okopf`/`okfs` — у общей метки «по ОКОПФ / ОКФС» два числа по порядку; `units` — текст после «Единица измерения» как есть, ОКЕИ 384/385 рядом только подтверждает; `report_date_text` — подзаголовок формы («на 31 декабря 2024 г.», «за 9 месяцев 2025 г.», «за январь - сентябрь 2025 г.»), `report_date` — его дата или конец периода; при отсутствии — тройка «число, месяц, год» у метки «Дата»/«Отчетная дата» (строки или числа). Для ОФР дата отчёта — конец периода. `*_confidence = 100` там, где поле найдено, `null` — где нет. Блок продолжения без шапки → все поля `null`, `form_name` из маркера «Форма 0710001 с.2» (тип по ОКУД в маркере). ### D-6. Строки и значения `statements/lines.py`: строка блока с кодом в колонке кодов → `StatementLine(line_code=<текст без пробелов>, line_name=, line_code_confidence=100, line_name_confidence=100)`; строки без кода (разделы, «в том числе:», пустые с нулями, строка «Реализация иного имущества…» у НПЗ) не отдаются (`ASSUMPTION(OQ-36)`), их число — в предупреждение «строк без кода пропущено: N» только если среди них есть суммы. Пятизначные подстроки отдаются как обычные строки (решение владельца, вопрос 4). Повтор кода внутри документа → остаётся первая строка, код в `checks.duplicate_line_codes`, `needs_check`. Значения: для каждой колонки-периода `StatementValue(column_index, amount, string_value, confidence=100, is_recognized)`: число → `amount` через `numbers.parse_money` (Decimal, 2 знака, скобки → минус, пробелы/NBSP убираются), `is_recognized = true`; пусто/«–»/«-» → `amount = null`, `string_value = null`, `is_recognized = true`; нечисловой текст → `amount = null`, `string_value = <текст>`, `is_recognized = false`. Единицы не нормализуются: если `header.units` содержит «тыс.» и хотя бы у 10 % значений дробная часть ≠ 0, в предупреждения пишется «N значений с дробной частью при единице тыс. руб.» (решение владельца, вопрос 3; новый OQ в docs). ### D-7. Соотношения §7 — данные в `statement_checks.yaml` Формат записи: `{form: BALANCE|INCOME, relation: "1600 = 1100 + 1200", strict: true|false, abs_terms: [2120, 2210, …]}`. Вычисление в `statements/checks.py` — чистая функция над `Mapping[str, Decimal | None]` по колонке: отсутствующий код — 0; правая часть считается дважды — как записано и с членами `abs_terms` по модулю (1С показывает расходы со знаком минус, 66н — в скобках, оба варианта допустимы); соотношение выполнено, если сходится хотя бы один вариант с допуском ±1 в единицах формы (`Decimal("1")`). Жёсткие (`strict`): ББ `1100 = 1110 + 1120 + 1130 + 1140 + 1150 + 1160 + 1170 + 1180 + 1190` (+1105 при наличии, `ASSUMPTION(OQ-37)`), `1200 = 1210 + 1220 + 1230 + 1240 + 1250 + 1260` (+1215), `1300 = 1310 + 1320 + 1340 + 1350 + 1360 + 1370`, `1400 = 1410 + 1420 + 1430 + 1450`, `1500 = 1510 + 1520 + 1530 + 1540 + 1550`, `1600 = 1100 + 1200`, `1700 = 1300 + 1400 + 1500`, `1600 = 1700`; ОФР `2100 = 2110 − 2120`, `2200 = 2100 − 2210 − 2220`, `2300 = 2200 + 2310 + 2320 − 2330 + 2340 − 2350`. Мягкие (`strict: false`, только предупреждение): `2400 = 2300 − 2410 + 2420` и варианты ФСБУ 4/2023 с 2411/2412/2460 (`ASSUMPTION(OQ-12)`), `2500 = 2400 + 2510 + 2520 + 2530`, и «сумма пятизначных подстрок = родитель» (генерируется по коду: `1110 = 11101 + 11102 + …`, только когда подстроки есть). Проверка по каждой колонке отдельно; результат — `StatementChecks(relations_failed[{column_index, relation, expected, actual}], soft_relations_failed[…], duplicate_line_codes[])` + строки в предупреждения. Для форм без строк (нет колонки кодов, DISCLOSURE) `checks` не заполняется. ### D-8. Склейка листов-продолжений внутри книги, статус документа `statements/merge.py`: `parse_sheets` идёт по листам книги по порядку; блок `STATEMENT` открывает документ; лист `STATEMENT_CONTINUATION` присоединяется к последнему открытому документу формы, если число колонок-сумм в нём совпадает (или заголовка таблицы на листе нет и число колонок с числами совпадает) — строки добавляются в конец, `locator` = `sheet:<первый>` + `,<следующие имена>` через запятую. Продолжение без открытого документа (книга из одной страницы 2, лист ОСВ между страницами) → документ-одиночка: шапка пустая, тип по ОКУД маркера или `OTHER`, `needs_check`, предупреждение «страница-продолжение без шапки: загрузите форму одним файлом» (решение владельца, вопрос 2; переход к признаку `is_continuation` в контракте — две необязательных поля, без смены логики). Документ `ok` ⇔ все жёсткие соотношения сошлись по всем колонкам ∧ нет дубликатов кодов ∧ найдена колонка кодов ∧ есть хотя бы одна колонка-период ∧ в шапке есть организация или ИНН ∧ документ не одиночка-продолжение. Шаблоны (коды есть, сумм нет) → все значения `null`, соотношения «0 = 0» сходятся, шапка пустая → `needs_check`. ### D-9. Контракт v1.2 — модели и копия `api/src/contract/result.py`: `StatementDocument.document_type: Literal["BALANCE","INCOME","DISCLOSURE","OTHER"]`; `StatementChecks` и `StatementDocument.checks: OptionalNotNull[StatementChecks] = None`; `FailedRelation(column_index, relation, expected: Money, actual: Money)`; `OsvHeader.period_start: date | None`, `period_end: date | None`, `is_from_year_start: bool | None`. `CONTRACT_VERSION = "1.2"`; копия схемы и примеры из `../docs/contracts` v1.2 (тест идентичности). Заполнение новых полей шапки ОСВ: `period_start`/`period_end` — по разобранному периоду (квартал/год → первое и последнее число; диапазон месяцев из issue #11 — его границы), `is_from_year_start` — `period_start` = 1 января. Если ветка #11 сливается раньше и уже вычисляет границы диапазона, заполнение делается здесь одной задачей на его результате; если позже — поля в этом change заполняются для квартала/года, а для диапазона — в #11. Поля необязательные, оба порядка совместимы. ### D-10. Словари форм — отдельные файлы `statement_headers.yaml` (заголовки форм и ОКУД по типам, маркер продолжения, метки шапки, синонимы «Код», «Наименование показателя», «Пояснения», признаки ОПФ, метки-исключения «аудитор»), `statement_periods.yaml` (месяцы, формы «на <дата>», «за месяцев <год>», «за <месяц> - <месяц> <год>», «за <год> г.», «<дд.мм.гггг>», «<год>»), `statement_checks.yaml` (D-7). `periods.yaml` ОСВ не переиспользуется и не правится — ветка #11 его меняет. Загрузчик `rules.py` расширяется моделями форм; тест покрытия словарей — на объединении файлов `statements-xlsx/**` и синтетики. ### D-11. Тесты, golden, синтетика Unit (`tests/unit/parser/statements/`) на `gridbuilder`; property (`tests/fixtures/test_statements_property.py`, маркер `fixtures`) по манифесту `tests/fixtures/statements_manifest.yaml` (каталог → организация, ИНН, ожидаемые типы и статусы, полная форма или страница); golden `../fixtures/golden/expected_statement_parse///.json` через обобщённые `golden_helpers` и `make golden-update`; синтетика в `tests/synthetic/generate.py`: `st01_both_forms_one_sheet`, `st02_csv_like`, `st03_no_codes`, `st04_mixed_book` (ОСВ + ББ), `st05_continuation_only`; линтер «нет координат» включает `statements/**` и `book.py`; интеграционный тест воркера: книга НПЗ → `done`, `result_schema_version = "1.2"`, два документа. CLI печатает формы так же, как ОСВ. ## Risks / Trade-offs - [Колонка кодов не найдена из-за кодов-чисел или объединений] → коды читаются как текст из любого типа ячейки, merged берётся по origin; синтетика `st02`. Остаточный риск закрывается фикстурой + синонимом, не `if`. - [Соотношения §7 не сходятся из-за знаков 1С] → двойной расчёт с `abs_terms`; при несходимости обоих — `needs_check`, перечень в `checks`; знак не угадывается. - [Постраничные xls всегда `needs_check`] → осознанно (вопрос 2); ядро в Срезе 3 решит склейку; данные не теряются. - [Конфликт с ветками #9–#11 в `osv.py`/`test_osv_parser.py`] → правило при rebase: логика ОСВ из их ветки, оркестрация книги отсюда; PR создаётся после rebase на main. - [Контракт 1.2 ломает идентичность копии в arm-core и scan-gateway] → bounded-PR с копией схемы и моделями раньше мержа парсера; порядок merge docs → arm-core/scan-gateway → tabular-parser → fixtures. - [Большие книги с формами] → те же лимиты и дедлайн, что у ОСВ; формы маленькие (≤ 100 строк на лист). ## Migration Plan Не требуется: сервис без состояния. После merge — пересборка образа; потребители контракта обновляют копии раньше. ## Open Questions Не блокируют: OQ-12 (знаки 2410/2420/2430 — мягкие проверки), OQ-36 (строки без кода — не отдаются), OQ-23 (расшифровки — только классификация), OQ-37 (1105 как есть), OQ-131 (границы диапазона — D-9). Новые вопросы вносятся в `07-open-questions.md` сквозным change'ем: единицы «тыс. руб.» против значений с копейками в 1С-выгрузках; форма без колонки кодов (сопоставление по именам); страница-продолжение в отдельном файле (склейка в ядре, признак в контракте); строка без кода с суммами в ОФР ФСБУ (НПЗ «Реализация иного имущества…»); шаблон ФСБУ с «АКТИВ» и 1110 в одной строке. ## Decisions made without discussion | № | Дата | Решение | Обоснование | Цена ошибки | |---|---|---|---|---| | 1 | 2026-10-06 | D-2: продолжение определяется маркером «с.2» либо заголовком таблицы/колонкой кодов без заголовка формы | у НПЗ листы 2–5 без маркера, у 1С — с маркером | добавить признак в словарь | | 2 | 2026-10-06 | D-3: 0710003/0710004 и прочие ОКУД 0710xxx → DISCLOSURE, не OTHER | DBML различает расшифровку и прочее; ОДДС/ОИК для консолидации не нужны | переназначить тип | | 3 | 2026-10-06 | D-4: порог 60 % кодов в колонке и диапазоны 1100–1799 / 2100–2999 (+5xxx) | §12 п. 2; 1105 и 1215 внутри диапазона | подвинуть границы в словаре | | 4 | 2026-10-06 | D-4: колонка заголовка без периода не читается, одно предупреждение | §12 п. 4 «в extra», но у `statementDocument` поля `extra` нет; MINOR ради диагностики не оправдан | добавить `extra` в v1.x | | 5 | 2026-10-06 | D-5: ИНН аудитора исключается по метке «аудитор» выше | иначе у Унагранде второй ИНН в шапке | синоним в словаре | | 6 | 2026-10-06 | D-6: порог 10 % значений с дробной частью для предупреждения о единицах | единичные копейки в тысячах бывают от округления | число в словаре | | 7 | 2026-10-06 | D-7: допуск ±1 единица формы, отсутствующий код = 0, двойной расчёт с `abs_terms` | §7 «расхождение > 0», но формы в тыс. руб. округляются построчно; знаки 1С vs 66н | допуск в словаре | | 8 | 2026-10-06 | D-7: «сумма подстрок = родитель» — мягкая проверка, генерируется по коду | подстроки бывают неполными («прочее» без кода) | перевести в строгие | | 9 | 2026-10-06 | D-8: критерий `ok` документа включает «в шапке есть организация или ИНН» | без шапки ядро не сверит владельца; страница 2 должна быть `needs_check` | ослабить критерий | | 10 | 2026-10-06 | D-9: `period_start/period_end/is_from_year_start` в шапке ОСВ добавляются в контракт здесь, заполнение делится с #11 | владелец: поля в v1.2 (вопрос 5) | follow-up на заполнение | | 11 | 2026-10-06 | D-1: документы книги — сначала ОСВ, потом формы | детерминированный порядок для golden | порядок листов | | 12 | 2026-10-06 | Контракт v1.2 раскатан в arm-core (PR #27) и scan-gateway (PR #5) копиями; в scan-gateway вместе с v1.2 догнан v1.1 (`OsvRow.extra`, пример `result-osv-needs-check.json`), `CONTRACT_VERSION` обоих репозиториев не менялся | копия схемы обязана быть байт в байт с docs; потребители версию не повышают, пока не пишут новых полей | откат отдельным коммитом, поле необязательное (R-2) | | 13 | 2026-10-06 | Минор-находки ревью задач 1–2 по `$comment` схемы («1.1»), статусу примера excel и `checks` в OCR-примере minimal отложены (правка в docs/contracts + пересинхронизация копий) | план запрещал трогать `$comment`; примеры предписаны планом | косметика примеров; ядро/парсер не затронуты (T1.1, T2.2, T2.3) | | 14 | 2026-10-06 | Golden `schema_version` 1.1 → 1.2 (113 файлов) пересозданы сразу и закоммичены в ветку `feat/14-statements-golden` worktree `fixtures-wt` (шаг 1 задачи 13 выполнен раньше), симлинк `fixtures` переключён на worktree; основной checkout fixtures не тронут | смена `CONTRACT_VERSION` ломала golden до задачи 14, `make check` был бы красным на задачах 4–13 | коммит в fixtures-ветке раньше плана; откат — reset ветки (R-3) | | 15 | 2026-10-06 | `docs/contracts/**` после задачи 1 заморожены: в задаче 16 не правятся; T1.1/T2.1/T2.2/T2.3 остаются deferred и уходят в follow-up | копии схемы в arm-core#27 и scan-gateway#5 на ревью и должны остаться байт в байт равными docs/contracts на main | три косметических замечания живут до следующего MINOR (R-4, указание координатора) | | 16 | 2026-10-06 | Находка T6.I1 (osv.py не знает SheetKind.STATEMENT_CONTINUATION) не чинится в задаче 6, закрывается заменой osv.py::parse на BookParser в задаче 7 | реальные фикстуры ОСВ листов-продолжений не содержат, промежуточное состояние невидимо | если T7 не покроет — поймает финальное ревью (R-5) |