374 lines
9.7 KiB
Python
374 lines
9.7 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', 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
|