From: Dustin Walde Date: Thu, 9 Nov 2023 01:00:15 +0000 (-0800) Subject: Update recurring plugin X-Git-Url: https://git.walde.dev/?a=commitdiff_plain;h=1dd8a4b9e23d15b872910cd0e60f09595db5ae7a;p=beanbeanbean Update recurring plugin - Add stopper to limit how far in the future to add transactions - Make recur/repeat and amortize different meta keywords - Add utils module to place shared logic --- diff --git a/src/beanbeanbean/recurring.py b/src/beanbeanbean/recurring.py index 9693bb9..590ca4b 100644 --- a/src/beanbeanbean/recurring.py +++ b/src/beanbeanbean/recurring.py @@ -1,4 +1,4 @@ -from datetime import date, datetime +from datetime import date, datetime, timedelta from decimal import ( Decimal, localcontext, @@ -15,8 +15,13 @@ from beancount.loader import LoadError from dateutil import rrule import recurrent +from .utils import flag + from typing import List, Optional, Union +RECURRING_KEYS = ['recur', 'recurring', 'repeat', 'repeating'] +AMORTIZE_KEYS = ['amortize'] + __plugins__ = ('recurring',) @@ -26,16 +31,52 @@ def generate_rrule_from_string(phrase: str, start_date: Optional[Union[date, dat r.parse(recurr_val) if r.dtstart is None and start_date is not None: r.dtstart = start_date + + latest_date = datetime.today()+timedelta(days=365) + if r.until is None or r.until > latest_date: + r.until = latest_date + return rrule.rrulestr(r.get_RFC_rrule()) +def is_recurring_transaction(entry) -> bool: + if type(entry) is not Transaction: + return False + + for key in AMORTIZE_KEYS + RECURRING_KEYS: + if key in entry.meta: + return True + return False + + def handle_recurring_transaction(txn: Transaction) -> Union[List[Transaction], LoadError]: post_vals = [] for post in txn.postings: post_vals.append([post.units, post.cost]) - amortize = 'amortize' in txn.meta - rr = generate_rrule_from_string(txn.meta['recurring'], txn.date) + match_key = None + amortize = False + phrase = None + for akey in AMORTIZE_KEYS: + if akey in txn.meta: + match_key = akey + amortize = True + phrase = txn.meta[akey] + break + if not amortize: + for rkey in RECURRING_KEYS: + if rkey in txn.meta: + match_key = rkey + phrase = txn.meta[rkey] + break + + if phrase is None: + return LoadError( + source=new_metadata(txn.meta["filename"], txn.meta["lineno"]), + message="Recurring metadata key found, but missing phrase", + entry=txn) + + rr = generate_rrule_from_string(phrase, txn.date) dates = [dt for dt in rr] # setup data_copy @@ -68,10 +109,11 @@ def handle_recurring_transaction(txn: Transaction) -> Union[List[Transaction], L # replace metadata with computed values to show it was processed # this dict is shared among all entries.. - txn.meta['rphrase'] = txn.meta['recurring'] - del txn.meta['recurring'] - txn.meta['rstart'] = txn.date - txn.meta['rcount'] = len(dates) + txn.meta['a͏mortize'] = amortize + txn.meta['p͏hrase'] = txn.meta[match_key] + del txn.meta[match_key] + txn.meta['s͏tart'] = txn.date + txn.meta['c͏ount'] = len(dates) return entries @@ -81,8 +123,7 @@ def recurring(entries: Entries, options_map, config_string=""): out_entries = [] errors = [] for entry in entries: - if type(entry) is not Transaction \ - or 'recurring' not in entry.meta: + if not is_recurring_transaction(entry): out_entries.append(entry) else: try: @@ -96,6 +137,7 @@ def recurring(entries: Entries, options_map, config_string=""): source=new_metadata(entry.meta["filename"], entry.meta["lineno"]), message="Failed to handle recurring transaction: {}".format(e), entry=entry)) + out_entries.append(flag(entry)) return out_entries, errors diff --git a/src/beanbeanbean/utils.py b/src/beanbeanbean/utils.py new file mode 100644 index 0000000..442f432 --- /dev/null +++ b/src/beanbeanbean/utils.py @@ -0,0 +1,8 @@ +from beancount.core.data import Transaction + + +def flag(entry): + tdict = entry._asdict() + tdict["flag"] = "!" + return Transaction(**tdict) +