Ejemplo n.º 1
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        st_id = int(data['id'])
        st = db_session.query(ScheduledTransaction).get(st_id)
        d = datetime.strptime(data['payperiod_start_date'], '%Y-%m-%d').date()
        desc = 'Skip ScheduledTransaction %d in period %s' % (
            st_id, data['payperiod_start_date'])
        t = Transaction(date=d,
                        budget_amounts={st.budget: Decimal('0.0')},
                        budgeted_amount=Decimal('0.0'),
                        description=desc,
                        notes=data['notes'],
                        account=st.account,
                        scheduled_trans=st,
                        planned_budget=st.budget)
        db_session.add(t)
        db_session.add(TxnReconcile(transaction=t, note=desc))
        db_session.commit()
        logger.info(
            'Created Transaction %d to skip '
            'ScheduledTransaction %d', t.id, st.id)
        return 'Successfully created Transaction %d to skip ' \
               'ScheduledTransaction %d.' % (t.id, st.id)
Ejemplo n.º 2
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        action = data.get('action', None)
        if action == 'add':
            proj = Project()
            proj.name = data['name'].strip()
            proj.notes = data['notes'].strip()
        elif action == 'activate':
            proj = db_session.query(Project).get(int(data['id']))
            logger.info('Activate %s', proj)
            proj.is_active = True
        elif action == 'deactivate':
            proj = db_session.query(Project).get(int(data['id']))
            logger.info('Deactivate %s', proj)
            proj.is_active = False
        else:
            raise RuntimeError('Invalid action: %s' % action)
        db_session.add(proj)
        db_session.commit()
        if action == 'add':
            logger.info('Created Project %s', proj)
        return 'Successfully saved Project %d in database.' % proj.id
Ejemplo n.º 3
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        if 'id' in data and data['id'].strip() != '':
            # updating an existing Vehicle
            veh = db_session.query(Vehicle).get(int(data['id']))
            if veh is None:
                raise RuntimeError("Error: no Vehicle with ID %s" % data['id'])
            action = 'updating Vehicle ' + data['id']
        else:
            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
        logger.info('%s: %s', action, veh.as_dict)
        db_session.add(veh)
        db_session.commit()
        return 'Successfully saved Vehicle %d in database.' % veh.id
Ejemplo n.º 4
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        if 'id' in data and data['id'].strip() != '':
            # updating an existing budget
            budget = db_session.query(Budget).get(int(data['id']))
            if budget is None:
                raise RuntimeError("Error: no Budget with ID %s" % data['id'])
            action = 'updating Budget ' + data['id']
        else:
            budget = Budget()
            action = 'creating new Budget'
        budget.name = data['name'].strip()
        budget.description = data['description'].strip()
        budget.is_periodic = data['is_periodic']
        if data['is_periodic'] is True:
            budget.starting_balance = Decimal(data['starting_balance'])
        else:
            budget.current_balance = Decimal(data['current_balance'])
        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()
        return 'Successfully saved Budget %d in database.' % budget.id
Ejemplo n.º 5
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        st_id = int(data['id'])
        st = db_session.query(ScheduledTransaction).get(st_id)
        d = datetime.strptime(data['date'], '%Y-%m-%d').date()
        t = Transaction(date=d,
                        budget_amounts={st.budget: Decimal(data['amount'])},
                        budgeted_amount=st.amount,
                        description=data['description'],
                        notes=data['notes'],
                        account=st.account,
                        planned_budget=st.budget,
                        scheduled_trans=st)
        db_session.add(t)
        db_session.commit()
        logger.info('Created Transaction %d for ScheduledTransaction %d', t.id,
                    st.id)
        return 'Successfully created Transaction %d ' \
               'for ScheduledTransaction %d.' % (t.id, st.id)
Ejemplo n.º 6
0
def main():
    global logger
    logging.basicConfig(level=logging.WARNING,
                        format="[%(asctime)s %(levelname)s] %(message)s")
    logger = logging.getLogger()

    args = parse_args()

    # set logging level
    if args.verbose > 1:
        set_log_debug(logger)
    elif args.verbose == 1:
        set_log_info(logger)

    atexit.register(cleanup_db)
    Base.metadata.reflect(engine)
    Base.metadata.drop_all(engine)
    db_session.flush()
    db_session.commit()
    init_db()

    logger.info('Loading data from: %s.%s', args.modname, args.clsname)
    klass = getattr(importlib.import_module(args.modname), args.clsname)
    inst = klass(db_session)
    logger.info('Loading data')
    inst.load()
    db_session.commit()
    logger.info('Data loaded.')
Ejemplo n.º 7
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        # get the data
        trans_date = datetime.strptime(data['date'], '%Y-%m-%d').date()
        amt = Decimal(data['amount'])
        from_acct = db_session.query(Account).get(int(data['from_account']))
        if from_acct is None:
            raise RuntimeError("Error: no Account with ID %s" %
                               data['from_account'])
        to_acct = db_session.query(Account).get(int(data['to_account']))
        if to_acct is None:
            raise RuntimeError("Error: no Account with ID %s" %
                               data['to_account'])
        budget = db_session.query(Budget).get(int(data['budget']))
        if budget is None:
            raise RuntimeError("Error: no Budget with ID %s" % data['budget'])
        notes = data['notes'].strip()
        desc = 'Account Transfer - %s from %s (%d) to %s (%d)' % (
            amt, from_acct.name, from_acct.id, to_acct.name, to_acct.id)
        logger.info(desc)
        t1 = Transaction(date=trans_date,
                         budget_amounts={budget: amt},
                         budgeted_amount=amt,
                         description=desc,
                         account=from_acct,
                         notes=notes,
                         planned_budget=budget)
        db_session.add(t1)
        t2 = Transaction(date=trans_date,
                         budget_amounts={budget: (-1 * amt)},
                         budgeted_amount=(-1 * amt),
                         description=desc,
                         account=to_acct,
                         notes=notes,
                         planned_budget=budget)
        db_session.add(t2)
        t1.transfer = t2
        db_session.add(t1)
        t2.transfer = t1
        db_session.add(t2)
        db_session.commit()
        return 'Successfully saved Transactions %d and %d in database.' % (
            t1.id, t2.id)
Ejemplo n.º 8
0
    def post(self):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        data = request.get_json(force=True, silent=True)
        if data is None:
            logger.error('Error parsing request JSON')
            return jsonify({
                'success': False,
                'error_message': 'Error parsing JSON'
            })
        setting = db_session.query(DBSetting).get('credit-payoff')
        if setting is None:
            setting = DBSetting(name='credit-payoff')
            logger.info('new DBSetting name=credit-payoff')
        else:
            logger.info('Existing DBSetting name=credit-payoff value=%s',
                        setting.value)
        fixeddata = {'increases': [], 'onetimes': []}
        for key in ['increases', 'onetimes']:
            for d in sorted(data[key], key=lambda k: k['date']):
                if d['date'] == '' or d['amount'] == '':
                    continue
                fixeddata[key].append(d)
        val = json.dumps(fixeddata, sort_keys=True, cls=MagicJSONEncoder)
        try:
            parse_payoff_settings_json(val)
        except Exception as ex:
            logger.error('Error converting payoff settings JSON',
                         exc_info=True)
            return jsonify({
                'success': False,
                'error_message': 'Error parsing JSON: %s' % ex
            })
        logger.info('Changing setting value to: %s', val)
        setting.value = val
        db_session.add(setting)
        db_session.commit()
        return jsonify({
            'success':
            True,
            'success_message':
            'Successfully updated setting '
            '"credit-payoff" in database.'
        })
Ejemplo n.º 9
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        acct = db_session.query(Account).get(int(data['id']))
        if acct is None:
            raise RuntimeError('ERROR: No Account with ID %s' % data['id'])
        stmt = db_session.query(OFXStatement).filter(
            OFXStatement.account_id.__eq__(acct.id),
            OFXStatement.filename.__eq__(data['filename'])
        ).one()
        if stmt is None:
            raise RuntimeError(
                'ERROR: No OFXStatement for account %d with filename %s' % (
                    acct.id, data['filename']
                )
            )
        int_amt = Decimal(data['interest_amt'])
        if int_amt < Decimal('0'):
            int_amt = int_amt * Decimal('-1')
        trans = OFXTransaction(
            account=acct,
            statement=stmt,
            fitid='%s-MANUAL-CCPAYOFF' % dtnow().strftime('%Y%m%d%H%M%S'),
            trans_type='debit',
            date_posted=stmt.as_of,
            amount=int_amt,
            name='Interest Charged - MANUALLY ENTERED',
            is_interest_charge=True
        )
        logger.info(
            'Adding manual interest transaction to OFXTransactions: '
            'account_id=%d statement_filename=%s statement=%s '
            'OFXTransaction=%s', acct.id, data['filename'], stmt,
            trans
        )
        db_session.add(trans)
        db_session.commit()
        return 'Successfully saved OFXTransaction with FITID %s in database' \
               '.' % trans.fitid
Ejemplo n.º 10
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        if 'id' in data and data['id'].strip() != '':
            # updating an existing budget
            trans = db_session.query(Transaction).get(int(data['id']))
            if trans is None:
                raise RuntimeError("Error: no Transaction with ID "
                                   "%s" % data['id'])
            if trans.reconcile is not None:
                raise RuntimeError(
                    "Transaction %d is already reconciled; cannot be edited."
                    "" % trans.id)
            action = 'updating Transaction ' + data['id']
        else:
            trans = Transaction()
            action = 'creating new Transaction'
        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()
        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()
        return {
            'success_message':
            'Successfully saved Transaction %d  in database.'
            '' % trans.id,
            'success':
            True,
            'trans_id':
            trans.id
        }
Ejemplo n.º 11
0
    def update_statement_ofx(self, acct_id, ofx, mtime=None, filename=None):
        """
        Update a single statement for the specified account, from an OFX file.

        :param acct_id: Account ID that statement is for
        :type acct_id: int
        :param ofx: Ofx instance for parsed file
        :type ofx: ``ofxparse.ofxparse.Ofx``
        :param mtime: OFX file modification time (or current time)
        :type mtime: datetime.datetime
        :param filename: OFX file name
        :type filename: str
        :returns: 3-tuple of the int ID of the
          :py:class:`~biweeklybudget.models.ofx_statement.OFXStatement`
          created by this run, int count of new :py:class:`~.OFXTransaction`
          created, and int count of :py:class:`~.OFXTransaction` updated
        :rtype: tuple
        :raises: :py:exc:`RuntimeError` on error parsing OFX or unknown account
          type; :py:exc:`~.DuplicateFileException` if the file (according to the
          OFX signon date/time) has already been recorded.
        """
        logger.info(
            'Updating Account %d with OFX Statement filename="%s" (mtime %s)',
            acct_id, filename, mtime
        )
        acct = db_session.query(Account).get(acct_id)
        if mtime is None:
            mtime = dtnow()
        if hasattr(ofx, 'status') and ofx.status['severity'] == 'ERROR':
            raise RuntimeError("OFX Error: %s" % vars(ofx))
        stmt = self._create_statement(acct, ofx, mtime, filename)
        if ofx.account.type == AccountType.Bank:
            stmt.type = 'Bank'
            s = self._update_bank_or_credit(acct, ofx, stmt)
        elif ofx.account.type == AccountType.CreditCard:
            stmt.type = 'CreditCard'
            s = self._update_bank_or_credit(acct, ofx, stmt)
        elif ofx.account.type == AccountType.Investment:
            s = self._update_investment(acct, ofx, stmt)
        else:
            raise RuntimeError("Don't know how to update AccountType %d",
                               ofx.account.type)
        count_new, count_upd = self._new_updated_counts()
        db_session.commit()
        return s.id, count_new, count_upd
Ejemplo n.º 12
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        if 'id' in data and data['id'].strip() != '':
            # updating an existing BoMItem
            item = db_session.query(BoMItem).get(int(data['id']))
            if item is None:
                raise RuntimeError("Error: no BoMItem with ID "
                                   "%s" % data['id'])
            action = 'updating BoMItem ' + data['id']
        else:
            item = BoMItem()
            action = 'creating new BoMItem'
        item.project = db_session.query(Project).get(int(data['project_id']))
        item.name = data['name'].strip()
        item.notes = data['notes'].strip()
        item.quantity = int(data['quantity'].strip())
        item.unit_cost = float(data['unit_cost'].strip())
        item.url = data['url'].strip()
        if data['is_active'] == 'true':
            item.is_active = True
        else:
            item.is_active = False
        logger.info('%s: %s', action, item.as_dict)
        db_session.add(item)
        db_session.commit()
        return {
            'success_message':
            'Successfully saved BoMItem %d '
            'in database.' % item.id,
            'success':
            True,
            'id':
            item.id
        }
Ejemplo n.º 13
0
    def _do_project(self, list_url, project):
        """
        Update a project with information from its wishlist.

        :param list_url: Amazon wishlist URL
        :type list_url: str
        :param project: the project to update
        :type project: Project
        :return: whether or not the update was successful
        :rtype: bool
        """
        logger.debug('Handling project: %s', project)
        pitems = self._project_items(project)
        witems = self._wishlist_items(list_url)
        logger.debug('Project has %d items; wishlist has %d',
                     len(pitems), len(witems))
        for url, item in pitems.items():
            if url not in witems:
                logger.info(
                    '%s (%s) removed from amazon list; setting inactive',
                    item, url
                )
                item.is_active = False
                db_session.add(item)
        for url, item in witems.items():
            if url in pitems:
                bitem = pitems[url]
                logger.info('Updating %s from Amazon wishlist', bitem)
            else:
                bitem = BoMItem()
                bitem.project = project
                logger.info('Adding new BoMItem for wishlist %s', url)
            bitem.url = url
            bitem.is_active = True
            bitem.quantity = item['quantity']
            bitem.unit_cost = item['cost']
            bitem.name = item['name']
            db_session.add(bitem)
        logger.info('Committing changes for project %s url %s',
                    project, list_url)
        db_session.commit()
        return True
Ejemplo n.º 14
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        if 'id' in data and data['id'].strip() != '':
            # updating an existing budget
            trans = db_session.query(ScheduledTransaction).get(int(data['id']))
            if trans is None:
                raise RuntimeError("Error: no ScheduledTransaction with ID "
                                   "%s" % data['id'])
            action = 'updating ScheduledTransaction ' + data['id']
        else:
            trans = ScheduledTransaction()
            action = 'creating new ScheduledTransaction'
        trans.description = data['description'].strip()
        if data['type'] == 'monthly':
            trans.day_of_month = int(data['day_of_month'])
        elif data['type'] == 'per_period':
            trans.num_per_period = int(data['num_per_period'])
        else:
            # date
            trans.date = datetime.strptime(data['date'], '%Y-%m-%d').date()
        trans.amount = float(data['amount'])
        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
        logger.info('%s: %s', action, trans.as_dict)
        db_session.add(trans)
        db_session.commit()
        return 'Successfully saved ScheduledTransaction' \
               ' %d in database.' % trans.id
Ejemplo n.º 15
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        # get the data
        trans_date = datetime.strptime(data['date'], '%Y-%m-%d').date()
        amt = Decimal(data['amount'])
        acct = db_session.query(Account).get(int(data['account']))
        if acct is None:
            raise RuntimeError("Error: no Account with ID %s" %
                               data['account'])
        from_budget = db_session.query(Budget).get(int(data['from_budget']))
        if from_budget is None:
            raise RuntimeError("Error: no Budget with ID %s" %
                               data['from_budget'])
        to_budget = db_session.query(Budget).get(int(data['to_budget']))
        if to_budget is None:
            raise RuntimeError("Error: no Budget with ID %s" %
                               data['to_budget'])
        notes = data['notes'].strip()
        desc = 'Budget Transfer - %s from %s (%d) to %s (%d)' % (
            amt, from_budget.name, from_budget.id, to_budget.name,
            to_budget.id)
        logger.info(desc)
        res = do_budget_transfer(db_session,
                                 trans_date,
                                 amt,
                                 acct,
                                 from_budget,
                                 to_budget,
                                 notes=notes)
        db_session.commit()
        return 'Successfully saved Transactions %d and %d in database.' % (
            res[0].id, res[1].id)
Ejemplo n.º 16
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        new_acct = False
        if 'id' in data and data['id'].strip() != '':
            # updating an existing account
            account = db_session.query(Account).get(int(data['id']))
            if account is None:
                raise RuntimeError("Error: no Account with ID %s" % data['id'])
            action = 'updating Account ' + data['id']
        else:
            new_acct = True
            account = Account()
            action = 'creating new Account'
        account.name = data['name'].strip()
        account.description = self.fix_string(data['description'])
        account.acct_type = getattr(AcctType, data['acct_type'])
        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'])
        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'])
            else:
                account.credit_limit = None
            if data['apr'].strip() != '':
                account.apr = Decimal(data['apr'])
                if account.apr > Decimal('1'):
                    account.apr = account.apr * Decimal('0.01')
            else:
                account.apr = None
            if data['prime_rate_margin'].strip() != '':
                account.prime_rate_margin = Decimal(data['prime_rate_margin'])
                if account.prime_rate_margin > Decimal('1'):
                    account.prime_rate_margin = account.prime_rate_margin * \
                                                Decimal('0.01')
            else:
                account.prime_rate_margin = None
            account.interest_class_name = data['interest_class_name']
            account.min_payment_class_name = data['min_payment_class_name']
        account.is_active = data['is_active']
        for f in RE_FIELD_NAMES:
            data[f] = data[f].strip()
            if data[f] == '':
                data[f] = None
            setattr(account, f, data[f])
        logger.info('%s: %s', action, account.as_dict)
        db_session.add(account)
        db_session.commit()
        if new_acct:
            account.set_balance(ledger=Decimal('0'), avail=Decimal('0'))
            db_session.add(account)
            db_session.commit()
        return 'Successfully saved Account %d in database.' % account.id
Ejemplo n.º 17
0
    def submit(self, data):
        """
        Handle form submission; create or update models in the DB. Raises an
        Exception for any errors.

        :param data: submitted form data
        :type data: dict
        :return: message describing changes to DB (i.e. link to created record)
        :rtype: str
        """
        total = float(data['total_cost'])
        dt = datetime.strptime(data['date'], '%Y-%m-%d').date()
        veh_name = db_session.query(Vehicle).get(int(data['vehicle'])).name
        fill = FuelFill()
        fill.date = dt
        fill.vehicle_id = int(data['vehicle'])
        fill.odometer_miles = int(data['odometer_miles'])
        fill.reported_miles = int(data['reported_miles'])
        fill.level_before = int(data['level_before'])
        fill.level_after = int(data['level_after'])
        fill.fill_location = data['fill_location'].strip()
        fill.cost_per_gallon = float(data['cost_per_gallon'])
        fill.total_cost = total
        fill.gallons = float(data['gallons'])
        fill.reported_mpg = float(data['reported_mpg'])
        fill.notes = data['notes'].strip()
        logger.info('Creating new FuelFill: %s', fill.as_dict)
        db_session.add(fill)
        db_session.commit()
        fill.calculate_mpg()
        db_session.commit()
        if data['add_trans'] != 'true':
            return {
                'success_message':
                'Successfully saved FuelFill %d '
                'in database.' % fill.id,
                'success':
                True,
                'fill_id':
                fill.id,
                'calculated_mpg':
                fill.calculated_mpg
            }
        trans = Transaction()
        budg = db_session.query(Budget).get(int(data['budget']))
        trans.description = '%s - FuelFill #%d (%s)' % (
            data['fill_location'].strip(), fill.id, veh_name)
        trans.date = dt
        trans.actual_amount = total
        trans.account_id = int(data['account'])
        trans.budget = budg
        trans.notes = data['notes'].strip()
        logger.info('Creating new Transaction for FuelFill: %s', trans.as_dict)
        db_session.add(trans)
        db_session.commit()
        return {
            'success_message':
            'Successfully saved FuelFill %d '
            ' and Transaction %d in database.' % (fill.id, trans.id),
            'success':
            True,
            'trans_id':
            trans.id,
            'fill_id':
            fill.id,
            'calculated_mpg':
            fill.calculated_mpg
        }
Ejemplo n.º 18
0
    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
        })