mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 18:40:34 -05:00
[Goals] fix limits (#3829)
* fix limits * cleanup * fix cases of negative previous balance
This commit is contained in:
@@ -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<boolean> {
|
||||
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
|
||||
|
||||
@@ -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';
|
||||
@@ -16,11 +16,10 @@ 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.
|
||||
* 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
|
||||
@@ -32,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);
|
||||
@@ -49,6 +61,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 +84,7 @@ export class CategoryTemplate {
|
||||
availStart: number,
|
||||
): Promise<number> {
|
||||
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 +153,18 @@ export class CategoryTemplate {
|
||||
available = available - toBudget;
|
||||
}
|
||||
|
||||
//check limit
|
||||
if (this.limitCheck) {
|
||||
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
|
||||
if (priority > 0 && available < 0) {
|
||||
this.fullAmount += toBudget;
|
||||
@@ -149,25 +177,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 +215,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 +355,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);
|
||||
} else {
|
||||
throw new Error('Invalid limit period. Check template syntax');
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,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();
|
||||
@@ -219,10 +220,6 @@ async function processTemplate(
|
||||
availBudget -= ret;
|
||||
}
|
||||
}
|
||||
// run limits
|
||||
catObjects.forEach(o => {
|
||||
availBudget += o.applyLimit();
|
||||
});
|
||||
// run remainder
|
||||
if (availBudget > 0 && remainderWeight) {
|
||||
const perWeight = availBudget / remainderWeight;
|
||||
|
||||
6
upcoming-release-notes/3829.md
Normal file
6
upcoming-release-notes/3829.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [youngcw]
|
||||
---
|
||||
|
||||
Fix template limits
|
||||
Reference in New Issue
Block a user