feat/goodwillback: бэкенд гудвилл #222
@ -1,4 +1,4 @@
|
||||
"""Таблица гудвилла, построчное сохранение, переключатель и XLSX."""
|
||||
"""Таблица гудвилла, пакетное сохранение, переключатель и XLSX (§8 «Гудвилл»)."""
|
||||
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
@ -283,7 +283,8 @@ def _decision(row: GoodwillRowOut, item: GoodwillRowKeyIn, key: tuple[UUID, UUID
|
||||
if not row.has_investments:
|
||||
# DOC-259025 §10 п. 9: отсутствующий ввод не затирает сохранённые данные неактивной пары.
|
||||
if share is None and row.ownership_share is not None:
|
||||
share = str(row.ownership_share)
|
||||
# numeric(7,4) читается с масштабом 4 («80.1200»): без хвостовых нулей для проверки двух знаков
|
||||
share = format(row.ownership_share.normalize(), "f")
|
||||
if comment is None:
|
||||
comment = row.comment
|
||||
try:
|
||||
|
||||
@ -19,6 +19,7 @@ COLUMNS = {
|
||||
"goodwill_amount": "Гудвилл (+/-)",
|
||||
"comment": "Комментарий",
|
||||
}
|
||||
NO_INVESTMENTS = "Нет вложений"
|
||||
MONEY = ("financial_investment_amount", "participation_amount", "charter_capital_amount", "goodwill_amount")
|
||||
|
||||
|
||||
@ -43,7 +44,9 @@ def build_workbook(table: GoodwillTableOut, sign: GoodwillSign = "all") -> bytes
|
||||
row = sheet.max_row + 1
|
||||
for col, key in enumerate(keys, 1):
|
||||
value = values.get(key)
|
||||
if key in MONEY or key == "ownership_share":
|
||||
if isinstance(value, str) and key in MONEY:
|
||||
_put_text(sheet, row, col, value)
|
||||
elif key in MONEY or key == "ownership_share":
|
||||
_put_money(sheet, row, col, value)
|
||||
else:
|
||||
_put_text(sheet, row, col, value)
|
||||
@ -66,6 +69,8 @@ def build_workbook(table: GoodwillTableOut, sign: GoodwillSign = "all") -> bytes
|
||||
)
|
||||
for item in visible:
|
||||
values = {key: getattr(item, key) for key in MONEY}
|
||||
if not item.has_investments:
|
||||
values["financial_investment_amount"] = NO_INVESTMENTS
|
||||
values.update(
|
||||
name=item.subsidiary_legal_entity.name,
|
||||
include_in_kfo="☑" if item.include_in_kfo else "☐",
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
from decimal import Decimal
|
||||
from http import HTTPStatus
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import func, select
|
||||
@ -13,8 +14,9 @@ from src.models.enums import AuditAction
|
||||
from src.models.kfo import Goodwill, KfoResult
|
||||
from src.models.osv import OsvEntry
|
||||
from src.models.statements import FinancialStatementValue
|
||||
from src.schemas.goodwill import GoodwillRowIn
|
||||
from src.services.goodwill import validate_decision
|
||||
from src.schemas.consolidations import RefOut
|
||||
from src.schemas.goodwill import GoodwillRowIn, GoodwillRowKeyIn, GoodwillRowOut
|
||||
from src.services.goodwill import _decision, validate_decision
|
||||
from tests.test_api_consolidated_osv import KA_RF
|
||||
from tests.test_api_goodwill import batch, goodwill_world
|
||||
|
||||
@ -228,3 +230,36 @@ async def test_new_row_with_defaults_is_not_created(db_session, auth_client, fix
|
||||
assert response.json()["rows"][0]["include_in_kfo"] is False
|
||||
assert await db_session.scalar(select(func.count()).select_from(Goodwill)) == 0
|
||||
assert await _goodwill_audit_count(db_session) == 0
|
||||
|
||||
|
||||
def test_inactive_row_keeps_stored_share_with_db_scale():
|
||||
"""Колонка numeric(7,4) читается как Decimal("80.1200"): null в пакете не должен давать INVALID_SHARE."""
|
||||
head, sub = uuid4(), uuid4()
|
||||
row = GoodwillRowOut(
|
||||
head_legal_entity=RefOut(id=head, name="ГО"),
|
||||
subsidiary_legal_entity=RefOut(id=sub, name="ДО"),
|
||||
ownership_share=Decimal("80.1200"),
|
||||
financial_investment_amount=None,
|
||||
charter_capital_amount=None,
|
||||
additional_capital_amount=None,
|
||||
participation_amount=None,
|
||||
goodwill_amount=None,
|
||||
include_in_kfo=True,
|
||||
comment="Было",
|
||||
has_investments=False,
|
||||
warnings=[],
|
||||
updated_by=None,
|
||||
updated_at=None,
|
||||
)
|
||||
item = GoodwillRowKeyIn(
|
||||
head_legal_entity_id=head,
|
||||
subsidiary_legal_entity_id=sub,
|
||||
ownership_share=None,
|
||||
include_in_kfo=True,
|
||||
comment=None,
|
||||
)
|
||||
decision = _decision(row, item, (head, sub))
|
||||
assert decision.ownership_share == Decimal("80.12")
|
||||
assert decision.comment == "Было"
|
||||
row100 = row.model_copy(update={"ownership_share": Decimal("100.0000")})
|
||||
assert _decision(row100, item, (head, sub)).ownership_share == Decimal(100)
|
||||
|
||||
@ -73,7 +73,7 @@ async def test_export_matches_server_table(db_session, auth_client, fixtures_dir
|
||||
)
|
||||
response = await client.get(base + "/goodwill/export")
|
||||
sheet = load_workbook(BytesIO(response.content)).active
|
||||
assert sheet.cell(3, 4).value is None
|
||||
assert sheet.cell(3, 4).value == "Нет вложений"
|
||||
assert sheet.cell(3, 7).value is None
|
||||
assert sheet.cell(5, 4).value == 0
|
||||
assert sheet.cell(5, 7).value == 0
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
### 3. Ручной ввод и отсутствие вложений
|
||||
|
||||
Ключ — (version_id, head_id, subsidiary_id), существующий UNIQUE. GET объединяет найденные пары с goodwill и ничего не пишет. Впервые запись создаётся при PUT строки; не материализуем все строки при чтении или смене переключателя. Это обычное сохранение по PUT из MD §3.3; GET формирует ответ без записи.
|
||||
Ключ — (version_id, head_id, subsidiary_id), существующий UNIQUE. GET объединяет найденные пары с goodwill и ничего не пишет. Впервые запись создаётся при сохранении строки пакетом (`PUT …/goodwill`, R-2); не материализуем все строки при чтении или смене переключателя; GET формирует ответ без записи.
|
||||
|
||||
has_investments — вычисляемый DTO-флаг, не колонка. По прямому решению пользователя для «Нет вложений» все денежные колонки ответа равны null; сохранённый ввод и последний денежный снимок в БД не удаляются. Строка исключается из итогов и применения в КФО. Собственной ошибки GOODWILL_PAIR_INACTIVE нет. Отсутствующие доля и комментарий не затирают сохранённый ввод неактивной пары (PDF §10).
|
||||
|
||||
@ -34,11 +34,11 @@ has_investments — вычисляемый DTO-флаг, не колонка. П
|
||||
|
||||
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.
|
||||
GoodwillTableOut и GoodwillRowOut повторяют MD с уточнениями spec: has_investments, warnings[] из {code, message}; charter_capital_amount и additional_capital_amount тоже nullable, чтобы отсутствие ББ не маскировать нулём. totals — итог по листу, groups — итоги «Всего» по каждой ГО; оба считает бэк (R-4). При 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 не нужны. PUT …/goodwill принимает набор строк и сохраняет его одной транзакцией (R-2): сначала проверяются все строки, потом пишутся изменённые. Отдельная оптимистическая ревизия не добавляется: существующая блокировка версии сериализует операции, последнее сохранение владельца побеждает.
|
||||
Маршруты гудвилла используют зависимости по образцу API корректировок. Service использует текущие policy и lock_editable_version. PUT …/goodwill под блокировкой версии загружает актуальные источники, проверяет пары и ввод всех строк, затем upsert изменённых записей и GOODWILL_EDIT before/after по каждой в той же транзакции. Вычисляемые суммы из тела не принимаются. PUT настроек не трогает строки; GOODWILL_EDIT имеет object_type=consolidation_version и changed_field=goodwill_enabled, строки — object_type=goodwill. Новые enum не нужны. Отдельная оптимистическая ревизия не добавляется: существующая блокировка версии сериализует операции, последнее сохранение владельца побеждает.
|
||||
|
||||
### 6. Граница КФО
|
||||
|
||||
@ -85,3 +85,8 @@ GoodwillTableOut и GoodwillRowOut повторяют MD с уточнениям
|
||||
- **R-6 (2026-11-06). Пометка результата КФО устаревшим при смене переключателя или строк не делается — в AURORA-1546.**
|
||||
Обоснование: результата КФО (`kfo_result`) ещё не строится, помечать нечего. Цена ошибки: в 1546 добавить сброс
|
||||
в `save_rows` и `settings`.
|
||||
- **R-7 (2026-11-06, финальное ревью I-1). Сохранённая доля неактивной строки нормализуется** (`80.1200` → `80.12`)
|
||||
перед проверкой ввода: колонка `numeric(7,4)` читается с масштабом 4, а ввод ограничен двумя знаками (R-1). Цена ошибки:
|
||||
если в БД окажется доля с 3–4 значащими знаками (данные до R-1), пакет с этой неактивной строкой даст 422 `INVALID_SHARE`.
|
||||
- **R-8 (2026-11-06, финальное ревью). В XLSX у строки «Нет вложений» в колонке ФВ — текст «Нет вложений»** (сценарий спеки
|
||||
«Выгрузка таблицы»). Цена ошибки: нет.
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
## 1. Источники и контракт расчёта
|
||||
|
||||
- [x] 1.1 arm-core: добавить тесты на реальных исходных данных с загрузкой неизменённых реальных fixtures счёта 58.01 «Приманыческий» 2025 Q3 и 58.03 РБ 2025 Q1 / 2024 Q3; проверить реальные счета, контрагентов и суммы, зафиксировать положительный набор 58.01 и отрицательный 58.03. Не подменять отсутствующий реальный источник синтетической суммой.
|
||||
- [x] 1.2 arm-core: добавить доменные тесты гудвилла и кейсы состояний на реальных исходных строках: направление, неиспользование is_head_entity, исчезновение/возврат пары, ноль/null, четыре знака, Decimal-округление, итоги, сумма долей >100. Проверка — воспроизводимый набор ожидаемых расчётов до реализации.
|
||||
- [x] 1.2 arm-core: добавить доменные тесты гудвилла и кейсы состояний на реальных исходных строках: направление, неиспользование is_head_entity, исчезновение/возврат пары, ноль/null, четыре знака (позже — два, R-1), Decimal-округление, итоги, сумма долей >100. Проверка — воспроизводимый набор ожидаемых расчётов до реализации.
|
||||
- [x] 1.3 arm-core: реализовать доменный модуль расчёта гудвилла: пары по дереву счетов, объединение сохранённого ввода, формулы и предупреждения; проверить тесты 1.1–1.2, отсутствие ORM/float и записи вычисляемых пар при чтении.
|
||||
|
||||
## 2. Загрузка и API чтения
|
||||
@ -18,7 +18,7 @@
|
||||
## 3. Сохранение и переключатель
|
||||
|
||||
- [x] 3.1 arm-core: добавить тесты изменения данных гудвилла для создания/повторного сохранения, снятия выбора, доли null/0/100/лишней точности, COMMENT_REQUIRED, неактивной пары, переполнения денег и отката аудита вместе с записью; добавить проверки статусов/блокировок и двух сериализованных сохранений.
|
||||
- [x] 3.2 arm-core: реализовать PUT строки в сервисе и репозитории гудвилла через consolidation_guard; запись goodwill и GOODWILL_EDIT атомарна. Проверка — тесты 3.1, одна запись на естественный ключ и неизменность остальных строк.
|
||||
- [x] 3.2 arm-core (позже заменено пакетным PUT, R-2): реализовать PUT строки в сервисе и репозитории гудвилла через consolidation_guard; запись goodwill и GOODWILL_EDIT атомарна. Проверка — тесты 3.1, одна запись на естественный ключ и неизменность остальных строк.
|
||||
- [x] 3.3 arm-core: реализовать PUT goodwill-settings, добавить тесты изменения/повтора значения, сохранности ручного ввода и нулевого вклада при выключении; проверить единое событие GOODWILL_EDIT только при изменении переключателя и отсутствие изменений KfoResult.
|
||||
- [x] 3.4 arm-core: обновить описательный контракт API и OpenAPI-контракт для PUT, ошибок и null доли снятой строки; проверить примеры запросов/ответов контрактными тестами и одинаковую семантику MD/OpenAPI.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user