From ed285e9ac5731af20035c09c20520b4e821d4482 Mon Sep 17 00:00:00 2001
From: youngcw <calebyoung94@gmail.com>
Date: Tue, 6 Jun 2023 13:41:09 -0700
Subject: [PATCH] Goals: Remainder option (#1101)

Added the option to add a remainder goal template. This will use the
remaining available funds and dump them into the respective category.
There is optional weighting. The remainder templates will be forced to
the lowest priority as to run after all other templates.

Usage: `#template remainder <weight>` Add the template line to any
categories you want to catch any remaining funds such as savings. The
amount added to the category will equal
`remaining_budget/total_of_weights*weight`. The default weight is 1.
---
 .../src/server/budget/goal-template.pegjs     |  9 +++--
 .../src/server/budget/goaltemplates.ts        | 35 +++++++++++++++++++
 upcoming-release-notes/1101.md                |  6 ++++
 3 files changed, 47 insertions(+), 3 deletions(-)
 create mode 100644 upcoming-release-notes/1101.md

diff --git a/packages/loot-core/src/server/budget/goal-template.pegjs b/packages/loot-core/src/server/budget/goal-template.pegjs
index 9c3fd1b80..e0b6d97c8 100644
--- a/packages/loot-core/src/server/budget/goal-template.pegjs
+++ b/packages/loot-core/src/server/budget/goal-template.pegjs
@@ -15,12 +15,14 @@ expr
       priority: +priority
     } }
   / priority: priority? _? monthly: amount limit: limit?
-    { return { type: 'simple', monthly, limit, priority: +priority  } } 
+    { return { type: 'simple', monthly, limit, priority: +priority } }
   / priority: priority? _? limit: limit
     { return { type: 'simple', limit , priority: +priority } }
   / priority: priority? _? schedule _ full:full? name: name
-  	{ return { type: 'schedule', name, priority: +priority, full } }
-  
+    { return { type: 'schedule', name, priority: +priority, full } }
+  / priority: priority? _? remainder: remainder
+    { return { type: 'remainder', priority: null, weight: remainder } }
+
 
 repeat 'repeat interval'
   = 'month'i { return { annual: false } }
@@ -48,6 +50,7 @@ upTo = 'up'i _ 'to'i
 schedule = 'schedule'i
 full = 'full'i _ {return true}
 priority = '-'i number: number _ {return number}
+remainder = 'remainder'i _? weight: positive? { return +weight || 1 }
 
 _ 'space' = ' '+
 d 'digit' = [0-9]
diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts
index 66644df6e..94a1e4b1a 100644
--- a/packages/loot-core/src/server/budget/goaltemplates.ts
+++ b/packages/loot-core/src/server/budget/goaltemplates.ts
@@ -80,8 +80,35 @@ async function processTemplate(month, force) {
       });
     }
   }
+  // find all remainder templates, place them after all other templates
+  let remainder_found;
+  let remainder_priority = lowestPriority + 1;
+  let remainder_weight_total = 0;
+  for (let c = 0; c < categories.length; c++) {
+    let category = categories[c];
+    let templates = category_templates[category.id];
+    if (templates) {
+      for (let i = 0; i < templates.length; i++) {
+        if (templates[i].type === 'remainder') {
+          templates[i].priority = remainder_priority;
+          remainder_weight_total += templates[i].weight;
+          remainder_found = true;
+        }
+      }
+    }
+  }
+  // so the remainders don't get skiped
+  if (remainder_found) lowestPriority = remainder_priority;
 
   for (let priority = 0; priority <= lowestPriority; priority++) {
+    // setup scaling for remainder
+    let remainder_scale = 1;
+    if (priority === lowestPriority) {
+      let sheetName = monthUtils.sheetForMonth(month);
+      let budgetAvailable = await getSheetValue(sheetName, `to-budget`);
+      remainder_scale = Math.round(budgetAvailable / remainder_weight_total);
+    }
+
     for (let c = 0; c < categories.length; c++) {
       let category = categories[c];
       let template = category_templates[category.id];
@@ -132,6 +159,7 @@ async function processTemplate(month, force) {
                 template,
                 month,
                 priority,
+                remainder_scale,
                 force,
               );
             if (to_budget != null) {
@@ -235,6 +263,7 @@ async function applyCategoryTemplate(
   template_lines,
   month,
   priority,
+  remainder_scale,
   force,
 ) {
   let current_month = getCorrectedDate(`${month}-01`);
@@ -549,6 +578,12 @@ async function applyCategoryTemplate(
         }
         break;
       }
+      case 'remainder': {
+        to_budget = Math.round(remainder_scale * template.weight);
+        // can over budget with the rounding, so checking that
+        if (to_budget > budgetAvailable) to_budget = budgetAvailable;
+        break;
+      }
       case 'error':
         return { errors };
       default:
diff --git a/upcoming-release-notes/1101.md b/upcoming-release-notes/1101.md
new file mode 100644
index 000000000..70ad7e55d
--- /dev/null
+++ b/upcoming-release-notes/1101.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [youngcw]
+---
+
+Goals:  Add remainder option to budget all extra funds automatically.
-- 
GitLab