mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
Fix schedule split template amounts (#4077)
* Fix incorrect argument to goals schedule function * Add argument types to prevent similar issues * Add release notes * Fix test types
This commit is contained in:
committed by
GitHub
parent
928260ca3a
commit
832fd1e5d8
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as monthUtils from '../../shared/months';
|
||||
import { amountToInteger } from '../../shared/util';
|
||||
import { CategoryEntity } from '../../types/models';
|
||||
import * as db from '../db';
|
||||
|
||||
import { getSheetValue, getSheetBoolean } from './actions';
|
||||
@@ -29,18 +30,18 @@ export class CategoryTemplate {
|
||||
// Class interface
|
||||
|
||||
// set up the class and check all templates
|
||||
static async init(templates: Template[], categoryID: string, month) {
|
||||
static async init(templates: Template[], category: CategoryEntity, month) {
|
||||
// get all the needed setup values
|
||||
const lastMonthSheet = monthUtils.sheetForMonth(
|
||||
monthUtils.subMonths(month, 1),
|
||||
);
|
||||
const lastMonthBalance = await getSheetValue(
|
||||
lastMonthSheet,
|
||||
`leftover-${categoryID}`,
|
||||
`leftover-${category.id}`,
|
||||
);
|
||||
const carryover = await getSheetBoolean(
|
||||
lastMonthSheet,
|
||||
`carryover-${categoryID}`,
|
||||
`carryover-${category.id}`,
|
||||
);
|
||||
let fromLastMonth;
|
||||
if (lastMonthBalance < 0 && !carryover) {
|
||||
@@ -52,7 +53,7 @@ export class CategoryTemplate {
|
||||
await CategoryTemplate.checkByAndScheduleAndSpend(templates, month);
|
||||
await CategoryTemplate.checkPercentage(templates);
|
||||
// call the private constructor
|
||||
return new CategoryTemplate(templates, categoryID, month, fromLastMonth);
|
||||
return new CategoryTemplate(templates, category, month, fromLastMonth);
|
||||
}
|
||||
|
||||
getPriorities(): number[] {
|
||||
@@ -126,7 +127,7 @@ export class CategoryTemplate {
|
||||
case 'schedule': {
|
||||
const budgeted = await getSheetValue(
|
||||
monthUtils.sheetForMonth(this.month),
|
||||
`leftover-${this.categoryID}`,
|
||||
`leftover-${this.category.id}`,
|
||||
);
|
||||
const ret = await goalsSchedule(
|
||||
scheduleFlag,
|
||||
@@ -137,7 +138,7 @@ export class CategoryTemplate {
|
||||
this.fromLastMonth,
|
||||
toBudget,
|
||||
[],
|
||||
this.categoryID,
|
||||
this.category,
|
||||
);
|
||||
toBudget = ret.to_budget;
|
||||
remainder = ret.remainder;
|
||||
@@ -203,7 +204,7 @@ export class CategoryTemplate {
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Implementation
|
||||
readonly categoryID: string; //readonly so we can double check the category this is using
|
||||
readonly category: CategoryEntity; //readonly so we can double check the category this is using
|
||||
private month: string;
|
||||
private templates = [];
|
||||
private remainder = [];
|
||||
@@ -223,11 +224,11 @@ export class CategoryTemplate {
|
||||
|
||||
private constructor(
|
||||
templates: Template[],
|
||||
categoryID: string,
|
||||
category: CategoryEntity,
|
||||
month: string,
|
||||
fromLastMonth: number,
|
||||
) {
|
||||
this.categoryID = categoryID;
|
||||
this.category = category;
|
||||
this.month = month;
|
||||
this.fromLastMonth = fromLastMonth;
|
||||
// sort the template lines into regular template, goals, and remainder templates
|
||||
@@ -424,7 +425,7 @@ export class CategoryTemplate {
|
||||
const sheetName = monthUtils.sheetForMonth(
|
||||
monthUtils.subMonths(this.month, template.lookBack),
|
||||
);
|
||||
return await getSheetValue(sheetName, `budget-${this.categoryID}`);
|
||||
return await getSheetValue(sheetName, `budget-${this.category.id}`);
|
||||
}
|
||||
|
||||
private runWeek(template): number {
|
||||
@@ -472,18 +473,18 @@ export class CategoryTemplate {
|
||||
//TODO figure out if I already found these values and can pass them in
|
||||
const spent = await getSheetValue(
|
||||
sheetName,
|
||||
`sum-amount-${this.categoryID}`,
|
||||
`sum-amount-${this.category.id}`,
|
||||
);
|
||||
const balance = await getSheetValue(
|
||||
sheetName,
|
||||
`leftover-${this.categoryID}`,
|
||||
`leftover-${this.category.id}`,
|
||||
);
|
||||
alreadyBudgeted = balance - spent;
|
||||
firstMonth = false;
|
||||
} else {
|
||||
alreadyBudgeted += await getSheetValue(
|
||||
sheetName,
|
||||
`budget-${this.categoryID}`,
|
||||
`budget-${this.category.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -536,7 +537,7 @@ export class CategoryTemplate {
|
||||
const sheetName = monthUtils.sheetForMonth(
|
||||
monthUtils.subMonths(this.month, i),
|
||||
);
|
||||
sum += await getSheetValue(sheetName, `sum-amount-${this.categoryID}`);
|
||||
sum += await getSheetValue(sheetName, `sum-amount-${this.category.id}`);
|
||||
}
|
||||
return -Math.round(sum / template.numMonths);
|
||||
}
|
||||
|
||||
@@ -22,14 +22,20 @@ describe('goalsSchedule', () => {
|
||||
it('should return correct budget when recurring schedule set', async () => {
|
||||
// Given
|
||||
const scheduleFlag = false;
|
||||
const template_lines = [{ type: 'schedule', name: 'Test Schedule' }];
|
||||
const template_lines = [
|
||||
{
|
||||
type: 'schedule',
|
||||
name: 'Test Schedule',
|
||||
directive: '#template schedule Test Schedule',
|
||||
} as const,
|
||||
];
|
||||
const current_month = '2024-08-01';
|
||||
const balance = 0;
|
||||
const remainder = 0;
|
||||
const last_month_balance = 0;
|
||||
const to_budget = 0;
|
||||
const errors: string[] = [];
|
||||
const category = { id: 1, name: 'Test Category' };
|
||||
const category = { id: '1', name: 'Test Category' };
|
||||
|
||||
(db.first as jest.Mock).mockResolvedValue({ id: 1, completed: 0 });
|
||||
(getRuleForSchedule as jest.Mock).mockResolvedValue({
|
||||
@@ -86,14 +92,20 @@ describe('goalsSchedule', () => {
|
||||
it('should return correct budget when yearly recurring schedule set and balance is greater than target', async () => {
|
||||
// Given
|
||||
const scheduleFlag = false;
|
||||
const template_lines = [{ type: 'schedule', name: 'Test Schedule' }];
|
||||
const template_lines = [
|
||||
{
|
||||
type: 'schedule',
|
||||
name: 'Test Schedule',
|
||||
directive: '#template schedule Test Schedule',
|
||||
} as const,
|
||||
];
|
||||
const current_month = '2024-09-01';
|
||||
const balance = 12000;
|
||||
const remainder = 0;
|
||||
const last_month_balance = 12000;
|
||||
const to_budget = 0;
|
||||
const errors: string[] = [];
|
||||
const category = { id: 1, name: 'Test Category' };
|
||||
const category = { id: '1', name: 'Test Category' };
|
||||
|
||||
(db.first as jest.Mock).mockResolvedValue({ id: 1, completed: 0 });
|
||||
(getRuleForSchedule as jest.Mock).mockResolvedValue({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-strict-ignore
|
||||
import * as monthUtils from '../../shared/months';
|
||||
import { extractScheduleConds } from '../../shared/schedules';
|
||||
import { CategoryEntity } from '../../types/models';
|
||||
import * as db from '../db';
|
||||
import {
|
||||
getRuleForSchedule,
|
||||
@@ -9,8 +10,13 @@ import {
|
||||
} from '../schedules/app';
|
||||
|
||||
import { isReflectBudget } from './actions';
|
||||
import { ScheduleTemplate, Template } from './types/templates';
|
||||
|
||||
async function createScheduleList(template, current_month, category) {
|
||||
async function createScheduleList(
|
||||
template: ScheduleTemplate[],
|
||||
current_month: string,
|
||||
category: CategoryEntity,
|
||||
) {
|
||||
const t = [];
|
||||
const errors = [];
|
||||
|
||||
@@ -186,22 +192,26 @@ async function getSinkingTotal(t) {
|
||||
}
|
||||
|
||||
export async function goalsSchedule(
|
||||
scheduleFlag,
|
||||
template_lines,
|
||||
current_month,
|
||||
balance,
|
||||
remainder,
|
||||
last_month_balance,
|
||||
to_budget,
|
||||
errors,
|
||||
category,
|
||||
scheduleFlag: boolean,
|
||||
template_lines: Template[],
|
||||
current_month: string,
|
||||
balance: number,
|
||||
remainder: number,
|
||||
last_month_balance: number,
|
||||
to_budget: number,
|
||||
errors: string[],
|
||||
category: CategoryEntity,
|
||||
) {
|
||||
if (!scheduleFlag) {
|
||||
scheduleFlag = true;
|
||||
const template = template_lines.filter(t => t.type === 'schedule');
|
||||
const scheduleTemplates = template_lines.filter(t => t.type === 'schedule');
|
||||
//in the case of multiple templates per category, schedules may have wrong priority level
|
||||
|
||||
const t = await createScheduleList(template, current_month, category);
|
||||
const t = await createScheduleList(
|
||||
scheduleTemplates,
|
||||
current_month,
|
||||
category,
|
||||
);
|
||||
errors = errors.concat(t.errors);
|
||||
|
||||
const isPayMonthOf = c =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Notification } from '../../client/state-types/notifications';
|
||||
import * as monthUtils from '../../shared/months';
|
||||
import { CategoryEntity } from '../../types/models';
|
||||
import * as db from '../db';
|
||||
import { batchMessages } from '../sync';
|
||||
|
||||
@@ -46,7 +47,7 @@ export function runCheckTemplates() {
|
||||
return checkTemplates();
|
||||
}
|
||||
|
||||
async function getCategories() {
|
||||
async function getCategories(): Promise<CategoryEntity[]> {
|
||||
return await db.all(
|
||||
`
|
||||
SELECT categories.* FROM categories
|
||||
@@ -150,7 +151,8 @@ async function processTemplate(
|
||||
const budgetList = [];
|
||||
const goalList = [];
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
const id = categories[i].id;
|
||||
const category = categories[i];
|
||||
const { id } = category;
|
||||
const sheetName = monthUtils.sheetForMonth(month);
|
||||
const templates = categoryTemplates[id];
|
||||
const budgeted = await getSheetValue(sheetName, `budget-${id}`);
|
||||
@@ -162,7 +164,7 @@ async function processTemplate(
|
||||
// gather needed priorities
|
||||
// gather remainder weights
|
||||
try {
|
||||
const obj = await CategoryTemplate.init(templates, id, month);
|
||||
const obj = await CategoryTemplate.init(templates, category, month);
|
||||
availBudget += budgeted;
|
||||
availBudget += obj.getLimitExcess();
|
||||
const p = obj.getPriorities();
|
||||
@@ -230,9 +232,9 @@ async function processTemplate(
|
||||
// finish
|
||||
catObjects.forEach(o => {
|
||||
const ret = o.getValues();
|
||||
budgetList.push({ category: o.categoryID, budgeted: ret.budgeted });
|
||||
budgetList.push({ category: o.category.id, budgeted: ret.budgeted });
|
||||
goalList.push({
|
||||
category: o.categoryID,
|
||||
category: o.category.id,
|
||||
goal: ret.goal,
|
||||
longGoal: ret.longGoal ? 1 : null,
|
||||
});
|
||||
|
||||
6
upcoming-release-notes/4077.md
Normal file
6
upcoming-release-notes/4077.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [jfdoming]
|
||||
---
|
||||
|
||||
Fix schedule split template amounts
|
||||
Reference in New Issue
Block a user