Three service modules with no Odoo dependencies: - date_periods: fiscal year/month/quarter bounds + comparison derivation - account_hierarchy: parent-child tree walker with type filtering - totaling: move-line aggregation primitives 18 unit tests covering edge cases (December rollover, Feb 29, fiscal- year-before-start, balance check tolerance). Made-with: Cursor
63 lines
2.2 KiB
Python
63 lines
2.2 KiB
Python
"""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
|