Adds links between transactions

Sometimes transctions are directly related to one another w/o being of
an equal value, e.g. someone pays for the meal w/ the CC, and everyone
pays him/her.

Clear leftover __repr__ methods in the model classes, the database
decorator will create those automatically.
This commit is contained in:
Luís Murta 2022-12-19 22:48:49 +00:00
parent f20cf685ad
commit 1d256d7def
Signed by: satprog
GPG Key ID: 169EF1BBD7049F94
7 changed files with 93 additions and 21 deletions

View File

@ -0,0 +1,46 @@
"""Links
Revision ID: 8cc9870b0d74
Revises: a910e1b2214d
Create Date: 2022-12-19 22:10:25.136479+00:00
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "8cc9870b0d74"
down_revision = "a910e1b2214d"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"links",
sa.Column("original", sa.BigInteger(), nullable=False),
sa.Column("link", sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint(
["link"],
["transactions.originals.id"],
name=op.f("fk_links_link_originals"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["original"],
["transactions.originals.id"],
name=op.f("fk_links_original_originals"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("original", "link", name=op.f("pk_links")),
schema="transactions",
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("links", schema="transactions")
# ### end Alembic commands ###

View File

@ -149,4 +149,10 @@ if __name__ == "__main__":
assert "group" in args, "argparser ill defined" assert "group" in args, "argparser ill defined"
params = [pfbudget.types.CategoryGroup(group) for group in args["group"]] params = [pfbudget.types.CategoryGroup(group) for group in args["group"]]
case pfbudget.Operation.Forge | pfbudget.Operation.Dismantle:
assert args.keys() >= {"original", "links"}, "argparser ill defined"
params = [
pfbudget.types.Link(args["original"][0], link) for link in args["links"]
]
pfbudget.Manager(db, verbosity, args).action(op, params) pfbudget.Manager(db, verbosity, args).action(op, params)

View File

@ -5,9 +5,7 @@ import decimal
import re import re
from pfbudget.common.types import Operation from pfbudget.common.types import Operation
from pfbudget.core.categories import categorize_data
from pfbudget.db.model import Period from pfbudget.db.model import Period
from pfbudget.input.json import JsonParser
from pfbudget.input.nordigen import NordigenInput from pfbudget.input.nordigen import NordigenInput
from pfbudget.db.sqlite import DatabaseClient from pfbudget.db.sqlite import DatabaseClient
import pfbudget.reporting.graph import pfbudget.reporting.graph
@ -253,6 +251,9 @@ def argparser() -> argparse.ArgumentParser:
# Tag # Tag
tags(subparsers.add_parser("tag", parents=[universal]), universal) tags(subparsers.add_parser("tag", parents=[universal]), universal)
# Link
link(subparsers.add_parser("link"))
return parser return parser
@ -420,6 +421,20 @@ def rules(parser: argparse.ArgumentParser):
parser.add_argument("--max", nargs=1, type=decimal.Decimal) parser.add_argument("--max", nargs=1, type=decimal.Decimal)
def link(parser: argparse.ArgumentParser):
commands = parser.add_subparsers(required=True)
forge = commands.add_parser("forge")
forge.set_defaults(op=Operation.Forge)
forge.add_argument("original", nargs=1, type=int)
forge.add_argument("links", nargs="+", type=int)
dismantle = commands.add_parser("dismantle")
dismantle.set_defaults(op=Operation.Dismantle)
dismantle.add_argument("original", nargs=1, type=int)
dismantle.add_argument("links", nargs="+", type=int)
def run(): def run():
args = vars(argparser().parse_args()) args = vars(argparser().parse_args())
assert "op" in args, "No operation selected" assert "op" in args, "No operation selected"

View File

@ -27,6 +27,8 @@ class Operation(Enum):
TagRuleAdd = auto() TagRuleAdd = auto()
TagRuleRemove = auto() TagRuleRemove = auto()
TagRuleModify = auto() TagRuleModify = auto()
Forge = auto()
Dismantle = auto()
class TransactionError(Exception): class TransactionError(Exception):

View File

@ -105,6 +105,16 @@ class Manager:
with self.db.session() as session: with self.db.session() as session:
session.remove_by_name(CategoryGroup, params) session.remove_by_name(CategoryGroup, params)
case Operation.Forge:
with self.db.session() as session:
session.add(params)
case Operation.Dismantle:
with self.db.session() as session:
original = params[0].original
links = [link.link for link in params]
session.remove_links(original, links)
# def init(self): # def init(self):
# client = DatabaseClient(self.__db) # client = DatabaseClient(self.__db)
# client.init() # client.init()

View File

@ -1,4 +1,3 @@
from copy import deepcopy
from dataclasses import asdict from dataclasses import asdict
from sqlalchemy import create_engine, delete, select, update from sqlalchemy import create_engine, delete, select, update
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
@ -10,6 +9,7 @@ from pfbudget.db.model import (
CategoryGroup, CategoryGroup,
CategoryRule, CategoryRule,
CategorySchedule, CategorySchedule,
Link,
Tag, Tag,
TagRule, TagRule,
Transaction, Transaction,
@ -83,12 +83,7 @@ class DbClient:
def commit(self): def commit(self):
self.__session.commit() self.__session.commit()
def add( def add(self, rows: list):
self,
rows: list[
Category | CategoryGroup | CategoryRule | Tag | TagRule | Transaction
],
):
self.__session.add_all(rows) self.__session.add_all(rows)
def remove_by_name(self, type: Category | Tag | Transaction, rows: list): def remove_by_name(self, type: Category | Tag | Transaction, rows: list):
@ -122,6 +117,10 @@ class DbClient:
def updaterules(self, rules: list[dict]): def updaterules(self, rules: list[dict]):
self.__session.execute(update(CategoryRule), rules) self.__session.execute(update(CategoryRule), rules)
def remove_links(self, original, links: list):
stmt = delete(Link).where(Link.original == original, Link.link.in_(link for link in links))
self.__session.execute(stmt)
def uncategorized(self) -> list[Transaction]: def uncategorized(self) -> list[Transaction]:
stmt = select(Transaction).where(~Transaction.category.has()) stmt = select(Transaction).where(~Transaction.category.has())
return self.__session.scalars(stmt).all() return self.__session.scalars(stmt).all()

View File

@ -63,9 +63,6 @@ class Bank(Base):
back_populates="bank", lazy="joined" back_populates="bank", lazy="joined"
) )
def __repr__(self) -> str:
return f"Bank(name={self.name}, BIC={self.BIC}, type={self.type}, nordigen={self.nordigen})"
bankfk = Annotated[str, mapped_column(Text, ForeignKey(Bank.name))] bankfk = Annotated[str, mapped_column(Text, ForeignKey(Bank.name))]
@ -83,13 +80,9 @@ class Transaction(Base):
amount: Mapped[money] amount: Mapped[money]
category: Mapped[Optional[TransactionCategory]] = relationship() category: Mapped[Optional[TransactionCategory]] = relationship()
)
note: Mapped[Optional[Note]] = relationship(back_populates="original") note: Mapped[Optional[Note]] = relationship(back_populates="original")
tags: Mapped[Optional[set[TransactionTag]]] = relationship() tags: Mapped[Optional[set[TransactionTag]]] = relationship()
def __repr__(self) -> str:
return f"Transaction(date={self.date}, description={self.description}, bank={self.bank}, amount={self.amount}, category={self.category})"
idfk = Annotated[ idfk = Annotated[
int, mapped_column(BigInteger, ForeignKey(Transaction.id, ondelete="CASCADE")) int, mapped_column(BigInteger, ForeignKey(Transaction.id, ondelete="CASCADE"))
@ -158,9 +151,6 @@ class Nordigen(Base):
bank: Mapped[Bank] = relationship(back_populates="nordigen") bank: Mapped[Bank] = relationship(back_populates="nordigen")
def __repr__(self) -> str:
return f"(bank_id={self.bank_id}, requisition_id={self.requisition_id}, invert={self.invert})"
class Tag(Base): class Tag(Base):
__tablename__ = "tags_available" __tablename__ = "tags_available"
@ -229,8 +219,12 @@ class CategorySchedule(Base):
category: Mapped[Category] = relationship(back_populates="schedule") category: Mapped[Category] = relationship(back_populates="schedule")
def __repr__(self) -> str:
return f"{self.name} schedule=Schedule(period={self.period}, multiplier={self.period_multiplier}, amount={self.amount})" class Link(Base):
__tablename__ = "links"
original: Mapped[idfk] = mapped_column(primary_key=True)
link: Mapped[idfk] = mapped_column(primary_key=True)
class Rule: class Rule: