Skip to content

Commit

Permalink
collapse Chart to CD
Browse files Browse the repository at this point in the history
  • Loading branch information
epogrebnyak committed Sep 24, 2024
1 parent 7e4867a commit 613bbbb
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 179 deletions.
155 changes: 39 additions & 116 deletions playground/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
from collections import UserDict
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, Iterator, Sequence, Type
from typing import Callable, Iterator, Sequence, Type, Set

from pydantic import BaseModel

Expand Down Expand Up @@ -106,74 +106,42 @@ def side(self) -> Side:
return Side.Credit


class Definition:
"""Base class for Regular, Contra and Just classes:
Regular(T5.Asset)
Contra("equity")
Just(DebitorCreditAccount)
"""


@dataclass
class Regular(Definition):
"""Regular account of any of five types of accounts."""

t: T5


@dataclass
class Contra(Definition):
"""A contra account to an existing account."""
class CD:
income_summary_account: str
retained_earnings_account: str
ledger_dict: dict[str, Type["TAccount"]] = field(default_factory=dict)
accounts: dict[str, T5] = field(default_factory=dict)
contra_accounts: dict[str, str] = field(default_factory=dict)

linked_to: AccountName


@dataclass
class Just(Definition):
taccount_class: Type["TAccount"]
def __post_init__(self):
self._set_isa(self.income_summary_account)
self._set_re(self.retained_earnings_account)

def set(self, t: T5, name: str):
self.ledger_dict[name] = t.side.taccount
self.accounts[name] = t

class ChartDict(UserDict[str, Definition]):
def _set_isa(self, name: str):
self.ledger_dict[name] = DebitOrCreditAccount

def set(self, t: T5, name: str):
"""Add regular account to chart."""
self.data[name] = Regular(t)
return self
def _set_re(self, name: str):
self.set(T5.Capital, name)

def offset(self, existing_name: str, contra_name: str):
"""Add contra account to chart."""
if existing_name not in self.data.keys():
if existing_name not in self.accounts:
raise AbacusError(f"Account name {existing_name} not found in chart.")
self.data[contra_name] = Contra(existing_name)
return self

def just(self, taccount_class: Type["TAccount"], name: str):
"""Add any account type to chart."""
self.data[name] = Just(taccount_class)
return self

def taccount(self, definition) -> "TAccount":
"""Decide what kind of T-account to create for the definition."""
match definition:
case Regular(t):
return t.side.taccount()
case Contra(linked_to):
return self[linked_to].t.side.reverse().taccount() # type: ignore
case Just(taccount_class):
return taccount_class()
case _:
raise AbacusError(f"Unknown account definition: {definition}")
self.ledger_dict[contra_name] = (
self.accounts[existing_name].side.reverse().taccount
)
self.contra_accounts[contra_name] = existing_name

def ledger(self) -> "Ledger":
return Ledger(
{name: self.taccount(definition) for name, definition in self.items()}
)
return Ledger({name: taccount() for name, taccount in self.ledger_dict.items()})

@property
def closing_pairs(
self,
income_summary_account: AccountName,
retained_earnings_account: AccountName,
) -> Iterator["Pair"]:
"""Return closing pairs for accounting period end."""
for t in T5.Income, T5.Expense:
Expand All @@ -184,81 +152,36 @@ def closing_pairs(

# 2. Close income and expense accounts to income summary account.
for name in self.by_type(t):
yield name, income_summary_account
yield name, self.income_summary_account

# 3. Close income summary account to retained earnings account.
yield income_summary_account, retained_earnings_account

def set_re(self, name: str):
"""Set retained earnings account in chart."""
# Retained earnings account is already in chart, do nothing
if self.get(name) == Regular(T5.Capital):
return self
# Retained earnings account is in chart but not a capital account, raise error
elif name in self.keys():
raise AbacusError(f"{name} already in chart and not a capital account.")
# Retained earnings account not in chart, set it to capital account
else:
self.set(T5.Capital, name)
return self

def set_isa(self, name: str):
"""Set income summary account in chart."""
self.just(DebitOrCreditAccount, name)
return self
yield self.income_summary_account, self.retained_earnings_account

def by_type(self, t: T5) -> list[AccountName]:
"""Return account names for a given account type."""
return [
name
for name, definition in self.items()
if isinstance(definition, Regular) and definition.t == t
]
return [name for name, _t in self.accounts.items() if _t == t]

def find_contra_accounts(self, name: AccountName) -> list[AccountName]:
"""Find contra accounts for a given account name."""
return [
_name
for _name, definition in self.items()
if isinstance(definition, Contra) and definition.linked_to == name
contra_name
for contra_name, _name in self.contra_accounts.items()
if _name == name
]

def qualify(self, income_summary_account, retained_earnings_account) -> "Chart":
"""Create chart with closing pairs."""
self.set_isa(isa := income_summary_account)
self.set_re(re := retained_earnings_account)
pairs = list(self.closing_pairs(isa, re))
return Chart(self, pairs)


@dataclass
class Chart:
"""Chart of accounts with closing pairs of accounts for the accounting period end."""

accounts: ChartDict
closing_pairs: list[Pair]

@property
def temporary_accounts(self) -> set[AccountName]:
def temporary_accounts(self) -> Set[str]:
"""Return temporary account names."""
return set(name for name, _ in self.closing_pairs)

def net_balances_factory(self, ledger) -> Callable[[T5], dict[AccountName, Amount]]:
"""Return a function that calculates net balances for a given account type."""

return lambda t: {
name: ledger.net_balance(name, self.accounts.find_contra_accounts(name))
for name in self.accounts.by_type(t)
name: ledger.net_balance(name, self.find_contra_accounts(name))
for name in self.by_type(t)
}

def validate(self):
"""Raise error if chart dictionary or closing pairs are not valid."""
try:
self.accounts.ledger().close_by_pairs(self.closing_pairs)
except KeyError as e:
s = str(e).replace("'", "")
raise AbacusError(s)


@dataclass
class SingleEntry(ABC):
Expand Down Expand Up @@ -433,9 +356,9 @@ def side(self) -> Side:
class Ledger(UserDict[AccountName, TAccount]):

@classmethod
def new(_, chart: Chart) -> "Ledger":
def new(_, chart: CD) -> "Ledger":
"""Create ledger from chart."""
return chart.accounts.ledger()
return chart.ledger()

def copy(self):
return Ledger({name: account.copy() for name, account in self.items()})
Expand Down Expand Up @@ -484,7 +407,7 @@ def net_balance(self, name: AccountName, contra_names: list[AccountName]) -> Amo
self[contra_name].balance for contra_name in contra_names
)

def close_by_pairs(self, pairs: Sequence[Pair]):
def close_by_pairs(self, pairs: Iterator[Pair]):
"""Close ledger by using closing pairs of accounts."""
closing_entries = []
for from_, to_ in pairs:
Expand All @@ -494,7 +417,7 @@ def close_by_pairs(self, pairs: Sequence[Pair]):
del self.data[from_]
return closing_entries

def close(self, chart: Chart) -> list[Entry]:
def close(self, chart: CD) -> list[Entry]:
"""Close ledger at accounting period end."""
return self.close_by_pairs(chart.closing_pairs)

Expand Down Expand Up @@ -530,7 +453,7 @@ class IncomeStatement(Report):
expenses: dict[AccountName, Amount]

@classmethod
def new(cls, ledger: Ledger, chart: Chart):
def new(cls, ledger: Ledger, chart: CD):
"""Create income statement from ledger and chart."""
fill = chart.net_balances_factory(ledger)
return cls(income=fill(T5.Income), expenses=fill(T5.Expense))
Expand All @@ -547,7 +470,7 @@ class BalanceSheet(Report):
liabilities: dict[AccountName, Amount]

@classmethod
def new(cls, ledger: Ledger, chart: Chart):
def new(cls, ledger: Ledger, chart: CD):
"""Create balance sheet from ledger and chart.
Account will balances will be shown net of contra account balances."""
fill = chart.net_balances_factory(ledger)
Expand Down
24 changes: 8 additions & 16 deletions playground/example_mini.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
BalanceSheet,
Chart,
ChartDict,
CD,
Entry,
IncomeStatement,
double_entry,
)

cd = ChartDict()
cd = CD("isa", "re")
cd.set(T5.Asset, "cash")
cd.set(T5.Capital, "equity")
cd.offset("equity", "treasury_shares")
Expand All @@ -17,26 +18,17 @@
cd.offset("sales", "refunds")
cd.offset("sales", "voids")
cd.set(T5.Expense, "salaries")
cd.set_isa("isa")
cd.set_re("re")
assert cd.find_contra_accounts("sales") == ["refunds", "voids"]
print(list(cd.closing_pairs("isa", "re")))
[
assert list(cd.closing_pairs) == [
("refunds", "sales"),
("voids", "sales"),
("sales", "isa"),
("salaries", "isa"),
("isa", "re"),
]

keys = set(cd.keys())
del cd["isa"]
del cd["re"]
chart = cd.qualify(income_summary_account="isa", retained_earnings_account="re")
assert "isa" in chart.accounts
assert chart.temporary_accounts == {"isa", "refunds", "sales", "salaries", "voids"}
ledger = chart.accounts.ledger()
assert keys == set(ledger.keys())
assert cd.temporary_accounts == {"isa", "refunds", "sales", "salaries", "voids"}
ledger = cd.ledger()
entries = [
double_entry("cash", "equity", 1200),
double_entry("treasury_shares", "cash", 200),
Expand All @@ -60,9 +52,9 @@
}

# Close ledger at accounting period end
income_statement = IncomeStatement.new(ledger, chart)
closing_entries = ledger.close(chart)
balance_sheet = BalanceSheet.new(ledger, chart)
income_statement = IncomeStatement.new(ledger, cd)
closing_entries = ledger.close(cd)
balance_sheet = BalanceSheet.new(ledger, cd)
print(income_statement)
assert income_statement.net_earnings == 1
print(balance_sheet)
Expand Down
Loading

0 comments on commit 613bbbb

Please sign in to comment.