accountant/accountant/views/accounts.py

374 lines
9.8 KiB
Python

"""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('/<int:account_id>')
@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('/<int:account_id>/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('/<int:account_id>/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("/<int:account_id>/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('/<int:account_id>/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