Skip to content
Snippets Groups Projects
Unverified Commit 66f7336b authored by shall0pass's avatar shall0pass Committed by GitHub
Browse files

Priorities for goals (#961)

This attempts to add priorities for goal templates and addresses most of
https://github.com/actualbudget/actual/issues/959

.

I couldn't find a good way to preserve both "Apply" and "Overwrite"
operations, so this PR does away with the current "Apply" action
behavior. Every box with a budgeted value will be overwritten if a
template goal is present.

The added syntax to define priorities is as follows:
#template    -- priority 0, highest priority
#template-1  --priority 1, 2nd highest priority
#template-2 --priority 2, 3rd highest priority
#template-N --priority N, as many as you'd like.

~~Leaving as a draft as this may not be the preferred implementation but
I wanted others to be able to try it with netlify.~~

---------

Co-authored-by: default avatarCaleb Young <cwy@rincon.com>
parent 4cebdb53
No related branches found
No related tags found
No related merge requests found
// https://peggyjs.org // https://peggyjs.org
expr expr
= percent: percent _ of _ category: $([^\r\n\t]+) = priority: priority? _? percent: percent _ of _ category: name
{ return { type: 'percentage', percent: +percent, category } } { return { type: 'percentage', percent: +percent, category, priority: +priority }}
/ amount: amount _ repeatEvery _ weeks: weekCount _ starting _ starting: date limit: limit? / priority: priority? _? amount: amount _ repeatEvery _ weeks: weekCount _ starting _ starting: date limit: limit?
{ return { type: 'week', amount, weeks, starting, limit } } { return { type: 'week', amount, weeks, starting, limit, priority: +priority }}
/ amount: amount _ by _ month: month from: spendFrom? repeat: (_ repeatEvery _ repeat)? / priority: priority? _? amount: amount _ by _ month: month from: spendFrom? repeat: (_ repeatEvery _ repeat)?
{ return { { return {
type: from ? 'spend' : 'by', type: from ? 'spend' : 'by',
amount, amount,
month, month,
...(repeat ? repeat[3] : {}), ...(repeat ? repeat[3] : {}),
from from,
priority: +priority
} } } }
/ monthly: amount limit: limit? / priority: priority? _? monthly: amount limit: limit?
{ return { type: 'simple', monthly, limit } } { return { type: 'simple', monthly, limit, priority: +priority } }
/ upTo _ limit: amount / priority: priority? _? upTo _ limit: amount
{ return { type: 'simple', limit } } { return { type: 'simple', limit , priority: +priority } }
/ schedule _ name: name / priority: priority? _? schedule _ name: name
{ return { type: 'schedule', name} } { return { type: 'schedule', name, priority: +priority } }
repeat 'repeat interval' repeat 'repeat interval'
= 'month'i { return { annual: false } } = 'month'i { return { annual: false } }
...@@ -42,6 +43,7 @@ repeatEvery = 'repeat'i _ 'every'i ...@@ -42,6 +43,7 @@ repeatEvery = 'repeat'i _ 'every'i
starting = 'starting'i starting = 'starting'i
upTo = 'up'i _ 'to'i upTo = 'up'i _ 'to'i
schedule = 'schedule'i schedule = 'schedule'i
priority = '-'i number: number _ {return number}
_ 'space' = ' '+ _ 'space' = ' '+
d 'digit' = [0-9] d 'digit' = [0-9]
......
...@@ -26,54 +26,141 @@ export function overwriteTemplate({ month }) { ...@@ -26,54 +26,141 @@ export function overwriteTemplate({ month }) {
return processTemplate(month, true); return processTemplate(month, true);
} }
function checkScheduleTemplates(template) {
let lowPriority = template[0].priority;
let errorNotice = false;
for (let l = 1; l < template.length; l++) {
if (template[l].priority !== lowPriority) {
lowPriority = Math.min(lowPriority, template[l].priority);
errorNotice = true;
}
}
return { lowPriority, errorNotice };
}
async function processTemplate(month, force) { async function processTemplate(month, force) {
let category_templates = await getCategoryTemplates(); let num_applied = 0;
let errors = []; let errors = [];
let category_templates = await getCategoryTemplates();
let lowestPriority = 0;
let originalCategoryBalance = [];
let categories = await db.all( let categories = await db.all(
'SELECT * FROM v_categories WHERE tombstone = 0', 'SELECT * FROM v_categories WHERE tombstone = 0',
); );
let num_applied = 0; //clears templated categories
for (let c = 0; c < categories.length; c++) { for (let c = 0; c < categories.length; c++) {
let category = categories[c]; let category = categories[c];
let budgeted = await getSheetValue( let budgeted = await getSheetValue(
monthUtils.sheetForMonth(month), monthUtils.sheetForMonth(month),
`budget-${category.id}`, `budget-${category.id}`,
); );
if (budgeted)
originalCategoryBalance.push({ cat: category, amount: budgeted });
let template = category_templates[category.id];
if (template) {
for (let l = 0; l < template.length; l++)
lowestPriority =
template[l].priority > lowestPriority
? template[l].priority
: lowestPriority;
await setBudget({
category: category.id,
month,
amount: 0,
});
}
}
if (budgeted === 0 || force) { for (let priority = 0; priority <= lowestPriority; priority++) {
for (let c = 0; c < categories.length; c++) {
let category = categories[c];
let template = category_templates[category.id]; let template = category_templates[category.id];
if (template) { if (template) {
errors = errors.concat( //check that all schedule and by lines have the same priority level
template let skipSchedule = false;
.filter(t => t.type === 'error') let isScheduleOrBy = false;
.map(({ line, error }) => let priorityCheck = 0;
[ if (
category.name + ': ' + error.message, template.filter(t => t.type === 'schedule' || t.type === 'by')
line, .length > 0
' '.repeat( ) {
TEMPLATE_PREFIX.length + error.location.start.offset, let { lowPriority, errorNotice } = await checkScheduleTemplates(
) + '^', template,
].join('\n'),
),
);
let { amount: to_budget, errors: applyErrors } =
await applyCategoryTemplate(category, template, month, force);
if (to_budget != null) {
num_applied++;
await setBudget({ category: category.id, month, amount: to_budget });
}
if (applyErrors != null) {
errors = errors.concat(
applyErrors.map(error => `${category.name}: ${error}`),
); );
priorityCheck = lowPriority;
skipSchedule = priorityCheck !== priority ? true : false;
isScheduleOrBy = true;
if (!skipSchedule && errorNotice)
errors.push(
category.name +
': Schedules and By templates should all have the same priority. Using priority ' +
priorityCheck,
);
}
if (!skipSchedule) {
if (!isScheduleOrBy)
template = template.filter(t => t.priority === priority);
if (template.length > 0) {
errors = errors.concat(
template
.filter(t => t.type === 'error')
.map(({ line, error }) =>
[
category.name + ': ' + error.message,
line,
' '.repeat(
TEMPLATE_PREFIX.length + error.location.start.offset,
) + '^',
].join('\n'),
),
);
let { amount: to_budget, errors: applyErrors } =
await applyCategoryTemplate(
category,
template,
month,
priority,
force,
);
if (to_budget != null) {
num_applied++;
await setBudget({
category: category.id,
month,
amount: to_budget,
});
}
if (applyErrors != null) {
errors = errors.concat(
applyErrors.map(error => `${category.name}: ${error}`),
);
}
}
} }
} }
} }
} }
if (!force) {
//if overwrite is not preferred, set cell to original value
for (let l = 0; l < originalCategoryBalance.length; l++) {
await setBudget({
category: originalCategoryBalance[l].cat.id,
month,
amount: originalCategoryBalance[l].amount,
});
//if overwrite is not preferred, remove template errors for category
let j = errors.length;
for (let k = 0; k < j; k++) {
if (errors[k].includes(originalCategoryBalance[l].cat.name)) {
errors.splice(k, 1);
j--;
}
}
}
}
if (num_applied === 0) { if (num_applied === 0) {
if (errors.length) { if (errors.length) {
return { return {
...@@ -104,8 +191,7 @@ async function processTemplate(month, force) { ...@@ -104,8 +191,7 @@ async function processTemplate(month, force) {
} }
} }
const TEMPLATE_PREFIX = '#template '; const TEMPLATE_PREFIX = '#template';
async function getCategoryTemplates() { async function getCategoryTemplates() {
let templates = {}; let templates = {};
...@@ -134,7 +220,13 @@ async function getCategoryTemplates() { ...@@ -134,7 +220,13 @@ async function getCategoryTemplates() {
return templates; return templates;
} }
async function applyCategoryTemplate(category, template_lines, month, force) { async function applyCategoryTemplate(
category,
template_lines,
month,
priority,
force,
) {
let current_month = new Date(`${month}-01`); let current_month = new Date(`${month}-01`);
let errors = []; let errors = [];
let all_schedule_names = await db.all( let all_schedule_names = await db.all(
...@@ -200,12 +292,13 @@ async function applyCategoryTemplate(category, template_lines, month, force) { ...@@ -200,12 +292,13 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
}); });
} }
let to_budget = 0;
let limit;
let sheetName = monthUtils.sheetForMonth(month); let sheetName = monthUtils.sheetForMonth(month);
let budgeted = await getSheetValue(sheetName, `budget-${category.id}`); let budgeted = await getSheetValue(sheetName, `budget-${category.id}`);
let spent = await getSheetValue(sheetName, `sum-amount-${category.id}`); let spent = await getSheetValue(sheetName, `sum-amount-${category.id}`);
let balance = await getSheetValue(sheetName, `leftover-${category.id}`); let balance = await getSheetValue(sheetName, `leftover-${category.id}`);
let budgetAvailable = await getSheetValue(sheetName, `to-budget`);
let to_budget = budgeted;
let limit;
let last_month_balance = balance - spent - budgeted; let last_month_balance = balance - spent - budgeted;
let totalTarget = 0; let totalTarget = 0;
let totalMonths = 0; let totalMonths = 0;
...@@ -223,11 +316,18 @@ async function applyCategoryTemplate(category, template_lines, month, force) { ...@@ -223,11 +316,18 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
limit = amountToInteger(template.limit); limit = amountToInteger(template.limit);
} }
} }
let increment = 0;
if (template.monthly != null) { if (template.monthly != null) {
let monthly = amountToInteger(template.monthly); let monthly = amountToInteger(template.monthly);
to_budget += monthly; increment = monthly;
} else {
increment = limit;
}
if (to_budget + increment < budgetAvailable || !priority) {
to_budget += increment;
} else { } else {
to_budget += limit; if (budgetAvailable > 0) to_budget += budgetAvailable;
errors.push(`Insufficient funds.`);
} }
break; break;
} }
...@@ -259,10 +359,16 @@ async function applyCategoryTemplate(category, template_lines, month, force) { ...@@ -259,10 +359,16 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
let diff = totalTarget - last_month_balance; let diff = totalTarget - last_month_balance;
if (diff >= 0 && totalMonths > 0 && l === N - 1) { if (diff >= 0 && totalMonths > 0 && l === N - 1) {
to_budget += Math.round( let increment = Math.round(
((totalTarget - last_month_balance) / totalMonths) * ((totalTarget - last_month_balance) / totalMonths) *
(N - skipMonths), (N - skipMonths),
); );
if (to_budget + increment < budgetAvailable || !priority) {
to_budget += increment;
} else {
if (budgetAvailable > 0) to_budget += budgetAvailable;
errors.push(`Insufficient funds.`);
}
} }
break; break;
} }
...@@ -284,9 +390,14 @@ async function applyCategoryTemplate(category, template_lines, month, force) { ...@@ -284,9 +390,14 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
while (w.getTime() < next_month.getTime()) { while (w.getTime() < next_month.getTime()) {
if (w.getTime() >= current_month.getTime()) { if (w.getTime() >= current_month.getTime()) {
to_budget += amount; if (to_budget + amount < budgetAvailable || !priority) {
to_budget += amount;
} else {
if (budgetAvailable > 0) to_budget += budgetAvailable;
errors.push(`Insufficient funds.`);
}
w = addWeeks(w, weeks);
} }
w = addWeeks(w, weeks);
} }
break; break;
} }
...@@ -324,16 +435,24 @@ async function applyCategoryTemplate(category, template_lines, month, force) { ...@@ -324,16 +435,24 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
} }
let num_months = differenceInCalendarMonths(to_month, current_month); let num_months = differenceInCalendarMonths(to_month, current_month);
let target = amountToInteger(template.amount); let target = amountToInteger(template.amount);
let increment = 0;
if (num_months < 0) { if (num_months < 0) {
errors.push(`${template.month} is in the past.`); errors.push(`${template.month} is in the past.`);
return { errors }; return { errors };
} else if (num_months === 0) { } else if (num_months === 0) {
to_budget = target - already_budgeted; increment = target - already_budgeted;
} else { } else {
to_budget = Math.round( increment = Math.round(
(target - already_budgeted) / (num_months + 1), (target - already_budgeted) / (num_months + 1),
); );
} }
if (increment < budgetAvailable || !priority) {
to_budget = increment;
} else {
if (budgetAvailable > 0) to_budget = budgetAvailable;
errors.push(`Insufficient funds.`);
}
break; break;
} }
case 'percentage': { case 'percentage': {
...@@ -356,7 +475,16 @@ async function applyCategoryTemplate(category, template_lines, month, force) { ...@@ -356,7 +475,16 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
`sum-amount-${income_category.id}`, `sum-amount-${income_category.id}`,
); );
} }
to_budget = Math.max(0, Math.round(monthlyIncome * (percent / 100))); let increment = Math.max(
0,
Math.round(monthlyIncome * (percent / 100)),
);
if (increment < budgetAvailable || !priority) {
to_budget = increment;
} else {
if (budgetAvailable > 0) to_budget = budgetAvailable;
errors.push(`Insufficient funds.`);
}
break; break;
} }
case 'error': case 'error':
...@@ -406,7 +534,13 @@ async function applyCategoryTemplate(category, template_lines, month, force) { ...@@ -406,7 +534,13 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
} else { } else {
monthly_target = target; monthly_target = target;
} }
to_budget += monthly_target - balance + budgeted; let increment = monthly_target - balance + budgeted;
if (to_budget + increment < budgetAvailable || !priority) {
to_budget += increment;
} else {
if (budgetAvailable > 0) to_budget = budgetAvailable;
errors.push(`Insufficient funds.`);
}
} }
break; break;
} }
...@@ -418,14 +552,13 @@ async function applyCategoryTemplate(category, template_lines, month, force) { ...@@ -418,14 +552,13 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
to_budget = limit - last_month_balance; to_budget = limit - last_month_balance;
} }
} }
if ( if (
((category.budgeted != null && category.budgeted !== 0) || ((category.budgeted != null && category.budgeted !== 0) ||
to_budget === 0) && to_budget === 0) &&
!force !force
) { ) {
return { errors }; return { errors };
} else if (category.budgeted === to_budget && force) { } else if (category.budgeted === to_budget) {
return null; return null;
} else { } else {
let str = category.name + ': ' + integerToAmount(last_month_balance); let str = category.name + ': ' + integerToAmount(last_month_balance);
......
---
category: Enhancements
authors: [shall0pass, youngcw]
---
Goals: Add priority support
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment