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:
parent
ac4e14ca35
commit
865874f637
@ -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"]
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user