Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cleared field to transactions #29

Merged
merged 7 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading