diff --git a/api/src/api/endpoints/consolidations/intercompany_matrix.py b/api/src/api/endpoints/consolidations/intercompany_matrix.py index eb10774..96f61e7 100644 --- a/api/src/api/endpoints/consolidations/intercompany_matrix.py +++ b/api/src/api/endpoints/consolidations/intercompany_matrix.py @@ -180,7 +180,7 @@ async def get_unpaired_osv( service: IntercompanyMatrixServiceDep, ) -> UnpairedOsvOut: """Все строки односторонних ячеек шахматки и их число.""" - return await service.unpaired_osv(version_id, rule_id) + return await service.get_unpaired_osv(version_id, rule_id) @router.get( diff --git a/api/src/repositories/elimination_rules.py b/api/src/repositories/elimination_rules.py index 1dc5dbc..476d132 100644 --- a/api/src/repositories/elimination_rules.py +++ b/api/src/repositories/elimination_rules.py @@ -1,56 +1,39 @@ """Чтение версионированных правил элиминации и счетов их сторон.""" -import datetime import uuid from collections.abc import Collection -from sqlalchemy import func, or_, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from src.models.accounting import ChartOfAccount, EliminationRule, EliminationRuleAccount async def list_rules_with_accounts( - session: AsyncSession, version_no: int, *, as_of: datetime.date | None = None + session: AsyncSession, version_no: int | None ) -> list[tuple[EliminationRule, EliminationRuleAccount | None]]: - """Правила выбранной версии и счета сторон; для текущей версии — только действующие на дату.""" + """Правила заданной версии либо все правила без `valid_to`.""" + condition = ( + EliminationRule.version_no == version_no + if version_no is not None + else EliminationRule.valid_to.is_(None) + ) stmt = ( select(EliminationRule, EliminationRuleAccount) .outerjoin(EliminationRuleAccount, EliminationRuleAccount.rule_id == EliminationRule.id) - .where(EliminationRule.version_no == version_no) + .where(condition) .order_by( EliminationRule.statement_type, EliminationRule.rule_no, EliminationRuleAccount.company_no, ) ) - if as_of is not None: - stmt = stmt.where( - EliminationRule.valid_from <= as_of, - or_(EliminationRule.valid_to.is_(None), EliminationRule.valid_to >= as_of), - ) return [(rule, account) for rule, account in (await session.execute(stmt)).all()] -async def current_version_no(session: AsyncSession, as_of: datetime.date) -> int | None: - """Старшая версия правил, у которой есть действующие на дату записи.""" - stmt = select(func.max(EliminationRule.version_no)).where( - EliminationRule.valid_from <= as_of, - or_(EliminationRule.valid_to.is_(None), EliminationRule.valid_to >= as_of), - ) - return await session.scalar(stmt) - - -def is_valid_for_version( - rule: EliminationRule | None, version_no: int | None, *, as_of: datetime.date | None = None -) -> bool: - """Проверить принадлежность правила версии и, если нужно, срок его действия.""" - return ( - rule is not None - and version_no is not None - and rule.version_no == version_no - and (as_of is None or (rule.valid_from <= as_of and (rule.valid_to is None or rule.valid_to >= as_of))) - ) +def is_valid_for_version(rule: EliminationRule | None, version_no: int | None) -> bool: + """Проверить номер версии либо отсутствие `valid_to`.""" + return rule is not None and (rule.version_no == version_no if version_no is not None else rule.valid_to is None) async def list_rules(session: AsyncSession, version_no: int) -> list[EliminationRule]: diff --git a/api/src/services/elimination_rules.py b/api/src/services/elimination_rules.py index 3ba8e45..be5608b 100644 --- a/api/src/services/elimination_rules.py +++ b/api/src/services/elimination_rules.py @@ -4,7 +4,6 @@ import uuid from sqlalchemy.ext.asyncio import AsyncSession -from src.common.clock import moscow_date, utc_now from src.common.exceptions import NotFoundError from src.domain.policy import VisibilityScope from src.repositories import consolidations as consolidations_repo @@ -27,17 +26,8 @@ class EliminationRuleService: raise NotFoundError(VERSION_NOT_FOUND) version, _ = found is_version_fixed = version.elimination_rules_version is not None - if is_version_fixed: - as_of = None - version_no = version.elimination_rules_version - else: - as_of = moscow_date(utc_now()) - version_no = await rules_repo.current_version_no(self._session, as_of) - rows = ( - await rules_repo.list_rules_with_accounts(self._session, version_no, as_of=as_of) - if version_no is not None - else [] - ) + version_no = version.elimination_rules_version + rows = await rules_repo.list_rules_with_accounts(self._session, version_no) by_id: dict[uuid.UUID, EliminationRuleOut] = {} for rule, account in rows: result = by_id.get(rule.id) @@ -64,6 +54,6 @@ class EliminationRuleService: return EliminationRulesOut( version_no=version_no, is_version_fixed=is_version_fixed, - as_of=as_of, + as_of=None, rules=list(by_id.values()), ) diff --git a/api/src/services/intercompany_matrix.py b/api/src/services/intercompany_matrix.py index 181b6c6..c28c3cc 100644 --- a/api/src/services/intercompany_matrix.py +++ b/api/src/services/intercompany_matrix.py @@ -10,7 +10,6 @@ from typing import Literal from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from src.common.clock import moscow_date, utc_now from src.common.exceptions import DomainValidationError, NotFoundError from src.domain.consolidated_osv import ConsolidatedRow, Entity from src.domain.policy import VisibilityScope @@ -79,6 +78,18 @@ class MatrixSelection: measure: Measure +@dataclass(frozen=True) +class MatrixBuildResult: + """Матрица и данные свода, использованные при её построении.""" + + matrix: IntercompanyMatrixOut + version: ConsolidationVersion + consolidation: Consolidation + accounts: list[ChartOfAccount] + selection: MatrixSelection + rows: tuple[ConsolidatedRow, ...] + + @dataclass(frozen=True) class MatrixExportInfo: group_name: str @@ -97,7 +108,7 @@ class MatrixExportInfo: return f"intercompany-matrix_{self.group_name}_{self.year}-{self.quarter.value}_v{self.version_no}{preset}.xlsx" -def _amount(row: ConsolidatedRow, measure: Measure, side: Side) -> Decimal: +def _get_amount(row: ConsolidatedRow, measure: Measure, side: Side) -> Decimal: if measure == "closing": value = row.closing_debit if side == "D" else row.closing_credit else: @@ -136,11 +147,11 @@ def calculate_matrix( continue if row.account_id in selection.company_1_account_ids: item = amounts_1[pair] - item["amount"] += _amount(row, selection.measure, "D") + item["amount"] += _get_amount(row, selection.measure, "D") item["entry_osv_ids"].update(row.entry_ids) if row.account_id in selection.company_2_account_ids: item = amounts_2[pair] - item["amount"] += _amount(row, selection.measure, "K") + item["amount"] += _get_amount(row, selection.measure, "K") item["entry_osv_ids"].update(row.entry_ids) cells: list[IntercompanyMatrixCellOut] = [] @@ -179,7 +190,7 @@ def calculate_matrix( return IntercompanyMatrixOut(companies=list(company_info), cells=cells) -def unpaired_rows(rows: Iterable[ConsolidatedRow], matrix: IntercompanyMatrixOut) -> list[ConsolidatedRow]: +def get_unpaired_rows(rows: Iterable[ConsolidatedRow], matrix: IntercompanyMatrixOut) -> list[ConsolidatedRow]: """Строки свода, попавшие только на одну сторону пары компаний в шахматке.""" entry_ids = { entry_id @@ -190,7 +201,7 @@ def unpaired_rows(rows: Iterable[ConsolidatedRow], matrix: IntercompanyMatrixOut return [row for row in rows if entry_ids.intersection(row.entry_ids)] -def _account_tree( +def _make_account_tree( accounts: Sequence[ChartOfAccount], ) -> tuple[dict[uuid.UUID, ChartOfAccount], dict[uuid.UUID, list[uuid.UUID]]]: by_id = {account.id: account for account in accounts} @@ -213,11 +224,11 @@ def _expand(roots: Iterable[uuid.UUID], children: dict[uuid.UUID, list[uuid.UUID return result -def _kinds(side: Side) -> frozenset[AccountKind]: +def _get_kinds(side: Side) -> frozenset[AccountKind]: return DEBIT_KINDS if side == "D" else CREDIT_KINDS -def _selected_accounts( +def _select_accounts( included: Iterable[uuid.UUID], excluded: Iterable[uuid.UUID], side: Side, @@ -229,10 +240,10 @@ def _selected_accounts( if missing: missing_ids = ", ".join(map(str, sorted(missing))) raise DomainValidationError("ACCOUNT_NOT_FOUND", f"Выбранного счёта нет ({missing_ids})") - if any(by_id[account_id].kind not in _kinds(side) for account_id in roots): + if any(by_id[account_id].kind not in _get_kinds(side) for account_id in roots): raise DomainValidationError("ACCOUNT_KIND_MISMATCH", "Сторона счёта не соответствует направлению расчёта") selected = _expand(roots, children) - _expand(excluded, children) - return frozenset(account_id for account_id in selected if by_id[account_id].kind in _kinds(side)) + return frozenset(account_id for account_id in selected if by_id[account_id].kind in _get_kinds(side)) def calculate_result_table( @@ -241,7 +252,7 @@ def calculate_result_table( accounts: Sequence[ChartOfAccount], ) -> IntercompanyResultTableOut: """Фиксированные статьи ВГО: сальдо по всем контрагентам периметра для каждого ЮЛ.""" - by_id, children = _account_tree(accounts) + by_id, children = _make_account_tree(accounts) by_code = {account.code: account.id for account in accounts} required_codes = {code for article in RESULT_ARTICLES for code in (*article.account_codes, *article.excluded_codes)} missing = sorted(required_codes - by_code.keys()) @@ -249,7 +260,7 @@ def calculate_result_table( raise DomainValidationError("RESULT_ACCOUNTS_MISSING", f"В плане счетов нет счетов: {', '.join(missing)}") selected = { - article.code: _selected_accounts( + article.code: _select_accounts( (by_code[code] for code in article.account_codes), (by_code[code] for code in article.excluded_codes), article.side, @@ -273,7 +284,7 @@ def calculate_result_table( continue for article in RESULT_ARTICLES: if row.account_id in selected[article.code]: - amounts[article.code][row.legal_entity_id] += _amount(row, "closing", article.side) + amounts[article.code][row.legal_entity_id] += _get_amount(row, "closing", article.side) articles: list[IntercompanyResultArticleOut] = [] for article in RESULT_ARTICLES: @@ -332,15 +343,19 @@ class IntercompanyMatrixService: credit_account_ids: Sequence[uuid.UUID] = (), rule_id: uuid.UUID | None = None, ) -> IntercompanyMatrixOut: - """Рассчитать матрицу по всем счетам, ручному выбору или правилу-пресету.""" - matrix, _, _, _, _, _ = await self._build_accounts( + """Построить шахматку по выбранному режиму. + + ALL включает все счета соответствующей стороны; MANUAL — выбранные счета и их потомков; + PRESET берёт счета и показатель из правила с идентификатором `rule_id`. + """ + result = await self._build_accounts( version_id, mode=mode, debit_account_ids=debit_account_ids, credit_account_ids=credit_account_ids, rule_id=rule_id, ) - return matrix + return result.matrix async def build_export( self, @@ -352,7 +367,7 @@ class IntercompanyMatrixService: rule_id: uuid.UUID | None = None, ) -> tuple[IntercompanyMatrixOut, MatrixExportInfo]: """Матрица и реквизиты книги из одного расчёта свода.""" - matrix, version, consolidation, accounts, selection, _ = await self._build_accounts( + result = await self._build_accounts( version_id, mode=mode, debit_account_ids=debit_account_ids, @@ -361,35 +376,35 @@ class IntercompanyMatrixService: ) group_name = ( await self._session.execute( - select(CompanyGroup.name).where(CompanyGroup.id == consolidation.company_group_id) + select(CompanyGroup.name).where(CompanyGroup.id == result.consolidation.company_group_id) ) ).scalar_one() - by_id = {account.id: account.code for account in accounts} + 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 selection.company_1_account_ids)) - credit_codes = tuple(sorted(by_id[account_id] for account_id in selection.company_2_account_ids)) + 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)) 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) info = MatrixExportInfo( group_name=group_name, - year=consolidation.year, - quarter=consolidation.quarter, - version_no=version.version_no, + year=result.consolidation.year, + quarter=result.consolidation.quarter, + version_no=result.version.version_no, mode=mode, debit_account_codes=debit_codes, credit_account_codes=credit_codes, - measure=selection.measure, + measure=result.selection.measure, rule_id=rule_id, ) - return matrix, info + return result.matrix, info - async def unpaired_osv(self, version_id: uuid.UUID, rule_id: uuid.UUID) -> UnpairedOsvOut: + async def get_unpaired_osv(self, version_id: uuid.UUID, rule_id: uuid.UUID) -> UnpairedOsvOut: """Все строки свода без встречной стороны в шахматке заданного пресета.""" - matrix, _, _, _, _, rows = await self._build_accounts( + result = await self._build_accounts( version_id, mode="PRESET", debit_account_ids=(), credit_account_ids=(), rule_id=rule_id ) - unmatched = unpaired_rows(rows, matrix) + unmatched = get_unpaired_rows(result.rows, result.matrix) return UnpairedOsvOut(items=[row_out(row) for row in unmatched], total=len(unmatched)) async def _apply_corrections( @@ -449,7 +464,44 @@ class IntercompanyMatrixService: ) return IntercompanyMatrixOut(companies=matrix.companies, cells=cells) - async def _build_accounts( # noqa: PLR0912 - три режима выбора счетов и проверка пресета + async def _resolve_preset_selection( + self, + rule_id: uuid.UUID | None, + version: ConsolidationVersion, + by_id: dict[uuid.UUID, ChartOfAccount], + children: dict[uuid.UUID, list[uuid.UUID]], + ) -> tuple[MatrixSelection, EliminationRule]: + """Выбрать счета и показатель пресета для версии консолидации.""" + if rule_id is None: + raise DomainValidationError("RULE_REQUIRED", "Выберите правило-пресет") + rule, rule_accounts = await elimination_rules_repo.rule_with_account(self._session, rule_id) + if rule is None or not elimination_rules_repo.is_valid_for_version(rule, version.elimination_rules_version): + raise NotFoundError("правило-пресет не найдено для версии консолидации") + if rule.side_1 not in ("D", "K") or rule.measure not in ("closing", "turnover"): + raise DomainValidationError("INVALID_RULE", "Неверное направление или показатель правила-пресета") + + sides: dict[int, tuple[list[uuid.UUID], list[uuid.UUID]]] = { + 1: ([], []), + 2: ([], []), + } + for rule_account in rule_accounts: + if rule_account.company_no not in sides: + raise DomainValidationError("INVALID_RULE", "Неверная сторона счёта в правиле-пресете") + included, excluded = sides[rule_account.company_no] + (excluded if rule_account.is_excluded else included).append(rule_account.account_id) + debit_side = 1 if rule.side_1 == "D" else 2 + credit_side = 2 if debit_side == 1 else 1 + debit_ids, debit_excluded = sides[debit_side] + credit_ids, credit_excluded = sides[credit_side] + if not debit_ids or not credit_ids: + raise DomainValidationError("ACCOUNTS_REQUIRED", "В правиле-пресете нужны счета обеих сторон") + debit = _select_accounts(debit_ids, debit_excluded, "D", by_id, children) + credit = _select_accounts(credit_ids, credit_excluded, "K", by_id, children) + measure = "closing" if rule.measure == "closing" else "turnover" + + return MatrixSelection(debit, credit, measure), rule + + async def _build_accounts( self, version_id: uuid.UUID, *, @@ -457,67 +509,36 @@ class IntercompanyMatrixService: debit_account_ids: Sequence[uuid.UUID], credit_account_ids: Sequence[uuid.UUID], rule_id: uuid.UUID | None = None, - ) -> tuple[ - IntercompanyMatrixOut, - ConsolidationVersion, - Consolidation, - list[ChartOfAccount], - MatrixSelection, - tuple[ConsolidatedRow, ...], - ]: + ) -> MatrixBuildResult: consolidated, version, consolidation, members = await self._osv_service.build_with_members(version_id) rows = consolidated.rows accounts = await directory_repo.list_accounts(self._session) - by_id, children = _account_tree(accounts) - measure: Measure = "closing" + by_id, children = _make_account_tree(accounts) rule: EliminationRule | None = None 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 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: raise DomainValidationError("ACCOUNTS_REQUIRED", "Выберите дебетовые и кредитовые счета") - debit = _selected_accounts(debit_account_ids, (), "D", by_id, children) - credit = _selected_accounts(credit_account_ids, (), "K", by_id, children) + debit = _select_accounts(debit_account_ids, (), "D", by_id, children) + credit = _select_accounts(credit_account_ids, (), "K", by_id, children) + selection = MatrixSelection(debit, credit, "closing") elif mode == "PRESET": - if rule_id is None: - raise DomainValidationError("RULE_REQUIRED", "Выберите правило-пресет") - rule, rule_accounts = await elimination_rules_repo.rule_with_account(self._session, rule_id) - as_of = None if version.elimination_rules_version is not None else moscow_date(utc_now()) - version_no = ( - version.elimination_rules_version - if as_of is None - else await elimination_rules_repo.current_version_no(self._session, as_of) - ) - if rule is None or not elimination_rules_repo.is_valid_for_version(rule, version_no, as_of=as_of): - raise NotFoundError("правило-пресет не найдено для версии консолидации") - if rule.side_1 not in ("D", "K") or rule.measure not in ("closing", "turnover"): - raise DomainValidationError("INVALID_RULE", "Неверное направление или показатель правила-пресета") - - sides: dict[int, tuple[list[uuid.UUID], list[uuid.UUID]]] = { - 1: ([], []), - 2: ([], []), - } - for rule_account in rule_accounts: - if rule_account.company_no not in sides: - raise DomainValidationError("INVALID_RULE", "Неверная сторона счёта в правиле-пресете") - included, excluded = sides[rule_account.company_no] - (excluded if rule_account.is_excluded else included).append(rule_account.account_id) - debit_side = 1 if rule.side_1 == "D" else 2 - credit_side = 2 if debit_side == 1 else 1 - debit_ids, debit_excluded = sides[debit_side] - credit_ids, credit_excluded = sides[credit_side] - if not debit_ids or not credit_ids: - raise DomainValidationError("ACCOUNTS_REQUIRED", "В правиле-пресете нужны счета обеих сторон") - debit = _selected_accounts(debit_ids, debit_excluded, "D", by_id, children) - credit = _selected_accounts(credit_ids, credit_excluded, "K", by_id, children) - measure = "closing" if rule.measure == "closing" else "turnover" + selection, rule = await self._resolve_preset_selection(rule_id, version, by_id, children) else: raise DomainValidationError("INVALID_MODE", "Неизвестный режим выбора счетов") - selection = MatrixSelection(debit, credit, measure) matrix = self._calculate(rows, members, selection) if rule is not None: matrix = await self._apply_corrections(matrix, version_id, rule.id) - return matrix, version, consolidation, accounts, selection, rows + return MatrixBuildResult( + matrix=matrix, + version=version, + consolidation=consolidation, + accounts=accounts, + selection=selection, + rows=rows, + ) diff --git a/api/tests/test_elimination_rules.py b/api/tests/test_elimination_rules.py index 6ac4755..77504be 100644 --- a/api/tests/test_elimination_rules.py +++ b/api/tests/test_elimination_rules.py @@ -1,6 +1,5 @@ """Выбор версии правил для шахматки без обращения к БД.""" -import datetime import uuid from types import SimpleNamespace from unittest.mock import AsyncMock @@ -24,39 +23,35 @@ def _rule() -> SimpleNamespace: @pytest.mark.parametrize( - ("rule", "version_no", "as_of", "expected"), + ("rule", "version_no", "expected"), [ - (None, 3, None, False), - (SimpleNamespace(version_no=3), None, None, False), - (SimpleNamespace(version_no=3), 2, None, False), - (SimpleNamespace(version_no=3), 3, None, True), - ( - SimpleNamespace(version_no=3, valid_from=datetime.date(2024, 1, 1), valid_to=datetime.date(2024, 12, 31)), - 3, - datetime.date(2023, 12, 31), - False, - ), - ( - SimpleNamespace(version_no=3, valid_from=datetime.date(2024, 1, 1), valid_to=datetime.date(2024, 12, 31)), - 3, - datetime.date(2024, 12, 31), - True, - ), - ( - SimpleNamespace(version_no=3, valid_from=datetime.date(2024, 1, 1), valid_to=datetime.date(2024, 12, 31)), - 3, - datetime.date(2025, 1, 1), - False, - ), + (None, 3, False), + (SimpleNamespace(version_no=3, valid_to=None), None, True), + (SimpleNamespace(version_no=3, valid_to="2026-12-31"), None, False), + (SimpleNamespace(version_no=3), 2, False), + (SimpleNamespace(version_no=3), 3, True), + (SimpleNamespace(version_no=3, valid_to="2026-12-31"), 3, True), ], ) def test_rule_is_valid_for_version( - rule: SimpleNamespace | None, version_no: int | None, as_of: datetime.date | None, expected: bool + rule: SimpleNamespace | None, version_no: int | None, expected: bool ) -> None: - assert rules_repo.is_valid_for_version(rule, version_no, as_of=as_of) is expected + assert rules_repo.is_valid_for_version(rule, version_no) is expected -async def test_fixed_rules_version_ignores_current_date(monkeypatch: pytest.MonkeyPatch) -> None: +async def test_rules_filter_by_version_or_open_valid_to() -> None: + version_no = 3 + session = SimpleNamespace(execute=AsyncMock(return_value=SimpleNamespace(all=lambda: []))) + + await rules_repo.list_rules_with_accounts(session, None) + assert "valid_to IS NULL" in str(session.execute.await_args.args[0]) + + await rules_repo.list_rules_with_accounts(session, version_no) + assert "valid_to IS NULL" not in str(session.execute.await_args.args[0]) + assert "version_no =" in str(session.execute.await_args.args[0]) + + +async def test_fixed_rules_use_version_number(monkeypatch: pytest.MonkeyPatch) -> None: session, scope = object(), object() version_id = uuid.uuid4() rule = _rule() @@ -67,9 +62,7 @@ async def test_fixed_rules_version_ignores_current_date(monkeypatch: pytest.Monk "get_version", AsyncMock(return_value=(SimpleNamespace(elimination_rules_version=rule.version_no), None)), ) - current = AsyncMock() listed = AsyncMock(return_value=[(rule, account)]) - monkeypatch.setattr(service_module.rules_repo, "current_version_no", current) monkeypatch.setattr(service_module.rules_repo, "list_rules_with_accounts", listed) result = await service_module.EliminationRuleService(session, scope).list_rules(version_id) @@ -79,34 +72,28 @@ async def test_fixed_rules_version_ignores_current_date(monkeypatch: pytest.Monk assert result.as_of is None assert [item.id for item in result.rules] == [rule.id] assert result.rules[0].company_1.accounts == [account_id] - current.assert_not_awaited() - listed.assert_awaited_once_with(session, rule.version_no, as_of=None) + listed.assert_awaited_once_with(session, rule.version_no) -async def test_unfixed_rules_use_latest_active_version_on_moscow_date(monkeypatch: pytest.MonkeyPatch) -> None: +async def test_unfixed_rules_use_open_valid_to(monkeypatch: pytest.MonkeyPatch) -> None: session, scope = object(), object() version_id = uuid.uuid4() rule = _rule() - expected_date = datetime.date(2026, 10, 19) - monkeypatch.setattr(service_module, "utc_now", lambda: datetime.datetime(2026, 10, 18, 22, tzinfo=datetime.UTC)) monkeypatch.setattr( service_module.consolidations_repo, "get_version", AsyncMock(return_value=(SimpleNamespace(elimination_rules_version=None), None)), ) - current = AsyncMock(return_value=rule.version_no) listed = AsyncMock(return_value=[(rule, None)]) - monkeypatch.setattr(service_module.rules_repo, "current_version_no", current) monkeypatch.setattr(service_module.rules_repo, "list_rules_with_accounts", listed) result = await service_module.EliminationRuleService(session, scope).list_rules(version_id) assert result.is_version_fixed is False - assert result.version_no == rule.version_no - assert result.as_of == expected_date + assert result.version_no is None + assert result.as_of is None assert [item.id for item in result.rules] == [rule.id] - current.assert_awaited_once_with(session, expected_date) - listed.assert_awaited_once_with(session, rule.version_no, as_of=expected_date) + listed.assert_awaited_once_with(session, None) async def test_no_current_rules_returns_empty_with_unfixed_flag(monkeypatch: pytest.MonkeyPatch) -> None: @@ -116,14 +103,13 @@ async def test_no_current_rules_returns_empty_with_unfixed_flag(monkeypatch: pyt "get_version", AsyncMock(return_value=(SimpleNamespace(elimination_rules_version=None), None)), ) - monkeypatch.setattr(service_module.rules_repo, "current_version_no", AsyncMock(return_value=None)) - listed = AsyncMock() + listed = AsyncMock(return_value=[]) monkeypatch.setattr(service_module.rules_repo, "list_rules_with_accounts", listed) result = await service_module.EliminationRuleService(session, scope).list_rules(uuid.uuid4()) assert result.is_version_fixed is False assert result.version_no is None - assert result.as_of is not None + assert result.as_of is None assert result.rules == [] - listed.assert_not_awaited() + listed.assert_awaited_once_with(session, None) diff --git a/api/tests/test_intercompany_matrix.py b/api/tests/test_intercompany_matrix.py index 0f2dd8b..efdd2eb 100644 --- a/api/tests/test_intercompany_matrix.py +++ b/api/tests/test_intercompany_matrix.py @@ -58,9 +58,9 @@ def test_routes_are_separate_and_matrix_expands_manual_accounts() -> None: debit_child = _account(AccountKind.A, debit_parent.id) credit_parent = _account(AccountKind.P) credit_child = _account(AccountKind.P, credit_parent.id) - by_id, children = matrix._account_tree([debit_parent, debit_child, credit_parent, credit_child]) - debit = matrix._selected_accounts([debit_parent.id], [], "D", by_id, children) - credit = matrix._selected_accounts([credit_parent.id], [], "K", by_id, children) + by_id, children = matrix._make_account_tree([debit_parent, debit_child, credit_parent, credit_child]) + debit = matrix._select_accounts([debit_parent.id], [], "D", by_id, children) + credit = matrix._select_accounts([credit_parent.id], [], "K", by_id, children) assert debit == {debit_parent.id, debit_child.id} assert credit == {credit_parent.id, credit_child.id} @@ -109,7 +109,7 @@ def test_unpaired_rows_uses_source_presence_and_preserves_osv_order() -> None: rows, companies, matrix.MatrixSelection(frozenset({debit_account}), frozenset({credit_account}), "closing") ) - assert matrix.unpaired_rows(rows, result) == [unpaired_credit] + assert matrix.get_unpaired_rows(rows, result) == [unpaired_credit] async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkeypatch: pytest.MonkeyPatch) -> None: @@ -152,15 +152,12 @@ async def test_preset_applies_exclusions_to_its_own_side_and_rule_measure(monkey assert captured[0].company_2_account_ids == {parent.id, child.id} assert captured[0].measure == "turnover" - current_version = AsyncMock(return_value=1) - monkeypatch.setattr(matrix.elimination_rules_repo, "current_version_no", current_version) version.elimination_rules_version = None _, 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.measure == "turnover" - current_version.assert_awaited_once() async def test_preset_corrections_exclude_amounts_without_changing_osv(monkeypatch: pytest.MonkeyPatch) -> None: