Compare commits
No commits in common. "2ef4c6ae86b66118465010227809466cd9cf256c" and "71ef6333b53287c421fc920c2f204d0cc1efa782" have entirely different histories.
2ef4c6ae86
...
71ef6333b5
@ -873,8 +873,6 @@ API: список счетов `GET /directories/accounts`, правила ве
|
||||
`GET …/intercompany-matrix/result-table` и её XLSX `GET …/intercompany-matrix/result-table/export`
|
||||
(аудит `EXPORT`, `report = "intercompany_result_table"`). Режим PRESET (счета и показатель из правила элиминации):
|
||||
`GET …/intercompany-matrix/preset/{rule_id}` и его выгрузка `…/preset/{rule_id}/export`.
|
||||
Режим ALL — только балансовые счета (без 001–012), MANUAL принимает и забалансовые. `one_sided` — ровно одна сумма
|
||||
пары нулевая; пара подсвечивается (и в xlsx) при `exceeds_threshold` или `one_sided` (AURORA-1651).
|
||||
`GET …/elimination-rules` отдаёт объект `{version_no, is_version_fixed, rules}` (у правила есть `id`): при незаданной
|
||||
версии правил — открытые правила (`valid_to IS NULL`) с максимальным `version_no`; ответ сообщает этот номер. Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`,
|
||||
`ACCOUNTS_REQUIRED`; правило недействительное для версии или несуществующее — 404. Имя XLSX пресета оканчивается на
|
||||
|
||||
@ -226,8 +226,7 @@ def calculate_matrix(
|
||||
variance_amount=variance_amount,
|
||||
variance_percent=variance_percent,
|
||||
exceeds_threshold=exceeds_threshold,
|
||||
# пара без встречной стороны — ровно одна сумма нулевая (знак не важен: сальдо бывает отрицательным)
|
||||
one_sided=(amount_a == ZERO) != (amount_b == ZERO),
|
||||
one_sided=bool(debit_ids) != bool(credit_ids),
|
||||
skipped_osv_entry_count=skipped_entries[(company_a, company_b)]
|
||||
+ skipped_entries[(company_b, company_a)],
|
||||
osv_entry_ids_debit=debit_ids,
|
||||
@ -405,8 +404,8 @@ class IntercompanyMatrixService:
|
||||
) -> IntercompanyMatrixOut:
|
||||
"""Построить шахматку по выбранному режиму.
|
||||
|
||||
ALL включает балансовые счета соответствующей стороны (без забалансовых); MANUAL — выбранные счета и их
|
||||
потомков; PRESET берёт счета и показатель из правила с идентификатором `rule_id`.
|
||||
ALL включает все счета соответствующей стороны; MANUAL — выбранные счета и их потомков;
|
||||
PRESET берёт счета и показатель из правила с идентификатором `rule_id`.
|
||||
"""
|
||||
result = await self._build_accounts(
|
||||
version_id,
|
||||
@ -520,12 +519,8 @@ class IntercompanyMatrixService:
|
||||
if mode == "ALL":
|
||||
if debit_account_ids or credit_account_ids:
|
||||
raise DomainValidationError("ACCOUNTS_NOT_ALLOWED", "Для режима ALL не нужно указывать счета")
|
||||
debit = frozenset(
|
||||
account.id for account in accounts if not account.is_off_balance and account.kind in DEBIT_KINDS
|
||||
)
|
||||
credit = frozenset(
|
||||
account.id for account in accounts if not account.is_off_balance and account.kind in CREDIT_KINDS
|
||||
)
|
||||
debit = frozenset(account.id for account in accounts if account.kind in DEBIT_KINDS)
|
||||
credit = frozenset(account.id for account in accounts if account.kind in CREDIT_KINDS)
|
||||
selection = MatrixSelection(debit, credit, "closing")
|
||||
elif mode == "MANUAL":
|
||||
if not debit_account_ids or not credit_account_ids:
|
||||
|
||||
@ -51,13 +51,11 @@ def _write_details(
|
||||
|
||||
|
||||
def _write_context(sheet: Worksheet, info: MatrixExportInfo) -> int:
|
||||
# режим ALL не берёт забалансовые счета (AURORA-1651), шапка говорит об этом прямо
|
||||
all_accounts = "Все балансовые счета"
|
||||
mode_name = {"ALL": all_accounts, "MANUAL": "Выбранные счета", "PRESET": "Пресет"}[info.mode]
|
||||
mode_name = {"ALL": "Все счета", "MANUAL": "Выбранные счета", "PRESET": "Пресет"}[info.mode]
|
||||
extra = [
|
||||
("Режим", mode_name),
|
||||
("Счета дебета", ", ".join(info.debit_account_codes) if info.mode != "ALL" else all_accounts),
|
||||
("Счета кредита", ", ".join(info.credit_account_codes) if info.mode != "ALL" else all_accounts),
|
||||
("Счета дебета", ", ".join(info.debit_account_codes) if info.mode != "ALL" else "Все счета"),
|
||||
("Счета кредита", ", ".join(info.credit_account_codes) if info.mode != "ALL" else "Все счета"),
|
||||
("Показатель", "Сальдо" if info.measure == "closing" else "Оборот"),
|
||||
]
|
||||
if info.mode == "PRESET":
|
||||
@ -136,7 +134,7 @@ def build_matrix_workbook(matrix: IntercompanyMatrixOut, info: MatrixExportInfo)
|
||||
if cell is None:
|
||||
continue
|
||||
_write_values(sheet, start_row, column, cell)
|
||||
if cell.exceeds_threshold or cell.one_sided:
|
||||
if cell.exceeds_threshold:
|
||||
for offset in range(len(METRICS)):
|
||||
marked = sheet.cell(start_row + offset, column)
|
||||
marked.fill = THRESHOLD_FILL
|
||||
|
||||
@ -264,28 +264,6 @@ async def test_unfixed_rules_choose_latest_open_version(auth_client: AuthClientF
|
||||
assert old_preset.status_code == NOT_FOUND
|
||||
|
||||
|
||||
async def test_all_excludes_off_balance_accounts_but_manual_can_select_them(
|
||||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||||
) -> None:
|
||||
world = await _world(seeded)
|
||||
assert world.accounts["001"].is_off_balance is True
|
||||
await _source(seeded, world, credit=False, amount="100", account="001")
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
all_response = await _request(client, world.version.id, "all", world.selection)
|
||||
assert all_response.status_code == OK, all_response.text
|
||||
assert all(Decimal(cell["amount_a"]) == 0 for cell in all_response.json()["cells"])
|
||||
|
||||
manual_selection = {
|
||||
"debit_account_ids": [str(world.accounts["001"].id)],
|
||||
"credit_account_ids": [str(world.accounts["60.01"].id)],
|
||||
}
|
||||
manual_response = await _request(client, world.version.id, "manual", manual_selection)
|
||||
assert manual_response.status_code == OK, manual_response.text
|
||||
cell = next(c for c in manual_response.json()["cells"] if c["company_a_id"] == str(world.members[0].id))
|
||||
assert Decimal(cell["amount_a"]) == Decimal("100")
|
||||
|
||||
|
||||
async def test_unmapped_account_source_is_visible_in_cell_count(
|
||||
auth_client: AuthClientFactory, seeded: AsyncSession
|
||||
) -> None:
|
||||
@ -318,7 +296,7 @@ async def test_pair_without_counterpart(
|
||||
cell = next(c for c in response.json()["cells"] if c["company_a_id"] == str(world.members[0].id))
|
||||
assert Decimal(cell["amount_a"]) == (Decimal(0) if credit else Decimal(amount))
|
||||
assert Decimal(cell["amount_b"]) == (Decimal(amount) if credit else Decimal(0))
|
||||
assert cell["one_sided"] is (Decimal(amount) != 0)
|
||||
assert cell["one_sided"] is True
|
||||
assert cell["variance_percent"] is None
|
||||
assert cell["exceeds_threshold"] is False
|
||||
assert bool(cell["osv_entry_ids_credit"]) is credit
|
||||
@ -426,9 +404,9 @@ async def test_export_filename_header_and_percentage(
|
||||
"2024-Q1",
|
||||
"v1",
|
||||
"руб.; расхождение — %",
|
||||
"Выбранные счета" if mode == "manual" else "Все балансовые счета",
|
||||
"62.01" if mode == "manual" else "Все балансовые счета",
|
||||
"60.01" if mode == "manual" else "Все балансовые счета",
|
||||
"Выбранные счета" if mode == "manual" else "Все счета",
|
||||
"62.01" if mode == "manual" else "Все счета",
|
||||
"60.01" if mode == "manual" else "Все счета",
|
||||
]
|
||||
assert sheet["B14"].value == "Расхождение, %"
|
||||
expected_percent = Decimal("11.11")
|
||||
|
||||
@ -201,58 +201,7 @@ async def test_preset_rejects_income_rule_before_selecting_accounts(monkeypatch:
|
||||
assert error.value.code == "INVALID_RULE"
|
||||
|
||||
|
||||
async def test_all_excludes_off_balance_accounts_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
company_a, company_b = uuid.uuid4(), uuid.uuid4()
|
||||
off_balance = SimpleNamespace(id=uuid.uuid4(), kind=AccountKind.A, parent_id=None, is_off_balance=True)
|
||||
off_balance_credit = SimpleNamespace(id=uuid.uuid4(), kind=AccountKind.P, parent_id=None, is_off_balance=True)
|
||||
debit = SimpleNamespace(id=uuid.uuid4(), kind=AccountKind.A, parent_id=None, is_off_balance=False)
|
||||
credit = SimpleNamespace(id=uuid.uuid4(), kind=AccountKind.P, parent_id=None, is_off_balance=False)
|
||||
accounts = [off_balance, off_balance_credit, debit, credit]
|
||||
monkeypatch.setattr(matrix.directory_repo, "list_accounts", AsyncMock(return_value=accounts))
|
||||
rows = [
|
||||
_row(company_a, company_b, off_balance.id, debit="100"),
|
||||
_row(company_a, company_b, debit.id, debit="30"),
|
||||
_row(company_b, company_a, off_balance_credit.id, credit="40"),
|
||||
_row(company_b, company_a, credit.id, credit="20"),
|
||||
]
|
||||
members = {
|
||||
company_a: SimpleNamespace(id=company_a, name="Компания А"),
|
||||
company_b: SimpleNamespace(id=company_b, name="Компания Б"),
|
||||
}
|
||||
osv_service = SimpleNamespace(
|
||||
build_with_members=AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
consolidated=SimpleNamespace(rows=rows), version=None, consolidation=None, members=members
|
||||
)
|
||||
)
|
||||
)
|
||||
service = matrix.IntercompanyMatrixService(SimpleNamespace(), None, osv_service)
|
||||
|
||||
all_matrix = await service.build_accounts(uuid.uuid4(), mode="ALL")
|
||||
manual_matrix = await service.build_accounts(
|
||||
uuid.uuid4(), mode="MANUAL", debit_account_ids=[off_balance.id], credit_account_ids=[off_balance_credit.id]
|
||||
)
|
||||
assert all_matrix.cells[0].amount_a == Decimal("30")
|
||||
assert all_matrix.cells[0].amount_b == Decimal("20")
|
||||
assert manual_matrix.cells[0].amount_a == Decimal("100")
|
||||
assert manual_matrix.cells[0].amount_b == Decimal("40")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("debit_amount", "credit_amount", "one_sided", "exceeds"),
|
||||
[
|
||||
("100000000", "0", True, False),
|
||||
("0", "100000000", True, False),
|
||||
("0", "0", False, False),
|
||||
("100", "100", False, False),
|
||||
("100", "50", False, True),
|
||||
("-50", "0", True, False),
|
||||
("0", "-50", True, False),
|
||||
],
|
||||
)
|
||||
def test_one_sided_uses_amounts_even_with_zero_balance_rows(
|
||||
debit_amount: str, credit_amount: str, one_sided: bool, exceeds: bool
|
||||
) -> None:
|
||||
def test_one_sided_reflects_osv_sources_even_when_amount_is_zero() -> None:
|
||||
company_a, company_b = uuid.uuid4(), uuid.uuid4()
|
||||
debit_account, credit_account = uuid.uuid4(), uuid.uuid4()
|
||||
companies = [
|
||||
@ -260,15 +209,15 @@ def test_one_sided_uses_amounts_even_with_zero_balance_rows(
|
||||
IntercompanyMatrixCompanyOut(id=company_b, name="Компания Б"),
|
||||
]
|
||||
selection = matrix.MatrixSelection(frozenset({debit_account}), frozenset({credit_account}), "closing")
|
||||
debit = _row(company_a, company_b, debit_account, debit=debit_amount)
|
||||
credit = _row(company_b, company_a, credit_account, credit=credit_amount)
|
||||
debit = _row(company_a, company_b, debit_account)
|
||||
credit = _row(company_b, company_a, credit_account)
|
||||
debit.entry_ids = (uuid.uuid4(),)
|
||||
credit.entry_ids = (uuid.uuid4(),)
|
||||
|
||||
cell = matrix.calculate_matrix([debit, credit], companies, selection).cells[0]
|
||||
assert cell.one_sided is one_sided
|
||||
assert cell.exceeds_threshold is exceeds
|
||||
assert cell.model_dump(mode="json")["one_sided"] is one_sided
|
||||
for rows, expected in (([], False), ([debit], True), ([credit], True), ([debit, credit], False)):
|
||||
cell = matrix.calculate_matrix(rows, companies, selection).cells[0]
|
||||
assert cell.one_sided is expected
|
||||
assert cell.model_dump(mode="json")["one_sided"] is expected
|
||||
|
||||
|
||||
def test_unmapped_account_entries_are_counted_for_both_directions_of_pair() -> None:
|
||||
|
||||
@ -150,40 +150,6 @@ def test_matrix_export_has_three_rows_per_debit_company_and_highlights_threshold
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("amount", "highlighted"), [("100000000", True), ("0", False)])
|
||||
def test_xlsx_highlights_one_sided_amount_but_not_two_zero_balances(amount: str, highlighted: bool) -> None:
|
||||
company_a = UUID("11111111-1111-1111-1111-111111111111")
|
||||
company_b = UUID("22222222-2222-2222-2222-222222222222")
|
||||
matrix = IntercompanyMatrixOut(
|
||||
companies=[
|
||||
IntercompanyMatrixCompanyOut(id=company_a, name="Компания А"),
|
||||
IntercompanyMatrixCompanyOut(id=company_b, name="Компания Б"),
|
||||
],
|
||||
cells=[
|
||||
IntercompanyMatrixCellOut(
|
||||
company_a_id=company_a,
|
||||
company_b_id=company_b,
|
||||
amount_a=amount,
|
||||
amount_b="0",
|
||||
variance_amount=amount,
|
||||
variance_percent=None,
|
||||
exceeds_threshold=False,
|
||||
one_sided=highlighted,
|
||||
skipped_osv_entry_count=0,
|
||||
osv_entry_ids_debit=[],
|
||||
osv_entry_ids_credit=[],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
sheet = load_workbook(BytesIO(build_matrix_workbook(matrix, _info(mode="ALL")))).active
|
||||
assert sheet is not None
|
||||
assert sheet["B6"].value == "Все балансовые счета"
|
||||
assert all(
|
||||
(sheet.cell(row, 4).fill.fgColor.rgb == THRESHOLD_FILL.fgColor.rgb) is highlighted for row in range(11, 15)
|
||||
)
|
||||
|
||||
|
||||
async def test_export_routes_use_matrix_calculation() -> None:
|
||||
version_id = UUID("11111111-1111-1111-1111-111111111111")
|
||||
account_id = UUID("22222222-2222-2222-2222-222222222222")
|
||||
@ -236,7 +202,7 @@ async def test_export_routes_use_matrix_calculation() -> None:
|
||||
assert disposition.startswith("attachment; filename=\"intercompany-matrix.xlsx\"; filename*=UTF-8''")
|
||||
assert unquote(disposition.split("filename*=UTF-8''", 1)[1]) == expected_info.filename
|
||||
book = load_workbook(BytesIO(response.body))
|
||||
assert book.active["B6"].value in {"Все балансовые счета", "Выбранные счета", "Пресет"}
|
||||
assert book.active["B6"].value in {"Все счета", "Выбранные счета", "Пресет"}
|
||||
preset_sheet = load_workbook(BytesIO(preset_response.body)).active
|
||||
assert preset_sheet["B7"].value == "62.01"
|
||||
assert preset_sheet["B8"].value == "60.01"
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-22
|
||||
@ -1,31 +0,0 @@
|
||||
## Context
|
||||
|
||||
PR RSHB/arm-core#165, задача AURORA-1651 (технический долг ревью PR #114). Шахматка — чистая функция от сводной ОСВ
|
||||
версии (DECISION-5), контракт ответа не меняется: меняются смысл `one_sided`, состав счетов режима ALL и заливка xlsx.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. `one_sided = (amount_a == 0) != (amount_b == 0)`. Формула из описания задачи (`min(amount_a, amount_b) == 0 and
|
||||
variance_amount > 0`) на неотрицательных суммах даёт то же, но на отрицательном сальдо (возможно по §1) пропускает
|
||||
пару: −50 против 0 → `min = −50`. Знак ненулевой суммы не важен.
|
||||
2. Подсветка в API и в xlsx — `exceeds_threshold or one_sided`. Поле `exceeds_threshold` сохраняет смысл «процент > 5»;
|
||||
экран шахматки красит пару по тому же правилу (PR #158, change `intercompany-matrix-ui`).
|
||||
3. Забалансовые счета (`is_off_balance = true`) исключаются только в ALL; MANUAL выбирает их явно, это не ошибка.
|
||||
4. Фильтры `GET /directories/accounts` (`kind[]`, `row_type`, `is_off_balance`) из первой редакции PR убраны: в задаче их
|
||||
нет, потребителей нет, при фильтре по `kind` в ответе оставались `parent_id` на строки вне ответа.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
- 2026-10-22. Формула `one_sided` через «ровно одна сумма нулевая» вместо формулы из описания задачи. Обоснование —
|
||||
отрицательное сальдо (решение 1). Цена ошибки: если аналитик решит, что отрицательная сторона — не «пара без
|
||||
встречной стороны», меняется одна строка и тест.
|
||||
- 2026-10-22. Влив `main` с #141 и разрешение конфликтов выполнил ревьюер (владелец: «доведём задачу до ума, потом
|
||||
сольём»). В ALL сохранены и фильтр забалансовых, и `MatrixSelection(..., "closing")` из #141; тесты #141 на старый
|
||||
смысл `one_sided` переписаны. Цена ошибки: низкая, расчёт PRESET не затронут (свои счета правила).
|
||||
|
||||
## Решения владельца (2026-10-22)
|
||||
|
||||
- Экран красит и по `one_sided` (а не бэкенд выставляет `exceeds_threshold`).
|
||||
- Фильтр `/directories/accounts` убрать из PR.
|
||||
- Счёт `000` «Вспомогательный» не трогать, вопрос — в `07-open-questions.md`.
|
||||
- Процент при отрицательной меньшей стороне — вопрос в `07-open-questions.md`, в этом PR не меняется.
|
||||
@ -1,34 +0,0 @@
|
||||
## Why
|
||||
|
||||
Технический долг повторного ревью PR #114 (AURORA-1537), задача AURORA-1651. Признак `one_sided` считался по наличию
|
||||
строк ОСВ, а не по суммам: пара «100 млн против строки с нулевым сальдо» выглядела сошедшейся (`one_sided = false`,
|
||||
`exceeds_threshold = false`), а «0.00 против 0.00» с одной строкой — подсвечивалась ложно. Книга xlsx заливала пару только
|
||||
по порогу. Режим «все счета» брал забалансовые счета 001–012: например, 009 «Обеспечения выданные» на компанию группы
|
||||
давал ложное расхождение (решение владельца 16.10.2026).
|
||||
|
||||
Реализация — PR RSHB/arm-core#165 (ветка `AURORA-1651`, автор — Потапова), доведена ревьюером по решению владельца
|
||||
22.10.2026.
|
||||
|
||||
## What Changes
|
||||
|
||||
- `one_sided` = ровно одна из сумм пары нулевая (знак не важен: сальдо в ОСВ бывает отрицательным).
|
||||
- XLSX шахматки заливает пару, если `exceeds_threshold` или `one_sided`; в шапке режима ALL — «Все балансовые счета».
|
||||
- Режим ALL — только балансовые счета (`is_off_balance = false`); MANUAL принимает любые, включая забалансовые.
|
||||
- Правила — `../docs/context/05-domain-rules.md` §6 (docs-PR вместе с этим change), открытые вопросы — OQ про счёт `000`
|
||||
и процент при отрицательной меньшей стороне.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Подсветка односторонней пары на экране шахматки — фронт, PR #158 (change `intercompany-matrix-ui`).
|
||||
- Фильтры `GET /directories/accounts` (были в первой редакции PR, убраны решением владельца 22.10.2026).
|
||||
- Процент расхождения при отрицательной меньшей стороне и исключение счёта `000` из ALL — открытые вопросы.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
- `consolidated-osv`: требования «Шахматка по сводной ОСВ версии» и «XLSX шахматки».
|
||||
|
||||
## Impact
|
||||
|
||||
- `api/src/services/intercompany_matrix.py`, `api/src/services/intercompany_matrix_export.py`; тесты шахматки.
|
||||
- Контракт API не меняется (поля те же), меняется смысл `one_sided` и состав счетов режима ALL.
|
||||
@ -1,72 +0,0 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Шахматка по сводной ОСВ версии
|
||||
`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/all` и
|
||||
`POST /api/v1/consolidations/versions/{version_id}/intercompany-matrix/manual`
|
||||
(`CONSOLIDATION_VIEW`, policy) SHALL рассчитывать матрицу без сохранения в БД, из строк сводной ОСВ версии
|
||||
и её периметра. ALL включает балансовые счета (`is_off_balance = false`) вида `A`/`AP` для дебета и `P`/`AP` для
|
||||
кредита; забалансовые счета (001–012) в ALL MUST NOT входить. MANUAL принимает обязательные непустые
|
||||
`debit_account_ids: UUID[]` и `credit_account_ids: UUID[]`, раскрывает выбранные счета по иерархии и оставляет
|
||||
подходящий вид; забалансовые счета в MANUAL допускаются. Неизвестный счёт SHALL давать 422 `ACCOUNT_NOT_FOUND`, неверный
|
||||
вид — 422 `ACCOUNT_KIND_MISMATCH`; пустой массив — 422 валидации тела. Невидимая и несуществующая версия SHALL
|
||||
отвечать одинаковым 404 `NOT_FOUND`.
|
||||
|
||||
Ответ SHALL содержать `companies[] {id, name}` для всего периметра и `cells[]` для каждой упорядоченной пары
|
||||
разных компаний: `company_a_id`, `company_b_id`, `amount_a`, `amount_b`, `variance_amount`, `variance_percent`,
|
||||
`exceeds_threshold`, `one_sided`, `skipped_osv_entry_count`, `osv_entry_ids_debit`, `osv_entry_ids_credit`.
|
||||
Для пары `(A, B)` `amount_a` — сумма `closing_debit` строк A с контрагентом B по дебетовым счетам,
|
||||
`amount_b` — сумма `closing_credit` строк B с контрагентом A по кредитовым. Расхождение — модуль разности;
|
||||
процент — расхождение / меньшую сумму × 100 или null при нуле. `exceeds_threshold` SHALL быть true при точном
|
||||
проценте > 5, до округления; суммы и процент в JSON SHALL быть десятичными строками с двумя знаками после
|
||||
запятой (`ROUND_HALF_UP`). `one_sided` (пара без встречной стороны) SHALL быть true тогда и только тогда, когда
|
||||
ровно одна из сумм `amount_a`, `amount_b` равна нулю (знак ненулевой суммы не важен); наличие строк ОСВ с нулевым
|
||||
сальдо на признак не влияет. Пара подсвечивается, если `exceeds_threshold` или `one_sided`. `skipped_osv_entry_count`
|
||||
SHALL считать исходные строки ОСВ пары с `account_id = null` на обеих сторонах; они не входят в суммы. Массивы
|
||||
`osv_entry_ids_debit/credit` SHALL содержать ID исходных записей соответствующей стороны.
|
||||
|
||||
#### Scenario: Пустой периметр и отсутствие ОСВ
|
||||
- **WHEN** у версии нет членов периметра
|
||||
- **THEN** `companies = []`, `cells = []`; если компании есть, но ОСВ по выбранным счетам нет, ячейки присутствуют с нулевыми суммами
|
||||
|
||||
#### Scenario: Только одна сторона пары
|
||||
- **WHEN** у A есть строка ОСВ с контрагентом B и ненулевым сальдо, а у B встречной строки нет
|
||||
- **THEN** для `(A, B)` `one_sided = true`, `variance_percent = null`, соответствующий массив `osv_entry_ids_*` пуст
|
||||
|
||||
#### Scenario: Встречная строка с нулевым сальдо
|
||||
- **WHEN** у A по B сальдо 100 000 000, а у B по A есть строка ОСВ с нулевым сальдо (одни обороты)
|
||||
- **THEN** `one_sided = true`, `exceeds_threshold = false`, пара подсвечивается
|
||||
|
||||
#### Scenario: Обе стороны нулевые
|
||||
- **WHEN** у A и у B есть строки ОСВ друг на друга, но обе суммы равны 0
|
||||
- **THEN** `one_sided = false`, `exceeds_threshold = false`, пара не подсвечивается
|
||||
|
||||
#### Scenario: Отрицательное сальдо
|
||||
- **WHEN** у A по B сальдо −50, у B по A — 0
|
||||
- **THEN** `one_sided = true`
|
||||
|
||||
#### Scenario: Забалансовые счета
|
||||
- **WHEN** у A по B есть сальдо на счёте 001 (`is_off_balance = true`)
|
||||
- **THEN** в режиме ALL оно в суммы не входит; в MANUAL с выбранным счётом 001 — входит
|
||||
|
||||
#### Scenario: Счёт не сопоставлен
|
||||
- **WHEN** у пары есть строка свода с `account_id = null` и двумя `entry_ids`
|
||||
- **THEN** она не входит в суммы, `skipped_osv_entry_count` обеих направленных ячеек пары увеличивается на 2
|
||||
|
||||
#### Scenario: Эталон «Русский бисквит»
|
||||
- **WHEN** матрица строится по ОСВ РБ и Пекарни Счастливочной из эталонных фикстур
|
||||
- **THEN** суммы сравниваются с эталоном только для точных пар счетов и компаний; общая сумма элиминации эталона не приравнивается к минимуму по паре в API (решение владельца 08.10.2026)
|
||||
|
||||
### Requirement: XLSX шахматки
|
||||
`GET …/intercompany-matrix/all/export` и `POST …/intercompany-matrix/manual/export` SHALL использовать
|
||||
те же режимы, тело и права, что и соответствующие JSON-маршруты. Ответ — XLSX с именем
|
||||
`intercompany-matrix_<ГК>_<год>-<квартал>_v<n>.xlsx`, шапкой (ГК, период, версия, единицы, режим,
|
||||
выбранные счета; для ALL — «Все балансовые счета») и строками разницы, дебета, кредита и процента расхождения для
|
||||
каждой пары. Пара SHALL заливаться цветом порога, если `exceeds_threshold` или `one_sided` — так же, как в API.
|
||||
|
||||
#### Scenario: Экспорт выбранных счетов
|
||||
- **WHEN** пользователь выгружает MANUAL для ГК «Русский бисквит» за 2024 Q1, версия 1
|
||||
- **THEN** имя содержит `Русский бисквит_2024-Q1_v1.xlsx`, шапка содержит выбранные счета, ячейки процента показывают два знака и символ `%` без повторного масштабирования
|
||||
|
||||
#### Scenario: Подсветка пары без встречной стороны
|
||||
- **WHEN** выгружается ALL, у пары `amount_a = 100000000`, `amount_b = 0`
|
||||
- **THEN** все строки пары залиты цветом порога, в шапке «Режим» и «Счета дебета/кредита» — «Все балансовые счета»
|
||||
@ -1,8 +0,0 @@
|
||||
## 1. Реализация (PR RSHB/arm-core#165, AURORA-1651)
|
||||
|
||||
- [x] 1.1 Автор: `one_sided` по суммам, заливка xlsx по `exceeds_threshold or one_sided`, ALL без забалансовых счетов.
|
||||
- [x] 1.2 Ревьюер: влив `main` (#141), конфликты; `one_sided` на отрицательных суммах; шапка ALL «Все балансовые счета»;
|
||||
фильтр `/directories/accounts` убран; тесты (суммы, нули, отрицательные, xlsx, ALL/MANUAL с 001).
|
||||
- [x] 1.3 Ревьюер: правила §6 и OQ в `../docs` (docs-PR вместе с этим change), гайд фронта §5.7.1.
|
||||
- [x] 1.4 Ревьюер: подсветка `one_sided` на экране — PR #158, change `intercompany-matrix-ui`.
|
||||
- [x] 1.5 После слияния: архив change.
|
||||
@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-23
|
||||
@ -1,117 +0,0 @@
|
||||
## Context
|
||||
|
||||
PR RSHB/arm-core#166 (ветка `AURORA-1589`, автор — Степанов, задача AURORA-1589) доводит до макета экраны консолидаций:
|
||||
список консолидаций ГК [34] (`widgets/ConsoleTable/`), модалку «Взять в работу» и перехват [35]
|
||||
(`widgets/ConsoleTable/LockModal.tsx`), панель «Организации» (`widgets/ConsoleLegalEntities/`), модалку «Новая
|
||||
консолидация» [42] (`widgets/NewConsoleModal/`, строка ЮЛ — `CompanyOptionRow.tsx`), кнопку «Изменить статус» в шапке
|
||||
карточки версии [30] (`layouts/ConsolidatedOsvPage.tsx`) и модалку [33-01] (`widgets/StatusChangeModal/ChangeStatusModal.tsx`).
|
||||
Бэкенд — спека `consolidations-api` (список с `in_work_only`, «Взять в работу, перехват, снятие», «Статусы Среза 4»); API не
|
||||
меняется. В ветку добавлена вторая библиотека иконок `lucide-react` (иконки макета: меню «⋯», «Взять в работу», «К
|
||||
отчётности», боковое меню).
|
||||
|
||||
Спеку, документацию и тесты ведёт ревьюер коммитами в ветку автора (правило владельца для чужих PR); продуктовый код
|
||||
правит автор. Спека описывает **целевое** поведение; расхождения кода ветки (коммит 71ef633) — раздел «Known issues».
|
||||
Источники: макет [34], [35], [42], [30], [33-01], [33], [31-00] (`docs/mockup/INDEX.md`), описание AURORA-1589, ТЗ
|
||||
«Статусная модель» (`docs/source/tz/…08_4. Статусная модель.md`), гайд фронта по API §5.7.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **D-1. Ручные переходы — зеркало `MANUAL_TRANSITIONS` во фронте.** API не отдаёт допустимые переходы (в `VersionCard`
|
||||
нет `allowed_transitions`, заметка AURORA-1589 от 12.10.2026). Фронт держит одну константу «статус → допустимые ручные
|
||||
переходы», совпадающую с `api/src/services/consolidation_status.py`: `INITIATED → {CANCELLED}`,
|
||||
`DOCUMENTS_PROCESSING → {CANCELLED, INITIATED}`, `DATA_CORRECTION → {CANCELLED, INITIATED}`. С маршрутом согласования
|
||||
(Срез 7) константа заменяется ответом API без переделки модалки. Расхождение константы с бэком сервер ловит 409
|
||||
`TRANSITION_NOT_ALLOWED`; заметка о зеркале — в гайде фронта §5.7 (PR в RSHB/arm-ka-docs).
|
||||
- **D-2. Комментарий обязателен для обоих ручных переходов** (AURORA-1589): для `CANCELLED` его требует и сервер (422
|
||||
`COMMENT_REQUIRED`), для возврата в `INITIATED` — только фронт.
|
||||
- **D-3. «Изменить статус» доступна по `lock` карточки и `/me`**: активна, если `lock.locked_by` = текущий пользователь и в
|
||||
D-1 есть переходы из текущего статуса. Подсказка «Возьмите консолидацию в работу» (текст задачи AURORA-1589).
|
||||
- **D-4. После смены статуса экран берёт карточку из ответа** `POST …/status` (`VersionCard`), без повторного
|
||||
`GET`: бейдж, пояснение и кнопка обновляются сразу; после `CANCELLED` сервер снимает блокировку, кнопка становится
|
||||
неактивной.
|
||||
- **D-5. По умолчанию в «Новом этапе» ничего не выбрано** (ревью раунда 1): в макете [33-01] предложен следующий этап, но
|
||||
сейчас один из двух вариантов — необратимая «Отменена», и случайная отправка по умолчанию недопустима.
|
||||
- **D-6. «Только в работе» — серверный фильтр `in_work_only`**, по умолчанию выключен (макет [34], значение API по
|
||||
умолчанию `false`). Фронт не фильтрует по статусу сам: множество «в работе» для списка определяет сервер.
|
||||
- **D-7. Модалка [35] читает свежую карточку версии** перед показом: данные `lock` строки списка могут устареть. Перехват —
|
||||
в той же модалке: при известной чужой блокировке сразу поле причины, при 409 `LOCKED_BY_OTHER` (гонка) — предупреждение
|
||||
и поле причины.
|
||||
- **D-8. ГСК в API нет (решение владельца П-3)**: колонка «ГСК» — «—», в тексте модалки [35] вместо ГСК макета — ГК
|
||||
(`company_group.name` карточки).
|
||||
- **D-9. Подпись `CANCELLED` — «Отменена»** (AURORA-1589, ревью): в ТЗ «Статусная модель» — «Отменен», в словаре UI
|
||||
`10-user-journey.md` — «Отмена КФО»; выбран вариант задачи (согласование с «консолидацией»). «Корректировка данных» —
|
||||
как в ТЗ (было «Коррекция данных»).
|
||||
- **D-10. Кнопку «Взять в работу» блокирует код статуса `CANCELLED`, а не подпись** — подпись меняется (D-9), условие по
|
||||
подписи ломается молча.
|
||||
|
||||
## Known issues
|
||||
|
||||
Расхождения кода ветки `AURORA-1589` (71ef633) с целевой спекой — по пунктам ревью раунда 1; исправляет автор PR.
|
||||
|
||||
1. **404 при смене статуса.** `services/api/consolidations.ts` `updateStatus` шлёт `POST /consolidation/versions/{id}/status`
|
||||
(нет «s»); нужен `/consolidations/versions/{id}/status`.
|
||||
2. **Переходы — заглушка `mockStatusMappingFetch()` внутри модалки** (`ChangeStatusModal.tsx`). Нужна одна константа (D-1)
|
||||
рядом с подписями статусов. Для статусов без переходов сейчас `nextStatusOptions = undefined`: форма скрыта, кнопка
|
||||
«Изменить статус» в шапке при этом активна.
|
||||
3. **«Новый этап» предвыбран** (`initialValues.to_status = nextStatusOptions[0]`, на стенде — «Отменён»); нужно пусто (D-5).
|
||||
4. **Кнопка «Изменить статус»**: подсказка «Сначала возьмите версию в работу» вместо «Возьмите консолидацию в работу»;
|
||||
отсутствие ручных переходов не учитывается (при своей блокировке `VERIFIED` кнопка активна, модалка пустая).
|
||||
5. **Колонка «Версия» инвертирована** (`ConsoleTable/config.tsx`): показывает «1» и скрывает 2+; нужно наоборот.
|
||||
6. **«Только в работе» включён по умолчанию** (`ConsoleTable.tsx`: `searchParams.get('in_work_only') || true`, флажок
|
||||
отмечен, пока параметр не `'false'`); нужно выключен (D-6).
|
||||
7. **Пункты «⋯» не `disabled`**: у них только класс `ant-dropdown-menu-item-disabled`, клик обрабатывается
|
||||
(«История изменений» ведёт в пустой «Журнал действий», остальные — `message.info('Будет позже')`); в `ITEMS` лишний
|
||||
пустой элемент `{}`.
|
||||
8. **Подпись `CANCELLED` — «Отменён»** (нужно «Отменена», D-9); «Взять в работу» блокируется сравнением
|
||||
`row.status === 'Отменён'` — после переименования условие перестанет срабатывать (D-10).
|
||||
9. **Перехват [35]**: плейсхолдер поля причины «Type message»; проверка длины `< 1000` вместо `≤ 1000`; квартал в тексте —
|
||||
`Q3` вместо «К3»; опечатки «обнаржуена», «нформацию».
|
||||
10. **Пояснение под заголовком**: для `INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION` повторяется название статуса;
|
||||
тексты `VERIFIED` и `CONSOLIDATION_RF` в `consStatusDescriptionMapping` расходятся с макетом [30], [33-01].
|
||||
11. **Модалка [31-00] «Остались неуказанные ИНН»** не реализована. Другие вкладки отчёта в ветке пока неактивны
|
||||
(`OsvTabs`), поэтому сценарий проверяется после появления вкладки «Шахматка» (PR #158) или в нём же.
|
||||
12. **«+ Создать» активна при одном отмеченном ЮЛ** (`NewConsoleModal.tsx`, `!legEntIds?.length`); нужно не меньше двух
|
||||
(сервер отвечает 422 `PERIMETER_TOO_SMALL`).
|
||||
13. **Модалка «Изменить статус», замечено при написании спеки** (в комментарий ревью раунда 1 не вошло): ошибка
|
||||
`getVersionCard` не обрабатывается — модалка остаётся в загрузке; `comment.trim()` вызывается на значении
|
||||
`Form.useWatch`, которое до монтирования формы `undefined`.
|
||||
14. **Тестов нет.** Чистые правила (подпись версии, переходы, активность кнопок, параметр `in_work_only`) зашиты в
|
||||
компоненты; тесты vitest — после их выноса в чистые функции (`tasks.md`).
|
||||
15. **`lucide-react@0.263.1` в зеркале npm банка не проверен.** Пакет в `dependencies`, снимок контура его оставляет
|
||||
(`docs/dev/contour-transfer-guide.md` §3.2); если версии нет в Nexus, сборка в контуре упадёт.
|
||||
16. **Предупреждение браузера о несохранённых правках** (пункт AURORA-1589) не реализовано; в спеку не входит (решение (е)).
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
- **2026-10-23 (а). Новая capability `consolidations-ui`; `consolidation-setup` и `consolidated-osv-ui` не правятся.** Вид
|
||||
модалки [42], шапка карточки версии и модалка [31-00] описаны в новой спеке; состав модалки [42], создание и «К
|
||||
отчётности» остаются в `consolidation-setup`, сводная ОСВ — в `consolidated-osv-ui`. Обоснование: не смешивать дельты
|
||||
трёх спек в одном PR фронта. Цена ошибки: при архивации модалку [42] и страницу отчёта описывают по две спеки; сведение —
|
||||
MODIFIED-дельтой позже.
|
||||
- **2026-10-23 (б). Временные пояснения статусов первой очереди и `CANCELLED` — дословные фрагменты ТЗ «Статусная
|
||||
модель»** (колонка «Описание»), пока аналитик не дал тексты (AURORA-1589: «тексты для статусов первой очереди — у
|
||||
аналитика»); для `VERIFIED` и `CONSOLIDATION_RF` — тексты макета вместо текстов автора. Обоснование: подзаголовок не
|
||||
должен повторять название статуса, а придумывать формулировки нельзя. Цена ошибки: замена строк в одном месте.
|
||||
- **2026-10-23 (в). Порядок версий в дереве — как в ответе API** (сверху последняя версия периода, под ней предыдущие), как
|
||||
в `main`. В макете [34] версия 1 стоит сверху, версия 2 — под ней. Цена ошибки: правка `flatten.ts` / `groupAdjacent.ts`
|
||||
и MODIFIED требования «Список консолидаций ГК».
|
||||
- **2026-10-23 (г). Минимум два ЮЛ для «+ Создать»** — из описания [42] в `docs/mockup/INDEX.md` («минимум 2») и 422
|
||||
`PERIMETER_TOO_SMALL`; фронт блокирует раньше сервера. Цена ошибки: одно условие.
|
||||
- **2026-10-23 (д). Модалка [31-00] — только при переходе на другую вкладку отчёта**, не при «Назад» и переходах по меню.
|
||||
Открытые вопросы владельцу: (1) нужно ли предупреждать и при уходе со страницы отчёта; (2) AURORA-1589: «на реальных
|
||||
данных в счётчик попадают строки, которые не являются организациями… до нуля он не доводится» — поведение согласовать с
|
||||
задачей «Блокировка шага до сопоставления контрагентов без ИНН»; до решения модалка не блокирует переход. Цена ошибки:
|
||||
расширение триггера — MODIFIED требования.
|
||||
- **2026-10-23 (е). Предупреждение браузера о несохранённых правках не включено в спеку**: пункт задачи касается всех форм
|
||||
и модалок приложения, а не экранов консолидаций; остаётся задачей автора в `tasks.md`. Цена ошибки: требование появится
|
||||
отдельной спекой (или дельтой) позже.
|
||||
- **2026-10-23 (ж). Подпись `DOCUMENTS_PROCESSING` оставлена «Обработка документов»** (как в `main`); в ТЗ — «Загрузка,
|
||||
распознавание и корректировка документов», в словаре UI — «Загрузка и распознавание документов». Вопрос аналитику.
|
||||
Цена ошибки: замена одной подписи.
|
||||
- **2026-10-23 (з). Тексты сообщений об ошибках 409/422 в модалке «Изменить статус» не фиксируются дословно**: спека
|
||||
требует сообщение и открытую модалку. Цена ошибки: низкая.
|
||||
- **2026-10-23 (и). В ветку влит `origin/main`** (шахматка «пара без встречной стороны» PR #165 и архивы OpenSpec
|
||||
PR #160, #167, #168) — без конфликтов; файлы `web/` ветки слиянием не затронуты. Цена ошибки: низкая, PR и так
|
||||
сливается в этот `main`.
|
||||
- **2026-10-23 (к). «Единицы» и «Знаков после запятой» сводной ОСВ (коммит 71ef633, AURORA-1539) в спеку не включены** —
|
||||
автору предложено вынести их в отдельный PR. Цена ошибки: если коммит останется в PR, его поведение будет без спеки.
|
||||
@ -1,62 +0,0 @@
|
||||
## Why
|
||||
|
||||
В ветке `AURORA-1589` (PR RSHB/arm-core#166, автор — Степанов, задача AURORA-1589) фронт доводит экраны консолидаций до
|
||||
макета: список консолидаций ГК [34] с «Взять в работу» и перехватом [35], модалку «Новая консолидация» [42] и кнопку
|
||||
«Изменить статус» в карточке версии [30] с модалкой [33-01]. Бэкенд этих действий уже в main (`consolidations-api`:
|
||||
список с `in_work_only`, блокировка и перехват, ручная смена статуса). У экранов нет UI-спеки. Спеку, документацию и тесты
|
||||
ведёт ревьюер коммитами в ветку автора (правило владельца): спека задаёт целевое поведение, расхождения кода ветки с ней —
|
||||
`design.md`, «Known issues»; продуктовый код правит автор.
|
||||
|
||||
## What Changes
|
||||
|
||||
Репозиторий — только arm-core (`web/`). Эндпоинты arm-core (без изменений): `GET /company-groups/{id}/consolidations`
|
||||
(`in_work_only`), `GET /consolidations/versions/{id}`, `POST /consolidations/versions/{id}/lock` (`takeover_reason`),
|
||||
`POST /consolidations/versions/{id}/status`, `POST /consolidations`.
|
||||
|
||||
- Список консолидаций [34]: колонки, дерево версий с номером только у версий 2+, фильтр «Только в работе» (выключен по
|
||||
умолчанию), «Взять в работу» с подтверждением [35] и перехватом с причиной, неактивные пункты меню «⋯».
|
||||
- Панель «Организации» без выбранной консолидации и серый статус «Отсутствует».
|
||||
- Подписи статусов версии («Корректировка данных», «Отменена») и пояснение статуса под заголовком карточки.
|
||||
- Модалка «Новая консолидация» [42]: ИНН отдельной колонкой, «+ Создать», без звёздочек обязательности.
|
||||
- «Изменить статус» [33-01]: допустимые переходы — зеркало `MANUAL_TRANSITIONS` бэка, по умолчанию ничего не выбрано,
|
||||
кнопка доступна только владельцу блокировки и при наличии ручных переходов.
|
||||
- Модалка [31-00] «Остались неуказанные ИНН» при уходе со «Сводной ОСВ» на другую вкладку отчёта.
|
||||
- Новая спека `consolidations-ui` (ADDED Requirements).
|
||||
- Документация фронта: `web/CLAUDE.md` (вторая библиотека иконок `lucide-react`), `web/ARCHITECTURE.md` (новые виджеты).
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Исправление продуктового кода по «Known issues» (`design.md`) — за автором PR.
|
||||
- «Единицы» и «Знаков после запятой» в сводной ОСВ (коммит 71ef633, AURORA-1539) — автору предложено вынести в отдельный
|
||||
PR; в эту спеку не входят.
|
||||
- Полный маршрут согласования (переходы Среза 7), «Добавить версию», «Отключить подтверждение КФО», «История изменений»,
|
||||
«Редактировать состав», «Импорт», «Дата верификации», «Утверждён в РФ» / «Верифицирован в ГО» — зонтичная задача
|
||||
AURORA-1416.
|
||||
- ГСК консолидации в API списка и карточки (колонка «ГСК») — отдельная задача ядра; до неё колонка пустая.
|
||||
- Список допустимых переходов из API (`allowed_transitions`) — с маршрутом согласования; до него фронт держит зеркало.
|
||||
- Предупреждение браузера о несохранённых правках (пункт задачи AURORA-1589) — касается всех форм приложения, в эту спеку
|
||||
не входит (`design.md`, решение без обсуждения (е)).
|
||||
- Изменения бэкенда и DBML.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `consolidations-ui`: список консолидаций ГК, взятие в работу и перехват, модалка новой консолидации (вид), смена статуса
|
||||
версии и шапка карточки версии.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
Нет: `consolidation-setup` (состав модалки [42], создание, «К отчётности») и `consolidated-osv-ui` (сводная ОСВ) не
|
||||
правятся — добавленное поведение описано в новой спеке (`design.md`, решение без обсуждения (а)).
|
||||
|
||||
## Impact
|
||||
|
||||
- `web/src/widgets/ConsoleTable/` (`ConsoleTable.tsx`, `LockModal.tsx`, `config.tsx`, `consts.ts`),
|
||||
`web/src/widgets/StatusChangeModal/ChangeStatusModal.tsx`, `web/src/widgets/NewConsoleModal/`
|
||||
(`NewConsoleModal.tsx`, `CompanyOptionRow.tsx`), `web/src/widgets/ConsoleLegalEntities/`,
|
||||
`web/src/layouts/ConsolidatedOsvPage.tsx`, `web/src/layouts/ConsolidationsList.tsx`,
|
||||
`web/src/services/api/consolidations.ts`, `web/src/store/slices/consolidations/thunks.ts`, `web/src/types/`,
|
||||
`web/src/widgets/Sidebar/` (иконки), `web/package.json` (`lucide-react`).
|
||||
- Права: действия требуют `CONSOLIDATION_EDIT` на сервере; меню по правам не скрывается.
|
||||
- Гайд фронта по API (`docs/dev/frontend-api-guide.md` §5.7): заметка о зеркале ручных переходов — PR в RSHB/arm-ka-docs,
|
||||
сливать вместе с этим PR.
|
||||
@ -1,226 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Список консолидаций ГК
|
||||
Экран консолидаций ГК [34] SHALL показывать версии из `GET /company-groups/{id}/consolidations` таблицей с колонками
|
||||
Год, Квартал (К1–К4), Версия, Статус (бейдж, подписи — «Подписи статусов версии»), ГСК, Дата создания, Дата верификации,
|
||||
Организаций (`members_count`) и колонкой действий: «Взять в работу» (или «В работе»), «К отчётности», меню «⋯». Версии
|
||||
одного периода SHALL показываться деревом: верхняя строка — первая версия периода из ответа API (по убыванию
|
||||
`version_no`), остальные — раскрываемыми строками под ней. Колонка «ГСК» SHALL иметь фильтр в заголовке, «Дата создания» —
|
||||
без фильтра. Пока API не отдаёт ГСК консолидации и дату верификации, колонки «ГСК» и «Дата верификации» SHALL показывать
|
||||
«—». Рядом с заголовком «Консолидации» — число строк («N шт.»). Клик по строке SHALL выбирать версию для панели
|
||||
«Организации» и подсвечивать строку.
|
||||
|
||||
#### Scenario: Колонки списка
|
||||
- **WHEN** открыт список консолидаций ГК «Русский бисквит»
|
||||
- **THEN** у таблицы колонки Год, Квартал, Версия, Статус, ГСК, Дата создания, Дата верификации, Организаций и колонка
|
||||
действий; у «ГСК» есть фильтр, у «Дата создания» — нет; в «ГСК» и «Дата верификации» — «—»
|
||||
|
||||
#### Scenario: Выбор версии
|
||||
- **WHEN** аналитик кликает по строке версии 2024 К3
|
||||
- **THEN** строка подсвечена, панель «Организации» показывает состав этой версии
|
||||
|
||||
### Requirement: Колонка «Версия»
|
||||
Колонка «Версия» SHALL быть пустой у версии с `version_no = 1` и показывать номер у версий с `version_no ≥ 2`.
|
||||
|
||||
#### Scenario: Единственная версия периода
|
||||
- **WHEN** у консолидации 2025 К1 одна версия (`version_no = 1`)
|
||||
- **THEN** ячейка «Версия» пустая
|
||||
|
||||
#### Scenario: Вторая версия периода
|
||||
- **WHEN** у консолидации 2024 К1 версии 2 и 1
|
||||
- **THEN** у строки версии 2 в колонке «Версия» — «2», у строки версии 1 — пусто
|
||||
|
||||
### Requirement: Фильтр «Только в работе»
|
||||
Над таблицей SHALL быть флажок «Только в работе», по умолчанию **выключенный**. Включённый флажок SHALL перечитывать список
|
||||
запросом `GET /company-groups/{id}/consolidations?in_work_only=true` (отбор делает сервер: остаются версии со статусом не
|
||||
из `VERIFIED`, `CANCELLED`, `ARCHIVED`, `REJECTED`); выключенный — запросом с `in_work_only=false` (или без параметра).
|
||||
Фронт MUST NOT фильтровать версии по статусу сам.
|
||||
|
||||
#### Scenario: Открытие экрана
|
||||
- **WHEN** аналитик впервые открывает список консолидаций ГК
|
||||
- **THEN** флажок «Только в работе» снят, запрос — с `in_work_only=false`, в таблице есть верифицированные и отменённые
|
||||
версии
|
||||
|
||||
#### Scenario: Включение фильтра
|
||||
- **WHEN** аналитик отмечает «Только в работе»
|
||||
- **THEN** список перечитан с `in_work_only=true`, отменённой версии в таблице нет
|
||||
|
||||
### Requirement: Взять в работу
|
||||
Кнопка «Взять в работу» в строке SHALL открывать модалку «Взять консолидацию в работу?» [35]. Состояние блокировки
|
||||
модалка SHALL брать из свежей карточки `GET /consolidations/versions/{id}` (`lock`):
|
||||
- блокировки нет — текст «Консолидация <год> К<n> · <ГК> будет закреплена за вами для редактирования.», кнопки
|
||||
«Отмена» / «Взять в работу»; «Взять в работу» отправляет `POST /consolidations/versions/{id}/lock` без
|
||||
`takeover_reason`;
|
||||
- версию держит другой пользователь — текст «Консолидация взята в работу <ФИО>. Укажите причину замены.» и поле причины
|
||||
(обязательно, ≤ 1000 символов, строка из пробелов считается пустой); «Взять в работу» неактивна, пока причина пуста, и
|
||||
отправляет `POST …/lock {takeover_reason}` (перехват).
|
||||
|
||||
После 200/201 модалка SHALL закрыться с сообщением «Консолидация взята в работу.» и перечитать список. Ответ 409
|
||||
`LOCKED_BY_OTHER` на запрос без причины (версию успели взять) SHALL переводить модалку в режим перехвата с предупреждением
|
||||
«<ФИО> только что взял(-а) консолидацию в работу.» и пустым полем причины. Ответ 409 `VERSION_NOT_EDITABLE` SHALL
|
||||
закрывать модалку с сообщением «Нельзя взять в работу отменённую версию.». У версии со статусом `CANCELLED` кнопка
|
||||
«Взять в работу» SHALL быть неактивной (условие — по коду статуса, а не по подписи); у версии, которую держит текущий
|
||||
пользователь, вместо неё SHALL показываться неактивная кнопка «В работе».
|
||||
|
||||
#### Scenario: Свободная версия
|
||||
- **WHEN** аналитик нажимает «Взять в работу» у версии без блокировки и подтверждает
|
||||
- **THEN** отправлен `POST …/lock` без тела причины, модалка закрыта, в строке — неактивная «В работе»
|
||||
|
||||
#### Scenario: Перехват с причиной
|
||||
- **WHEN** версию держит «Соколов А. В.», аналитик нажимает «Взять в работу»
|
||||
- **THEN** модалка показывает «Консолидация взята в работу Соколов А. В. Укажите причину замены.» и поле причины;
|
||||
«Взять в работу» неактивна, пока причина пуста; после ввода «Отпуск» отправлен `POST …/lock {takeover_reason: "Отпуск"}`
|
||||
|
||||
#### Scenario: Версию взяли, пока модалка открыта
|
||||
- **WHEN** в модалке свободной версии аналитик нажимает «Взять в работу», а сервер отвечает 409 `LOCKED_BY_OTHER`
|
||||
- **THEN** модалка остаётся открытой в режиме перехвата с предупреждением «<ФИО> только что взял(-а) консолидацию в
|
||||
работу.» и пустым полем причины
|
||||
|
||||
#### Scenario: Отменённая версия
|
||||
- **WHEN** версия в статусе «Отменена»
|
||||
- **THEN** «Взять в работу» в её строке неактивна
|
||||
|
||||
### Requirement: Меню «⋯» строки консолидации
|
||||
Меню «⋯» строки SHALL содержать пункты «Добавить версию», «Отключить подтверждение КФО», «История изменений» с иконками
|
||||
[43]. Пока функции нет, пункты SHALL быть неактивными (`disabled`) с подсказкой «Будет позже»; нажатие на неактивный пункт
|
||||
MUST NOT выполнять действий (ни перехода, ни всплывающего сообщения).
|
||||
|
||||
#### Scenario: Неактивный пункт
|
||||
- **WHEN** аналитик открывает «⋯» и нажимает «История изменений»
|
||||
- **THEN** пункт неактивен, видна подсказка «Будет позже», переход в «Журнал действий» не происходит
|
||||
|
||||
### Requirement: Панель «Организации»
|
||||
Пока в списке не выбрана версия, панель «Организации» SHALL показывать заголовок «Организации» с подписью «Выберите
|
||||
консолидацию» и текст «Выберите консолидацию выше» вместо таблицы. В колонках «ОФР» и «Бух. баланс» статус «Отсутствует»
|
||||
SHALL быть нейтральным серым бейджем (как в легенде чек-листа [37]); красный — только у «Ошибка».
|
||||
|
||||
#### Scenario: Версия не выбрана
|
||||
- **WHEN** открыт список консолидаций, строка не выбрана
|
||||
- **THEN** панель «Организации» без таблицы и без «0 шт.», в ней текст «Выберите консолидацию выше»
|
||||
|
||||
#### Scenario: Документ отсутствует
|
||||
- **WHEN** у ЮЛ выбранной версии нет ББ
|
||||
- **THEN** в колонке «Бух. баланс» — серый бейдж «Отсутствует»
|
||||
|
||||
### Requirement: Подписи статусов версии
|
||||
Фронт SHALL показывать статусы версии одними подписями в списке, фильтре колонки «Статус», шапке карточки и модалке
|
||||
«Изменить статус»: `INITIATED` — «Инициация», `DOCUMENTS_PROCESSING` — «Обработка документов», `DATA_CORRECTION` —
|
||||
«Корректировка данных», `CONSOLIDATION_RF` — «Консолидация РФ», `VERIFICATION_RF` — «Верификация РФ», `CONSOLIDATION_GO` —
|
||||
«Консолидация ГО», `VERIFICATION_GO` — «Верификация ГО», `VERIFIED` — «Верифицирован», `CANCELLED` — «Отменена»,
|
||||
`ADJUSTMENT` — «Корректировка», `ARCHIVED` — «В архиве», `REJECTED` — «Отклонён».
|
||||
|
||||
#### Scenario: Корректировка данных
|
||||
- **WHEN** версия в статусе `DATA_CORRECTION`
|
||||
- **THEN** в списке и в карточке — бейдж «Корректировка данных»
|
||||
|
||||
#### Scenario: Отменённая версия
|
||||
- **WHEN** версия в статусе `CANCELLED`
|
||||
- **THEN** в списке и в карточке — бейдж «Отменена»
|
||||
|
||||
### Requirement: Вид модалки «Новая консолидация»
|
||||
Модалка [42] (состав и создание — спека `consolidation-setup`) SHALL показывать поля без звёздочек обязательности и кнопку
|
||||
«+ Создать» (иконка «плюс»), неактивную, пока не выбраны ГК, ГСК, год, квартал и не отмечено не меньше двух ЮЛ. В блоке
|
||||
«ЮЛ консолидации» строка ЮЛ SHALL содержать флажок, наименование (длинное — с многоточием) и ИНН отдельной колонкой,
|
||||
выровненной по правому краю независимо от длины наименования; над списком — поиск по наименованию, под списком —
|
||||
«Выбрано: N / M».
|
||||
|
||||
#### Scenario: Выравнивание ИНН
|
||||
- **WHEN** в блоке «ЮЛ консолидации» есть ЮЛ «ВМГ, ООО» и «ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО»
|
||||
- **THEN** их ИНН стоят в одной колонке на одной вертикали
|
||||
|
||||
#### Scenario: Одно ЮЛ
|
||||
- **WHEN** выбраны ГК, ГСК, год, квартал и отмечено одно ЮЛ
|
||||
- **THEN** «+ Создать» неактивна
|
||||
|
||||
### Requirement: Кнопка «Изменить статус» в карточке версии
|
||||
Шапка карточки версии [30] SHALL показывать справа «Статус отчёта» (бейдж) и кнопку «Изменить статус». Кнопка SHALL быть
|
||||
активной, только если версия взята в работу текущим пользователем (`lock.locked_by` карточки = `id` из `/me`) и для
|
||||
текущего статуса есть хотя бы один ручной переход (требование «Модалка «Изменить статус»»). Без своей блокировки кнопка
|
||||
SHALL быть неактивной с подсказкой «Возьмите консолидацию в работу»; при своей блокировке и отсутствии ручных переходов —
|
||||
неактивной.
|
||||
|
||||
#### Scenario: Версия не взята в работу
|
||||
- **WHEN** аналитик открыл карточку версии `DATA_CORRECTION`, блокировки нет
|
||||
- **THEN** «Изменить статус» неактивна, подсказка «Возьмите консолидацию в работу»
|
||||
|
||||
#### Scenario: Версия у текущего пользователя
|
||||
- **WHEN** аналитик взял версию `DATA_CORRECTION` в работу и открыл карточку
|
||||
- **THEN** «Изменить статус» активна
|
||||
|
||||
#### Scenario: Нет ручных переходов
|
||||
- **WHEN** аналитик держит блокировку версии `VERIFIED`
|
||||
- **THEN** «Изменить статус» неактивна
|
||||
|
||||
### Requirement: Модалка «Изменить статус»
|
||||
Модалка [33-01] SHALL показывать «Текущий этап» (бейдж статуса), «Новый этап» (выпадающий список) и «Комментарий»
|
||||
(≤ 1000 символов), кнопки «Отмена» / «Изменить статус». Варианты «Нового этапа» SHALL совпадать с ручными переходами бэка
|
||||
(`MANUAL_TRANSITIONS` в `api/src/services/consolidation_status.py`; API их не отдаёт, фронт держит зеркало в одном месте):
|
||||
- `INITIATED` → `CANCELLED`;
|
||||
- `DOCUMENTS_PROCESSING` → `CANCELLED`, `INITIATED`;
|
||||
- `DATA_CORRECTION` → `CANCELLED`, `INITIATED`;
|
||||
- из остальных статусов ручных переходов нет.
|
||||
|
||||
По умолчанию в «Новом этапе» ничего не выбрано. «Изменить статус» SHALL быть активной, только когда выбран новый этап и
|
||||
комментарий непуст после обрезки пробелов (комментарий обязателен для обоих переходов). Нажатие SHALL отправлять
|
||||
`POST /consolidations/versions/{id}/status {to_status, comment}` (комментарий обрезан); при 200 модалка закрывается, а
|
||||
карточка версии на экране заменяется карточкой из ответа: бейдж, пояснение под заголовком и состояние кнопки «Изменить
|
||||
статус» берутся из неё (после «Отменена» блокировка снята — кнопка неактивна). Ответы 409 (`VERSION_NOT_EDITABLE`,
|
||||
`LOCK_REQUIRED`, `LOCKED_BY_OTHER`, `TRANSITION_NOT_ALLOWED`) и 422 (`COMMENT_REQUIRED`) SHALL показываться сообщением об
|
||||
ошибке, модалка остаётся открытой, статус на экране не меняется.
|
||||
|
||||
#### Scenario: Варианты из «Корректировки данных»
|
||||
- **WHEN** аналитик держит блокировку версии `DATA_CORRECTION` и открывает «Изменить статус»
|
||||
- **THEN** «Текущий этап» — «Корректировка данных», в «Новом этапе» ничего не выбрано, варианты — «Отменена» и
|
||||
«Инициация»; «Изменить статус» неактивна
|
||||
|
||||
#### Scenario: Отмена версии
|
||||
- **WHEN** аналитик выбирает «Отменена», пишет комментарий «Дубль» и нажимает «Изменить статус»
|
||||
- **THEN** отправлен `POST /consolidations/versions/{id}/status {to_status: "CANCELLED", comment: "Дубль"}`; модалка
|
||||
закрыта; бейдж — «Отменена», «Изменить статус» неактивна; в списке консолидаций у версии статус «Отменена»
|
||||
|
||||
#### Scenario: Возврат в «Инициацию»
|
||||
- **WHEN** аналитик держит блокировку версии `DOCUMENTS_PROCESSING`, выбирает «Инициация» и пишет комментарий
|
||||
- **THEN** после ответа 200 бейдж — «Инициация», кнопка «Изменить статус» остаётся активной (переход в «Отменена» доступен)
|
||||
|
||||
#### Scenario: Комментарий из пробелов
|
||||
- **WHEN** выбран новый этап, комментарий состоит из пробелов
|
||||
- **THEN** «Изменить статус» неактивна
|
||||
|
||||
#### Scenario: Переход отклонён сервером
|
||||
- **WHEN** пока модалка открыта, статус версии изменился, и сервер отвечает 409 `TRANSITION_NOT_ALLOWED`
|
||||
- **THEN** показано сообщение об ошибке, модалка открыта, бейдж на экране прежний
|
||||
|
||||
### Requirement: Пояснение статуса под заголовком карточки
|
||||
Под заголовком карточки версии «<ГК> — <год> <квартал>» SHALL показываться пояснение текущего статуса, а не его
|
||||
название: `CONSOLIDATION_RF` — «Автоматическая консолидация, ручные проверки и сравнение КФО клиента и банка», `VERIFIED` —
|
||||
«Отчет верифицирован ГО, КА ГО внес в систему номер и дату решения УОБ» (макет [30], [33-01]); `INITIATED`,
|
||||
`DOCUMENTS_PROCESSING`, `DATA_CORRECTION`, `CANCELLED` — тексты аналитика, до их получения — временные из ТЗ «Статусная
|
||||
модель» (`design.md`, решение без обсуждения (б)): `INITIATED` — «Система зафиксировала состав ГСК на отчетную дату и
|
||||
инициатора (КА РФ или КА ГО)», `DOCUMENTS_PROCESSING` — «КА загружает документы; система отправляет pdf файлы в ИС
|
||||
Сканирование, производит парсинг ОСВ, бухгалтерского баланса и ОФР», `DATA_CORRECTION` — «Выявлены некорректные данные в
|
||||
ИС Сканирование, проводится ручная корректировка. Пользователь валидирует распарсенные данные и вносит правки»,
|
||||
`CANCELLED` — «Заявка на формирование/подтверждение КФО аннулирована по инициативе пользователя». Тексты держатся в одном
|
||||
месте рядом с подписями статусов.
|
||||
|
||||
#### Scenario: Пояснение «Корректировки данных»
|
||||
- **WHEN** открыта карточка версии `DATA_CORRECTION`
|
||||
- **THEN** под заголовком — пояснение статуса, а не слова «Корректировка данных»
|
||||
|
||||
#### Scenario: Пояснение после смены статуса
|
||||
- **WHEN** версия переведена в «Отменена» из модалки «Изменить статус»
|
||||
- **THEN** пояснение под заголовком сменилось на пояснение статуса `CANCELLED` без перезагрузки страницы
|
||||
|
||||
### Requirement: Предупреждение «Остались неуказанные ИНН»
|
||||
При уходе со вкладки «ОСВ» на другую вкладку отчёта версии, если в сводке `unmapped_total > 0`, фронт SHALL показывать
|
||||
модалку [31-00] «Остались неуказанные ИНН»: «В ОСВ остались неуказанные ИНН», «Не заполнено ИНН: N. Точно хотите перейти
|
||||
в другой раздел?», кнопки «Остаться» / «Перейти». «Остаться» SHALL закрывать модалку без перехода, «Перейти» — выполнять
|
||||
переход. При `unmapped_total = 0` переход SHALL выполняться без модалки.
|
||||
|
||||
#### Scenario: Есть несопоставленные строки
|
||||
- **WHEN** в сводке `unmapped_total = 2` и аналитик выбирает другую вкладку отчёта
|
||||
- **THEN** показана модалка «Не заполнено ИНН: 2. Точно хотите перейти в другой раздел?»; «Остаться» оставляет вкладку
|
||||
«ОСВ», «Перейти» открывает выбранную вкладку
|
||||
|
||||
#### Scenario: Все строки сопоставлены
|
||||
- **WHEN** `unmapped_total = 0` и аналитик выбирает другую вкладку отчёта
|
||||
- **THEN** вкладка открывается без модалки
|
||||
@ -1,35 +0,0 @@
|
||||
## 1. Ревьюер (RSHB/arm-core#166, AURORA-1589)
|
||||
|
||||
- [x] 1.1 Влить `origin/main` в ветку `AURORA-1589` (без конфликтов).
|
||||
- [x] 1.2 Change `consolidations-ui`: proposal, design (Decisions, Known issues, Decisions made without discussion), спека
|
||||
`consolidations-ui` — целевое поведение.
|
||||
- [x] 1.3 `web/CLAUDE.md` — `lucide-react` в «Стек»; `web/ARCHITECTURE.md` — новые виджеты экранов консолидаций.
|
||||
- [x] 1.4 Гайд фронта по API §5.7 — заметка о зеркале ручных переходов (PR в RSHB/arm-ka-docs, сливать вместе с #166).
|
||||
|
||||
## 2. Автор PR
|
||||
|
||||
- [ ] 2.1 Исправить «Known issues» 1–13 из `design.md`.
|
||||
- [ ] 2.2 Вынести коммит 71ef633 («Единицы» сводной ОСВ, AURORA-1539) в отдельный PR.
|
||||
- [ ] 2.3 Вынести чистые правила из компонентов в функции без React: подпись колонки «Версия», константа ручных переходов
|
||||
(D-1), активность кнопки «Изменить статус» (D-3), активность «Изменить статус» в модалке, параметр `in_work_only`,
|
||||
подписи и пояснения статусов.
|
||||
- [ ] 2.4 Предупреждение браузера о несохранённых правках (пункт AURORA-1589; вне спеки, решение (е) в `design.md`).
|
||||
- [ ] 2.5 Проверить наличие `lucide-react@0.263.1` в зеркале npm банка (Known issues 15) или заменить иконки на
|
||||
`@ant-design/icons`.
|
||||
- [ ] 2.6 Перед слиянием: `make web-check`, `npm test`, `npm run lint` — зелёные, вывод в описание PR.
|
||||
- [ ] 2.7 Сверка экранов с макетом [34], [35], [42], [30], [33-01], [31-00] на стенде; сценарий AURORA-1589: взять версию
|
||||
в работу → «Изменить статус» → «Отменена» с комментарием → новый статус в списке и запись в истории; то же для
|
||||
возврата в «Инициация».
|
||||
|
||||
## 3. Тесты (ревьюер, после 2.3)
|
||||
|
||||
- [ ] 3.1 vitest на вынесенные функции: «Версия» (1 → пусто, 2 → «2»); переходы — совпадение с `MANUAL_TRANSITIONS`
|
||||
и пустой список для `VERIFIED`, `CANCELLED`, статусов Среза 7; кнопка «Изменить статус» (своя / чужая / нет
|
||||
блокировки × есть / нет переходов); модалка — нет выбора, комментарий из пробелов; `in_work_only` по умолчанию `false`;
|
||||
подписи «Корректировка данных», «Отменена»; пояснение не равно подписи для статусов из спеки.
|
||||
|
||||
## 4. После слияния
|
||||
|
||||
- [ ] 4.1 Тексты пояснений статусов первой очереди от аналитика вместо временных (решение (б)).
|
||||
- [ ] 4.2 С маршрутом согласования (Срез 7): допустимые переходы из API вместо зеркала (D-1), MODIFIED-дельта спеки.
|
||||
- [ ] 4.3 ГСК консолидации в API — колонка «ГСК» и текст модалки [35] (D-8).
|
||||
@ -20,4 +20,4 @@
|
||||
- [x] 3.3 Мелкие замечания раунда 2 (шапка xlsx пресета — счета правила и «кроме», подписи статей при `side_1 = K`,
|
||||
лишняя проверка в `_export_response`, реквизиты правила в `MatrixExportInfo` и `_filename`, описания ошибок preset в
|
||||
OpenAPI, проверка правила до расчёта свода) вынесены в технический долг AURORA-1761 (решение владельца 22.10.2026) — см. design.md.
|
||||
- [x] 3.4 После слияния: архив change.
|
||||
- [ ] 3.4 После слияния: архив change.
|
||||
@ -29,9 +29,4 @@
|
||||
в файле. Цена ошибки: при таком дефекте расчёта выгрузка ответит 500 вместо файла с нулём.
|
||||
- 22.10.2026. Общие реквизиты книги вынесены в базовый `VersionExportInfo`, затронут и код выгрузки шахматки.
|
||||
Обоснование: замечание ревью о копиях шапки и имени файла. Цена ошибки: конфликт слияния с PR #141 (тот меняет
|
||||
`MatrixExportInfo`); порядок — сначала #141, затем ребейз #159. Владелец 22.10.2026 решил иначе: #159 слит первым,
|
||||
конфликт разрешается при слиянии #141.
|
||||
- 22.10.2026. M-7 ревью (openpyxl пишет числа через `%.16g`, `Decimal` теряет точность после 16 значащих цифр)
|
||||
отклонено. Обоснование: Excel хранит число с точностью 15 значащих цифр, выгрузка в ячейку-число не может быть точнее;
|
||||
потеря начинается с сумм порядка 10^14 руб., таких в отчётности групп нет. То же относится к выгрузке шахматки. Цена
|
||||
ошибки: для сумм от 10^14 руб. последние копейки в файле округлятся; лечится только выгрузкой сумм текстом.
|
||||
`MatrixExportInfo`); порядок — сначала #141, затем ребейз #159.
|
||||
@ -6,7 +6,5 @@
|
||||
|
||||
## 2. После слияния
|
||||
|
||||
- [x] 2.1 PR #159 слит раньше #141 (решение владельца 22.10.2026); конфликт с #141 (`build_with_members` возвращает
|
||||
объект) разрешается при слиянии #141
|
||||
- [x] 2.2 Кнопка выгрузки на экране результирующей таблицы — в PR #158 (коммит 473492f); строка в
|
||||
`../docs/dev/frontend-api-guide.md` — follow-up в arm-ka-docs
|
||||
- [ ] 2.1 Ребейз на `main` после слияния PR #141 (`build_with_members` возвращает объект), `make check-full`
|
||||
- [ ] 2.2 Кнопка выгрузки на экране результирующей таблицы (`web/`) и строка в `../docs/dev/frontend-api-guide.md`
|
||||
@ -147,35 +147,13 @@ ORM-модель `audit_event` MUST NOT использоваться для `UPD
|
||||
- **THEN** событие `CONSOLIDATION_STATUS_CHANGE` с `user_id` подтвердившего, `before_value.status = INITIATED`
|
||||
|
||||
### Requirement: Аудит экспорта шахматки
|
||||
Каждый успешный `GET …/intercompany-matrix/all/export`, `POST …/intercompany-matrix/manual/export` и
|
||||
`GET …/intercompany-matrix/preset/{rule_id}/export`
|
||||
Каждый успешный `GET …/intercompany-matrix/all/export` и `POST …/intercompany-matrix/manual/export`
|
||||
SHALL добавлять отдельное неизменяемое событие `audit_event` с `action = EXPORT`, `section = EXPORT`,
|
||||
`object_type = consolidation_version`, `object_id = consolidation_version_id`, `user_id` инициатора,
|
||||
`before_value = null` и `after_value = {report: "intercompany_matrix", mode, debit_account_ids,
|
||||
credit_account_ids, cells}`; для `PRESET` в `after_value` SHALL быть дополнительно `rule_id`, `rule_no` и
|
||||
`rule_version_no` (фактическая версия применённого правила, в том числе при незафиксированной версии правил). `mode` — `ALL`,
|
||||
`MANUAL` или `PRESET`, массивы счетов — ID из запроса (в ALL и PRESET пустые), `cells` — число ячеек. Выгрузка
|
||||
сводной ОСВ продолжает писать собственное событие `EXPORT`.
|
||||
credit_account_ids, cells}`. `mode` — `ALL` или `MANUAL`, массивы счетов — ID из запроса (в ALL пустые),
|
||||
`cells` — число ячеек. Выгрузка сводной ОСВ продолжает писать собственное событие `EXPORT`.
|
||||
|
||||
#### Scenario: Две выгрузки
|
||||
- **WHEN** пользователь дважды выгружает ту же шахматку
|
||||
- **THEN** в `audit_event` две разные записи `EXPORT`, каждая со своим payload и числом ячеек
|
||||
|
||||
#### Scenario: Выгрузка пресета
|
||||
- **WHEN** пользователь выгружает `GET …/intercompany-matrix/preset/{rule_id}/export`
|
||||
- **THEN** событие `EXPORT` содержит `mode = PRESET`, `rule_id`, `rule_no`, `rule_version_no`, пустые массивы счетов и число ячеек
|
||||
|
||||
### Requirement: Аудит экспорта результирующей таблицы ВГО
|
||||
Каждый успешный `GET …/intercompany-matrix/result-table/export` SHALL добавлять отдельное неизменяемое событие
|
||||
`audit_event` с `action = EXPORT`, `section = EXPORT`, `object_type = consolidation_version`,
|
||||
`object_id = consolidation_version_id`, `consolidation_version_id` версии, `user_id` инициатора, `before_value = null`
|
||||
и `after_value = {report: "intercompany_result_table", articles}`, где `articles` — число статей в книге. Неуспешная
|
||||
выгрузка (404, 422, ошибка сборки книги) события не пишет.
|
||||
|
||||
#### Scenario: Выгрузка таблицы
|
||||
- **WHEN** пользователь выгружает результирующую таблицу версии
|
||||
- **THEN** в `audit_event` одна запись `EXPORT` с `after_value = {report: "intercompany_result_table", articles: 5}`
|
||||
|
||||
#### Scenario: Нет счёта статьи
|
||||
- **WHEN** выгрузка отвечает 422 `RESULT_ACCOUNTS_MISSING`
|
||||
- **THEN** событие `EXPORT` не записано
|
||||
|
||||
@ -167,11 +167,10 @@ filename="svod-osv.xlsx"; filename*=UTF-8''svod-osv_<ГК>_<год>-<кварт
|
||||
`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/all` и
|
||||
`POST /api/v1/consolidations/versions/{version_id}/intercompany-matrix/manual`
|
||||
(`CONSOLIDATION_VIEW`, policy) SHALL рассчитывать матрицу без сохранения в БД, из строк сводной ОСВ версии
|
||||
и её периметра. ALL включает балансовые счета (`is_off_balance = false`) вида `A`/`AP` для дебета и `P`/`AP` для
|
||||
кредита; забалансовые счета (001–012) в ALL MUST NOT входить. MANUAL принимает обязательные непустые
|
||||
`debit_account_ids: UUID[]` и `credit_account_ids: UUID[]`, раскрывает выбранные счета по иерархии и оставляет
|
||||
подходящий вид; забалансовые счета в MANUAL допускаются. Неизвестный счёт SHALL давать 422 `ACCOUNT_NOT_FOUND`, неверный
|
||||
вид — 422 `ACCOUNT_KIND_MISMATCH`; пустой массив — 422 валидации тела. Невидимая и несуществующая версия SHALL
|
||||
и её периметра. ALL включает все счета вида `A`/`AP` для дебета и `P`/`AP` для кредита. MANUAL принимает
|
||||
обязательные непустые `debit_account_ids: UUID[]` и `credit_account_ids: UUID[]`, раскрывает выбранные счета
|
||||
по иерархии и оставляет подходящий вид. Неизвестный счёт SHALL давать 422 `ACCOUNT_NOT_FOUND`, неверный вид —
|
||||
422 `ACCOUNT_KIND_MISMATCH`; пустой массив — 422 валидации тела. Невидимая и несуществующая версия SHALL
|
||||
отвечать одинаковым 404 `NOT_FOUND`.
|
||||
|
||||
Ответ SHALL содержать `companies[] {id, name}` для всего периметра и `cells[]` для каждой упорядоченной пары
|
||||
@ -181,36 +180,19 @@ filename="svod-osv.xlsx"; filename*=UTF-8''svod-osv_<ГК>_<год>-<кварт
|
||||
`amount_b` — сумма `closing_credit` строк B с контрагентом A по кредитовым. Расхождение — модуль разности;
|
||||
процент — расхождение / меньшую сумму × 100 или null при нуле. `exceeds_threshold` SHALL быть true при точном
|
||||
проценте > 5, до округления; суммы и процент в JSON SHALL быть десятичными строками с двумя знаками после
|
||||
запятой (`ROUND_HALF_UP`). `one_sided` (пара без встречной стороны) SHALL быть true тогда и только тогда, когда
|
||||
ровно одна из сумм `amount_a`, `amount_b` равна нулю (знак ненулевой суммы не важен); наличие строк ОСВ с нулевым
|
||||
сальдо на признак не влияет. Пара подсвечивается, если `exceeds_threshold` или `one_sided`. `skipped_osv_entry_count`
|
||||
SHALL считать исходные строки ОСВ пары с `account_id = null` на обеих сторонах; они не входят в суммы. Массивы
|
||||
`osv_entry_ids_debit/credit` SHALL содержать ID исходных записей соответствующей стороны.
|
||||
запятой (`ROUND_HALF_UP`). `one_sided` SHALL быть true при наличии исходных записей ОСВ только с одной стороны,
|
||||
в том числе при нулевой сумме. `skipped_osv_entry_count` SHALL считать исходные строки ОСВ пары с
|
||||
`account_id = null` на обеих сторонах; они не входят в суммы. Массивы `osv_entry_ids_debit/credit` SHALL
|
||||
содержать ID исходных записей соответствующей стороны.
|
||||
|
||||
#### Scenario: Пустой периметр и отсутствие ОСВ
|
||||
- **WHEN** у версии нет членов периметра
|
||||
- **THEN** `companies = []`, `cells = []`; если компании есть, но ОСВ по выбранным счетам нет, ячейки присутствуют с нулевыми суммами
|
||||
|
||||
#### Scenario: Только одна сторона пары
|
||||
- **WHEN** у A есть строка ОСВ с контрагентом B и ненулевым сальдо, а у B встречной строки нет
|
||||
- **WHEN** у A есть строка ОСВ с контрагентом B, а у B встречной строки нет
|
||||
- **THEN** для `(A, B)` `one_sided = true`, `variance_percent = null`, соответствующий массив `osv_entry_ids_*` пуст
|
||||
|
||||
#### Scenario: Встречная строка с нулевым сальдо
|
||||
- **WHEN** у A по B сальдо 100 000 000, а у B по A есть строка ОСВ с нулевым сальдо (одни обороты)
|
||||
- **THEN** `one_sided = true`, `exceeds_threshold = false`, пара подсвечивается
|
||||
|
||||
#### Scenario: Обе стороны нулевые
|
||||
- **WHEN** у A и у B есть строки ОСВ друг на друга, но обе суммы равны 0
|
||||
- **THEN** `one_sided = false`, `exceeds_threshold = false`, пара не подсвечивается
|
||||
|
||||
#### Scenario: Отрицательное сальдо
|
||||
- **WHEN** у A по B сальдо −50, у B по A — 0
|
||||
- **THEN** `one_sided = true`
|
||||
|
||||
#### Scenario: Забалансовые счета
|
||||
- **WHEN** у A по B есть сальдо на счёте 001 (`is_off_balance = true`)
|
||||
- **THEN** в режиме ALL оно в суммы не входит; в MANUAL с выбранным счётом 001 — входит
|
||||
|
||||
#### Scenario: Счёт не сопоставлен
|
||||
- **WHEN** у пары есть строка свода с `account_id = null` и двумя `entry_ids`
|
||||
- **THEN** она не входит в суммы, `skipped_osv_entry_count` обеих направленных ячеек пары увеличивается на 2
|
||||
@ -223,60 +205,8 @@ SHALL считать исходные строки ОСВ пары с `account_i
|
||||
`GET …/intercompany-matrix/all/export` и `POST …/intercompany-matrix/manual/export` SHALL использовать
|
||||
те же режимы, тело и права, что и соответствующие JSON-маршруты. Ответ — XLSX с именем
|
||||
`intercompany-matrix_<ГК>_<год>-<квартал>_v<n>.xlsx`, шапкой (ГК, период, версия, единицы, режим,
|
||||
выбранные счета; для ALL — «Все балансовые счета») и строками разницы, дебета, кредита и процента расхождения для
|
||||
каждой пары. Пара SHALL заливаться цветом порога, если `exceeds_threshold` или `one_sided` — так же, как в API.
|
||||
выбранные счета) и строками разницы, дебета, кредита и процента расхождения для каждой пары.
|
||||
|
||||
#### Scenario: Экспорт выбранных счетов
|
||||
- **WHEN** пользователь выгружает MANUAL для ГК «Русский бисквит» за 2024 Q1, версия 1
|
||||
- **THEN** имя содержит `Русский бисквит_2024-Q1_v1.xlsx`, шапка содержит выбранные счета, ячейки процента показывают два знака и символ `%` без повторного масштабирования
|
||||
|
||||
#### Scenario: Подсветка пары без встречной стороны
|
||||
- **WHEN** выгружается ALL, у пары `amount_a = 100000000`, `amount_b = 0`
|
||||
- **THEN** все строки пары залиты цветом порога, в шапке «Режим» и «Счета дебета/кредита» — «Все балансовые счета»
|
||||
|
||||
### Requirement: Шахматка по правилу-пресету
|
||||
`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}`
|
||||
(`CONSOLIDATION_VIEW`, policy) SHALL рассчитывать шахматку в режиме PRESET с тем же ответом, что ALL и MANUAL.
|
||||
Счета дебета и кредита SHALL браться из правила элиминации `rule_id`: дебет — сторона `company_1`, если
|
||||
`side_1 = D`, иначе `company_2`; кредит — вторая сторона; исключения (`excluded`) вычитаются, выбранные счета
|
||||
раскрываются по иерархии и фильтруются по виду, как в MANUAL. Показатель SHALL браться из `measure` правила:
|
||||
`closing` — конечное сальдо (`closing_debit`/`closing_credit`), `turnover` — обороты (`turnover_debit`/`turnover_credit`).
|
||||
Правило SHALL быть действительным для версии консолидации: при заданном `elimination_rules_version` — с тем же
|
||||
`version_no`, иначе — без `valid_to` и с максимальным открытым `version_no`; несуществующее или недействительное правило SHALL давать 404. Ошибки 422:
|
||||
`RULE_REQUIRED` — правило не выбрано, `INVALID_RULE` — правило ОФР либо неверные `side_1`, `measure` или сторона счёта правила,
|
||||
`ACCOUNTS_REQUIRED` — у одной из сторон нет счетов. Невидимая и несуществующая версия SHALL отвечать 404 `NOT_FOUND`.
|
||||
|
||||
#### Scenario: Пресет по сальдо
|
||||
- **WHEN** правило с `side_1 = D`, `measure = closing` содержит дебетовые счета на стороне 1 и кредитовые на стороне 2
|
||||
- **THEN** `amount_a` пары — сумма `closing_debit` по счетам стороны 1, `amount_b` — сумма `closing_credit` по счетам стороны 2
|
||||
|
||||
#### Scenario: Пресет по оборотам
|
||||
- **WHEN** у правила `measure = turnover`
|
||||
- **THEN** суммы считаются по `turnover_debit`/`turnover_credit`
|
||||
|
||||
#### Scenario: Правило другой версии
|
||||
- **WHEN** у версии `elimination_rules_version = 2`, а `rule_id` принадлежит правилу версии 1
|
||||
- **THEN** 404 для расчёта и выгрузки
|
||||
|
||||
#### Scenario: Неизвестное правило
|
||||
- **WHEN** `rule_id` не существует
|
||||
- **THEN** 404
|
||||
|
||||
#### Scenario: Правило ОФР
|
||||
- **WHEN** у правила `statement_type = income`
|
||||
- **THEN** расчёт и выгрузка возвращают 422 `INVALID_RULE`
|
||||
|
||||
### Requirement: XLSX шахматки пресета
|
||||
`GET …/intercompany-matrix/preset/{rule_id}/export` SHALL возвращать XLSX с теми же строками, что JSON-расчёт
|
||||
пресета. Имя файла SHALL иметь вид `intercompany-matrix_<ГК>_<год>-<квартал>_v<n>_rule-balance-<rule_no>.xlsx`. Шапка SHALL
|
||||
содержать режим «Пресет», коды счетов дебета и кредита (раскрытые по иерархии, с учётом исключений правила), номер
|
||||
правила, статьи А и Б (`article_1_line` / `article_2_line`), фактическую версию правил и строку «Показатель» со значением «Сальдо» для `closing` или «Оборот» для `turnover`. Строка «Показатель» SHALL присутствовать и в выгрузках ALL и MANUAL (значение
|
||||
«Сальдо»).
|
||||
|
||||
#### Scenario: Выгрузка по оборотам
|
||||
- **WHEN** пользователь выгружает пресет с `measure = turnover`
|
||||
- **THEN** шапка содержит «Режим — Пресет», «Показатель — Оборот», номер, статьи и версию правила; имя файла заканчивается на `_rule-balance-<rule_no>.xlsx`
|
||||
|
||||
#### Scenario: Правило не найдено
|
||||
- **WHEN** `rule_id` не существует
|
||||
- **THEN** 404, файл не отдаётся, событие аудита не пишется
|
||||
|
||||
@ -226,12 +226,8 @@ account, status, missing_quarters, document_ids}`: `document_type` и `account`
|
||||
|
||||
### Requirement: Правила элиминации версии
|
||||
`GET /api/v1/consolidations/versions/{version_id}/elimination-rules` (`CONSOLIDATION_VIEW`, policy по ГК)
|
||||
SHALL возвращать объект `{version_no, is_version_fixed, rules}`. Если у версии задан
|
||||
`consolidation_version.elimination_rules_version`, `version_no` SHALL равняться ему, `is_version_fixed = true`, а
|
||||
`rules` — правилами этой версии. Если он не задан, `is_version_fixed = false`, а `rules` —
|
||||
открытые правила (`valid_to IS NULL`) только с максимальным `version_no`; ответ SHALL содержать этот фактический
|
||||
`version_no`. При отсутствии открытых правил `version_no = null`, `rules = []`. Каждое правило SHALL содержать `id`, `rule_no`,
|
||||
`version_no`, `statement_type`, `side_1` (`D`/`K`), `measure` (`closing`/`turnover`), `company_1`, `company_2`;
|
||||
SHALL возвращать все правила версии массивом. Каждое правило SHALL содержать `rule_no`, `version_no`,
|
||||
`statement_type`, `side_1` (`D`/`K`), `measure` (`closing`/`turnover`), `company_1`, `company_2`;
|
||||
каждая сторона SHALL содержать `accounts: UUID[]` и `excluded: UUID[]` до раскрытия иерархии счетов.
|
||||
Невидимая и несуществующая версия SHALL отвечать одинаковым 404 `NOT_FOUND`.
|
||||
|
||||
@ -239,18 +235,6 @@ SHALL возвращать объект `{version_no, is_version_fixed, rules}`.
|
||||
- **WHEN** у правила первая сторона включает счёт 76 и исключает 76.07.5
|
||||
- **THEN** ID 76 находится в `company_1.accounts`, ID 76.07.5 — в `company_1.excluded`
|
||||
|
||||
#### Scenario: Версия правил зафиксирована
|
||||
- **WHEN** у версии консолидации `elimination_rules_version = 1`
|
||||
- **THEN** ответ содержит `is_version_fixed = true`, `version_no = 1` и только правила с `version_no = 1`
|
||||
|
||||
#### Scenario: Версия правил не зафиксирована
|
||||
- **WHEN** у версии консолидации `elimination_rules_version` пуст
|
||||
- **THEN** ответ содержит `is_version_fixed = false`, фактический `version_no` и открытые правила только этой версии
|
||||
|
||||
#### Scenario: Открыты две версии правил
|
||||
- **WHEN** открыты правила с `version_no = 1` и `version_no = 2`, в том числе с одинаковым `rule_no`
|
||||
- **THEN** ответ содержит только правила версии 2 и `version_no = 2`
|
||||
|
||||
### Requirement: Результирующая таблица ВГО
|
||||
`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/result-table`
|
||||
(`CONSOLIDATION_VIEW`, policy) SHALL возвращать `{companies[], articles[]}`. Компании — все члены периметра
|
||||
@ -267,24 +251,3 @@ SHALL возвращать объект `{version_no, is_version_fixed, rules}`.
|
||||
#### Scenario: Нет обязательного счёта
|
||||
- **WHEN** в плане счетов отсутствует счёт 58.03
|
||||
- **THEN** ответ 422 `RESULT_ACCOUNTS_MISSING`, сообщение содержит `58.03`
|
||||
|
||||
### Requirement: XLSX результирующей таблицы ВГО
|
||||
`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/result-table/export` (`CONSOLIDATION_VIEW`,
|
||||
policy) SHALL возвращать XLSX с теми же статьями, компаниями и итогами, что `GET …/intercompany-matrix/result-table`,
|
||||
из того же расчёта сводной ОСВ версии. Книга — лист «Результирующая таблица» с шапкой (ГК, период, версия, единицы
|
||||
«руб.») и таблицей «Статья ВГО» × компании периметра + колонка «Итого по группе»; суммы — числа с двумя знаками. Имя
|
||||
файла — `intercompany-result-table_<ГК>_<год>-<квартал>_v<n>.xlsx`. Ошибки — как у JSON-маршрута: невидимая или
|
||||
несуществующая версия — 404, отсутствие необходимого кода счёта — 422 `RESULT_ACCOUNTS_MISSING`.
|
||||
|
||||
#### Scenario: Совпадение с JSON
|
||||
- **WHEN** пользователь выгружает результирующую таблицу ГК «Русский бисквит» за 2024 Q1, версия 1
|
||||
- **THEN** имя содержит `Русский бисквит_2024-Q1_v1.xlsx`, порядок компаний, названия статей, суммы по компаниям и
|
||||
«Итого по группе» совпадают с ответом `GET …/result-table` той же версии
|
||||
|
||||
#### Scenario: Нет счёта статьи
|
||||
- **WHEN** в плане счетов нет кода 58.03
|
||||
- **THEN** выгрузка отвечает 422 `RESULT_ACCOUNTS_MISSING` с кодом в сообщении, файл не формируется
|
||||
|
||||
#### Scenario: Версия вне видимости
|
||||
- **WHEN** пользователь филиала выгружает таблицу версии чужой ГК
|
||||
- **THEN** ответ 404 с тем же телом, что у несуществующей версии
|
||||
|
||||
@ -15,23 +15,9 @@ SPA поверх API arm-core (`/api/v1`). Своих данных и бизне
|
||||
| `src/utils/` | чистые функции: деньги, подписи, скачивание | `types/` |
|
||||
| `src/types/` | DTO API (повторяют `../api/src/schemas/`) | — |
|
||||
| `src/components/` | примитивы UI (таблица, теги, заголовок) | antd |
|
||||
| `src/widgets/` | блоки экранов (`DocumentReview` — проверка документа со сканом, `ConsolidatedOsv` — сводная ОСВ, `FilesDrawer`; консолидации — см. ниже) | `components/`, `store/`, `services/api/`, `utils/` |
|
||||
| `src/widgets/` | блоки экранов (`DocumentReview` — проверка документа со сканом, `ConsolidatedOsv` — сводная ОСВ, `FilesDrawer`) | `components/`, `store/`, `services/api/`, `utils/` |
|
||||
| `src/layouts/` | страницы-маршруты | `widgets/` |
|
||||
|
||||
### Виджеты экранов консолидаций
|
||||
|
||||
Требования — `../openspec/changes/consolidations-ui/` (после архивации — `../openspec/specs/consolidations-ui/`),
|
||||
`consolidation-setup`, `consolidated-osv-ui`.
|
||||
|
||||
| Виджет | Экран | Что делает |
|
||||
|---|---|---|
|
||||
| `ConsoleTable/ConsoleTable.tsx` | [34] | список консолидаций ГК: дерево версий, «Только в работе» (`in_work_only`), действия строки, меню «⋯» |
|
||||
| `ConsoleTable/LockModal.tsx` | [35] | «Взять в работу» (`POST …/lock`) и перехват с причиной (`takeover_reason`) |
|
||||
| `ConsoleTable/consts.ts` | — | подписи, цвета и пояснения статусов версии — одно место для списка, карточки и модалки |
|
||||
| `ConsoleLegalEntities/` | [34] | панель «Организации» выбранной версии и чек-лист |
|
||||
| `NewConsoleModal/` (`CompanyOptionRow.tsx`) | [42] | «Новая консолидация»; строка ЮЛ с ИНН отдельной колонкой |
|
||||
| `StatusChangeModal/ChangeStatusModal.tsx` | [33-01] | «Изменить статус» (`POST …/status`); открывается из шапки `layouts/ConsolidatedOsvPage.tsx` |
|
||||
|
||||
## Границы
|
||||
- Сетевые запросы — только через `src/api/client.ts`; `fetch` напрямую не вызывается.
|
||||
- `components/` не знают про store и API; `types/` ни от чего не зависят.
|
||||
|
||||
@ -27,9 +27,6 @@ arm-web перенесены в arm-core #150–#157 (arm-web#7 → #151), сп
|
||||
## Стек и ограничения
|
||||
- React 18 + TypeScript strict, rsbuild, antd 6 (основной UI-кит; daisyui/tailwind — из шаблона), react-router 7,
|
||||
Redux Toolkit, pdfjs-dist (legacy-сборка — для Яндекс Браузера, см. `src/widgets/DocumentReview/pdfjs.ts`).
|
||||
- Иконки: основная библиотека — `@ant-design/icons`; вторая — `lucide-react` (зафиксированная версия), только для иконок
|
||||
макета, которых в antd нет (боковое меню, «Взять в работу», «К отчётности», меню «⋯» консолидации). Новую иконку сначала
|
||||
ищем в `@ant-design/icons`.
|
||||
- Пакеты `raisa-*` — `optionalDependencies` (вне банка недоступны); `package-lock.json` в git не хранится (в банке
|
||||
зависимости ставятся через Nexus). В контуре `package.json` урезается до сборки
|
||||
(`../../docs/dev/contour-transfer-guide.md` §3.2): новые пакеты, нужные сборке, добавлять и в разрешённый список там.
|
||||
@ -49,8 +46,7 @@ arm-web перенесены в arm-core #150–#157 (arm-web#7 → #151), сп
|
||||
- `src/store/` — Redux Toolkit: `store.ts`, слайсы `companyGroupsSlice` и `consolidations` (+ `thunks.ts`).
|
||||
- `src/layouts/` — страницы (маршруты в `src/App.tsx`, в т.ч. `/documents/:document_id` и
|
||||
`/groups/:group_id/versions/:version_id/osv`), `src/widgets/` — блоки экранов (`DocumentReview` — проверка ББ/ОФР/ОСВ
|
||||
со сканом, `ConsolidatedOsv` — сводная ОСВ, `FilesDrawer` — файлы ЮЛ, `ConsoleTable` — список консолидаций ГК и
|
||||
«Взять в работу», `StatusChangeModal` — «Изменить статус» версии), `src/components/` — примитивы
|
||||
со сканом, `ConsolidatedOsv` — сводная ОСВ, `FilesDrawer` — файлы ЮЛ), `src/components/` — примитивы
|
||||
(`GenericTable`, `TableTag`, `PageTitle`), `src/types/` — DTO API.
|
||||
|
||||
## Команды (из `web/`)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user