diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 06a5ef21dffe2718cd9b42f81f09246a543e4e0c..eede6a8c0704bd30df44aef05d4aecac3f4de0e4 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -1,9 +1,6 @@ import { Notification } from '../../client/state-types/notifications'; import * as monthUtils from '../../shared/months'; -import { - extractScheduleConds, - getScheduledAmount, -} from '../../shared/schedules'; +import { extractScheduleConds } from '../../shared/schedules'; import { amountToInteger, integerToAmount } from '../../shared/util'; import * as db from '../db'; import { getRuleForSchedule, getNextDate } from '../schedules/app'; @@ -156,9 +153,19 @@ async function processTemplate(month, force, category_templates) { let isScheduleOrBy = false; let priorityCheck = 0; if ( - template.filter(t => t.type === 'schedule' || t.type === 'by') - .length > 0 + template.filter( + t => + (t.type === 'schedule' || t.type === 'by') && + t.priority === priority, + ).length > 0 ) { + template = template.filter( + t => + (t.priority === priority && + (t.type !== 'schedule' || t.type !== 'by')) || + t.type === 'schedule' || + t.type === 'by', + ); let { lowPriority, errorNotice } = await checkScheduleTemplates( template, ); @@ -322,6 +329,8 @@ async function applyCategoryTemplate( ); all_schedule_names = all_schedule_names.map(v => v.name); + let scheduleFlag = false; //only run schedules portion once + // remove lines for past dates, calculate repeating dates template_lines = template_lines.filter(template => { switch (template.type) { @@ -377,6 +386,8 @@ async function applyCategoryTemplate( `${a.month}-01`, `${b.month}-01`, ); + } else if (a.type === 'schedule' || b.type === 'schedule') { + return a.priority - b.priority; } else { return a.type.localeCompare(b.type); } @@ -411,7 +422,7 @@ async function applyCategoryTemplate( } else { increment = limit; } - if (increment < budgetAvailable || !priority) { + if (to_budget + increment < budgetAvailable || !priority) { to_budget += increment; } else { if (budgetAvailable > 0) to_budget += budgetAvailable; @@ -608,82 +619,157 @@ async function applyCategoryTemplate( break; } case 'schedule': { - let { id: schedule_id } = await db.first( - 'SELECT id FROM schedules WHERE name = ?', - [template.name], - ); - let rule = await getRuleForSchedule(schedule_id); - let conditions = rule.serialize().conditions; - let { date: dateCond, amount: amountCond } = - extractScheduleConds(conditions); - let next_date_string = getNextDate( - dateCond, - monthUtils._parse(current_month), - ); - - let isRepeating = - Object(dateCond.value) === dateCond.value && - 'frequency' in dateCond.value; - - let num_months = monthUtils.differenceInCalendarMonths( - next_date_string, - current_month, - ); - - if (isRepeating) { - let monthlyTarget = 0; - let next_month = monthUtils.addMonths(current_month, num_months + 1); - let next_date = getNextDate( - dateCond, - monthUtils._parse(current_month), - ); - while (next_date < next_month) { - monthlyTarget += amountCond.value; - next_date = monthUtils.addDays(next_date, 1); - next_date = getNextDate(dateCond, monthUtils._parse(next_date)); - } - amountCond.value = monthlyTarget; - } - - if (template.full === true || isReflectBudget()) { - if (num_months === 0) { - to_budget = -getScheduledAmount(amountCond.value); - } - if (isReflectBudget() && !template.full) { - errors.push( - `Report budgets require the full option for Schedules.`, + if (!scheduleFlag) { + scheduleFlag = true; + let template = template_lines.filter(t => t.type === 'schedule'); + //in the case of multiple templates per category, schedules may have wrong priority level + let t = []; + let totalScheduledGoal = 0; + + for (let ll = 0; ll < template.length; ll++) { + let { id: sid, completed: complete } = await db.first( + 'SELECT * FROM schedules WHERE name = ?', + [template[ll].name], + ); + console.log(complete); + let rule = await getRuleForSchedule(sid); + let conditions = rule.serialize().conditions; + let { date: dateConditions, amount: amountCondition } = + extractScheduleConds(conditions); + let target = -amountCondition.value; + let next_date_string = getNextDate( + dateConditions, + monthUtils._parse(current_month), ); + let target_interval = dateConditions.value.interval; + let target_frequency = dateConditions.value.frequency; + let isRepeating = + Object(dateConditions.value) === dateConditions.value && + 'frequency' in dateConditions.value; + let num_months = monthUtils.differenceInCalendarMonths( + next_date_string, + current_month, + ); + t.push({ + template: template[ll], + target: target, + next_date_string: next_date_string, + target_interval: target_interval, + target_frequency: target_frequency, + num_months: num_months, + completed: complete, + }); + if (!complete) { + if (isRepeating) { + let monthlyTarget = 0; + let next_month = monthUtils.addMonths( + current_month, + t[ll].num_months + 1, + ); + let next_date = getNextDate( + dateConditions, + monthUtils._parse(current_month), + ); + while (next_date < next_month) { + monthlyTarget += amountCondition.value; + next_date = monthUtils.addDays(next_date, 1); + next_date = getNextDate( + dateConditions, + monthUtils._parse(next_date), + ); + } + t[ll].target = -monthlyTarget; + totalScheduledGoal += target; + } + } else { + errors.push( + `Schedule ${t[ll].template.name} is a completed schedule.`, + ); + } } - break; - } - if (l === 0) remainder = last_month_balance; - remainder = -getScheduledAmount(amountCond.value) - remainder; - let target = 0; - if (remainder >= 0) { - target = remainder; - remainder = 0; - } else { - target = 0; - remainder = Math.abs(remainder); - } - let diff = num_months >= 0 ? Math.round(target / (num_months + 1)) : 0; - if (num_months < 0) { - errors.push( - `Non-repeating schedule ${template.name} was due on ${next_date_string}, which is in the past.`, - ); - return { errors }; - } else if (num_months >= 0) { - if ( - (diff >= 0 && - num_months >= 0 && - to_budget + diff < budgetAvailable) || - !priority - ) { + t = t.filter(t => t.completed === 0); + t = t.sort((a, b) => b.target - a.target); + + let diff = 0; + if (balance >= totalScheduledGoal) { + for (let ll = 0; ll < t.length; ll++) { + if (t[ll].num_months < 0) { + errors.push( + `Non-repeating schedule ${t[ll].template.name} was due on ${t[ll].next_date_string}, which is in the past.`, + ); + break; + } + if ( + (t[ll].template.full && t[ll].num_months === 0) || + t[ll].target_frequency === 'weekly' || + t[ll].target_frequency === 'daily' + ) { + diff += t[ll].target; + } else if (t[ll].template.full && t[ll].num_months > 0) { + diff += 0; + } else { + diff += t[ll].target / t[ll].target_interval; + } + } + } else if (balance < totalScheduledGoal) { + for (let ll = 0; ll < t.length; ll++) { + if (isReflectBudget()) { + if (!t[ll].template.full) { + errors.push( + `Report budgets require the full option for Schedules.`, + ); + break; + } + if (t[ll].template.full && t[ll].num_months === 0) { + to_budget += t[ll].target; + } + } + if (!isReflectBudget()) { + if (t[ll].num_months < 0) { + errors.push( + `Non-repeating schedule ${t[ll].template.name} was due on ${t[ll].next_date_string}, which is in the past.`, + ); + break; + } + if (t[ll].template.full && t[ll].num_months > 0) { + remainder = 0; + } else if (ll === 0 && !t[ll].template.full) { + remainder = t[ll].target - last_month_balance; + } else { + remainder = t[ll].target - remainder; + } + let tg = 0; + if (remainder >= 0) { + tg = remainder; + remainder = 0; + } else { + tg = 0; + remainder = Math.abs(remainder); + } + if ( + t[ll].template.full || + t[ll].num_months === 0 || + t[ll].target_frequency === 'weekly' || + t[ll].target_frequency === 'daily' + ) { + diff += tg; + } else if (t[ll].template.full && t[ll].num_months > 0) { + diff += 0; + } else { + diff += tg / (t[ll].num_months + 1); + } + } + } + } + diff = Math.round(diff); + if ((diff > 0 && to_budget + diff <= budgetAvailable) || !priority) { to_budget += diff; - if (l === template_lines.length - 1) to_budget -= spent; - } else { - if (budgetAvailable > 0) to_budget = budgetAvailable; + } else if ( + to_budget + diff > budgetAvailable && + budgetAvailable >= 0 + ) { + to_budget = budgetAvailable; errors.push(`Insufficient funds.`); } } diff --git a/upcoming-release-notes/1452.md b/upcoming-release-notes/1452.md new file mode 100644 index 0000000000000000000000000000000000000000..824eef0192db19d93c69fe093553690eaed7c7f7 --- /dev/null +++ b/upcoming-release-notes/1452.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [shall0pass] +--- + +Goals: Schedules allow filling for future months \ No newline at end of file