Compare commits
10 Commits
d321481e29
...
1d256d7def
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d256d7def | |||
| f20cf685ad | |||
| e57859f601 | |||
| e27f2f08cf | |||
| 058d0cc05d | |||
| 6c33a94a5f | |||
| a2b2f2c1d1 | |||
| 7986fe8e5b | |||
| 17d8d5d813 | |||
| 72a8995fe6 |
32
alembic/versions/0ce89e987770_regex_rule.py
Normal file
32
alembic/versions/0ce89e987770_regex_rule.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""Regex rule
|
||||||
|
|
||||||
|
Revision ID: 0ce89e987770
|
||||||
|
Revises: 7adf89ec8d14
|
||||||
|
Create Date: 2022-12-10 14:00:49.418494+00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "0ce89e987770"
|
||||||
|
down_revision = "7adf89ec8d14"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column(
|
||||||
|
"categories_rules",
|
||||||
|
sa.Column("regex", sa.String(), nullable=True),
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column("categories_rules", "regex", schema="transactions")
|
||||||
|
# ### end Alembic commands ###
|
||||||
49
alembic/versions/753c0bfb2062_rule_min_max.py
Normal file
49
alembic/versions/753c0bfb2062_rule_min_max.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""Rule min/max
|
||||||
|
|
||||||
|
Revision ID: 753c0bfb2062
|
||||||
|
Revises: e36e6321568e
|
||||||
|
Create Date: 2022-12-18 00:24:03.861461+00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "753c0bfb2062"
|
||||||
|
down_revision = "e36e6321568e"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column(
|
||||||
|
"categories_rules",
|
||||||
|
"min_amount",
|
||||||
|
new_column_name="min",
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
"categories_rules",
|
||||||
|
"max_amount",
|
||||||
|
new_column_name="max",
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column(
|
||||||
|
"categories_rules",
|
||||||
|
"min",
|
||||||
|
new_column_name="min_amount",
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
"categories_rules",
|
||||||
|
"max",
|
||||||
|
new_column_name="max_amount",
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
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 ###
|
||||||
68
alembic/versions/9028b0f3b985_tag_rules.py
Normal file
68
alembic/versions/9028b0f3b985_tag_rules.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"""Tag rules
|
||||||
|
|
||||||
|
Revision ID: 9028b0f3b985
|
||||||
|
Revises: 753c0bfb2062
|
||||||
|
Create Date: 2022-12-18 22:53:13.334046+00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "9028b0f3b985"
|
||||||
|
down_revision = "753c0bfb2062"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
"tags_available",
|
||||||
|
sa.Column("name", sa.String(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("name", name=op.f("pk_tags_available")),
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
op.create_table(
|
||||||
|
"tag_rules",
|
||||||
|
sa.Column("id", sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column("tag", sa.String(), nullable=False),
|
||||||
|
sa.Column("date", sa.Date(), nullable=True),
|
||||||
|
sa.Column("description", sa.String(), nullable=True),
|
||||||
|
sa.Column("regex", sa.String(), nullable=True),
|
||||||
|
sa.Column("bank", sa.String(), nullable=True),
|
||||||
|
sa.Column("min", sa.Numeric(precision=16, scale=2), nullable=True),
|
||||||
|
sa.Column("max", sa.Numeric(precision=16, scale=2), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["tag"],
|
||||||
|
["transactions.tags_available.name"],
|
||||||
|
name=op.f("fk_tag_rules_tag_tags_available"),
|
||||||
|
ondelete="CASCADE",
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_tag_rules")),
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
op.f("fk_tags_tag_tags_available"),
|
||||||
|
"tags",
|
||||||
|
"tags_available",
|
||||||
|
["tag"],
|
||||||
|
["name"],
|
||||||
|
source_schema="transactions",
|
||||||
|
referent_schema="transactions",
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(
|
||||||
|
op.f("fk_tags_tag_tags_available"),
|
||||||
|
"tags",
|
||||||
|
schema="transactions",
|
||||||
|
type_="foreignkey",
|
||||||
|
)
|
||||||
|
op.drop_table("tag_rules", schema="transactions")
|
||||||
|
op.drop_table("tags_available", schema="transactions")
|
||||||
|
# ### end Alembic commands ###
|
||||||
56
alembic/versions/a910e1b2214d_rule_inheritance.py
Normal file
56
alembic/versions/a910e1b2214d_rule_inheritance.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"""Rule inheritance
|
||||||
|
|
||||||
|
Revision ID: a910e1b2214d
|
||||||
|
Revises: 9028b0f3b985
|
||||||
|
Create Date: 2022-12-19 20:48:04.682812+00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "a910e1b2214d"
|
||||||
|
down_revision = "9028b0f3b985"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_categorized_name_categories_available",
|
||||||
|
"categorized",
|
||||||
|
schema="transactions",
|
||||||
|
type_="foreignkey",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
op.f("fk_categorized_name_categories_available"),
|
||||||
|
"categorized",
|
||||||
|
"categories_available",
|
||||||
|
["name"],
|
||||||
|
["name"],
|
||||||
|
source_schema="transactions",
|
||||||
|
referent_schema="transactions",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(
|
||||||
|
op.f("fk_categorized_name_categories_available"),
|
||||||
|
"categorized",
|
||||||
|
schema="transactions",
|
||||||
|
type_="foreignkey",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_categorized_name_categories_available",
|
||||||
|
"categorized",
|
||||||
|
"categories_available",
|
||||||
|
["name"],
|
||||||
|
["name"],
|
||||||
|
source_schema="transactions",
|
||||||
|
referent_schema="transactions",
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
58
alembic/versions/e36e6321568e_rules_min_max_money.py
Normal file
58
alembic/versions/e36e6321568e_rules_min_max_money.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"""Rules min/max money
|
||||||
|
|
||||||
|
Revision ID: e36e6321568e
|
||||||
|
Revises: 0ce89e987770
|
||||||
|
Create Date: 2022-12-10 18:55:07.149010+00:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "e36e6321568e"
|
||||||
|
down_revision = "0ce89e987770"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column(
|
||||||
|
"categories_rules",
|
||||||
|
"min_amount",
|
||||||
|
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
||||||
|
type_=sa.Numeric(precision=16, scale=2),
|
||||||
|
existing_nullable=True,
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
"categories_rules",
|
||||||
|
"max_amount",
|
||||||
|
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
||||||
|
type_=sa.Numeric(precision=16, scale=2),
|
||||||
|
existing_nullable=True,
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column(
|
||||||
|
"categories_rules",
|
||||||
|
"max_amount",
|
||||||
|
existing_type=sa.Numeric(precision=16, scale=2),
|
||||||
|
type_=sa.DOUBLE_PRECISION(precision=53),
|
||||||
|
existing_nullable=True,
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
op.alter_column(
|
||||||
|
"categories_rules",
|
||||||
|
"min_amount",
|
||||||
|
existing_type=sa.Numeric(precision=16, scale=2),
|
||||||
|
type_=sa.DOUBLE_PRECISION(precision=53),
|
||||||
|
existing_nullable=True,
|
||||||
|
schema="transactions",
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -6,24 +6,26 @@ if __name__ == "__main__":
|
|||||||
args = vars(argparser.parse_args())
|
args = vars(argparser.parse_args())
|
||||||
|
|
||||||
assert "op" in args, "No pfbudget.Operation selected"
|
assert "op" in args, "No pfbudget.Operation selected"
|
||||||
op: pfbudget.Operation = args["op"]
|
op: pfbudget.Operation = args.pop("op")
|
||||||
|
|
||||||
assert "database" in args, "No database selected"
|
assert "database" in args, "No database selected"
|
||||||
db = args["database"]
|
db = args.pop("database")
|
||||||
|
|
||||||
|
assert "verbose" in args, "No verbose level specified"
|
||||||
|
verbosity = args.pop("verbose")
|
||||||
|
|
||||||
params = None
|
params = None
|
||||||
match (op):
|
match (op):
|
||||||
case pfbudget.Operation.CategoryAdd:
|
case pfbudget.Operation.CategoryAdd:
|
||||||
assert args.keys() >= {"category", "group"}, "argparser ill defined"
|
assert args.keys() >= {"category", "group"}, "argparser ill defined"
|
||||||
params = [
|
params = [
|
||||||
pfbudget.types.Category(cat, args["group"][0])
|
pfbudget.types.Category(cat, args["group"]) for cat in args["category"]
|
||||||
for cat in args["category"]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
case pfbudget.Operation.CategoryUpdate:
|
case pfbudget.Operation.CategoryUpdate:
|
||||||
assert args.keys() >= {"category", "group"}, "argparser ill defined"
|
assert args.keys() >= {"category", "group"}, "argparser ill defined"
|
||||||
params = [pfbudget.types.Category(cat) for cat in args["category"]]
|
params = [pfbudget.types.Category(cat) for cat in args["category"]]
|
||||||
params.append(args["group"][0])
|
params.append(args["group"])
|
||||||
|
|
||||||
case pfbudget.Operation.CategoryRemove:
|
case pfbudget.Operation.CategoryRemove:
|
||||||
assert "category" in args, "argparser ill defined"
|
assert "category" in args, "argparser ill defined"
|
||||||
@ -43,7 +45,7 @@ if __name__ == "__main__":
|
|||||||
for cat in args["category"]
|
for cat in args["category"]
|
||||||
]
|
]
|
||||||
|
|
||||||
case pfbudget.Operation.CategoryRule:
|
case pfbudget.Operation.RuleAdd:
|
||||||
assert args.keys() >= {
|
assert args.keys() >= {
|
||||||
"category",
|
"category",
|
||||||
"date",
|
"date",
|
||||||
@ -58,6 +60,7 @@ if __name__ == "__main__":
|
|||||||
cat,
|
cat,
|
||||||
args["date"][0] if args["date"] else None,
|
args["date"][0] if args["date"] else None,
|
||||||
args["description"][0] if args["description"] else None,
|
args["description"][0] if args["description"] else None,
|
||||||
|
args["regex"][0] if args["regex"] else None,
|
||||||
args["bank"][0] if args["bank"] else None,
|
args["bank"][0] if args["bank"] else None,
|
||||||
args["min"][0] if args["min"] else None,
|
args["min"][0] if args["min"] else None,
|
||||||
args["max"][0] if args["max"] else None,
|
args["max"][0] if args["max"] else None,
|
||||||
@ -65,6 +68,79 @@ if __name__ == "__main__":
|
|||||||
for cat in args["category"]
|
for cat in args["category"]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
case pfbudget.Operation.RuleRemove | pfbudget.Operation.TagRuleRemove:
|
||||||
|
assert args.keys() >= {"id"}, "argparser ill defined"
|
||||||
|
params = args["id"]
|
||||||
|
|
||||||
|
case pfbudget.Operation.RuleModify:
|
||||||
|
assert args.keys() >= {
|
||||||
|
"id",
|
||||||
|
"category",
|
||||||
|
"date",
|
||||||
|
"description",
|
||||||
|
"bank",
|
||||||
|
"min",
|
||||||
|
"max",
|
||||||
|
"remove",
|
||||||
|
}, "argparser ill defined"
|
||||||
|
|
||||||
|
nargs_1 = ["category", "date", "description", "regex", "bank", "min", "max"]
|
||||||
|
params = []
|
||||||
|
for id in args["id"]:
|
||||||
|
param = {"id": id}
|
||||||
|
param |= {k: v[0] for k, v in args.items() if k in nargs_1 and args[k]}
|
||||||
|
param |= {k: None for k in args["remove"] if k in nargs_1}
|
||||||
|
|
||||||
|
params.append(param)
|
||||||
|
|
||||||
|
case pfbudget.Operation.TagAdd:
|
||||||
|
assert args.keys() >= {"tag"}, "argparser ill defined"
|
||||||
|
params = [pfbudget.types.Tag(tag) for tag in args["tag"]]
|
||||||
|
|
||||||
|
case pfbudget.Operation.TagRuleAdd:
|
||||||
|
assert args.keys() >= {
|
||||||
|
"tag",
|
||||||
|
"date",
|
||||||
|
"description",
|
||||||
|
"bank",
|
||||||
|
"min",
|
||||||
|
"max",
|
||||||
|
}, "argparser ill defined"
|
||||||
|
|
||||||
|
params = [
|
||||||
|
pfbudget.types.TagRule(
|
||||||
|
tag,
|
||||||
|
args["date"][0] if args["date"] else None,
|
||||||
|
args["description"][0] if args["description"] else None,
|
||||||
|
args["regex"][0] if args["regex"] else None,
|
||||||
|
args["bank"][0] if args["bank"] else None,
|
||||||
|
args["min"][0] if args["min"] else None,
|
||||||
|
args["max"][0] if args["max"] else None,
|
||||||
|
)
|
||||||
|
for tag in args["tag"]
|
||||||
|
]
|
||||||
|
|
||||||
|
case pfbudget.Operation.TagRuleModify:
|
||||||
|
assert args.keys() >= {
|
||||||
|
"id",
|
||||||
|
"tag",
|
||||||
|
"date",
|
||||||
|
"description",
|
||||||
|
"bank",
|
||||||
|
"min",
|
||||||
|
"max",
|
||||||
|
"remove",
|
||||||
|
}, "argparser ill defined"
|
||||||
|
|
||||||
|
nargs_1 = ["tag", "date", "description", "regex", "bank", "min", "max"]
|
||||||
|
params = []
|
||||||
|
for id in args["id"]:
|
||||||
|
param = {"id": id}
|
||||||
|
param |= {k: v[0] for k, v in args.items() if k in nargs_1 and args[k]}
|
||||||
|
param |= {k: None for k in args["remove"] if k in nargs_1}
|
||||||
|
|
||||||
|
params.append(param)
|
||||||
|
|
||||||
case pfbudget.Operation.GroupAdd:
|
case pfbudget.Operation.GroupAdd:
|
||||||
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"]]
|
||||||
@ -73,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"]]
|
||||||
|
|
||||||
pfbudget.Manager(db, args).action(op, params)
|
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)
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import argparse
|
import argparse
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
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
|
||||||
@ -39,12 +38,7 @@ def argparser() -> argparse.ArgumentParser:
|
|||||||
help="select current database",
|
help="select current database",
|
||||||
default=DEFAULT_DB,
|
default=DEFAULT_DB,
|
||||||
)
|
)
|
||||||
universal.add_argument(
|
universal.add_argument("-v", "--verbose", action="count", default=0)
|
||||||
"-q", "--quiet", action="store_true", help="reduces the amount of verbose"
|
|
||||||
)
|
|
||||||
universal.add_argument(
|
|
||||||
"-v", "--verbose", action="store_true", help="increases the amount of verbose"
|
|
||||||
)
|
|
||||||
|
|
||||||
period = argparse.ArgumentParser(add_help=False).add_mutually_exclusive_group()
|
period = argparse.ArgumentParser(add_help=False).add_mutually_exclusive_group()
|
||||||
period.add_argument(
|
period.add_argument(
|
||||||
@ -254,6 +248,12 @@ def argparser() -> argparse.ArgumentParser:
|
|||||||
category_parser = subparsers.add_parser("category", parents=[universal])
|
category_parser = subparsers.add_parser("category", parents=[universal])
|
||||||
category(category_parser, universal)
|
category(category_parser, universal)
|
||||||
|
|
||||||
|
# Tag
|
||||||
|
tags(subparsers.add_parser("tag", parents=[universal]), universal)
|
||||||
|
|
||||||
|
# Link
|
||||||
|
link(subparsers.add_parser("link"))
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@ -341,13 +341,7 @@ def category(parser: argparse.ArgumentParser, universal: argparse.ArgumentParser
|
|||||||
schedule.add_argument("--frequency", nargs=1, default=[1], type=int)
|
schedule.add_argument("--frequency", nargs=1, default=[1], type=int)
|
||||||
|
|
||||||
rule = commands.add_parser("rule", parents=[universal])
|
rule = commands.add_parser("rule", parents=[universal])
|
||||||
rule.set_defaults(op=Operation.CategoryRule)
|
category_rule(rule, universal)
|
||||||
rule.add_argument("category", nargs="+", type=str)
|
|
||||||
rule.add_argument("--date", nargs=1, type=dt.date.fromisoformat)
|
|
||||||
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])
|
group = commands.add_parser("group", parents=[universal])
|
||||||
category_group(group, universal)
|
category_group(group, universal)
|
||||||
@ -365,6 +359,82 @@ def category_group(parser: argparse.ArgumentParser, universal: argparse.Argument
|
|||||||
remove.add_argument("group", nargs="+", type=str)
|
remove.add_argument("group", nargs="+", type=str)
|
||||||
|
|
||||||
|
|
||||||
|
def category_rule(parser: argparse.ArgumentParser, universal: argparse.ArgumentParser):
|
||||||
|
commands = parser.add_subparsers(required=True)
|
||||||
|
|
||||||
|
add = commands.add_parser("add", parents=[universal])
|
||||||
|
add.set_defaults(op=Operation.RuleAdd)
|
||||||
|
add.add_argument("category", nargs="+", type=str)
|
||||||
|
rules(add)
|
||||||
|
|
||||||
|
remove = commands.add_parser("remove", parents=[universal])
|
||||||
|
remove.set_defaults(op=Operation.RuleRemove)
|
||||||
|
remove.add_argument("id", nargs="+", type=int)
|
||||||
|
|
||||||
|
modify = commands.add_parser("modify", parents=[universal])
|
||||||
|
modify.set_defaults(op=Operation.RuleModify)
|
||||||
|
modify.add_argument("id", nargs="+", type=int)
|
||||||
|
modify.add_argument("--category", nargs=1, type=str)
|
||||||
|
rules(modify)
|
||||||
|
|
||||||
|
|
||||||
|
def tags(parser: argparse.ArgumentParser, universal: argparse.ArgumentParser):
|
||||||
|
commands = parser.add_subparsers(required=True)
|
||||||
|
|
||||||
|
add = commands.add_parser("add", parents=[universal])
|
||||||
|
add.set_defaults(op=Operation.TagAdd)
|
||||||
|
add.add_argument("tag", nargs="+", type=str)
|
||||||
|
|
||||||
|
remove = commands.add_parser("remove", parents=[universal])
|
||||||
|
remove.set_defaults(op=Operation.TagRemove)
|
||||||
|
remove.add_argument("tag", nargs="+", type=str)
|
||||||
|
|
||||||
|
rule = commands.add_parser("rule", parents=[universal])
|
||||||
|
tag_rule(rule, universal)
|
||||||
|
|
||||||
|
|
||||||
|
def tag_rule(parser: argparse.ArgumentParser, universal: argparse.ArgumentParser):
|
||||||
|
commands = parser.add_subparsers(required=True)
|
||||||
|
|
||||||
|
add = commands.add_parser("add", parents=[universal])
|
||||||
|
add.set_defaults(op=Operation.TagRuleAdd)
|
||||||
|
add.add_argument("tag", nargs="+", type=str)
|
||||||
|
rules(add)
|
||||||
|
|
||||||
|
remove = commands.add_parser("remove", parents=[universal])
|
||||||
|
remove.set_defaults(op=Operation.TagRuleRemove)
|
||||||
|
remove.add_argument("id", nargs="+", type=int)
|
||||||
|
|
||||||
|
modify = commands.add_parser("modify", parents=[universal])
|
||||||
|
modify.set_defaults(op=Operation.TagRuleModify)
|
||||||
|
modify.add_argument("id", nargs="+", type=int)
|
||||||
|
modify.add_argument("--tag", nargs=1, type=str)
|
||||||
|
rules(modify)
|
||||||
|
|
||||||
|
|
||||||
|
def rules(parser: argparse.ArgumentParser):
|
||||||
|
parser.add_argument("--date", nargs=1, type=dt.date.fromisoformat)
|
||||||
|
parser.add_argument("--description", nargs=1, type=str)
|
||||||
|
parser.add_argument("--regex", nargs=1, type=str)
|
||||||
|
parser.add_argument("--bank", nargs=1, type=str)
|
||||||
|
parser.add_argument("--min", 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"
|
||||||
|
|||||||
@ -17,9 +17,18 @@ class Operation(Enum):
|
|||||||
CategoryUpdate = auto()
|
CategoryUpdate = auto()
|
||||||
CategoryRemove = auto()
|
CategoryRemove = auto()
|
||||||
CategorySchedule = auto()
|
CategorySchedule = auto()
|
||||||
CategoryRule = auto()
|
RuleAdd = auto()
|
||||||
|
RuleRemove = auto()
|
||||||
|
RuleModify = auto()
|
||||||
GroupAdd = auto()
|
GroupAdd = auto()
|
||||||
GroupRemove = auto()
|
GroupRemove = auto()
|
||||||
|
TagAdd = auto()
|
||||||
|
TagRemove = auto()
|
||||||
|
TagRuleAdd = auto()
|
||||||
|
TagRuleRemove = auto()
|
||||||
|
TagRuleModify = auto()
|
||||||
|
Forge = auto()
|
||||||
|
Dismantle = auto()
|
||||||
|
|
||||||
|
|
||||||
class TransactionError(Exception):
|
class TransactionError(Exception):
|
||||||
|
|||||||
@ -2,8 +2,10 @@ from pfbudget.db.model import (
|
|||||||
Category,
|
Category,
|
||||||
CategorySelector,
|
CategorySelector,
|
||||||
Selector,
|
Selector,
|
||||||
|
Tag,
|
||||||
Transaction,
|
Transaction,
|
||||||
TransactionCategory,
|
TransactionCategory,
|
||||||
|
TransactionTag,
|
||||||
)
|
)
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@ -15,7 +17,12 @@ class Categorizer:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.options["null_days"] = 4
|
self.options["null_days"] = 4
|
||||||
|
|
||||||
def categorize(self, transactions: list[Transaction], categories: list[Category]):
|
def categorize(
|
||||||
|
self,
|
||||||
|
transactions: list[Transaction],
|
||||||
|
categories: list[Category],
|
||||||
|
tags: list[Tag],
|
||||||
|
):
|
||||||
"""Overarching categorization tool
|
"""Overarching categorization tool
|
||||||
|
|
||||||
Receives a list of transactions (by ref) and updates their category
|
Receives a list of transactions (by ref) and updates their category
|
||||||
@ -25,7 +32,8 @@ class Categorizer:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self._nullify(transactions)
|
self._nullify(transactions)
|
||||||
self._rules(transactions, categories)
|
self._rule_based_categories(transactions, categories)
|
||||||
|
self._rule_based_tags(transactions, tags)
|
||||||
|
|
||||||
def _nullify(self, transactions: list[Transaction]):
|
def _nullify(self, transactions: list[Transaction]):
|
||||||
count = 0
|
count = 0
|
||||||
@ -57,27 +65,57 @@ class Categorizer:
|
|||||||
|
|
||||||
print(f"Nullified {count} transactions")
|
print(f"Nullified {count} transactions")
|
||||||
|
|
||||||
def _rules(self, transactions: list[Transaction], categories: list[Category]):
|
def _rule_based_categories(
|
||||||
|
self, transactions: list[Transaction], categories: list[Category]
|
||||||
|
):
|
||||||
|
d = {}
|
||||||
for category in [c for c in categories if c.rules]:
|
for category in [c for c in categories if c.rules]:
|
||||||
for rule in category.rules:
|
for rule in category.rules:
|
||||||
for transaction in [t for t in transactions if not t.category]:
|
# for transaction in [t for t in transactions if not t.category]:
|
||||||
if rule.date:
|
for transaction in [
|
||||||
if rule.date < transaction.date:
|
t
|
||||||
continue
|
for t in transactions
|
||||||
if rule.description:
|
if not t.category or t.category.name != "null"
|
||||||
if rule.description not in transaction.description:
|
]:
|
||||||
continue
|
if not rule.matches(transaction):
|
||||||
if rule.bank:
|
continue
|
||||||
if rule.bank != transaction.bank:
|
|
||||||
continue
|
|
||||||
if rule.min_amount:
|
|
||||||
if rule.min_amount > transaction.amount:
|
|
||||||
continue
|
|
||||||
if rule.max_amount:
|
|
||||||
if rule.max_amount <= transaction.amount:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# passed all conditions, assign category
|
# passed all conditions, assign category
|
||||||
transaction.category = TransactionCategory(
|
transaction.category = TransactionCategory(
|
||||||
category.name, CategorySelector(Selector.rules)
|
category.name, CategorySelector(Selector.rules)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if rule in d:
|
||||||
|
d[rule] += 1
|
||||||
|
else:
|
||||||
|
d[rule] = 1
|
||||||
|
|
||||||
|
for k, v in d.items():
|
||||||
|
print(f"{v}: {k}")
|
||||||
|
|
||||||
|
def _rule_based_tags(self, transactions: list[Transaction], tags: list[Tag]):
|
||||||
|
d = {}
|
||||||
|
for tag in [t for t in tags if t.rules]:
|
||||||
|
for rule in tag.rules:
|
||||||
|
# for transaction in [t for t in transactions if not t.category]:
|
||||||
|
for transaction in [
|
||||||
|
t
|
||||||
|
for t in transactions
|
||||||
|
if tag.name not in [tag.tag for tag in t.tags]
|
||||||
|
]:
|
||||||
|
if not rule.matches(transaction):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not transaction.tags:
|
||||||
|
transaction.tags = {TransactionTag(tag.name)}
|
||||||
|
else:
|
||||||
|
transaction.tags.add(TransactionTag(tag.name))
|
||||||
|
|
||||||
|
if rule in d:
|
||||||
|
d[rule] += 1
|
||||||
|
else:
|
||||||
|
d[rule] = 1
|
||||||
|
|
||||||
|
for k, v in d.items():
|
||||||
|
print(f"{v}: {k}")
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,14 @@ from pfbudget.input.input import Input
|
|||||||
from pfbudget.input.nordigen import NordigenClient
|
from pfbudget.input.nordigen import NordigenClient
|
||||||
from pfbudget.input.parsers import parse_data
|
from pfbudget.input.parsers import parse_data
|
||||||
from pfbudget.db.client import DbClient
|
from pfbudget.db.client import DbClient
|
||||||
from pfbudget.db.model import Category, CategoryGroup, CategoryRule, CategorySchedule
|
from pfbudget.db.model import (
|
||||||
|
Category,
|
||||||
|
CategoryGroup,
|
||||||
|
CategoryRule,
|
||||||
|
CategorySchedule,
|
||||||
|
Tag,
|
||||||
|
TagRule,
|
||||||
|
)
|
||||||
from pfbudget.common.types import Operation
|
from pfbudget.common.types import Operation
|
||||||
from pfbudget.core.categorizer import Categorizer
|
from pfbudget.core.categorizer import Categorizer
|
||||||
from pfbudget.utils import convert
|
from pfbudget.utils import convert
|
||||||
@ -11,13 +18,16 @@ from pfbudget.cli.runnable import download, parse
|
|||||||
|
|
||||||
|
|
||||||
class Manager:
|
class Manager:
|
||||||
def __init__(self, db: str, args: dict):
|
def __init__(self, db: str, verbosity: int = 0, args: dict = {}):
|
||||||
self._args = args
|
self._args = args
|
||||||
print(args)
|
|
||||||
|
|
||||||
self._db = db
|
self._db = db
|
||||||
|
self._verbosity = verbosity
|
||||||
|
|
||||||
def action(self, op: Operation, params: list):
|
def action(self, op: Operation, params: list):
|
||||||
|
if self._verbosity > 0:
|
||||||
|
print(f"op={op}, params={params}")
|
||||||
|
|
||||||
match (op):
|
match (op):
|
||||||
case Operation.Init:
|
case Operation.Init:
|
||||||
pass
|
pass
|
||||||
@ -27,11 +37,13 @@ class Manager:
|
|||||||
case Operation.Download:
|
case Operation.Download:
|
||||||
# TODO this is a monstrosity, remove when possible
|
# TODO this is a monstrosity, remove when possible
|
||||||
download(self, self.args)
|
download(self, self.args)
|
||||||
|
|
||||||
case Operation.Categorize:
|
case Operation.Categorize:
|
||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
uncategorized = session.uncategorized()
|
uncategorized = session.uncategorized()
|
||||||
categories = session.categories()
|
categories = session.categories()
|
||||||
Categorizer().categorize(uncategorized, categories)
|
tags = session.tags()
|
||||||
|
Categorizer().categorize(uncategorized, categories, tags)
|
||||||
|
|
||||||
case Operation.Register:
|
case Operation.Register:
|
||||||
# self._db = DbClient(args["database"])
|
# self._db = DbClient(args["database"])
|
||||||
@ -49,9 +61,9 @@ class Manager:
|
|||||||
self.args["name"], self.args["country"]
|
self.args["name"], self.args["country"]
|
||||||
)
|
)
|
||||||
|
|
||||||
case Operation.CategoryAdd:
|
case Operation.CategoryAdd | Operation.RuleAdd | Operation.TagAdd | Operation.TagRuleAdd:
|
||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
session.addcategories(params)
|
session.add(params)
|
||||||
|
|
||||||
case Operation.CategoryUpdate:
|
case Operation.CategoryUpdate:
|
||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
@ -59,26 +71,49 @@ class Manager:
|
|||||||
|
|
||||||
case Operation.CategoryRemove:
|
case Operation.CategoryRemove:
|
||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
session.removecategories(params)
|
session.remove_by_name(Category, params)
|
||||||
|
|
||||||
case Operation.CategorySchedule:
|
case Operation.CategorySchedule:
|
||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
session.updateschedules(params)
|
session.updateschedules(params)
|
||||||
|
|
||||||
case Operation.CategoryRule:
|
case Operation.RuleRemove:
|
||||||
|
assert all(isinstance(param, int) for param in params)
|
||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
session.addrules(params)
|
session.remove_by_id(CategoryRule, params)
|
||||||
|
|
||||||
|
case Operation.TagRemove:
|
||||||
|
with self.db.session() as session:
|
||||||
|
session.remove_by_name(Tag, params)
|
||||||
|
|
||||||
|
case Operation.TagRuleRemove:
|
||||||
|
assert all(isinstance(param, int) for param in params)
|
||||||
|
with self.db.session() as session:
|
||||||
|
session.remove_by_id(TagRule, params)
|
||||||
|
|
||||||
|
case Operation.RuleModify | Operation.TagRuleModify:
|
||||||
|
assert all(isinstance(param, dict) for param in params)
|
||||||
|
with self.db.session() as session:
|
||||||
|
session.updaterules(params)
|
||||||
|
|
||||||
case Operation.GroupAdd:
|
case Operation.GroupAdd:
|
||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
for group in self.args["group"]:
|
session.add(CategoryGroup(params))
|
||||||
session.addcategorygroup(CategoryGroup(name=group))
|
|
||||||
|
|
||||||
case Operation.GroupRemove:
|
case Operation.GroupRemove:
|
||||||
|
assert all(isinstance(param, CategoryGroup) for param in params)
|
||||||
with self.db.session() as session:
|
with self.db.session() as session:
|
||||||
session.removecategorygroup(
|
session.remove_by_name(CategoryGroup, params)
|
||||||
[CategoryGroup(name=group) for group in self.args["group"]]
|
|
||||||
)
|
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)
|
||||||
@ -119,7 +154,7 @@ class Manager:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def db(self) -> DbClient:
|
def db(self) -> DbClient:
|
||||||
return DbClient(self._db, self.args["verbose"])
|
return DbClient(self._db, self._verbosity > 0)
|
||||||
|
|
||||||
@db.setter
|
@db.setter
|
||||||
def db(self, url: str):
|
def db(self, url: str):
|
||||||
|
|||||||
@ -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,9 @@ from pfbudget.db.model import (
|
|||||||
CategoryGroup,
|
CategoryGroup,
|
||||||
CategoryRule,
|
CategoryRule,
|
||||||
CategorySchedule,
|
CategorySchedule,
|
||||||
|
Link,
|
||||||
|
Tag,
|
||||||
|
TagRule,
|
||||||
Transaction,
|
Transaction,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -81,16 +83,11 @@ class DbClient:
|
|||||||
def commit(self):
|
def commit(self):
|
||||||
self.__session.commit()
|
self.__session.commit()
|
||||||
|
|
||||||
def add(self, transactions: list[Transaction]):
|
def add(self, rows: list):
|
||||||
self.__session.add_all(transactions)
|
self.__session.add_all(rows)
|
||||||
|
|
||||||
def addcategories(self, category: list[Category]):
|
def remove_by_name(self, type: Category | Tag | Transaction, rows: list):
|
||||||
self.__session.add_all(category)
|
stmt = delete(type).where(type.name.in_([row.name for row in rows]))
|
||||||
|
|
||||||
def removecategories(self, categories: list[Category]):
|
|
||||||
stmt = delete(Category).where(
|
|
||||||
Category.name.in_([cat.name for cat in categories])
|
|
||||||
)
|
|
||||||
self.__session.execute(stmt)
|
self.__session.execute(stmt)
|
||||||
|
|
||||||
def updategroup(self, categories: list[Category], group: CategoryGroup):
|
def updategroup(self, categories: list[Category], group: CategoryGroup):
|
||||||
@ -113,16 +110,15 @@ class DbClient:
|
|||||||
)
|
)
|
||||||
self.__session.execute(stmt)
|
self.__session.execute(stmt)
|
||||||
|
|
||||||
def addrules(self, rules: list[CategoryRule]):
|
def remove_by_id(self, type: CategoryRule | TagRule, ids: list[int]):
|
||||||
self.__session.add_all(rules)
|
stmt = delete(type).where(type.id.in_(ids))
|
||||||
|
self.__session.execute(stmt)
|
||||||
|
|
||||||
def addcategorygroup(self, group: CategoryGroup):
|
def updaterules(self, rules: list[dict]):
|
||||||
self.__session.add(group)
|
self.__session.execute(update(CategoryRule), rules)
|
||||||
|
|
||||||
def removecategorygroup(self, groups: list[CategoryGroup]):
|
def remove_links(self, original, links: list):
|
||||||
stmt = delete(CategoryGroup).where(
|
stmt = delete(Link).where(Link.original == original, Link.link.in_(link for link in links))
|
||||||
CategoryGroup.name.in_([grp.name for grp in groups])
|
|
||||||
)
|
|
||||||
self.__session.execute(stmt)
|
self.__session.execute(stmt)
|
||||||
|
|
||||||
def uncategorized(self) -> list[Transaction]:
|
def uncategorized(self) -> list[Transaction]:
|
||||||
@ -133,5 +129,9 @@ class DbClient:
|
|||||||
stmt = select(Category)
|
stmt = select(Category)
|
||||||
return self.__session.scalars(stmt).all()
|
return self.__session.scalars(stmt).all()
|
||||||
|
|
||||||
|
def tags(self) -> list[Tag]:
|
||||||
|
stmt = select(Tag)
|
||||||
|
return self.__session.scalars(stmt).all()
|
||||||
|
|
||||||
def session(self) -> ClientSession:
|
def session(self) -> ClientSession:
|
||||||
return self.ClientSession(self.engine)
|
return self.ClientSession(self.engine)
|
||||||
|
|||||||
@ -21,6 +21,7 @@ from decimal import Decimal
|
|||||||
from typing import Annotated, Optional
|
from typing import Annotated, Optional
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import enum
|
import enum
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
class Base(MappedAsDataclass, DeclarativeBase):
|
class Base(MappedAsDataclass, DeclarativeBase):
|
||||||
@ -62,36 +63,25 @@ 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))]
|
||||||
|
|
||||||
idpk = Annotated[int, mapped_column(BigInteger, primary_key=True)]
|
idpk = Annotated[int, mapped_column(BigInteger, primary_key=True, autoincrement=True)]
|
||||||
money = Annotated[Decimal, mapped_column(Numeric(16, 2), nullable=False)]
|
money = Annotated[Decimal, mapped_column(Numeric(16, 2))]
|
||||||
|
|
||||||
|
|
||||||
class Transaction(Base):
|
class Transaction(Base):
|
||||||
__tablename__ = "originals"
|
__tablename__ = "originals"
|
||||||
|
|
||||||
id: Mapped[idpk] = mapped_column(autoincrement=True, init=False)
|
id: Mapped[idpk] = mapped_column(init=False)
|
||||||
date: Mapped[dt.date]
|
date: Mapped[dt.date]
|
||||||
description: Mapped[Optional[str]]
|
description: Mapped[Optional[str]]
|
||||||
bank: Mapped[bankfk]
|
bank: Mapped[bankfk]
|
||||||
amount: Mapped[money]
|
amount: Mapped[money]
|
||||||
|
|
||||||
category: Mapped[Optional[TransactionCategory]] = relationship()
|
category: Mapped[Optional[TransactionCategory]] = relationship()
|
||||||
note: Mapped[Optional[Note]] = relationship(back_populates="original", default=None)
|
note: Mapped[Optional[Note]] = relationship(back_populates="original")
|
||||||
tags: Mapped[Optional[set[Tag]]] = relationship(
|
tags: Mapped[Optional[set[TransactionTag]]] = relationship()
|
||||||
back_populates="original",
|
|
||||||
cascade="all, delete-orphan",
|
|
||||||
passive_deletes=True,
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
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[
|
||||||
@ -114,7 +104,7 @@ class Category(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
rules: Mapped[Optional[set[CategoryRule]]] = relationship(
|
rules: Mapped[Optional[set[CategoryRule]]] = relationship(
|
||||||
cascade="all, delete-orphan", passive_deletes=True, default=None
|
cascade="all, delete-orphan", passive_deletes=True, default_factory=set
|
||||||
)
|
)
|
||||||
schedule: Mapped[CategorySchedule] = relationship(
|
schedule: Mapped[CategorySchedule] = relationship(
|
||||||
back_populates="category", default=None
|
back_populates="category", default=None
|
||||||
@ -134,9 +124,9 @@ class TransactionCategory(Base):
|
|||||||
__tablename__ = "categorized"
|
__tablename__ = "categorized"
|
||||||
|
|
||||||
id: Mapped[idfk] = mapped_column(primary_key=True, init=False)
|
id: Mapped[idfk] = mapped_column(primary_key=True, init=False)
|
||||||
name: Mapped[str] = mapped_column(ForeignKey(Category.name))
|
name: Mapped[catfk]
|
||||||
|
|
||||||
selector: Mapped[CategorySelector] = relationship()
|
selector: Mapped[CategorySelector] = relationship(cascade="all, delete-orphan")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Category({self.name})"
|
return f"Category({self.name})"
|
||||||
@ -145,7 +135,7 @@ class TransactionCategory(Base):
|
|||||||
class Note(Base):
|
class Note(Base):
|
||||||
__tablename__ = "notes"
|
__tablename__ = "notes"
|
||||||
|
|
||||||
id: Mapped[idfk] = mapped_column(primary_key=True)
|
id: Mapped[idfk] = mapped_column(primary_key=True, init=False)
|
||||||
note: Mapped[str]
|
note: Mapped[str]
|
||||||
|
|
||||||
original: Mapped[Transaction] = relationship(back_populates="note")
|
original: Mapped[Transaction] = relationship(back_populates="note")
|
||||||
@ -161,29 +151,22 @@ 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"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(primary_key=True)
|
||||||
|
|
||||||
|
rules: Mapped[Optional[set[TagRule]]] = relationship(
|
||||||
|
cascade="all, delete-orphan", passive_deletes=True, default_factory=set
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionTag(Base):
|
||||||
__tablename__ = "tags"
|
__tablename__ = "tags"
|
||||||
|
|
||||||
id: Mapped[idfk] = mapped_column(primary_key=True)
|
id: Mapped[idfk] = mapped_column(primary_key=True, init=False)
|
||||||
tag: Mapped[str] = mapped_column(primary_key=True)
|
tag: Mapped[str] = mapped_column(ForeignKey(Tag.name), primary_key=True)
|
||||||
|
|
||||||
original: Mapped[Transaction] = relationship(back_populates="tags")
|
|
||||||
|
|
||||||
|
|
||||||
class CategoryRule(Base):
|
|
||||||
__tablename__ = "categories_rules"
|
|
||||||
|
|
||||||
id: Mapped[idpk] = mapped_column(autoincrement=True, init=False)
|
|
||||||
name: Mapped[catfk] = mapped_column()
|
|
||||||
date: Mapped[Optional[dt.date]] = 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()
|
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.id)
|
return hash(self.id)
|
||||||
@ -236,5 +219,60 @@ 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:
|
||||||
|
date: Mapped[Optional[dt.date]]
|
||||||
|
description: Mapped[Optional[str]]
|
||||||
|
regex: Mapped[Optional[str]]
|
||||||
|
bank: Mapped[Optional[str]]
|
||||||
|
min: Mapped[Optional[money]]
|
||||||
|
max: Mapped[Optional[money]]
|
||||||
|
|
||||||
|
def matches(self, transaction: Transaction) -> bool:
|
||||||
|
if (
|
||||||
|
(self.date and self.date < transaction.date)
|
||||||
|
or (
|
||||||
|
self.description
|
||||||
|
and transaction.description
|
||||||
|
and self.description not in transaction.description
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
self.regex
|
||||||
|
and transaction.description
|
||||||
|
and not re.compile(self.regex, re.IGNORECASE).search(
|
||||||
|
transaction.description
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or (self.bank and self.bank != transaction.bank)
|
||||||
|
or (self.min and self.min > transaction.amount)
|
||||||
|
or (self.max and self.max < transaction.amount)
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryRule(Base, Rule):
|
||||||
|
__tablename__ = "categories_rules"
|
||||||
|
|
||||||
|
id: Mapped[idpk] = mapped_column(init=False)
|
||||||
|
name: Mapped[catfk]
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.id)
|
||||||
|
|
||||||
|
|
||||||
|
class TagRule(Base, Rule):
|
||||||
|
__tablename__ = "tag_rules"
|
||||||
|
|
||||||
|
id: Mapped[idpk] = mapped_column(init=False)
|
||||||
|
tag: Mapped[str] = mapped_column(ForeignKey(Tag.name, ondelete="CASCADE"))
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.id)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user