diff --git a/CHANGES.rst b/CHANGES.rst index 5709527e..45222bf0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -51,6 +51,13 @@ Unreleased Changes * Upgrade chromedriver in TravisCI builds from 2.33 to 2.36, to fix failing acceptance tests caused by Ubuntu upgrade from Chrome 64 to 65. * Fix bug in ``/budgets`` view where "Spending By Budget, Per Calendar Month" chart was showing only inactive budgets instead of only active budgets. +* `Issue #178 `_ - UI support for splitting Transactions between multiple Budgets. +* Have frontend forms submit as JSON POST instead of urlencoded. +* Properly capture Chrome console logs during acceptance tests. +* Bump ``versionfinder`` requirement version to 0.1.3 to work with pip 9.0.2. +* On help view, show long version string if we have it. +* `Issue #177 `_ - Fix bug in ``flask rundev`` logging. +* Many workarounds for flaky acceptance tests, including some for the selenium/Chrome "Element is not clickable at point... Other element would receive the click" error. 0.7.1 (2018-01-10) ------------------ diff --git a/README.rst b/README.rst index 10a6ab1d..5959432e 100644 --- a/README.rst +++ b/README.rst @@ -76,6 +76,7 @@ Main Features * Tracking of vehicle fuel fills (fuel log) and graphing of fuel economy. * Cost tracking for multiple projects, including bills-of-materials for them. Optional synchronization from Amazon Wishlists to projects. * Calculation of estimated credit card payoff amount and time, with configurable payment methods, payment increases on specific dates, and additional payments on specific dates. +* Ability to split a Transaction across multiple Budgets. Requirements ------------ diff --git a/biweeklybudget/biweeklypayperiod.py b/biweeklybudget/biweeklypayperiod.py index fa390837..b54394c5 100644 --- a/biweeklybudget/biweeklypayperiod.py +++ b/biweeklybudget/biweeklypayperiod.py @@ -428,36 +428,67 @@ def _make_budget_sums(self): 'is_income': b.is_income } for t in self.transactions_list: - if t['budget_id'] not in res: - # Issue #161 - inactive budget, but transaction for it in this - # period. Add it if it's a periodic budget. - b = self._db.query(Budget).get(t['budget_id']) - if not b.is_periodic: - continue - res[b.id] = { - 'budget_amount': b.starting_balance, - 'allocated': Decimal('0.0'), - 'spent': Decimal('0.0'), - 'trans_total': Decimal('0.0'), - 'is_income': b.is_income - } + # if a ScheduledTransaction, update some values and then continue if t['type'] == 'ScheduledTransaction': - res[t['budget_id']]['allocated'] += t['amount'] - res[t['budget_id']]['trans_total'] += t['amount'] + for budg_id, budg_data in t['budgets'].items(): + if budg_id not in res: + # Issue #161 - inactive budget, but transaction for it + # in this period. Add it if it's a periodic budget. + b = self._db.query(Budget).get(budg_id) + if not b.is_periodic: + continue + res[b.id] = { + 'budget_amount': b.starting_balance, + 'allocated': Decimal('0.0'), + 'spent': Decimal('0.0'), + 'trans_total': Decimal('0.0'), + 'is_income': b.is_income + } + res[budg_id]['allocated'] += budg_data['amount'] + res[budg_id]['trans_total'] += budg_data['amount'] continue - # t['type'] == 'Transaction' - res[t['budget_id']]['trans_total'] += t['amount'] - if t['budgeted_amount'] is None: - res[t['budget_id']]['allocated'] += t['amount'] - res[t['budget_id']]['spent'] += t['amount'] - else: + # NOT a ScheduledTransaction; must be an actual Transaction + for budg_id, budg_data in t['budgets'].items(): + if budg_id not in res: + # Issue #161 - inactive budget, but transaction for it + # in this period. Add it if it's a periodic budget. + b = self._db.query(Budget).get(budg_id) + if not b.is_periodic: + continue + res[b.id] = { + 'budget_amount': b.starting_balance, + 'allocated': Decimal('0.0'), + 'spent': Decimal('0.0'), + 'trans_total': Decimal('0.0'), + 'is_income': b.is_income + } + # update the budget's transactions total and spent amount + res[budg_id]['trans_total'] += budg_data['amount'] + res[budg_id]['spent'] += budg_data['amount'] + if t['budgeted_amount'] is None: + res[budg_id]['allocated'] += budg_data['amount'] + if t['budgeted_amount'] is not None: + # has a budgeted amount. It _shouldn't_ be possible to have a + # budgeted_amount without a planned_budget_id, but if that + # happens, allocate to the first budget. if t.get('planned_budget_id', None) is None: - res[t['budget_id']]['allocated'] += t[ - 'budgeted_amount'] + bid = list(t['budgets'].keys())[0] else: - res[t['planned_budget_id']]['allocated'] += t[ - 'budgeted_amount'] - res[t['budget_id']]['spent'] += t['amount'] + bid = t['planned_budget_id'] + if bid not in res: + # Issue #161 - inactive budget, but transaction for it + # in this period. Add it if it's a periodic budget. + b = self._db.query(Budget).get(bid) + if not b.is_periodic: + continue + res[bid] = { + 'budget_amount': b.starting_balance, + 'allocated': Decimal('0.0'), + 'spent': Decimal('0.0'), + 'trans_total': Decimal('0.0'), + 'is_income': b.is_income + } + res[bid]['allocated'] += t['budgeted_amount'] for b in res.keys(): if res[b]['trans_total'] > res[b]['allocated']: res[b]['remaining'] = res[ @@ -549,10 +580,9 @@ def _trans_dict(self, t): against. * ``account_name`` (**str**) the name of the Account the transaction is against. - * ``budget_id`` (**int**) the id of the Budget the transaction is - against. - * ``budget_name`` (**str**) the name of the Budget the transaction is - against. + * ``budgets`` (**dict**) dict of information on the Budgets this + Transaction is against. Keys are budget IDs (**int**), values are + dicts with keys "amount" (**Decimal**) and "name" (**string**). * ``reconcile_id`` (**int**) the ID of the TxnReconcile, or None * ``planned_budget_id`` (**int**) the id of the Budget the transaction was planned against, if any. May be None. @@ -591,15 +621,14 @@ def _dict_for_trans(self, t): against. * ``account_name`` (**str**) the name of the Account the transaction is against. - * ``budget_id`` (**int**) the id of the Budget the transaction is - against. - * ``budget_name`` (**str**) the name of the Budget the transaction is - against. * ``reconcile_id`` (**int**) the ID of the TxnReconcile, or None * ``planned_budget_id`` (**int**) the id of the Budget the transaction was planned against, if any. May be None. * ``planned_budget_name`` (**str**) the name of the Budget the transaction was planned against, if any. May be None. + * ``budgets`` (**dict**) dict of information on the Budgets this + Transaction is against. Keys are budget IDs (**int**), values are + dicts with keys "amount" (**Decimal**) and "name" (**string**). :param t: transaction to describe :type t: Transaction @@ -616,10 +645,14 @@ def _dict_for_trans(self, t): 'amount': t.actual_amount, 'account_id': t.account_id, 'account_name': t.account.name, - 'budget_id': t.budget_transactions[0].budget_id, - 'budget_name': t.budget_transactions[0].budget.name, 'planned_budget_id': t.planned_budget_id, - 'planned_budget_name': None + 'planned_budget_name': None, + 'budgets': { + bt.budget_id: { + 'amount': bt.amount, + 'name': bt.budget.name + } for bt in t.budget_transactions + } } if t.reconcile is None: res['reconcile_id'] = None @@ -654,11 +687,10 @@ def _dict_for_sched_trans(self, t): against. * ``account_name`` (**str**) the name of the Account the transaction is against. - * ``budget_id`` (**int**) the id of the Budget the transaction is - against. - * ``budget_name`` (**str**) the name of the Budget the transaction is - against. * ``reconcile_id`` (**int**) the ID of the TxnReconcile, or None + * ``budgets`` (**dict**) dict of information on the Budgets this + Transaction is against. Keys are budget IDs (**int**), values are + dicts with keys "amount" (**Decimal**) and "name" (**string**). :param t: ScheduledTransaction to describe :type t: ScheduledTransaction @@ -675,9 +707,13 @@ def _dict_for_sched_trans(self, t): 'budgeted_amount': None, 'account_id': t.account_id, 'account_name': t.account.name, - 'budget_id': t.budget_id, - 'budget_name': t.budget.name, - 'reconcile_id': None + 'reconcile_id': None, + 'budgets': { + t.budget_id: { + 'name': t.budget.name, + 'amount': t.amount + } + } } if t.schedule_type == 'date': res['date'] = t.date diff --git a/biweeklybudget/flaskapp/app.py b/biweeklybudget/flaskapp/app.py index bc07185a..70faec89 100644 --- a/biweeklybudget/flaskapp/app.py +++ b/biweeklybudget/flaskapp/app.py @@ -38,6 +38,13 @@ import logging import os +# workaround for https://github.com/jantman/versionfinder/issues/5 +# caused by versionfinder import in ``views/help.py`` +try: + import pip # noqa +except (ImportError, KeyError): + pass + from flask import Flask from biweeklybudget.db import init_db, cleanup_db diff --git a/biweeklybudget/flaskapp/cli_commands.py b/biweeklybudget/flaskapp/cli_commands.py index a0cfbf0d..943ec050 100644 --- a/biweeklybudget/flaskapp/cli_commands.py +++ b/biweeklybudget/flaskapp/cli_commands.py @@ -58,9 +58,8 @@ def send_response(self, *args, **kw): def log_request(self, code='-', size='-'): duration = int((self._time_finished - self._time_started) * 1000) self.log( - 'info', '"{0}" {1} {2} [{3}ms]'.format( - self.requestline, code, size, duration - ) + 'info', '"%s" %s %s [%sms]', + self.requestline, code, size, duration ) diff --git a/biweeklybudget/flaskapp/filters.py b/biweeklybudget/flaskapp/filters.py index d5fbb4ab..abaecdf7 100644 --- a/biweeklybudget/flaskapp/filters.py +++ b/biweeklybudget/flaskapp/filters.py @@ -200,3 +200,21 @@ def monthsyears(num): if num < 12: return '%f months' % num return '%.1f years' % (num / 12.0) + + +@app.template_filter('budgetCell') +def budget_cell_filter(d): + """ + Given a dictionary of budget IDs to names and amounts like that returned by + :py:meth:`~.BiweeklyPayPeriod._dict_for_trans`, return the ```` content + for those budgets. + """ + if len(d) == 1: + foo = list(d.keys())[0] + return '%s' % (foo, d[foo]['name']) + items = [ + '%s (%s)' % ( + k, d[k]['name'], dollars_filter(d[k]['amount']) + ) for k in sorted(d.keys(), key=lambda x: d[x]['amount'], reverse=True) + ] + return '
'.join(items) diff --git a/biweeklybudget/flaskapp/static/js/formBuilder.js b/biweeklybudget/flaskapp/static/js/formBuilder.js index 2171aed6..ad54e243 100644 --- a/biweeklybudget/flaskapp/static/js/formBuilder.js +++ b/biweeklybudget/flaskapp/static/js/formBuilder.js @@ -310,13 +310,18 @@ FormBuilder.prototype.addRadioInline = function(name, label, options) { * @param {String} name - The name of the form element * @param {String} label - The label text for the form element * @param {Boolean} checked - Whether to default to checked or not + * @param {Object} options + * @param {String} options.inputHtml - extra HTML string to include in the + * actual ``input`` element *(optional; defaults to null)* * @return {FormBuilder} this */ -FormBuilder.prototype.addCheckbox = function(id, name, label, checked) { +FormBuilder.prototype.addCheckbox = function(id, name, label, checked, options) { + if(options === undefined) { options = {}; } this.html += '
' + '
\n'; return this; }; diff --git a/biweeklybudget/flaskapp/static/js/forms.js b/biweeklybudget/flaskapp/static/js/forms.js index 241204b0..ffb37235 100644 --- a/biweeklybudget/flaskapp/static/js/forms.js +++ b/biweeklybudget/flaskapp/static/js/forms.js @@ -46,15 +46,25 @@ Jason Antman * @param {string} form_id - The ID of the form itself. * @param {string} post_url - Relative URL to post form data to. * @param {Object} dataTableObj - passed on to ``handleFormSubmitted()`` + * @param {Object} serialize_func - If set (i.e. not ``undefined``), this is + * a function used serialize the form in place of :js:func:`serializeForm`. + * This function will be passed the ID of the form (``form_id``) and should + * return an Object suitable for passing to ``JSON.stringify()``. */ -function handleForm(container_id, form_id, post_url, dataTableObj) { - var data = serializeForm(form_id); +function handleForm(container_id, form_id, post_url, dataTableObj, serialize_func) { + if (serialize_func === undefined) { + var data = serializeForm(form_id); + } else { + var data = serialize_func(form_id); + } $('.formfeedback').remove(); $('.has-error').each(function(index) { $(this).removeClass('has-error'); }); $.ajax({ type: "POST", url: post_url, - data: data, + data: JSON.stringify(data), + dataType: "json", + contentType: "application/json", success: function(data) { handleFormSubmitted(data, container_id, form_id, dataTableObj); }, diff --git a/biweeklybudget/flaskapp/static/js/reconcile.js b/biweeklybudget/flaskapp/static/js/reconcile.js index 4ad52459..d2b4cbc5 100644 --- a/biweeklybudget/flaskapp/static/js/reconcile.js +++ b/biweeklybudget/flaskapp/static/js/reconcile.js @@ -168,11 +168,22 @@ function reconcileDoUnreconcile(trans_id, acct_id, fitid) { * @param {Object} trans - ajax JSON object representing one Transaction */ function reconcileTransDiv(trans) { - var div = '
' + var div = '
'; div += '
' + trans['date']['str'] + '
'; div += '
' + fmt_currency(trans['actual_amount']) + '
'; div += ''; - div += ''; + div += '
Budget: '; + for (var index = 0; index < trans['budgets'].length; ++index) { + var budg = trans['budgets'][index]; + var txt = budg['name']; + txt = txt + ' (' + budg['id'] + ')'; + if (trans['budgets'].length > 1) { + txt = txt + ' (' + fmt_currency(budg['amount']) + ')'; + } + div += '' + txt + ''; + if(index < trans['budgets'].length - 1) { div += '
'; } + } + div += '
'; div += '
'; div += '
'; div += '
Trans ' + trans['id'] + ': ' + trans['description'] + '
'; @@ -343,12 +354,21 @@ function makeTransFromOfx(acct_id, fitid) { $('#modalSaveButton').off(); $('#modalSaveButton').click( function() { - handleForm( - 'modalBody', 'transForm', '/forms/transaction', - function(data) { - makeTransSaveCallback(data, acct_id, fitid); - } - ); + var valid = validateTransModalSplits(); + if (valid == null) { + $('#modalSaveButton').prop('disabled', false); + $('#budget-split-feedback').html(''); + handleForm( + 'modalBody', 'transForm', '/forms/transaction', + function(data) { + makeTransSaveCallback(data, acct_id, fitid); + }, + transModalFormSerialize + ); + } else { + $('#budget-split-feedback').html('

' + valid + '

'); + $('#modalSaveButton').prop('disabled', true); + } } ).show(); $('#trans_frm_date_input_group').datepicker({ diff --git a/biweeklybudget/flaskapp/static/js/reconcile_modal.js b/biweeklybudget/flaskapp/static/js/reconcile_modal.js index 93ba12c2..15c9b541 100644 --- a/biweeklybudget/flaskapp/static/js/reconcile_modal.js +++ b/biweeklybudget/flaskapp/static/js/reconcile_modal.js @@ -58,7 +58,18 @@ function txnReconcileModalDiv(msg) { frm += 'Budgeted Amount' + fmt_currency(msg['transaction']['budgeted_amount']) + '\n'; frm += 'Description' + msg['transaction']['description'] + '\n'; frm += 'Account' + msg['acct_name'] + ' (' + msg['acct_id'] + ')\n'; - frm += 'Budget' + msg['budget_name'] + ' (' + msg['budget_id'] + ')\n'; + frm += 'Budget'; + for (var index = 0; index < msg['transaction']['budgets'].length; ++index) { + var budg = msg['transaction']['budgets'][index]; + var txt = budg['name']; + txt = txt + ' (' + budg['id'] + ')'; + if (msg['transaction']['budgets'].length > 1) { + txt = txt + ' (' + fmt_currency(budg['amount']) + ')'; + } + frm += '' + txt + ''; + if(index < msg['transaction']['budgets'].length - 1) { frm += '
'; } + } + frm += '\n'; frm += 'Notes' + msg['transaction']['notes'] + '\n'; frm += 'Scheduled?'; if (msg['transaction']['scheduled_trans_id'] !== null) { diff --git a/biweeklybudget/flaskapp/static/js/transactions.js b/biweeklybudget/flaskapp/static/js/transactions.js index 9afe54b6..598002ad 100644 --- a/biweeklybudget/flaskapp/static/js/transactions.js +++ b/biweeklybudget/flaskapp/static/js/transactions.js @@ -67,8 +67,27 @@ $(document).ready(function() { }, { data: "DT_RowData.budget", + orderable: false, // split-budget transactions breaks sorting "render": function(data, type, row) { - return $("
").append($("").attr("href", "/budgets/" + row.DT_RowData.budget_id).text(data)).html(); + if(row.DT_RowData.budgets.length == 1) { + var budg = row.DT_RowData.budgets[0]; + var txt = budg['name']; + if(budg['is_income'] === true) { txt = txt + ' (income)'; } + txt = txt + ' (' + budg['id'] + ')'; + return $("
").append($("").attr("href", "/budgets/" + budg['id']).text(txt)).html(); + } else { + var div = $("
"); + for (index = 0; index < row.DT_RowData.budgets.length; ++index) { + var budg = row.DT_RowData.budgets[index]; + var txt = budg['name']; + if(budg['is_income'] === true) { txt = txt + ' (income)'; } + txt = txt + ' (' + budg['id'] + ')'; + txt = txt + ' (' + fmt_currency(budg['amount']) + ')'; + div.append($("").attr("href", "/budgets/" + budg['id']).text(txt)); + if(index < row.DT_RowData.budgets.length - 1) { div.append($('
')); } + } + return div.html(); + } } }, { diff --git a/biweeklybudget/flaskapp/static/js/transactions_modal.js b/biweeklybudget/flaskapp/static/js/transactions_modal.js index 4ce85f57..d65980e9 100644 --- a/biweeklybudget/flaskapp/static/js/transactions_modal.js +++ b/biweeklybudget/flaskapp/static/js/transactions_modal.js @@ -35,6 +35,8 @@ Jason Antman ################################################################################ */ +var validation_count = 0; // helper for acceptance testing of validation logic + /** * Generate the HTML for the form on the Modal */ @@ -45,7 +47,23 @@ function transModalDivForm() { .addCurrency('trans_frm_amount', 'amount', 'Amount', { helpBlock: 'Transaction amount (positive for expenses, negative for income).' }) .addText('trans_frm_description', 'description', 'Description') .addLabelToValueSelect('trans_frm_account', 'account', 'Account', acct_names_to_id, 'None', true) + .addCheckbox( + 'trans_frm_is_split', 'is_split', 'Budget Split?', false, + { inputHtml: 'onchange="transModalHandleSplit()"' } + ) + .addHTML('
') .addLabelToValueSelect('trans_frm_budget', 'budget', 'Budget', budget_names_to_id, 'None', true) + .addHTML( + '
' + ) .addText('trans_frm_notes', 'notes', 'Notes') .addHTML('') .render(); @@ -61,7 +79,6 @@ function transModalDivFillAndShow(msg) { $('#trans_frm_date').val(msg['date']['str']); $('#trans_frm_amount').val(msg['actual_amount']); $('#trans_frm_account option[value=' + msg['account_id'] + ']').prop('selected', 'selected').change(); - $('#trans_frm_budget option[value=' + msg['budget_id'] + ']').prop('selected', 'selected').change(); $('#trans_frm_notes').val(msg['notes']); if(msg['transfer_id'] !== null) { $('#trans_frm_transfer_p').html( @@ -70,6 +87,19 @@ function transModalDivFillAndShow(msg) { ); $('#trans_frm_transfer_p').show(); } + if(msg['budgets'].length == 1) { + $('#trans_frm_budget option[value=' + msg['budgets'][0]['id'] + ']').prop('selected', 'selected').change(); + } else { + $('#trans_frm_is_split').prop('checked', true); + $('#trans_frm_budget_group').hide(); + $('#trans_frm_split_budget_container').show(); + for(var i = 0; i < msg['budgets'].length; i++) { + if(i > 1) { $('#trans_frm_budget_splits_div').append(transModalBudgetSplitRowHtml(i)); } + var budg = msg['budgets'][i]; + $('#trans_frm_budget_' + i + ' option[value=' + budg['id'] + ']').prop('selected', 'selected').change(); + $('#trans_frm_budget_amount_' + i).val(budg['amount']); + } + } $("#modalDiv").modal('show'); } @@ -78,7 +108,8 @@ function transModalDivFillAndShow(msg) { * information for one Transaction. This function calls * :js:func:`transModalDivForm` to generate the form HTML, * :js:func:`transModalDivFillAndShow` to populate the form for editing, - * and :js:func:`handleForm` to handle the Submit action. + * and :js:func:`handleForm` to handle the Submit action (using + * :js:func:`transModalFormSerialize` as a custom serialization function). * * @param {number} id - the ID of the Transaction to show a modal for, * or null to show modal to add a new Transaction. @@ -96,7 +127,15 @@ function transModal(id, dataTableObj) { }); $('#modalSaveButton').off(); $('#modalSaveButton').click(function() { - handleForm('modalBody', 'transForm', '/forms/transaction', dataTableObj); + var valid = validateTransModalSplits(); + if (valid == null) { + $('#modalSaveButton').prop('disabled', false); + $('#budget-split-feedback').html(''); + handleForm('modalBody', 'transForm', '/forms/transaction', dataTableObj, transModalFormSerialize); + } else { + $('#budget-split-feedback').html('

' + valid + '

'); + $('#modalSaveButton').prop('disabled', true); + } }).show(); if(id) { var url = "/ajax/transactions/" + id; @@ -107,3 +146,159 @@ function transModal(id, dataTableObj) { $("#modalDiv").modal('show'); } } + +/** + * Custom serialization function passed to :js:func:`handleForm` for + * Transaction modal forms generated by :js:func:`transModal`. This handles + * serialization of Transaction forms that may have a budget split, generating + * data with a ``budgets`` Object (hash/mapping/dict) with budget ID keys and + * amount values, suitable for passing directly to + * :py:meth:`~.Transaction.set_budget_amounts`. + * + * @param {String} form_id the ID of the form on the page. + */ +function transModalFormSerialize(form_id) { + var data = serializeForm(form_id); + data['budgets'] = {}; + var num_rows = $('.budget_split_row').length; + var rownum = 0; + if($('#trans_frm_is_split').prop('checked')) { + for (rownum = 0; rownum < num_rows; rownum++) { + var bid = $('#trans_frm_budget_' + rownum).find(':selected').val(); + if(bid != 'None') { + data['budgets'][bid] = $('#trans_frm_budget_amount_' + rownum).val(); + } + } + } else { + data['budgets'][data['budget']] = data['amount']; + } + // strip out the budget_ and amount_ items + delete data['budget']; + delete data['is_split']; + for (rownum = 0; rownum < num_rows; rownum++) { + delete data['budget_' + rownum]; + delete data['amount_' + rownum]; + } + return data; +} + +/** + * Handler for change of the "Budget Split?" (``#trans_frm_is_split``) checkbox. + */ +function transModalHandleSplit() { + if($('#trans_frm_is_split').prop('checked')) { + // split + $('#trans_frm_budget_group').hide(); + $('#trans_frm_split_budget_container').show(); + var oldid = $('#trans_frm_budget').find(':selected').val(); + if(oldid != 'None') { $('#trans_frm_budget_0 option[value=' + oldid + ']').prop('selected', 'selected').change(); } + } else { + // not split + $('#trans_frm_split_budget_container').hide(); + $('#trans_frm_budget_group').show(); + } +} + +/** + * Function to validate Transaction modal split budgets. Returns null if valid + * or otherwise a String error message. + */ +function validateTransModalSplits() { + if(! $('#trans_frm_is_split').prop('checked')) { return null; } + var budget_ids = []; + var total = 0.0; + for (rownum = 0; rownum < $('.budget_split_row').length; rownum++) { + var bid = $('#trans_frm_budget_' + rownum).find(':selected').val(); + if(bid != 'None') { + if(budget_ids.indexOf(bid) > -1) { + return 'Error: A given budget may only be specified once.'; + } + budget_ids.push(bid); + } + if($('#trans_frm_budget_amount_' + rownum).val() != '') { + total = total + parseFloat($('#trans_frm_budget_amount_' + rownum).val()); + } + } + var amt = parseFloat($('#trans_frm_amount').val()); + // Note: workaround for JS floating point math issues... + if(amt.toFixed(4) != total.toFixed(4)) { + return 'Error: Sum of budget allocations (' + total.toFixed(4) + ') must equal transaction amount (' + amt.toFixed(4) + ').'; + } + return null; +} + +/** + * Handler for the "Add Budget" link on trans modal when using budget split. + */ +function transModalAddSplitBudget() { + var next_row_num = $('.budget_split_row').length; + $('#trans_frm_budget_splits_div').append(transModalBudgetSplitRowHtml(next_row_num)); + transModalSplitBudgetChanged(next_row_num); +} + +/** + * Triggered when a form element for budget splits loses focus. Calls + * :js:func:`validateTransModalSplits` and updates the warning div with the + * result. + */ +function budgetSplitBlur() { + var res = validateTransModalSplits(); + if (res == null) { + $('#budget-split-feedback').html(''); + $('#modalSaveButton').prop('disabled', false); + validation_count = validation_count + 1; + return null; + } + $('#budget-split-feedback').html('

' + res + '

'); + $('#modalSaveButton').prop('disabled', true); + validation_count = validation_count + 1; +} + +/** + * Generate HTML for a budget div inside the split budgets div. + * + * @param {Integer} row_num - the budget split row number + */ +function transModalBudgetSplitRowHtml(row_num) { + var html = '
'; + // budget select + html += '
'; + html += ''; + html += '
'; // .form-group #trans_frm_budget_group + // amount + html += '
'; + html += '
'; + html += '$'; + html += ''; + html += '
'; // .input-group + html += '
'; // .form-group #trans_frm_budget_amount_group + // close overall div + html += '
'; // .budget_split_row + return html; +} + +/** + * Called when a budget split dropdown is changed. If its amount box is empty, + * set it to the transaction amount minus the sum of all other budget splits. + * + * @param {Integer} row_num - the budget split row number + */ +function transModalSplitBudgetChanged(row_num) { + if($('#trans_frm_budget_amount_' + row_num).val() != '') { return null; } + var amt = parseFloat($('#trans_frm_amount').val()); + var total = 0.0; + for (var rownum = 0; rownum < $('.budget_split_row').length; rownum++) { + if($('#trans_frm_budget_amount_' + rownum).val() != '') { + total = total + parseFloat($('#trans_frm_budget_amount_' + rownum).val()); + } + } + var remainder = amt - total; + if(remainder > 0) { + $('#trans_frm_budget_amount_' + row_num).val(remainder.toFixed(2)); + } +} diff --git a/biweeklybudget/flaskapp/templates/payperiod.html b/biweeklybudget/flaskapp/templates/payperiod.html index a78f475f..3e9cb961 100644 --- a/biweeklybudget/flaskapp/templates/payperiod.html +++ b/biweeklybudget/flaskapp/templates/payperiod.html @@ -187,7 +187,7 @@ (sched) {{ t['description'] }} ({{ t['id'] }}) {% endif %} {{ t['account_name'] }} - {{ t['budget_name'] }} + {{ t['budgets']|budgetCell|safe }} {% if t['type'] == 'Transaction' %} {% if t['sched_trans_id'] != None %} (from {{ t['sched_trans_id'] }}) diff --git a/biweeklybudget/flaskapp/views/accounts.py b/biweeklybudget/flaskapp/views/accounts.py index 5adf93ee..d7cd234c 100644 --- a/biweeklybudget/flaskapp/views/accounts.py +++ b/biweeklybudget/flaskapp/views/accounts.py @@ -195,22 +195,13 @@ def submit(self, data): account.name = data['name'].strip() account.description = self.fix_string(data['description']) account.acct_type = getattr(AcctType, data['acct_type']) - if data['ofx_cat_memo_to_name'] == 'true': - account.ofx_cat_memo_to_name = True - else: - account.ofx_cat_memo_to_name = False + account.ofx_cat_memo_to_name = data['ofx_cat_memo_to_name'] account.vault_creds_path = self.fix_string(data['vault_creds_path']) account.ofxgetter_config_json = self.fix_string( data['ofxgetter_config_json'] ) - if data['negate_ofx_amounts'] == 'true': - account.negate_ofx_amounts = True - else: - account.negate_ofx_amounts = False - if data['reconcile_trans'] == 'true': - account.reconcile_trans = True - else: - account.reconcile_trans = False + account.negate_ofx_amounts = data['negate_ofx_amounts'] + account.reconcile_trans = data['reconcile_trans'] if account.acct_type == AcctType.Credit: if data['credit_limit'].strip() != '': account.credit_limit = Decimal(data['credit_limit']) @@ -231,10 +222,7 @@ def submit(self, data): account.prime_rate_margin = None account.interest_class_name = data['interest_class_name'] account.min_payment_class_name = data['min_payment_class_name'] - if data['is_active'] == 'true': - account.is_active = True - else: - account.is_active = False + account.is_active = data['is_active'] for f in RE_FIELD_NAMES: data[f] = data[f].strip() if data[f] == '': diff --git a/biweeklybudget/flaskapp/views/budgets.py b/biweeklybudget/flaskapp/views/budgets.py index e06c37cd..75fc6718 100644 --- a/biweeklybudget/flaskapp/views/budgets.py +++ b/biweeklybudget/flaskapp/views/budgets.py @@ -144,7 +144,7 @@ def validate(self, data): errors['name'].append('Name cannot be empty') have_errors = True if ( - data['is_periodic'] == 'true' and + data['is_periodic'] is True and data['starting_balance'].strip() == '' ): errors['starting_balances'].append( @@ -152,7 +152,7 @@ def validate(self, data): ) have_errors = True if ( - data['is_periodic'] == 'false' and + data['is_periodic'] is False and data['current_balance'].strip() == '' ): errors['current_balances'].append( @@ -184,24 +184,14 @@ def submit(self, data): action = 'creating new Budget' budget.name = data['name'].strip() budget.description = data['description'].strip() - if data['is_periodic'] == 'true': - budget.is_periodic = True + budget.is_periodic = data['is_periodic'] + if data['is_periodic'] is True: budget.starting_balance = Decimal(data['starting_balance']) else: - budget.is_periodic = False budget.current_balance = Decimal(data['current_balance']) - if data['is_active'] == 'true': - budget.is_active = True - else: - budget.is_active = False - if data['is_income'] == 'true': - budget.is_income = True - else: - budget.is_income = False - if data['omit_from_graphs'] == 'true': - budget.omit_from_graphs = True - else: - budget.omit_from_graphs = False + budget.is_active = data['is_active'] + budget.is_income = data['is_income'] + budget.omit_from_graphs = data['omit_from_graphs'] logger.info('%s: %s', action, budget.as_dict) db_session.add(budget) db_session.commit() @@ -353,16 +343,17 @@ def _by_month(self): ), Transaction.date.__le__(dt_now) ).all(): - if t.budget_transactions[0].budget_id not in budget_names: - continue - budg_name = t.budget_transactions[0].budget.name - budgets_present.add(budg_name) - ds = t.date.strftime('%Y-%m') - if ds not in records: - records[ds] = {'date': ds} - if budg_name not in records[ds]: - records[ds][budg_name] = Decimal('0') - records[ds][budg_name] += t.budget_transactions[0].amount + for bt in t.budget_transactions: + if bt.budget_id not in budget_names: + continue + budg_name = bt.budget.name + budgets_present.add(budg_name) + ds = t.date.strftime('%Y-%m') + if ds not in records: + records[ds] = {'date': ds} + if budg_name not in records[ds]: + records[ds][budg_name] = Decimal('0') + records[ds][budg_name] += bt.amount result = [records[k] for k in sorted(records.keys())] res = { 'data': result, diff --git a/biweeklybudget/flaskapp/views/formhandlerview.py b/biweeklybudget/flaskapp/views/formhandlerview.py index acd43a05..a1513a00 100644 --- a/biweeklybudget/flaskapp/views/formhandlerview.py +++ b/biweeklybudget/flaskapp/views/formhandlerview.py @@ -57,7 +57,10 @@ def post(self): 'success' -> boolean 'success_message' -> string success message """ - data = request.form.to_dict() + data = request.get_json() + if data is None: + # not JSON, must be form encoded + data = request.form.to_dict() res = self.validate(data) if res is not None: logger.info('Form validation failed. data=%s errors=%s', diff --git a/biweeklybudget/flaskapp/views/fuel.py b/biweeklybudget/flaskapp/views/fuel.py index 237c3a45..61805c1d 100644 --- a/biweeklybudget/flaskapp/views/fuel.py +++ b/biweeklybudget/flaskapp/views/fuel.py @@ -253,10 +253,7 @@ def submit(self, data): veh = Vehicle() action = 'creating new Vehicle' veh.name = data['name'].strip() - if data['is_active'] == 'true': - veh.is_active = True - else: - veh.is_active = False + veh.is_active = data['is_active'] logger.info('%s: %s', action, veh.as_dict) db_session.add(veh) db_session.commit() @@ -280,7 +277,7 @@ def validate(self, data): """ errors = {k: [] for k in data.keys()} errors = self._validate_date_ymd('date', data, errors) - if data['add_trans'] == 'true': + if data['add_trans'] is True: if data['account'] == 'None': errors['account'].append('Transactions must have an account') if data['budget'] == 'None': @@ -335,7 +332,7 @@ def submit(self, data): db_session.commit() fill.calculate_mpg() db_session.commit() - if data['add_trans'] != 'true': + if data['add_trans'] is not True: return { 'success_message': 'Successfully saved FuelFill %d ' 'in database.' % fill.id, diff --git a/biweeklybudget/flaskapp/views/help.py b/biweeklybudget/flaskapp/views/help.py index 48cdb783..18082c2b 100644 --- a/biweeklybudget/flaskapp/views/help.py +++ b/biweeklybudget/flaskapp/views/help.py @@ -65,7 +65,7 @@ def get(self): if 'git' in VERSION: ver = VERSION else: - ver = find_version('biweeklybudget').version + ver = find_version('biweeklybudget').long_str return render_template( 'help.html', ver_info=ver, diff --git a/biweeklybudget/flaskapp/views/projects.py b/biweeklybudget/flaskapp/views/projects.py index a6aed87e..b606b82d 100644 --- a/biweeklybudget/flaskapp/views/projects.py +++ b/biweeklybudget/flaskapp/views/projects.py @@ -400,10 +400,7 @@ def submit(self, data): item.quantity = int(data['quantity'].strip()) item.unit_cost = Decimal(data['unit_cost'].strip()) item.url = data['url'].strip() - if data['is_active'] == 'true': - item.is_active = True - else: - item.is_active = False + item.is_active = data['is_active'] logger.info('%s: %s', action, item.as_dict) db_session.add(item) db_session.commit() diff --git a/biweeklybudget/flaskapp/views/reconcile.py b/biweeklybudget/flaskapp/views/reconcile.py index 12061a2d..e404036a 100644 --- a/biweeklybudget/flaskapp/views/reconcile.py +++ b/biweeklybudget/flaskapp/views/reconcile.py @@ -81,10 +81,20 @@ def get(self, reconcile_id): res = { 'reconcile': rec.as_dict, - 'transaction': rec.transaction.as_dict, - 'budget_name': rec.transaction.budget_transactions[0].budget.name, - 'budget_id': rec.transaction.budget_transactions[0].budget_id + 'transaction': rec.transaction.as_dict } + res['transaction']['budgets'] = [ + { + 'name': bt.budget.name, + 'id': bt.budget_id, + 'amount': bt.amount, + 'is_income': bt.budget.is_income + } + for bt in sorted( + rec.transaction.budget_transactions, + key=lambda x: x.amount, reverse=True + ) + ] if rec.ofx_trans is not None: res['ofx_trans'] = rec.ofx_trans.as_dict res['ofx_stmt'] = rec.ofx_trans.statement.as_dict @@ -124,9 +134,19 @@ def get(self): for t in Transaction.unreconciled( db_session).order_by(Transaction.date).all(): d = t.as_dict + d['budgets'] = [ + { + 'name': bt.budget.name, + 'id': bt.budget_id, + 'amount': bt.amount, + 'is_income': bt.budget.is_income + } + for bt in sorted( + t.budget_transactions, key=lambda x: x.amount, + reverse=True + ) + ] d['account_name'] = t.account.name - d['budget_name'] = t.budget_transactions[0].budget.name - d['budget_id'] = t.budget_transactions[0].budget_id res.append(d) return jsonify(res) diff --git a/biweeklybudget/flaskapp/views/scheduled.py b/biweeklybudget/flaskapp/views/scheduled.py index 5366b775..2c1fb34c 100644 --- a/biweeklybudget/flaskapp/views/scheduled.py +++ b/biweeklybudget/flaskapp/views/scheduled.py @@ -269,10 +269,7 @@ def submit(self, data): trans.account_id = int(data['account']) trans.budget_id = int(data['budget']) trans.notes = data['notes'].strip() - if data['is_active'] == 'true': - trans.is_active = True - else: - trans.is_active = False + trans.is_active = data['is_active'] logger.info('%s: %s', action, trans.as_dict) db_session.add(trans) db_session.commit() diff --git a/biweeklybudget/flaskapp/views/transactions.py b/biweeklybudget/flaskapp/views/transactions.py index 02001ccc..cbff0c61 100644 --- a/biweeklybudget/flaskapp/views/transactions.py +++ b/biweeklybudget/flaskapp/views/transactions.py @@ -195,14 +195,19 @@ def get(self): ) table.add_data( acct_id=lambda o: o.account_id, - budget_id=lambda o: o.budget_transactions[0].budget_id, - id=lambda o: o.id, - budget=lambda o: "{} {}({})".format( - o.budget_transactions[0].budget.name, - '(income) ' - if o.budget_transactions[0].budget.is_income else '', - o.budget_transactions[0].budget_id - ) + budgets=lambda o: [ + { + 'name': bt.budget.name, + 'id': bt.budget_id, + 'amount': bt.amount, + 'is_income': bt.budget.is_income + } + for bt in sorted( + o.budget_transactions, key=lambda x: x.amount, + reverse=True + ) + ], + id=lambda o: o.id ) if args['search[value]'] != '': table.searchable(lambda qs, s: self._filterhack(qs, s, args_dict)) @@ -218,9 +223,18 @@ def get(self, trans_id): t = db_session.query(Transaction).get(trans_id) d = copy(t.as_dict) d['account_name'] = t.account.name - d['budget'] = t.budget_transactions[0].budget - d['budget_id'] = t.budget_transactions[0].budget_id - d['budget_name'] = t.budget_transactions[0].budget.name + d['budgets'] = [ + { + 'name': bt.budget.name, + 'id': bt.budget_id, + 'amount': bt.amount, + 'is_income': bt.budget.is_income + } + for bt in sorted( + t.budget_transactions, key=lambda x: x.amount, + reverse=True + ) + ] return jsonify(d) @@ -248,29 +262,47 @@ def validate(self, data): if data.get('description', '').strip() == '': errors['description'].append('Description cannot be empty') have_errors = True - if float(data['amount']) == 0: + if Decimal(data['amount']) == Decimal('0'): errors['amount'].append('Amount cannot be zero') have_errors = True if data['account'] == 'None': errors['account'].append('Transactions must have an account') have_errors = True - if data['budget'] == 'None': - errors['budget'].append('Transactions must have a budget') + if len(data['budgets']) < 1: + errors['budgets'].append('Transactions must have a budget.') have_errors = True else: - budg = db_session.query(Budget).get(int(data['budget'])) - if not budg.is_active and txn is None: - errors['budget'].append( - 'New transactions cannot use an inactive budget.' - ) - have_errors = True - elif ( - not budg.is_active and - txn.budget_transactions[0].budget_id != budg.id - ): - errors['budget'].append( - 'Existing transactions cannot be changed to use an ' - 'inactive budget.' + budgets_total = Decimal('0.0') + for bid, budg_amt in data['budgets'].items(): + budg = db_session.query(Budget).get(int(bid)) + budgets_total += Decimal(budg_amt) + if budg is None: + errors['budgets'].append( + 'Budget ID %s is invalid.' % bid + ) + have_errors = True + continue + if not budg.is_active and txn is None: + errors['budgets'].append( + 'New transactions cannot use an inactive budget ' + '(%s).' % budg.name + ) + have_errors = True + elif ( + not budg.is_active and + budg.id not in [ + x.budget_id for x in txn.budget_transactions + ] + ): + errors['budgets'].append( + 'Existing transactions cannot be changed to use an ' + 'inactive budget (%s).' % budg.name + ) + have_errors = True + if budgets_total != Decimal(data['amount']): + errors['budgets'].append( + 'Sum of all budget amounts (%s) must equal Transaction ' + 'amount (%s).' % (budgets_total, Decimal(data['amount'])) ) have_errors = True if data['date'].strip() == '': @@ -312,13 +344,15 @@ def submit(self, data): else: trans = Transaction() action = 'creating new Transaction' - budg = db_session.query(Budget).get(int(data['budget'])) trans.description = data['description'].strip() trans.date = datetime.strptime(data['date'], '%Y-%m-%d').date() trans.account_id = int(data['account']) trans.notes = data['notes'].strip() - # @TODO this only supports a single budget per transaction - trans.set_budget_amounts({budg: Decimal(data['amount'])}) + budg_amts = {} + for bid, budg_amt in data['budgets'].items(): + budg = db_session.query(Budget).get(int(bid)) + budg_amts[budg] = Decimal(budg_amt) + trans.set_budget_amounts(budg_amts) logger.info('%s: %s', action, trans.as_dict) db_session.add(trans) db_session.commit() diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_accounts.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_accounts.py index 4ab83f24..4b151e69 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_accounts.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_accounts.py @@ -118,7 +118,7 @@ def test_credit_table(self, selenium): assert self.tbody2textlist(table) == [ [ 'CreditOne', '-$952.06 (13 hours ago)', '$2,000.00', - '$1,047.94', '$222.22', '$825.72' + '$1,047.94', '$544.54', '$503.40' ], [ 'CreditTwo', '-$5,498.65 (a day ago)', '$5,500.00', '$1.35', @@ -303,8 +303,7 @@ def test_20_verify_db(self, testdb): def test_21_get_acct2_click(self, base_url, selenium): self.get(selenium, base_url + '/accounts') link = selenium.find_element_by_xpath('//a[text()="BankTwoStale"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Account 2' assert selenium.find_element_by_id('account_frm_id').get_attribute( @@ -366,8 +365,7 @@ def test_21_get_acct2_click(self, base_url, selenium): def test_22_edit_acct2(self, base_url, selenium): self.get(selenium, base_url + '/accounts') link = selenium.find_element_by_xpath('//a[text()="BankTwoStale"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Account 2' selenium.find_element_by_id('account_frm_name').send_keys('Edited') @@ -462,8 +460,7 @@ def test_30_verify_db(self, testdb): def test_31_get_acct3_click(self, base_url, selenium): self.get(selenium, base_url + '/accounts') link = selenium.find_element_by_xpath('//a[text()="CreditOne"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Account 3' assert selenium.find_element_by_id('account_frm_id').get_attribute( @@ -520,8 +517,7 @@ def test_31_get_acct3_click(self, base_url, selenium): def test_32_edit_acct3(self, base_url, selenium): self.get(selenium, base_url + '/accounts') link = selenium.find_element_by_xpath('//a[text()="CreditOne"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Account 3' selenium.find_element_by_id('account_frm_name').send_keys('Edited') @@ -738,8 +734,7 @@ def test_50_verify_db(self, testdb): def test_51_get_acct5_click(self, base_url, selenium): self.get(selenium, base_url + '/accounts') link = selenium.find_element_by_xpath('//a[text()="InvestmentOne"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Account 5' assert selenium.find_element_by_id('account_frm_id').get_attribute( @@ -784,8 +779,7 @@ def test_51_get_acct5_click(self, base_url, selenium): def test_52_edit_acct5(self, base_url, selenium): self.get(selenium, base_url + '/accounts') link = selenium.find_element_by_xpath('//a[text()="InvestmentOne"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Account 5' selenium.find_element_by_id('account_frm_name').send_keys('Edited') @@ -832,8 +826,7 @@ def test_60_verify_db(self, testdb): def test_61_modal_add_bank(self, base_url, selenium): self.get(selenium, base_url + '/accounts') link = selenium.find_element_by_id('btn_add_acct_bank') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Account' assert selenium.find_element_by_id('account_frm_id').get_attribute( @@ -923,8 +916,7 @@ def test_70_verify_db(self, testdb): def test_71_modal_add_credit(self, base_url, selenium): self.get(selenium, base_url + '/accounts') link = selenium.find_element_by_id('btn_add_acct_credit') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Account' assert selenium.find_element_by_id('account_frm_id').get_attribute( @@ -1040,8 +1032,7 @@ def test_80_verify_db(self, testdb): def test_81_modal_add_bank(self, base_url, selenium): self.get(selenium, base_url + '/accounts') link = selenium.find_element_by_id('btn_add_acct_invest') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Account' assert selenium.find_element_by_id('account_frm_id').get_attribute( diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_budgets.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_budgets.py index 428fd01f..1312d165 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_budgets.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_budgets.py @@ -119,8 +119,7 @@ def test_00_budget_modal_verify_db(self, testdb): def test_01_budget_modal_populate_modal(self, base_url, selenium): self.get(selenium, base_url + '/budgets') link = selenium.find_element_by_xpath('//a[text()="Periodic1 (1)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Budget 1' assert selenium.find_element_by_id('budget_frm_name').get_attribute( @@ -149,8 +148,7 @@ def test_02_budget_modal_update_modal(self, base_url, selenium): # Fill in the form self.get(selenium, base_url + '/budgets') link = selenium.find_element_by_xpath('//a[text()="Periodic1 (1)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) name = selenium.find_element_by_id('budget_frm_name') name.clear() @@ -202,8 +200,7 @@ def test_03_budget_modal_verify_db(self, testdb): def test_10_populate_edit_periodic_2_modal(self, base_url, selenium): self.get(selenium, base_url + '/budgets') link = selenium.find_element_by_xpath('//a[text()="Periodic2 (2)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Budget 2' assert selenium.find_element_by_id('budget_frm_name').get_attribute( @@ -233,8 +230,7 @@ def test_11_populate_edit_periodic_3_modal(self, base_url, selenium): link = selenium.find_element_by_xpath( '//a[text()="Periodic3 Inactive (3)"]' ) - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Budget 3' assert selenium.find_element_by_id('budget_frm_name').get_attribute( @@ -263,8 +259,7 @@ def test_11_populate_edit_periodic_3_modal(self, base_url, selenium): def test_12_populate_edit_standing_1_modal(self, base_url, selenium): self.get(selenium, base_url + '/budgets') link = selenium.find_element_by_xpath('//a[text()="Standing1 (4)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Budget 4' assert selenium.find_element_by_id('budget_frm_name').get_attribute( @@ -292,8 +287,7 @@ def test_12_populate_edit_standing_1_modal(self, base_url, selenium): def test_13_populate_edit_standing_2_modal(self, base_url, selenium): self.get(selenium, base_url + '/budgets') link = selenium.find_element_by_xpath('//a[text()="Standing2 (5)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Budget 5' assert selenium.find_element_by_id('budget_frm_name').get_attribute( @@ -323,8 +317,7 @@ def test_14_populate_edit_standing_3_modal(self, base_url, selenium): link = selenium.find_element_by_xpath( '//a[text()="Standing3 Inactive (6)"]' ) - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Budget 6' assert selenium.find_element_by_id('budget_frm_name').get_attribute( @@ -364,8 +357,7 @@ def test_20_income_verify_db(self, testdb): def test_21_populate_income_modal(self, base_url, selenium): self.get(selenium, base_url + '/budgets') link = selenium.find_element_by_xpath('//a[text()="Income (7)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Budget 7' assert selenium.find_element_by_id('budget_frm_name').get_attribute( @@ -393,8 +385,7 @@ def test_22_update_income_modal(self, base_url, selenium): # Fill in the form self.get(selenium, base_url + '/budgets') link = selenium.find_element_by_xpath('//a[text()="Income (7)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) name = selenium.find_element_by_id('budget_frm_name') name.clear() @@ -476,8 +467,7 @@ def test_41_add_standing_modal(self, base_url, selenium): # Fill in the form self.get(selenium, base_url + '/budgets') link = selenium.find_element_by_id('btn_add_budget') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) name = selenium.find_element_by_id('budget_frm_name') name.clear() @@ -531,8 +521,7 @@ def test_51_add_income_modal(self, base_url, selenium): # Fill in the form self.get(selenium, base_url + '/budgets') link = selenium.find_element_by_id('btn_add_budget') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) name = selenium.find_element_by_id('budget_frm_name') name.clear() @@ -595,7 +584,7 @@ def test_1_verify_db(self, testdb): max_t = max([ t.id for t in testdb.query(Transaction).all() ]) - assert max_t == 3 + assert max_t == 4 max_r = max([ t.id for t in testdb.query(TxnReconcile).all() ]) @@ -612,8 +601,7 @@ def test_2_transfer_modal(self, base_url, selenium): ptexts = self.tbody2textlist(ptable) assert ptexts[2] == ['yes', 'Periodic2 (2)', '$234.00'] link = selenium.find_element_by_id('btn_budget_txfr') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Budget Transfer' assert body.find_element_by_id( @@ -684,7 +672,7 @@ def test_2_transfer_modal(self, base_url, selenium): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully saved Transactions 4 and 5' \ + assert x.text.strip() == 'Successfully saved Transactions 5 and 6' \ ' in database.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() @@ -700,7 +688,7 @@ def test_2_transfer_modal(self, base_url, selenium): def test_3_verify_db(self, testdb): desc = 'Budget Transfer - 123.45 from Periodic2 (2) to Standing2 (5)' - t1 = testdb.query(Transaction).get(4) + t1 = testdb.query(Transaction).get(5) assert t1.date == dtnow().date() assert t1.actual_amount == Decimal('123.45') assert t1.budgeted_amount == Decimal('123.45') @@ -712,11 +700,11 @@ def test_3_verify_db(self, testdb): assert t1.budget_transactions[0].budget_id == 2 assert t1.budget_transactions[0].amount == Decimal('123.45') rec1 = testdb.query(TxnReconcile).get(2) - assert rec1.txn_id == 4 + assert rec1.txn_id == 5 assert rec1.ofx_fitid is None assert rec1.ofx_account_id is None assert rec1.note == desc - t2 = testdb.query(Transaction).get(5) + t2 = testdb.query(Transaction).get(6) assert t2.date == dtnow().date() assert t2.actual_amount == Decimal('-123.45') assert t2.budgeted_amount == Decimal('-123.45') @@ -728,7 +716,7 @@ def test_3_verify_db(self, testdb): assert t2.budget_transactions[0].budget_id == 5 assert t2.budget_transactions[0].amount == Decimal('-123.45') rec2 = testdb.query(TxnReconcile).get(3) - assert rec2.txn_id == 5 + assert rec2.txn_id == 6 assert rec2.ofx_fitid is None assert rec2.ofx_account_id is None assert rec2.note == desc @@ -743,7 +731,7 @@ def test_1_verify_db(self, testdb): max_t = max([ t.id for t in testdb.query(Transaction).all() ]) - assert max_t == 3 + assert max_t == 4 max_r = max([ t.id for t in testdb.query(TxnReconcile).all() ]) @@ -766,8 +754,7 @@ def test_2_transfer_modal(self, base_url, selenium, testdb): assert pp.budget_sums[2]['spent'] == Decimal('222.22') assert pp.budget_sums[2]['trans_total'] == Decimal('222.22') link = selenium.find_element_by_id('btn_budget_txfr') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Budget Transfer' assert body.find_element_by_id( @@ -838,7 +825,7 @@ def test_2_transfer_modal(self, base_url, selenium, testdb): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully saved Transactions 4 and 5' \ + assert x.text.strip() == 'Successfully saved Transactions 5 and 6' \ ' in database.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() @@ -862,7 +849,7 @@ def test_3_verify_db(self, testdb): assert pp.budget_sums[2]['spent'] == Decimal('98.77') assert pp.budget_sums[2]['trans_total'] == Decimal('98.77') desc = 'Budget Transfer - 123.45 from Standing2 (5) to Periodic2 (2)' - t1 = testdb.query(Transaction).get(4) + t1 = testdb.query(Transaction).get(5) assert t1.date == dtnow().date() assert t1.actual_amount == Decimal('123.45') assert t1.budgeted_amount == Decimal('123.45') @@ -874,11 +861,11 @@ def test_3_verify_db(self, testdb): assert t1.budget_transactions[0].budget_id == 5 assert t1.budget_transactions[0].amount == Decimal('123.45') rec1 = testdb.query(TxnReconcile).get(2) - assert rec1.txn_id == 4 + assert rec1.txn_id == 5 assert rec1.ofx_fitid is None assert rec1.ofx_account_id is None assert rec1.note == desc - t2 = testdb.query(Transaction).get(5) + t2 = testdb.query(Transaction).get(6) assert t2.date == dtnow().date() assert t2.actual_amount == Decimal('-123.45') assert t2.budgeted_amount == Decimal('-123.45') @@ -890,7 +877,7 @@ def test_3_verify_db(self, testdb): assert t2.budget_transactions[0].budget_id == 2 assert t2.budget_transactions[0].amount == Decimal('-123.45') rec2 = testdb.query(TxnReconcile).get(3) - assert rec2.txn_id == 5 + assert rec2.txn_id == 6 assert rec2.ofx_fitid is None assert rec2.ofx_account_id is None assert rec2.note == desc diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_credit_payoffs.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_credit_payoffs.py index 7e20590d..d0129b7d 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_credit_payoffs.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_credit_payoffs.py @@ -146,8 +146,7 @@ def test_06_add_interest(self, selenium, base_url): '//a[text()="manually input the interest charge from your ' 'last statement"]' ) - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add Manual Interest Charge for Account 4' frm_id = selenium.find_element_by_id('payoff_acct_frm_id') diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_fuel.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_fuel.py index 453ebdf4..f960bb0b 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_fuel.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_fuel.py @@ -272,8 +272,7 @@ def test_01_vehicle_populate_modal(self, base_url, selenium): self.get(selenium, base_url + '/fuel') link = selenium.find_element_by_xpath('//a[text()="Veh1"]') self.wait_until_clickable(selenium, '//a[text()="Veh1"]', by='xpath') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Vehicle 1' assert selenium.find_element_by_id('vehicle_frm_id').get_attribute( @@ -288,8 +287,7 @@ def test_02_vehicle_edit_modal_inactive(self, base_url, selenium): self.wait_until_clickable( selenium, '//a[text()="Veh3Inactive"]', by='xpath' ) - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Vehicle 3' assert selenium.find_element_by_id('vehicle_frm_id').get_attribute( @@ -350,8 +348,7 @@ def test_04_vehicle_modal_add(self, base_url, selenium): self.get(selenium, base_url + '/fuel') link = selenium.find_element_by_id('btn-add-vehicle') self.wait_until_clickable(selenium, 'btn-add-vehicle') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Vehicle' name = selenium.find_element_by_id('vehicle_frm_name') @@ -410,15 +407,14 @@ def test_10_fuel_verify_db(self, testdb): trans_ids = [ x.id for x in testdb.query(Transaction).all() ] - assert len(trans_ids) == 3 - assert max(trans_ids) == 3 + assert len(trans_ids) == 4 + assert max(trans_ids) == 4 def test_11_fuel_populate_modal(self, base_url, selenium): self.get(selenium, base_url + '/fuel') link = selenium.find_element_by_id('btn-add-fuel') self.wait_until_clickable(selenium, 'btn-add-fuel') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add Fuel Fill' veh_sel = Select(selenium.find_element_by_id('fuel_frm_vehicle')) @@ -488,8 +484,7 @@ def test_12_fuel_add_no_trans(self, base_url, selenium): self.get(selenium, base_url + '/fuel') link = selenium.find_element_by_id('btn-add-fuel') self.wait_until_clickable(selenium, 'btn-add-fuel') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add Fuel Fill' veh_sel = Select(selenium.find_element_by_id('fuel_frm_vehicle')) @@ -584,15 +579,14 @@ def test_13_fuel_verify_db(self, testdb): trans_ids = [ x.id for x in testdb.query(Transaction).all() ] - assert len(trans_ids) == 3 - assert max(trans_ids) == 3 + assert len(trans_ids) == 4 + assert max(trans_ids) == 4 def test_14_fuel_add_with_trans(self, base_url, selenium): self.get(selenium, base_url + '/fuel') link = selenium.find_element_by_id('btn-add-fuel') self.wait_until_clickable(selenium, 'btn-add-fuel') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add Fuel Fill' veh_sel = Select(selenium.find_element_by_id('fuel_frm_vehicle')) @@ -648,7 +642,7 @@ def test_14_fuel_add_with_trans(self, base_url, selenium): x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') assert x.text.strip() == 'Successfully saved FuelFill 8 ' \ - 'and Transaction 4 in database.' + 'and Transaction 5 in database.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() self.wait_for_jquery_done(selenium) @@ -686,9 +680,9 @@ def test_15_fuel_verify_db(self, testdb): trans_ids = [ x.id for x in testdb.query(Transaction).all() ] - assert len(trans_ids) == 4 - assert max(trans_ids) == 4 - trans = testdb.query(Transaction).get(4) + assert len(trans_ids) == 5 + assert max(trans_ids) == 5 + trans = testdb.query(Transaction).get(5) assert trans.date == (dtnow() - timedelta(days=2)).date() assert trans.actual_amount == Decimal('14.82') assert trans.budgeted_amount is None diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_index.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_index.py index 53c55383..8d402ec9 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_index.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_index.py @@ -121,7 +121,7 @@ def test_credit_table(self, selenium): 'Account', 'Balance', 'Available', 'Avail - Unrec' ] assert self.tbody2textlist(table) == [ - ['CreditOne', '-$952.06 (13 hours ago)', '$1,047.94', '$825.72'], + ['CreditOne', '-$952.06 (13 hours ago)', '$1,047.94', '$503.40'], ['CreditTwo', '-$5,498.65 (a day ago)', '$1.35', '$1.35'] ] links = [] diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_ofx.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_ofx.py index c6ec042f..dbbebc60 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_ofx.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_ofx.py @@ -289,8 +289,7 @@ def get_page(self, base_url, selenium): def test_modal_on_click(self, selenium): link = selenium.find_element_by_xpath('//a[text()="T1"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'OFXTransaction Account=3 FITID=T1' texts = self.tbody2textlist(body) @@ -385,8 +384,7 @@ def test_1_modal(self, base_url, selenium): self.get(selenium, base_url + '/transactions') link = selenium.find_element_by_xpath( '//a[@href="javascript:txnReconcileModal(1)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Transaction Reconcile 1' dl = body.find_element_by_tag_name('dl') diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_payperiods.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_payperiods.py index 403591a4..cd32407f 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_payperiods.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_payperiods.py @@ -627,7 +627,10 @@ def test_3_add_transactions(self, testdb): testdb.add(t2) t3 = Transaction( date=(ppdate + timedelta(days=3)), - budget_amounts={e1budget: Decimal('600.00')}, + budget_amounts={ + e1budget: Decimal('600.00'), + e2budget: Decimal('20.00') + }, budgeted_amount=Decimal('500.00'), description='prev trans 2', account=acct, @@ -745,9 +748,9 @@ def test_4_confirm_sums(self, testdb): periods = self.pay_periods(testdb) assert periods['prev'].overall_sums == { 'allocated': Decimal('750.0'), - 'spent': Decimal('850.0'), + 'spent': Decimal('870.0'), 'income': Decimal('1000.0'), - 'remaining': Decimal('150.0') + 'remaining': Decimal('130.0') } assert periods['curr'].overall_sums == { 'allocated': Decimal('2350.0'), @@ -790,7 +793,7 @@ def test_5_other_periods_table(self, base_url, selenium, testdb): '%s' % pp.next.next.next.start_date.strftime('%Y-%m-%d') ] assert self.tbody2textlist(table) == [[ - '$150.00', + '$130.00', '-$1,050.00', '$12.00', '$798.00', @@ -830,7 +833,7 @@ def test_00_inactivate_scheduled(self, testdb): # delete existing transactions for tr in testdb.query(TxnReconcile).all(): testdb.delete(tr) - for idx in [1, 2, 3]: + for idx in [1, 2, 3, 4]: t = testdb.query(Transaction).get(idx) testdb.delete(t) testdb.flush() @@ -904,7 +907,10 @@ def test_01_add_transactions(self, testdb): testdb.add(t3) t4 = Transaction( date=ppdate, - budget_amounts={e2budget: Decimal('222.22')}, + budget_amounts={ + e1budget: Decimal('10.00'), + e2budget: Decimal('222.22') + }, description='T3', notes='notesT3', account=testdb.query(Account).get(3) @@ -924,9 +930,9 @@ def test_03_info_panels(self, base_url, selenium, testdb): ) assert selenium.find_element_by_id( 'amt-income').text == '$2,345.67' - assert selenium.find_element_by_id('amt-allocated').text == '$411.10' - assert selenium.find_element_by_id('amt-spent').text == '$345.35' - assert selenium.find_element_by_id('amt-remaining').text == '$1,934.57' + assert selenium.find_element_by_id('amt-allocated').text == '$421.10' + assert selenium.find_element_by_id('amt-spent').text == '$355.35' + assert selenium.find_element_by_id('amt-remaining').text == '$1,924.57' def test_04_periodic_budgets(self, base_url, selenium, testdb): self.get( @@ -945,9 +951,9 @@ def test_04_periodic_budgets(self, base_url, selenium, testdb): [ 'Periodic1', '$100.00', - '$155.55', - '$123.13', - '-$56.46' + '$165.55', + '$133.13', + '-$66.46' ], [ 'Periodic2', @@ -996,8 +1002,7 @@ def test_06_add_trans_button(self, base_url, selenium, testdb): PAY_PERIOD_START_DATE.strftime('%Y-%m-%d') ) btn = selenium.find_element_by_id('btn-add-txn') - btn.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, btn) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Transaction' @@ -1032,17 +1037,18 @@ def test_07_transaction_table(self, base_url, selenium, testdb): ], [ ppdate.strftime('%Y-%m-%d'), - '$222.22', - 'T3 (7)', + '$232.22', + 'T3 (8)', 'CreditOne', - 'Periodic2', + 'Periodic2 ($222.22)
' + 'Periodic1 ($10.00)', ' ', ' ' ], [ (ppdate + timedelta(days=2)).strftime('%Y-%m-%d'), '-$333.33', - 'T2 (6)', + 'T2 (7)', 'BankTwoStale', 'Standing1', '(from 3)' @@ -1066,7 +1072,7 @@ def test_07_transaction_table(self, base_url, selenium, testdb): [ (ppdate + timedelta(days=6)).strftime('%Y-%m-%d'), '$111.13', - 'T1foo (5)', + 'T1foo (6)', 'BankOne', 'Periodic1', '(from 1)' @@ -1090,8 +1096,8 @@ def test_07_transaction_table(self, base_url, selenium, testdb): [ (pp.start_date + timedelta(days=8)).strftime('%Y-%m-%d'), '$12.00', - 'Txn From ST7' - ' (4)', + 'Txn From ST7' + ' (5)', 'BankOne', 'Periodic1', '(from 7)' @@ -1107,13 +1113,12 @@ def test_08_transaction_modal(self, base_url, selenium, testdb): base_url + '/payperiod/' + PAY_PERIOD_START_DATE.strftime('%Y-%m-%d') ) - link = selenium.find_element_by_xpath('//a[text()="T1foo (5)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + link = selenium.find_element_by_xpath('//a[text()="T1foo (6)"]') + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) - assert title.text == 'Edit Transaction 5' + assert title.text == 'Edit Transaction 6' assert body.find_element_by_id( - 'trans_frm_id').get_attribute('value') == '5' + 'trans_frm_id').get_attribute('value') == '6' assert body.find_element_by_id( 'trans_frm_date').get_attribute('value') == ( pp.start_date + timedelta(days=6)).strftime('%Y-%m-%d') @@ -1159,13 +1164,12 @@ def test_09_transaction_modal_edit(self, base_url, selenium, testdb): base_url + '/payperiod/' + PAY_PERIOD_START_DATE.strftime('%Y-%m-%d') ) - link = selenium.find_element_by_xpath('//a[text()="T1foo (5)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + link = selenium.find_element_by_xpath('//a[text()="T1foo (6)"]') + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) - assert title.text == 'Edit Transaction 5' + assert title.text == 'Edit Transaction 6' assert body.find_element_by_id( - 'trans_frm_id').get_attribute('value') == '5' + 'trans_frm_id').get_attribute('value') == '6' desc = body.find_element_by_id('trans_frm_description') desc.send_keys('edited') # submit the form @@ -1175,7 +1179,7 @@ def test_09_transaction_modal_edit(self, base_url, selenium, testdb): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully saved Transaction 5 ' \ + assert x.text.strip() == 'Successfully saved Transaction 6 ' \ 'in database.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() @@ -1183,11 +1187,11 @@ def test_09_transaction_modal_edit(self, base_url, selenium, testdb): # test that updated budget was removed from the page table = selenium.find_element_by_id('trans-table') texts = [y[2] for y in self.tbody2textlist(table)] - assert 'T1fooedited (5)' in texts + assert 'T1fooedited (6)' in texts def test_10_transaction_modal_verify_db(self, testdb): pp = BiweeklyPayPeriod(PAY_PERIOD_START_DATE, testdb) - t = testdb.query(Transaction).get(5) + t = testdb.query(Transaction).get(6) assert t is not None assert t.description == 'T1fooedited' assert t.date == (pp.start_date + timedelta(days=6)) @@ -1222,8 +1226,7 @@ def test_12_sched_trans_modal(self, base_url, selenium): ) link = selenium.find_element_by_xpath( '//a[text()="ST7 per_period (7)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Scheduled Transaction 7' assert body.find_element_by_id( @@ -1255,8 +1258,7 @@ def test_13_sched_trans_modal_edit(self, base_url, selenium): ) link = selenium.find_element_by_xpath( '//a[text()="ST7 per_period (7)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) desc = body.find_element_by_id('sched_frm_description') desc.send_keys('edited') @@ -1299,8 +1301,7 @@ def test_20_issue152_active_budget(self, base_url, selenium): PAY_PERIOD_START_DATE.strftime('%Y-%m-%d') ) btn = selenium.find_element_by_id('btn-add-txn') - btn.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, btn) self.assert_modal_displayed(modal, title, body) dt_text = body.find_element_by_id('trans_frm_date') dt_text.clear() @@ -1352,7 +1353,7 @@ def test_20_issue152_active_budget(self, base_url, selenium): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully saved Transaction 8 ' \ + assert x.text.strip() == 'Successfully saved Transaction 9 ' \ 'in database.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() @@ -1360,11 +1361,11 @@ def test_20_issue152_active_budget(self, base_url, selenium): # test that updated budget was removed from the page table = selenium.find_element_by_id('trans-table') texts = [y[2] for y in self.tbody2textlist(table)] - assert 'issue152regression1 (8)' in texts + assert 'issue152regression1 (9)' in texts def test_21_issue152_verify_db(self, testdb): """verify the transaction we added""" - t1 = testdb.query(Transaction).get(8) + t1 = testdb.query(Transaction).get(9) assert t1.date == (dtnow() - timedelta(days=2)).date() assert t1.actual_amount == Decimal('100.00') assert t1.budgeted_amount is None @@ -1386,9 +1387,9 @@ def test_22_issue152_info_panels(self, base_url, selenium): ) assert selenium.find_element_by_id( 'amt-income').text == '$2,345.67' - assert selenium.find_element_by_id('amt-allocated').text == '$511.10' - assert selenium.find_element_by_id('amt-spent').text == '$445.35' - assert selenium.find_element_by_id('amt-remaining').text == '$1,834.57' + assert selenium.find_element_by_id('amt-allocated').text == '$521.10' + assert selenium.find_element_by_id('amt-spent').text == '$455.35' + assert selenium.find_element_by_id('amt-remaining').text == '$1,824.57' def test_23_issue152_periodic_budgets(self, base_url, selenium): """verify budget totals""" @@ -1408,9 +1409,9 @@ def test_23_issue152_periodic_budgets(self, base_url, selenium): [ 'Periodic1', '$100.00', - '$155.55', - '$123.13', - '-$56.46' + '$165.55', + '$133.13', + '-$66.46' ], [ 'Periodic2', @@ -1436,8 +1437,7 @@ def test_24_issue152_attempt_add_inactive_budget(self, base_url, selenium): PAY_PERIOD_START_DATE.strftime('%Y-%m-%d') ) btn = selenium.find_element_by_id('btn-add-txn') - btn.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, btn) self.assert_modal_displayed(modal, title, body) dt_text = body.find_element_by_id('trans_frm_date') dt_text.clear() @@ -1489,14 +1489,14 @@ def test_24_issue152_attempt_add_inactive_budget(self, base_url, selenium): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' not in x.get_attribute('class') - budg_grp = body.find_element_by_id('trans_frm_budget_group') - assert 'has-error' in budg_grp.get_attribute('class') + budg_grp = body.find_element_by_id('budgets-error-div-container') p_elems = budg_grp.find_elements_by_tag_name('p') assert len(p_elems) == 1 assert 'text-danger' in p_elems[0].get_attribute('class') assert p_elems[0].get_attribute( 'innerHTML' - ) == 'New transactions cannot use an inactive budget.' + ) == 'New transactions cannot use an inactive budget ' \ + '(Periodic3 Inactive).' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() self.wait_for_load_complete(selenium) @@ -1518,7 +1518,7 @@ def test_30_issue152_inactive_budget(self, testdb): ) testdb.add(trans) testdb.commit() - t1 = testdb.query(Transaction).get(9) + t1 = testdb.query(Transaction).get(10) assert t1.date == (dtnow() - timedelta(days=1)).date() assert t1.actual_amount == Decimal('200.00') assert t1.budgeted_amount is None @@ -1538,14 +1538,13 @@ def test_31_issue152_edit_inactive(self, base_url, selenium): PAY_PERIOD_START_DATE.strftime('%Y-%m-%d') ) link = selenium.find_element_by_xpath( - '//a[text()="issue152regression3 (9)"]' + '//a[text()="issue152regression3 (10)"]' ) - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) - assert title.text == 'Edit Transaction 9' + assert title.text == 'Edit Transaction 10' assert body.find_element_by_id( - 'trans_frm_id').get_attribute('value') == '9' + 'trans_frm_id').get_attribute('value') == '10' desc = body.find_element_by_id('trans_frm_description') desc.send_keys('-edited') # submit the form @@ -1555,7 +1554,7 @@ def test_31_issue152_edit_inactive(self, base_url, selenium): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully saved Transaction 9 ' \ + assert x.text.strip() == 'Successfully saved Transaction 10 ' \ 'in database.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() @@ -1563,10 +1562,10 @@ def test_31_issue152_edit_inactive(self, base_url, selenium): # test that updated budget was removed from the page table = selenium.find_element_by_id('trans-table') texts = [y[2] for y in self.tbody2textlist(table)] - assert 'issue152regression3-edited (9)' in texts + assert 'issue152regression3-edited (10)' in texts def test_32_issue152_verify_db(self, testdb): - t1 = testdb.query(Transaction).get(9) + t1 = testdb.query(Transaction).get(10) assert t1.date == (dtnow() - timedelta(days=1)).date() assert t1.actual_amount == Decimal('200.00') assert t1.budgeted_amount is None @@ -1588,9 +1587,9 @@ def test_40_issue161_info_panels(self, base_url, selenium): ) assert selenium.find_element_by_id( 'amt-income').text == '$2,345.67' - assert selenium.find_element_by_id('amt-allocated').text == '$711.10' - assert selenium.find_element_by_id('amt-spent').text == '$645.35' - assert selenium.find_element_by_id('amt-remaining').text == '$1,634.57' + assert selenium.find_element_by_id('amt-allocated').text == '$721.10' + assert selenium.find_element_by_id('amt-spent').text == '$655.35' + assert selenium.find_element_by_id('amt-remaining').text == '$1,624.57' def test_41_issue161_periodic_budgets(self, base_url, selenium): """verify budget totals""" @@ -1610,9 +1609,9 @@ def test_41_issue161_periodic_budgets(self, base_url, selenium): [ 'Periodic1', '$100.00', - '$155.55', - '$123.13', - '-$56.46' + '$165.55', + '$133.13', + '-$66.46' ], [ 'Periodic2', @@ -1775,8 +1774,7 @@ def test_04_make_trans_modal_num_per(self, base_url, selenium, testdb): link = selenium.find_elements_by_xpath( '//a[@href="javascript:schedToTransModal(7, \'%s\');"]' '' % pp.start_date.strftime('%Y-%m-%d'))[0] - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Scheduled Transaction 7 to Transaction' assert body.find_element_by_id( @@ -1808,8 +1806,7 @@ def test_05_modal_schedtrans_to_trans(self, base_url, selenium, testdb): link = selenium.find_elements_by_xpath( '//a[@href="javascript:schedToTransModal(7, \'%s\');"]' '' % pp.start_date.strftime('%Y-%m-%d'))[0] - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) body.find_element_by_id('schedtotrans_frm_date').clear() body.find_element_by_id('schedtotrans_frm_date').send_keys( @@ -1829,7 +1826,7 @@ def test_05_modal_schedtrans_to_trans(self, base_url, selenium, testdb): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully created Transaction 6 for ' \ + assert x.text.strip() == 'Successfully created Transaction 7 for ' \ 'ScheduledTransaction 7.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() @@ -1839,7 +1836,7 @@ def test_05_modal_schedtrans_to_trans(self, base_url, selenium, testdb): texts = self.tbody2textlist(table) # sort order changes when we make this active assert texts[0][2] == '(sched) ST7 per_period (7)' - assert texts[1][2] == 'ST7 per_period Trans (6)' + assert texts[1][2] == 'ST7 per_period Trans (7)' def test_06_verify_db(self, testdb): t = testdb.query(ScheduledTransaction).get(7) @@ -1854,7 +1851,7 @@ def test_06_verify_db(self, testdb): assert t.notes is None assert t.is_active is True pp = BiweeklyPayPeriod(PAY_PERIOD_START_DATE, testdb) - t = testdb.query(Transaction).get(6) + t = testdb.query(Transaction).get(7) assert t is not None assert t.description == 'ST7 per_period Trans' assert t.date == (pp.start_date + timedelta(days=2)) @@ -1886,7 +1883,7 @@ def test_00_inactivate_scheduled(self, testdb): # delete existing transactions for tr in testdb.query(TxnReconcile).all(): testdb.delete(tr) - for idx in [1, 2, 3]: + for idx in [1, 2, 3, 4]: t = testdb.query(Transaction).get(idx) testdb.delete(t) testdb.flush() @@ -2077,7 +2074,7 @@ def test_06_transaction_table(self, base_url, selenium, testdb): [ ppdate.strftime('%Y-%m-%d'), '$222.22', - 'T3 (7)', + 'T3 (8)', 'CreditOne', 'Periodic2', ' ', @@ -2086,7 +2083,7 @@ def test_06_transaction_table(self, base_url, selenium, testdb): [ (ppdate + timedelta(days=2)).strftime('%Y-%m-%d'), '-$333.33', - 'T2 (6)', + 'T2 (7)', 'BankTwoStale', 'Standing1', '(from 3)' @@ -2110,7 +2107,7 @@ def test_06_transaction_table(self, base_url, selenium, testdb): [ (ppdate + timedelta(days=6)).strftime('%Y-%m-%d'), '$111.13', - 'T1foo (5)', + 'T1foo (6)', 'BankOne', 'Periodic1', '(from 1)' @@ -2134,8 +2131,8 @@ def test_06_transaction_table(self, base_url, selenium, testdb): [ (pp.start_date + timedelta(days=8)).strftime('%Y-%m-%d'), '$12.00', - 'Txn From ST7' - ' (4)', + 'Txn From ST7' + ' (5)', 'BankOne', 'Periodic1', '(from 7)' @@ -2148,7 +2145,7 @@ def test_07_verify_db_ids(self, testdb): max_t = max([ t.id for t in testdb.query(Transaction).all() ]) - assert max_t == 7 + assert max_t == 8 max_r = max([ t.id for t in testdb.query(TxnReconcile).all() ]) @@ -2161,8 +2158,7 @@ def test_08_budget_transfer(self, base_url, selenium, testdb): PAY_PERIOD_START_DATE.strftime('%Y-%m-%d') ) link = selenium.find_element_by_id('btn-budg-txfr-periodic') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Budget Transfer' assert body.find_element_by_id( @@ -2233,7 +2229,7 @@ def test_08_budget_transfer(self, base_url, selenium, testdb): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully saved Transactions 8 and 9' \ + assert x.text.strip() == 'Successfully saved Transactions 9 and 10' \ ' in database.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() @@ -2242,7 +2238,7 @@ def test_08_budget_transfer(self, base_url, selenium, testdb): def test_10_verify_db(self, testdb): desc = 'Budget Transfer - 123.45 from Periodic2 (2) to Standing2 (5)' - t1 = testdb.query(Transaction).get(8) + t1 = testdb.query(Transaction).get(9) assert t1.date == dtnow().date() assert t1.actual_amount == Decimal('123.45') assert t1.budgeted_amount == Decimal('123.45') @@ -2255,11 +2251,11 @@ def test_10_verify_db(self, testdb): assert t1.budget_transactions[0].budget_id == 2 assert t1.budget_transactions[0].amount == Decimal('123.45') rec1 = testdb.query(TxnReconcile).get(3) - assert rec1.txn_id == 8 + assert rec1.txn_id == 9 assert rec1.ofx_fitid is None assert rec1.ofx_account_id is None assert rec1.note == desc - t2 = testdb.query(Transaction).get(9) + t2 = testdb.query(Transaction).get(10) assert t2.date == dtnow().date() assert t2.actual_amount == Decimal('-123.45') assert t2.budgeted_amount == Decimal('-123.45') @@ -2272,7 +2268,7 @@ def test_10_verify_db(self, testdb): assert t2.budget_transactions[0].budget_id == 5 assert t2.budget_transactions[0].amount == Decimal('-123.45') rec2 = testdb.query(TxnReconcile).get(4) - assert rec2.txn_id == 9 + assert rec2.txn_id == 10 assert rec2.ofx_fitid is None assert rec2.ofx_account_id is None assert rec2.note == desc @@ -2382,7 +2378,7 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ ppdate.strftime('%Y-%m-%d'), '$222.22', - 'T3 (7)', + 'T3 (8)', 'CreditOne', 'Periodic2', ' ', @@ -2391,7 +2387,7 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ (ppdate + timedelta(days=2)).strftime('%Y-%m-%d'), '-$333.33', - 'T2 (6)', + 'T2 (7)', 'BankTwoStale', 'Standing1', '(from 3)' @@ -2415,7 +2411,7 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ (ppdate + timedelta(days=6)).strftime('%Y-%m-%d'), '$111.13', - 'T1foo (5)', + 'T1foo (6)', 'BankOne', 'Periodic1', '(from 1)' @@ -2439,8 +2435,8 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ (pp.start_date + timedelta(days=8)).strftime('%Y-%m-%d'), '$12.00', - 'Txn From ST7' - ' (4)', + 'Txn From ST7' + ' (5)', 'BankOne', 'Periodic1', '(from 7)' @@ -2450,9 +2446,9 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ dtnow().date().strftime('%Y-%m-%d'), '$123.45', - '' + '' 'Budget Transfer - 123.45 from Periodic2 (2) to ' - 'Standing2 (5) (8)', + 'Standing2 (5) (9)', 'BankOne', 'Periodic2', ' ', @@ -2461,9 +2457,9 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ dtnow().date().strftime('%Y-%m-%d'), '-$123.45', - '' + '' 'Budget Transfer - 123.45 from Periodic2 (2) to ' - 'Standing2 (5) (9)', + 'Standing2 (5) (10)', 'BankOne', 'Standing2', ' ', @@ -2480,8 +2476,7 @@ def test_17_budget_transfer_modal_date_curr_period( PAY_PERIOD_START_DATE.strftime('%Y-%m-%d') ) link = selenium.find_element_by_id('btn-budg-txfr-periodic') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Budget Transfer' assert body.find_element_by_id( @@ -2497,8 +2492,7 @@ def test_18_budget_transfer_modal_date_prev_period( date(2017, 6, 23).strftime('%Y-%m-%d') ) link = selenium.find_element_by_id('btn-budg-txfr-periodic') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Budget Transfer' assert body.find_element_by_id( @@ -2513,8 +2507,7 @@ def test_19_budget_transfer_modal_date_next_period( date(2017, 8, 18).strftime('%Y-%m-%d') ) link = selenium.find_element_by_id('btn-budg-txfr-periodic') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Budget Transfer' assert body.find_element_by_id( @@ -2538,7 +2531,7 @@ def test_00_inactivate_scheduled(self, testdb): # delete existing transactions for tr in testdb.query(TxnReconcile).all(): testdb.delete(tr) - for idx in [1, 2, 3]: + for idx in [1, 2, 3, 4]: t = testdb.query(Transaction).get(idx) testdb.delete(t) testdb.flush() @@ -2729,7 +2722,7 @@ def test_06_transaction_table(self, base_url, selenium, testdb): [ ppdate.strftime('%Y-%m-%d'), '$222.22', - 'T3 (7)', + 'T3 (8)', 'CreditOne', 'Periodic2', ' ', @@ -2738,7 +2731,7 @@ def test_06_transaction_table(self, base_url, selenium, testdb): [ (ppdate + timedelta(days=2)).strftime('%Y-%m-%d'), '-$333.33', - 'T2 (6)', + 'T2 (7)', 'BankTwoStale', 'Standing1', '(from 3)' @@ -2762,7 +2755,7 @@ def test_06_transaction_table(self, base_url, selenium, testdb): [ (ppdate + timedelta(days=6)).strftime('%Y-%m-%d'), '$111.13', - 'T1foo (5)', + 'T1foo (6)', 'BankOne', 'Periodic1', '(from 1)' @@ -2786,8 +2779,8 @@ def test_06_transaction_table(self, base_url, selenium, testdb): [ (pp.start_date + timedelta(days=8)).strftime('%Y-%m-%d'), '$12.00', - 'Txn From ST7' - ' (4)', + 'Txn From ST7' + ' (5)', 'BankOne', 'Periodic1', '(from 7)' @@ -2800,7 +2793,7 @@ def test_07_verify_db_ids(self, testdb): max_t = max([ t.id for t in testdb.query(Transaction).all() ]) - assert max_t == 7 + assert max_t == 8 max_r = max([ t.id for t in testdb.query(TxnReconcile).all() ]) @@ -2816,8 +2809,7 @@ def test_08_budget_transfer(self, base_url, selenium, testdb): link = selenium.find_elements_by_xpath( '//a[@href="javascript:skipSchedTransModal(8, \'%s\');"]' '' % pp.start_date.strftime('%Y-%m-%d'))[0] - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Skip Scheduled Transaction 8 in period %s' \ % pp.start_date.strftime('%Y-%m-%d') @@ -2870,7 +2862,7 @@ def test_08_budget_transfer(self, base_url, selenium, testdb): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully created Transaction 8 to' \ + assert x.text.strip() == 'Successfully created Transaction 9 to' \ ' skip ScheduledTransaction 8.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() @@ -2881,7 +2873,7 @@ def test_10_verify_db(self, testdb): pp = BiweeklyPayPeriod(PAY_PERIOD_START_DATE, testdb) desc = 'Skip ScheduledTransaction 8 in period %s' % \ pp.start_date.strftime('%Y-%m-%d') - t1 = testdb.query(Transaction).get(8) + t1 = testdb.query(Transaction).get(9) assert t1.date == pp.start_date assert t1.actual_amount == Decimal('0.0') assert t1.budgeted_amount == Decimal('0.0') @@ -2894,7 +2886,7 @@ def test_10_verify_db(self, testdb): assert t1.budget_transactions[0].budget_id == 1 assert t1.budget_transactions[0].amount == Decimal('0.0') rec1 = testdb.query(TxnReconcile).get(3) - assert rec1.txn_id == 8 + assert rec1.txn_id == 9 assert rec1.ofx_fitid is None assert rec1.ofx_account_id is None assert rec1.note == desc @@ -3000,7 +2992,7 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ ppdate.strftime('%Y-%m-%d'), '$222.22', - 'T3 (7)', + 'T3 (8)', 'CreditOne', 'Periodic2', ' ', @@ -3009,8 +3001,8 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ ppdate.strftime('%Y-%m-%d'), '$0.00', - 'Skip ' - 'ScheduledTransaction 8 in period %s (8)' % ( + 'Skip ' + 'ScheduledTransaction 8 in period %s (9)' % ( pp.start_date.strftime('%Y-%m-%d') ), 'BankOne', @@ -3022,7 +3014,7 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ (ppdate + timedelta(days=2)).strftime('%Y-%m-%d'), '-$333.33', - 'T2 (6)', + 'T2 (7)', 'BankTwoStale', 'Standing1', '(from 3)' @@ -3032,7 +3024,7 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ (ppdate + timedelta(days=6)).strftime('%Y-%m-%d'), '$111.13', - 'T1foo (5)', + 'T1foo (6)', 'BankOne', 'Periodic1', '(from 1)' @@ -3056,8 +3048,8 @@ def test_16_transaction_table(self, base_url, selenium, testdb): [ (pp.start_date + timedelta(days=8)).strftime('%Y-%m-%d'), '$12.00', - 'Txn From ST7' - ' (4)', + 'Txn From ST7' + ' (5)', 'BankOne', 'Periodic1', '(from 7)' diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_projects.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_projects.py index 8afdb14f..0516a536 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_projects.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_projects.py @@ -513,8 +513,7 @@ def test_03_populate_modal(self, base_url, selenium): editlink = selenium.find_element_by_xpath( '//a[@onclick="bomItemModal(3)"]' ) - editlink.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, editlink) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit BoM Item 3' id = body.find_element_by_id('bom_frm_id') @@ -539,8 +538,7 @@ def test_04_edit_item(self, base_url, selenium): editlink = selenium.find_element_by_xpath( '//a[@onclick="bomItemModal(3)"]' ) - editlink.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, editlink) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit BoM Item 3' id = body.find_element_by_id('bom_frm_id') @@ -664,8 +662,7 @@ def test_05_verify_db(self, testdb): def test_06_add_item(self, base_url, selenium): self.get(selenium, base_url + '/projects/1') editlink = selenium.find_element_by_id('btn_add_item') - editlink.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, editlink) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New BoM Item' id = body.find_element_by_id('bom_frm_id') diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py index 6f5c208e..a692a08f 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_reconcile.py @@ -81,9 +81,19 @@ def txn_div(id, dt, amt, acct_name, acct_id, s += '
' s += '
Budget: ' s += '' - s += '%s (%s)' % ( - budget_id, budget_name, budget_id - ) + if isinstance(budget_name, type([])) and budget_id is None: + b1 = budget_name[0] + b2 = budget_name[1] + s += '%s (%s) (%s)
' % ( + b1[1], b1[0], b1[1], b1[2] + ) + s += '%s (%s) (%s)' % ( + b2[1], b2[0], b2[1], b2[2] + ) + else: + s += '%s (%s)' % ( + budget_id, budget_name, budget_id + ) s += '
' s += '
' s += '
' @@ -326,7 +336,10 @@ def test_03_add_transactions(self, testdb): testdb.add(st1) t3 = Transaction( date=date(2017, 4, 11), - budget_amounts={e1budget: Decimal('600.00')}, + budget_amounts={ + e1budget: Decimal('590.00'), + e2budget: Decimal('10.00') + }, budgeted_amount=Decimal('500.0'), description='trans2', account=acct2, @@ -529,7 +542,11 @@ def test_06_transactions(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2' ), txn_div( @@ -727,7 +744,11 @@ def test_06_transactions(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2' ), txn_div( @@ -922,8 +943,7 @@ def test_07_edit(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/reconcile') link = selenium.find_element_by_xpath('//a[text()="Trans 1"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Transaction 1' assert body.find_element_by_id( @@ -973,7 +993,11 @@ def test_07_edit(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2' ), txn_div( @@ -1028,7 +1052,11 @@ def test_06_success(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2', drop_div=ofx_div( date(2017, 4, 9), @@ -1194,7 +1222,11 @@ def test_11_unreconcile(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2', drop_div=ofx_div( date(2017, 4, 9), @@ -1233,7 +1265,11 @@ def test_11_unreconcile(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2' ) assert self.normalize_html(tgt.get_attribute('outerHTML')) == expected @@ -1513,8 +1549,9 @@ def test_14_verify_reconcile_modal(self, base_url, selenium, testdb): res = testdb.query(TxnReconcile).all() txn_id = res[-1].txn_id self.get(selenium, base_url + '/transactions') - selenium.find_element_by_link_text('Yes (%s)' % txn_id).click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal( + selenium, selenium.find_element_by_link_text('Yes (%s)' % txn_id) + ) self.assert_modal_displayed(modal, title, body) assert title.text == 'Transaction Reconcile %s' % txn_id assert 'Foo Bar Baz' in body.text @@ -1659,9 +1696,8 @@ def test_08_trans_from_ofx(self, base_url, selenium): self.get(selenium, base_url + '/reconcile') ofxdiv = selenium.find_element_by_id('ofx-2-OFX2') link = ofxdiv.find_element_by_xpath('//a[text()="(make trans)"]') - link.click() # test the modal population - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add Transaction for OFX (2, OFX2)' assert body.find_element_by_id( @@ -1851,9 +1887,8 @@ def test_33_ignore_ofx(self, base_url, selenium): self.get(selenium, base_url + '/reconcile') ofxdiv = selenium.find_element_by_id('ofx-2-OFX31') link = ofxdiv.find_element_by_xpath('//a[text()="(ignore)"]') - link.click() # test the modal population - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Ignore OFXTransaction (2, "OFX31")' assert body.find_element_by_id( @@ -1970,9 +2005,8 @@ def test_36_ignore_and_unignore_ofx(self, base_url, selenium): # ignore ofxdiv = selenium.find_element_by_id('ofx-2-OFX30') link = ofxdiv.find_element_by_xpath('//a[text()="(ignore)"]') - link.click() # test the modal population - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Ignore OFXTransaction (2, "OFX30")' assert body.find_element_by_id( @@ -2077,7 +2111,11 @@ def test_06_transactions_column(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2' ), txn_div( @@ -2183,7 +2221,11 @@ def test_08_reconcile_unreconcile_noOFX_visible(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2' ) ofx = selenium.find_element_by_id('ofx-2-OFX3') @@ -2214,7 +2256,11 @@ def test_08_reconcile_unreconcile_noOFX_visible(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2', drop_div=ofx_div( date(2017, 4, 9), @@ -2239,7 +2285,11 @@ def test_08_reconcile_unreconcile_noOFX_visible(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2' ) ofx = selenium.find_element_by_id('ofx-2-OFX3') @@ -2272,7 +2322,11 @@ def test_09_reconcile_unreconcile_noOFX(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2' ) ofx = selenium.find_element_by_id('ofx-2-OFX3') @@ -2286,8 +2340,9 @@ def test_09_reconcile_unreconcile_noOFX(self, base_url, selenium): ) assert self.get_reconciled(selenium) == {} # reconcile as noOFX - trans.find_element_by_link_text('(no OFX)').click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal( + selenium, trans.find_element_by_link_text('(no OFX)') + ) self.assert_modal_displayed(modal, title, body) assert title.text == 'Reconcile Transaction 3 Without OFX' assert body.find_element_by_id( @@ -2313,7 +2368,11 @@ def test_09_reconcile_unreconcile_noOFX(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2', drop_div=noofx_div ) @@ -2327,7 +2386,11 @@ def test_09_reconcile_unreconcile_noOFX(self, base_url, selenium): date(2017, 4, 11), 600, 'BankTwo', 2, - '2Periodic', 2, + [ + ['2Periodic', 2, '$590.00'], + ['3Periodic', 3, '$10.00'] + ], + None, 'trans2' ) assert self.get_reconciled(selenium) == {} diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_scheduled.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_scheduled.py index c6d6fcf9..7dca9795 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_scheduled.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_scheduled.py @@ -269,8 +269,7 @@ def test_1_modal_on_click(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/scheduled') link = selenium.find_element_by_xpath('//a[text()="ST3"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Scheduled Transaction 3' assert body.find_element_by_id( @@ -471,8 +470,7 @@ def test_10_add_date_modal_on_click(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/scheduled') link = selenium.find_element_by_id('btn_add_sched') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Scheduled Transaction' desc = body.find_element_by_id('sched_frm_description') @@ -538,8 +536,7 @@ def test_21_add_monthly_modal_on_click(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/scheduled') link = selenium.find_element_by_id('btn_add_sched') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Scheduled Transaction' desc = body.find_element_by_id('sched_frm_description') @@ -594,8 +591,7 @@ def test_31_add_per_period_modal_on_click(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/scheduled') link = selenium.find_element_by_id('btn_add_sched') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Scheduled Transaction' desc = body.find_element_by_id('sched_frm_description') @@ -650,8 +646,7 @@ def test_41_add_income_modal_on_click(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/scheduled') link = selenium.find_element_by_id('btn_add_sched') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Scheduled Transaction' desc = body.find_element_by_id('sched_frm_description') diff --git a/biweeklybudget/tests/acceptance/flaskapp/views/test_transactions.py b/biweeklybudget/tests/acceptance/flaskapp/views/test_transactions.py index f6acbe6c..22c6d4de 100644 --- a/biweeklybudget/tests/acceptance/flaskapp/views/test_transactions.py +++ b/biweeklybudget/tests/acceptance/flaskapp/views/test_transactions.py @@ -38,8 +38,10 @@ import pytest from datetime import timedelta, date, datetime from pytz import UTC -from selenium.webdriver.support.ui import Select +from selenium.webdriver.support.ui import Select, WebDriverWait +from selenium.common.exceptions import TimeoutException from decimal import Decimal +import requests from biweeklybudget.utils import dtnow from biweeklybudget.tests.acceptance_helpers import AcceptanceHelper @@ -117,6 +119,16 @@ def test_table(self, selenium): '', '', '' + ], + [ + (self.dt - timedelta(days=35)).date().strftime('%Y-%m-%d'), + '$322.32', + 'T4split', + 'CreditOne (3)', + 'Periodic2 (2) ($222.22)\nPeriodic1 (1) ($100.10)', + '', + '', + '' ] ] linkcols = [ @@ -129,6 +141,7 @@ def test_table(self, selenium): ] for c in elems ] + assert len(linkcols) == 4 assert linkcols[0] == [ 'T1foo', 'BankOne (1)', @@ -150,6 +163,14 @@ def test_table(self, selenium): ' ', ' ' ] + assert linkcols[3] == [ + 'T4split', + 'CreditOne (3)', + 'Periodic2 (2) ($222.22)
' + 'Periodic1 (1) ($100.10)', + ' ', + ' ' + ] def test_acct_filter_opts(self, selenium): self.get(selenium, self.baseurl + '/transactions') @@ -172,7 +193,8 @@ def test_acct_filter(self, selenium): p1trans = [ 'T1foo', 'T2', - 'T3' + 'T3', + 'T4split' ] self.get(selenium, self.baseurl + '/transactions') table = self.retry_stale( @@ -225,7 +247,8 @@ def test_budg_filter(self, selenium): p1trans = [ 'T1foo', 'T2', - 'T3' + 'T3', + 'T4split' ] self.get(selenium, self.baseurl + '/transactions') table = self.retry_stale( @@ -245,7 +268,7 @@ def test_budg_filter(self, selenium): ) texts = self.retry_stale(self.tbody2textlist, table) trans = [t[2] for t in texts] - assert trans == ['T3'] + assert trans == ['T3', 'T4split'] # select Standing1 (4) budg_filter.select_by_value('4') table = self.retry_stale( @@ -376,8 +399,7 @@ def test_01_simple_modal_modal_on_click(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/transactions') link = selenium.find_element_by_xpath('//a[text()="T2"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Transaction 2' assert body.find_element_by_id( @@ -425,8 +447,7 @@ def test_02_simple_modal_modal_edit(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/transactions') link = selenium.find_element_by_xpath('//a[text()="T2"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Transaction 2' assert body.find_element_by_id( @@ -500,8 +521,7 @@ def test_11_cant_edit_reconciled_modal_on_click(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/transactions') link = selenium.find_element_by_xpath('//a[text()="T1foo"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Transaction 1' assert body.find_element_by_id( @@ -549,8 +569,7 @@ def test_12_cant_edit_reconciled_modal_edit(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/transactions') link = selenium.find_element_by_xpath('//a[text()="T1foo"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Edit Transaction 1' assert body.find_element_by_id( @@ -572,8 +591,7 @@ def test_22_modal_add(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/transactions') link = selenium.find_element_by_id('btn_add_trans') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Transaction' date_input = body.find_element_by_id('trans_frm_date') @@ -592,7 +610,7 @@ def test_22_modal_add(self, base_url, selenium): amt.clear() amt.send_keys('123.45') desc = body.find_element_by_id('trans_frm_description') - desc.send_keys('NewTrans4') + desc.send_keys('NewTrans5') acct_sel = Select(body.find_element_by_id('trans_frm_account')) assert acct_sel.first_selected_option.get_attribute('value') == '1' acct_sel.select_by_value('1') @@ -607,7 +625,7 @@ def test_22_modal_add(self, base_url, selenium): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully saved Transaction 4 ' \ + assert x.text.strip() == 'Successfully saved Transaction 5 ' \ 'in database.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() @@ -615,12 +633,12 @@ def test_22_modal_add(self, base_url, selenium): # test that new trans was added to the table table = selenium.find_element_by_id('table-transactions') texts = [y[2] for y in self.tbody2textlist(table)] - assert 'NewTrans4' in texts + assert 'NewTrans5' in texts def test_23_modal_add_verify_db(self, testdb): - t = testdb.query(Transaction).get(4) + t = testdb.query(Transaction).get(5) assert t is not None - assert t.description == 'NewTrans4' + assert t.description == 'NewTrans5' dnow = dtnow() assert t.date == date(year=dnow.year, month=dnow.month, day=15) assert t.actual_amount == Decimal('123.45') @@ -648,8 +666,7 @@ def test_32_modal_add(self, base_url, selenium): self.baseurl = base_url self.get(selenium, base_url + '/transactions') link = selenium.find_element_by_id('btn_add_trans') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Add New Transaction' date_input = body.find_element_by_id('trans_frm_date') @@ -660,7 +677,7 @@ def test_32_modal_add(self, base_url, selenium): amt.clear() amt.send_keys('345.67') desc = body.find_element_by_id('trans_frm_description') - desc.send_keys('NewTrans5') + desc.send_keys('NewTrans6') acct_sel = Select(body.find_element_by_id('trans_frm_account')) assert acct_sel.first_selected_option.get_attribute('value') == '1' acct_sel.select_by_value('1') @@ -675,7 +692,7 @@ def test_32_modal_add(self, base_url, selenium): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully saved Transaction 5 ' \ + assert x.text.strip() == 'Successfully saved Transaction 6 ' \ 'in database.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() @@ -683,12 +700,12 @@ def test_32_modal_add(self, base_url, selenium): # test that new trans was added to the table table = selenium.find_element_by_id('table-transactions') texts = [y[2] for y in self.tbody2textlist(table)] - assert 'NewTrans5' in texts + assert 'NewTrans6' in texts def test_33_verify_db(self, testdb): - t = testdb.query(Transaction).get(5) + t = testdb.query(Transaction).get(6) assert t is not None - assert t.description == 'NewTrans5' + assert t.description == 'NewTrans6' assert t.date == dtnow().date() assert t.actual_amount == Decimal('345.67') assert t.budgeted_amount is None @@ -721,13 +738,12 @@ def test_41_modal_edit_change_between_standing(self, base_url, selenium): """ self.baseurl = base_url self.get(selenium, base_url + '/transactions') - link = selenium.find_element_by_xpath('//a[text()="NewTrans5"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + link = selenium.find_element_by_xpath('//a[text()="NewTrans6"]') + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) - assert title.text == 'Edit Transaction 5' + assert title.text == 'Edit Transaction 6' assert body.find_element_by_id( - 'trans_frm_id').get_attribute('value') == '5' + 'trans_frm_id').get_attribute('value') == '6' amt = body.find_element_by_id('trans_frm_amount') assert amt.get_attribute('value') == '345.67' budget_sel = Select(body.find_element_by_id('trans_frm_budget')) @@ -740,15 +756,15 @@ def test_41_modal_edit_change_between_standing(self, base_url, selenium): _, _, body = self.get_modal_parts(selenium) x = body.find_elements_by_tag_name('div')[0] assert 'alert-success' in x.get_attribute('class') - assert x.text.strip() == 'Successfully saved Transaction 5 ' \ + assert x.text.strip() == 'Successfully saved Transaction 6 ' \ 'in database.' # dismiss the modal selenium.find_element_by_id('modalCloseButton').click() def test_42_simple_modal_verify_db(self, testdb): - t = testdb.query(Transaction).get(5) + t = testdb.query(Transaction).get(6) assert t is not None - assert t.description == 'NewTrans5' + assert t.description == 'NewTrans6' assert t.date == dtnow().date() assert t.actual_amount == Decimal('345.67') assert t.budgeted_amount is None @@ -782,8 +798,7 @@ def test_1_modal(self, base_url, selenium): self.get(selenium, base_url + '/transactions') link = selenium.find_element_by_xpath( '//a[@href="javascript:txnReconcileModal(1)"]') - link.click() - modal, title, body = self.get_modal_parts(selenium) + modal, title, body = self.try_click_and_get_modal(selenium, link) self.assert_modal_displayed(modal, title, body) assert title.text == 'Transaction Reconcile 1' dl = body.find_element_by_tag_name('dl') @@ -843,3 +858,963 @@ def test_1_modal(self, base_url, selenium): ofx_elems = self.tbody2elemlist(ofx_tbl) assert ofx_elems[1][1].get_attribute('innerHTML') == 'BankOne (1)' + + def test_2_split_trans(self, testdb): + b1 = testdb.query(Budget).get(1) # Periodic1 + b2 = testdb.query(Budget).get(2) # Periodic2 + t = testdb.query(Transaction).get(1) + t.set_budget_amounts({ + b1: Decimal('110.02'), + b2: Decimal('1.11') + }) + testdb.commit() + + def test_3_split_trans_modal(self, base_url, selenium): + self.baseurl = base_url + self.get(selenium, base_url + '/transactions') + link = selenium.find_element_by_xpath( + '//a[@href="javascript:txnReconcileModal(1)"]') + modal, title, body = self.try_click_and_get_modal(selenium, link) + self.assert_modal_displayed(modal, title, body) + assert title.text == 'Transaction Reconcile 1' + dl = body.find_element_by_tag_name('dl') + assert dl.get_attribute('innerHTML') == '\n' \ + '
Date Reconciled
2017-04-10 08:09:11 UTC
\n' \ + '
Note
reconcile notes
\n' \ + '
Rule
null
\n' + trans_tbl = body.find_element_by_id('txnReconcileModal-trans') + trans_texts = self.tbody2textlist(trans_tbl) + assert trans_texts == [ + ['Transaction'], + [ + 'Date', + (dtnow() + timedelta(days=4)).strftime('%Y-%m-%d') + ], + ['Amount', '$111.13'], + ['Budgeted Amount', '$111.11'], + ['Description', 'T1foo'], + ['Account', 'BankOne (1)'], + ['Budget', 'Periodic1 (1) ($110.02)\nPeriodic2 (2) ($1.11)'], + ['Notes', 'notesT1'], + ['Scheduled?', 'Yes (1)'] + ] + trans_elems = self.tbody2elemlist(trans_tbl) + assert trans_elems[5][1].get_attribute('innerHTML') == 'BankOne (1)' + assert trans_elems[6][1].get_attribute('innerHTML') == 'Periodic1 (1) ($110.02)
Periodic2 (2) ($1.11)' + assert trans_elems[8][1].get_attribute('innerHTML') == 'Yes (1)' + ofx_tbl = body.find_element_by_id('txnReconcileModal-ofx') + ofx_texts = self.tbody2textlist(ofx_tbl) + assert ofx_texts == [ + ['OFX Transaction'], + ['Account', 'BankOne (1)'], + ['FITID', 'BankOne.0.1'], + ['Date Posted', (dtnow() - timedelta(days=6)).strftime('%Y-%m-%d')], + ['Amount', '-$20.00'], + ['Name', 'Late Fee'], + ['Memo', ''], + ['Type', 'Debit'], + ['Description', ''], + ['Notes', ''], + ['Checknum', ''], + ['MCC', ''], + ['SIC', ''], + ['OFX Statement'], + ['ID', '1'], + ['Date', (dtnow() - timedelta(hours=46)).strftime('%Y-%m-%d')], + ['Filename', '/stmt/BankOne/0'], + [ + 'File mtime', + (dtnow() - timedelta(hours=46)).strftime('%Y-%m-%d') + ], + ['Ledger Balance', '$12,345.67'] + ] + ofx_elems = self.tbody2elemlist(ofx_tbl) + assert ofx_elems[1][1].get_attribute('innerHTML') == 'BankOne (1)' + + +@pytest.mark.acceptance +@pytest.mark.usefixtures('class_refresh_db', 'refreshdb', 'testflask') +@pytest.mark.incremental +class TestTransModalBudgetSplits(AcceptanceHelper): + + def test_01_verify_db(self, testdb): + t = testdb.query(Transaction).get(4) + assert t is not None + assert t.description == 'T4split' + assert t.date == (dtnow() - timedelta(days=35)).date() + assert t.actual_amount == Decimal('322.32') + assert t.budgeted_amount is None + assert t.planned_budget_id is None + assert t.account_id == 3 + assert t.scheduled_trans_id is None + assert t.notes == 'notesT4split' + assert {bt.budget_id: bt.amount for bt in t.budget_transactions} == { + 1: Decimal('100.10'), + 2: Decimal('222.22') + } + + def test_02_resave_transaction(self, base_url): + res = requests.post( + base_url + '/forms/transaction', + json={ + 'id': '4', + 'date': (dtnow() - timedelta(days=35)).strftime('%Y-%m-%d'), + 'amount': '322.32', + 'description': 'T4split', + 'notes': 'notesT4split', + 'account': '3', + 'budgets': { + '2': '222.22', + '1': '100.10' + } + } + ) + assert res.status_code == 200 + assert res.json() == { + 'success': True, + 'success_message': 'Successfully saved Transaction 4 in database.', + 'trans_id': 4 + } + + def test_03_verify_db(self, testdb): + t = testdb.query(Transaction).get(4) + assert t is not None + assert t.description == 'T4split' + assert t.date == (dtnow() - timedelta(days=35)).date() + assert t.actual_amount == Decimal('322.32') + assert t.budgeted_amount is None + assert t.planned_budget_id is None + assert t.account_id == 3 + assert t.scheduled_trans_id is None + assert t.notes == 'notesT4split' + assert {bt.budget_id: bt.amount for bt in t.budget_transactions} == { + 1: Decimal('100.10'), + 2: Decimal('222.22') + } + + def test_10_backend_validation_amounts(self, base_url): + res = requests.post( + base_url + '/forms/transaction', + json={ + 'id': '4', + 'date': (dtnow() - timedelta(days=35)).strftime('%Y-%m-%d'), + 'amount': '322.32', + 'description': 'T4split', + 'notes': 'notesT4split', + 'account': '3', + 'budgets': { + '2': '422.32' + } + } + ) + assert res.status_code == 200 + assert res.json() == { + 'success': False, + 'errors': { + 'account': [], + 'amount': [], + 'budgets': [ + 'Sum of all budget amounts (422.32) must equal ' + 'Transaction amount (322.32).' + ], + 'date': [], + 'description': [], + 'id': [], + 'notes': [] + } + } + + def test_11_backend_validation_amounts(self, base_url): + res = requests.post( + base_url + '/forms/transaction', + json={ + 'id': '4', + 'date': (dtnow() - timedelta(days=35)).strftime('%Y-%m-%d'), + 'amount': '322.32', + 'description': 'T4split', + 'notes': 'notesT4split', + 'account': '3', + 'budgets': { + '1': '222.32', + '2': '200.12' + } + } + ) + assert res.status_code == 200 + assert res.json() == { + 'success': False, + 'errors': { + 'account': [], + 'amount': [], + 'budgets': [ + 'Sum of all budget amounts (422.44) must equal ' + 'Transaction amount (322.32).' + ], + 'date': [], + 'description': [], + 'id': [], + 'notes': [] + } + } + + def test_12_backend_validation_no_budgets(self, base_url): + res = requests.post( + base_url + '/forms/transaction', + json={ + 'id': '4', + 'date': (dtnow() - timedelta(days=35)).strftime('%Y-%m-%d'), + 'amount': '322.32', + 'description': 'T4split', + 'notes': 'notesT4split', + 'account': '3', + 'budgets': {} + } + ) + assert res.status_code == 200 + assert res.json() == { + 'success': False, + 'errors': { + 'account': [], + 'amount': [], + 'budgets': [ + 'Transactions must have a budget.' + ], + 'date': [], + 'description': [], + 'id': [], + 'notes': [] + } + } + + def test_13_backend_validation_invalid_budget_id(self, base_url): + res = requests.post( + base_url + '/forms/transaction', + json={ + 'date': (dtnow() - timedelta(days=35)).strftime('%Y-%m-%d'), + 'amount': '322.32', + 'description': 'T4split', + 'notes': 'notesT4split', + 'account': '3', + 'budgets': {'99': '322.32'} + } + ) + assert res.status_code == 200 + assert res.json() == { + 'success': False, + 'errors': { + 'account': [], + 'amount': [], + 'budgets': [ + 'Budget ID 99 is invalid.' + ], + 'date': [], + 'description': [], + 'notes': [] + } + } + + def test_14_backend_validation_inactive_budget(self, base_url): + res = requests.post( + base_url + '/forms/transaction', + json={ + 'date': (dtnow() - timedelta(days=35)).strftime('%Y-%m-%d'), + 'amount': '322.32', + 'description': 'T4split', + 'notes': 'notesT4split', + 'account': '3', + 'budgets': {'3': '322.32'} + } + ) + assert res.status_code == 200 + assert res.json() == { + 'success': False, + 'errors': { + 'account': [], + 'amount': [], + 'budgets': [ + 'New transactions cannot use an inactive budget ' + '(Periodic3 Inactive).' + ], + 'date': [], + 'description': [], + 'notes': [] + } + } + + def validation_count_increased(self, driver, previous): + c = driver.execute_script('return validation_count;') + return c > previous + + def assert_budget_split_has_error(self, driver, msg): + # get validate count + c = driver.execute_script('return validation_count;') + # change focus + driver.find_element_by_id('trans_frm_description').click() + # wait for validate count to increase + try: + WebDriverWait(driver, 5).until( + lambda x: self.validation_count_increased(driver, c) + ) + except TimeoutException: + pass + assert driver.find_element_by_id('budget-split-feedback').text == msg + assert driver.find_element_by_id( + 'modalSaveButton').is_enabled() is False + + def assert_budget_split_does_not_have_error(self, driver): + # get validate count + c = driver.execute_script('return validation_count;') + # change focus + driver.find_element_by_id('trans_frm_description').click() + # wait for validate count to increase + try: + WebDriverWait(driver, 5).until( + lambda x: self.validation_count_increased(driver, c) + ) + except TimeoutException: + pass + assert driver.find_element_by_id('budget-split-feedback').text == '' + assert driver.find_element_by_id('modalSaveButton').is_enabled() + + def test_20_modal_frontend_validation(self, base_url, selenium): + self.baseurl = base_url + self.get(selenium, base_url + '/transactions') + link = selenium.find_element_by_id('btn_add_trans') + modal, title, body = self.try_click_and_get_modal(selenium, link) + self.assert_modal_displayed(modal, title, body) + assert title.text == 'Add New Transaction' + # set an amount + amt = body.find_element_by_id('trans_frm_amount') + amt.clear() + amt.send_keys('200.22') + # assert budget split items are hidden and checkbox is unchecked + assert selenium.find_element_by_id( + 'trans_frm_is_split').is_selected() is False + assert selenium.find_element_by_id( + 'trans_frm_budget_group').is_displayed() + assert selenium.find_element_by_id( + 'trans_frm_split_budget_container').is_displayed() is False + # check the budget split checkbox + selenium.find_element_by_id('trans_frm_is_split').click() + # assert budget split items are shown and checkbox is checked + assert selenium.find_element_by_id( + 'trans_frm_is_split').is_selected() is True + assert selenium.find_element_by_id( + 'trans_frm_budget_group').is_displayed() is False + assert selenium.find_element_by_id( + 'trans_frm_split_budget_container').is_displayed() + # there should be two split budget input groups + assert len( + selenium.find_elements_by_class_name('budget_split_row') + ) == 2 + self.assert_budget_split_does_not_have_error(selenium) + # Select 2 different budgets and valid amounts + Select( + body.find_element_by_id('trans_frm_budget_0')).select_by_value('1') + tmp = body.find_element_by_id('trans_frm_budget_amount_0') + tmp.clear() + tmp.send_keys('100') + Select( + body.find_element_by_id('trans_frm_budget_1')).select_by_value('2') + tmp = body.find_element_by_id('trans_frm_budget_amount_1') + tmp.clear() + tmp.send_keys('100.22') + self.assert_budget_split_does_not_have_error(selenium) + # change one amount + tmp = body.find_element_by_id('trans_frm_budget_amount_1') + tmp.clear() + tmp.send_keys('100.00') + self.assert_budget_split_has_error( + selenium, + 'Error: Sum of budget allocations (200.0000) must equal ' + 'transaction amount (200.2200).' + ) + # fix the amount + tmp = body.find_element_by_id('trans_frm_budget_amount_1') + tmp.clear() + tmp.send_keys('100.22') + self.assert_budget_split_does_not_have_error(selenium) + # change one budget to the same as the other + Select( + body.find_element_by_id('trans_frm_budget_1')).select_by_value('1') + self.assert_budget_split_has_error( + selenium, + 'Error: A given budget may only be specified once.' + ) + # fix the budget + Select( + body.find_element_by_id('trans_frm_budget_1')).select_by_value('2') + self.assert_budget_split_does_not_have_error(selenium) + # click "Add Budget" link + self.try_click( + selenium, selenium.find_element_by_id('trans_frm_add_budget_link') + ) + # there should be three split budget input groups + assert len( + selenium.find_elements_by_class_name('budget_split_row') + ) == 3 + # decrease an amount in one of the previous groups + tmp = body.find_element_by_id('trans_frm_budget_amount_1') + tmp.clear() + tmp.send_keys('50.11') + self.assert_budget_split_has_error( + selenium, + 'Error: Sum of budget allocations (150.1100) must equal ' + 'transaction amount (200.2200).' + ) + # add difference to amount in the third budget group + tmp = body.find_element_by_id('trans_frm_budget_amount_2') + tmp.clear() + tmp.send_keys('50.11') + self.assert_budget_split_does_not_have_error(selenium) + # select budget in third group, same as second + Select( + body.find_element_by_id('trans_frm_budget_2')).select_by_value('2') + self.assert_budget_split_has_error( + selenium, + 'Error: A given budget may only be specified once.' + ) + # change budget in third group to a unique one + Select( + body.find_element_by_id('trans_frm_budget_2')).select_by_value('4') + self.assert_budget_split_does_not_have_error(selenium) + # uncheck the Budget Split checkbox + selenium.find_element_by_id('trans_frm_is_split').click() + # assert budget split items are hidden and checkbox is unchecked + assert selenium.find_element_by_id( + 'trans_frm_is_split').is_selected() is False + assert selenium.find_element_by_id( + 'trans_frm_budget_group').is_displayed() + assert selenium.find_element_by_id( + 'trans_frm_split_budget_container').is_displayed() is False + + def test_30_verify_db_before(self, testdb): + t = testdb.query(Transaction).get(4) + assert t is not None + assert t.description == 'T4split' + assert t.date == (dtnow() - timedelta(days=35)).date() + assert t.actual_amount == Decimal('322.32') + assert t.budgeted_amount is None + assert t.planned_budget_id is None + assert t.account_id == 3 + assert t.scheduled_trans_id is None + assert t.notes == 'notesT4split' + assert {bt.budget_id: bt.amount for bt in t.budget_transactions} == { + 1: Decimal('100.10'), + 2: Decimal('222.22') + } + assert max([ + tx.id for tx in testdb.query(Transaction).all() + ]) == 4 + + def test_31_split_2_modal_populate(self, base_url, selenium): + self.baseurl = base_url + self.get(selenium, base_url + '/transactions') + link = selenium.find_element_by_xpath('//a[text()="T4split"]') + modal, title, body = self.try_click_and_get_modal(selenium, link) + self.assert_modal_displayed(modal, title, body) + assert title.text == 'Edit Transaction 4' + assert body.find_element_by_id( + 'trans_frm_id').get_attribute('value') == '4' + assert body.find_element_by_id( + 'trans_frm_date').get_attribute('value') == ( + dtnow() - timedelta(days=35) + ).strftime('%Y-%m-%d') + assert body.find_element_by_id( + 'trans_frm_amount').get_attribute('value') == '322.32' + assert body.find_element_by_id( + 'trans_frm_description').get_attribute('value') == 'T4split' + acct_sel = Select(body.find_element_by_id('trans_frm_account')) + opts = [] + for o in acct_sel.options: + opts.append([o.get_attribute('value'), o.text]) + assert opts == [ + ['None', ''], + ['1', 'BankOne'], + ['2', 'BankTwoStale'], + ['3', 'CreditOne'], + ['4', 'CreditTwo'], + ['6', 'DisabledBank'], + ['5', 'InvestmentOne'] + ] + assert acct_sel.first_selected_option.get_attribute('value') == '3' + # Split Budget + assert selenium.find_element_by_id( + 'trans_frm_is_split').is_selected() is True + assert selenium.find_element_by_id( + 'trans_frm_budget_group').is_displayed() is False + assert selenium.find_element_by_id( + 'trans_frm_split_budget_container').is_displayed() + assert len( + selenium.find_elements_by_class_name('budget_split_row') + ) == 2 + # BUDGET 0 + budget_sel = Select(body.find_element_by_id('trans_frm_budget_0')) + opts = [] + for o in budget_sel.options: + opts.append([o.get_attribute('value'), o.text]) + assert opts == [ + ['None', ''], + ['7', 'Income (income)'], + ['1', 'Periodic1'], + ['2', 'Periodic2'], + ['3', 'Periodic3 Inactive'], + ['4', 'Standing1'], + ['5', 'Standing2'], + ['6', 'Standing3 Inactive'] + ] + assert budget_sel.first_selected_option.get_attribute('value') == '2' + assert body.find_element_by_id( + 'trans_frm_budget_amount_0').get_attribute('value') == '222.22' + # BUDGET 1 + budget_sel = Select(body.find_element_by_id('trans_frm_budget_1')) + opts = [] + for o in budget_sel.options: + opts.append([o.get_attribute('value'), o.text]) + assert opts == [ + ['None', ''], + ['7', 'Income (income)'], + ['1', 'Periodic1'], + ['2', 'Periodic2'], + ['3', 'Periodic3 Inactive'], + ['4', 'Standing1'], + ['5', 'Standing2'], + ['6', 'Standing3 Inactive'] + ] + assert budget_sel.first_selected_option.get_attribute('value') == '1' + assert body.find_element_by_id( + 'trans_frm_budget_amount_1').get_attribute('value') == '100.1' + assert selenium.find_element_by_id( + 'trans_frm_notes').get_attribute('value') == 'notesT4split' + + def test_32_new_split_trans(self, base_url, selenium, testdb): + self.baseurl = base_url + self.get(selenium, base_url + '/transactions') + link = selenium.find_element_by_id('btn_add_trans') + modal, title, body = self.try_click_and_get_modal(selenium, link) + self.assert_modal_displayed(modal, title, body) + assert title.text == 'Add New Transaction' + date_input = body.find_element_by_id('trans_frm_date') + assert date_input.get_attribute( + 'value') == dtnow().strftime('%Y-%m-%d') + # END date chooser popup + amt = body.find_element_by_id('trans_frm_amount') + amt.clear() + amt.send_keys('375.00') + desc = body.find_element_by_id('trans_frm_description') + desc.send_keys('NewTrans5') + acct_sel = Select(body.find_element_by_id('trans_frm_account')) + assert acct_sel.first_selected_option.get_attribute('value') == '1' + acct_sel.select_by_value('1') + # check the budget split checkbox + selenium.find_element_by_id('trans_frm_is_split').click() + # assert budget split items are shown and checkbox is checked + assert selenium.find_element_by_id( + 'trans_frm_is_split').is_selected() is True + assert selenium.find_element_by_id( + 'trans_frm_budget_group').is_displayed() is False + assert selenium.find_element_by_id( + 'trans_frm_split_budget_container').is_displayed() + # there should be two split budget input groups + assert len( + selenium.find_elements_by_class_name('budget_split_row') + ) == 2 + # set the budgets and amounts + Select( + body.find_element_by_id('trans_frm_budget_0')).select_by_value('1') + tmp = body.find_element_by_id('trans_frm_budget_amount_0') + tmp.clear() + tmp.send_keys('100.00') + Select( + body.find_element_by_id('trans_frm_budget_1')).select_by_value('2') + # the next value should be populated automatically + assert body.find_element_by_id( + 'trans_frm_budget_amount_1').get_attribute('value') == '275.00' + tmp = body.find_element_by_id('trans_frm_budget_amount_1') + tmp.clear() + tmp.send_keys('200') + # change focus + body.find_element_by_id('trans_frm_budget_amount_0').send_keys('') + # add a row + self.try_click( + selenium, selenium.find_element_by_id('trans_frm_add_budget_link') + ) + # there should be three split budget input groups + assert len( + selenium.find_elements_by_class_name('budget_split_row') + ) == 3 + # the amount should be populated automatically + assert body.find_element_by_id( + 'trans_frm_budget_amount_2').get_attribute('value') == '75.00' + # fill in the third row + Select( + body.find_element_by_id('trans_frm_budget_2')).select_by_value('4') + self.assert_budget_split_does_not_have_error(selenium) + notes = selenium.find_element_by_id('trans_frm_notes') + notes.send_keys('NewSplitTransNotes') + # submit the form + selenium.find_element_by_id('modalSaveButton').click() + self.wait_for_jquery_done(selenium) + # check that we got positive confirmation + _, _, body = self.get_modal_parts(selenium) + x = body.find_elements_by_tag_name('div')[0] + assert 'alert-success' in x.get_attribute('class') + assert x.text.strip() == 'Successfully saved Transaction 5 ' \ + 'in database.' + # dismiss the modal + selenium.find_element_by_id('modalCloseButton').click() + self.wait_for_jquery_done(selenium) + # test that new trans was added to the table + table = selenium.find_element_by_id('table-transactions') + texts = [y[2] for y in self.tbody2textlist(table)] + assert 'NewTrans5' in texts + t = testdb.query(Transaction).get(5) + assert t is not None + assert t.description == 'NewTrans5' + assert t.date == dtnow().date() + assert t.actual_amount == Decimal('375') + assert t.budgeted_amount is None + assert t.planned_budget_id is None + assert t.account_id == 1 + assert t.scheduled_trans_id is None + assert t.notes == 'NewSplitTransNotes' + assert {bt.budget_id: bt.amount for bt in t.budget_transactions} == { + 1: Decimal('100'), + 2: Decimal('200'), + 4: Decimal('75') + } + assert max([ + tx.id for tx in testdb.query(Transaction).all() + ]) == 5 + + def test_33_change_split_trans(self, base_url, selenium, testdb): + self.baseurl = base_url + self.get(selenium, base_url + '/transactions') + link = selenium.find_element_by_xpath('//a[text()="NewTrans5"]') + modal, title, body = self.try_click_and_get_modal(selenium, link) + self.assert_modal_displayed(modal, title, body) + assert title.text == 'Edit Transaction 5' + assert body.find_element_by_id( + 'trans_frm_id').get_attribute('value') == '5' + assert body.find_element_by_id( + 'trans_frm_date' + ).get_attribute('value') == dtnow().strftime('%Y-%m-%d') + assert body.find_element_by_id( + 'trans_frm_amount').get_attribute('value') == '375' + assert body.find_element_by_id( + 'trans_frm_description').get_attribute('value') == 'NewTrans5' + acct_sel = Select(body.find_element_by_id('trans_frm_account')) + opts = [] + for o in acct_sel.options: + opts.append([o.get_attribute('value'), o.text]) + assert opts == [ + ['None', ''], + ['1', 'BankOne'], + ['2', 'BankTwoStale'], + ['3', 'CreditOne'], + ['4', 'CreditTwo'], + ['6', 'DisabledBank'], + ['5', 'InvestmentOne'] + ] + assert acct_sel.first_selected_option.get_attribute('value') == '1' + # Split Budget + assert selenium.find_element_by_id( + 'trans_frm_is_split').is_selected() is True + assert selenium.find_element_by_id( + 'trans_frm_budget_group').is_displayed() is False + assert selenium.find_element_by_id( + 'trans_frm_split_budget_container').is_displayed() + assert len( + selenium.find_elements_by_class_name('budget_split_row') + ) == 3 + # BUDGET 0 + budget_sel = Select(body.find_element_by_id('trans_frm_budget_0')) + opts = [] + for o in budget_sel.options: + opts.append([o.get_attribute('value'), o.text]) + assert opts == [ + ['None', ''], + ['7', 'Income (income)'], + ['1', 'Periodic1'], + ['2', 'Periodic2'], + ['3', 'Periodic3 Inactive'], + ['4', 'Standing1'], + ['5', 'Standing2'], + ['6', 'Standing3 Inactive'] + ] + assert budget_sel.first_selected_option.get_attribute('value') == '2' + assert body.find_element_by_id( + 'trans_frm_budget_amount_0').get_attribute('value') == '200' + # BUDGET 1 + budget_sel = Select(body.find_element_by_id('trans_frm_budget_1')) + opts = [] + for o in budget_sel.options: + opts.append([o.get_attribute('value'), o.text]) + assert opts == [ + ['None', ''], + ['7', 'Income (income)'], + ['1', 'Periodic1'], + ['2', 'Periodic2'], + ['3', 'Periodic3 Inactive'], + ['4', 'Standing1'], + ['5', 'Standing2'], + ['6', 'Standing3 Inactive'] + ] + assert budget_sel.first_selected_option.get_attribute('value') == '1' + assert body.find_element_by_id( + 'trans_frm_budget_amount_1').get_attribute('value') == '100' + # BUDGET 2 + budget_sel = Select(body.find_element_by_id('trans_frm_budget_2')) + opts = [] + for o in budget_sel.options: + opts.append([o.get_attribute('value'), o.text]) + assert opts == [ + ['None', ''], + ['7', 'Income (income)'], + ['1', 'Periodic1'], + ['2', 'Periodic2'], + ['3', 'Periodic3 Inactive'], + ['4', 'Standing1'], + ['5', 'Standing2'], + ['6', 'Standing3 Inactive'] + ] + assert budget_sel.first_selected_option.get_attribute('value') == '4' + assert body.find_element_by_id( + 'trans_frm_budget_amount_2').get_attribute('value') == '75' + assert selenium.find_element_by_id( + 'trans_frm_notes').get_attribute('value') == 'NewSplitTransNotes' + # Ok, now edit it... + Select(body.find_element_by_id( + 'trans_frm_budget_1')).select_by_value('None') + body.find_element_by_id('trans_frm_budget_amount_1').clear() + budget_amt = body.find_element_by_id('trans_frm_budget_amount_0') + budget_amt.clear() + budget_amt.send_keys('300') + self.assert_budget_split_does_not_have_error(selenium) + # submit the form + selenium.find_element_by_id('modalSaveButton').click() + self.wait_for_jquery_done(selenium) + # check that we got positive confirmation + _, _, body = self.get_modal_parts(selenium) + x = body.find_elements_by_tag_name('div')[0] + assert 'alert-success' in x.get_attribute('class') + assert x.text.strip() == 'Successfully saved Transaction 5 ' \ + 'in database.' + # dismiss the modal + selenium.find_element_by_id('modalCloseButton').click() + self.wait_for_jquery_done(selenium) + # test that new trans was added to the table + table = selenium.find_element_by_id('table-transactions') + texts = [y[2] for y in self.tbody2textlist(table)] + assert 'NewTrans5' in texts + t = testdb.query(Transaction).get(5) + assert t is not None + assert t.description == 'NewTrans5' + assert t.date == dtnow().date() + assert t.actual_amount == Decimal('375') + assert t.budgeted_amount is None + assert t.planned_budget_id is None + assert t.account_id == 1 + assert t.scheduled_trans_id is None + assert t.notes == 'NewSplitTransNotes' + assert {bt.budget_id: bt.amount for bt in t.budget_transactions} == { + 2: Decimal('300'), + 4: Decimal('75') + } + assert max([ + tx.id for tx in testdb.query(Transaction).all() + ]) == 5 + + def test_34_existing_trans_to_split(self, base_url, selenium, testdb): + t = testdb.query(Transaction).get(3) + assert t is not None + assert t.description == 'T3' + assert t.date == (dtnow() - timedelta(days=2)).date() + assert t.actual_amount == Decimal('222.22') + assert t.budgeted_amount is None + assert t.planned_budget_id is None + assert t.account_id == 3 + assert t.scheduled_trans_id is None + assert t.notes == 'notesT3' + assert {bt.budget_id: bt.amount for bt in t.budget_transactions} == { + 2: Decimal('222.22') + } + assert max([ + tx.id for tx in testdb.query(Transaction).all() + ]) == 5 + self.baseurl = base_url + self.get(selenium, base_url + '/transactions') + link = selenium.find_element_by_xpath('//a[text()="T3"]') + modal, title, body = self.try_click_and_get_modal(selenium, link) + self.assert_modal_displayed(modal, title, body) + assert title.text == 'Edit Transaction 3' + assert body.find_element_by_id( + 'trans_frm_id').get_attribute('value') == '3' + assert body.find_element_by_id( + 'trans_frm_date').get_attribute('value') == ( + dtnow() - timedelta(days=2) + ).strftime('%Y-%m-%d') + assert body.find_element_by_id( + 'trans_frm_amount').get_attribute('value') == '222.22' + # NOT Split Budget + assert selenium.find_element_by_id( + 'trans_frm_is_split').is_selected() is False + assert selenium.find_element_by_id( + 'trans_frm_budget_group').is_displayed() is True + assert selenium.find_element_by_id( + 'trans_frm_split_budget_container').is_displayed() is False + # Ok, click to split it... + self.try_click( + selenium, selenium.find_element_by_id('trans_frm_is_split') + ) + # Should be split now... + assert selenium.find_element_by_id( + 'trans_frm_is_split').is_selected() + assert selenium.find_element_by_id( + 'trans_frm_budget_group').is_displayed() is False + assert selenium.find_element_by_id( + 'trans_frm_split_budget_container').is_displayed() + assert len( + selenium.find_elements_by_class_name('budget_split_row') + ) == 2 + # Verify that initial budget was set + assert Select( + body.find_element_by_id('trans_frm_budget_0') + ).first_selected_option.get_attribute('value') == '2' + # Verify that amount has been set + assert body.find_element_by_id( + 'trans_frm_budget_amount_0').get_attribute('value') == '222.22' + # Set the amount + budget_amt = body.find_element_by_id('trans_frm_budget_amount_0') + budget_amt.clear() + budget_amt.send_keys('100.02') + # select the second budget + Select(body.find_element_by_id( + 'trans_frm_budget_1')).select_by_value('4') + # Verify that second amount is set + assert body.find_element_by_id( + 'trans_frm_budget_amount_1').get_attribute('value') == '122.20' + self.assert_budget_split_does_not_have_error(selenium) + # submit the form + selenium.find_element_by_id('modalSaveButton').click() + self.wait_for_jquery_done(selenium) + # check that we got positive confirmation + _, _, body = self.get_modal_parts(selenium) + x = body.find_elements_by_tag_name('div')[0] + assert 'alert-success' in x.get_attribute('class') + assert x.text.strip() == 'Successfully saved Transaction 3 ' \ + 'in database.' + # dismiss the modal + selenium.find_element_by_id('modalCloseButton').click() + self.wait_for_jquery_done(selenium) + + def test_35_verify_db(self, testdb): + t = testdb.query(Transaction).get(3) + assert t is not None + assert t.description == 'T3' + assert t.date == (dtnow() - timedelta(days=2)).date() + assert t.actual_amount == Decimal('222.22') + assert t.budgeted_amount is None + assert t.planned_budget_id is None + assert t.account_id == 3 + assert t.scheduled_trans_id is None + assert t.notes == 'notesT3' + assert {bt.budget_id: bt.amount for bt in t.budget_transactions} == { + 2: Decimal('100.02'), + 4: Decimal('122.20') + } + assert max([ + tx.id for tx in testdb.query(Transaction).all() + ]) == 5 + + def test_36_existing_split_trans_to_not(self, base_url, selenium, testdb): + t = testdb.query(Transaction).get(3) + assert t is not None + assert t.description == 'T3' + assert t.date == (dtnow() - timedelta(days=2)).date() + assert t.actual_amount == Decimal('222.22') + assert t.budgeted_amount is None + assert t.planned_budget_id is None + assert t.account_id == 3 + assert t.scheduled_trans_id is None + assert t.notes == 'notesT3' + assert {bt.budget_id: bt.amount for bt in t.budget_transactions} == { + 2: Decimal('100.02'), + 4: Decimal('122.20') + } + assert max([ + tx.id for tx in testdb.query(Transaction).all() + ]) == 5 + self.baseurl = base_url + self.get(selenium, base_url + '/transactions') + link = selenium.find_element_by_xpath('//a[text()="T3"]') + modal, title, body = self.try_click_and_get_modal(selenium, link) + self.assert_modal_displayed(modal, title, body) + assert title.text == 'Edit Transaction 3' + assert body.find_element_by_id( + 'trans_frm_id').get_attribute('value') == '3' + assert body.find_element_by_id( + 'trans_frm_date').get_attribute('value') == ( + dtnow() - timedelta(days=2) + ).strftime('%Y-%m-%d') + assert body.find_element_by_id( + 'trans_frm_amount').get_attribute('value') == '222.22' + # Should be split... + assert selenium.find_element_by_id( + 'trans_frm_is_split').is_selected() + assert selenium.find_element_by_id( + 'trans_frm_budget_group').is_displayed() is False + assert selenium.find_element_by_id( + 'trans_frm_split_budget_container').is_displayed() + assert len( + selenium.find_elements_by_class_name('budget_split_row') + ) == 2 + # Ok, click to un-split it... + self.try_click( + selenium, selenium.find_element_by_id('trans_frm_is_split') + ) + # NOT Split Budget + assert selenium.find_element_by_id( + 'trans_frm_is_split').is_selected() is False + assert selenium.find_element_by_id( + 'trans_frm_budget_group').is_displayed() is True + assert selenium.find_element_by_id( + 'trans_frm_split_budget_container').is_displayed() is False + # Ok, now edit it... + budget_sel = Select(body.find_element_by_id('trans_frm_budget')) + budget_sel.select_by_value('2') + # submit the form + selenium.find_element_by_id('modalSaveButton').click() + self.wait_for_jquery_done(selenium) + # check that we got positive confirmation + _, _, body = self.get_modal_parts(selenium) + x = body.find_elements_by_tag_name('div')[0] + assert 'alert-success' in x.get_attribute('class') + assert x.text.strip() == 'Successfully saved Transaction 3 ' \ + 'in database.' + # dismiss the modal + selenium.find_element_by_id('modalCloseButton').click() + self.wait_for_jquery_done(selenium) + + def test_37_verify_db(self, testdb): + t = testdb.query(Transaction).get(3) + assert t is not None + assert t.description == 'T3' + assert t.date == (dtnow() - timedelta(days=2)).date() + assert t.actual_amount == Decimal('222.22') + assert t.budgeted_amount is None + assert t.planned_budget_id is None + assert t.account_id == 3 + assert t.scheduled_trans_id is None + assert t.notes == 'notesT3' + assert {bt.budget_id: bt.amount for bt in t.budget_transactions} == { + 2: Decimal('222.22') + } + assert max([ + tx.id for tx in testdb.query(Transaction).all() + ]) == 5 diff --git a/biweeklybudget/tests/acceptance/test_biweeklypayperiod.py b/biweeklybudget/tests/acceptance/test_biweeklypayperiod.py index 842dd3da..9f1cbe1a 100644 --- a/biweeklybudget/tests/acceptance/test_biweeklypayperiod.py +++ b/biweeklybudget/tests/acceptance/test_biweeklypayperiod.py @@ -239,6 +239,7 @@ def test_1_confirm_pay_period_start(self, testdb): def test_2_add_data(self, testdb): acct = testdb.query(Account).get(1) budg = testdb.query(Budget).get(1) + budg2 = testdb.query(Budget).get(2) st_daynum = ScheduledTransaction( amount=Decimal('111.11'), description='ST_day_9', @@ -323,7 +324,10 @@ def test_2_add_data(self, testdb): ) testdb.add(t_foo) t_bar = Transaction( - budget_amounts={budg: Decimal('666.66')}, + budget_amounts={ + budg: Decimal('666.66'), + budg2: Decimal('100.00') + }, date=date(2017, 4, 16), description='Trans_bar', account=acct @@ -343,136 +347,143 @@ def test_3_ignore_scheduled_converted_to_real_trans(self, testdb): { 'account_id': 1, 'account_name': 'BankOne', - 'amount': Decimal('222.22'), - 'budget_id': 1, - 'budget_name': 'Periodic1', + 'amount': Decimal('222.2200'), 'budgeted_amount': None, + 'budgets': { + 1: {'amount': Decimal('222.2200'), 'name': 'Periodic1'} + }, 'date': None, 'description': 'ST_pp_1', 'id': 8, + 'reconcile_id': None, 'sched_trans_id': None, 'sched_type': 'per period', - 'type': 'ScheduledTransaction', - 'reconcile_id': None + 'type': 'ScheduledTransaction' }, { 'account_id': 1, 'account_name': 'BankOne', - 'amount': Decimal('333.33'), - 'budget_id': 1, - 'budget_name': 'Periodic1', + 'amount': Decimal('333.3300'), 'budgeted_amount': None, + 'budgets': { + 1: {'amount': Decimal('333.3300'), 'name': 'Periodic1'} + }, 'date': None, 'description': 'ST_pp_3', 'id': 9, + 'reconcile_id': None, 'sched_trans_id': None, 'sched_type': 'per period', - 'type': 'ScheduledTransaction', - 'reconcile_id': None + 'type': 'ScheduledTransaction' }, { 'account_id': 1, 'account_name': 'BankOne', - 'amount': Decimal('555.55'), - 'budget_id': 1, - 'budget_name': 'Periodic1', + 'amount': Decimal('555.5500'), 'budgeted_amount': None, + 'budgets': { + 1: {'amount': Decimal('555.5500'), 'name': 'Periodic1'} + }, 'date': date(2017, 4, 8), 'description': 'Trans_foo', - 'id': 8, + 'id': 9, + 'planned_budget_id': None, + 'planned_budget_name': None, + 'reconcile_id': None, 'sched_trans_id': None, 'sched_type': None, - 'type': 'Transaction', - 'reconcile_id': None, - 'planned_budget_id': None, - 'planned_budget_name': None + 'type': 'Transaction' }, - # ST7 (ST_day_9) { 'account_id': 1, 'account_name': 'BankOne', - 'amount': Decimal('111.33'), - 'budget_id': 1, - 'budget_name': 'Periodic1', - 'budgeted_amount': Decimal('111.11'), + 'amount': Decimal('111.3300'), + 'budgeted_amount': Decimal('111.1100'), + 'budgets': { + 1: {'amount': Decimal('111.3300'), 'name': 'Periodic1'} + }, 'date': date(2017, 4, 9), 'description': 'Trans_ST_day_9', - 'id': 4, + 'id': 5, + 'planned_budget_id': 1, + 'planned_budget_name': 'Periodic1', + 'reconcile_id': 2, 'sched_trans_id': 7, 'sched_type': None, - 'type': 'Transaction', - 'reconcile_id': 2, - 'planned_budget_id': 1, - 'planned_budget_name': 'Periodic1' + 'type': 'Transaction' }, - # ST10 (ST_date) { 'account_id': 1, 'account_name': 'BankOne', - 'amount': Decimal('444.44'), - 'budget_id': 1, - 'budget_name': 'Periodic1', - 'budgeted_amount': Decimal('444.44'), + 'amount': Decimal('444.4400'), + 'budgeted_amount': Decimal('444.4400'), + 'budgets': { + 1: {'amount': Decimal('444.4400'), 'name': 'Periodic1'} + }, 'date': date(2017, 4, 12), 'description': 'Trans_ST_date', - 'id': 7, + 'id': 8, + 'planned_budget_id': 1, + 'planned_budget_name': 'Periodic1', + 'reconcile_id': None, 'sched_trans_id': 10, 'sched_type': None, - 'type': 'Transaction', - 'reconcile_id': None, - 'planned_budget_id': 1, - 'planned_budget_name': 'Periodic1' + 'type': 'Transaction' }, { 'account_id': 1, 'account_name': 'BankOne', - 'amount': Decimal('333.33'), - 'budget_id': 1, - 'budget_name': 'Periodic1', - 'budgeted_amount': Decimal('333.33'), + 'amount': Decimal('333.3300'), + 'budgeted_amount': Decimal('333.3300'), + 'budgets': { + 1: {'amount': Decimal('333.3300'), 'name': 'Periodic1'} + }, 'date': date(2017, 4, 14), 'description': 'Trans_ST_pp_3_A', - 'id': 5, + 'id': 6, + 'planned_budget_id': 1, + 'planned_budget_name': 'Periodic1', + 'reconcile_id': None, 'sched_trans_id': 9, 'sched_type': None, - 'type': 'Transaction', - 'reconcile_id': None, - 'planned_budget_id': 1, - 'planned_budget_name': 'Periodic1' + 'type': 'Transaction' }, { 'account_id': 1, 'account_name': 'BankOne', - 'amount': Decimal('333.33'), - 'budget_id': 1, - 'budget_name': 'Periodic1', - 'budgeted_amount': Decimal('333.33'), + 'amount': Decimal('333.3300'), + 'budgeted_amount': Decimal('333.3300'), + 'budgets': { + 1: {'amount': Decimal('333.3300'), 'name': 'Periodic1'} + }, 'date': date(2017, 4, 15), 'description': 'Trans_ST_pp_3_B', - 'id': 6, + 'id': 7, + 'planned_budget_id': 1, + 'planned_budget_name': 'Periodic1', + 'reconcile_id': None, 'sched_trans_id': 9, 'sched_type': None, - 'type': 'Transaction', - 'reconcile_id': None, - 'planned_budget_id': 1, - 'planned_budget_name': 'Periodic1' + 'type': 'Transaction' }, { 'account_id': 1, 'account_name': 'BankOne', - 'amount': Decimal('666.66'), - 'budget_id': 1, - 'budget_name': 'Periodic1', + 'amount': Decimal('766.6600'), 'budgeted_amount': None, + 'budgets': { + 1: {'amount': Decimal('666.6600'), 'name': 'Periodic1'}, + 2: {'amount': Decimal('100.0000'), 'name': 'Periodic2'} + }, 'date': date(2017, 4, 16), 'description': 'Trans_bar', - 'id': 9, + 'id': 10, + 'planned_budget_id': None, + 'planned_budget_name': None, + 'reconcile_id': None, 'sched_trans_id': None, 'sched_type': None, - 'type': 'Transaction', - 'reconcile_id': None, - 'planned_budget_id': None, - 'planned_budget_name': None + 'type': 'Transaction' } ] @@ -547,7 +558,10 @@ def test_3_add_transactions(self, testdb): # Budget 3 Income Transaction t1 = Transaction( date=date(2017, 4, 7), - budget_amounts={budgets[3]: Decimal('100.00')}, + budget_amounts={ + budgets[3]: Decimal('100.00'), + budgets[4]: Decimal('50.00') + }, budgeted_amount=Decimal('100.00'), description='B3 Income', account=acct @@ -630,10 +644,10 @@ def test_4_budget_sums(self, testdb): 4: { 'budget_amount': Decimal('500.00'), 'allocated': Decimal('1000.0'), - 'spent': Decimal('850.0'), - 'trans_total': Decimal('1100.0'), + 'spent': Decimal('900.0'), + 'trans_total': Decimal('1150.0'), 'is_income': False, - 'remaining': Decimal('-600.0') + 'remaining': Decimal('-650.0') }, 5: { 'budget_amount': Decimal('100.0'), @@ -652,9 +666,9 @@ def test_5_overall_sums(self, testdb): ) assert pp._data['overall_sums'] == { 'allocated': Decimal('1100.0'), - 'spent': Decimal('853.0'), + 'spent': Decimal('903.0'), 'income': Decimal('322.45'), - 'remaining': Decimal('-530.55') + 'remaining': Decimal('-580.55') } @patch('%s.settings.PAY_PERIOD_START_DATE' % pbm, date(2017, 4, 7)) @@ -711,10 +725,10 @@ def test_7_spent_greater_than_allocated(self, testdb): 4: { 'budget_amount': Decimal('500.00'), 'allocated': Decimal('1000.0'), - 'spent': Decimal('850.0'), - 'trans_total': Decimal('1100.0'), + 'spent': Decimal('900.0'), + 'trans_total': Decimal('1150.0'), 'is_income': False, - 'remaining': Decimal('-600.0') + 'remaining': Decimal('-650.0') }, 5: { 'budget_amount': Decimal('100.0'), @@ -727,9 +741,9 @@ def test_7_spent_greater_than_allocated(self, testdb): } assert pp._data['overall_sums'] == { 'allocated': Decimal('1100.0'), - 'spent': Decimal('2885.0'), + 'spent': Decimal('2935.0'), 'income': Decimal('322.45'), - 'remaining': Decimal('-2562.55') + 'remaining': Decimal('-2612.55') } @patch('%s.settings.PAY_PERIOD_START_DATE' % pbm, date(2017, 4, 7)) diff --git a/biweeklybudget/tests/acceptance/test_db_event_handlers.py b/biweeklybudget/tests/acceptance/test_db_event_handlers.py index cd27b9c1..a9ed9b37 100644 --- a/biweeklybudget/tests/acceptance/test_db_event_handlers.py +++ b/biweeklybudget/tests/acceptance/test_db_event_handlers.py @@ -56,7 +56,7 @@ def test_0_verify_db(self, testdb): max_t = max([ t.id for t in testdb.query(Transaction).all() ]) - assert max_t == 3 + assert max_t == 4 standing = testdb.query(Budget).get(5) assert standing.is_periodic is False assert standing.name == 'Standing2' @@ -71,8 +71,8 @@ def test_1_add_trans_periodic_budget(self, testdb): t = Transaction( budget_amounts={testdb.query(Budget).get(2): Decimal('222.22')}, budgeted_amount=Decimal('123.45'), - description='T4', - notes='notesT4', + description='T5', + notes='notesT5', account=testdb.query(Account).get(1), planned_budget=testdb.query(Budget).get(2) ) @@ -85,7 +85,7 @@ def test_2_verify_db(self, testdb): max_t = max([ t.id for t in testdb.query(Transaction).all() ]) - assert max_t == 4 + assert max_t == 5 standing = testdb.query(Budget).get(5) assert standing.is_periodic is False assert standing.name == 'Standing2' @@ -100,8 +100,8 @@ def test_3_add_trans_standing_budget(self, testdb): t = Transaction( budget_amounts={testdb.query(Budget).get(5): Decimal('222.22')}, budgeted_amount=Decimal('123.45'), - description='T5', - notes='notesT5', + description='T6', + notes='notesT6', account=testdb.query(Account).get(1), planned_budget=testdb.query(Budget).get(5) ) @@ -114,7 +114,7 @@ def test_4_verify_db(self, testdb): max_t = max([ t.id for t in testdb.query(Transaction).all() ]) - assert max_t == 5 + assert max_t == 6 standing = testdb.query(Budget).get(5) assert standing.is_periodic is False assert standing.name == 'Standing2' @@ -126,7 +126,7 @@ def test_4_verify_db(self, testdb): def test_5_edit_trans_standing_budget(self, testdb): """edit a transaction against a standing budget""" - t = testdb.query(Transaction).get(5) + t = testdb.query(Transaction).get(6) budg = testdb.query(Budget).get(5) t.set_budget_amounts({budg: Decimal('111.11')}) testdb.add(t) @@ -138,7 +138,7 @@ def test_6_verify_db(self, testdb): max_t = max([ t.id for t in testdb.query(Transaction).all() ]) - assert max_t == 5 + assert max_t == 6 standing = testdb.query(Budget).get(5) assert standing.is_periodic is False assert standing.name == 'Standing2' @@ -150,7 +150,7 @@ def test_6_verify_db(self, testdb): def test_7_edit_trans_standing_budget(self, testdb): """edit a transaction against a standing budget""" - t = testdb.query(Transaction).get(5) + t = testdb.query(Transaction).get(6) t.set_budget_amounts({testdb.query(Budget).get(5): Decimal('-111.11')}) testdb.add(t) testdb.flush() @@ -161,7 +161,7 @@ def test_8_verify_db(self, testdb): max_t = max([ t.id for t in testdb.query(Transaction).all() ]) - assert max_t == 5 + assert max_t == 6 standing = testdb.query(Budget).get(5) assert standing.is_periodic is False assert standing.name == 'Standing2' diff --git a/biweeklybudget/tests/acceptance_helpers.py b/biweeklybudget/tests/acceptance_helpers.py index 569f54ff..13b6bbd4 100644 --- a/biweeklybudget/tests/acceptance_helpers.py +++ b/biweeklybudget/tests/acceptance_helpers.py @@ -39,7 +39,7 @@ from time import sleep from decimal import Decimal from selenium.common.exceptions import ( - StaleElementReferenceException, TimeoutException + StaleElementReferenceException, TimeoutException, WebDriverException ) from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait @@ -294,12 +294,27 @@ def get_modal_parts(self, selenium, wait=True): modalBody WebElement) :rtype: tuple """ - if wait: - self.wait_for_modal_shown(selenium) - modal = selenium.find_element_by_id('modalDiv') - title = selenium.find_element_by_id('modalLabel') - body = selenium.find_element_by_id('modalBody') - return modal, title, body + count = 0 + while True: + count += 1 + try: + if wait: + self.wait_for_modal_shown(selenium) + modal = selenium.find_element_by_id('modalDiv') + title = selenium.find_element_by_id('modalLabel') + body = selenium.find_element_by_id('modalBody') + return modal, title, body + except TimeoutException: + if count > 6: + raise + print( + 'TimeoutException waiting for modal to be shown; ' + 'try again in 3 seconds.' + ) + sleep(3) + except Exception: + raise + return None, None, None def assert_modal_displayed(self, modal, title, body): """ @@ -397,3 +412,55 @@ def sort_trans_rows(self, rows): row[1] = fmt_currency(row[1]) ret.append(row) return ret + + def try_click(self, driver, elem): # noqa + """ + Wrapper for recent Chrome Headless + "Other element would receive the click" errors. Attempts to retry the + click after a short wait if it throws that error. + + :param driver: Selenium driver instance + :type driver: selenium.webdriver.remote.webdriver.WebDriver + :param elem: element to click + :type elem: selenium.webdriver.remote.webelement.WebElement + """ + max_tries = 4 + for i in range(0, max_tries): + try: + elem.click() + return + except WebDriverException as ex: + if 'Other element would receive the click' not in str(ex): + raise + if i == max_tries - 1: + raise + sleep(1.0) + + def try_click_and_get_modal(self, driver, elem_to_click, wait=True): + """ + Combination of :py:meth:`~.try_click` and :py:meth:`~.get_modal_parts` + to work around both the "Other element would receive the click" error + and TimeoutExceptions waiting for the modal to be shown. + + :param driver: Selenium driver instance + :type driver: selenium.webdriver.remote.webdriver.WebDriver + :param elem_to_click: element to click + :type elem_to_click: selenium.webdriver.remote.webelement.WebElement + :param wait: whether or not to wait for presence of modalLabel + :type wait: bool + :return: 3-tuple of (modalDiv WebElement, modalLabel WebElement, + modalBody WebElement) + :rtype: tuple + """ + max_tries = 4 + for i in range(0, max_tries): + try: + self.try_click(driver, elem_to_click) + return self.get_modal_parts(driver, wait=wait) + except (WebDriverException, TimeoutException): + if i == max_tries - 1: + raise + logger.error('ERROR: Unable to click link and get modal. ' + 'Trying again in 1s', exc_info=True) + sleep(1.0) + return None, None, None diff --git a/biweeklybudget/tests/conftest.py b/biweeklybudget/tests/conftest.py index 5418d8a5..c54456e3 100644 --- a/biweeklybudget/tests/conftest.py +++ b/biweeklybudget/tests/conftest.py @@ -63,6 +63,7 @@ import pytest_selenium.pytest_selenium from selenium.webdriver.support.event_firing_webdriver import \ EventFiringWebDriver + from selenium.webdriver.chrome.webdriver import WebDriver as ChromeWD HAVE_PYTEST_SELENIUM = True except ImportError: HAVE_PYTEST_SELENIUM = False @@ -266,7 +267,12 @@ def driver(request, driver_class, driver_kwargs): out and replaced it with the ``get_driver_for_class()`` function, which is wrapped in the retrying package's ``@retry`` decorator. """ - driver = get_driver_for_class(driver_class, driver_kwargs) + kwargs = driver_kwargs + if driver_class == ChromeWD: + kwargs['desired_capabilities']['loggingPrefs'] = { + 'browser': 'ALL' + } + driver = get_driver_for_class(driver_class, kwargs) event_listener = request.config.getoption('event_listener') if event_listener is not None: diff --git a/biweeklybudget/tests/fixtures/sampledata.py b/biweeklybudget/tests/fixtures/sampledata.py index ca19be36..538c1143 100644 --- a/biweeklybudget/tests/fixtures/sampledata.py +++ b/biweeklybudget/tests/fixtures/sampledata.py @@ -240,6 +240,16 @@ def _transactions(self): description='T3', notes='notesT3', account=self.accounts['CreditOne']['account'] + ), + Transaction( + date=(self.dt - timedelta(days=35)).date(), + budget_amounts={ + self.budgets['Periodic2']: Decimal('222.22'), + self.budgets['Periodic1']: Decimal('100.10') + }, + description='T4split', + notes='notesT4split', + account=self.accounts['CreditOne']['account'] ) ] for x in res: diff --git a/biweeklybudget/tests/unit/test_biweeklypayperiod.py b/biweeklybudget/tests/unit/test_biweeklypayperiod.py index b331dab7..4ff0b936 100644 --- a/biweeklybudget/tests/unit/test_biweeklypayperiod.py +++ b/biweeklybudget/tests/unit/test_biweeklypayperiod.py @@ -581,33 +581,44 @@ def test_scheduled_income(self): 'type': 'ScheduledTransaction', 'amount': Decimal('11.11'), 'budgeted_amount': None, - 'budget_id': 1 + 'budgets': { + 1: {'name': 'foo', 'amount': Decimal('11.11')} + } }, { 'type': 'Transaction', 'amount': Decimal('22.22'), 'budgeted_amount': None, - 'budget_id': 1 + 'budgets': { + 1: {'name': 'foo', 'amount': Decimal('20.00')}, + 2: {'name': 'foo', 'amount': Decimal('2.22')} + } }, { 'type': 'Transaction', 'amount': Decimal('22.22'), 'budgeted_amount': Decimal('20.20'), - 'budget_id': 1, + 'budgets': { + 1: {'name': 'foo', 'amount': Decimal('22.22')} + }, 'planned_budget_id': 1 }, { 'type': 'Transaction', 'amount': Decimal('33.33'), 'budgeted_amount': Decimal('33.33'), - 'budget_id': 2, + 'budgets': { + 2: {'name': 'foo', 'amount': Decimal('33.33')} + }, 'planned_budget_id': 2 }, { 'type': 'ScheduledTransaction', 'amount': Decimal('-1234.56'), 'budgeted_amount': Decimal('-1234.56'), - 'budget_id': 4, + 'budgets': { + 4: {'name': 'foo', 'amount': Decimal('-1234.56')} + }, 'planned_budget_id': 4 } ] @@ -636,19 +647,19 @@ def test_scheduled_income(self): assert res == { 1: { 'budget_amount': Decimal('123.45'), - 'allocated': Decimal('53.53'), - 'spent': Decimal('44.44'), - 'trans_total': Decimal('55.55'), + 'allocated': Decimal('51.31'), + 'spent': Decimal('42.22'), + 'trans_total': Decimal('53.33'), 'is_income': False, - 'remaining': Decimal('67.90') + 'remaining': Decimal('70.12') }, 2: { 'budget_amount': Decimal('456.78'), - 'allocated': Decimal('33.33'), - 'spent': Decimal('33.33'), - 'trans_total': Decimal('33.33'), + 'allocated': Decimal('35.55'), + 'spent': Decimal('35.55'), + 'trans_total': Decimal('35.55'), 'is_income': False, - 'remaining': Decimal('423.45') + 'remaining': Decimal('421.23') }, 3: { 'budget_amount': Decimal('789.10'), @@ -686,33 +697,43 @@ def test_actual_income(self): 'type': 'ScheduledTransaction', 'amount': Decimal('11.11'), 'budgeted_amount': None, - 'budget_id': 1 + 'budgets': { + 1: {'name': 'foo', 'amount': Decimal('11.11')} + } }, { 'type': 'Transaction', 'amount': Decimal('22.22'), 'budgeted_amount': None, - 'budget_id': 1 + 'budgets': { + 1: {'name': 'foo', 'amount': Decimal('22.22')} + } }, { 'type': 'Transaction', 'amount': Decimal('22.22'), 'budgeted_amount': Decimal('20.20'), - 'budget_id': 1, + 'budgets': { + 1: {'name': 'foo', 'amount': Decimal('22.22')} + }, 'planned_budget_id': 1 }, { 'type': 'Transaction', 'amount': Decimal('33.33'), 'budgeted_amount': Decimal('33.33'), - 'budget_id': 2, + 'budgets': { + 2: {'name': 'foo', 'amount': Decimal('33.33')} + }, 'planned_budget_id': 2 }, { 'type': 'Transaction', 'amount': Decimal('-1234.56'), 'budgeted_amount': Decimal('-1234.56'), - 'budget_id': 4, + 'budgets': { + 4: {'name': 'foo', 'amount': Decimal('-1234.56')} + }, 'planned_budget_id': 4 } ] @@ -992,11 +1013,76 @@ def test_simple(self): 'budgeted_amount': Decimal('120.00'), 'account_id': 2, 'account_name': 'foo', - 'budget_id': 3, - 'budget_name': 'bar', 'reconcile_id': None, 'planned_budget_id': 3, - 'planned_budget_name': 'bar' + 'planned_budget_name': 'bar', + 'budgets': { + 3: {'name': 'bar', 'amount': Decimal('123.45')} + } + } + + def test_budget_split(self): + m_account = Mock(name='foo') + type(m_account).name = 'foo' + m_budget = Mock(name='bar') + type(m_budget).name = 'bar' + m_budget2 = Mock(name='foo') + type(m_budget2).name = 'foo' + m_budget3 = Mock(name='baz') + type(m_budget3).name = 'baz' + m = Mock( + spec=Transaction, + id=123, + date=date(year=2017, month=7, day=15), + scheduled_trans_id=567, + description='desc', + actual_amount=Decimal('123.45'), + budgeted_amount=Decimal('120.00'), + account_id=2, + account=m_account, + planned_budget_id=3, + planned_budget=m_budget, + budget_transactions=[ + Mock( + spec_set=BudgetTransaction, + budget_id=1, + budget=m_budget, + amount=Decimal('53.00') + ), + Mock( + spec_set=BudgetTransaction, + budget_id=2, + budget=m_budget2, + amount=Decimal('20.45') + ), + Mock( + spec_set=BudgetTransaction, + budget_id=3, + budget=m_budget3, + amount=Decimal('50.00') + ) + ] + ) + type(m).reconcile = None + assert self.cls._dict_for_trans(m) == { + 'type': 'Transaction', + 'id': 123, + 'date': date(year=2017, month=7, day=15), + 'sched_type': None, + 'sched_trans_id': 567, + 'description': 'desc', + 'amount': Decimal('123.45'), + 'budgeted_amount': Decimal('120.00'), + 'account_id': 2, + 'account_name': 'foo', + 'reconcile_id': None, + 'planned_budget_id': 3, + 'planned_budget_name': 'bar', + 'budgets': { + 1: {'name': 'bar', 'amount': Decimal('53.00')}, + 2: {'name': 'foo', 'amount': Decimal('20.45')}, + 3: {'name': 'baz', 'amount': Decimal('50.00')} + } } def test_reconciled(self): @@ -1037,11 +1123,12 @@ def test_reconciled(self): 'budgeted_amount': Decimal('120.00'), 'account_id': 2, 'account_name': 'foo', - 'budget_id': 3, - 'budget_name': 'bar', 'reconcile_id': 2, 'planned_budget_id': 3, - 'planned_budget_name': 'bar' + 'planned_budget_name': 'bar', + 'budgets': { + 3: {'name': 'bar', 'amount': Decimal('123.45')} + } } def test_budgeted_amount_none(self): @@ -1082,11 +1169,12 @@ def test_budgeted_amount_none(self): 'budgeted_amount': None, 'account_id': 2, 'account_name': 'foo', - 'budget_id': 3, - 'budget_name': 'bar', 'planned_budget_id': None, 'planned_budget_name': None, - 'reconcile_id': None + 'reconcile_id': None, + 'budgets': { + 3: {'name': 'bar', 'amount': Decimal('123.45')} + } } @@ -1124,9 +1212,10 @@ def test_date(self): 'budgeted_amount': None, 'account_id': 2, 'account_name': 'foo', - 'budget_id': 3, - 'budget_name': 'bar', - 'reconcile_id': None + 'reconcile_id': None, + 'budgets': { + 3: {'name': 'bar', 'amount': Decimal('123.45')} + } } def test_per_period(self): @@ -1142,9 +1231,10 @@ def test_per_period(self): 'budgeted_amount': None, 'account_id': 2, 'account_name': 'foo', - 'budget_id': 3, - 'budget_name': 'bar', - 'reconcile_id': None + 'reconcile_id': None, + 'budgets': { + 3: {'name': 'bar', 'amount': Decimal('123.45')} + } } def test_day_of_month_single_month(self): @@ -1161,9 +1251,10 @@ def test_day_of_month_single_month(self): 'budgeted_amount': None, 'account_id': 2, 'account_name': 'foo', - 'budget_id': 3, - 'budget_name': 'bar', - 'reconcile_id': None + 'reconcile_id': None, + 'budgets': { + 3: {'name': 'bar', 'amount': Decimal('123.45')} + } } def test_day_of_month_cross_month_before(self): @@ -1181,9 +1272,10 @@ def test_day_of_month_cross_month_before(self): 'budgeted_amount': None, 'account_id': 2, 'account_name': 'foo', - 'budget_id': 3, - 'budget_name': 'bar', - 'reconcile_id': None + 'reconcile_id': None, + 'budgets': { + 3: {'name': 'bar', 'amount': Decimal('123.45')} + } } def test_day_of_month_cross_month_after(self): @@ -1201,7 +1293,8 @@ def test_day_of_month_cross_month_after(self): 'budgeted_amount': None, 'account_id': 2, 'account_name': 'foo', - 'budget_id': 3, - 'budget_name': 'bar', - 'reconcile_id': None + 'reconcile_id': None, + 'budgets': { + 3: {'name': 'bar', 'amount': Decimal('123.45')} + } } diff --git a/docs/make_screenshots.py b/docs/make_screenshots.py index 612a3e9f..710c55b8 100644 --- a/docs/make_screenshots.py +++ b/docs/make_screenshots.py @@ -40,12 +40,6 @@ import os import re from collections import defaultdict - -index_head = """Screenshots -=========== - -""" - import os import glob import socket @@ -73,6 +67,11 @@ from selenium.webdriver.chrome.options import Options from PIL import Image +index_head = """Screenshots +=========== + +""" + format = "%(asctime)s [%(levelname)s %(filename)s:%(lineno)s - " \ "%(name)s.%(funcName)s() ] %(message)s" logging.basicConfig(level=logging.DEBUG, format=format) @@ -118,6 +117,26 @@ class Screenshotter(object): 'postshot_func': '_index_postshot', 'preshot_func': '_index_preshot' }, + { + 'path': '/transactions', + 'filename': 'transactions', + 'title': 'Transactions View', + 'description': 'Shows all manually-entered transactions.' + }, + { + 'path': '/transactions/2', + 'filename': 'transaction2', + 'title': 'Transaction Detail', + 'description': 'Transaction detail modal to view and edit a ' + 'transaction.' + }, + { + 'path': '/transactions/4', + 'filename': 'transaction4', + 'title': 'Transactions with Budget Splits', + 'description': 'A single Transaction can be split across ' + 'multiple budgets.' + }, { 'path': '/accounts/credit-payoff', 'filename': 'credit-payoff', @@ -183,19 +202,6 @@ class Screenshotter(object): 'postshot_func': '_balance_postshot', 'preshot_func': '_sleep_2' }, - { - 'path': '/transactions', - 'filename': 'transactions', - 'title': 'Transactions View', - 'description': 'Shows all manually-entered transactions.' - }, - { - 'path': '/transactions/2', - 'filename': 'transaction2', - 'title': 'Transaction Detail', - 'description': 'Transaction detail modal to view and edit a ' - 'transaction.' - }, { 'path': '/accounts', 'filename': 'accounts', @@ -350,18 +356,26 @@ def _fuel_log_preshot(self): assert veh is not None for i in range(3, 33): last_odo += 200 + randrange(-50, 50) - cpg = 2.0 + (randrange(-100, 100) / 100) - gals = 10.0 + (randrange(-300, 300) / 100) + cpg = Decimal('2.0') + Decimal(randrange(-100, 100) / 100) + gals = Decimal('10.0') + Decimal(randrange(-300, 300) / 100) fill = FuelFill( date=(dtnow() + timedelta(days=i)).date(), - cost_per_gallon=cpg, + cost_per_gallon=Decimal( + cpg.quantize(Decimal('.001'), rounding=ROUND_HALF_UP) + ), fill_location='foo', - gallons=gals, + gallons=Decimal( + gals.quantize(Decimal('.001'), rounding=ROUND_HALF_UP) + ), level_before=choice([0, 10, 20, 30, 40]), level_after=100, odometer_miles=last_odo, reported_miles=last_odo, - total_cost=cpg * gals, + total_cost=Decimal( + (cpg * gals).quantize( + Decimal('.001'), rounding=ROUND_HALF_UP + ) + ), vehicle=veh ) data_sess.add(fill) @@ -454,10 +468,10 @@ def _payperiods_preshot(self): ) pp = BiweeklyPayPeriod.period_for_date(dtnow(), data_sess).previous data_sess.add(Budget( - name='Budget3', is_periodic=True, starting_balance=0 + name='Budget3', is_periodic=True, starting_balance=Decimal('0') )) data_sess.add(Budget( - name='Budget4', is_periodic=True, starting_balance=0 + name='Budget4', is_periodic=True, starting_balance=Decimal('0') )) data_sess.flush() data_sess.commit() @@ -483,10 +497,11 @@ def _payperiods_preshot(self): data_sess.add(Transaction( account_id=1, budgeted_amount=amt, - actual_amount=amt, - budget=choice(budgets), date=pp.start_date + timedelta(days=1), - description='Transaction %d.%d' % (i, count) + description='Transaction %d.%d' % (i, count), + budget_amounts={ + choice(budgets): amt + } )) data_sess.flush() data_sess.commit() @@ -536,10 +551,11 @@ def _add_transactions(self, data_sess, pp): data_sess.add(Transaction( account_id=1, budgeted_amount=amt, - actual_amount=amt, - budget=choice(budgets), date=pp.start_date + timedelta(days=randrange(0, 12)), - description='Transaction %d' % count + description='Transaction %d' % count, + budget_amounts={ + choice(budgets): amt + } )) data_sess.flush() data_sess.commit() @@ -577,13 +593,13 @@ def _budgets_preshot(self): ) pp = BiweeklyPayPeriod.period_for_date(dtnow(), data_sess).previous data_sess.add(Budget( - name='Budget3', is_periodic=True, starting_balance=0 + name='Budget3', is_periodic=True, starting_balance=Decimal('0') )) data_sess.add(Budget( - name='Budget4', is_periodic=True, starting_balance=0 + name='Budget4', is_periodic=True, starting_balance=Decimal('0') )) data_sess.add(Budget( - name='Budget5', is_periodic=True, starting_balance=250 + name='Budget5', is_periodic=True, starting_balance=Decimal('250') )) data_sess.flush() data_sess.commit() diff --git a/docs/source/account1.png b/docs/source/account1.png index 010fd89d..9832cfea 100644 Binary files a/docs/source/account1.png and b/docs/source/account1.png differ diff --git a/docs/source/account1_sm.png b/docs/source/account1_sm.png index cb4e7146..1aca09e9 100644 Binary files a/docs/source/account1_sm.png and b/docs/source/account1_sm.png differ diff --git a/docs/source/accounts.png b/docs/source/accounts.png index eca15379..c8dac721 100644 Binary files a/docs/source/accounts.png and b/docs/source/accounts.png differ diff --git a/docs/source/accounts_sm.png b/docs/source/accounts_sm.png index 0de02750..42281436 100644 Binary files a/docs/source/accounts_sm.png and b/docs/source/accounts_sm.png differ diff --git a/docs/source/bom.png b/docs/source/bom.png index c52e1fdb..7bf4ac0e 100644 Binary files a/docs/source/bom.png and b/docs/source/bom.png differ diff --git a/docs/source/bom_sm.png b/docs/source/bom_sm.png index 075402f3..12daf573 100644 Binary files a/docs/source/bom_sm.png and b/docs/source/bom_sm.png differ diff --git a/docs/source/budget2.png b/docs/source/budget2.png index 7ad446ee..b87b657c 100644 Binary files a/docs/source/budget2.png and b/docs/source/budget2.png differ diff --git a/docs/source/budget2_sm.png b/docs/source/budget2_sm.png index b9f2907b..4a0ce958 100644 Binary files a/docs/source/budget2_sm.png and b/docs/source/budget2_sm.png differ diff --git a/docs/source/budgets.png b/docs/source/budgets.png index 9f8ba3d7..08561340 100644 Binary files a/docs/source/budgets.png and b/docs/source/budgets.png differ diff --git a/docs/source/budgets_sm.png b/docs/source/budgets_sm.png index eef348b8..7eb1ec25 100644 Binary files a/docs/source/budgets_sm.png and b/docs/source/budgets_sm.png differ diff --git a/docs/source/credit-payoff.png b/docs/source/credit-payoff.png index 9e20b884..def013fd 100644 Binary files a/docs/source/credit-payoff.png and b/docs/source/credit-payoff.png differ diff --git a/docs/source/credit-payoff_sm.png b/docs/source/credit-payoff_sm.png index c0d75b68..753d2919 100644 Binary files a/docs/source/credit-payoff_sm.png and b/docs/source/credit-payoff_sm.png differ diff --git a/docs/source/foo.png b/docs/source/foo.png index 4a1150e2..b122574b 100644 Binary files a/docs/source/foo.png and b/docs/source/foo.png differ diff --git a/docs/source/fuel.png b/docs/source/fuel.png index 01d15b3c..5efc590e 100644 Binary files a/docs/source/fuel.png and b/docs/source/fuel.png differ diff --git a/docs/source/fuel_sm.png b/docs/source/fuel_sm.png index d313d99f..b359f039 100644 Binary files a/docs/source/fuel_sm.png and b/docs/source/fuel_sm.png differ diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 7b6f8518..e23dc54d 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -200,7 +200,7 @@ Host-Local MySQL Example It is also possible to use a MySQL server on the physical (Docker) host system. To do so, you'll need to know the host system's IP address. On Linux when using the default "bridge" Docker networking mode, this will coorespond to a ``docker0`` interface on the host system. -The Docker documentation on `adding entries to the Container's hosts file `_ +The Docker documentation on `adding entries to the Container's hosts file `_ provides a helpful snippet for this (on my systems, this results in ``172.17.0.1``): .. code-block:: none @@ -236,7 +236,7 @@ Settings Module Example If you need to provide biweeklybudget with more complicated configuration, this is still possible via a Python settings module. The easiest way to inject one into the -Docker image is to `mount `_ +Docker image is to `mount `_ a python module directly into the biweeklybudget package directory. Assuming you have a custom settings module on your local machine at ``/opt/biweeklybudget-settings.py``, you would run the container as shown below to mount the custom settings module into the container and use it. diff --git a/docs/source/index.png b/docs/source/index.png index 234005a8..6b3eacf9 100644 Binary files a/docs/source/index.png and b/docs/source/index.png differ diff --git a/docs/source/index_sm.png b/docs/source/index_sm.png index 1bd9123d..7d5d4d05 100644 Binary files a/docs/source/index_sm.png and b/docs/source/index_sm.png differ diff --git a/docs/source/jsdoc.formBuilder.rst b/docs/source/jsdoc.formBuilder.rst index ae628af3..0ba5ca2c 100644 --- a/docs/source/jsdoc.formBuilder.rst +++ b/docs/source/jsdoc.formBuilder.rst @@ -12,7 +12,7 @@ File: ``biweeklybudget/flaskapp/static/js/formBuilder.js`` -.. js:function:: FormBuilder.addCheckbox(id, name, label, checked) +.. js:function:: FormBuilder.addCheckbox(id, name, label, checked, options) Add a checkbox to the form. @@ -20,6 +20,8 @@ File: ``biweeklybudget/flaskapp/static/js/formBuilder.js`` :param String name: The name of the form element :param String label: The label text for the form element :param Boolean checked: Whether to default to checked or not + :param Object options: + :param String options.inputHtml: extra HTML string to include in the actual ``input`` element *(optional; defaults to null)* :returns: **FormBuilder** -- this diff --git a/docs/source/jsdoc.forms.rst b/docs/source/jsdoc.forms.rst index e56d3ad7..7385d40a 100644 --- a/docs/source/jsdoc.forms.rst +++ b/docs/source/jsdoc.forms.rst @@ -3,7 +3,7 @@ jsdoc.forms File: ``biweeklybudget/flaskapp/static/js/forms.js`` -.. js:function:: handleForm(container_id, form_id, post_url, dataTableObj) +.. js:function:: handleForm(container_id, form_id, post_url, dataTableObj, serialize_func) Generic function to handle form submission with server-side validation. @@ -13,6 +13,7 @@ File: ``biweeklybudget/flaskapp/static/js/forms.js`` :param string form_id: The ID of the form itself. :param string post_url: Relative URL to post form data to. :param Object dataTableObj: passed on to ``handleFormSubmitted()`` + :param Object serialize_func: If set (i.e. not ``undefined``), this is a function used serialize the form in place of :js:func:`serializeForm`. This function will be passed the ID of the form (``form_id``) and should return an Object suitable for passing to ``JSON.stringify()``. diff --git a/docs/source/jsdoc.transactions_modal.rst b/docs/source/jsdoc.transactions_modal.rst index 77bc8214..a3926428 100644 --- a/docs/source/jsdoc.transactions_modal.rst +++ b/docs/source/jsdoc.transactions_modal.rst @@ -3,13 +3,24 @@ jsdoc.transactions\_modal File: ``biweeklybudget/flaskapp/static/js/transactions_modal.js`` +.. js:function:: budgetSplitBlur() + + Triggered when a form element for budget splits loses focus. Calls + :js:func:`validateTransModalSplits` and updates the warning div with the + result. + + + + + .. js:function:: transModal(id, dataTableObj) Show the Transaction modal popup, optionally populated with information for one Transaction. This function calls :js:func:`transModalDivForm` to generate the form HTML, :js:func:`transModalDivFillAndShow` to populate the form for editing, - and :js:func:`handleForm` to handle the Submit action. + and :js:func:`handleForm` to handle the Submit action (using + :js:func:`transModalFormSerialize` as a custom serialization function). :param number id: the ID of the Transaction to show a modal for, or null to show modal to add a new Transaction. :param Object|null dataTableObj: passed on to :js:func:`handleForm` @@ -17,6 +28,23 @@ File: ``biweeklybudget/flaskapp/static/js/transactions_modal.js`` +.. js:function:: transModalAddSplitBudget() + + Handler for the "Add Budget" link on trans modal when using budget split. + + + + + +.. js:function:: transModalBudgetSplitRowHtml(row_num) + + Generate HTML for a budget div inside the split budgets div. + + :param Integer row_num: the budget split row number + + + + .. js:function:: transModalDivFillAndShow(msg) Ajax callback to fill in the modalDiv with data on a Transaction. @@ -33,3 +61,44 @@ File: ``biweeklybudget/flaskapp/static/js/transactions_modal.js`` +.. js:function:: transModalFormSerialize(form_id) + + Custom serialization function passed to :js:func:`handleForm` for + Transaction modal forms generated by :js:func:`transModal`. This handles + serialization of Transaction forms that may have a budget split, generating + data with a ``budgets`` Object (hash/mapping/dict) with budget ID keys and + amount values, suitable for passing directly to + :py:meth:`~.Transaction.set_budget_amounts`. + + :param String form_id: the ID of the form on the page. + + + + +.. js:function:: transModalHandleSplit() + + Handler for change of the "Budget Split?" (``#trans_frm_is_split``) checkbox. + + + + + +.. js:function:: transModalSplitBudgetChanged(row_num) + + Called when a budget split dropdown is changed. If its amount box is empty, + set it to the transaction amount minus the sum of all other budget splits. + + :param Integer row_num: the budget split row number + + + + +.. js:function:: validateTransModalSplits() + + Function to validate Transaction modal split budgets. Returns null if valid + or otherwise a String error message. + + + + + diff --git a/docs/source/ofx.png b/docs/source/ofx.png index 56c04184..afa5d206 100644 Binary files a/docs/source/ofx.png and b/docs/source/ofx.png differ diff --git a/docs/source/ofx_sm.png b/docs/source/ofx_sm.png index cbb790c0..2ce13ee1 100644 Binary files a/docs/source/ofx_sm.png and b/docs/source/ofx_sm.png differ diff --git a/docs/source/payperiod.png b/docs/source/payperiod.png index 23ceb632..253aee44 100644 Binary files a/docs/source/payperiod.png and b/docs/source/payperiod.png differ diff --git a/docs/source/payperiod_sm.png b/docs/source/payperiod_sm.png index df9e1a68..5872e90c 100644 Binary files a/docs/source/payperiod_sm.png and b/docs/source/payperiod_sm.png differ diff --git a/docs/source/payperiods.png b/docs/source/payperiods.png index 7a38ee29..668ea92f 100644 Binary files a/docs/source/payperiods.png and b/docs/source/payperiods.png differ diff --git a/docs/source/payperiods_sm.png b/docs/source/payperiods_sm.png index a53ab7f9..afeba763 100644 Binary files a/docs/source/payperiods_sm.png and b/docs/source/payperiods_sm.png differ diff --git a/docs/source/projects.png b/docs/source/projects.png index b2c10a3c..b4eabbf5 100644 Binary files a/docs/source/projects.png and b/docs/source/projects.png differ diff --git a/docs/source/projects_sm.png b/docs/source/projects_sm.png index 61e970bd..84d34e77 100644 Binary files a/docs/source/projects_sm.png and b/docs/source/projects_sm.png differ diff --git a/docs/source/reconcile-drag.png b/docs/source/reconcile-drag.png index 217bb982..456a3a83 100644 Binary files a/docs/source/reconcile-drag.png and b/docs/source/reconcile-drag.png differ diff --git a/docs/source/reconcile-drag_sm.png b/docs/source/reconcile-drag_sm.png index 54787cc6..db6c4c2b 100644 Binary files a/docs/source/reconcile-drag_sm.png and b/docs/source/reconcile-drag_sm.png differ diff --git a/docs/source/reconcile.png b/docs/source/reconcile.png index 6d96219d..be89bfde 100644 Binary files a/docs/source/reconcile.png and b/docs/source/reconcile.png differ diff --git a/docs/source/reconcile_sm.png b/docs/source/reconcile_sm.png index 6747f5f6..cb5a97a3 100644 Binary files a/docs/source/reconcile_sm.png and b/docs/source/reconcile_sm.png differ diff --git a/docs/source/scheduled.png b/docs/source/scheduled.png index 65c6c766..151ed571 100644 Binary files a/docs/source/scheduled.png and b/docs/source/scheduled.png differ diff --git a/docs/source/scheduled1.png b/docs/source/scheduled1.png index f2cfcc63..354cc114 100644 Binary files a/docs/source/scheduled1.png and b/docs/source/scheduled1.png differ diff --git a/docs/source/scheduled1_sm.png b/docs/source/scheduled1_sm.png index 4e08de5f..c83cab93 100644 Binary files a/docs/source/scheduled1_sm.png and b/docs/source/scheduled1_sm.png differ diff --git a/docs/source/scheduled2.png b/docs/source/scheduled2.png index f70cbdf4..abb5f327 100644 Binary files a/docs/source/scheduled2.png and b/docs/source/scheduled2.png differ diff --git a/docs/source/scheduled2_sm.png b/docs/source/scheduled2_sm.png index 20856c47..5ecae4d8 100644 Binary files a/docs/source/scheduled2_sm.png and b/docs/source/scheduled2_sm.png differ diff --git a/docs/source/scheduled3.png b/docs/source/scheduled3.png index 1fe42091..7a169e4e 100644 Binary files a/docs/source/scheduled3.png and b/docs/source/scheduled3.png differ diff --git a/docs/source/scheduled3_sm.png b/docs/source/scheduled3_sm.png index d7a606ec..88fe68cd 100644 Binary files a/docs/source/scheduled3_sm.png and b/docs/source/scheduled3_sm.png differ diff --git a/docs/source/scheduled_sm.png b/docs/source/scheduled_sm.png index 7c97f97d..fd258b7b 100644 Binary files a/docs/source/scheduled_sm.png and b/docs/source/scheduled_sm.png differ diff --git a/docs/source/transaction2.png b/docs/source/transaction2.png index 717eeeae..45df0efb 100644 Binary files a/docs/source/transaction2.png and b/docs/source/transaction2.png differ diff --git a/docs/source/transaction2_sm.png b/docs/source/transaction2_sm.png index c9e0e17a..edee3e20 100644 Binary files a/docs/source/transaction2_sm.png and b/docs/source/transaction2_sm.png differ diff --git a/docs/source/transaction4.png b/docs/source/transaction4.png new file mode 100644 index 00000000..13fd5cd8 Binary files /dev/null and b/docs/source/transaction4.png differ diff --git a/docs/source/transaction4_sm.png b/docs/source/transaction4_sm.png new file mode 100644 index 00000000..2bb17496 Binary files /dev/null and b/docs/source/transaction4_sm.png differ diff --git a/docs/source/transactions.png b/docs/source/transactions.png index 1fa93ace..91ea3c60 100644 Binary files a/docs/source/transactions.png and b/docs/source/transactions.png differ diff --git a/docs/source/transactions_sm.png b/docs/source/transactions_sm.png index 3f807c02..d2695843 100644 Binary files a/docs/source/transactions_sm.png and b/docs/source/transactions_sm.png differ diff --git a/requirements.txt b/requirements.txt index 954d6cf9..0b967b8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,5 +33,5 @@ pytz requests==2.18.4 selenium==3.8.1 six==1.11.0 -versionfinder>=0.1.1 +versionfinder>=0.1.3 wishlist==0.2.0