Adds group configuration and bugfixes

Now possible to configure each group in the categories.yaml. Only color
added at this moment. The order in which the groups are listed is used
by the graph module and will later be used by the report.

Fixes search for vacations. The negative match is now done by groups,
and the key changes from negative_regex to not_in_groups.
Only updates transactions when any travel is assigned. Was previously
updating all uncategorized transactions that reached the travel search.

Regex match now correctly done lower letters to lower letters.

Transaction __repr__ method added and updated along with __str__, to
provide a more clearer output when manually categorizing.
This commit is contained in:
Luís Murta 2021-08-10 23:35:47 +01:00
parent ac4e14ca35
commit 865874f637
Signed by: satprog
GPG Key ID: DDF2EFC6179009DC
4 changed files with 68 additions and 22 deletions

View File

@ -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"]

View File

@ -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:

View File

@ -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:

View File

@ -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
)