Skip to content
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
18 changes: 16 additions & 2 deletions actual/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import unicodedata

import pydantic
import sqlalchemy.orm.session
from pydantic import model_serializer

from actual import ActualError
Expand Down Expand Up @@ -42,6 +43,8 @@ class ConditionType(enum.Enum):
IS_BETWEEN = "isbetween"
MATCHES = "matches"
HAS_TAGS = "hasTags"
ON_BUDGET = "onBudget"
OFF_BUDGET = "offBudget"


class ActionType(enum.Enum):
Expand Down Expand Up @@ -99,7 +102,7 @@ def is_valid(self, operation: ConditionType) -> bool:
"hasTags",
)
elif self == ValueType.ID:
return operation.value in ("is", "isNot", "oneOf", "notOneOf")
return operation.value in ("is", "isNot", "oneOf", "notOneOf", "onBudget", "offBudget")
elif self == ValueType.NUMBER:
return operation.value in ("is", "isapprox", "isbetween", "gt", "gte", "lt", "lte")
else:
Expand Down Expand Up @@ -173,9 +176,12 @@ def condition_evaluation(
true_value: int | list[str] | str | datetime.date | None,
self_value: int | list[str] | str | datetime.date | BetweenValue | None,
options: dict | None = None,
session: sqlalchemy.orm.session.Session | None = None,
) -> bool:
"""Helper function to evaluate the condition based on the true_value, value found on the transaction, and the
self_value, value defined on rule condition."""
from actual.queries import get_account # lazy import to prevent circular issues

if true_value is None:
# short circuit as comparisons with NoneType are useless
return False
Expand Down Expand Up @@ -230,6 +236,12 @@ def condition_evaluation(
# taken from https://stackoverflow.com/a/26740753/12681470
tags = re.findall(r"\#[\U00002600-\U000027BF\U0001f300-\U0001f64F\U0001f680-\U0001f6FF\w-]+", self_value)
return any(tag in true_value for tag in tags)
elif op == ConditionType.ON_BUDGET:
account = get_account(session, true_value)
return account is not None and account.offbudget == 0
elif op == ConditionType.OFF_BUDGET:
account = get_account(session, true_value)
return account is not None and account.offbudget == 1
else:
raise ActualError(f"Operation {op} not supported")

Expand Down Expand Up @@ -322,7 +334,9 @@ def run(self, transaction: Transactions) -> bool:
attr = get_attribute_by_table_name(Transactions.__tablename__, self.field)
true_value = get_value(getattr(transaction, attr), self.type)
self_value = self.get_value()
return condition_evaluation(self.op, true_value, self_value, self.options)
# get inner session from object
session = transaction._sa_instance_state.session # noqa
return condition_evaluation(self.op, true_value, self_value, self.options, session=session)


class Action(pydantic.BaseModel):
Expand Down
24 changes: 24 additions & 0 deletions tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ def test_value_type_condition_validation():
assert ValueType.BOOLEAN.is_valid(ConditionType.IS) is True
assert ValueType.ID.is_valid(ConditionType.NOT_ONE_OF) is True
assert ValueType.ID.is_valid(ConditionType.CONTAINS) is False
assert ValueType.ID.is_valid(ConditionType.ON_BUDGET) is True
assert ValueType.ID.is_valid(ConditionType.OFF_BUDGET) is True
assert ValueType.STRING.is_valid(ConditionType.CONTAINS) is True
assert ValueType.STRING.is_valid(ConditionType.GT) is False
assert ValueType.IMPORTED_PAYEE.is_valid(ConditionType.CONTAINS) is True
Expand Down Expand Up @@ -413,3 +415,25 @@ def test_delete_transaction_action(session):

assert str(action) == "delete transaction"
assert "delete transaction" in str(rule)


def test_on_budget_condition(session):
# create basic items
acct = create_account(session, "Bank", off_budget=False)
t = create_transaction(session, datetime.date(2024, 1, 1), acct, imported_payee="")
condition = {"type": "id", "field": "acct", "op": "onBudget", "value": None}
cond = Condition.model_validate(condition)
assert cond.run(t) is True
acct.offbudget = 1
assert cond.run(t) is False


def test_off_budget_condition(session):
# create basic items
acct = create_account(session, "Bank", off_budget=True)
t = create_transaction(session, datetime.date(2024, 1, 1), acct, imported_payee="")
condition = {"type": "id", "field": "acct", "op": "offBudget", "value": None}
cond = Condition.model_validate(condition)
assert cond.run(t) is True
acct.offbudget = 0
assert cond.run(t) is False
Loading