Compare commits

...

3 Commits

Author SHA1 Message Date
fd24ac3318
Remove unnecessary __init__.pys
and import using submodules in __main__.py
2023-01-23 23:52:20 +00:00
36e7f84bd9
Interactive categorization moved to __main__.py
It didn't make sense to have it inside the manager, it should only be
used to process commands and its paramaters.
2023-01-23 23:31:38 +00:00
5235fcdfc3
Adds business logic for split transaction
Moves split member to be part of parent class and removes it from the
init.
2023-01-23 23:03:42 +00:00
15 changed files with 179 additions and 133 deletions

View File

@ -0,0 +1,40 @@
"""Split member of base transaction
Revision ID: 18572111d9ff
Revises: 28556ab17c56
Create Date: 2023-01-23 20:09:37.892997+00:00
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "18572111d9ff"
down_revision = "28556ab17c56"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"transactions",
"split",
existing_type=sa.BOOLEAN(),
nullable=False,
schema="transactions",
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"transactions",
"split",
existing_type=sa.BOOLEAN(),
nullable=True,
schema="transactions",
)
# ### end Alembic commands ###

View File

@ -1,12 +1,2 @@
__all__ = ["argparser", "Manager", "parse_data", "categorize_data"]
__author__ = "Luís Murta" __author__ = "Luís Murta"
__version__ = "0.1" __version__ = "0.1"
from pfbudget.common.types import Operation
from pfbudget.core.categories import categorize_data
from pfbudget.core.manager import Manager
from pfbudget.cli.runnable import argparser
from pfbudget.input.parsers import parse_data
from pfbudget.utils.utils import parse_args_period
import pfbudget.db.model as t

View File

@ -1,12 +1,57 @@
import pfbudget from pfbudget.cli.runnable import argparser
from pfbudget.common.types import Operation
from pfbudget.core.manager import Manager
import pfbudget.db.model as type
from pfbudget.utils.utils import parse_args_period
def interactive(manager: Manager):
with manager.db.session() as session:
categories = session.get(type.Category)
print(f"Available categories: {categories}")
print(f"Available tags: {session.get(type.Tag)}")
transactions = session.get(type.Transaction, ~type.Transaction.category.has())
print(f"{len(transactions)} transactions left to categorize")
for transaction in sorted(transactions):
print(f"{transaction}")
quit = False
next = True
while next:
match (input("(<category>/split/tag/note/quit): ")):
case "quit" | "exit":
next = False
quit = True
case "tag":
tag = input("tag: ")
transaction.tags.add(type.TransactionTag(tag))
case "note":
note = input("note: ")
transaction.note = type.Note(note)
case other:
if other not in [c.name for c in categories]:
print(f"{other} is not a valid category")
continue
transaction.category = type.TransactionCategory(
other,
type.CategorySelector(type.Selector_T.manual),
)
next = False
if quit:
break
if __name__ == "__main__": if __name__ == "__main__":
argparser = pfbudget.argparser() argparser = argparser()
args = vars(argparser.parse_args()) args = vars(argparser.parse_args())
assert "op" in args, "No pfbudget.Operation selected" assert "op" in args, "No Operation selected"
op: pfbudget.Operation = args.pop("op") op: Operation = args.pop("op")
assert "database" in args, "No database selected" assert "database" in args, "No database selected"
db = args.pop("database") db = args.pop("database")
@ -16,23 +61,27 @@ if __name__ == "__main__":
params = [] params = []
match (op): match (op):
case pfbudget.Operation.Parse: case Operation.ManualCategorization:
interactive(Manager(db, verbosity))
exit()
case Operation.Parse:
keys = {"path", "bank", "creditcard"} keys = {"path", "bank", "creditcard"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [args["path"], args["bank"], args["creditcard"]] params = [args["path"], args["bank"], args["creditcard"]]
case pfbudget.Operation.RequisitionId: case Operation.RequisitionId:
keys = {"name", "country"} keys = {"name", "country"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [args["name"][0], args["country"][0]] params = [args["name"][0], args["country"][0]]
case pfbudget.Operation.Download: case Operation.Download:
keys = {"all", "banks", "interval", "start", "end", "year", "dry_run"} keys = {"all", "banks", "interval", "start", "end", "year", "dry_run"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
start, end = pfbudget.parse_args_period(args) start, end = parse_args_period(args)
params = [start, end, args["dry_run"]] params = [start, end, args["dry_run"]]
if not args["all"]: if not args["all"]:
@ -40,19 +89,19 @@ if __name__ == "__main__":
else: else:
params.append([]) params.append([])
case pfbudget.Operation.BankAdd: case Operation.BankAdd:
keys = {"bank", "bic", "type"} keys = {"bank", "bic", "type"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [ params = [
pfbudget.t.Bank( type.Bank(
args["bank"][0], args["bank"][0],
args["bic"][0], args["bic"][0],
args["type"][0], args["type"][0],
) )
] ]
case pfbudget.Operation.BankMod: case Operation.BankMod:
keys = {"bank", "bic", "type", "remove"} keys = {"bank", "bic", "type", "remove"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
@ -64,16 +113,16 @@ if __name__ == "__main__":
params = [param] params = [param]
case pfbudget.Operation.BankDel: case Operation.BankDel:
assert len(args["bank"]) > 0, "argparser ill defined" assert len(args["bank"]) > 0, "argparser ill defined"
params = args["bank"] params = args["bank"]
case pfbudget.Operation.NordigenAdd: case Operation.NordigenAdd:
keys = {"bank", "bank_id", "requisition_id", "invert"} keys = {"bank", "bank_id", "requisition_id", "invert"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [ params = [
pfbudget.t.Nordigen( type.Nordigen(
args["bank"][0], args["bank"][0],
args["bank_id"][0] if args["bank_id"] else None, args["bank_id"][0] if args["bank_id"] else None,
args["requisition_id"][0] if args["requisition_id"] else None, args["requisition_id"][0] if args["requisition_id"] else None,
@ -81,7 +130,7 @@ if __name__ == "__main__":
) )
] ]
case pfbudget.Operation.NordigenMod: case Operation.NordigenMod:
keys = {"bank", "bank_id", "requisition_id", "invert", "remove"} keys = {"bank", "bank_id", "requisition_id", "invert", "remove"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
@ -95,52 +144,50 @@ if __name__ == "__main__":
params = [param] params = [param]
case pfbudget.Operation.NordigenDel: case Operation.NordigenDel:
assert len(args["bank"]) > 0, "argparser ill defined" assert len(args["bank"]) > 0, "argparser ill defined"
params = args["bank"] params = args["bank"]
case pfbudget.Operation.NordigenCountryBanks: case Operation.NordigenCountryBanks:
keys = {"country"} keys = {"country"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [args["country"][0]] params = [args["country"][0]]
case pfbudget.Operation.CategoryAdd: case Operation.CategoryAdd:
keys = {"category", "group"} keys = {"category", "group"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [ params = [type.Category(cat, args["group"]) for cat in args["category"]]
pfbudget.t.Category(cat, args["group"]) for cat in args["category"]
]
case pfbudget.Operation.CategoryUpdate: case Operation.CategoryUpdate:
keys = {"category", "group"} keys = {"category", "group"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [pfbudget.t.Category(cat) for cat in args["category"]] params = [type.Category(cat) for cat in args["category"]]
params.append(args["group"]) params.append(args["group"])
case pfbudget.Operation.CategoryRemove: case Operation.CategoryRemove:
assert "category" in args, "argparser ill defined" assert "category" in args, "argparser ill defined"
params = [pfbudget.t.Category(cat) for cat in args["category"]] params = [type.Category(cat) for cat in args["category"]]
case pfbudget.Operation.CategorySchedule: case Operation.CategorySchedule:
keys = {"category", "period", "frequency"} keys = {"category", "period", "frequency"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [ params = [
pfbudget.t.CategorySchedule( type.CategorySchedule(
cat, args["period"][0], args["frequency"][0], None cat, args["period"][0], args["frequency"][0], None
) )
for cat in args["category"] for cat in args["category"]
] ]
case pfbudget.Operation.RuleAdd: case Operation.RuleAdd:
keys = {"category", "date", "description", "bank", "min", "max"} keys = {"category", "date", "description", "bank", "min", "max"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [ params = [
pfbudget.t.CategoryRule( type.CategoryRule(
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["regex"][0] if args["regex"] else None,
@ -152,13 +199,13 @@ if __name__ == "__main__":
for cat in args["category"] for cat in args["category"]
] ]
case pfbudget.Operation.RuleRemove | pfbudget.Operation.TagRuleRemove: case Operation.RuleRemove | Operation.TagRuleRemove:
keys = {"id"} keys = {"id"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = args["id"] params = args["id"]
case pfbudget.Operation.RuleModify: case Operation.RuleModify:
keys = { keys = {
"id", "id",
"category", "category",
@ -180,18 +227,18 @@ if __name__ == "__main__":
params.append(param) params.append(param)
case pfbudget.Operation.TagAdd: case Operation.TagAdd:
keys = {"tag"} keys = {"tag"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [pfbudget.t.Tag(tag) for tag in args["tag"]] params = [type.Tag(tag) for tag in args["tag"]]
case pfbudget.Operation.TagRuleAdd: case Operation.TagRuleAdd:
keys = {"tag", "date", "description", "bank", "min", "max"} keys = {"tag", "date", "description", "bank", "min", "max"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [ params = [
pfbudget.t.TagRule( type.TagRule(
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["regex"][0] if args["regex"] else None,
@ -203,7 +250,7 @@ if __name__ == "__main__":
for tag in args["tag"] for tag in args["tag"]
] ]
case pfbudget.Operation.TagRuleModify: case Operation.TagRuleModify:
keys = {"id", "tag", "date", "description", "bank", "min", "max", "remove"} keys = {"id", "tag", "date", "description", "bank", "min", "max", "remove"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
@ -216,39 +263,37 @@ if __name__ == "__main__":
params.append(param) params.append(param)
case pfbudget.Operation.GroupAdd: case Operation.GroupAdd:
assert "group" in args, "argparser ill defined" assert "group" in args, "argparser ill defined"
params = [pfbudget.t.CategoryGroup(group) for group in args["group"]] params = [type.CategoryGroup(group) for group in args["group"]]
case pfbudget.Operation.GroupRemove: case Operation.GroupRemove:
assert "group" in args, "argparser ill defined" assert "group" in args, "argparser ill defined"
params = [pfbudget.t.CategoryGroup(group) for group in args["group"]] params = [type.CategoryGroup(group) for group in args["group"]]
case pfbudget.Operation.Forge | pfbudget.Operation.Dismantle: case Operation.Forge | Operation.Dismantle:
keys = {"original", "links"} keys = {"original", "links"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = [ params = [type.Link(args["original"][0], link) for link in args["links"]]
pfbudget.t.Link(args["original"][0], link) for link in args["links"]
]
case ( case (
pfbudget.Operation.Export Operation.Export
| pfbudget.Operation.Import | Operation.Import
| pfbudget.Operation.ExportBanks | Operation.ExportBanks
| pfbudget.Operation.ImportBanks | Operation.ImportBanks
| pfbudget.Operation.ExportCategoryRules | Operation.ExportCategoryRules
| pfbudget.Operation.ImportCategoryRules | Operation.ImportCategoryRules
| pfbudget.Operation.ExportTagRules | Operation.ExportTagRules
| pfbudget.Operation.ImportTagRules | Operation.ImportTagRules
| pfbudget.Operation.ExportCategories | Operation.ExportCategories
| pfbudget.Operation.ImportCategories | Operation.ImportCategories
| pfbudget.Operation.ExportCategoryGroups | Operation.ExportCategoryGroups
| pfbudget.Operation.ImportCategoryGroups | Operation.ImportCategoryGroups
): ):
keys = {"file"} keys = {"file"}
assert args.keys() >= keys, f"missing {args.keys() - keys}" assert args.keys() >= keys, f"missing {args.keys() - keys}"
params = args["file"] params = args["file"]
pfbudget.Manager(db, verbosity).action(op, params) Manager(db, verbosity).action(op, params)

View File

@ -11,25 +11,13 @@ from pfbudget.db.model import AccountType, Period
from pfbudget.db.sqlite import DatabaseClient from pfbudget.db.sqlite import DatabaseClient
import pfbudget.reporting.graph import pfbudget.reporting.graph
import pfbudget.reporting.report import pfbudget.reporting.report
import pfbudget.utils import pfbudget.utils.utils
load_dotenv() load_dotenv()
DEFAULT_DB = os.environ.get("DEFAULT_DB") DEFAULT_DB = os.environ.get("DEFAULT_DB")
class PfBudgetInitialized(Exception):
pass
class PfBudgetNotInitialized(Exception):
pass
class DataFileMissing(Exception):
pass
def argparser() -> argparse.ArgumentParser: def argparser() -> argparse.ArgumentParser:
universal = argparse.ArgumentParser(add_help=False) universal = argparse.ArgumentParser(add_help=False)
universal.add_argument( universal.add_argument(
@ -175,7 +163,7 @@ def graph(args):
Args: Args:
args (dict): argparse variables args (dict): argparse variables
""" """
start, end = pfbudget.utils.parse_args_period(args) start, end = pfbudget.utils.utils.parse_args_period(args)
if args.option == "monthly": if args.option == "monthly":
pfbudget.reporting.graph.monthly( pfbudget.reporting.graph.monthly(
DatabaseClient(args.database), vars(args), start, end DatabaseClient(args.database), vars(args), start, end
@ -196,7 +184,7 @@ def report(args):
Args: Args:
args (dict): argparse variables args (dict): argparse variables
""" """
start, end = pfbudget.utils.parse_args_period(args) start, end = pfbudget.utils.utils.parse_args_period(args)
if args.option == "net": if args.option == "net":
pfbudget.reporting.report.net(DatabaseClient(args.database), start, end) pfbudget.reporting.report.net(DatabaseClient(args.database), start, end)
elif args.option == "detailed": elif args.option == "detailed":

View File

@ -28,6 +28,7 @@ class Operation(Enum):
TagRuleModify = auto() TagRuleModify = auto()
Forge = auto() Forge = auto()
Dismantle = auto() Dismantle = auto()
Split = auto()
BankAdd = auto() BankAdd = auto()
BankMod = auto() BankMod = auto()
BankDel = auto() BankDel = auto()

View File

@ -33,21 +33,6 @@ class Categorizer:
self._rule_based_categories(transactions, categories) self._rule_based_categories(transactions, categories)
self._rule_based_tags(transactions, tags) self._rule_based_tags(transactions, tags)
def manual(
self,
transactions: Sequence[t.Transaction],
categories: Sequence[t.Category],
tags: Sequence[t.Tag],
):
"""Manual categorization input
Args:
transactions (Sequence[Transaction]): uncategorized transactions
categories (Sequence[Category]): available categories
tags (Sequence[Tag]): currently available tags
"""
self._manual(transactions)
@Timer(name="nullify") @Timer(name="nullify")
def _nullify(self, transactions: Sequence[t.BankTransaction]): def _nullify(self, transactions: Sequence[t.BankTransaction]):
count = 0 count = 0
@ -151,21 +136,3 @@ class Categorizer:
for k, v in d.items(): for k, v in d.items():
print(f"{v}: {k}") print(f"{v}: {k}")
def _manual(self, transactions: Sequence[t.Transaction]):
uncategorized = [t for t in transactions if not t.category]
print(f"{len(uncategorized)} transactions left to categorize")
for transaction in uncategorized:
while True:
category = input(f"{transaction} category: ")
if category == "quit":
return
if not category:
print("{category} doesn't exist")
continue
transaction.category = t.TransactionCategory(
category, t.CategorySelector(t.Selector_T.manual)
)
break

View File

@ -17,6 +17,7 @@ from pfbudget.db.model import (
MoneyTransaction, MoneyTransaction,
Nordigen, Nordigen,
Rule, Rule,
SplitTransaction,
Tag, Tag,
TagRule, TagRule,
Transaction, Transaction,
@ -87,15 +88,6 @@ class Manager:
tags = session.get(Tag) tags = session.get(Tag)
Categorizer().rules(uncategorized, categories, tags) Categorizer().rules(uncategorized, categories, tags)
case Operation.ManualCategorization:
with self.db.session() as session:
uncategorized = session.get(
Transaction, ~Transaction.category.has()
)
categories = session.get(Category)
tags = session.get(Tag)
Categorizer().manual(uncategorized, categories, tags)
case Operation.BankMod: case Operation.BankMod:
with self.db.session() as session: with self.db.session() as session:
session.update(Bank, params) session.update(Bank, params)
@ -180,6 +172,32 @@ class Manager:
links = [link.link for link in params] links = [link.link for link in params]
session.remove_links(original, links) session.remove_links(original, links)
case Operation.Split:
if len(params) < 1 and not all(
isinstance(p, Transaction) for p in params
):
raise TypeError(f"{params} are not transactions")
# t -> t1, t2, t3; t.value == Σti.value
original: Transaction = params[0]
if not original.amount == sum(t.amount for t in params[1:]):
raise ValueError(
f"{original.amount}€ != {sum(v for v, _ in params[1:])}"
)
with self.db.session() as session:
originals = session.get(Transaction, Transaction.id, [original.id])
assert len(originals) == 1, ">1 transactions matched {original.id}!"
originals[0].split = True
transactions = [
SplitTransaction(
originals[0].date, t.description, t.amount, originals[0].id
)
for t in params[1:]
]
session.add(transactions)
case Operation.Export: case Operation.Export:
with self.db.session() as session: with self.db.session() as session:
self.dump(params[0], sorted(session.get(Transaction))) self.dump(params[0], sorted(session.get(Transaction)))
@ -194,12 +212,11 @@ class Manager:
row["description"], row["description"],
row["amount"], row["amount"],
row["bank"], row["bank"],
False,
) )
case "money": case "money":
transaction = MoneyTransaction( transaction = MoneyTransaction(
row["date"], row["description"], row["amount"], False row["date"], row["description"], row["amount"]
) )
# TODO case "split" how to match to original transaction?? also save ids? # TODO case "split" how to match to original transaction?? also save ids?

View File

@ -91,6 +91,8 @@ class Transaction(Base, Export):
description: Mapped[Optional[str]] description: Mapped[Optional[str]]
amount: Mapped[money] amount: Mapped[money]
split: Mapped[bool] = mapped_column(init=False)
type: Mapped[str] = mapped_column(init=False) type: Mapped[str] = mapped_column(init=False)
category: Mapped[Optional[TransactionCategory]] = relationship(init=False) category: Mapped[Optional[TransactionCategory]] = relationship(init=False)
@ -105,6 +107,7 @@ class Transaction(Base, Export):
date=self.date, date=self.date,
description=self.description, description=self.description,
amount=self.amount, amount=self.amount,
split=self.split,
type=self.type, type=self.type,
category=self.category.format if self.category else None, category=self.category.format if self.category else None,
# TODO note # TODO note
@ -122,7 +125,6 @@ idfk = Annotated[
class BankTransaction(Transaction): class BankTransaction(Transaction):
bank: Mapped[bankfk] = mapped_column(nullable=True) bank: Mapped[bankfk] = mapped_column(nullable=True)
split: Mapped[bool] = mapped_column(use_existing_column=True, nullable=True)
__mapper_args__ = {"polymorphic_identity": "bank", "polymorphic_load": "inline"} __mapper_args__ = {"polymorphic_identity": "bank", "polymorphic_load": "inline"}
@ -132,8 +134,6 @@ class BankTransaction(Transaction):
class MoneyTransaction(Transaction): class MoneyTransaction(Transaction):
split: Mapped[bool] = mapped_column(use_existing_column=True, nullable=True)
__mapper_args__ = {"polymorphic_identity": "money"} __mapper_args__ = {"polymorphic_identity": "money"}

View File

@ -8,7 +8,7 @@ import time
import uuid import uuid
import pfbudget.db.model as t import pfbudget.db.model as t
import pfbudget.utils as utils from pfbudget.utils.converters import convert
from .input import Input from .input import Input
@ -72,7 +72,7 @@ class NordigenInput(Input):
json.dump(downloaded, f) json.dump(downloaded, f)
converted = [ converted = [
utils.convert(t, bank) for t in downloaded["transactions"]["booked"] convert(t, bank) for t in downloaded["transactions"]["booked"]
] ]
transactions.extend( transactions.extend(

View File

@ -1,2 +0,0 @@
from .converters import convert
from .utils import *