"""Account hierarchy walker. Given a flat list of accounts with parent_id pointers, build a tree and provide a recursive walker that yields (account, depth, ancestors) tuples. Used by report line resolvers to render group sub-totals.""" from dataclasses import dataclass, field from typing import Iterator @dataclass class AccountNode: id: int code: str name: str account_type: str parent_id: int | None children: list['AccountNode'] = field(default_factory=list) def build_tree(accounts: list[dict]) -> list[AccountNode]: """Build a forest from a flat list of account dicts. Each dict must have keys: id, code, name, account_type, parent_id (nullable).""" nodes: dict[int, AccountNode] = {} for acc in accounts: nodes[acc['id']] = AccountNode( id=acc['id'], code=acc['code'], name=acc['name'], account_type=acc['account_type'], parent_id=acc.get('parent_id'), ) roots: list[AccountNode] = [] for node in nodes.values(): if node.parent_id and node.parent_id in nodes: nodes[node.parent_id].children.append(node) else: roots.append(node) for node in nodes.values(): node.children.sort(key=lambda n: n.code) roots.sort(key=lambda n: n.code) return roots def walk(roots: list[AccountNode], *, max_depth: int = 10) -> Iterator[tuple[AccountNode, int, list[AccountNode]]]: """Depth-first walk yielding (node, depth, ancestors).""" def _walk(node: AccountNode, depth: int, ancestors: list[AccountNode]): yield (node, depth, ancestors) if depth < max_depth: for child in node.children: yield from _walk(child, depth + 1, ancestors + [node]) for root in roots: yield from _walk(root, 0, []) def filter_by_account_type(roots: list[AccountNode], type_prefix: str) -> list[AccountNode]: """Return all nodes whose account_type starts with type_prefix (e.g. 'asset_' returns asset_receivable, asset_cash, etc.).""" matches: list[AccountNode] = [] for node, _depth, _ancestors in walk(roots): if node.account_type.startswith(type_prefix): matches.append(node) return matches