diff --git a/actual/rules.py b/actual/rules.py index bbf68a4..4c79198 100644 --- a/actual/rules.py +++ b/actual/rules.py @@ -8,6 +8,7 @@ import unicodedata import pydantic +import sqlalchemy.orm.session from pydantic import model_serializer from actual import ActualError @@ -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): @@ -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: @@ -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 @@ -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") @@ -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): diff --git a/tests/test_rules.py b/tests/test_rules.py index 5148889..09febd4 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -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 @@ -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