]> git.walde.dev - beanbeanbean/commitdiff
Update recurring plugin
authorDustin Walde <redacted>
Thu, 9 Nov 2023 01:00:15 +0000 (17:00 -0800)
committerDustin Walde <redacted>
Thu, 9 Nov 2023 01:00:15 +0000 (17:00 -0800)
- 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

src/beanbeanbean/recurring.py
src/beanbeanbean/utils.py [new file with mode: 0644]

index 9693bb9b78269a10524d0f8b9beb6e1e7397f2f7..590ca4ba4a6cb9ce3b1d2355ca2ea925ca4842c7 100644 (file)
@@ -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 (file)
index 0000000..442f432
--- /dev/null
@@ -0,0 +1,8 @@
+from beancount.core.data import Transaction
+
+
+def flag(entry):
+    tdict = entry._asdict()
+    tdict["flag"] = "!"
+    return Transaction(**tdict)
+