Skip to content

Commit

Permalink
todo: test TrialBalance class
Browse files Browse the repository at this point in the history
  • Loading branch information
epogrebnyak committed Jan 21, 2024
1 parent 00920e3 commit b8a086e
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 18 deletions.
File renamed without changes.
43 changes: 40 additions & 3 deletions core/test_uncore.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import pytest
import uncore
from uncore import (
Account,
BalanceSheet,
Chart,
ChartDict,
Contra,
IncomeStatement,
Intermediate,
Journal,
Move,
Pipeline,
Reference,
Regular,
Side,
T,
Pipeline,
Move,
close,
credit,
debit,
double_entry,
close,
)


Expand Down Expand Up @@ -224,3 +227,37 @@ def test_close_pipeline_logic(chart, journal):
.close_contra(T.Asset, T.Capital, T.Liability)
.journal
)


@pytest.fixture
def income_statement(chart, journal):
return uncore.income_statement(chart, journal)


@pytest.fixture
def balance_sheet(chart, journal):
return uncore.balance_sheet(chart, journal)


def test_statements(chart, journal, income_statement, balance_sheet):
_, i, b, _ = uncore.statements(chart, journal)
assert i == income_statement
assert b == balance_sheet


def test_income_statement(income_statement):
assert income_statement == IncomeStatement(
income={"sales": 30}, expenses={"salary": 18}
)


def test_balance_sheet(balance_sheet):
assert balance_sheet == BalanceSheet(
assets={"cash": 1012, "ar": 5},
capital={"equity": 1000, "retained_earnings": 12},
liabilities={"vat": 5},
)


def test_current_profit(income_statement):
assert income_statement.current_profit == 12
151 changes: 136 additions & 15 deletions core/uncore.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from abc import ABC, abstractmethod
"""Accounting chart, journal and reports."""

from collections import UserDict, namedtuple
from dataclasses import dataclass, field
from decimal import Decimal
from enum import Enum
from typing import Protocol


class Side(Enum):
Expand All @@ -20,8 +22,10 @@ class T(Enum):
Expense = "expense"


class AccountType:
...
class AccountType(Protocol):
@property
def side(self) -> Side:
...


@dataclass
Expand All @@ -44,7 +48,12 @@ def side(self):

@dataclass
class Intermediate(AccountType):
side: Side
_side: Side

# workaround for mypy
@property
def side(self):
return self._side


def which(t: T) -> Side:
Expand Down Expand Up @@ -75,12 +84,14 @@ def assert_reference_exists(key, keys):
)


# not used
@dataclass
class Label:
prefix: str
name: str


# not used
@dataclass
class Offset:
points_to: str
Expand Down Expand Up @@ -244,6 +255,10 @@ def set_re(self, name):
self[name] = Account(Regular(T.Capital))
return self

def condense(self):
cls = self.__class__
return cls({n: a.condense() for n, a in self.items()})

def post(self, entry: Entry):
if not is_balanced(entry):
raise ValueError(entry)
Expand Down Expand Up @@ -295,15 +310,15 @@ def to_entry(self, journal):

@dataclass
class Pipeline:
"""A pipeline to accumulate ledger transformations."""

chart: Chart
journal: Journal
moves: list[Move] = field(default_factory=list)
closing_entries: list[Entry] = field(default_factory=list)

def __post_init__(self):
import copy

self.journal = copy.deepcopy(self.journal)
self.journal = self.journal.condense()

def move(self, frm: str, to: str):
"""Transfer balance of account `frm` to account `to`."""
Expand Down Expand Up @@ -344,7 +359,7 @@ def close_second(self):
return self.close_temporary().close_isa()

def close_last(self):
"""Do netting (close contra accounts) for permanent accounts."""
"""Close contra accounts for permanent accounts (for assets, capital and liabilities)."""
return self.close_contra(T.Asset, T.Capital, T.Liability)

def flush(self):
Expand All @@ -358,14 +373,112 @@ def close(chart, journal):


def statements(chart, journal):
a = Pipeline(chart, journal).close_first()
b = Pipeline(chart, journal).close_first().close_second().close_last()
return a.journal, b.journal
t1 = trial_balance(journal)
a = Pipeline(chart, journal).close_first().journal.condense()
b = Pipeline(chart, a).close_second().close_last().journal.condense()
t2 = trial_balance(b)
return t1, IncomeStatement.new(a), BalanceSheet.new(b), t2


def income_statement(chart, journal):
return IncomeStatement.new(Pipeline(chart, journal).close_first().journal)


def balance_sheet(chart, journal):
return BalanceSheet.new(close(chart, journal))


class Statement:
...


AccountBalances = dict[str, Amount]


@dataclass
class BalanceSheet(Statement):
assets: AccountBalances
capital: AccountBalances
liabilities: AccountBalances

@classmethod
def new(cls, journal: Journal):
return cls(
assets=journal.subset(T.Asset).balances,
capital=journal.subset(T.Capital).balances,
liabilities=journal.subset(T.Liability).balances,
)


@dataclass
class IncomeStatement(Statement):
income: AccountBalances
expenses: AccountBalances

@classmethod
def new(cls, journal: Journal):
return cls(
income=journal.subset(T.Income).balances,
expenses=journal.subset(T.Expense).balances,
)

@property
def current_profit(self):
return sum(self.income.values()) - sum(self.expenses.values())


class TrialBalance(UserDict[str, tuple[Amount, Amount, Side]], Statement):
"""Trial balance is a dictionary of account names and
their debit side and credit side balances."""

@classmethod
def new(cls, journal: Journal):
tb = cls()
for side in [Side.Debit, Side.Credit]:
for name, a in journal.items():
if a.account_type.side == side:
x, y = a.tuple()
tb[name] = x, y, side
return tb

def net(self):
"""Show net balance on one side of account."""
tb = self.__class__()
for n, (a, b, s) in self.items():
if s == Side.Debit:
tb[n] = a - b, 0, s
else:
tb[n] = 0, b - a, s
return tb

def drop_null(self):
"""Drop accounts where balance is null.
These are usually temporary and intermediate accounts after closing."""
tb = self.__class__()
for n, (a, b, s) in self.items():
if a + b != 0:
tb[n] = (a, b, s)
return tb

def brief(self) -> dict[str, tuple[Amount, Amount]]:
"""Hide information about side of accounts."""
return {n: (a, b) for n, (a, b, _) in self.items()}

def balances(self) -> dict[str, Amount]:
"""Return dictionary with account balances."""
return {n: a + b for n, (a, b, _) in self.net().items()}

def non_zero_balances(self) -> dict[str, Amount]:
return self.drop_null().balances()


def trial_balance(journal) -> TrialBalance:
return TrialBalance.new(journal)


if __name__ == "__main__":
chart = (
Chart("isa", "re")
Chart(income_summary_account="isa", retained_earnings_account="re")
.add_many(T.Asset, "cash", "ar")
.add(T.Capital, "equity", contra_names=["buyback"])
.add(T.Income, "sales", contra_names=["refunds", "voids"])
Expand All @@ -388,6 +501,14 @@ def statements(chart, journal):
[debit("salary", 18), credit("cash", 18)],
]
)

# - Statement and viewers classes next
# - Company with chart and entries from experimental.py are next
t, i, b, final_t = statements(chart, journal)
# todo: test TrialBalance
print(t.brief())
print(b)
print(i)
print(i.current_profit)
print(final_t.drop_null().brief())

# Next:
# - viewers (separate file)
# - company with chart and transactions from experimental.py
Empty file added core/viewers.py
Empty file.

0 comments on commit b8a086e

Please sign in to comment.