budget/pfbudget/transform/nullifier.py
Luís Murta ec22b5e5bd
[DB][Refactor] Compact the category selector
The `CategorySelector` was possibly added to be incremented with other
attributes. However, since none other that the selector enum is used at
the moment, it is only adding unnecessary cluter.
The category selector value is moved to the parent
`TransactionCategory`.
2024-01-22 21:47:47 +00:00

94 lines
3.0 KiB
Python

from copy import deepcopy
import datetime as dt
from typing import Sequence
from .exceptions import MoreThanOneMatchError
from .transform import Transformer
from pfbudget.db.model import (
CategorySelector,
Transaction,
TransactionCategory,
)
class Nullifier(Transformer):
NULL_DAYS = 4
def __init__(self, rules=None):
self.rules = rules
def transform(self, transactions: Sequence[Transaction]) -> Sequence[Transaction]:
"""transform
Find transactions that nullify each others, e.g. transfers between banks or
between bank and credit cards.
Args:
transactions (Sequence[Transaction]): ordered sequence of transactions
Raises:
MoreThanOneMatchError: if there is more than a match for a single transation
Returns:
Sequence[Transaction]: nullified sequence of transactions
"""
result = deepcopy(transactions)
for i, transaction in enumerate(result[:-1]):
if matches := [t for t in result[i + 1 :] if self._cancels(transaction, t)]:
if len(matches) > 1:
raise MoreThanOneMatchError(f"{transaction} -> {matches}")
match = matches[0]
transaction = self._nullify(transaction)
match = self._nullify(match)
return result
def transform_inplace(self, transactions: Sequence[Transaction]) -> None:
"""_summary_
Find transactions that nullify each others, e.g. transfers between banks or
between bank and credit cards.
Args:
transactions (Sequence[Transaction]): ordered sequence of transactions that
will be modified inplace
Raises:
MoreThanOneMatchError: if there is more than a match for a single transation
"""
for transaction in transactions:
if matches := [t for t in transactions if self._cancels(transaction, t)]:
if len(matches) > 1:
raise MoreThanOneMatchError(f"{transaction} -> {matches}")
match = matches[0]
transaction = self._nullify(transaction)
match = self._nullify(match)
def _cancels(self, transaction: Transaction, cancel: Transaction):
return (
transaction.date
<= cancel.date
<= transaction.date + dt.timedelta(days=self.NULL_DAYS)
and cancel != transaction
and cancel.bank != transaction.bank
and cancel.amount == -transaction.amount
and (not cancel.category or cancel.category.name != "null")
and (
any(r.matches(transaction) for r in self.rules) if self.rules else True
)
and (any(r.matches(cancel) for r in self.rules) if self.rules else True)
)
def _nullify(self, transaction: Transaction) -> Transaction:
transaction.category = TransactionCategory(
"null", selector=CategorySelector.nullifier
)
return transaction