diff --git a/api/src/services/intercompany_matrix.py b/api/src/services/intercompany_matrix.py index cd3d7e3..2052745 100644 --- a/api/src/services/intercompany_matrix.py +++ b/api/src/services/intercompany_matrix.py @@ -90,6 +90,10 @@ class MatrixSelection: company_1_account_ids: frozenset[uuid.UUID] company_2_account_ids: frozenset[uuid.UUID] measure: Measure + debit_rule_codes: tuple[str, ...] = () + credit_rule_codes: tuple[str, ...] = () + debit_excluded_codes: tuple[str, ...] = () + credit_excluded_codes: tuple[str, ...] = () @dataclass(frozen=True) @@ -129,6 +133,9 @@ class MatrixExportInfo(VersionExportInfo): rule_statement_type: StatementType | None = None article_1_line: str | None = None article_2_line: str | None = None + rule_side_1: str | None = None + debit_excluded_codes: tuple[str, ...] = () + credit_excluded_codes: tuple[str, ...] = () @property def preset_details(self) -> tuple[StatementType, int, int, str, str]: @@ -704,8 +711,8 @@ class IntercompanyMatrixService: group_name = await self._group_name(result.consolidation) by_id = {account.id: account.code for account in result.accounts} if mode == "PRESET": - debit_codes = tuple(sorted(by_id[account_id] for account_id in result.selection.company_1_account_ids)) - credit_codes = tuple(sorted(by_id[account_id] for account_id in result.selection.company_2_account_ids)) + debit_codes = result.selection.debit_rule_codes + credit_codes = result.selection.credit_rule_codes else: debit_codes = tuple(by_id[account_id] for account_id in debit_account_ids) credit_codes = tuple(by_id[account_id] for account_id in credit_account_ids) @@ -724,6 +731,9 @@ class IntercompanyMatrixService: rule_statement_type=result.rule.statement_type if result.rule is not None else None, article_1_line=result.rule.article_1_line if result.rule is not None else None, article_2_line=result.rule.article_2_line if result.rule is not None else None, + rule_side_1=result.rule.side_1 if result.rule is not None else None, + debit_excluded_codes=result.selection.debit_excluded_codes, + credit_excluded_codes=result.selection.credit_excluded_codes, ) return result.matrix, info @@ -764,7 +774,16 @@ class IntercompanyMatrixService: credit = _select_accounts(credit_ids, credit_excluded, "K", by_id, children) measure: Measure = "closing" if rule.measure == "closing" else "turnover" - return MatrixSelection(debit, credit, measure), rule + def codes(ids: Sequence[uuid.UUID]) -> tuple[str, ...]: + return tuple(sorted(by_id[account_id].code for account_id in ids)) + + return MatrixSelection( + debit, credit, measure, + debit_rule_codes=codes(debit_ids), + credit_rule_codes=codes(credit_ids), + debit_excluded_codes=codes(debit_excluded), + credit_excluded_codes=codes(credit_excluded), + ), rule async def _build_accounts( self, diff --git a/api/src/services/intercompany_matrix_export.py b/api/src/services/intercompany_matrix_export.py index a0e0fd2..689a5c3 100644 --- a/api/src/services/intercompany_matrix_export.py +++ b/api/src/services/intercompany_matrix_export.py @@ -57,17 +57,21 @@ def _write_context(sheet: Worksheet, info: MatrixExportInfo) -> int: 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), - ("Показатель", "Сальдо" if info.measure == "closing" else "Оборот"), ] + if info.mode == "PRESET" and info.debit_excluded_codes: + extra.append(("кроме", ", ".join(info.debit_excluded_codes))) + extra.append(("Счета кредита", ", ".join(info.credit_account_codes) if info.mode != "ALL" else all_accounts)) + if info.mode == "PRESET" and info.credit_excluded_codes: + extra.append(("кроме", ", ".join(info.credit_excluded_codes))) + extra.append(("Показатель", "Сальдо" if info.measure == "closing" else "Оборот")) if info.mode == "PRESET": statement_type, rule_no, rule_version_no, article_1_line, article_2_line = info.preset_details rule_type = "ББ" if statement_type.value == "balance" else "ОФР" extra.extend( ( ("Правило", f"{rule_type} № {rule_no}"), - ("Статья А", article_1_line), - ("Статья Б", article_2_line), + ("Статья А", article_1_line if info.rule_side_1 != "K" else article_2_line), + ("Статья Б", article_2_line if info.rule_side_1 != "K" else article_1_line), ("Версия правил", f"v{rule_version_no}"), ) ) diff --git a/api/tests/test_intercompany_matrix.py b/api/tests/test_intercompany_matrix.py index 2719670..1f22f11 100644 --- a/api/tests/test_intercompany_matrix.py +++ b/api/tests/test_intercompany_matrix.py @@ -168,7 +168,10 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey _, info = await service.build_export(uuid.uuid4(), mode="PRESET", rule_id=uuid.uuid4()) assert info.mode == "PRESET" assert info.debit_account_codes == ("76",) - assert info.credit_account_codes == ("76", "76.01") + assert info.credit_account_codes == ("76",) + assert info.debit_excluded_codes == ("76.01",) + assert info.credit_excluded_codes == () + assert info.rule_side_1 == "D" assert info.measure == "turnover" assert info.rule_no == rule.rule_no assert info.rule_version_no == rule.version_no @@ -181,6 +184,10 @@ async def test_preset_credit_first_uses_second_side_for_debit(monkeypatch: pytes credit_child = _account(AccountKind.P, credit_parent.id) debit_parent = _account(AccountKind.A) debit_child = _account(AccountKind.A, debit_parent.id) + credit_parent.code = "76" + credit_child.code = "76.07.1" + debit_parent.code = "62" + debit_child.code = "62.01" accounts = [credit_parent, credit_child, debit_parent, debit_child] by_id, children = matrix._make_account_tree(accounts) rule = SimpleNamespace( @@ -208,6 +215,9 @@ async def test_preset_credit_first_uses_second_side_for_debit(monkeypatch: pytes assert selection.company_1_account_ids == {debit_parent.id, debit_child.id} assert selection.company_2_account_ids == {credit_parent.id} assert selection.measure == "turnover" + assert selection.debit_rule_codes == ("62",) + assert selection.credit_rule_codes == ("76",) + assert selection.credit_excluded_codes == ("76.07.1",) async def test_preset_rejects_income_rule_before_selecting_accounts(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/api/tests/test_intercompany_matrix_export.py b/api/tests/test_intercompany_matrix_export.py index e4a72d4..f934b2a 100644 --- a/api/tests/test_intercompany_matrix_export.py +++ b/api/tests/test_intercompany_matrix_export.py @@ -1,5 +1,6 @@ """XLSX-выгрузка шахматки из готового результата расчёта.""" +from dataclasses import replace from io import BytesIO from types import SimpleNamespace from unittest.mock import AsyncMock @@ -52,9 +53,35 @@ def _info(*, mode: str = "MANUAL", rule_id: UUID | None = None) -> MatrixExportI rule_statement_type=StatementType.balance if mode == "PRESET" else None, article_1_line="1230" if mode == "PRESET" else None, article_2_line="1520" if mode == "PRESET" else None, + rule_side_1="D" if mode == "PRESET" else None, ) +def test_preset_header_shows_rule_codes_exclusions_and_credit_first_articles() -> None: + matrix = IntercompanyMatrixOut(companies=[], cells=[]) + info = replace( + _info(mode="PRESET"), + debit_account_codes=("60.02", "76"), + credit_account_codes=("60.01", "76"), + debit_excluded_codes=("76.07.5", "76.07.9"), + credit_excluded_codes=("76.07.1",), + rule_side_1="K", + ) + + sheet = load_workbook(BytesIO(build_matrix_workbook(matrix, info))).active + assert [(sheet.cell(row, 1).value, sheet.cell(row, 2).value) for row in range(7, 16)] == [ + ("Счета дебета", "60.02, 76"), + ("кроме", "76.07.5, 76.07.9"), + ("Счета кредита", "60.01, 76"), + ("кроме", "76.07.1"), + ("Показатель", "Оборот"), + ("Правило", "ББ № 6"), + ("Статья А", "1520"), + ("Статья Б", "1230"), + ("Версия правил", "v3"), + ] + + def test_matrix_export_has_three_rows_per_debit_company_and_highlights_threshold() -> None: company_a = UUID("11111111-1111-1111-1111-111111111111") company_b = UUID("22222222-2222-2222-2222-222222222222") diff --git a/openspec/specs/consolidated-osv/spec.md b/openspec/specs/consolidated-osv/spec.md index 61a9661..65abd28 100644 --- a/openspec/specs/consolidated-osv/spec.md +++ b/openspec/specs/consolidated-osv/spec.md @@ -280,8 +280,8 @@ SHALL считать исходные строки ОСВ пары с `account_i ### Requirement: XLSX шахматки пресета `GET …/intercompany-matrix/preset/{rule_id}/export` SHALL возвращать XLSX с теми же строками, что JSON-расчёт пресета. Имя файла SHALL иметь вид `intercompany-matrix_<ГК>_<год>-<квартал>_v_rule-balance-.xlsx`. Шапка SHALL -содержать режим «Пресет», коды счетов дебета и кредита (раскрытые по иерархии, с учётом исключений правила), номер -правила, статьи А и Б (`article_1_line` / `article_2_line`), фактическую версию правил и строку «Показатель» со значением «Сальдо» для `closing` или «Оборот» для `turnover`. Строка «Показатель» SHALL присутствовать и в выгрузках ALL и MANUAL (значение +содержать режим «Пресет», исходные коды счетов дебета и кредита из правила и отдельную строку «кроме» для исключений каждой стороны, если они есть, номер +правила, статьи А и Б по дебетовой и кредитовой сторонам соответственно (`article_1_line` / `article_2_line` при `side_1 = D`, наоборот при `side_1 = K`), фактическую версию правил и строку «Показатель» со значением «Сальдо» для `closing` или «Оборот» для `turnover`. Строка «Показатель» SHALL присутствовать и в выгрузках ALL и MANUAL (значение «Сальдо»). #### Scenario: Выгрузка по оборотам