mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 10:33:02 -05:00
[Goals] Round template amounts if hide decimals is set (#5288)
* round amounts * cleanup, handle not pref set * move comment * fix test * bunny, add a test
This commit is contained in:
@@ -3,6 +3,7 @@ import { vi } from 'vitest';
|
||||
import { amountToInteger } from '../../shared/util';
|
||||
import { type CategoryEntity } from '../../types/models';
|
||||
import { type Template } from '../../types/models/templates';
|
||||
import * as aql from '../aql';
|
||||
import * as db from '../db';
|
||||
|
||||
import * as actions from './actions';
|
||||
@@ -18,6 +19,10 @@ vi.mock('../db', () => ({
|
||||
getCategories: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../aql', () => ({
|
||||
aqlQuery: vi.fn(),
|
||||
}));
|
||||
|
||||
// Test helper class to access constructor and methods
|
||||
class TestCategoryTemplateContext extends CategoryTemplateContext {
|
||||
public constructor(
|
||||
@@ -801,6 +806,10 @@ 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(aql.aqlQuery).mockResolvedValueOnce({
|
||||
data: [{ value: 'false' }],
|
||||
dependencies: [],
|
||||
});
|
||||
|
||||
// Initialize the template
|
||||
const instance = await CategoryTemplateContext.init(
|
||||
@@ -864,6 +873,10 @@ 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(aql.aqlQuery).mockResolvedValueOnce({
|
||||
data: [{ value: 'false' }],
|
||||
dependencies: [],
|
||||
});
|
||||
|
||||
// Initialize the template
|
||||
const instance = await CategoryTemplateContext.init(
|
||||
@@ -916,6 +929,10 @@ 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(aql.aqlQuery).mockResolvedValueOnce({
|
||||
data: [{ value: 'false' }],
|
||||
dependencies: [],
|
||||
});
|
||||
|
||||
// Initialize the template
|
||||
const instance = await CategoryTemplateContext.init(
|
||||
@@ -974,6 +991,10 @@ 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(aql.aqlQuery).mockResolvedValueOnce({
|
||||
data: [{ value: 'false' }],
|
||||
dependencies: [],
|
||||
});
|
||||
|
||||
// Initialize the template
|
||||
const instance = await CategoryTemplateContext.init(
|
||||
@@ -1015,6 +1036,10 @@ 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(aql.aqlQuery).mockResolvedValueOnce({
|
||||
data: [{ value: 'false' }],
|
||||
dependencies: [],
|
||||
});
|
||||
|
||||
// Initialize the template
|
||||
const instance = await CategoryTemplateContext.init(
|
||||
@@ -1033,5 +1058,56 @@ describe('CategoryTemplateContext', () => {
|
||||
expect(values.goal).toBe(100000); // Should be the goal amount
|
||||
expect(values.longGoal).toBe(true); // Should have a long goal
|
||||
});
|
||||
|
||||
it('should handle hide fraction', async () => {
|
||||
const category: CategoryEntity = {
|
||||
id: 'test',
|
||||
name: 'Test Category',
|
||||
group: 'test-group',
|
||||
is_income: false,
|
||||
};
|
||||
const templates: Template[] = [
|
||||
{
|
||||
type: 'simple',
|
||||
monthly: 100.89,
|
||||
directive: 'template',
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
type: 'goal',
|
||||
amount: 1000,
|
||||
directive: 'goal',
|
||||
},
|
||||
];
|
||||
|
||||
// Mock the sheet values needed for init
|
||||
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
|
||||
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
|
||||
vi.mocked(aql.aqlQuery).mockResolvedValueOnce({
|
||||
data: [{ value: 'true' }],
|
||||
dependencies: [],
|
||||
});
|
||||
|
||||
// Initialize the template
|
||||
const instance = await CategoryTemplateContext.init(
|
||||
templates,
|
||||
category,
|
||||
'2024-01',
|
||||
0,
|
||||
);
|
||||
|
||||
// Run the templates with more than enough funds
|
||||
const result = await instance.runTemplatesForPriority(1, 100000, 100000);
|
||||
|
||||
// Get the final values
|
||||
const values = instance.getValues();
|
||||
|
||||
// Verify the results
|
||||
expect(result).toBe(10100); // Should get full amount rounded up
|
||||
expect(values.budgeted).toBe(10100); // Should match the result
|
||||
expect(values.goal).toBe(100000); // Should be the goal amount
|
||||
expect(values.longGoal).toBe(true); // Should have a long goal
|
||||
expect(instance.isGoalOnly()).toBe(false); // Should not be goal only
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { q } from 'loot-core/shared/query';
|
||||
|
||||
import * as monthUtils from '../../shared/months';
|
||||
import { amountToInteger } from '../../shared/util';
|
||||
import { amountToInteger, integerToAmount } from '../../shared/util';
|
||||
import { CategoryEntity } from '../../types/models';
|
||||
import {
|
||||
AverageTemplate,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
Template,
|
||||
WeekTemplate,
|
||||
} from '../../types/models/templates';
|
||||
import { aqlQuery } from '../aql';
|
||||
import * as db from '../db';
|
||||
|
||||
import { getSheetValue, getSheetBoolean } from './actions';
|
||||
@@ -71,6 +73,11 @@ export class CategoryTemplateContext {
|
||||
// run all checks
|
||||
await CategoryTemplateContext.checkByAndScheduleAndSpend(templates, month);
|
||||
await CategoryTemplateContext.checkPercentage(templates);
|
||||
|
||||
const hideDecimal = await aqlQuery(
|
||||
q('preferences').filter({ id: 'hideFraction' }).select('*'),
|
||||
);
|
||||
|
||||
// call the private constructor
|
||||
return new CategoryTemplateContext(
|
||||
templates,
|
||||
@@ -78,6 +85,9 @@ export class CategoryTemplateContext {
|
||||
month,
|
||||
fromLastMonth,
|
||||
budgeted,
|
||||
hideDecimal.data.length > 0
|
||||
? hideDecimal.data[0].value === 'true'
|
||||
: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,6 +220,10 @@ export class CategoryTemplateContext {
|
||||
available = available + orig - toBudget;
|
||||
}
|
||||
}
|
||||
|
||||
//round all budget values if needed
|
||||
if (this.hideDecimal) toBudget = this.removeFraction(toBudget);
|
||||
|
||||
// don't overbudget when using a priority
|
||||
if (priority > 0 && available < 0) {
|
||||
this.fullAmount += toBudget;
|
||||
@@ -258,6 +272,7 @@ export class CategoryTemplateContext {
|
||||
private remainder: RemainderTemplate[] = [];
|
||||
private goals: GoalTemplate[] = [];
|
||||
private priorities: number[] = [];
|
||||
readonly hideDecimal: boolean = false;
|
||||
private remainderWeight: number = 0;
|
||||
private toBudgetAmount: number = 0; // amount that will be budgeted by the templates
|
||||
private fullAmount: number = null; // the full requested amount, start null for remainder only cats
|
||||
@@ -277,11 +292,13 @@ export class CategoryTemplateContext {
|
||||
month: string,
|
||||
fromLastMonth: number,
|
||||
budgeted: number,
|
||||
hideDecimal: boolean = false,
|
||||
) {
|
||||
this.category = category;
|
||||
this.month = month;
|
||||
this.fromLastMonth = fromLastMonth;
|
||||
this.previouslyBudgeted = budgeted;
|
||||
this.hideDecimal = hideDecimal;
|
||||
// sort the template lines into regular template, goals, and remainder templates
|
||||
if (templates) {
|
||||
templates.forEach(t => {
|
||||
@@ -474,6 +491,10 @@ export class CategoryTemplateContext {
|
||||
}
|
||||
}
|
||||
|
||||
private removeFraction(amount: number): number {
|
||||
return amountToInteger(Math.round(integerToAmount(amount)));
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Processor Functions
|
||||
|
||||
|
||||
6
upcoming-release-notes/5288.md
Normal file
6
upcoming-release-notes/5288.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [youngcw]
|
||||
---
|
||||
|
||||
Round all template amounts when hide decimals is set
|
||||
Reference in New Issue
Block a user