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:
parent
f20cf685ad
commit
1d256d7def
46
alembic/versions/8cc9870b0d74_links.py
Normal file
46
alembic/versions/8cc9870b0d74_links.py
Normal 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 ###
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user