добавлена информация о искоючения в выгрузку (шахматка пресеты)

This commit is contained in:
PotapovaA 2026-11-05 17:29:27 +03:00
parent 967fa01fb6
commit e5b998df20
5 changed files with 70 additions and 10 deletions

View File

@ -90,6 +90,10 @@ class MatrixSelection:
company_1_account_ids: frozenset[uuid.UUID] company_1_account_ids: frozenset[uuid.UUID]
company_2_account_ids: frozenset[uuid.UUID] company_2_account_ids: frozenset[uuid.UUID]
measure: Measure 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) @dataclass(frozen=True)
@ -129,6 +133,9 @@ class MatrixExportInfo(VersionExportInfo):
rule_statement_type: StatementType | None = None rule_statement_type: StatementType | None = None
article_1_line: str | None = None article_1_line: str | None = None
article_2_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 @property
def preset_details(self) -> tuple[StatementType, int, int, str, str]: def preset_details(self) -> tuple[StatementType, int, int, str, str]:
@ -704,8 +711,8 @@ class IntercompanyMatrixService:
group_name = await self._group_name(result.consolidation) group_name = await self._group_name(result.consolidation)
by_id = {account.id: account.code for account in result.accounts} by_id = {account.id: account.code for account in result.accounts}
if mode == "PRESET": if mode == "PRESET":
debit_codes = tuple(sorted(by_id[account_id] for account_id in result.selection.company_1_account_ids)) debit_codes = result.selection.debit_rule_codes
credit_codes = tuple(sorted(by_id[account_id] for account_id in result.selection.company_2_account_ids)) credit_codes = result.selection.credit_rule_codes
else: else:
debit_codes = tuple(by_id[account_id] for account_id in debit_account_ids) 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) 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, 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_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, 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 return result.matrix, info
@ -764,7 +774,16 @@ class IntercompanyMatrixService:
credit = _select_accounts(credit_ids, credit_excluded, "K", by_id, children) credit = _select_accounts(credit_ids, credit_excluded, "K", by_id, children)
measure: Measure = "closing" if rule.measure == "closing" else "turnover" 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( async def _build_accounts(
self, self,

View File

@ -57,17 +57,21 @@ def _write_context(sheet: Worksheet, info: MatrixExportInfo) -> int:
extra = [ extra = [
("Режим", mode_name), ("Режим", mode_name),
("Счета дебета", ", ".join(info.debit_account_codes) if info.mode != "ALL" else all_accounts), ("Счета дебета", ", ".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": if info.mode == "PRESET":
statement_type, rule_no, rule_version_no, article_1_line, article_2_line = info.preset_details statement_type, rule_no, rule_version_no, article_1_line, article_2_line = info.preset_details
rule_type = "ББ" if statement_type.value == "balance" else "ОФР" rule_type = "ББ" if statement_type.value == "balance" else "ОФР"
extra.extend( extra.extend(
( (
("Правило", f"{rule_type} № {rule_no}"), ("Правило", f"{rule_type} № {rule_no}"),
("Статья А", article_1_line), ("Статья А", article_1_line if info.rule_side_1 != "K" else article_2_line),
("Статья Б", article_2_line), ("Статья Б", article_2_line if info.rule_side_1 != "K" else article_1_line),
("Версия правил", f"v{rule_version_no}"), ("Версия правил", f"v{rule_version_no}"),
) )
) )

View File

@ -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()) _, info = await service.build_export(uuid.uuid4(), mode="PRESET", rule_id=uuid.uuid4())
assert info.mode == "PRESET" assert info.mode == "PRESET"
assert info.debit_account_codes == ("76",) 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.measure == "turnover"
assert info.rule_no == rule.rule_no assert info.rule_no == rule.rule_no
assert info.rule_version_no == rule.version_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) credit_child = _account(AccountKind.P, credit_parent.id)
debit_parent = _account(AccountKind.A) debit_parent = _account(AccountKind.A)
debit_child = _account(AccountKind.A, debit_parent.id) 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] accounts = [credit_parent, credit_child, debit_parent, debit_child]
by_id, children = matrix._make_account_tree(accounts) by_id, children = matrix._make_account_tree(accounts)
rule = SimpleNamespace( 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_1_account_ids == {debit_parent.id, debit_child.id}
assert selection.company_2_account_ids == {credit_parent.id} assert selection.company_2_account_ids == {credit_parent.id}
assert selection.measure == "turnover" 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: async def test_preset_rejects_income_rule_before_selecting_accounts(monkeypatch: pytest.MonkeyPatch) -> None:

View File

@ -1,5 +1,6 @@
"""XLSX-выгрузка шахматки из готового результата расчёта.""" """XLSX-выгрузка шахматки из готового результата расчёта."""
from dataclasses import replace
from io import BytesIO from io import BytesIO
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import AsyncMock 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, rule_statement_type=StatementType.balance if mode == "PRESET" else None,
article_1_line="1230" if mode == "PRESET" else None, article_1_line="1230" if mode == "PRESET" else None,
article_2_line="1520" 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: def test_matrix_export_has_three_rows_per_debit_company_and_highlights_threshold() -> None:
company_a = UUID("11111111-1111-1111-1111-111111111111") company_a = UUID("11111111-1111-1111-1111-111111111111")
company_b = UUID("22222222-2222-2222-2222-222222222222") company_b = UUID("22222222-2222-2222-2222-222222222222")

View File

@ -280,8 +280,8 @@ SHALL считать исходные строки ОСВ пары с `account_i
### Requirement: XLSX шахматки пресета ### Requirement: XLSX шахматки пресета
`GET …/intercompany-matrix/preset/{rule_id}/export` SHALL возвращать XLSX с теми же строками, что JSON-расчёт `GET …/intercompany-matrix/preset/{rule_id}/export` SHALL возвращать XLSX с теми же строками, что JSON-расчёт
пресета. Имя файла SHALL иметь вид `intercompany-matrix_<ГК>_<год>-<квартал>_v<n>_rule-balance-<rule_no>.xlsx`. Шапка SHALL пресета. Имя файла SHALL иметь вид `intercompany-matrix_<ГК>_<год>-<квартал>_v<n>_rule-balance-<rule_no>.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: Выгрузка по оборотам #### Scenario: Выгрузка по оборотам