Skip to content

Commit

Permalink
fine.py tested
Browse files Browse the repository at this point in the history
epogrebnyak committed Dec 29, 2023
1 parent 2addab1 commit 876616d
Showing 3 changed files with 226 additions and 116 deletions.
197 changes: 116 additions & 81 deletions x/fine.py
Original file line number Diff line number Diff line change
@@ -14,15 +14,51 @@ class T(Enum):
Expense = "expense"


class Holder(ABC):

@abstractmethod
def t_account(self) -> Type["TAccount"]:
...

@dataclass
class Regular:
class Regular(Holder):
t: T

@property
def t_account(self) -> Type["TAccount"]:
match self:
case Regular(T.Asset):
return Asset
case Regular(T.Capital):
return Capital
case Regular(T.Liability):
return Liability
case Regular(T.Income):
return Income
case Regular(T.Expense):
return Expense


@dataclass
class Contra:
class Contra(Holder):
t: T

@property
def t_account(self) -> Type["TAccount"]:
match self:
case Contra(T.Asset):
return ContraAsset
case Contra(T.Capital):
return ContraCapital
case Contra(T.Liability):
return ContraLiability
case Contra(T.Income):
return ContraIncome
case Contra(T.Expense):
return ContraExpense
case _:
raise ValueError(f"Invalid type: {self.t}")


@dataclass
class Account:
@@ -136,44 +172,12 @@ class TemporaryDebitAccount(TemporaryAccount, DebitAccount):


@dataclass
class Temporary:
class Temporary(Holder):
t: Type[TAccount]


def taccount(t: Type[Contra | Regular | Temporary]) -> Type[TAccount]:
match t:
case Regular(T.Asset):
return Asset
case Regular(T.Capital):
return Capital
case Regular(T.Liability):
return Liability
case Regular(T.Income):
return Income
case Regular(T.Expense):
return Expense
case Contra(_):
return contra(t)
case Temporary(x):
return x
case _:
raise ValueError(f"Invalid type: {t}")


def contra(t: Type[Contra]) -> Type[TAccount]:
match t:
case Contra(T.Asset):
return ContraAsset
case Contra(T.Capital):
return ContraCapital
case Contra(T.Liability):
return ContraLiability
case Contra(T.Income):
return ContraIncome
case Contra(T.Expense):
return ContraExpense
case _:
raise ValueError(f"Invalid type: {t}")
@property
def t_account(self):
return self.t


@dataclass
@@ -216,7 +220,7 @@ def stream(self, xs, t):
yield b, Contra(t)

def ledger(self):
return Ledger({name: taccount(t)() for name, t in self.dict_items()})
return Ledger({name: t.t_account() for name, t in self.dict_items()})


@dataclass
@@ -238,25 +242,34 @@ class Ledger(UserDict[str, TAccount]):
def post(self, debit: str, credit: str, amount: Amount):
return self.post_one(Entry(debit, credit, amount))


def post_one(self, entry: Entry):
self.data[entry.debit].debit(entry.amount)
self.data[entry.credit].credit(entry.amount)
return self
return self

def post_many(self, entries: list[Entry]):
for entry in entries:
self.post_one(entry)
return self
return self

@property
@property
def balances(self):
return AccountBalances(
{name: account.balance() for name, account in self.items()}
)

def subset(self, cls: Type[TAccount]):
"""Filter ledger by account type."""
return self.__class__(
{
account_name: t_account
for account_name, t_account in self.data.items()
if isinstance(t_account, cls)
}
)


def contra_pairs(chart: Chart, contra_t: Type[ContraAccount]):
def contra_pairs(chart: Chart, contra_t: Type[ContraAccount]) -> list[tuple[str, str]]:
attr = {
"ContraAsset": "assets",
"ContraLiability": "liabilities",
@@ -283,11 +296,12 @@ def append_and_post(self, entry: Entry):
self.ledger.post_one(entry)
self.closing_entries.append(entry)

def close_contra(self, contra_t: Type[ContraAccount]):
"""Close contra accounts of type `contra_t`."""
for from_, to_ in contra_pairs(self.chart, contra_t):
account = self.ledger.data[from_]
entry = account.transfer_balance(from_, to_)
def close_contra(self, t: Type[ContraAccount]):
"""Close contra accounts of type `t`."""
for account, contra_account in contra_pairs(self.chart, t):
entry = self.ledger.data[contra_account].transfer_balance(
contra_account, account
)
self.append_and_post(entry)
return self

@@ -332,35 +346,56 @@ def close_last(self):
return self


chart = Chart(
assets=["cash", "ar", "inventory"],
capital=[Account("equity", contra_accounts=["ts"])],
income=[Account("sales", contra_accounts=["refunds", "voids"])],
liabilities=["ap", "dd"],
expenses=["salaries"],
)

ledger = chart.ledger().post_many([
Entry("cash", "equity", 120),
Entry("ts", "cash", 20),
# Entry("cash", "sales", 130),
# Entry("refunds", "cash", 20),
# Entry("voids", "cash", 10),
# Entry("salaries", "cash", 50),
])

# initial ledger does not change after copy
le0 = Chart(assets=["cash"], capital=["equity"]).ledger().post("cash", "equity", 100)
assert le0.balances.nonzero() == {'cash': 100, 'equity': 100}
le1 = deepcopy(le0)
le1 = le1.post("cash", "equity", 200)
assert le0.balances.nonzero() == {'cash': 100, 'equity': 100}

# correct up to this point

book = Pipeline(chart, ledger).close_first().close_second().close_last().ledger
print(book.balances.nonzero())
# Incorrect:
# {'cash': 150, 'ts': -100, 're': -50, 'refunds': -110, 'voids': 10}

# # Next: entry, post and report
@dataclass
class Reporter:
chart: Chart
ledger: Ledger

@property
def pipeline(self):
return Pipeline(self.chart, self.ledger)

@property
def balance_sheet(self):
p = self.pipeline.close_first().close_second().close_last()
return BalanceSheet.new(p.ledger)

@property
def income_statement(self):
p = self.pipeline.close_first()
return IncomeStatement.new(p.ledger)


class Statement:
...


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

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


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

@classmethod
def new(cls, ledger: Ledger):
return cls(
income=ledger.subset(Income).balances,
expenses=ledger.subset(Expense).balances,
)

def current_account(self):
return sum(self.income.values()) - sum(self.expenses.values())
87 changes: 87 additions & 0 deletions x/test_fine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from copy import deepcopy
from fine import (
Chart,
Account,
Entry,
Ledger,
ContraIncome,
Pipeline,
contra_pairs,
Reporter,
BalanceSheet,
IncomeStatement
)


def test_ledger_does_not_change_after_copy():
le0 = (
Chart(assets=["cash"], capital=["equity"]).ledger().post("cash", "equity", 100)
)
assert le0.balances.nonzero() == {"cash": 100, "equity": 100}
le1 = deepcopy(le0)
le1 = le1.post("cash", "equity", 200)
assert le0.balances.nonzero() == {"cash": 100, "equity": 100}


def test_contra_pairs():
chart = Chart(
income=[Account("sales", contra_accounts=["refunds", "voids"])],
)
assert contra_pairs(chart, ContraIncome) == [
("sales", "refunds"),
("sales", "voids"),
]


import pytest


@pytest.fixture
def chart0():
return Chart(
assets=["cash"],
capital=[Account("equity", contra_accounts=["ts"])],
income=[Account("sales", contra_accounts=["refunds", "voids"])],
liabilities=[],
expenses=["salaries"],
)


@pytest.fixture
def entries0():
return [
Entry("cash", "equity", 120),
Entry("ts", "cash", 20),
Entry("cash", "sales", 47),
Entry("refunds", "cash", 5),
Entry("voids", "cash", 2),
Entry("salaries", "cash", 30),
]


def test_pipleine(chart0, entries0):
ledger = chart0.ledger().post_many(entries0)
p = Pipeline(chart0, ledger).close_first().close_second().close_last()
assert p.ledger.balances.nonzero() == {"cash": 110, "equity": 100, "re": 10}


@pytest.fixture
def reporter0(chart0, entries0):
ledger = chart0.ledger().post_many(entries0)
return Reporter(chart0, ledger)


def test_balance_sheet(reporter0):
assert reporter0.balance_sheet == BalanceSheet(
assets={"cash": 115}, capital={"equity": 100, "re": 15}, liabilities={"dd": 0}
)


def test_balance_sheet(reporter0):
assert reporter0.income_statement == IncomeStatement(
income={"sales": 40}, expenses={"salaries": 25}
)


def test_current_account(reporter0):
assert reporter0.income_statement.current_account() == 15
Loading

0 comments on commit 876616d

Please sign in to comment.