mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 10:33:02 -05:00
Enhance Average Goal Template to allow adjusting the budgetted amount… (#6711)
* Enhance Average Goal Template to allow adjusting the budgetted amount from the average by a percent or fixed amount
* fix typos
* ensure check is for undefined, so zero doesn't cause edge cases
* typo in enhancement message
* scale by cents for fixed amount, fixup tests for this as well
* Add support for fixed values in Schedule template
* [autofix.ci] apply automated fixes
* Changed to 'fixed' from 'amount'. Added unparse logic for Average, also fixed Schedule unparse
* move to generic for syntax from specific number
* consider currency preferences when calculating fixed modifications
* lint
* [autofix.ci] apply automated fixes
* timeframe -> period
* percentage -> value
* pass currency for calculation
* lint
* import from proper local dir
* [autofix.ci] apply automated fixes
* script block around {number} to prevent mdx conflict
* match order of ops between schedule and average for percent adjustment
* diversify example
* Link to schedule>adjustments from average
* Removed example column in favor of extra context
Slight rewording to avoid 'average' overload
* rabbit nitpicks
* number
* period
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
This commit is contained in:
@@ -394,43 +394,57 @@ Below is an example of using the "Full" flag assuming a once-per-year schedule f
|
||||
| `#template schedule full Simplefin` | $ 0 | Budget in all months except May |
|
||||
| `#template schedule full Simplefin` | $ 15 | Budget in May |
|
||||
|
||||
#### Percentage Increase / Decrease
|
||||
#### Adjustments
|
||||
|
||||
Yearly expenses (e.g. insurance, property rates, etc.) increase year on year. Often the amount is unknown until close to the due date. This creates a budget crunch - if your $ 1,000 insurance jumps 20% ($ 1,200), you need to make up that extra $ 200 in just a month or two.
|
||||
|
||||
This feature adds percentage adjustments to templates, letting you gradually save the expected increase throughout the year. By proactively budgeting a percentage change for these yearly increases, you avoid last-minute scrambling when renewal notices arrive with higher amounts.
|
||||
This feature adds adjustments to the template (either percentage or fixed), letting you gradually save the expected increase throughout the year. By proactively budgeting a percentage/fixed change for these yearly increases, you avoid last-minute scrambling when renewal notices arrive with higher amounts.
|
||||
|
||||
| Syntax | Description |
|
||||
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `#template schedule {SCHEDULE NAME} [{increase/decrease} {number}%]` | Fund the upcoming scheduled transaction over time, increasing or decreasing the amount by the given percentage |
|
||||
| Syntax | Description |
|
||||
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `#template schedule {SCHEDULE NAME} [{increase/decrease} {number\|number%}]` | Fund the upcoming scheduled transaction over time, increasing or decreasing the amount by the given value |
|
||||
|
||||
As an example, assume the amount Scheduled for 'Insurance' the prior year was $ 1000 and $ 83.33 was budgeted monthly; the below will apply.
|
||||
As an example, assume the amount scheduled for 'Insurance' the prior year was $ 1000 and $ 83.33 was budgeted monthly; the below will apply.
|
||||
|
||||
| Category | Template line | Budgeted Amount |
|
||||
| --------- | --------------------------------------------- | :-------------: |
|
||||
| Insurance | `#template schedule Insurance [increase 20%]` | $ 100 |
|
||||
| Category | Template line | Monthly Budget | Annual Budget |
|
||||
| --------- | --------------------------------------------- | :------------: | :-----------: |
|
||||
| Insurance | `#template schedule Insurance [increase 20%]` | $ 100 | $ 1200 |
|
||||
| Insurance | `#template schedule Insurance [increase 500]` | $ 125 | $ 1500 |
|
||||
|
||||
When "Insurance" comes due at the end of the year, $1200 will be available.
|
||||
When "Insurance" comes due at the end of the year, $1200 will be available for the first example, or $1500 for the second example.
|
||||
|
||||
#### Available Variations
|
||||
|
||||
Below is a table of the variations of the Schedule template.
|
||||
|
||||
| Syntax | Description | Example Application |
|
||||
| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| `#template schedule {SCHEDULE NAME}` | Fund upcoming scheduled transactions over time | Monthly schedules, or larger non-monthly scheduled transactions |
|
||||
| `#template schedule full {SCHEDULE NAME}` | Fund upcoming scheduled transaction only on needed month | Small schedules that are non-monthly |
|
||||
| `#template schedule {SCHEDULE NAME} [{increase/decrease} {number}%]` | Fund upcoming scheduled transaction over time, increasing or decreasing the amount by the given percentage | Yearly renewals where the amount changes |
|
||||
| Syntax | Description | Example Application |
|
||||
| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| `#template schedule {SCHEDULE NAME}` | Fund upcoming scheduled transactions over time | Monthly schedules, or larger non-monthly scheduled transactions |
|
||||
| `#template schedule full {SCHEDULE NAME}` | Fund upcoming scheduled transaction only on needed month | Small schedules that are non-monthly |
|
||||
| `#template schedule {SCHEDULE NAME} [{increase/decrease} {number\|number%}]` | Fund upcoming scheduled transaction over time, increasing or decreasing the amount by the given value | Yearly renewals where the amount changes |
|
||||
|
||||
### Average Type
|
||||
|
||||
The Average template allows you to budget the average amount spend over a number of months.
|
||||
This is the same function provided by the menu in the budget table but it can be used in a single category automatically where the menu option must be applied to the whole budget or a single category.
|
||||
The table below shows how to use the Average template.
|
||||
This template allows you to budget based on the average amount spent over a number of months, adding flexibility beyond the menu built-ins (3 months, 6 months).
|
||||
|
||||
| Syntax | Description | Example Application |
|
||||
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `#template average 6 months` | Budget the average amount spent over the last 6 months. Can set the number to any number > 0. Matches the existing option on the budget page but with flexible month ranges | Try to budget only what you need to spend based on the last 6 months of spending data |
|
||||
You can also adjust the budgeted amount from the average by a percentage or by a fixed whole number. This functionality may be useful when you want to budget an average, but bump it up or down a bit to account for inflation or to slowly wean off a category you'd like to spend less on. (See also [adjustments](#adjustments))
|
||||
|
||||
| Syntax | Description |
|
||||
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `#template average {number} months` | Budget the average amount spent over the last `{number}` months. Can set the number to any number > 0. Matches the existing option on the budget page but with flexible month ranges |
|
||||
| `#template average {number} months [{increase/decrease} {number\|number%}]` | Budget the average amount spent over a period, with an adjustment |
|
||||
|
||||
#### Examples
|
||||
|
||||
As an example, assume the spend for the category was [\$40, \$50, \$60] for the past 3 months; here are some example usages.
|
||||
|
||||
| Template line | Budgeted Amount |
|
||||
| ------------------------------------------- | :-------------: |
|
||||
| `#template average 3 months` | \$ 50 |
|
||||
| `#template average 3 months [increase 20%]` | \$ 60 |
|
||||
| `#template average 3 months [decrease 10%]` | \$ 45 |
|
||||
| `#template average 3 months [increase 11]` | \$ 61 |
|
||||
| `#template average 3 months [decrease 1]` | \$ 49 |
|
||||
|
||||
### Copy Type
|
||||
|
||||
|
||||
@@ -591,6 +591,137 @@ describe('CategoryTemplateContext', () => {
|
||||
);
|
||||
expect(result).toBe(67); // Average of -100, 200, -300
|
||||
});
|
||||
|
||||
it('should handle positive percent adjustments', async () => {
|
||||
const template: Template = {
|
||||
type: 'average',
|
||||
numMonths: 3,
|
||||
directive: 'template',
|
||||
priority: 1,
|
||||
adjustment: 10,
|
||||
adjustmentType: 'percent',
|
||||
};
|
||||
|
||||
vi.mocked(actions.getSheetValue)
|
||||
.mockResolvedValueOnce(-100)
|
||||
.mockResolvedValueOnce(-100)
|
||||
.mockResolvedValueOnce(-100);
|
||||
|
||||
const result = await CategoryTemplateContext.runAverage(
|
||||
template,
|
||||
instance,
|
||||
);
|
||||
expect(result).toBe(110);
|
||||
});
|
||||
|
||||
it('should handle negative percent adjustments', async () => {
|
||||
const template: Template = {
|
||||
type: 'average',
|
||||
numMonths: 3,
|
||||
directive: 'template',
|
||||
priority: 1,
|
||||
adjustment: -10,
|
||||
adjustmentType: 'percent',
|
||||
};
|
||||
|
||||
vi.mocked(actions.getSheetValue)
|
||||
.mockResolvedValueOnce(-100)
|
||||
.mockResolvedValueOnce(-100)
|
||||
.mockResolvedValueOnce(-100);
|
||||
|
||||
const result = await CategoryTemplateContext.runAverage(
|
||||
template,
|
||||
instance,
|
||||
);
|
||||
expect(result).toBe(90);
|
||||
});
|
||||
it('should handle zero percent adjustments', async () => {
|
||||
const template: Template = {
|
||||
type: 'average',
|
||||
numMonths: 3,
|
||||
directive: 'template',
|
||||
priority: 1,
|
||||
adjustment: 0,
|
||||
adjustmentType: 'percent',
|
||||
};
|
||||
|
||||
vi.mocked(actions.getSheetValue)
|
||||
.mockResolvedValueOnce(-100)
|
||||
.mockResolvedValueOnce(-100)
|
||||
.mockResolvedValueOnce(-100);
|
||||
|
||||
const result = await CategoryTemplateContext.runAverage(
|
||||
template,
|
||||
instance,
|
||||
);
|
||||
expect(result).toBe(100);
|
||||
});
|
||||
|
||||
it('should handle zero amount adjustments', async () => {
|
||||
const template: Template = {
|
||||
type: 'average',
|
||||
numMonths: 3,
|
||||
directive: 'template',
|
||||
priority: 1,
|
||||
adjustment: 0,
|
||||
adjustmentType: 'fixed',
|
||||
};
|
||||
|
||||
vi.mocked(actions.getSheetValue)
|
||||
.mockResolvedValueOnce(-100)
|
||||
.mockResolvedValueOnce(-100)
|
||||
.mockResolvedValueOnce(-100);
|
||||
|
||||
const result = await CategoryTemplateContext.runAverage(
|
||||
template,
|
||||
instance,
|
||||
);
|
||||
expect(result).toBe(100);
|
||||
});
|
||||
|
||||
it('should handle positive amount adjustments', async () => {
|
||||
const template: Template = {
|
||||
type: 'average',
|
||||
numMonths: 3,
|
||||
directive: 'template',
|
||||
priority: 1,
|
||||
adjustment: 11,
|
||||
adjustmentType: 'fixed',
|
||||
};
|
||||
|
||||
vi.mocked(actions.getSheetValue)
|
||||
.mockResolvedValueOnce(-10000)
|
||||
.mockResolvedValueOnce(-10000)
|
||||
.mockResolvedValueOnce(-10000);
|
||||
|
||||
const result = await CategoryTemplateContext.runAverage(
|
||||
template,
|
||||
instance,
|
||||
);
|
||||
expect(result).toBe(11100);
|
||||
});
|
||||
|
||||
it('should handle negative amount adjustments', async () => {
|
||||
const template: Template = {
|
||||
type: 'average',
|
||||
numMonths: 3,
|
||||
directive: 'template',
|
||||
priority: 1,
|
||||
adjustment: -1,
|
||||
adjustmentType: 'fixed',
|
||||
};
|
||||
|
||||
vi.mocked(actions.getSheetValue)
|
||||
.mockResolvedValueOnce(-10000)
|
||||
.mockResolvedValueOnce(-10000)
|
||||
.mockResolvedValueOnce(-10000);
|
||||
|
||||
const result = await CategoryTemplateContext.runAverage(
|
||||
template,
|
||||
instance,
|
||||
);
|
||||
expect(result).toBe(9900);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runBy', () => {
|
||||
|
||||
@@ -199,6 +199,7 @@ export class CategoryTemplateContext {
|
||||
toBudget,
|
||||
[],
|
||||
this.category,
|
||||
this.currency,
|
||||
);
|
||||
// Schedules assume that its to budget value is the whole thing so this
|
||||
// needs to remove the previous funds so they aren't double counted
|
||||
@@ -737,7 +738,31 @@ export class CategoryTemplateContext {
|
||||
`sum-amount-${templateContext.category.id}`,
|
||||
);
|
||||
}
|
||||
return -Math.round(sum / template.numMonths);
|
||||
|
||||
// negate as sheet value is cost ie negative
|
||||
let average = -(sum / template.numMonths);
|
||||
|
||||
if (template.adjustment !== undefined && template.adjustmentType) {
|
||||
switch (template.adjustmentType) {
|
||||
case 'percent': {
|
||||
const adjustmentFactor = 1 + template.adjustment / 100;
|
||||
average = adjustmentFactor * average;
|
||||
break;
|
||||
}
|
||||
case 'fixed': {
|
||||
average += amountToInteger(
|
||||
template.adjustment,
|
||||
templateContext.currency.decimalPlaces,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
//no valid adjustment was found
|
||||
}
|
||||
}
|
||||
|
||||
return Math.round(average);
|
||||
}
|
||||
|
||||
static runBy(templateContext: CategoryTemplateContext): number {
|
||||
|
||||
@@ -19,11 +19,11 @@ expr
|
||||
/ template: template _ limit: limit
|
||||
{ return { type: 'simple', monthly: null, limit, priority: template.priority, directive: template.directive }}
|
||||
/ template: template _ schedule:schedule _ full:full? name:rawScheduleName modifiers:modifiers?
|
||||
{ return { type: 'schedule', name: name.trim(), priority: template.priority, directive: template.directive, full, adjustment: modifiers?.adjustment }}
|
||||
{ return { type: 'schedule', name: name.trim(), priority: template.priority, directive: template.directive, full, adjustment: modifiers?.adjustment, adjustmentType: modifiers?.adjustmentType }}
|
||||
/ template: template _ remainder: remainder limit: limit?
|
||||
{ return { type: 'remainder', priority: null, directive: template.directive, weight: remainder, limit }}
|
||||
/ template: template _ 'average'i _ amount: positive _ 'months'i?
|
||||
{ return { type: 'average', numMonths: +amount, priority: template.priority, directive: template.directive }}
|
||||
/ template: template _ 'average'i _ amount: positive _ 'months'i? modifiers:modifiers?
|
||||
{ return { type: 'average', numMonths: +amount, priority: template.priority, directive: template.directive, adjustment: modifiers?.adjustment, adjustmentType: modifiers?.adjustmentType }}
|
||||
/ template: template _ 'copy from'i _ lookBack: positive _ 'months ago'i limit:limit?
|
||||
{ return { type: 'copy', priority: template.priority, directive: template.directive, lookBack: +lookBack, limit }}
|
||||
/ goal: goal amount: amount { return {type: 'goal', amount: amount, priority: null, directive: goal }}
|
||||
@@ -31,11 +31,15 @@ expr
|
||||
modifiers = _ '[' modifier:modifier ']' { return modifier }
|
||||
|
||||
modifier
|
||||
= op:('increase'i / 'decrease'i) _ value:percent {
|
||||
= op:('increase'i / 'decrease'i) _ value:percentOrNumber {
|
||||
const multiplier = op.toLowerCase() === 'increase' ? 1 : -1;
|
||||
return { adjustment: multiplier * +value }
|
||||
return { adjustment: multiplier * +value.value, adjustmentType: value.type }
|
||||
}
|
||||
|
||||
percentOrNumber
|
||||
= value:$(d+ ('.' (d+)?)?) _? '%' { return { value: value, type: 'percent' } }
|
||||
/ value:$(d+ ('.' (d+)?)?) { return { value: value, type: 'fixed' } }
|
||||
|
||||
repeat 'repeat interval'
|
||||
= 'month'i { return { annual: false }}
|
||||
/ months: positive _ 'months'i { return { annual: false, repeat: +months }}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type Currency } from 'loot-core/shared/currencies';
|
||||
|
||||
import { type CategoryEntity } from '../../types/models';
|
||||
import * as db from '../db';
|
||||
import { Rule } from '../rules';
|
||||
@@ -38,6 +40,14 @@ describe('runSchedule', () => {
|
||||
const to_budget = 0;
|
||||
const errors: string[] = [];
|
||||
const category = { id: '1', name: 'Test Category' } as CategoryEntity;
|
||||
const currency: Currency = {
|
||||
code: '',
|
||||
symbol: '',
|
||||
name: '',
|
||||
decimalPlaces: 2,
|
||||
numberFormat: 'comma-dot',
|
||||
symbolFirst: false,
|
||||
};
|
||||
|
||||
vi.mocked(db.first).mockResolvedValue({ id: 1, completed: 0 });
|
||||
vi.mocked(getRuleForSchedule).mockResolvedValue(
|
||||
@@ -84,6 +94,7 @@ describe('runSchedule', () => {
|
||||
to_budget,
|
||||
errors,
|
||||
category,
|
||||
currency,
|
||||
);
|
||||
|
||||
// Then
|
||||
@@ -109,6 +120,14 @@ describe('runSchedule', () => {
|
||||
const to_budget = 0;
|
||||
const errors: string[] = [];
|
||||
const category = { id: '1', name: 'Test Category' } as CategoryEntity;
|
||||
const currency: Currency = {
|
||||
code: '',
|
||||
symbol: '',
|
||||
name: '',
|
||||
decimalPlaces: 2,
|
||||
numberFormat: 'comma-dot',
|
||||
symbolFirst: false,
|
||||
};
|
||||
|
||||
vi.mocked(db.first).mockResolvedValue({ id: 1, completed: 0 });
|
||||
vi.mocked(getRuleForSchedule).mockResolvedValue(
|
||||
@@ -155,6 +174,7 @@ describe('runSchedule', () => {
|
||||
to_budget,
|
||||
errors,
|
||||
category,
|
||||
currency,
|
||||
);
|
||||
|
||||
// Then
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { type Currency } from 'loot-core/shared/currencies';
|
||||
import { amountToInteger } from 'loot-core/shared/util';
|
||||
|
||||
import * as monthUtils from '../../shared/months';
|
||||
import {
|
||||
extractScheduleConds,
|
||||
@@ -31,6 +35,7 @@ async function createScheduleList(
|
||||
templates: ScheduleTemplate[],
|
||||
current_month: string,
|
||||
category: CategoryEntity,
|
||||
currency: Currency,
|
||||
) {
|
||||
const t: Array<ScheduleTemplateTarget> = [];
|
||||
const errors: string[] = [];
|
||||
@@ -52,10 +57,27 @@ async function createScheduleList(
|
||||
2
|
||||
: amountCondition.value;
|
||||
// Apply adjustment percentage if specified
|
||||
if (template.adjustment) {
|
||||
const adjustmentFactor = 1 + template.adjustment / 100;
|
||||
scheduleAmount = Math.round(scheduleAmount * adjustmentFactor);
|
||||
if (template.adjustment !== undefined && template.adjustmentType) {
|
||||
switch (template.adjustmentType) {
|
||||
case 'percent': {
|
||||
const adjustmentFactor = 1 + template.adjustment / 100;
|
||||
scheduleAmount = scheduleAmount * adjustmentFactor;
|
||||
break;
|
||||
}
|
||||
case 'fixed': {
|
||||
const sign = scheduleAmount < 0 ? -1 : 1;
|
||||
scheduleAmount +=
|
||||
sign * amountToInteger(template.adjustment, currency.decimalPlaces);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
//no valid adjustment was found
|
||||
}
|
||||
}
|
||||
|
||||
scheduleAmount = Math.round(scheduleAmount);
|
||||
|
||||
const { amount: postRuleAmount, subtransactions } = rule.execActions({
|
||||
amount: scheduleAmount,
|
||||
category: category.id,
|
||||
@@ -265,6 +287,7 @@ export async function runSchedule(
|
||||
to_budget: number,
|
||||
errors: string[],
|
||||
category: CategoryEntity,
|
||||
currency: Currency,
|
||||
) {
|
||||
const scheduleTemplates = template_lines.filter(t => t.type === 'schedule');
|
||||
|
||||
@@ -272,6 +295,7 @@ export async function runSchedule(
|
||||
scheduleTemplates,
|
||||
current_month,
|
||||
category,
|
||||
currency,
|
||||
);
|
||||
errors = errors.concat(t.errors);
|
||||
|
||||
|
||||
@@ -99,16 +99,21 @@ async function getCategoriesWithTemplates(): Promise<
|
||||
|
||||
// Validate schedule adjustments
|
||||
if (
|
||||
parsedTemplate.type === 'schedule' &&
|
||||
(parsedTemplate.type === 'average' ||
|
||||
parsedTemplate.type === 'schedule') &&
|
||||
parsedTemplate.adjustment !== undefined
|
||||
) {
|
||||
if (
|
||||
parsedTemplate.adjustment <= -100 ||
|
||||
parsedTemplate.adjustment > 1000
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid adjustment percentage (${parsedTemplate.adjustment}%). Must be between -100% and 1000%`,
|
||||
);
|
||||
if (parsedTemplate.adjustmentType === 'percent') {
|
||||
if (
|
||||
parsedTemplate.adjustment <= -100 ||
|
||||
parsedTemplate.adjustment > 1000
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid adjustment percentage (${parsedTemplate.adjustment}%). Must be between -100% and 1000%`,
|
||||
);
|
||||
}
|
||||
} else if (parsedTemplate.adjustmentType === 'fixed') {
|
||||
//placeholder for potential validation of amount/fixed adjustments
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +180,8 @@ export async function unparse(templates: Template[]): Promise<string> {
|
||||
const adj = template.adjustment;
|
||||
const op = adj >= 0 ? 'increase' : 'decrease';
|
||||
const val = Math.abs(adj);
|
||||
result += ` [${op} ${val}%]`;
|
||||
const type = template.adjustmentType === 'percent' ? '%' : '';
|
||||
result += ` [${op} ${val}${type}]`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -221,8 +227,18 @@ export async function unparse(templates: Template[]): Promise<string> {
|
||||
return result;
|
||||
}
|
||||
case 'average': {
|
||||
// #template average <numMonths> months
|
||||
return `${prefix} average ${template.numMonths} months`;
|
||||
let result = `${prefix} average ${template.numMonths} months`;
|
||||
|
||||
if (template.adjustment !== undefined) {
|
||||
const adj = template.adjustment;
|
||||
const op = adj >= 0 ? 'increase' : 'decrease';
|
||||
const val = Math.abs(adj);
|
||||
const type = template.adjustmentType === 'percent' ? '%' : '';
|
||||
result += ` [${op} ${val}${type}]`;
|
||||
}
|
||||
|
||||
// #template average <numMonths> months [increase/decrease {number|number%}]
|
||||
return result;
|
||||
}
|
||||
case 'copy': {
|
||||
// #template copy from <lookBack> months ago [limit]
|
||||
|
||||
@@ -64,11 +64,14 @@ export type ScheduleTemplate = {
|
||||
name: string;
|
||||
full?: boolean;
|
||||
adjustment?: number;
|
||||
adjustmentType?: 'percent' | 'fixed';
|
||||
} & BaseTemplateWithPriority;
|
||||
|
||||
export type AverageTemplate = {
|
||||
type: 'average';
|
||||
numMonths: number;
|
||||
adjustment?: number;
|
||||
adjustmentType?: 'percent' | 'fixed';
|
||||
} & BaseTemplateWithPriority;
|
||||
|
||||
export type CopyTemplate = {
|
||||
|
||||
6
upcoming-release-notes/6711.md
Normal file
6
upcoming-release-notes/6711.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [totallynotjon]
|
||||
---
|
||||
|
||||
Enhance Average Goal Template to allow adjusting the budgeted amount from the average by a percent or fixed amount
|
||||
Reference in New Issue
Block a user