diff --git a/alembic/versions/8b5d5fbc8211_transaction_based_rules.py b/alembic/versions/8b5d5fbc8211_transaction_based_rules.py new file mode 100644 index 0000000..4874fff --- /dev/null +++ b/alembic/versions/8b5d5fbc8211_transaction_based_rules.py @@ -0,0 +1,92 @@ +"""Transaction based rules + +Revision ID: 8b5d5fbc8211 +Revises: e77395969585 +Create Date: 2022-12-08 21:05:41.378466+00:00 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "8b5d5fbc8211" +down_revision = "e77395969585" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.drop_constraint("pk_categories_rules", "categories_rules", schema="transactions") + op.execute(sa.schema.CreateSequence(sa.schema.Sequence("categories_rules_id_seq", schema="transactions"))) + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "categories_rules", + sa.Column( + "id", + sa.BigInteger(), + autoincrement=True, + nullable=False, + server_default=sa.text( + "nextval('transactions.categories_rules_id_seq'::regclass)" + ), + ), + schema="transactions", + ) + op.add_column( + "categories_rules", + sa.Column("date", sa.String(), nullable=True), + schema="transactions", + ) + op.add_column( + "categories_rules", + sa.Column("description", sa.String(), nullable=True), + schema="transactions", + ) + op.add_column( + "categories_rules", + sa.Column("bank", sa.String(), nullable=True), + schema="transactions", + ) + op.add_column( + "categories_rules", + sa.Column("min_amount", sa.Float(), nullable=True), + schema="transactions", + ) + op.add_column( + "categories_rules", + sa.Column("max_amount", sa.Float(), nullable=True), + schema="transactions", + ) + op.drop_column("categories_rules", "rule", schema="transactions") + # ### end Alembic commands ### + op.create_primary_key( + "pk_categories_rules", + "categories_rules", + ["id"], + schema="transactions", + ) + + +def downgrade() -> None: + op.drop_constraint("pk_categories_rules", "categories_rules", schema="transactions") + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "categories_rules", + sa.Column("rule", sa.String(), autoincrement=False, nullable=False), + schema="transactions", + ) + op.drop_column("categories_rules", "max_amount", schema="transactions") + op.drop_column("categories_rules", "min_amount", schema="transactions") + op.drop_column("categories_rules", "bank", schema="transactions") + op.drop_column("categories_rules", "description", schema="transactions") + op.drop_column("categories_rules", "date", schema="transactions") + op.drop_column("categories_rules", "id", schema="transactions") + # ### end Alembic commands ### + op.execute(sa.schema.DropSequence(sa.schema.Sequence("categories_rules_id_seq", schema="transactions"))) + op.create_primary_key( + "pk_categories_rules", + "categories_rules", + ["name", "rule"], + schema="transactions", + ) diff --git a/pfbudget/cli/runnable.py b/pfbudget/cli/runnable.py index 766baea..49f6f77 100644 --- a/pfbudget/cli/runnable.py +++ b/pfbudget/cli/runnable.py @@ -340,6 +340,15 @@ def category(parser: argparse.ArgumentParser, universal: argparse.ArgumentParser schedule.add_argument("period", nargs=1, choices=[e.value for e in Period]) schedule.add_argument("--frequency", nargs=1, default=[1], type=int) + rule = commands.add_parser("rule", parents=[universal]) + rule.set_defaults(op=Operation.CategoryRule) + rule.add_argument("category", nargs="+", type=str) + rule.add_argument("--date", nargs=1, type=str) + rule.add_argument("--description", nargs=1, type=str) + rule.add_argument("--bank", nargs=1, type=str) + rule.add_argument("--min", nargs=1, type=float) + rule.add_argument("--max", nargs=1, type=float) + group = commands.add_parser("group", parents=[universal]) category_group(group, universal) diff --git a/pfbudget/common/types.py b/pfbudget/common/types.py index 2c30b9c..dd8f431 100644 --- a/pfbudget/common/types.py +++ b/pfbudget/common/types.py @@ -17,6 +17,7 @@ class Operation(Enum): CategoryUpdate = auto() CategoryRemove = auto() CategorySchedule = auto() + CategoryRule = auto() GroupAdd = auto() GroupRemove = auto() diff --git a/pfbudget/core/manager.py b/pfbudget/core/manager.py index 9dd2911..fb0d54f 100644 --- a/pfbudget/core/manager.py +++ b/pfbudget/core/manager.py @@ -2,7 +2,7 @@ from pfbudget.input.input import Input from pfbudget.input.nordigen import NordigenClient from pfbudget.input.parsers import parse_data from pfbudget.db.client import DbClient -from pfbudget.db.model import Category, CategoryGroup, CategorySchedule +from pfbudget.db.model import Category, CategoryGroup, CategoryRule, CategorySchedule from pfbudget.common.types import Operation from pfbudget.core.categorizer import Categorizer from pfbudget.utils import convert @@ -72,15 +72,9 @@ class Manager: "period" in self.args and "frequency" in self.args ), "Schedule not well defined" + case Operation.CategoryRule: with self.db.session() as session: - session.updateschedules( - [Category(name=category) for category in self.args["category"]], - CategorySchedule( - recurring=True, - period=self.args["period"][0], - period_multiplier=self.args["frequency"][0], - ), - ) + session.addrules(params) case Operation.GroupAdd: with self.db.session() as session: diff --git a/pfbudget/db/client.py b/pfbudget/db/client.py index d2da511..9e44a93 100644 --- a/pfbudget/db/client.py +++ b/pfbudget/db/client.py @@ -8,6 +8,7 @@ from pfbudget.db.model import ( Bank, Category, CategoryGroup, + CategoryRule, CategorySchedule, Transaction, ) @@ -124,6 +125,9 @@ class DbClient: ) self.__session.execute(stmt) + def addrules(self, rules: list[CategoryRule]): + self.__session.add_all(rules) + def addcategorygroup(self, group: CategoryGroup): self.__session.add(group) diff --git a/pfbudget/db/model.py b/pfbudget/db/model.py index 17470b7..be0c5dc 100644 --- a/pfbudget/db/model.py +++ b/pfbudget/db/model.py @@ -9,7 +9,13 @@ from sqlalchemy import ( String, Text, ) -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship +from sqlalchemy.orm import ( + DeclarativeBase, + Mapped, + mapped_column, + MappedAsDataclass, + relationship, +) from decimal import Decimal from typing import Annotated, Optional @@ -17,7 +23,7 @@ import datetime as dt import enum -class Base(DeclarativeBase): +class Base(MappedAsDataclass, DeclarativeBase): __table_args__ = {"schema": "transactions"} metadata = MetaData( naming_convention={ @@ -69,18 +75,21 @@ money = Annotated[Decimal, mapped_column(Numeric(16, 2), nullable=False)] class Transaction(Base): __tablename__ = "originals" - id: Mapped[idpk] = mapped_column(autoincrement=True) + id: Mapped[idpk] = mapped_column(autoincrement=True, init=False) date: Mapped[dt.date] description: Mapped[Optional[str]] bank: Mapped[bankfk] amount: Mapped[money] category: Mapped[Optional[TransactionCategory]] = relationship( - back_populates="original", lazy="joined" + back_populates="original", lazy="joined", default=None ) - note: Mapped[Optional[Note]] = relationship(back_populates="original") + note: Mapped[Optional[Note]] = relationship(back_populates="original", default=None) tags: Mapped[Optional[set[Tag]]] = relationship( - back_populates="original", cascade="all, delete-orphan", passive_deletes=True + back_populates="original", + cascade="all, delete-orphan", + passive_deletes=True, + default=None, ) def __repr__(self) -> str: @@ -102,12 +111,16 @@ class Category(Base): __tablename__ = "categories_available" name: Mapped[str] = mapped_column(primary_key=True) - group: Mapped[Optional[str]] = mapped_column(ForeignKey(CategoryGroup.name)) + group: Mapped[Optional[str]] = mapped_column( + ForeignKey(CategoryGroup.name), default=None + ) rules: Mapped[Optional[set[CategoryRule]]] = relationship( - cascade="all, delete-orphan", passive_deletes=True + cascade="all, delete-orphan", passive_deletes=True, default=None + ) + schedule: Mapped[CategorySchedule] = relationship( + back_populates="category", default=None ) - schedule: Mapped[CategorySchedule] = relationship(back_populates="category") def __repr__(self) -> str: return f"Category(name={self.name}, group={self.group}, #rules={len(self.rules)}, schedule={self.schedule})" @@ -122,7 +135,7 @@ catfk = Annotated[ class TransactionCategory(Base): __tablename__ = "categorized" - id: Mapped[idfk] = mapped_column(primary_key=True) + id: Mapped[idfk] = mapped_column(primary_key=True, init=False) name: Mapped[str] = mapped_column(ForeignKey(Category.name)) original: Mapped[Transaction] = relationship(back_populates="category") @@ -167,10 +180,13 @@ class Tag(Base): class CategoryRule(Base): __tablename__ = "categories_rules" - name: Mapped[str] = mapped_column( - ForeignKey(Category.name, ondelete="CASCADE"), primary_key=True - ) - rule: Mapped[str] = mapped_column(primary_key=True) + id: Mapped[idpk] = mapped_column(autoincrement=True, init=False) + name: Mapped[catfk] = mapped_column() + date: Mapped[Optional[str]] = mapped_column() + description: Mapped[Optional[str]] = mapped_column() + bank: Mapped[Optional[str]] = mapped_column() + min_amount: Mapped[Optional[float]] = mapped_column() + max_amount: Mapped[Optional[float]] = mapped_column() class Selector(enum.Enum):