diff --git a/accountant/models/operations.py b/accountant/models/operations.py index 4e8f5d0..ee9ce69 100644 --- a/accountant/models/operations.py +++ b/accountant/models/operations.py @@ -166,8 +166,9 @@ class Operation(db.Model): return query @classmethod - def get_ohlc_per_day_for_range(cls, account, begin=None, end=None): - """Get Opening, High, Low, Closing per day for a specific range""" + def query_daily_balances(cls, account, begin=None, end=None): + """Get expenses, revenues, income and balance per day for a specific + range and a specific account.""" if isinstance(account, (int, str)): account_id = account else: @@ -175,53 +176,65 @@ class Operation(db.Model): end = end if end else arrow.now().ceil('month').date() - sold = db.func.sum(cls.value).over( - order_by=[cls.operation_date, db.desc(cls.value), cls.label] - ) - - previous = sold - cls.value - - subquery = db.session.query( - cls.operation_date, - sold.label("sold"), - previous.label("previous") - ).filter( - cls.account_id == account_id, - cls.canceled == db.false() - ).subquery() - query = db.session.query( - subquery.c.operation_date, - db.func.first_value(subquery.c.previous).over( - partition_by=subquery.c.operation_date - ).label("open"), - db.func.max( - db.func.greatest( - subquery.c.previous, subquery.c.sold + cls.operation_date, + db.func.coalesce( + db.func.sum( + cls.value + ).filter( + db.func.sign(cls.value) == -1 + ).over( + partition_by=[cls.account_id, cls.operation_date], + ), + 0 + ).label("expenses"), + db.func.coalesce( + db.func.sum( + cls.value + ).filter( + db.func.sign(cls.value) == 1 + ).over( + partition_by=[cls.account_id, cls.operation_date], + ), + 0 + ).label("revenues"), + db.func.coalesce( + db.func.sum( + cls.value + ).over( + partition_by=[cls.account_id, cls.operation_date], ) - ).over( - partition_by=subquery.c.operation_date - ).label('high'), - db.func.min( - db.func.least( - subquery.c.previous, subquery.c.sold + ).label("income"), + db.func.coalesce( + db.func.sum( + cls.value + ).over( + partition_by=cls.account_id, + order_by=cls.operation_date ) - ).over( - partition_by=subquery.c.operation_date - ).label('low'), - db.func.last_value(subquery.c.sold).over( - partition_by=subquery.c.operation_date - ).label('close') - ).distinct() + ).label("balance") + ).distinct( + ).order_by( + cls.operation_date + ).filter( + cls.account_id == account_id + ) if begin: - query = query.filter(subquery.c.operation_date >= str(begin)) + base_query = query.subquery() - if end: - query = query.filter(subquery.c.operation_date <= str(end)) + query = db.session.query( + base_query + ).filter( + base_query.c.operation_date >= str(begin) + ).order_by( + base_query.c.operation_date + ) - query = query.order_by( - subquery.c.operation_date - ) + if end: + query = query.filter(query.c.operation_date <= str(end)) + + elif end: + query = query.filter(cls.operation_date <= str(end)) return query diff --git a/accountant/views/accounts.py b/accountant/views/accounts.py index eab9d77..093e3df 100644 --- a/accountant/views/accounts.py +++ b/accountant/views/accounts.py @@ -7,7 +7,7 @@ import dateutil.parser from flask_jwt_extended import jwt_required from flask_restplus import Namespace, Resource, fields -from ..models import db, row_as_dict +from ..models import db, row_as_dict, result_as_dicts from ..models.accounts import Account from ..models.operations import Operation @@ -71,33 +71,33 @@ category_model = ns.model('Category', { description='Total income for the category') }) -# OHLC model. -ohlc_model = ns.model('OHLC', { - 'operation_date': fields.DateTime( +# Daily balance model. +daily_balance_model = ns.model('Daily balance', { + 'operation_date': fields.Date( dt_format='iso8601', readonly=True, required=True, - description='Date of the OHLC object' + description='Date' ), - 'open': fields.Float( + 'expenses': fields.Float( readonly=True, required=True, - description='Open value' + description='Expenses' ), - 'high': fields.Float( + 'revenues': fields.Float( readonly=True, required=True, - description='High value' + description='Revenues' ), - 'low': fields.Float( + 'income': fields.Float( readonly=True, required=True, - description='Low value' + description='Income' ), - 'close': fields.Float( + 'balance': fields.Float( readonly=True, required=True, - description='Close value' + description='Balance' ) }) @@ -330,26 +330,29 @@ class CategoryResource(Resource): return Operation.get_categories_for_range(account_id, **data).all() -@ns.route('//ohlc') +@ns.route('//daily_balances') @ns.doc( security='apikey', params={ 'account_id': 'Id of the account to manage' }, responses={ - 200: ('OK', [ohlc_model]), + 200: ('OK', [daily_balance_model]), 401: 'Unauthorized', 404: 'Account not found' }) -class OHLCResource(Resource): - """Resource to expose OHLC.""" +class DailyBalancesResource(Resource): + """Resource to expose account daily balances.""" @ns.expect(range_parser) - @ns.marshal_list_with(ohlc_model) + @ns.marshal_list_with(daily_balance_model) @jwt_required def get(self, account_id): - """Get OHLC data for a specific date range and account.""" + """Get account daily balance data for a specific date range and + account.""" data = range_parser.parse_args() - return Operation.get_ohlc_per_day_for_range(account_id, **data).all() + return list(result_as_dicts( + Operation.query_daily_balances(account_id, **data) + )), 200