убраны лишние проверки
This commit is contained in:
parent
e5b998df20
commit
fb04765ae9
@ -34,11 +34,20 @@ RESULT_ERRORS: dict[int | str, dict[str, Any]] = {
|
||||
"description": "В плане счетов нет счетов статей результирующей таблицы",
|
||||
},
|
||||
}
|
||||
PRESET_ERRORS: dict[int | str, dict[str, Any]] = {
|
||||
**ERRORS,
|
||||
status.HTTP_404_NOT_FOUND: {"model": ErrorDetail, "description": "Версия или правило не найдены либо недоступны"},
|
||||
status.HTTP_422_UNPROCESSABLE_CONTENT: {
|
||||
"model": ErrorDetail,
|
||||
"description": "INVALID_RULE, RULE_REQUIRED или ACCOUNTS_REQUIRED",
|
||||
},
|
||||
}
|
||||
XLSX_MEDIA_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
XLSX_CONTENT: dict[int | str, dict[str, Any]] = {
|
||||
status.HTTP_200_OK: {"content": {XLSX_MEDIA_TYPE: {"schema": {"type": "string", "format": "binary"}}}}
|
||||
}
|
||||
EXPORT_RESPONSES = {**ERRORS, **XLSX_CONTENT}
|
||||
PRESET_EXPORT_RESPONSES = {**PRESET_ERRORS, **XLSX_CONTENT}
|
||||
RESULT_EXPORT_RESPONSES = {**RESULT_ERRORS, **XLSX_CONTENT}
|
||||
|
||||
|
||||
@ -59,8 +68,6 @@ async def _export_response(
|
||||
selection: IntercompanyMatrixManualIn | None = None,
|
||||
) -> Response:
|
||||
content = await run_in_threadpool(build_matrix_workbook, matrix, info)
|
||||
if info.mode == "PRESET" and (info.rule_id is None or info.rule_no is None or info.rule_version_no is None):
|
||||
raise ValueError("Для аудита PRESET нужны ID, номер и версия правила")
|
||||
response = _xlsx_response(content, info.filename, ascii_fallback="intercompany-matrix.xlsx")
|
||||
await audit.record(
|
||||
action=AuditAction.EXPORT,
|
||||
@ -75,11 +82,11 @@ async def _export_response(
|
||||
"credit_account_ids": [str(account_id) for account_id in selection.credit_account_ids] if selection else [],
|
||||
**(
|
||||
{
|
||||
"rule_id": str(info.rule_id),
|
||||
"rule_no": info.rule_no,
|
||||
"rule_version_no": info.rule_version_no,
|
||||
"rule_id": str(info.preset.id),
|
||||
"rule_no": info.preset.rule_no,
|
||||
"rule_version_no": info.preset.version_no,
|
||||
}
|
||||
if info.mode == "PRESET"
|
||||
if info.preset is not None
|
||||
else {}
|
||||
),
|
||||
"cells": len(matrix.cells),
|
||||
@ -172,7 +179,7 @@ async def export_intercompany_matrix_manual(
|
||||
"/{version_id}/intercompany-matrix/preset/{rule_id}",
|
||||
response_model=IntercompanyMatrixOut,
|
||||
dependencies=VIEW,
|
||||
responses=ERRORS,
|
||||
responses=PRESET_ERRORS,
|
||||
summary="Рассчитать шахматку по пресету",
|
||||
)
|
||||
async def build_intercompany_matrix_preset(
|
||||
@ -188,7 +195,7 @@ async def build_intercompany_matrix_preset(
|
||||
"/{version_id}/intercompany-matrix/preset/{rule_id}/export",
|
||||
response_class=Response,
|
||||
dependencies=VIEW,
|
||||
responses=EXPORT_RESPONSES,
|
||||
responses=PRESET_EXPORT_RESPONSES,
|
||||
summary="Скачать шахматку по пресету в XLSX",
|
||||
)
|
||||
async def export_intercompany_matrix_preset(
|
||||
|
||||
@ -5,7 +5,7 @@ from collections import defaultdict
|
||||
from collections.abc import Iterable, Sequence
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
from typing import Literal, cast
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@ -117,8 +117,21 @@ class VersionExportInfo:
|
||||
quarter: PeriodQuarter
|
||||
version_no: int
|
||||
|
||||
def _filename(self, prefix: str) -> str:
|
||||
return f"{prefix}_{self.group_name}_{self.year}-{self.quarter.value}_v{self.version_no}.xlsx"
|
||||
def _filename(self, prefix: str, suffix: str = "") -> str:
|
||||
return f"{prefix}_{self.group_name}_{self.year}-{self.quarter.value}_v{self.version_no}{suffix}.xlsx"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PresetRuleDetails:
|
||||
id: uuid.UUID
|
||||
rule_no: int
|
||||
version_no: int
|
||||
statement_type: StatementType
|
||||
article_1_line: str
|
||||
article_2_line: str
|
||||
side_1: Side
|
||||
debit_excluded_codes: tuple[str, ...] = ()
|
||||
credit_excluded_codes: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -127,40 +140,12 @@ class MatrixExportInfo(VersionExportInfo):
|
||||
debit_account_codes: tuple[str, ...]
|
||||
credit_account_codes: tuple[str, ...]
|
||||
measure: Measure = "closing"
|
||||
rule_id: uuid.UUID | None = None
|
||||
rule_no: int | None = None
|
||||
rule_version_no: int | None = None
|
||||
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]:
|
||||
"""Реквизиты правила §4 для имени и шапки XLSX шахматки §6."""
|
||||
details = (
|
||||
self.rule_statement_type,
|
||||
self.rule_no,
|
||||
self.rule_version_no,
|
||||
self.article_1_line,
|
||||
self.article_2_line,
|
||||
)
|
||||
if any(value is None for value in details):
|
||||
raise ValueError("Для выгрузки PRESET нужны тип, номер, версия и статьи правила")
|
||||
statement_type, rule_no, rule_version_no, article_1_line, article_2_line = details
|
||||
assert statement_type is not None and rule_no is not None and rule_version_no is not None
|
||||
assert article_1_line is not None and article_2_line is not None
|
||||
return statement_type, rule_no, rule_version_no, article_1_line, article_2_line
|
||||
preset: PresetRuleDetails | None = None
|
||||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
preset = ""
|
||||
if self.mode == "PRESET":
|
||||
statement_type, rule_no, _, _, _ = self.preset_details
|
||||
preset = f"_rule-{statement_type.value}-{rule_no}"
|
||||
return f"intercompany-matrix_{self.group_name}_{self.year}-{self.quarter.value}_v{self.version_no}{preset}.xlsx"
|
||||
suffix = f"_rule-{self.preset.statement_type.value}-{self.preset.rule_no}" if self.preset else ""
|
||||
return self._filename("intercompany-matrix", suffix)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -725,16 +710,20 @@ class IntercompanyMatrixService:
|
||||
debit_account_codes=debit_codes,
|
||||
credit_account_codes=credit_codes,
|
||||
measure=result.selection.measure,
|
||||
rule_id=result.rule.id if result.rule is not None else None,
|
||||
rule_no=result.rule.rule_no if result.rule is not None else None,
|
||||
rule_version_no=result.rule.version_no 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_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,
|
||||
preset=PresetRuleDetails(
|
||||
id=result.rule.id,
|
||||
rule_no=result.rule.rule_no,
|
||||
version_no=result.rule.version_no,
|
||||
statement_type=result.rule.statement_type,
|
||||
article_1_line=result.rule.article_1_line,
|
||||
article_2_line=result.rule.article_2_line,
|
||||
side_1=cast(Side, result.rule.side_1),
|
||||
debit_excluded_codes=result.selection.debit_excluded_codes,
|
||||
credit_excluded_codes=result.selection.credit_excluded_codes,
|
||||
)
|
||||
if result.rule is not None
|
||||
else None,
|
||||
)
|
||||
return result.matrix, info
|
||||
|
||||
async def _resolve_preset_selection(
|
||||
@ -778,7 +767,9 @@ class IntercompanyMatrixService:
|
||||
return tuple(sorted(by_id[account_id].code for account_id in ids))
|
||||
|
||||
return MatrixSelection(
|
||||
debit, credit, measure,
|
||||
debit,
|
||||
credit,
|
||||
measure,
|
||||
debit_rule_codes=codes(debit_ids),
|
||||
credit_rule_codes=codes(credit_ids),
|
||||
debit_excluded_codes=codes(debit_excluded),
|
||||
|
||||
@ -58,21 +58,21 @@ def _write_context(sheet: Worksheet, info: MatrixExportInfo) -> int:
|
||||
("Режим", mode_name),
|
||||
("Счета дебета", ", ".join(info.debit_account_codes) if info.mode != "ALL" else all_accounts),
|
||||
]
|
||||
if info.mode == "PRESET" and info.debit_excluded_codes:
|
||||
extra.append(("кроме", ", ".join(info.debit_excluded_codes)))
|
||||
if info.preset is not None and info.preset.debit_excluded_codes:
|
||||
extra.append(("кроме", ", ".join(info.preset.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)))
|
||||
if info.preset is not None and info.preset.credit_excluded_codes:
|
||||
extra.append(("кроме", ", ".join(info.preset.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 "ОФР"
|
||||
if info.preset is not None:
|
||||
preset = info.preset
|
||||
rule_type = "ББ" if preset.statement_type.value == "balance" else "ОФР"
|
||||
extra.extend(
|
||||
(
|
||||
("Правило", f"{rule_type} № {rule_no}"),
|
||||
("Статья А", 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}"),
|
||||
("Правило", f"{rule_type} № {preset.rule_no}"),
|
||||
("Статья А", preset.article_1_line if preset.side_1 != "K" else preset.article_2_line),
|
||||
("Статья Б", preset.article_2_line if preset.side_1 != "K" else preset.article_1_line),
|
||||
("Версия правил", f"v{preset.version_no}"),
|
||||
)
|
||||
)
|
||||
return _write_details(
|
||||
|
||||
@ -89,6 +89,18 @@ def test_routes_are_separate_and_matrix_expands_manual_accounts() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_preset_routes_describe_rule_errors() -> None:
|
||||
preset_routes = [route for route in router.routes if "/intercompany-matrix/preset/{rule_id}" in route.path]
|
||||
assert {route.path for route in preset_routes} == {
|
||||
"/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}",
|
||||
"/consolidations/versions/{version_id}/intercompany-matrix/preset/{rule_id}/export",
|
||||
}
|
||||
for route in preset_routes:
|
||||
assert "правило" in route.responses[404]["description"]
|
||||
description = route.responses[422]["description"]
|
||||
assert all(code in description for code in ("INVALID_RULE", "RULE_REQUIRED", "ACCOUNTS_REQUIRED"))
|
||||
|
||||
|
||||
async def test_build_pairs_uses_supplied_rows_and_prebuilt_osv(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
version_id, rule_id, company_a, company_b = (uuid.uuid4() for _ in range(4))
|
||||
full_rows = [object()]
|
||||
@ -169,14 +181,15 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey
|
||||
assert info.mode == "PRESET"
|
||||
assert info.debit_account_codes == ("76",)
|
||||
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.preset is not None
|
||||
assert info.preset.debit_excluded_codes == ("76.01",)
|
||||
assert info.preset.credit_excluded_codes == ()
|
||||
assert info.preset.side_1 == "D"
|
||||
assert info.measure == "turnover"
|
||||
assert info.rule_no == rule.rule_no
|
||||
assert info.rule_version_no == rule.version_no
|
||||
assert info.article_1_line == rule.article_1_line
|
||||
assert info.article_2_line == rule.article_2_line
|
||||
assert info.preset.rule_no == rule.rule_no
|
||||
assert info.preset.version_no == rule.version_no
|
||||
assert info.preset.article_1_line == rule.article_1_line
|
||||
assert info.preset.article_2_line == rule.article_2_line
|
||||
|
||||
|
||||
async def test_preset_credit_first_uses_second_side_for_debit(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
@ -27,7 +27,7 @@ from src.schemas.consolidations import (
|
||||
IntercompanyResultArticleOut,
|
||||
IntercompanyResultTableOut,
|
||||
)
|
||||
from src.services.intercompany_matrix import MatrixExportInfo, ResultTableExportInfo
|
||||
from src.services.intercompany_matrix import MatrixExportInfo, PresetRuleDetails, ResultTableExportInfo
|
||||
from src.services.intercompany_matrix_export import (
|
||||
DIAGONAL_FILL,
|
||||
METRICS,
|
||||
@ -47,38 +47,58 @@ def _info(*, mode: str = "MANUAL", rule_id: UUID | None = None) -> MatrixExportI
|
||||
debit_account_codes=("62.01",) if mode != "ALL" else (),
|
||||
credit_account_codes=("60.01",) if mode != "ALL" else (),
|
||||
measure="turnover" if mode == "PRESET" else "closing",
|
||||
rule_id=rule_id,
|
||||
rule_no=6 if mode == "PRESET" else None,
|
||||
rule_version_no=3 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_2_line="1520" if mode == "PRESET" else None,
|
||||
rule_side_1="D" if mode == "PRESET" else None,
|
||||
preset=PresetRuleDetails(
|
||||
id=rule_id or UUID(int=0),
|
||||
rule_no=6,
|
||||
version_no=3,
|
||||
statement_type=StatementType.balance,
|
||||
article_1_line="1230",
|
||||
article_2_line="1520",
|
||||
side_1="D",
|
||||
)
|
||||
if mode == "PRESET"
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
def test_preset_header_shows_rule_codes_exclusions_and_credit_first_articles() -> None:
|
||||
def test_preset_header_shows_rule_6_codes_and_exclusions() -> None:
|
||||
matrix = IntercompanyMatrixOut(companies=[], cells=[])
|
||||
info = replace(
|
||||
_info(mode="PRESET"),
|
||||
debit_account_codes=("60.02", "76"),
|
||||
credit_account_codes=("60.01", "76"),
|
||||
debit_account_codes=("60.02", "60.22", "60.32", "62.01", "62.03", "62.21", "62.31", "76"),
|
||||
credit_account_codes=("60.01", "60.03", "60.21", "60.31", "62.02", "62.22", "62.32", "76"),
|
||||
preset=replace(
|
||||
_info(mode="PRESET").preset,
|
||||
debit_excluded_codes=("76.07.5", "76.07.9"),
|
||||
credit_excluded_codes=("76.07.1",),
|
||||
rule_side_1="K",
|
||||
side_1="D",
|
||||
),
|
||||
)
|
||||
|
||||
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"),
|
||||
("Счета дебета", "60.02, 60.22, 60.32, 62.01, 62.03, 62.21, 62.31, 76"),
|
||||
("кроме", "76.07.5, 76.07.9"),
|
||||
("Счета кредита", "60.01, 76"),
|
||||
("Счета кредита", "60.01, 60.03, 60.21, 60.31, 62.02, 62.22, 62.32, 76"),
|
||||
("кроме", "76.07.1"),
|
||||
("Показатель", "Оборот"),
|
||||
("Правило", "ББ № 6"),
|
||||
("Статья А", "1230"),
|
||||
("Статья Б", "1520"),
|
||||
("Версия правил", "v3"),
|
||||
]
|
||||
|
||||
|
||||
def test_preset_header_swaps_articles_for_credit_first_rule() -> None:
|
||||
matrix = IntercompanyMatrixOut(companies=[], cells=[])
|
||||
info = _info(mode="PRESET")
|
||||
assert info.preset is not None
|
||||
info = replace(info, preset=replace(info.preset, 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 (11, 12)] == [
|
||||
("Статья А", "1520"),
|
||||
("Статья Б", "1230"),
|
||||
("Версия правил", "v3"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -284,6 +284,14 @@ SHALL считать исходные строки ОСВ пары с `account_i
|
||||
правила, статьи А и Б по дебетовой и кредитовой сторонам соответственно (`article_1_line` / `article_2_line` при `side_1 = D`, наоборот при `side_1 = K`), фактическую версию правил и строку «Показатель» со значением «Сальдо» для `closing` или «Оборот» для `turnover`. Строка «Показатель» SHALL присутствовать и в выгрузках ALL и MANUAL (значение
|
||||
«Сальдо»).
|
||||
|
||||
#### Scenario: Правило № 6 с исключениями
|
||||
- **WHEN** пользователь выгружает пресет правила № 6 из seed-набора Прил. 3
|
||||
- **THEN** шапка показывает исходные коды счетов сторон правила, в отдельной строке дебета «кроме 76.07.5, 76.07.9», а в отдельной строке кредита — «кроме 76.07.1»; раскрытые субсчета счёта 76 в шапку не добавляются
|
||||
|
||||
#### Scenario: Кредитная первая сторона правила
|
||||
- **WHEN** пользователь выгружает пресет с `side_1 = K`
|
||||
- **THEN** «Статья А» содержит `article_2_line`, «Статья Б» — `article_1_line`
|
||||
|
||||
#### Scenario: Выгрузка по оборотам
|
||||
- **WHEN** пользователь выгружает пресет с `measure = turnover`
|
||||
- **THEN** шапка содержит «Режим — Пресет», «Показатель — Оборот», номер, статьи и версию правила; имя файла заканчивается на `_rule-balance-<rule_no>.xlsx`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user