diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 0fb34de..b3c5813 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -299,6 +299,33 @@ AURORA-1661; прикладной сценарий валютных строк на ней, а не на первой «ИТОГ по …» (та не начинается с «Итого» и итогом листа не является). Сверка «Итого = Σ строк уровня 0» (§1.4) идёт по субсчетам. +**Иерархия по отступам: подуровень счёта, строка закрытия, вложенная аналитика** (AURORA-1858, tabular-parser #78). +ОСВ 1С без outline («Субконто», вид аналитики в шапке не назван) задаёт уровни только отступом: 0 у счетов, 1 у +аналитики. Отступов не хватает в трёх случаях, стратегия `indent` их уточняет: +1. **Подуровень счёта на том же отступе.** `68.01.1` стоит на одном отступе с `68.01`. Счёт, код которого продолжает код + более раннего счёта того же отступа в том же блоке (`68.01.1` → `68.01`), становится его ребёнком, вместе со своей + аналитикой. Только если все такие пары листа на одном отступе, раскладка однозначна и сумма детей перекладываемого + родителя после перекладки сходится; иначе лист остаётся по отступам (защищает плоские списки `60`, `60.01`, `60.02`). +2. **Безымянная строка закрытия.** Строка без текста и кода сразу за строкой счёта, с оборотами, равными оборотам счёта + (1С печатает её под 90.09 и 91.09), — ребёнок счёта. Пустую ячейку отступа читатель не хранит, поэтому без правила + строка попадала на уровень 0 и удваивала сумму корней. +3. **Вложенность аналитики по суммам** (решение владельца 28.10.2026 после ревью). Если аналитика родителя на одном + отступе, а вложенность видна только по суммам (91.01: «Прочие внереализационные» = две строки ниже), то **только у + родителя с нарушением «сумма детей = родитель»** строка, равная сумме следующих соседей, жадно становится их + родителем (как стратегия `sums`). Перестройка принимается, только если после неё родитель сходится, иначе — откат. + Защиты — правило применяется, только если одновременно: + - в схеме шапки после счёта нет названных видов аналитики («Субконто» без вида), то есть вложение не меняет + `group_type` и имя группы строки (иначе контрагент мог бы стать «договором» соседа); + - все дети родителя стоят на одном отступе (если отступ различает уровни, ему верим); + - ни вкладываемая строка, ни её новый родитель не несут ИНН. + Вложение меняет дерево в результате (`level`, `parent_row_number`), а не только проверку. Родитель без нарушения не + перестраивается; равные суммы соседей (`50 + 50 = 100`) сами по себе вложенности не дают. Остаточный риск: + - RR-6 — на листе «Субконто» без вида дубль строки аналитики с той же суммой, что у соседа выше, неотличим от + «группа → единственная статья» (на 91.01: «Проценты к получению» = «по векселям») и принимается за вложенность: + ошибка «лишняя строка» в такой форме не ловится. +Правила 1 и 3 опираются на сходимость, но это не «подгонка» (§1.0 п. 7): дерево меняется только в узких случаях, где +структура подтверждается и кодом/формой выгрузки, и суммами; реальное расхождение в остальных случаях даёт `needs_check`. + Нормализация кода счёта (ядро, `domain/osv_mapping.py`): `60.1 → 60.01`, `60.01.1 → 60.01.1`, `76.7.1 → 76.07.1` — числовой **второй** сегмент (1–3 цифры) дополняется нулём слева до двух цифр, первый и третий сегменты и буквенные (`76.АВ`, `МЦ.04`) остаются как в файле. `account_code` строки хранится как в результате (`account_code_raw`), @@ -341,6 +368,30 @@ AURORA-1661; прикладной сценарий валютных строк значения родителя и выше не затронуты); - родитель (0, 0) при детях (X Дт, X Кт) принимается (нетто 0) — осознанное поведение, закреплено тестом; - вне R-21: текст вида «12.ООО» принимается за счёт (§1.3, R-16). + + **Развёрнутый родитель над свёрнутым субсчётом** (AURORA-1863, решение владельца 28.10.2026, tabular-parser #78). + 1С считает развёрнутое сальдо счёта по нижнему уровню аналитики, а субсчёт может показать свёрнутым: счёт 76 — + 47,1 млн Дт / 12,9 млн Кт, его субсчёт 76.07 — 8,2 млн Кт при аналитике 4,6 млн Дт / 12,8 млн Кт. По колонкам счёт + с субсчетами не сходится, по нетто сходится до копейки. Сальдо развёрнутого родителя сверяется по нетто, **только если + среди его прямых детей есть «нетто-ребёнок»** — ребёнок, который сам сошёлся со своими детьми лишь по нетто (по + колонкам — нет). Признак идёт вверх только по непрерывной цепочке таких строк: строка, сошедшаяся по колонкам, его не + передаёт, так что слепая зона не растёт до корня. Обороты — по колонкам всегда. Развёрнутый родитель без + нетто-ребёнка сверяется по колонкам, как раньше: «нетто всегда» (широкий вариант) отклонён — он прячет любую ошибку, + сохраняющую нетто. Так в банке первичных документов остаются `needs_check` 14 файлов одной организации, где счёт 68 + (и 71) расходится с субсчетами на одинаковую сумму по Дт и Кт (0,05; 137,00; 5 939,76) без свёрнутого ребёнка: + отличить схлопнутую пару от потерянной по данным нельзя. Остаточный риск: + - RR-5 — у развёрнутого родителя с нетто-ребёнком колонки сальдо не информативны: ошибка, сохраняющая нетто родителя + (одинаковый сдвиг Дт и Кт, перестановка сторон), не ловится, как у свёрнутого родителя (RR-1). + + **Строки разреза «Период»** (AURORA-1823, tabular-parser #78). В выгрузке 1С с группировкой «Период» в шапке под + контрагентом идут строки «Обороты за <месяц> <год>» (или «… кв. <год>»), у каждой своё сальдо на начало и конец + периода; сальдо конца периода — сальдо начала следующего. Группировка распознаётся по имени в схеме шапки (словарь + `period_groups` в `osv_groups.yaml`; тип группы остаётся `OTHER`, контракт не меняется). Если **все** дети родителя — + строки-периоды, родитель сверяется цепочкой: обороты = Σ оборотов периодов (по колонкам), сальдо начала = сальдо начала + **первого** периода, сальдо конца = сальдо конца **последнего** (в порядке строк; по колонкам либо по нетто, как выше), + и цепочка непрерывна: сальдо конца каждого периода = сальдо начала следующего (пропущенные месяцы разрыва не дают — + 1С не печатает месяц без движения, сальдо переходит дальше). Сумма сальдо периодов не считается. У родителя со смешанными детьми (периоды и обычные строки) суммы считаются как + прежде. - Строка «Итого» (TOTAL) == корневая строка ACCOUNT (первая строка уровня 0); если корней уровня 0 несколько (лист без строки корневого счёта: верхний уровень — субсчета 58.01, 58.03, OQ-133, PR #13) — «Итого» == **сумме строк уровня 0** (показатели без суммы и сама «Итого» не входят; сальдо — по колонкам либо по нетто, если «Итого» свёрнуто, как у родителя