diff --git a/categories.yaml b/categories.yaml index 8c80ae8..07020ae 100644 --- a/categories.yaml +++ b/categories.yaml @@ -1,3 +1,15 @@ +Groups: + income: + color: limegreen + fixed: + color: tab:blue + required: + color: tab:orange + health: + color: tab:red + discretionary: + color: tab:brown + Income1: group: income regex: @@ -69,9 +81,9 @@ Travel: - ryanair - easyjet - airbnb - negative_regex: - - Commute - - Utilities + not_in_groups: + - income + - fixed date_fmt: "%Y-%m-%d" vacations: - ["2019-12-23", "2020-01-02"] diff --git a/pfbudget/categories.py b/pfbudget/categories.py index 398d8d2..ecd9405 100644 --- a/pfbudget/categories.py +++ b/pfbudget/categories.py @@ -19,7 +19,7 @@ Options = namedtuple( "regex", "banks", "regular", - "negative_regex", + "not_in_groups", "date_fmt", "vacations", "timedelta", @@ -29,7 +29,9 @@ Options = namedtuple( cfg = yaml.safe_load(open("categories.yaml")) try: - categories = {k: Options(**v) if v else Options() for k, v in cfg.items()} + categories = { + k: Options(**v) if v and k != "Groups" else Options() for k, v in cfg.items() + } except TypeError: logging.exception("Invalid option in categories.yaml") categories = {} @@ -42,6 +44,12 @@ groups = { } categories.setdefault("Null", Options()) +order = {k: i for i, k in enumerate(cfg["Groups"])} +groups = { + group: groups[group] + for group in sorted(groups, key=lambda x: order.get(x, len(groups))) +} + def categorize_data(db: DBManager): @@ -71,9 +79,11 @@ def categorize_data(db: DBManager): ) for transaction in transactions: while True: - category = input(f"{transaction} category: ") + category = input(f"{repr(transaction)} category: ") if category == "quit" or category == "exit": return + if not category: + break if category not in categories: print( f"Category {category} doesn't exist. Please use one of {categories.keys()}" @@ -95,20 +105,28 @@ def vacations(db: DBManager) -> None: logging.warning(f"{e} continuing...") continue - not_vacations = categories["Travel"].negative_regex # default is [] + not_in_groups = categories["Travel"].not_in_groups # default is [] + update = False if transactions := db.get_daterange_uncategorized_transactions(start, end): for transaction in transactions: - if not_vacations: - for category in not_vacations: - if not matches( - transaction, categories.get(category, Options()) - ): - transaction.category = "Travel" + if not_in_groups: + if not any( + matches( + transaction, + categories.get(category, Options()), + ) + for group in not_in_groups + for category in groups[group] + ): + transaction.category = "Travel" + update = True else: transaction.category = "Travel" + update = True - db.update_categories(transactions) + if update: + db.update_categories(transactions) except KeyError as e: print(e) @@ -151,7 +169,7 @@ def matches(transaction: Transaction, category: Options): return False try: return any( - re.compile(pattern).search(transaction.description.lower()) + re.compile(pattern.lower()).search(transaction.description.lower()) for pattern in category.regex ) except re.error as e: diff --git a/pfbudget/graph.py b/pfbudget/graph.py index 42034f0..8f315f5 100644 --- a/pfbudget/graph.py +++ b/pfbudget/graph.py @@ -12,6 +12,9 @@ if TYPE_CHECKING: from pfbudget.database import DBManager +groups = pfbudget.categories.cfg["Groups"] + + def monthly( db: DBManager, args: dict, start: dt.date = dt.date.min, end: dt.date = dt.date.max ): @@ -45,6 +48,7 @@ def monthly( plt.plot( list(rrule(MONTHLY, dtstart=start.replace(day=1), until=end.replace(day=1))), [groups["income"] for _, groups in monthly_transactions], + color=groups["income"]["color"], ) plt.stackplot( list(rrule(MONTHLY, dtstart=start.replace(day=1), until=end.replace(day=1))), @@ -58,6 +62,11 @@ def monthly( for group in pfbudget.categories.groups if group != "income" and group != "investment" ], + colors=[ + groups.get(group, {"color": "gray"})["color"] + for group in pfbudget.categories.groups + if group != "income" and group != "investment" + ], ) plt.legend(loc="upper left") if args["save"]: @@ -95,7 +104,7 @@ def discrete( ] ) - plt.figure(figsize=(30, 10)) + plt.figure(tight_layout=True) plt.plot( list(rrule(MONTHLY, dtstart=start.replace(day=1), until=end.replace(day=1))), [ @@ -106,6 +115,7 @@ def discrete( ) for _, categories in monthly_transactions ], + color=groups["income"]["color"], ) plt.stackplot( list(rrule(MONTHLY, dtstart=start.replace(day=1), until=end.replace(day=1))), @@ -114,16 +124,18 @@ def discrete( for category in pfbudget.categories.categories if category not in pfbudget.categories.groups["income"] and category not in pfbudget.categories.groups["investment"] + and category != "Null" ], labels=[ category for category in pfbudget.categories.categories if category not in pfbudget.categories.groups["income"] and category not in pfbudget.categories.groups["investment"] + and category != "Null" ], ) + plt.grid() plt.legend(loc="upper left") - plt.tight_layout() if args["save"]: plt.savefig("graph.png") else: diff --git a/pfbudget/transactions.py b/pfbudget/transactions.py index ceb2cf8..f8ed94b 100644 --- a/pfbudget/transactions.py +++ b/pfbudget/transactions.py @@ -84,12 +84,16 @@ class Transaction: def __ge__(self, other): return self.date >= other.date - def desc(self): - return "{} {} {}€ ({})".format( - self.date.strftime("%d/%m/%y"), self.description, self.value, self.bank + def __repr__(self): + return "{} {} ({}) {}€ at {}".format( + self.date.strftime("%d/%m/%y"), + self.description, + self.category, + self.value, + self.bank, ) - def __repr__(self): - return "{} {} {}€ ({})".format( + def __str__(self): + return "{} {} {}€ at {}".format( self.date.strftime("%d/%m/%y"), self.category, self.value, self.bank )