diff --git a/README.md b/README.md index fbe094a..96ec2ee 100644 --- a/README.md +++ b/README.md @@ -1057,7 +1057,10 @@ email без учёта регистра проверяет только сер | `RESULT_ACCOUNTS_MISSING` | 422 | результирующая таблица ВГО: в плане счетов нет обязательных кодов статей | | `INVALID_COMPANY_PAIR` | 422 | пары ВГО тройки: компании совпадают или не входят в периметр версии | | `ACCOUNTS_NOT_ALLOWED` / `INVALID_MODE` | 422 | защита внутреннего вызова сервиса шахматки; маршруты ALL/MANUAL не дают вызвать эти ветки | -| `COMMENT_REQUIRED` | 422 | `POST …/status` в `CANCELLED` без комментария (из одних пробелов — тоже) | +| `COMMENT_REQUIRED` | 422 | `POST …/status` в `CANCELLED` без комментария (из одних пробелов — тоже); `PUT …/goodwill` — отмеченная строка без комментария (с ключом пары) | +| `INVALID_SHARE` | 422 | `PUT …/goodwill`: доля не 0–100, больше двух знаков после запятой или пустая у отмеченной строки (`head_legal_entity_id`, `subsidiary_legal_entity_id`) | +| `DUPLICATE_ROW` | 422 | `PUT …/goodwill`: пара дважды в одном пакете (ключ пары) | +| `MONEY_OUT_OF_RANGE` | 422 | `PUT …/goodwill`: сумма снимка строки не помещается в `numeric(18,2)` (ключ пары) | | `VERSION_NOT_EDITABLE` | 409 | версия не «в работе» (правки) либо `CANCELLED` при «взять в работу» | | `LOCK_REQUIRED` | 409 | правка версии без своей активной блокировки | | `LOCKED_BY_OTHER` | 409 | версию держит другой пользователь: из `POST …/lock` — `locked_by`, `locked_by_name`, `locked_at`; из правок (`ensure_version_editable`) — только `locked_by`, `locked_at`; гонка двух взятий (частичный UNIQUE) — то же 409 | @@ -1343,7 +1346,11 @@ Dockerfile только для локального compose ### Таблица гудвилла -Бэкенд таблицы гудвилла версии: фактические вложения 58.01 между участниками периметра, построчные решения, -переключатель и XLSX. `is_head_entity` не используется. Деньги в рублях, доля до четырёх знаков, нерассчитанные -значения — `null`; исчезнувшие сохранённые пары остаются как «Нет вложений». -Контракт и ограничения этапа — [контракт API](goodwill/api-contract-goodwill.md). Фронт и построение/фиксация результата КФО — отдельные задачи. +Бэкенд таблицы гудвилла версии (§8 «Гудвилл», DOC-259025): пары ГО → ДО по фактическим вложениям 58.01 и потомков +между участниками периметра (`is_head_entity` не используется), УК — 1310 ББ ДО на конец квартала (единицы — словарём +`arm_statements`). `GET …/goodwill` — строки, итоги по ГО (`groups`) и по листу (`totals`); `PUT …/goodwill` — пакет +строк одной транзакцией (ошибка любой строки откатывает пакет и несёт её ключ, неизменённые строки не перезаписываются); +`PUT …/goodwill-settings` — переключатель `goodwill_enabled`; `GET …/goodwill/export?sign=` — xlsx. Деньги в рублях, +доля — до двух знаков, нерассчитанные значения — `null`; исчезнувшие сохранённые пары остаются как «Нет вложений». +Контракт — [`../docs/dev/api-contract-goodwill.md`](../docs/dev/api-contract-goodwill.md). Применение гудвилла в КФО — +AURORA-1546. diff --git a/api/tests/test_goodwill_contract.py b/api/tests/test_goodwill_contract.py index 6120c45..662896d 100644 --- a/api/tests/test_goodwill_contract.py +++ b/api/tests/test_goodwill_contract.py @@ -1,22 +1,29 @@ -"""Локальный OpenAPI совпадает с четырьмя маршрутами приложения.""" +"""Контракт гудвилла в docs (`dev/api-contract-goodwill.openapi.yaml`) совпадает с маршрутами и схемами приложения.""" +from decimal import Decimal from pathlib import Path +import pytest import yaml from src.app import app from src.schemas.goodwill import GoodwillRowIn from src.services.goodwill import validate_decision +CONTRACT = Path(__file__).resolve().parents[3] / "docs/dev/api-contract-goodwill.openapi.yaml" + def test_contract_matches_application(): - contract = yaml.safe_load( - (Path(__file__).resolve().parents[2] / "goodwill/api-contract-goodwill.openapi.yaml").read_text() - ) + if not CONTRACT.exists(): + pytest.skip("нет ../docs: контракт гудвилла не проверен") + contract = yaml.safe_load(CONTRACT.read_text()) actual = app.openapi() paths = {key: value for key, value in actual["paths"].items() if "/goodwill" in key} assert contract["paths"] == paths for name, schema in contract["components"]["schemas"].items(): - assert schema == actual["components"]["schemas"][name] - data = GoodwillRowIn(ownership_share="80.1234", include_in_kfo=True, comment="Доля подтверждена") - assert str(validate_decision(data).ownership_share) == "80.1234" + assert schema == actual["components"]["schemas"][name], name + + +def test_share_with_comma(): + data = GoodwillRowIn(ownership_share="80,12", include_in_kfo=True, comment="Доля подтверждена") + assert validate_decision(data).ownership_share == Decimal("80.12") diff --git a/openspec/changes/goodwill-table/design.md b/openspec/changes/goodwill-table/design.md index a961051..2ae7676 100644 --- a/openspec/changes/goodwill-table/design.md +++ b/openspec/changes/goodwill-table/design.md @@ -14,7 +14,7 @@ ### 1. Приоритет требований -Явные решения пользователя имеют приоритет над PDF, затем PDF, затем MD/OpenAPI. Четыре знака доли, построчное PUT и XLSX намеренно отличаются от PDF. Сохраняем четыре пути MD. is_head_entity полностью исключается. ASSUMPTION(OQ-10): источник УК — только ББ 1310; 1350 можно показывать справочно, но не включать в P. ASSUMPTION(OQ-37): вклад использует GOODWILL. Подтверждение другого источника УК или формата экспорта требует обновления этого change перед соответствующей реализацией. +Явные решения пользователя имеют приоритет над PDF, затем PDF, затем MD/OpenAPI. XLSX вместо CSV намеренно отличается от PDF (по контракту). Доля — два знака, сохранение — пакетом (R-1, R-2). is_head_entity полностью исключается. ASSUMPTION(OQ-10): источник УК — только ББ 1310; 1350 можно показывать справочно, но не включать в P. ASSUMPTION(OQ-37): вклад использует GOODWILL. Подтверждение другого источника УК или формата экспорта требует обновления этого change перед соответствующей реализацией. ### 2. Слои и источники @@ -32,13 +32,13 @@ has_investments — вычисляемый DTO-флаг, не колонка. П ### 4. Деньги и DTO -Decimal, P.quantize(0.01, ROUND_HALF_UP), затем G=ФВ−P. Ограничение numeric(18,2) проверять до записи, переполнение отклонять 422 MONEY_OUT_OF_RANGE без частичного сохранения; float не использовать. Значения в API — рубли с двумя знаками, ownership_share — проценты с четырьмя. Максимум четыре дробных знака во входе, без автоматического округления; запятая и точка допустимы по PDF §5.2; null допускается для снятой галочки. P/G рассчитываются только у действующей отмеченной строки при наличии доли и капитала (PDF §6); итоги P/G учитывают только выбранные действующие строки. Для первоначальной строки include_in_kfo=false, доля/comment/updated_* = null. +Decimal, P.quantize(0.01, ROUND_HALF_UP), затем G=ФВ−P. Ограничение numeric(18,2) проверять до записи, переполнение отклонять 422 MONEY_OUT_OF_RANGE без частичного сохранения; float не использовать. Значения в API — рубли с двумя знаками, ownership_share — проценты с двумя (R-1). Максимум два дробных знака во входе, без автоматического округления; запятая и точка допустимы по PDF §5.2; null допускается для снятой галочки. P/G рассчитываются только у действующей отмеченной строки при наличии доли и капитала (PDF §6); итоги P/G учитывают только выбранные действующие строки. Для первоначальной строки include_in_kfo=false, доля/comment/updated_* = null. GoodwillTableOut и GoodwillRowOut повторяют MD с уточнениями spec: has_investments, warnings[] из {code, message}; charter_capital_amount и additional_capital_amount тоже nullable, чтобы отсутствие ББ не маскировать нулём. totals — поля MD; в этом этапе без отдельного API групповых итогов: они выводимы из всех строк. При goodwill_enabled=false потенциальные G видны, goodwill_in_kfo_amount=0.00. ### 5. Транзакции и безопасность -Маршруты гудвилла используют зависимости по образцу API корректировок. Service использует текущие policy и lock_editable_version. PUT строки под блокировкой версии повторно загружает актуальные источники, проверяет пару и ввод, upsert существующей записи, записывает GOODWILL_EDIT before/after в той же транзакции. Вычисляемые суммы из тела не принимаются. PUT настроек не трогает строки; GOODWILL_EDIT имеет object_type=consolidation_version и changed_field=goodwill_enabled, строки — object_type=goodwill. Новые enum не нужны. Отдельная оптимистическая ревизия не добавляется: существующая блокировка версии сериализует операции, последнее сохранение владельца побеждает. +Маршруты гудвилла используют зависимости по образцу API корректировок. Service использует текущие policy и lock_editable_version. PUT строки под блокировкой версии повторно загружает актуальные источники, проверяет пару и ввод, upsert существующей записи, записывает GOODWILL_EDIT before/after в той же транзакции. Вычисляемые суммы из тела не принимаются. PUT настроек не трогает строки; GOODWILL_EDIT имеет object_type=consolidation_version и changed_field=goodwill_enabled, строки — object_type=goodwill. Новые enum не нужны. PUT …/goodwill принимает набор строк и сохраняет его одной транзакцией (R-2): сначала проверяются все строки, потом пишутся изменённые. Отдельная оптимистическая ревизия не добавляется: существующая блокировка версии сериализует операции, последнее сохранение владельца побеждает. ### 6. Граница КФО @@ -61,3 +61,27 @@ GoodwillTableOut и GoodwillRowOut повторяют MD с уточнениям Миграция БД не требуется: существующие поля и enum достаточны. Выкладка добавляет маршруты и обновлённый контракт; перед подключением фронта проверить nullable поля. Откат кода оставляет goodwill и аудит в совместимой схеме. Пока фронт отдельно, существующие страницы не изменяются. По решению пользователя ФВ пары — сумма только заполненных closing_debit; пустые значения остальных строк не блокируют расчёт P/G и сохранение ручного ввода. + +## Decisions made without discussion + +Ревью arm-core #222 (06.11.2026, владелец: «давай всё сделаем сами»; варианты предложены владельцу, ответа по ним нет — +взяты рекомендованные). + +- **R-1 (2026-11-06). Доля владения — не больше двух знаков после запятой.** Обоснование: DOC-259025 §4, §5.2, §9 + («максимум два знака»), фронт вводит два. DBML `numeric(7,4)` не меняется — хранение шире ввода. Цена ошибки: если + методологам нужны 4 знака — расширить регулярку `validate_decision` и сериализацию `Share`, данные не теряются. +- **R-2 (2026-11-06). Построчный `PUT …/goodwill/{head}/{sub}` заменён пакетным `PUT …/goodwill`.** Обоснование: + DOC-259025 §5.1 п. 4 — набор сохраняется целиком или не сохраняется; фронт слал строки `Promise.all` и мог сохранить + часть. Цена ошибки: если понадобится построчное — пакет из одной строки уже его покрывает. +- **R-3 (2026-11-06). Подготовка вклада в КФО (`contributions`, `load_for_kfo`, фильтры общего свода ОСВ) удалена, + перенесена в AURORA-1546.** Обоснование: код не вызывался, а менял общие `load_osv_rows`/`build_with_members`. + Цена ошибки: в 1546 написать заново по DOC-259025 §7, §10 (было ~150 строк с тестами, история — коммит 94895b6). +- **R-4 (2026-11-06). Итоги «Всего» по ГО (`groups`) и итог УК считает бэк.** Обоснование: агрегаты отдаёт бэк + (правило владельца), фронт считал их через `Number`; DOC-259025 §6 требует итоги ФВ и УК. Цена ошибки: лишние поля + ответа, вреда нет. +- **R-5 (2026-11-06). Единицы ББ — словарём единиц `arm_statements` (`StatementsEngine.canonical_units`).** + Обоснование: единицы правятся аналитиком свободным текстом, точное сравнение «тыс. руб.» теряло УК. Цена ошибки: + незнакомая подпись — УК `null` с предупреждением, как раньше. +- **R-6 (2026-11-06). Пометка результата КФО устаревшим при смене переключателя или строк не делается — в AURORA-1546.** + Обоснование: результата КФО (`kfo_result`) ещё не строится, помечать нечего. Цена ошибки: в 1546 добавить сброс + в `save_rows` и `settings`. diff --git a/openspec/changes/goodwill-table/proposal.md b/openspec/changes/goodwill-table/proposal.md index 25a6b85..6b35b7b 100644 --- a/openspec/changes/goodwill-table/proposal.md +++ b/openspec/changes/goodwill-table/proposal.md @@ -6,10 +6,10 @@ ## What Changes -- Срез 6: бэкенд таблицы, построчное сохранение, переключатель версии и экспорт XLSX. +- Срез 6: бэкенд таблицы, пакетное сохранение, переключатель версии и экспорт XLSX. - ГО определяется по фактическим вложениям 58.01 и дочерних счетов в другие компании периметра; `is_head_entity` не используется. - Найденные пары объединяются с сохранёнными `goodwill`; исчезнувшие остаются с вычисляемым `has_investments=false`, без участия в итогах. -- Доля 0–100%, до четырёх знаков; нерассчитанные P/G допускают null. Сохранение одной строки атомарно. +- Доля 0–100%, до двух знаков; нерассчитанные P/G допускают null. Пакет строк сохраняется одной транзакцией; итоги по ГО и УК отдаёт бэк. - Расчёт по §8 domain-rules с уточнениями PDF §§3–10 и решениями пользователя: ASSUMPTION(OQ-10) — УК только из 1310 ББ, 1350 не входит; ASSUMPTION(OQ-37) — подготовленный вклад имеет технический код GOODWILL. - Подготовка вклада в КФО и исключение ББ-2 — не в этом change, перенесено в AURORA-1546 (R-3). - Уточнения PDF, отсутствующие в §8 (неактивные пары, выборка по дереву, итоги, исключение ББ-2), требуют отдельной синхронизации в docs; при реализации не подменять ими молча старую редакцию правил. @@ -28,7 +28,7 @@ Читаются таблицы DBML v5.24: consolidation, consolidation_version, consolidation_member, consolidation_document, document, osv_entry, chart_of_account, financial_statement_meta/line/column/value, legal_entity, app_user, elimination_rule. Изменяются goodwill, consolidation_version.goodwill_enabled и audit_event. Для аудита использовать существующий GOODWILL_EDIT, для выгрузки EXPORT; новых enum и колонок нет. `has_investments`, предупреждения и вклад КФО — вычисляемые DTO. -Затрагиваются domain/, repositories/, schemas/, services/ и endpoints/consolidations; четыре маршрута из MD. Изменение чернового контракта: новые признаки состояния и nullable P/G, обновлённая семантика пар и итогов. Используются существующие guard, Decimal и openpyxl. +Затрагиваются domain/, repositories/, schemas/, services/ и endpoints/consolidations; маршруты GET/PUT …/goodwill, PUT …/goodwill-settings, GET …/goodwill/export; контракт — ../docs/dev/api-contract-goodwill.md. Изменение чернового контракта: новые признаки состояния и nullable P/G, обновлённая семантика пар и итогов. Используются существующие guard, Decimal и openpyxl. ## Non-goals diff --git a/openspec/changes/goodwill-table/specs/goodwill-table/spec.md b/openspec/changes/goodwill-table/specs/goodwill-table/spec.md index 3668cf2..fd354b8 100644 --- a/openspec/changes/goodwill-table/specs/goodwill-table/spec.md +++ b/openspec/changes/goodwill-table/specs/goodwill-table/spec.md @@ -25,36 +25,52 @@ - **THEN** строка остаётся с has_investments=false, прежними долей, комментарием и галочкой, но без вклада в итоги; повторное появление пары восстанавливает расчёт по текущим источникам ### Requirement: Источник капитала и расчёт -Система SHALL использовать 1310 ББ ДО на конец отчётного периода в рублях: P=round(УК×доля/100,2), G=ФВ−P. Дубликаты 1310 обрабатываются агрегированием формы. Ноль допустим; отсутствие доли или достоверного УК даёт null P/G. 1350 не участвует: ASSUMPTION(OQ-10). +Система SHALL использовать 1310 ББ ДО на конец отчётного периода в рублях; единицы формы SHALL распознаваться словарём единиц arm_statements, нераспознанные единицы дают отсутствующий УК. P=round(УК×доля/100,2), G=ФВ−P. Дубликаты 1310 обрабатываются агрегированием формы. Ноль допустим; отсутствие доли или достоверного УК даёт null P/G. 1350 не участвует: ASSUMPTION(OQ-10). #### Scenario: Ноль отличается от отсутствия расчёта - **WHEN** из привязанного реального ББ получен УК U, доля ещё не введена - **THEN** P и G равны null; при доле 0 P равен 0.00 и G равен ФВ; отсутствующая 1310 не подменяется нулём +#### Scenario: Единицы, исправленные вручную +- **WHEN** у формы ББ единицы «тыс.руб.» или «в тысячах рублей» +- **THEN** УК переводится в рубли умножением на 1000; единицы «штук» дают УК null и предупреждение CAPITAL_SOURCE_MISSING + #### Scenario: Проверка отчётной колонки - **WHEN** в привязанном ББ есть несколько сравнительных колонок и единицы «тыс. руб.» - **THEN** УК берётся по дате конца отчётного периода, а не только is_current; сумма переводится в рубли до расчёта ### Requirement: Получение таблицы -GET /api/v1/consolidations/versions/{version_id}/goodwill SHALL возвращать version_id, goodwill_enabled, editable, read_only_reason, rows, totals. Строки отсортированы по именам ГО/ДО и ID; содержат поля MD-контракта, has_investments и warnings. Деньги — строки с двумя знаками в рублях или null, доли — строки с четырьмя. +GET /api/v1/consolidations/versions/{version_id}/goodwill SHALL возвращать version_id, goodwill_enabled, editable, read_only_reason, rows, groups, totals. Строки отсортированы по именам ГО/ДО и ID; содержат поля MD-контракта, has_investments и warnings. Деньги — строки с двумя знаками в рублях или null, доли — строки с двумя знаками. groups — итог «Всего» по каждой ГО в порядке строк, totals — итог по листу. #### Scenario: Пустая таблица - **WHEN** нет подходящих вложений и сохранённых пар - **THEN** rows пуст, денежные итоги 0.00; запрос не создаёт goodwill ### Requirement: Итоги и предупреждения -Система SHALL суммировать ФВ действующих строк независимо от выбора; P/G — только действующих выбранных рассчитанных строк. goodwill_in_kfo_amount включает только положительные применимые G и равен нулю при выключенном goodwill_enabled. Сумма заполненных долей действующих ГО в одной ДО свыше 100% даёт предупреждение, не ошибку. +Система SHALL суммировать ФВ и УК действующих строк независимо от выбора; P/G — только действующих выбранных рассчитанных строк. goodwill_in_kfo_amount включает только положительные применимые G и равен нулю при выключенном goodwill_enabled. Сумма заполненных долей действующих ГО в одной ДО свыше 100% даёт предупреждение, не ошибку. #### Scenario: Несколько ГО одной ДО - **WHEN** у действующих пар одной ДО заполнены доли 70% и 40% - **THEN** строки содержат предупреждение OWNERSHIP_TOTAL_EXCEEDED, сохранение не блокируется; проценты по разным ДО не складываются в общий процент -### Requirement: Построчное сохранение -PUT …/goodwill/{head_id}/{subsidiary_id} SHALL атомарно сохранять ownership_share, include_in_kfo, comment и расчётные суммы только одной пары. Доля допускает null только при снятом выборе, иначе 0–100 с точностью до четырёх знаков; превышение точности не округляется. Для выбранной действующей строки обязателен непустой комментарий до 1000 символов. Ошибки — INVALID_SHARE и COMMENT_REQUIRED (422). +#### Scenario: Пример DOC-259025 §11.1 +- **WHEN** у ГО «Альфа» отмечены Бета (ФВ 180 000, УК 100 000, 80 %) и Гамма (ФВ 50 000, УК 100 000, 80 %), Дельта (ФВ 60 000, УК 60 000) без доли и не отмечена +- **THEN** «Всего» и «Итого по листу»: ФВ 290 000, УК 260 000, P 160 000, G 70 000, в КФО 100 000 (при выключенном переключателе — 0) + +### Requirement: Пакетное сохранение +PUT …/goodwill SHALL принимать набор строк {head_legal_entity_id, subsidiary_legal_entity_id, ownership_share, include_in_kfo, comment} и сохранять его в одной транзакции: все строки проверяются до записи, ошибка любой строки откатывает весь пакет и несёт ключ пары. Доля допускает null только при снятом выборе, иначе 0–100, не больше двух знаков после запятой, запятая или точка; превышение точности не округляется. Для выбранной действующей строки обязателен непустой комментарий до 1000 символов. Ошибки — 404 NOT_FOUND (пары нет в таблице), 422 INVALID_SHARE, COMMENT_REQUIRED, MONEY_OUT_OF_RANGE, DUPLICATE_ROW. Строка, ввод которой не изменился, MUST NOT перезаписываться и писать аудит. Ответ — пересчитанная таблица. + +#### Scenario: Ошибка одной строки пакета +- **WHEN** пакет содержит корректную строку и строку отмеченной пары без комментария +- **THEN** ответ 422 COMMENT_REQUIRED с head_legal_entity_id и subsidiary_legal_entity_id этой строки; ни одна запись goodwill и ни одно событие GOODWILL_EDIT не созданы + +#### Scenario: Пакет без изменений +- **WHEN** повторно отправлена сохранённая строка с той же долей («80,5» после «80.50»), галочкой и комментарием +- **THEN** updated_at строки прежний, событий GOODWILL_EDIT не добавилось #### Scenario: Снятие выбора - **WHEN** сохраняется действующая строка с include_in_kfo=false и прежними долей и комментарием -- **THEN** ввод сохраняется, строка исключается из итогов P/G и вклада КФО; ответ содержит пересчитанную строку +- **THEN** ввод сохраняется, строка исключается из итогов P/G и вклада КФО; ответ содержит пересчитанную таблицу #### Scenario: Повторное сохранение и неактивная строка - **WHEN** повторно сохраняется существующая пара diff --git a/openspec/changes/goodwill-table/tasks.md b/openspec/changes/goodwill-table/tasks.md index 73cf0c5..27d3c1c 100644 --- a/openspec/changes/goodwill-table/tasks.md +++ b/openspec/changes/goodwill-table/tasks.md @@ -53,3 +53,12 @@ ## 8. Экспорт по экранной таблице - [x] 8.1 arm-core: привести XLSX к составу и порядку восьми колонок фронта со стандартным оформлением. Сохранить прежний контракт ручки и вызов экспорта на фронте. Проверить интеграционные сценарии выгрузки. + + +## 9. Доводка по ревью (06.11.2026, план docs/superpowers/plans/2026-11-06-goodwill-review-back.md) + +- [x] 9.1 arm-core: заготовка вклада КФО удалена, общий свод ОСВ возвращён к main (R-3). +- [x] 9.2 arm-core: доля — два знака, `participation()` в домене, итоги по ГО (`groups`) и УК (R-1, R-4). +- [x] 9.3 arm-core: пакетный `PUT …/goodwill` одной транзакцией вместо построчного (R-2). +- [x] 9.4 arm-core: единицы ББ по словарю arm_statements (R-5). +- [x] 9.5 docs (#204) и arm-core: контракт `../docs/dev/api-contract-goodwill.*`, §8 правил, OQ-10/OQ-37, README.