"""Module containing account related views.""" # vim: set tw=80 ts=4 sw=4 sts=4: import dateutil.parser from flask_jwt_extended import jwt_required from flask_restplus import Namespace, Resource, fields from ..models import db, row_as_dict, result_as_dicts from ..models.accounts import Account # pylint: disable=invalid-name ns = Namespace('account', description='Account management') # Account model. account_model = ns.model('Account', { 'id': fields.Integer( default=None, readonly=True, description='Id of the account'), 'name': fields.String( required=True, description='Name of the account'), 'authorized_overdraft': fields.Float( required=True, description='Authorized overdraft') }) # Account status model. balances_model = ns.model('Account balances', { 'current': fields.Float( readonly=True, description='Current balance of the account'), 'pointed': fields.Float( readonly=True, description='Pointed balance of the account'), 'future': fields.Float( readonly=True, description='Future balance of the account') }) # Account balance model. income_model = ns.model('Income', { 'expenses': fields.Float( readonly=True, description='Total amount of expenses'), 'revenues': fields.Float( readonly=True, description='Total amount of revenues'), 'income': fields.Float( readonly=True, description='Income'), }) # Category with expenses and revenues. category_model = ns.model('Category', { 'category': fields.String( readonly=True, description='Category name'), 'expenses': fields.Float( readonly=True, description='Total amount of expenses for the category'), 'revenues': fields.Float( readonly=True, description='Total amount of revenues for the category'), 'income': fields.Float( readonly=True, description='Total income for the category') }) # Daily balance model. daily_balance_model = ns.model('Daily balance', { 'operation_date': fields.Date( dt_format='iso8601', readonly=True, required=True, description='Date' ), 'expenses': fields.Float( readonly=True, required=True, description='Expenses' ), 'revenues': fields.Float( readonly=True, required=True, description='Revenues' ), 'income': fields.Float( readonly=True, required=True, description='Income' ), 'balance': fields.Float( readonly=True, required=True, description='Balance' ) }) # Parser for a date range. range_parser = ns.parser() range_parser.add_argument( 'begin', type=lambda a: dateutil.parser.parse(a) if a else None, required=False, default=None, location='args', help='Begin date of the time period' ) range_parser.add_argument( 'end', type=lambda a: dateutil.parser.parse(a) if a else None, required=False, default=None, location='args', help='End date of the time period' ) # pylint: enable=invalid-name # pylint: disable=no-self-use @ns.route('/') @ns.doc( security='apikey', responses={ 401: 'Unauthorized' }) class AccountListResource(Resource): """Resource used to handle account lists.""" @ns.response(200, 'OK', [account_model]) @ns.marshal_list_with(account_model) @jwt_required def get(self): """ Returns accounts with their balances.""" return Account.query().all(), 200 @ns.expect(account_model) @ns.response(201, 'Account created', account_model) @ns.response(406, 'Invalid account data') @ns.marshal_with(account_model) @jwt_required def post(self): """Create a new account.""" data = self.api.payload # A new account MUST NOT have an id; if data.get('id') is not None: ns.abort(406, 'Id must not be provided on creation.') # Instantiate account with data. account = Account(**data) # Add new account in session. db.session.add(account) # pylint: disable=no-member # Flush session to have id in account. db.session.flush() # pylint: disable=no-member # Return account. return account, 201 @ns.route('/') @ns.doc( security='apikey', params={ 'account_id': 'Id of the account to manage' }, responses={ 401: 'Unauthorized', 404: 'Account not found' }) class AccountResource(Resource): """Resource to handle accounts.""" @ns.response(200, 'OK', account_model) @ns.marshal_with(account_model) @jwt_required def get(self, account_id): """Get an account.""" account = Account.query().get(account_id) if not account: ns.abort(404, 'Account with id %d not found.' % account_id) # Note: if we don't pass the code, the result is seen as a tuple and # causes error on marshalling. return account, 200 @ns.expect(account_model) @ns.response(200, 'OK', account_model) @ns.response(406, 'Invalid account data') @ns.marshal_with(account_model) @jwt_required def post(self, account_id): """Update an account.""" data = self.api.payload # Check ID consistency. if data.get('id', default=account_id) != account_id: ns.abort(406, 'Id must not be provided or changed on update.') # Need to get the object to update it. account = Account.query().get(account_id) if not account: ns.abort(404, 'Account with id %d not found.' % account_id) # SQLAlchemy objects ignore __dict__.update() with merge. for key, value in data.items(): setattr(account, key, value) db.session.merge(account) # pylint: disable=no-member # Return account. return account, 200 @ns.response(204, 'Account deleted', account_model) @ns.marshal_with(account_model) @jwt_required def delete(self, account_id): """Delete an account.""" # Need to get the object to update it. account = Account.query().get(account_id) if not account: ns.abort(404, 'Account with id %d not found.' % account_id) db.session.delete(account) # pylint: disable=no-member return None, 204 @ns.route('//balances') @ns.doc( security='apikey', params={ 'account_id': 'Id of the account to manage' }, responses={ 200: ('OK', balances_model), 401: 'Unauthorized', 404: 'Account not found' }) class BalancesResource(Resource): """Resource to expose current, pointed and future balances.""" @ns.marshal_with(balances_model) @jwt_required def get(self, account_id): """Get current, pointed and future balances for a specific account and date range.""" account = Account.query().get(account_id) if not account: ns.abort(404, 'Account with id %d not found.' % account_id) # Note: if we don't pass the code, the result is seen as a tuple and # causes error on marshalling. return row_as_dict( account.balances() ), 200 @ns.route('//income') @ns.doc( security='apikey', params={ 'account_id': 'Id of the account to manage' }, responses={ 200: ('OK', income_model), 401: 'Unauthorized', 404: 'Account not found' }) class BalanceResource(Resource): """Resource to expose balances.""" @ns.expect(range_parser) @ns.marshal_with(income_model) @jwt_required def get(self, account_id): """Get account income for a specific date range.""" account = Account.query().get(account_id) if not account: ns.abort(404, 'Account with id %d not found.' % account_id) data = range_parser.parse_args() return row_as_dict( account.income(**data) ), 200 @ns.route("//category") @ns.doc( security='apikey', params={ 'account_id': 'Id of the account to manage' }, responses={ 200: ('OK', [category_model]), 401: 'Unauthorized', 404: 'Account not found' }) class CategoryResource(Resource): """Resource to expose categories.""" @ns.expect(range_parser) @ns.marshal_list_with(category_model) @jwt_required def get(self, account_id): """Get account category balances for a specific date range.""" data = range_parser.parse_args() # FIXME Alexis Lahouze 2017-05-23 check data. account = Account.query().get(account_id) if not account: ns.abort(404, 'Account with id %d not found.' % account_id) return list(result_as_dicts( account.category_incomes(**data) )), 200 @ns.route('//daily_balances') @ns.doc( security='apikey', params={ 'account_id': 'Id of the account to manage' }, responses={ 200: ('OK', [daily_balance_model]), 401: 'Unauthorized', 404: 'Account not found' }) class DailyBalancesResource(Resource): """Resource to expose account daily balances.""" @ns.expect(range_parser) @ns.marshal_list_with(daily_balance_model) @jwt_required def get(self, account_id): """Get account daily balance data for a specific date range and account.""" data = range_parser.parse_args() # FIXME Alexis Lahouze 2017-05-23 check data. account = Account.query().get(account_id) if not account: ns.abort(404, 'Account with id %d not found.' % account_id) return list(result_as_dicts( account.daily_balances(**data) )), 200