Skip to content
Snippets Groups Projects
Unverified Commit 6e7e98e1 authored by Pol Eyschen's avatar Pol Eyschen Committed by GitHub
Browse files

Allow templates to follow named schedules (#885)


Co-authored-by: default avatarJed Fox <git@jedfox.com>
parent b89b74c3
No related branches found
No related tags found
No related merge requests found
......@@ -25,6 +25,7 @@ import DateSelect from '../select/DateSelect';
import RecurringSchedulePicker from '../select/RecurringSchedulePicker';
import { SelectedItemsButton } from '../table';
import { AmountInput, BetweenAmountInput } from '../util/AmountInput';
import GenericInput from '../util/GenericInput';
function mergeFields(defaults, initial) {
let res = { ...defaults };
......@@ -115,6 +116,7 @@ export default function ScheduleDetails() {
amountOp: schedule._amountOp || 'isapprox',
date: schedule._date,
posts_transaction: action.schedule.posts_transaction,
name: schedule.name,
},
};
}
......@@ -201,6 +203,7 @@ export default function ScheduleDetails() {
amountOp: null,
date: null,
posts_transaction: false,
name: null,
},
initialFields,
),
......@@ -341,6 +344,18 @@ export default function ScheduleDetails() {
async function onSave() {
dispatch({ type: 'form-error', error: null });
if (state.fields.name) {
let { data: sameName } = await runQuery(
q('schedules').filter({ name: state.fields.name }).select('id'),
);
if (sameName.length > 0 && sameName[0].id !== state.schedule.id) {
dispatch({
type: 'form-error',
error: 'There is already a schedule with this name',
});
return;
}
}
let { error, conditions } = updateScheduleConditions(
state.schedule,
......@@ -356,6 +371,7 @@ export default function ScheduleDetails() {
schedule: {
id: state.schedule.id,
posts_transaction: state.fields.posts_transaction,
name: state.fields.name,
},
conditions,
});
......@@ -431,6 +447,20 @@ export default function ScheduleDetails() {
title={payee ? `Schedule: ${payee.name}` : 'Schedule'}
modalSize="medium"
>
<Stack direction="row" style={{ marginTop: 10 }}>
<FormField style={{ flex: 1 }}>
<FormLabel title="Schedule Name" htmlFor="name-field" />
<GenericInput
field="string"
type="string"
value={state.fields.name}
multi={false}
onChange={e => {
dispatch({ type: 'set-field', field: 'name', value: e });
}}
/>
</FormField>
</Stack>
<Stack direction="row" style={{ marginTop: 20 }}>
<FormField style={{ flex: 1 }}>
<FormLabel title="Payee" htmlFor="payee-field" />
......
......@@ -195,6 +195,14 @@ export function SchedulesTable({
':hover': { backgroundColor: colors.hover },
}}
>
<Field width="flex" name="name">
<Text
style={item.name == null ? { color: colors.n8 } : null}
title={item.name ? item.name : ''}
>
{item.name ? item.name : 'None'}
</Text>
</Field>
<Field width="flex" name="payee">
<DisplayId type="payees" id={item._payee} />
</Field>
......@@ -263,6 +271,7 @@ export function SchedulesTable({
return (
<View style={[{ flex: 1 }, tableStyle]}>
<TableHeader height={ROW_HEIGHT} inset={15} version="v2">
<Field width="flex">Name</Field>
<Field width="flex">Payee</Field>
<Field width="flex">Account</Field>
<Field width={110}>Next date</Field>
......
BEGIN TRANSACTION;
ALTER TABLE schedules ADD COLUMN name TEXT DEFAULT NULL;
COMMIT;
......@@ -85,6 +85,7 @@ export const schema = {
},
schedules: {
id: f('id'),
name: f('string'),
rule: f('id', { ref: 'rules', required: true }),
next_date: f('date'),
completed: f('boolean'),
......
// https://pegjs.org
expr
= percent: percent _ of _ category: $([^\n] *)
= percent: percent _ of _ category: $([^\r\n\t]+)
{ return { type: 'percentage', percent: +percent, category } }
/ amount: amount _ repeatEvery _ weeks: weekCount _ starting _ starting: date limit: limit?
{ return { type: 'week', amount, weeks, starting, limit } }
......@@ -17,6 +17,8 @@ expr
{ return { type: 'simple', monthly, limit } }
/ upTo _ limit: amount
{ return { type: 'simple', limit } }
/ schedule _ name: name
{ return { type: 'schedule', name} }
repeat 'repeat interval'
= 'month'i { return { annual: false } }
......@@ -39,6 +41,7 @@ of = 'of'i
repeatEvery = 'repeat'i _ 'every'i
starting = 'starting'i
upTo = 'up'i _ 'to'i
schedule = 'schedule'i
_ 'space' = ' '+
d 'digit' = [0-9]
......@@ -50,3 +53,5 @@ month 'month' = $(year '-' d d)
day 'day' = $(d d)
date = $(month '-' day)
currencySymbol 'currency symbol' = symbol: . & { return /\p{Sc}/u.test(symbol) }
name 'Name' = $([^\r\n\t]+)
......@@ -2,12 +2,18 @@ import {
differenceInCalendarMonths,
addMonths,
addWeeks,
addDays,
format,
} from 'date-fns';
import * as monthUtils from '../../shared/months';
import {
extractScheduleConds,
getScheduledAmount,
} from '../../shared/schedules';
import { amountToInteger, integerToAmount } from '../../shared/util';
import * as db from '../db';
import { getRuleForSchedule, getNextDate } from '../schedules/app';
import { setBudget, getSheetValue } from './actions';
import { parse } from './goal-template.pegjs';
......@@ -131,6 +137,10 @@ async function getCategoryTemplates() {
async function applyCategoryTemplate(category, template_lines, month, force) {
let current_month = new Date(`${month}-01`);
let errors = [];
let all_schedule_names = await db.all(
'SELECT name from schedules WHERE name NOT NULL AND tombstone = 0',
);
all_schedule_names = all_schedule_names.map(v => v.name);
// remove lines for past dates, calculate repeating dates
template_lines = template_lines.filter(template => {
......@@ -166,6 +176,12 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
template.from = format(spend_from, 'yyyy-MM');
}
break;
case 'schedule':
if (!all_schedule_names.includes(template.name)) {
errors.push(`Schedule ${template.name} does not exist`);
return null;
}
break;
default:
}
return true;
......@@ -323,6 +339,54 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
case 'error':
return { errors };
default:
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 isRepeating =
Object(dateCond.value) === dateCond.value &&
'frequency' in dateCond.value;
let next_date_string = getNextDate(dateCond, current_month);
let num_months = differenceInCalendarMonths(
new Date(next_date_string),
current_month,
);
let target = -getScheduledAmount(amountCond.value);
let diff = target - balance + budgeted;
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 > -1) {
to_budget += Math.round(diff / num_months);
}
} else {
let monthly_target = 0;
let next_month = addMonths(current_month, 1);
let next_date = new Date(next_date_string);
if (isRepeating) {
while (next_date.getTime() < next_month.getTime()) {
if (next_date.getTime() >= current_month.getTime()) {
monthly_target += target;
}
next_date = addDays(next_date, 1);
next_date_string = getNextDate(dateCond, next_date);
next_date = new Date(next_date_string);
}
} else {
monthly_target = target;
}
to_budget += monthly_target - balance + budgeted;
}
break;
}
}
}
......@@ -349,6 +413,6 @@ async function applyCategoryTemplate(category, template_lines, month, force) {
integerToAmount(last_month_balance + to_budget);
str += ' ' + template_lines.map(x => x.line).join('\n');
console.log(str);
return { amount: to_budget };
return { amount: to_budget, errors };
}
}
......@@ -176,6 +176,20 @@ export async function setNextDate({ id, start, conditions, reset }) {
// Methods
export async function checkIfScheduleExists(name, scheduleId) {
let idForName = await db.first('SELECT id from schedules WHERE name = ?', [
name,
]);
if (idForName == null) {
return false;
}
if (scheduleId) {
return idForName['id'] !== scheduleId;
}
return true;
}
export async function createSchedule({ schedule, conditions = [] } = {}) {
let scheduleId = (schedule && schedule.id) || uuid.v4Sync();
......@@ -189,6 +203,15 @@ export async function createSchedule({ schedule, conditions = [] } = {}) {
let nextDate = getNextDate(dateCond);
let nextDateRepr = nextDate ? toDateRepr(nextDate) : null;
if (schedule) {
if (schedule.name) {
if (await checkIfScheduleExists(schedule.name, scheduleId)) {
throw new Error('Cannot create schedules with the same name');
}
} else {
schedule.name = null;
}
}
// Create the rule here based on the info
let ruleId;
......@@ -224,6 +247,13 @@ export async function updateSchedule({ schedule, conditions, resetNextDate }) {
throw new Error('You cannot change the rule of a schedule');
}
if (schedule.name) {
if (await checkIfScheduleExists(schedule.name, schedule.id)) {
throw new Error('There is already a schedule with this name');
}
} else {
schedule.name = null;
}
// We need the rule if there are conditions
let rule;
......
---
category: Features
authors: [pole95]
---
Add template keyword to budget according to named schedules
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