diff --git a/api/src/domain/goodwill.py b/api/src/domain/goodwill.py index 9011240..3d5c226 100644 --- a/api/src/domain/goodwill.py +++ b/api/src/domain/goodwill.py @@ -91,42 +91,3 @@ def totals(rows: Sequence[Row], enabled: bool) -> dict[str, Decimal]: "goodwill_amount": sum((r.goodwill_amount or ZERO for r in selected), ZERO), "goodwill_in_kfo_amount": sum((r.goodwill_amount or ZERO for r in active if enabled and r.applicable), ZERO), } - - -@dataclass(frozen=True) -class Contribution: - version_id: uuid.UUID - rule_id: uuid.UUID - head_id: uuid.UUID - subsidiary_id: uuid.UUID - financial_investment_amount: Decimal - participation_amount: Decimal - goodwill_amount: Decimal - # Ключ исключения — balance, rule_id, head_id, subsidiary_id. - deltas: tuple[tuple[uuid.UUID, str, Decimal], ...] - - -def contributions(version_id: uuid.UUID, rule_id: uuid.UUID, rows: Sequence[Row], enabled: bool) -> list[Contribution]: - result = [] - for row in rows: - if not enabled or not row.applicable: - continue - assert row.financial_investment_amount is not None - assert row.participation_amount is not None and row.goodwill_amount is not None - result.append( - Contribution( - version_id, - rule_id, - row.head_id, - row.subsidiary_id, - row.financial_investment_amount, - row.participation_amount, - row.goodwill_amount, - ( - (row.head_id, "1170", -row.financial_investment_amount), - (row.subsidiary_id, "1310", -row.participation_amount), - (row.head_id, "GOODWILL", row.goodwill_amount), - ), - ) - ) - return result diff --git a/api/src/repositories/consolidations.py b/api/src/repositories/consolidations.py index 3d04392..ec4d1bb 100644 --- a/api/src/repositories/consolidations.py +++ b/api/src/repositories/consolidations.py @@ -306,13 +306,7 @@ async def document_versions(session: AsyncSession, document_id: uuid.UUID) -> li return list((await session.execute(stmt)).scalars()) -async def load_osv_rows( - session: AsyncSession, - version_id: uuid.UUID, - *, - company_pairs: Collection[tuple[uuid.UUID, uuid.UUID]] | None = None, - account_ids: Collection[uuid.UUID] | None = None, -) -> list[tuple[OsvEntry, Document, LegalEntity]]: +async def load_osv_rows(session: AsyncSession, version_id: uuid.UUID) -> list[tuple[OsvEntry, Document, LegalEntity]]: """Исходные строки свода: контрагентские (`group_type = COUNTERPARTY`) строки BU/SUM привязанных документов. `TOTAL` не хранится, строки `ACCOUNT` и данные `NU` отбрасываются. Порядок: имя ЮЛ, код счёта документа, квартал, @@ -332,22 +326,6 @@ async def load_osv_rows( LegalEntity.name, Document.osv_account_code, Document.period_quarter, OsvEntry.row_number, OsvEntry.id ) ) - if company_pairs is not None: - # NULL-контрагент может быть сопоставлен общим дорезолвом; точная пара проверяется после него. - stmt = stmt.where( - or_( - false(), - *( - and_( - OsvEntry.owner_legal_entity_id == head, - or_(OsvEntry.counterparty_id == subsidiary, OsvEntry.counterparty_id.is_(None)), - ) - for head, subsidiary in company_pairs - ), - ) - ) - if account_ids is not None: - stmt = stmt.where(OsvEntry.account_id.in_(account_ids)) return [(row[0], row[1], row[2]) for row in await session.execute(stmt)] diff --git a/api/src/repositories/goodwill.py b/api/src/repositories/goodwill.py index 6dc6a65..f5b354a 100644 --- a/api/src/repositories/goodwill.py +++ b/api/src/repositories/goodwill.py @@ -1,6 +1,5 @@ """Пакетное чтение goodwill и привязанных подтверждённых ББ.""" -from collections.abc import Collection from uuid import UUID from sqlalchemy import select @@ -25,10 +24,8 @@ from src.models.statements import ( ) -async def saved(session: AsyncSession, version_id: UUID, *, included_only: bool = False) -> list[Goodwill]: +async def saved(session: AsyncSession, version_id: UUID) -> list[Goodwill]: stmt = select(Goodwill).where(Goodwill.consolidation_version_id == version_id) - if included_only: - stmt = stmt.where(Goodwill.include_in_kfo.is_(True)) return list((await session.scalars(stmt)).all()) @@ -39,7 +36,7 @@ async def accounts(session: AsyncSession) -> list[ChartOfAccount]: return list((await session.scalars(select(ChartOfAccount).join(tree, ChartOfAccount.id == tree.c.id))).all()) -async def forms(session: AsyncSession, version_id: UUID, company_ids: Collection[UUID] | None = None): +async def forms(session: AsyncSession, version_id: UUID): stmt = ( select(Document, Meta) .join(Meta, Meta.document_id == Document.id) @@ -50,8 +47,6 @@ async def forms(session: AsyncSession, version_id: UUID, company_ids: Collection Meta.statement_type == StatementType.balance, ) ) - if company_ids is not None: - stmt = stmt.where(Document.legal_entity_id.in_(company_ids)) return list((await session.execute(stmt)).all()) diff --git a/api/src/services/consolidated_osv.py b/api/src/services/consolidated_osv.py index 1d74dd0..b548d42 100644 --- a/api/src/services/consolidated_osv.py +++ b/api/src/services/consolidated_osv.py @@ -13,7 +13,7 @@ import uuid from collections.abc import Collection, Sequence -from dataclasses import dataclass, replace +from dataclasses import dataclass from typing import Literal from fastapi.concurrency import run_in_threadpool @@ -149,13 +149,7 @@ class ConsolidatedOsvService: self._session = session self._scope = scope - async def build_with_members( - self, - version_id: uuid.UUID, - *, - company_pairs: Collection[tuple[uuid.UUID, uuid.UUID]] | None = None, - account_ids: Collection[uuid.UUID] | None = None, - ) -> ConsolidatedOsvBuildResult: + async def build_with_members(self, version_id: uuid.UUID) -> ConsolidatedOsvBuildResult: """Свод и участники периметра, загруженные при его расчёте; версия вне видимости — 404.""" found = await repo.get_version(self._session, self._scope, version_id) if found is None: @@ -173,12 +167,7 @@ class ConsolidatedOsvService: for document in await consolidation_documents.for_consolidation(self._session, bound, consolidation) ] # ровно то, что отдал `load_osv_rows`: он оставляет `COUNTERPARTY`/`BU`+`SUM`, домен их не перефильтровывает - rows = [ - _source_row(entry) - for entry, _, _ in await repo.load_osv_rows( - self._session, version.id, company_pairs=company_pairs, account_ids=account_ids - ) - ] + rows = [_source_row(entry) for entry, _, _ in await repo.load_osv_rows(self._session, version.id)] index = await CounterpartyIndex.load(self._session) entities = {**await _entities(self._session, _candidate_ids(rows, index) - members.keys()), **members} consolidated = domain.build( @@ -191,12 +180,6 @@ class ConsolidatedOsvService: rows=rows, resolver=index, ) - if company_pairs is not None: - pairs = set(company_pairs) - consolidated = replace( - consolidated, - rows=tuple(row for row in consolidated.rows if (row.legal_entity_id, row.counterparty_id) in pairs), - ) return ConsolidatedOsvBuildResult( consolidated=consolidated, version=version, diff --git a/api/src/services/goodwill.py b/api/src/services/goodwill.py index b3162d9..b2f6579 100644 --- a/api/src/services/goodwill.py +++ b/api/src/services/goodwill.py @@ -15,9 +15,8 @@ from src.common.exceptions import ConflictError, DomainValidationError, NotFound from src.domain import goodwill as domain from src.domain.policy import VisibilityScope, ensure_version_editable from src.models.access import AppUser -from src.models.accounting import EliminationRule from src.models.directories import LegalEntity -from src.models.enums import AuditAction, AuditSection, StatementType +from src.models.enums import AuditAction, AuditSection from src.models.kfo import Goodwill from src.repositories import consolidations as versions from src.repositories import goodwill as repo @@ -30,12 +29,11 @@ from src.schemas.goodwill import ( GoodwillTotalsOut, WarningOut, ) -from src.services.consolidation_guard import VERSION_NOT_FOUND, lock_editable_version +from src.services.consolidation_guard import lock_editable_version from src.services.goodwill_export import build_workbook, matches_sign from src.services.goodwill_source import GoodwillSource MAX_SHARE = Decimal(100) -BB_RULE_NO = 2 MESSAGES = { "CAPITAL_SOURCE_MISSING": "Нет однозначного значения 1310 на конец отчётного периода", @@ -258,40 +256,3 @@ class GoodwillService: after_value={"report": "goodwill", "rows": sum(matches_sign(row, sign) for row in table.rows)}, ) return content - - async def contributions(self, version_id: UUID) -> list[domain.Contribution]: - - found = await versions.get_version(self.session, self.scope, version_id) - if found is None: - raise NotFoundError(VERSION_NOT_FOUND) - version, _ = found - if not version.goodwill_enabled: - return [] - saved = await repo.saved(self.session, version_id, included_only=True) - if not saved: - return [] - pairs = {(row.head_legal_entity_id, row.subsidiary_legal_entity_id) for row in saved} - sources = await self.source.load_for_kfo(version_id, pairs) - decisions = { - (r.head_legal_entity_id, r.subsidiary_legal_entity_id): domain.Decision( - r.ownership_share, r.include_in_kfo, r.comment - ) - for r in saved - } - rows = domain.build(sources.investments, decisions, sources.capital) - if not any(row.applicable for row in rows): - return [] - rules = list( - ( - await self.session.scalars( - select(EliminationRule).where( - EliminationRule.version_no == sources.context.version.elimination_rules_version, - EliminationRule.statement_type == StatementType.balance, - EliminationRule.rule_no == BB_RULE_NO, - ) - ) - ).all() - ) - if len(rules) != 1: - raise ConflictError("GOODWILL_RULE_MISSING", "Нет однозначного ББ-2 закреплённой версии правил") - return domain.contributions(version_id, rules[0].id, rows, sources.context.version.goodwill_enabled) diff --git a/api/src/services/goodwill_source.py b/api/src/services/goodwill_source.py index 29f721f..f5f2b20 100644 --- a/api/src/services/goodwill_source.py +++ b/api/src/services/goodwill_source.py @@ -9,7 +9,7 @@ from uuid import UUID from sqlalchemy.ext.asyncio import AsyncSession -from src.domain.goodwill import Investment, Pair +from src.domain.goodwill import Investment from src.domain.statement_aggregate import AggLine, aggregate_by_code from src.repositories import goodwill as repo from src.services.consolidated_osv import ConsolidatedOsvBuildResult, ConsolidatedOsvService @@ -71,23 +71,11 @@ class GoodwillSource: self.session, self.osv = session, osv async def load(self, version_id: UUID) -> Sources: - return await self._load(version_id) - - async def load_for_kfo(self, version_id: UUID, pairs: set[Pair]) -> Sources: - """Источники только сохранённых выбранных пар; таблица использует обычный load.""" - return await self._load(version_id, pairs) - - async def _load(self, version_id: UUID, pairs: set[Pair] | None = None) -> Sources: accounts = await repo.accounts(self.session) allowed = {account.id for account in accounts} - if pairs is None: - ctx = await self.osv.build_with_members(version_id) - else: - ctx = await self.osv.build_with_members(version_id, company_pairs=pairs, account_ids=allowed) + ctx = await self.osv.build_with_members(version_id) result = Sources(ctx, investments=_investments(ctx, allowed)) - forms = await repo.forms( - self.session, version_id, {subsidiary for _, subsidiary in pairs} if pairs is not None else None - ) + forms = await repo.forms(self.session, version_id) columns, values = await repo.form_data(self.session, [m.id for _, m in forms]) month = int(ctx.consolidation.quarter.value[1:]) * 3 date = dt.date(ctx.consolidation.year, month, calendar.monthrange(ctx.consolidation.year, month)[1]) diff --git a/api/tests/test_goodwill_contribution.py b/api/tests/test_goodwill_contribution.py deleted file mode 100644 index 7ab5c6c..0000000 --- a/api/tests/test_goodwill_contribution.py +++ /dev/null @@ -1,227 +0,0 @@ -"""Гудвилл изменяет актив и пассив на одинаковую сумму, не смешивает пары.""" - -from decimal import Decimal -from http import HTTPStatus -from pathlib import Path -from types import SimpleNamespace -from unittest.mock import AsyncMock -from uuid import UUID - -import pytest -from sqlalchemy import func, select - -from src.common.exceptions import ConflictError -from src.domain.goodwill import Decision, Investment, build, contributions -from src.domain.policy import VisibilityScope -from src.models.access import AppUser -from src.models.accounting import ChartOfAccount, EliminationRule -from src.models.consolidation import ConsolidationDocument -from src.models.enums import DocumentType, PeriodQuarter, StatementType -from src.models.files import StoredFile -from src.models.kfo import Correction, Goodwill, KfoResult -from src.models.osv import OsvEntry -from src.models.statements import FinancialStatementMeta -from src.repositories import consolidations as version_repo -from src.repositories import goodwill as goodwill_repo -from src.services.consolidated_osv import ConsolidatedOsvService -from src.services.goodwill import GoodwillService -from src.services.goodwill_source import GoodwillSource -from tests.factories import make_confirmed_osv -from tests.test_api_consolidated_osv import KA_RF -from tests.test_api_goodwill import goodwill_world -from tests.test_goodwill_golden import investment_rows - -BB_RULE_NO = 2 - -pytestmark = pytest.mark.fixtures - - -def test_contribution_balance(fixtures_dir: Path) -> None: - a, b, version, rule = (UUID(int=i) for i in range(1, 5)) - fv = Decimal(investment_rows(fixtures_dir)[0]["closing_debit"]) - saved = {(a, b): Decision(Decimal("80"), True, "Подтверждено")} - rows = build([Investment(a, b, fv)], saved, {b: Decimal("100000000")}) - item = contributions(version, rule, rows, True)[0] - assert item.rule_id == rule and item.head_id == a and item.subsidiary_id == b - assert sum(v for _, code, v in item.deltas if code in ("1170", "GOODWILL")) == -item.participation_amount - assert dict((code, value) for _, code, value in item.deltas)["1310"] == -item.participation_amount - assert contributions(version, rule, rows, False) == [] - assert contributions(version, rule, build([Investment(a, b, fv)], saved, {}), True) == [] - assert contributions(version, rule, build([Investment(a, b, fv)], saved, {b: fv * 2}), True) == [] - - -@pytest.mark.integration -async def test_service_uses_pinned_rule_without_writing_kfo(db_session, auth_client, fixtures_dir): - world = await goodwill_world(db_session, fixtures_dir) - client = await auth_client(KA_RF) - base = f"/consolidations/versions/{world.version.id}" - assert (await client.post(base + "/lock")).status_code == HTTPStatus.CREATED - assert ( - await client.put( - base + f"/goodwill/{world.a.id}/{world.b.id}", - json=dict(ownership_share="80", include_in_kfo=True, comment="Да"), - ) - ).status_code == HTTPStatus.OK - assert (await client.put(base + "/goodwill-settings", json={"goodwill_enabled": True})).status_code == HTTPStatus.OK - pinned = ( - await db_session.scalars( - select(EliminationRule).where( - EliminationRule.version_no == 1, - EliminationRule.rule_no == BB_RULE_NO, - EliminationRule.statement_type == StatementType.balance, - ) - ) - ).one() - other = EliminationRule( - **{ - col.name: getattr(pinned, col.name) - for col in EliminationRule.__table__.columns - if col.name not in ("id", "version_no") - }, - version_no=2, - ) - db_session.add(other) - await db_session.flush() - principal_user = (await db_session.scalars(select(AppUser).where(AppUser.email == KA_RF))).one() - scope = VisibilityScope(office_id=None) - service = GoodwillService( - db_session, - scope, - SimpleNamespace(user=principal_user, permissions={"CONSOLIDATION_EDIT"}), - GoodwillSource(db_session, ConsolidatedOsvService(db_session, scope)), - ) - result = await service.contributions(world.version.id) - assert len(result) == 1 and result[0].rule_id == pinned.id - assert await db_session.scalar(select(func.count()).select_from(KfoResult)) == 0 - assert await db_session.scalar(select(func.count()).select_from(Correction)) == 0 - # Дорезолв на чтение сохраняет те же выбранные пары и суммы. - entry = ( - await db_session.scalars( - select(OsvEntry).where(OsvEntry.owner_legal_entity_id == world.a.id, OsvEntry.account_code == "58.01") - ) - ).one() - entry.counterparty_id = None - entry.counterparty_name = world.b.name - await db_session.flush() - assert await service.contributions(world.version.id) == result - pinned.rule_no = 99 - await db_session.flush() - with pytest.raises(ConflictError) as error: - await service.contributions(world.version.id) - assert error.value.code == "GOODWILL_RULE_MISSING" - - world.version.goodwill_enabled = False - await db_session.flush() - assert await service.contributions(world.version.id) == [] - - -@pytest.mark.integration -async def test_kfo_loads_only_saved_selected_pairs(db_session, auth_client, fixtures_dir, monkeypatch): - world = await goodwill_world(db_session, fixtures_dir) - client = await auth_client(KA_RF) - base = f"/consolidations/versions/{world.version.id}" - await client.post(base + "/lock") - await client.put( - base + f"/goodwill/{world.a.id}/{world.b.id}", - json=dict(ownership_share="80", include_in_kfo=True, comment="Да"), - ) - await client.put(base + "/goodwill-settings", json={"goodwill_enabled": True}) - db_session.add( - Goodwill( - consolidation_version_id=world.version.id, - head_legal_entity_id=world.a.id, - subsidiary_legal_entity_id=world.chkf.id, - ownership_share=Decimal("100"), - include_in_kfo=False, - ) - ) - account = (await db_session.scalars(select(ChartOfAccount).where(ChartOfAccount.code == "58.01"))).one() - for owner, other in ((world.a, world.chkf), (world.b, world.a)): - doc = await make_confirmed_osv( - db_session, - entity=owner, - account="58.01", - year=2024, - quarter=PeriodQuarter.Q3, - is_cumulative=True, - rows=[ - dict( - name=other.name, - counterparty_id=other.id, - account_id=account.id, - closing_debit=Decimal("269997300.00"), - ) - ], - ) - db_session.add(ConsolidationDocument(consolidation_version_id=world.version.id, document_id=doc.id)) - extra = await make_confirmed_osv( - db_session, entity=world.a, account="80", year=2024, quarter=PeriodQuarter.Q3, is_cumulative=True, rows=[] - ) - extra.document_type = DocumentType.BALANCE - extra.osv_account_code = None - file = await db_session.get(StoredFile, extra.stored_file_id) - meta = FinancialStatementMeta( - document_id=extra.id, - recognition_job_id=file.current_recognition_job_id, - statement_type=StatementType.balance, - units="тыс. руб.", - ) - db_session.add(meta) - db_session.add(ConsolidationDocument(consolidation_version_id=world.version.id, document_id=extra.id)) - await db_session.flush() - original_rows, original_forms = version_repo.load_osv_rows, goodwill_repo.forms - loaded_pairs, loaded_form_owners = [], [] - - async def capture_rows(*args, **kwargs): - assert kwargs["company_pairs"] == {(world.a.id, world.b.id)} - assert account.id in kwargs["account_ids"] - rows = await original_rows(*args, **kwargs) - loaded_pairs.extend((e.owner_legal_entity_id, e.counterparty_id) for e, _, _ in rows) - return rows - - async def capture_forms(*args, **kwargs): - rows = await original_forms(*args, **kwargs) - loaded_form_owners.extend(doc.legal_entity_id for doc, _ in rows) - return rows - - monkeypatch.setattr(version_repo, "load_osv_rows", capture_rows) - monkeypatch.setattr(goodwill_repo, "forms", capture_forms) - user = (await db_session.scalars(select(AppUser).where(AppUser.email == KA_RF))).one() - scope = VisibilityScope(office_id=None) - source = GoodwillSource(db_session, ConsolidatedOsvService(db_session, scope)) - source.load = AsyncMock(side_effect=AssertionError("Полная таблица для КФО не нужна")) - service = GoodwillService(db_session, scope, SimpleNamespace(user=user), source) - result = await service.contributions(world.version.id) - assert len(result) == 1 - assert {(r.head_id, r.subsidiary_id) for r in result} == {(world.a.id, world.b.id)} - assert loaded_pairs and set(loaded_pairs) == {(world.a.id, world.b.id)} - assert loaded_form_owners == [world.b.id] - source.load.assert_not_awaited() - - -@pytest.mark.integration -async def test_kfo_without_selected_rows_does_not_load_sources(db_session, auth_client, fixtures_dir): - world = await goodwill_world(db_session, fixtures_dir) - world.version.goodwill_enabled = True - await db_session.flush() - user = (await db_session.scalars(select(AppUser).where(AppUser.email == KA_RF))).one() - scope = VisibilityScope(office_id=None) - source = GoodwillSource(db_session, ConsolidatedOsvService(db_session, scope)) - source.load_for_kfo = AsyncMock(side_effect=AssertionError("Нет выбранных пар — нет источников")) - service = GoodwillService(db_session, scope, SimpleNamespace(user=user), source) - assert await service.contributions(world.version.id) == [] - source.load_for_kfo.assert_not_awaited() - db_session.add( - Goodwill( - consolidation_version_id=world.version.id, - head_legal_entity_id=world.a.id, - subsidiary_legal_entity_id=world.b.id, - ownership_share=Decimal("100"), - include_in_kfo=True, - comment="Да", - ) - ) - world.version.goodwill_enabled = False - await db_session.flush() - assert await service.contributions(world.version.id) == [] - source.load_for_kfo.assert_not_awaited() diff --git a/docs/superpowers/plans/2026-11-06-goodwill-review-back.md b/docs/superpowers/plans/2026-11-06-goodwill-review-back.md new file mode 100644 index 0000000..a6b95fe --- /dev/null +++ b/docs/superpowers/plans/2026-11-06-goodwill-review-back.md @@ -0,0 +1,181 @@ +# Гудвилл: доводка бэка по ревью (arm-core #222) — план + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** довести PR arm-core #222 (ветка `feat/goodwill-back`, AURORA-1543) до мержа: убрать невызываемую заготовку КФО, +итоги по ГО и УК отдать с бэка, долю ограничить двумя знаками, сохранение сделать пакетным в одной транзакции, единицы ББ +читать через словарь `arm_statements`, привести контракт в docs (ветка `feat/204-goodwill-contract`, issue docs #204), +§8 правил и OpenSpec change к фактическому API. + +**Architecture:** чистый расчёт — `api/src/domain/goodwill.py`; загрузка источников — `services/goodwill_source.py` + +`repositories/goodwill.py`; сервис/маршруты — `services/goodwill.py`, `api/endpoints/consolidations/goodwill.py`; экспорт — +`services/goodwill_export.py`. Общий свод ОСВ (`repositories/consolidations.load_osv_rows`, +`services/consolidated_osv.build_with_members`) возвращается к состоянию `main`. + +**Tech Stack:** Python 3.11, FastAPI, SQLAlchemy 2 async, pydantic v2, openpyxl, pytest (маркеры `integration`, `fixtures`). + +**Spec:** `openspec/changes/goodwill-table/` (этого репозитория) + страница аналитики DOC-259025 «Гудвилл» (версия 03.11.2026, +`mcp__sdlc-wiki__page code=DOC-259025`) + решения ревью ниже. + +Рабочие каталоги: arm-core — `/Users/andrey.epifancev/projects/awp-ca/.wt/review-222/arm-core` (ветка `feat/goodwill-back`), +docs — `/Users/andrey.epifancev/projects/awp-ca/.wt/review-222/docs` (ветка `feat/204-goodwill-contract`). Git — только +`git -C <абсолютный путь worktree>`. + +## Global Constraints + +- Деньги — `Decimal`, `ROUND_HALF_UP` до 0.01, в API — строки с двумя знаками в рублях; `null` ≠ `0.00`. +- Доля владения: строка `0`…`100`, не больше **двух** знаков после запятой, запятая или точка; лишние знаки — 422 + `INVALID_SHARE`, без округления и обрезки. В ответе — строка с двумя знаками (`"80.00"`). +- `is_head_entity` не используется; ГО — владелец строки 58.01 (и потомков) с контрагентом из периметра. +- Аудит — `GOODWILL_EDIT` (строки: `object_type=goodwill`; переключатель: `object_type=consolidation_version`, + `changed_field=goodwill_enabled`), выгрузка — `EXPORT`; новых enum и колонок нет, миграций нет. +- Коммиты — по-русски, в конце `Co-Authored-By: Claude Opus 5.5 `. +- Решения ревью (дописать в `openspec/changes/goodwill-table/design.md`, раздел `## Decisions made without discussion`): + R-1 доля — 2 знака (DOC-259025 §4, §5.2, §9; DBML `numeric(7,4)` хранит без изменений); R-2 построчный `PUT …/goodwill/{head}/{sub}` + заменён пакетным `PUT …/goodwill` в одной транзакции (DOC-259025 §5.1 п. 4); R-3 заготовка вклада КФО (`contributions`, + `load_for_kfo`, фильтры общего свода) вынесена в AURORA-1546; R-4 итоги по ГО и итог УК считает бэк; R-5 единицы ББ — + через словарь единиц `arm_statements`; R-6 пометка результата КФО устаревшим — в AURORA-1546 (результата КФО ещё нет). + +## Review Focus + +- Пакет, где одна строка невалидна: ничего не сохраняется, ни одной записи `goodwill` и `audit_event`, в 422 есть ключ строки. +- Повторное сохранение без изменений: `updated_by/updated_at` не меняются, аудита нет (фронт шлёт весь набор отмеченных строк). +- Единицы ББ, исправленные аналитиком вручную («тыс.руб.», «в тысячах рублей»): УК всё равно переводится в рубли. +- Итог «Всего» блока: ФВ и УК — по всем действующим строкам блока, P и G — только отмеченным; «Нет вложений» не входит. +- Доля `"80,123"` — 422, а не молча `80.12`. + +--- + +### Task 1: Убрать заготовку вклада КФО + +**Files:** +- Modify: `api/src/domain/goodwill.py` (удалить `Contribution`, `contributions`) +- Modify: `api/src/services/goodwill.py` (удалить `contributions`, `BB_RULE_NO`, импорт `EliminationRule`, `StatementType`) +- Modify: `api/src/services/goodwill_source.py` (удалить `load_for_kfo`, параметр `pairs` у `_load`) +- Modify: `api/src/repositories/goodwill.py` (удалить `included_only` у `saved`, `company_ids` у `forms`) +- Restore from `origin/main`: `api/src/repositories/consolidations.py`, `api/src/services/consolidated_osv.py` +- Delete: `api/tests/test_goodwill_contribution.py` +- Modify: `openspec/changes/goodwill-table/{proposal,design,tasks}.md`, `specs/goodwill-table/spec.md` + +**Interfaces:** +- Produces: `GoodwillSource.load(version_id: UUID) -> Sources` (единственный метод загрузки); `repo.saved(session, version_id) -> list[Goodwill]`; + `repo.forms(session, version_id)`. + +- [ ] **Step 1:** `git -C checkout origin/main -- api/src/repositories/consolidations.py api/src/services/consolidated_osv.py`; + убедиться `git diff origin/main -- <эти два файла>` пуст. +- [ ] **Step 2:** удалить перечисленный код и тест; `grep -rn "contributions\|load_for_kfo\|company_pairs\|included_only" api/src api/tests` — пусто. +- [ ] **Step 3:** в OpenSpec удалить требования «Подготовка вклада в КФО» и «Источники КФО только выбранных сохранённых пар», + в design §6 и разделе «Отбор источников для КФО» оставить одну фразу «перенесено в AURORA-1546 (R-3)», в tasks.md пункты + 4.2, 4.3, 7.x — пометить «перенесено в AURORA-1546». +- [ ] **Step 4:** `make lint` и `cd api && .venv/bin/pytest -m integration tests/test_api_goodwill.py tests/test_api_goodwill_mutations.py tests/test_goodwill_export.py tests/test_api_consolidated_osv.py -q` — зелёные. +- [ ] **Step 5:** коммит «Гудвилл: заготовка вклада КФО вынесена в AURORA-1546». + +### Task 2: Доля — два знака, расчёт в домене, итоги по ГО и УК + +**Files:** +- Modify: `api/src/domain/goodwill.py`, `api/src/schemas/goodwill.py`, `api/src/services/goodwill.py`, `api/src/services/goodwill_export.py` +- Test: `api/tests/test_goodwill_domain.py`, `api/tests/test_api_goodwill.py`, `api/tests/test_goodwill_export.py`, `api/tests/test_api_goodwill_mutations.py` + +**Interfaces:** +- Produces (домен): `participation(capital: Decimal, share: Decimal) -> Decimal` (`capital*share/100`, HALF_UP до 0.01) — единственное + место формулы, `build` и сервис используют его; `totals(rows, enabled) -> dict[str, Decimal]` с ключами + `financial_investment_amount, charter_capital_amount, participation_amount, goodwill_amount, goodwill_in_kfo_amount` + (ФВ и УК — все действующие строки; P/G — действующие отмеченные; в КФО — `applicable` и `enabled`); + `group_totals(rows, enabled) -> dict[UUID, dict[str, Decimal]]` = `totals` по строкам каждой ГО (`head_id`). +- Produces (схемы): `GoodwillTotalsOut` + поле `charter_capital_amount: Money`; `GoodwillGroupOut {head_legal_entity: RefOut, totals: GoodwillTotalsOut}`; + `GoodwillTableOut.groups: list[GoodwillGroupOut]` — порядок как у строк (имя ГО, id); `Share` сериализуется `format(v, ".2f")`. +- Produces (сервис): `validate_decision(data) -> domain.Decision` принимает `^\d{1,3}(?:[.,]\d{1,2})?$` и `≤ 100`. + +- [ ] **Step 1: тест домена на примере DOC-259025 §11.1** — `test_goodwill_domain.py::test_doc_example_totals`: ГО «Альфа»; + Бета ФВ 180000 УК 100000 доля 80 отмечена, комментарий → P 80000.00, G 100000.00; Гамма ФВ 50000 УК 100000 доля 80 отмечена → P 80000.00, G −30000.00; + Дельта ФВ 60000 УК 60000 без доли, не отмечена → P/G None. `totals(rows, True)` = ФВ 290000.00, УК 260000.00, P 160000.00, + G 70000.00, в КФО 100000.00; `totals(rows, False)["goodwill_in_kfo_amount"] == 0`; `group_totals` для «Альфа» совпадает с `totals`; + строка «Нет вложений» (сохранённое решение без ФВ) не меняет ни одной суммы. +- [ ] **Step 2: тест доли** — `test_api_goodwill_mutations.py`: параметры `test_invalid_share` дополнить `"80.123"`, `"80,123"`, `"100.01"`, `"-1"`, `"1e2"`; + `test_valid_share` — `"0"`, `"100"`, `"80,12"`, `"7.5"`; ответ строки отдаёт `"80.12"`. +- [ ] **Step 3:** прогнать — FAIL (нет `participation`, `group_totals`, `charter_capital_amount`, регулярка пропускает 4 знака). +- [ ] **Step 4:** реализовать по Interfaces; в `service.save` (до Task 3) и `build` убрать собственные формулы P в пользу `participation`; + `table()` заполняет `groups` из `group_totals`; сообщение `INVALID_SHARE` — «Доля — число от 0 до 100, не больше двух знаков после запятой». +- [ ] **Step 5: экспорт** — «Всего» блока брать из `table.groups` (удалить `_totals`), в «Итого по листу» добавить УК, формат доли `"0.00"`; + `test_goodwill_export.py`: значение УК в строке «Итого по листу» равно `totals.charter_capital_amount`. +- [ ] **Step 6:** `test_api_goodwill.py`: в ответе таблицы есть `groups[0].totals` и `totals.charter_capital_amount` с ожидаемыми строками. +- [ ] **Step 7:** `make lint`, интеграционные тесты гудвилла — PASS; коммит «Гудвилл: доля до двух знаков, итоги по ГО и УК с бэка». + +### Task 3: Пакетное сохранение в одной транзакции + +**Files:** +- Modify: `api/src/schemas/goodwill.py`, `api/src/services/goodwill.py`, `api/src/api/endpoints/consolidations/goodwill.py` +- Test: `api/tests/test_api_goodwill_mutations.py`, `api/tests/test_api_goodwill.py`, `api/tests/test_goodwill_export.py`, `api/tests/test_openapi_routes.py` + +**Interfaces:** +- Consumes: `validate_decision`, `participation` (Task 2). +- Produces: маршрут `PUT /api/v1/consolidations/versions/{version_id}/goodwill`, право `CONSOLIDATION_EDIT`, тело + `GoodwillBatchIn {rows: list[GoodwillRowKeyIn]}` (`min_length=1`, `extra="forbid"`), где + `GoodwillRowKeyIn = {head_legal_entity_id: UUID, subsidiary_legal_entity_id: UUID, ownership_share: str | None, include_in_kfo: bool, comment: str | None (≤1000)}`; + ответ 200 `GoodwillTableOut`. Маршрут `PUT …/goodwill/{head}/{sub}` и `GoodwillService.save` удаляются; новый + `GoodwillService.save_rows(version_id: UUID, rows: list[GoodwillRowKeyIn], audit: RequestAudit) -> GoodwillTableOut`. +- Ошибки (все — откат всего пакета): 404 `NOT_FOUND` «Пара не найдена» и 422 `INVALID_SHARE` / `COMMENT_REQUIRED` / + `MONEY_OUT_OF_RANGE` с доп. полями `head_legal_entity_id`, `subsidiary_legal_entity_id` (для `NotFoundError` — создать + `DomainError(404, "NOT_FOUND", …, extra=…)`); пара дважды в пакете — 422 `DUPLICATE_ROW` с теми же полями; 409 правок версии — как раньше. + +- [ ] **Step 1: тесты** (`test_api_goodwill_mutations.py`, переписать вызовы на пакет): + `test_batch_rolls_back_when_one_row_invalid` — две пары, вторая с `include_in_kfo=true` без комментария → 422 `COMMENT_REQUIRED`, + `detail.subsidiary_legal_entity_id` = id второй ДО, в БД 0 записей `goodwill` и 0 `audit_event(GOODWILL_EDIT)`; + `test_unchanged_rows_are_not_rewritten` — сохранить строку, повторить тот же пакет → `updated_at` прежний, событий `GOODWILL_EDIT` по-прежнему одно; + `test_new_row_with_defaults_is_not_created` — пакет `{share: null, include: false, comment: null}` по паре без записи → записей 0; + `test_duplicate_pair_in_batch` → 422 `DUPLICATE_ROW`; `test_unknown_pair` → 404 с ключом пары. Сценарии прежнего + `test_save_settings_and_disappearing_pair` и `test_money_overflow_keeps_database_unchanged` сохранить, переведя на пакет. +- [ ] **Step 2:** `test_openapi_routes.py` — путь `…/goodwill`: `{"get", "put"}`, путь `…/goodwill/{head_legal_entity_id}/{subsidiary_legal_entity_id}` удалён. +- [ ] **Step 3:** прогнать — FAIL. +- [ ] **Step 4:** реализовать `save_rows`: `lock_editable_version` → одна сборка таблицы → проверка дублей, пар и ввода всех + строк до любой записи → upsert только изменённых (сравнение `ownership_share`, `include_in_kfo`, `comment` с сохранённым; + для пары без записи «изменение» — отличие от `Decision()`) с пересчётом сумм как сейчас → `flush` → одна сборка таблицы → + `GOODWILL_EDIT` по каждой изменённой строке (`before` — строка ответа до, `null` для новой; `after` — строка ответа после) → + вернуть таблицу. Поведение «отсутствующий ввод не затирает сохранённое у неактивной пары» сохранить. +- [ ] **Step 5:** PASS; `make lint`; коммит «Гудвилл: пакетное сохранение таблицы одной транзакцией». + +### Task 4: Единицы ББ через словарь arm_statements + +**Files:** +- Modify: `api/src/services/statements_engine.py` (Protocol `StatementsEngine` и `ArmStatementsEngine`), `api/src/services/goodwill_source.py` +- Test: `api/tests/test_goodwill_source.py` + +**Interfaces:** +- Produces: `StatementsEngine.canonical_units(text: str | None) -> str | None` — `normalize_caption(text)` (из `arm_statements.text`) + и первый совпавший шаблон `self._rules.units.spellings` → каноническая единица («руб.», «тыс. руб.», «млн руб.»), иначе `None`. + `capital_amounts` берёт множитель по `default_engine().canonical_units(units)`. + +- [ ] **Step 1: тест** `test_capital_units_spellings` (параметры): `"тыс.руб."`, `"в тысячах рублей"`, `"тыс. руб."` → УК = значение × 1000; + `"руб"` → × 1; `"млн. руб."` → × 1000000; `"штук"` и `None` → `(None, None)`. +- [ ] **Step 2:** FAIL → реализовать → PASS; если пакетный словарь в ядре подменяется заглушкой в тестах (`StatementsEngine` фейки в `api/tests`), + добавить метод и туда (`grep -rn "class .*Engine" api/tests`). +- [ ] **Step 3:** `make lint`; коммит «Гудвилл: единицы ББ по словарю arm_statements». + +### Task 5: Контракт, §8, OpenSpec, README + +**Files:** +- docs: `dev/api-contract-goodwill.md`, `dev/api-contract-goodwill.openapi.yaml`, `context/05-domain-rules.md` (§8 «Гудвилл») +- arm-core: `api/tests/test_goodwill_contract.py`, `README.md` (раздел «Таблица гудвилла», таблица «Ошибки API»), + `openspec/changes/goodwill-table/{proposal,design,tasks}.md`, `specs/goodwill-table/spec.md` + +**Interfaces:** +- Consumes: фактический `app.openapi()` после Tasks 1–4. + +- [ ] **Step 1: тест контракта** — читать `Path(__file__).resolve().parents[3] / "docs/dev/api-contract-goodwill.openapi.yaml"`, + при отсутствии файла — `pytest.skip("нет ../docs: контракт гудвилла не проверен")`; сравнение путей `/goodwill` и всех схем + контракта с `app.openapi()` — как сейчас; проверку `validate_decision` перевести на `"80,12"` → `Decimal("80.12")`. +- [ ] **Step 2: OpenAPI-копия** — выгрузить из `app.openapi()` пути с `/goodwill` и все схемы, на которые они ссылаются + (транзитивно, включая `RefOut`, `WarningOut`, `HTTPValidationError`), в `docs/dev/api-contract-goodwill.openapi.yaml` (`openapi`, `info` + с версией `1.0`, `paths`, `components.schemas`); тест Step 1 — PASS. +- [ ] **Step 3: MD-контракт** — переписать `docs/dev/api-contract-goodwill.md` под факт: источники (DOC-259025 03.11 первым), понятия + (ГО по вложениям, «Нет вложений», P/G только у отмеченных, предупреждения `CAPITAL_SOURCE_MISSING`/`CAPITAL_SOURCE_AMBIGUOUS`/ + `OWNERSHIP_TOTAL_EXCEEDED`), эндпоинты (GET, PUT пакет, PUT settings, GET export с `sign=all|positive|negative|zero`), схемы, ошибки, + пример ответа с `groups`, открытые вопросы OQ-10/OQ-37; раздел «Что изменилось против черновика 30.10» — для фронта. +- [ ] **Step 4: §8** — заменить абзац «Гудвилл» в `05-domain-rules.md` на выжимку DOC-259025 §§1–8 с решениями R-1, R-5 и + пометкой «применение в КФО и исключение ББ-2 — AURORA-1546, не реализовано». +- [ ] **Step 5: arm-core** — README: ссылка на `../docs/dev/api-contract-goodwill.md`, пакетный PUT, коды `INVALID_SHARE`, + `COMMENT_REQUIRED`, `MONEY_OUT_OF_RANGE`, `DUPLICATE_ROW` в «Ошибки API»; OpenSpec — требования «Построчное сохранение» → «Пакетное сохранение», + доля — два знака, итоги по ГО и УК, единицы; раздел `## Decisions made without discussion` с R-1…R-6 (дата 2026-11-06, решение, + обоснование, цена ошибки); `openspec validate goodwill-table --strict` — valid. +- [ ] **Step 6:** `make check` (весь unit + lint) и `make test-integration` в arm-core — зелёные; `make dbml-check` — без расхождений. +- [ ] **Step 7:** коммиты: docs — «Гудвилл: контракт API и §8 под DOC-259025 и ревью core #222 (#204)»; arm-core — «Гудвилл: контракт в docs, README, OpenSpec под ревью». diff --git a/openspec/changes/goodwill-table/design.md b/openspec/changes/goodwill-table/design.md index 2a1d5a5..a961051 100644 --- a/openspec/changes/goodwill-table/design.md +++ b/openspec/changes/goodwill-table/design.md @@ -42,7 +42,7 @@ GoodwillTableOut и GoodwillRowOut повторяют MD с уточнениям ### 6. Граница КФО -Внутренний DTO/чистая функция возвращает применимые пары с version_id, head_id, subsidiary_id, FV/P/G, дельтами и ключом исключения (balance, rule_id ББ-2 закреплённой elimination_rules_version, head_id, subsidiary_id). Переключатель — дополнительное обязательное условие. При выключенном переключателе или отсутствии применимых пар вклад пуст и правило не запрашивается. Для непустого вклада нет правила ББ-2 в закреплённом наборе — подготовка вклада даёт явный конфликт GOODWILL_RULE_MISSING, не подставляет старое правило №7. GET таблицы может работать без подготовки вклада. Не записывать KfoResult и не менять статусы/суммы Correction. Не создавать фиктивный механизм инвалидирования: требование AURORA-1543 о stale и фиксация VERIFIED перенесены в будущий change КФО с явной зависимостью на этот контракт. +Подготовка вклада в КФО (применимые пары, изменения 1170/1310/GOODWILL, исключение ББ-2), выбор источников только сохранённых выбранных пар и пометка результата КФО устаревшим в этот change не входят — перенесено в AURORA-1546 (R-3, R-6). Этот change не пишет KfoResult и не трогает Correction. ### 7. XLSX @@ -61,12 +61,3 @@ GoodwillTableOut и GoodwillRowOut повторяют MD с уточнениям Миграция БД не требуется: существующие поля и enum достаточны. Выкладка добавляет маршруты и обновлённый контракт; перед подключением фронта проверить nullable поля. Откат кода оставляет goodwill и аудит в совместимой схеме. Пока фронт отдельно, существующие страницы не изменяются. По решению пользователя ФВ пары — сумма только заполненных closing_debit; пустые значения остальных строк не блокируют расчёт P/G и сохранение ручного ввода. - -## Отбор источников для КФО по запросу пользователя - -У таблицы остаётся полный load. contributions проверяет видимость/переключатель, выбирает только сохранённые -include_in_kfo=true и вызывает load_for_kfo с направленными парами. Репозиторий ОСВ получает company_pairs и -account_ids ветки 58.01, репозиторий ББ — идентификаторы выбранных ДО. Общая нормализация периодов и дорезолв -сохраняются: NULL-контрагенты выбранных владельцев/счетов рассматриваются как кандидаты, итог свода после -дорезолва фильтруется строго по выбранным парам. Невыбранные сопоставленные пары не загружаются из osv_entry. -Без выбранных записей и при выключении нет загрузки ОСВ/ББ. diff --git a/openspec/changes/goodwill-table/proposal.md b/openspec/changes/goodwill-table/proposal.md index e596a3f..25a6b85 100644 --- a/openspec/changes/goodwill-table/proposal.md +++ b/openspec/changes/goodwill-table/proposal.md @@ -11,7 +11,7 @@ - Найденные пары объединяются с сохранёнными `goodwill`; исчезнувшие остаются с вычисляемым `has_investments=false`, без участия в итогах. - Доля 0–100%, до четырёх знаков; нерассчитанные P/G допускают null. Сохранение одной строки атомарно. - Расчёт по §8 domain-rules с уточнениями PDF §§3–10 и решениями пользователя: ASSUMPTION(OQ-10) — УК только из 1310 ББ, 1350 не входит; ASSUMPTION(OQ-37) — подготовленный вклад имеет технический код GOODWILL. -- Подготовка чистого результата для будущего КФО: применимые пары, изменения 1170/1310/GOODWILL и исключаемое направление ББ-2. Запись КФО не выполняется. +- Подготовка вклада в КФО и исключение ББ-2 — не в этом change, перенесено в AURORA-1546 (R-3). - Уточнения PDF, отсутствующие в §8 (неактивные пары, выборка по дереву, итоги, исключение ББ-2), требуют отдельной синхронизации в docs; при реализации не подменять ими молча старую редакцию правил. ## Capabilities diff --git a/openspec/changes/goodwill-table/specs/goodwill-table/spec.md b/openspec/changes/goodwill-table/specs/goodwill-table/spec.md index 72ba810..3668cf2 100644 --- a/openspec/changes/goodwill-table/specs/goodwill-table/spec.md +++ b/openspec/changes/goodwill-table/specs/goodwill-table/spec.md @@ -81,27 +81,9 @@ GET …/goodwill/export SHALL выгружать XLSX с составом и п - **WHEN** выгружается таблица реальной версии с сохранёнными решениями и неактивной парой - **THEN** XLSX соответствует серверной таблице, содержит «Нет вложений» и не учитывает эту строку в итогах; отдельное событие EXPORT содержит report=goodwill и число строк -### Requirement: Подготовка вклада в КФО -Система SHALL готовить данные только из сохранённого ввода и актуальных источников: при включённом переключателе, действующей выбранной паре, заполненных доле/комментарии и G>0 — 1170 ГО −ФВ, 1310 ДО −P, GOODWILL ГО +G и направление исключения ББ-2 закреплённой версии правил. Запись КФО и изменение корректировок MUST NOT выполняться. - -#### Scenario: Неположительный гудвилл -- **WHEN** пересчитанная сохранённая пара имеет G≤0, нет УК либо выключен переключатель -- **THEN** её специальный вклад пуст, ББ-2 не исключается; отрицательное G не переносится в 1370 и не уменьшает положительный вклад других пар - ### Requirement: Пустые значения конечного дебета По решению пользователя система SHALL суммировать только заполненные closing_debit пары; пустые значения остальных строк MUST NOT блокировать расчёт P/G и сохранение ручного ввода. #### Scenario: Заполненная и пустая строки одной пары - **WHEN** у пары есть вложения 269997300.00 и строка с closing_debit=null, доля 100%, УК 100000000.00 и комментарий - **THEN** сохранение разрешено; ФВ=269997300.00, P=100000000.00, G=169997300.00 - -### Requirement: Источники КФО только выбранных сохранённых пар -Подготовка КФО SHALL брать только сохранённые goodwill выбранной версии с include_in_kfo=true, ОСВ этих направленных пар и ББ выбранных ДО. При выключенном переключателе или пустом выборе источники ОСВ/ББ MUST NOT загружаться. Дорезолв SHALL сохранять соответствие расчёту таблицы. - -#### Scenario: Выбранная и невыбранная пары -- **WHEN** сохранены А→Б с include_in_kfo=true и А→В с false, а для Б→А goodwill отсутствует -- **THEN** загружаются ОСВ А→Б и ББ Б; А→В и Б→А не дают источников и вклада, полный load таблицы не вызывается - -#### Scenario: Пустой выбор -- **WHEN** выбранных сохранённых строк нет либо goodwill_enabled=false -- **THEN** вклад пуст, загрузка ОСВ и ББ не выполняется diff --git a/openspec/changes/goodwill-table/tasks.md b/openspec/changes/goodwill-table/tasks.md index f6e4cbe..73cf0c5 100644 --- a/openspec/changes/goodwill-table/tasks.md +++ b/openspec/changes/goodwill-table/tasks.md @@ -25,8 +25,8 @@ ## 4. Экспорт и граница КФО - [x] 4.1 arm-core: добавить тесты экспорта гудвилла и сервис экспорта гудвилла, маршрут экспорта; проверить содержимое XLSX, групповые/общие итоги, пустые ячейки null, текстовую безопасность, имя кириллицей и EXPORT с report=goodwill. Документировать экспорт в локальном MD/OpenAPI. -- [x] 4.2 arm-core: добавить чистую подготовку вклада в доменном модуле расчёта гудвилла и адаптер сохранённого ввода в сервисе гудвилла; тестами подготовки вклада гудвилла проверить баланс −ФВ+G=−P, только положительные G, выключенный режим, отсутствие капитала, закреплённое ББ-2 и направление исключения. Проверить, что KfoResult и Correction не записываются. -- [x] 4.3 arm-core: описать внутренний контракт вклада и ограничения этапа в описательном контракте API; проверка — документ явно отделяет подготовку от применения, устаревания и снимка КФО, не обещает завершение полного AURORA-1543. +- [x] 4.2 перенесено в AURORA-1546 (R-3): подготовка вклада в КФО удалена из этого change. +- [x] 4.3 перенесено в AURORA-1546 (R-3). ## 5. Сквозная проверка и передача @@ -36,7 +36,7 @@ ## Workflow follow-up - Отдельный change ../docs синхронизирует архитектуру, доменные правила, открытые вопросы и общий API-контракт; не назначать новые OQ-номера без проверки реестра. -- Следующий change КФО реализует запись результата, устаревание, применение/исключение ББ-2 и фиксацию VERIFIED; AURORA-1543 полностью закрывается после его приёмки. +- Следующий change КФО (AURORA-1546) реализует подготовку вклада, запись результата, устаревание, применение/исключение ББ-2 и фиксацию VERIFIED; AURORA-1543 полностью закрывается после его приёмки. - Фронт AURORA-1544 подключается по согласованному обновлённому контракту, отдельной задачей. ## 6. Исправления по ревью пользователя @@ -47,9 +47,8 @@ ## 7. Выбор источников для КФО -- [x] 7.1 arm-core: отдельный GoodwillSource.load_for_kfo; contributions загружает только goodwill include_in_kfo=true, ОСВ выбранных направленных пар и ББ выбранных ДО. Проверить SQL-отбор и отсутствие полной загрузки при пустом выборе/выключении. -- [x] 7.2 arm-core: обновить описание внутреннего контракта и спецификацию; проверить регрессию таблицы/ОСВ, линтеры и типы. - +- [x] 7.1 перенесено в AURORA-1546 (R-3): `load_for_kfo` и фильтры общего свода удалены. +- [x] 7.2 перенесено в AURORA-1546 (R-3). ## 8. Экспорт по экранной таблице