Source code for biweeklybudget.flaskapp.views.reconcile

"""
The latest version of this package is available at:
<http://github.com/jantman/biweeklybudget>

################################################################################
Copyright 2016 Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>

    This file is part of biweeklybudget, also known as biweeklybudget.

    biweeklybudget is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    biweeklybudget is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with biweeklybudget.  If not, see <http://www.gnu.org/licenses/>.

The Copyright and Authors attributions contained herein may not be removed or
otherwise altered, except to add the Author attribution of a contributor to
this work. (Additional Terms pursuant to Section 7b of the AGPL v3)
################################################################################
While not legally required, I sincerely request that anyone who finds
bugs please submit them at <https://github.com/jantman/biweeklybudget> or
to me via email, and that you send any contributions or improvements
either as a pull request on GitHub, or to me via email.
################################################################################

AUTHORS:
Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
################################################################################
"""

import logging

from flask.views import MethodView
from flask import render_template, jsonify, request

from biweeklybudget.flaskapp.app import app
from biweeklybudget.models.budget_model import Budget
from biweeklybudget.models.account import Account
from biweeklybudget.models.txn_reconcile import TxnReconcile
from biweeklybudget.models.transaction import Transaction
from biweeklybudget.models.ofx_transaction import OFXTransaction
from biweeklybudget.db import db_session

logger = logging.getLogger(__name__)


[docs]class ReconcileView(MethodView): """ Render the top-level GET /reconcile view using ``reconcile.html`` template. """
[docs] def get(self): budgets = {} active_budgets = {} for b in db_session.query(Budget).all(): k = b.name if b.is_income: k = '%s (i)' % b.name budgets[b.id] = k if b.is_active: active_budgets[b.id] = k accts = {a.name: a.id for a in db_session.query(Account).all()} return render_template( 'reconcile.html', budgets=budgets, accts=accts, active_budgets=active_budgets )
[docs]class TxnReconcileAjax(MethodView): """ Handle GET /ajax/reconcile/<int:reconcile_id> endpoint. """
[docs] def get(self, reconcile_id): rec = db_session.query(TxnReconcile).get(reconcile_id) res = { 'reconcile': rec.as_dict, '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 res['acct_id'] = rec.ofx_trans.account_id res['acct_name'] = rec.ofx_trans.account.name else: res['ofx_trans'] = None res['ofx_stmt'] = None res['acct_id'] = rec.transaction.account_id res['acct_name'] = rec.transaction.account.name return jsonify(res)
[docs]class OfxUnreconciledAjax(MethodView): """ Handle GET /ajax/unreconciled/ofx endpoint. """
[docs] def get(self): res = [] for t in OFXTransaction.unreconciled( db_session ).order_by(OFXTransaction.date_posted, OFXTransaction.fitid).all(): d = t.as_dict d['account_name'] = t.account.name d['account_amount'] = t.account_amount res.append(d) return jsonify(res)
[docs]class TransUnreconciledAjax(MethodView): """ Handle GET /ajax/unreconciled/trans endpoint. """
[docs] def get(self): res = [] for t in Transaction.unreconciled( db_session ).order_by(Transaction.date, Transaction.id).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 res.append(d) return jsonify(res)
[docs]class ReconcileAjax(MethodView): """ Handle POST ``/ajax/reconcile`` endpoint. """
[docs] def post(self): """ Handle POST ``/ajax/reconcile`` Request is a JSON dict with two keys, "reconciled" and "ofxIgnored". "reconciled" value is a dict of integer transaction ID keys, to values which are either a string reason why the Transaction is being reconciled as "No OFX" or a 2-item list of OFXTransaction acct_id and fitid. "ofxIgnored" is a dict with string keys which are strings identifying an OFXTransaction in the form "<ACCT_ID>%<FITID>", and values are a string reason why the OFXTransaction is being reconciled without a matching Transaction. Response is a JSON dict. Keys are ``success`` (boolean) and either ``error_message`` (string) or ``success_message`` (string). :return: JSON response """ raw = request.get_json() data = { 'reconciled': { int(x): raw['reconciled'][x] for x in raw['reconciled'] }, 'ofxIgnored': raw.get('ofxIgnored', {}) } logger.debug('POST /ajax/reconcile: %s', data) rec_count = 0 for trans_id in sorted(data['reconciled'].keys()): trans = db_session.query(Transaction).get(trans_id) if trans is None: logger.error('Invalid transaction ID: %s', trans_id) return jsonify({ 'success': False, 'error_message': 'Invalid Transaction ID: %s' % trans_id }), 400 if not isinstance(data['reconciled'][trans_id], type([])): # it's a string; reconcile without OFX db_session.add(TxnReconcile( txn_id=trans_id, note=data['reconciled'][trans_id] )) logger.info( 'Reconcile %s as NoOFX; note=%s', trans, data['reconciled'][trans_id] ) rec_count += 1 continue # else reconcile with OFX ofx_key = ( data['reconciled'][trans_id][0], data['reconciled'][trans_id][1] ) ofx = db_session.query(OFXTransaction).get(ofx_key) if ofx is None: logger.error('Invalid OFXTransaction: %s', ofx_key) return jsonify({ 'success': False, 'error_message': 'Invalid OFXTransaction: (%s, \'%s\')' % ( ofx_key[0], ofx_key[1] ) }), 400 db_session.add(TxnReconcile( txn_id=trans_id, ofx_account_id=data['reconciled'][trans_id][0], ofx_fitid=data['reconciled'][trans_id][1] )) logger.info('Reconcile %s with %s', trans, ofx) rec_count += 1 # handle OFXTransactions to reconcile with no Transaction for ofxkey in sorted(data['ofxIgnored'].keys()): note = data['ofxIgnored'][ofxkey] acct_id, fitid = ofxkey.split('%', 1) db_session.add(TxnReconcile( ofx_account_id=acct_id, ofx_fitid=fitid, note=note )) logger.info( 'Reconcile OFXTransaction (%s, %s) as NoTransaction; note=%s', acct_id, fitid, note ) rec_count += 1 try: db_session.flush() db_session.commit() except Exception as ex: logger.error('Exception committing transaction reconcile', exc_info=True) return jsonify({ 'success': False, 'error_message': 'Exception committing reconcile(s): %s' % ex }), 400 return jsonify({ 'success': True, 'success_message': 'Successfully reconciled ' '%d transactions' % rec_count })
app.add_url_rule( '/reconcile', view_func=ReconcileView.as_view('reconcile_view') ) app.add_url_rule( '/ajax/reconcile/<int:reconcile_id>', view_func=TxnReconcileAjax.as_view('txn_reconcile_ajax') ) app.add_url_rule( '/ajax/unreconciled/ofx', view_func=OfxUnreconciledAjax.as_view('ofx_unreconciled_ajax') ) app.add_url_rule( '/ajax/unreconciled/trans', view_func=TransUnreconciledAjax.as_view('trans_unreconciled_ajax') ) app.add_url_rule( '/ajax/reconcile', view_func=ReconcileAjax.as_view('reconcile_ajax') )