Skip to content

Commit

Permalink
Merge pull request #29 from budgeapp:cleared
Browse files Browse the repository at this point in the history
Add cleared field to transactions
  • Loading branch information
jbhannah authored Jan 22, 2025
2 parents c8ec714 + 166b44a commit 1b01e03
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 38 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## v1.2.0 (unreleased)

- Add `Transaction.cleared` boolean field.
- Add `RepeatingTransaction.last_cleared` date field. All `Transactions` yielded
will have their `cleared` field set to true if the date is before the value of
`last_cleared`. The same rule applies to `RepeatingTransfer`.
- Add `cleared: bool | None` parameters to `Account` methods, where a non-`None`
value filters account transactions by the given value for `cleared`.

## v1.1.1 (2025-01-13)

- Removed the ability to set a transaction's amount to a callback until I've
Expand Down
48 changes: 33 additions & 15 deletions src/budge/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,42 +56,53 @@ def __iter__(self):
)

def transactions_range(
self, start_date: date | None = None, end_date: date | None = None
self,
start_date: date | None = None,
end_date: date | None = None,
cleared: bool | None = None,
) -> Generator[Transaction]:
"""Iterate over transactions in the account over the given range."""
for transaction in self:
if start_date and transaction.date < start_date:
continue
if end_date and transaction.date > end_date:
break
yield transaction

if cleared is None or transaction.cleared == cleared:
yield transaction

def running_balance(
self, start_date: date | None = None, end_date: date | None = None
self,
start_date: date | None = None,
end_date: date | None = None,
cleared: bool | None = None,
) -> Generator[RunningBalance]:
"""Iterate over transactions in the account over the given range with a
running account balance."""
balance = (
self.balance(start_date + relativedelta(days=-1))
self.balance(start_date + relativedelta(days=-1), cleared)
if start_date is not None
else Money(0)
)

for transaction in self.transactions_range(start_date, end_date):
for transaction in self.transactions_range(start_date, end_date, cleared):
balance += transaction.amount
yield RunningBalance(transaction, balance)

def balance(self, as_of: date | None = None):
def balance(self, as_of: date | None = None, cleared: bool | None = None):
"""Calculate the account balance as of the given date."""
as_of = as_of or date.today()

return Money.sum(
transaction.amount
for transaction in self.transactions_range(end_date=as_of)
for transaction in self.transactions_range(end_date=as_of, cleared=cleared)
)

def daily_balance(
self, start_date: date | None = None, end_date: date | None = None
self,
start_date: date | None = None,
end_date: date | None = None,
cleared: bool | None = None,
) -> Generator[DailyBalance]:
"""
Iterate over the daily balance of the account, yielding tuples of date
Expand All @@ -102,20 +113,23 @@ def daily_balance(
given, the date of the first transaction is used. If the end date is not
given, today's date is used.
"""
start_date = start_date or next(self.transactions_range()).date
start_date = start_date or next(self.transactions_range(cleared=cleared)).date
end_date = end_date or date.today()

balance = self.balance(start_date)
balance = self.balance(start_date, cleared)
yield DailyBalance(start_date, balance)

for _date, delta in self._daily_balance_delta(
start_date + timedelta(days=1), end_date
start_date + timedelta(days=1), end_date, cleared
):
balance += delta
yield DailyBalance(_date, balance)

def _deltas_by_date(
self, start_date: date, end_date: date
self,
start_date: date,
end_date: date,
cleared: bool | None = None,
) -> Generator[DailyBalance]:
"""
Iterate over the deltas in the account balance for each date in the
Expand All @@ -129,12 +143,16 @@ def _deltas_by_date(
_date, Money.sum(transaction.amount for transaction in transactions)
)
for _date, transactions in groupby(
self.transactions_range(start_date, end_date), key=lambda t: t.date
self.transactions_range(start_date, end_date, cleared),
key=lambda t: t.date,
)
)

def _daily_balance_delta(
self, start_date: date, end_date: date
self,
start_date: date,
end_date: date,
cleared: bool | None = None,
) -> Generator[DailyBalance]:
"""
Calculate the daily change in account balance over the specified date range.
Expand All @@ -148,7 +166,7 @@ def _daily_balance_delta(
DailyBalance(_date, Money.sum(delta.balance for delta in deltas))
for _date, deltas in groupby(
merge(
self._deltas_by_date(start_date, end_date),
self._deltas_by_date(start_date, end_date, cleared),
(
DailyBalance(_date, Money(0))
for _date in daterange(start_date, end_date)
Expand Down
23 changes: 21 additions & 2 deletions src/budge/transaction.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass, field
from datetime import date as _date
from datetime import datetime
from typing import Self

from dateutil.rrule import rrule
Expand All @@ -17,8 +18,9 @@ class Transaction:
description: str
amount: IntoMoney = IntoMoney()
date: _date = field(default_factory=_date.today)
account: "account.Account | None" = field(default=None, kw_only=True)
parent: Self | None = field(default=None, kw_only=True)
account: "account.Account | None" = None
parent: Self | None = None
cleared: bool = False

def __hash__(self):
return hash((self.amount, self.description, self.date))
Expand All @@ -36,6 +38,22 @@ class RepeatingTransaction(Transaction):
"""

schedule: rrule | rruleset
_last_cleared: _date | None = None

@property
def last_cleared(self) -> _date | None:
if self._last_cleared is None:
return None

return (
self._last_cleared
if isinstance(self._last_cleared, datetime)
else datetime.combine(self._last_cleared, datetime.max.time())
)

@last_cleared.setter
def last_cleared(self, date: _date | None):
self._last_cleared = date

def __hash__(self):
return hash((self.amount, self.description, self.schedule))
Expand All @@ -53,6 +71,7 @@ def __iter__(self):
date=next.date(),
account=self.account,
parent=self,
cleared=(next <= self.last_cleared if self.last_cleared else False),
)
for next in self.schedule
)
39 changes: 34 additions & 5 deletions src/budge/transfer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import InitVar, dataclass, field
from datetime import date

from .account import Account
from .transaction import RepeatingTransaction, Transaction
Expand All @@ -18,8 +19,18 @@ def __post_init__(self, from_account: Account, to_account: Account):
Create the from and to transactions, add them to the respective accounts,
and set their parent to this transfer.
"""
self.from_transaction = Transaction(self.description, -self.amount, self.date)
self.to_transaction = Transaction(self.description, self.amount, self.date)
self.from_transaction = Transaction(
self.description,
-self.amount,
self.date,
cleared=self.cleared,
)
self.to_transaction = Transaction(
self.description,
self.amount,
self.date,
cleared=self.cleared,
)

self.from_transaction.parent = self.to_transaction.parent = self

Expand All @@ -34,19 +45,37 @@ class RepeatingTransfer(Transfer, RepeatingTransaction):
`dateutil.rrule.rrule` or `dateutil.rrule.rruleset`.
"""

@property
def last_cleared(self) -> date | None:
return super().last_cleared

@last_cleared.setter
def last_cleared(self, date: date | None):
self._last_cleared = date
self.from_transaction.last_cleared = self.to_transaction.last_cleared = date # type: ignore

def __post_init__(self, from_account: Account, to_account: Account):
"""
Create the from and to repeating transactions, add them to the
respective accounts, and set their parent to this repeating transfer.
"""
self.from_transaction = RepeatingTransaction(
self.description, -self.amount, schedule=self.schedule
self.description,
-self.amount,
parent=self,
schedule=self.schedule,
)

self.to_transaction = RepeatingTransaction(
self.description, self.amount, schedule=self.schedule
self.description,
self.amount,
parent=self,
schedule=self.schedule,
)

self.from_transaction.parent = self.to_transaction.parent = self
self.from_transaction.last_cleared = self.to_transaction.last_cleared = (
self.last_cleared
)

from_account.repeating_transactions.add(self.from_transaction)
to_account.repeating_transactions.add(self.to_transaction)
Loading

0 comments on commit 1b01e03

Please sign in to comment.