From 07ff514c125279c33f4f77289e54ca8670551395 Mon Sep 17 00:00:00 2001 From: youngcw Date: Tue, 10 Feb 2026 18:52:49 -0700 Subject: [PATCH] [Goals] fix tracking budget balance carryover for templates (#6922) * fix tracking budget balance carryover for templates * Add release notes for PR #6922 * fix note * fix tests --------- Co-authored-by: github-actions[bot] --- .../budget/category-template-context.test.ts | 9 +++++++++ .../server/budget/category-template-context.ts | 18 +++++++++--------- upcoming-release-notes/6922.md | 6 ++++++ 3 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 upcoming-release-notes/6922.md diff --git a/packages/loot-core/src/server/budget/category-template-context.test.ts b/packages/loot-core/src/server/budget/category-template-context.test.ts index f320b17333..98c957597d 100644 --- a/packages/loot-core/src/server/budget/category-template-context.test.ts +++ b/packages/loot-core/src/server/budget/category-template-context.test.ts @@ -13,6 +13,7 @@ import { CategoryTemplateContext } from './category-template-context'; vi.mock('./actions', () => ({ getSheetValue: vi.fn(), getSheetBoolean: vi.fn(), + isReflectBudget: vi.fn(), })); vi.mock('../db', () => ({ @@ -1051,6 +1052,7 @@ describe('CategoryTemplateContext', () => { // Mock the sheet values needed for init vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover + vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false); mockPreferences(false, 'USD'); // Initialize the template @@ -1115,6 +1117,7 @@ describe('CategoryTemplateContext', () => { // Mock the sheet values needed for init vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover + vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false); mockPreferences(false, 'USD'); // Initialize the template @@ -1169,6 +1172,7 @@ describe('CategoryTemplateContext', () => { // Mock the sheet values needed for init vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover + vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false); mockPreferences(false, 'USD'); // Initialize the template @@ -1228,6 +1232,7 @@ describe('CategoryTemplateContext', () => { // Mock the sheet values needed for init vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover + vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false); mockPreferences(false, 'USD'); // Initialize the template @@ -1270,6 +1275,7 @@ describe('CategoryTemplateContext', () => { // Mock the sheet values needed for init vi.mocked(actions.getSheetValue).mockResolvedValueOnce(10000); // lastMonthBalance vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover + vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false); mockPreferences(false, 'USD'); // Initialize the template @@ -1314,6 +1320,7 @@ describe('CategoryTemplateContext', () => { // Mock the sheet values needed for init vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover + vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false); mockPreferences(true, 'USD'); // Initialize the template @@ -1356,6 +1363,7 @@ describe('CategoryTemplateContext', () => { vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); + vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false); mockPreferences(true, 'JPY'); const instance = await CategoryTemplateContext.init( @@ -1387,6 +1395,7 @@ describe('CategoryTemplateContext', () => { vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); + vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false); mockPreferences(true, 'JPY'); const instance = await CategoryTemplateContext.init( diff --git a/packages/loot-core/src/server/budget/category-template-context.ts b/packages/loot-core/src/server/budget/category-template-context.ts index 3bf5618903..1f19b2843a 100644 --- a/packages/loot-core/src/server/budget/category-template-context.ts +++ b/packages/loot-core/src/server/budget/category-template-context.ts @@ -21,7 +21,7 @@ import type { import { aqlQuery } from '../aql'; import * as db from '../db'; -import { getSheetBoolean, getSheetValue } from './actions'; +import { getSheetBoolean, getSheetValue, isReflectBudget } from './actions'; import { runSchedule } from './schedule-template'; import { getActiveSchedules } from './statements'; @@ -55,7 +55,7 @@ export class CategoryTemplateContext { const lastMonthSheet = monthUtils.sheetForMonth( monthUtils.subMonths(month, 1), ); - const lastMonthBalance = await getSheetValue( + let fromLastMonth = await getSheetValue( lastMonthSheet, `leftover-${category.id}`, ); @@ -63,15 +63,15 @@ export class CategoryTemplateContext { lastMonthSheet, `carryover-${category.id}`, ); - let fromLastMonth; - if (lastMonthBalance < 0 && !carryover) { + + if ( + (fromLastMonth < 0 && !carryover) || // overspend no carryover + category.is_income || // tracking budget income categories + (isReflectBudget() && !carryover) // tracking budget regular categories + ) { fromLastMonth = 0; - } else if (category.is_income) { - //for tracking budget - fromLastMonth = 0; - } else { - fromLastMonth = lastMonthBalance; } + // run all checks await CategoryTemplateContext.checkByAndScheduleAndSpend(templates, month); await CategoryTemplateContext.checkPercentage(templates); diff --git a/upcoming-release-notes/6922.md b/upcoming-release-notes/6922.md new file mode 100644 index 0000000000..e35c9e87bc --- /dev/null +++ b/upcoming-release-notes/6922.md @@ -0,0 +1,6 @@ +--- +category: Bugfixes +authors: [youngcw] +--- + +Fix template balance carryover handling in the tracking budget