From e667e14e510bc0cb86cdd4f7db3a8dad3090906b Mon Sep 17 00:00:00 2001 From: youngcw Date: Wed, 13 Nov 2024 14:42:42 -0700 Subject: [PATCH 1/3] fix limits --- .../src/server/budget/categoryTemplate.ts | 91 ++++++++++--------- .../src/server/budget/goaltemplates.ts | 18 ++-- 2 files changed, 57 insertions(+), 52 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index a930fc601ff..53bdc818485 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -16,9 +16,8 @@ export class CategoryTemplate { * templates: all templates for this category (including templates and goals) * categoryID: the ID of the category that this Class will be for * month: the month string of the month for templates being applied - * 2. gather needed data for external use. ex: remainder weights, priorities + * 2. gather needed data for external use. ex: remainder weights, priorities, limitExcess * 3. run each priority level that is needed via runTemplatesForPriority - * 4. run applyLimits to apply any existing limit to the category * 5. run the remainder templates via runRemainder() * 6. finish processing by running getValues() and saving values for batch processing. * Alternate: @@ -49,6 +48,9 @@ export class CategoryTemplate { getRemainderWeight(): number { return this.remainderWeight; } + getLimitExcess(): number { + return this.limitExcess; + } // what is the full requested amount this month async runAll(available: number) { @@ -69,6 +71,7 @@ export class CategoryTemplate { availStart: number, ): Promise { if (!this.priorities.includes(priority)) return 0; + if (this.limitMet) return 0; const t = this.templates.filter(t => t.priority === priority); let available = budgetAvail || 0; @@ -137,6 +140,13 @@ export class CategoryTemplate { available = available - toBudget; } + //check limit + if (this.limitCheck) { + if (toBudget + this.fromLastMonth >= this.limitAmount) { + toBudget = this.limitAmount - this.fromLastMonth; + this.limitMet = true; + } + } // don't overbudget when using a priority if (priority > 0 && available < 0) { this.fullAmount += toBudget; @@ -149,25 +159,6 @@ export class CategoryTemplate { return toBudget; } - applyLimit(): number { - if (this.limitCheck === false) { - return 0; - } - if (this.limitHold && this.fromLastMonth >= this.limitAmount) { - const orig = this.toBudgetAmount; - this.fullAmount = 0; - this.toBudgetAmount = 0; - return orig; - } - if (this.toBudgetAmount + this.fromLastMonth > this.limitAmount) { - const orig = this.toBudgetAmount; - this.toBudgetAmount = this.limitAmount - this.fromLastMonth; - this.fullAmount = this.toBudgetAmount; - return orig - this.toBudgetAmount; - } - return 0; - } - // run all of the 'remainder' type templates runRemainder(budgetAvail: number, perWeight: number) { if (this.remainder.length === 0) return 0; @@ -206,6 +197,8 @@ export class CategoryTemplate { private isLongGoal: boolean = null; //defaulting the goals to null so templates can be unset private goalAmount: number = null; private fromLastMonth = 0; // leftover from last month + private limitMet = false; + private limitExcess: number = 0; private limitAmount = 0; private limitCheck = false; private limitHold = false; @@ -344,31 +337,43 @@ export class CategoryTemplate { if (!t.limit) continue; if (this.limitCheck) { throw new Error('Only one `up to` allowed per category'); - } else if (t.limit) { - if (t.limit.period === 'daily') { - const numDays = monthUtils.differenceInCalendarDays( - monthUtils.addMonths(this.month, 1), - this.month, - ); - this.limitAmount += amountToInteger(t.limit.amount) * numDays; - } else if (t.limit.period === 'weekly') { - const nextMonth = monthUtils.nextMonth(this.month); - let week = t.limit.start; - const baseLimit = amountToInteger(t.limit.amount); - while (week < nextMonth) { - if (week >= this.month) { - this.limitAmount += baseLimit; - } - week = monthUtils.addWeeks(week, 1); + } + if (t.limit.period === 'daily') { + const numDays = monthUtils.differenceInCalendarDays( + monthUtils.addMonths(this.month, 1), + this.month, + ); + this.limitAmount += amountToInteger(t.limit.amount) * numDays; + } else if (t.limit.period === 'weekly') { + const nextMonth = monthUtils.nextMonth(this.month); + let week = t.limit.start; + const baseLimit = amountToInteger(t.limit.amount); + while (week < nextMonth) { + if (week >= this.month) { + this.limitAmount += baseLimit; } - } else if (t.limit.period === 'monthly') { - this.limitAmount = amountToInteger(t.limit.amount); + week = monthUtils.addWeeks(week, 1); + } + } else if (t.limit.period === 'monthly') { + this.limitAmount = amountToInteger(t.limit.amount); + } else { + throw new Error('Invalid limit period. Check template syntax'); + } + //amount is good save the rest + this.limitCheck = true; + this.limitHold = t.limit.hold ? true : false; + // check if the limit is already met and save the excess + if (this.fromLastMonth >= this.limitAmount) { + this.limitMet = true; + if (this.limitHold) { + this.limitExcess = 0; + this.toBudgetAmount = 0; + this.fullAmount = 0; } else { - throw new Error('Invalid limit period. Check template syntax'); + this.limitExcess = this.fromLastMonth - this.limitAmount; + this.toBudgetAmount = -this.limitExcess; + this.fullAmount = -this.limitExcess; } - //amount is good save the rest - this.limitCheck = true; - this.limitHold = t.limit.hold ? true : false; } } } diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index b1cc0d01ef0..047aca545dc 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -166,6 +166,7 @@ async function processTemplate( try { const obj = await CategoryTemplate.init(templates, id, month); availBudget += budgeted; + availBudget += obj.getLimitExcess(); const p = obj.getPriorities(); p.forEach(pr => priorities.push(pr)); remainderWeight += obj.getRemainderWeight(); @@ -190,13 +191,15 @@ async function processTemplate( if (catObjects.length === 0 && errors.length === 0) { return { type: 'message', - message: t('Everything is up to date'), + //message: t('Everything is up to date'), + message: 'Everything is up to date', }; } if (errors.length > 0) { return { sticky: true, - message: t('There were errors interpreting some templates:'), + //message: t('There were errors interpreting some templates:'), + message: 'There were errors interpreting some templates:', pre: errors.join(`\n\n`), }; } @@ -221,10 +224,6 @@ async function processTemplate( availBudget -= ret; } } - // run limits - catObjects.forEach(o => { - availBudget += o.applyLimit(); - }); // run remainder if (availBudget > 0 && remainderWeight) { const perWeight = availBudget / remainderWeight; @@ -247,8 +246,9 @@ async function processTemplate( return { type: 'message', - message: t('Successfully applied templates to {length} categories', { - length: catObjects.length, - }), + //message: t('Successfully applied templates to {length} categories', { + // length: catObjects.length, + //}), + message: 'Successfully applied', }; } From c515adb32f4713cd0d455b3acbbcc9f116b00a24 Mon Sep 17 00:00:00 2001 From: youngcw Date: Wed, 13 Nov 2024 14:54:46 -0700 Subject: [PATCH 2/3] cleanup --- packages/loot-core/src/server/budget/categoryTemplate.ts | 4 ++-- packages/loot-core/src/server/budget/goaltemplates.ts | 4 ++-- upcoming-release-notes/3829.md | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 upcoming-release-notes/3829.md diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 53bdc818485..8103a3dbe7b 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -18,8 +18,8 @@ export class CategoryTemplate { * month: the month string of the month for templates being applied * 2. gather needed data for external use. ex: remainder weights, priorities, limitExcess * 3. run each priority level that is needed via runTemplatesForPriority - * 5. run the remainder templates via runRemainder() - * 6. finish processing by running getValues() and saving values for batch processing. + * 4. run the remainder templates via runRemainder() + * 5. finish processing by running getValues() and saving values for batch processing. * Alternate: * If the situation calls for it you can run all templates in a catagory in one go using the * method runAll which will run all templates and goals for reference, and can optionally be saved diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 047aca545dc..9b2cb5595a0 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -1,5 +1,5 @@ // @ts-strict-ignore -import { t } from 'i18next'; +//import { t } from 'i18next'; import { Notification } from '../../client/state-types/notifications'; import * as monthUtils from '../../shared/months'; @@ -249,6 +249,6 @@ async function processTemplate( //message: t('Successfully applied templates to {length} categories', { // length: catObjects.length, //}), - message: 'Successfully applied', + message: `Successfully applied templates to ${catObjects.length} categories`, }; } diff --git a/upcoming-release-notes/3829.md b/upcoming-release-notes/3829.md new file mode 100644 index 00000000000..316a9ffb0db --- /dev/null +++ b/upcoming-release-notes/3829.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [youngcw] +--- + +Fix template limits From 0a6f162041894346c5d8d66f2e985810e76c1e41 Mon Sep 17 00:00:00 2001 From: youngcw Date: Wed, 13 Nov 2024 16:01:30 -0700 Subject: [PATCH 3/3] fix cases of negative previous balance --- .../loot-core/src/server/budget/actions.ts | 9 ++++++ .../src/server/budget/categoryTemplate.ts | 28 +++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/loot-core/src/server/budget/actions.ts b/packages/loot-core/src/server/budget/actions.ts index e0bfbf05c14..ebd1b0bf76f 100644 --- a/packages/loot-core/src/server/budget/actions.ts +++ b/packages/loot-core/src/server/budget/actions.ts @@ -1,4 +1,5 @@ // @ts-strict-ignore + import * as monthUtils from '../../shared/months'; import { integerToCurrency, safeNumber } from '../../shared/util'; import * as db from '../db'; @@ -13,6 +14,14 @@ export async function getSheetValue( return safeNumber(typeof node.value === 'number' ? node.value : 0); } +export async function getSheetBoolean( + sheetName: string, + cell: string, +): Promise { + const node = await sheet.getCell(sheetName, cell); + return typeof node.value === 'boolean' ? node.value : false; +} + // We want to only allow the positive movement of money back and // forth. buffered should never be allowed to go into the negative, // and you shouldn't be allowed to pull non-existent money from diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 8103a3dbe7b..0f1e3787330 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -4,7 +4,7 @@ import * as monthUtils from '../../shared/months'; import { amountToInteger } from '../../shared/util'; import * as db from '../db'; -import { getSheetValue } from './actions'; +import { getSheetValue, getSheetBoolean } from './actions'; import { goalsSchedule } from './goalsSchedule'; import { getActiveSchedules } from './statements'; import { Template } from './types/templates'; @@ -31,10 +31,23 @@ export class CategoryTemplate { // set up the class and check all templates static async init(templates: Template[], categoryID: string, month) { // get all the needed setup values - const fromLastMonth = await getSheetValue( - monthUtils.sheetForMonth(monthUtils.subMonths(month, 1)), + const lastMonthSheet = monthUtils.sheetForMonth( + monthUtils.subMonths(month, 1), + ); + const lastMonthBalance = await getSheetValue( + lastMonthSheet, `leftover-${categoryID}`, ); + const carryover = await getSheetBoolean( + lastMonthSheet, + `carryover-${categoryID}`, + ); + let fromLastMonth; + if (lastMonthBalance < 0 && !carryover) { + fromLastMonth = 0; + } else { + fromLastMonth = lastMonthBalance; + } // run all checks await CategoryTemplate.checkByAndScheduleAndSpend(templates, month); await CategoryTemplate.checkPercentage(templates); @@ -142,9 +155,14 @@ export class CategoryTemplate { //check limit if (this.limitCheck) { - if (toBudget + this.fromLastMonth >= this.limitAmount) { - toBudget = this.limitAmount - this.fromLastMonth; + if ( + toBudget + this.toBudgetAmount + this.fromLastMonth >= + this.limitAmount + ) { + const orig = toBudget; + toBudget = this.limitAmount - this.toBudgetAmount - this.fromLastMonth; this.limitMet = true; + available = available + orig - toBudget; } } // don't overbudget when using a priority