diff --git a/accountant/api/models/accounts.py b/accountant/api/models/accounts.py index d19d56b..1662d81 100644 --- a/accountant/api/models/accounts.py +++ b/accountant/api/models/accounts.py @@ -30,8 +30,14 @@ class Account(db.Model): @classmethod def query(cls, begin=None, end=None): - query = db.session.query( - Operation.account_id, + return db.session.query( + cls + ).order_by( + cls.name + ) + + def solds(self): + return db.session.query( db.func.sum(Operation.value).label("future"), db.func.sum( db.case( @@ -47,14 +53,12 @@ class Account(db.Model): else_=0 ) ).label("current"), - ).group_by( - Operation.account_id - ) - - status_query = query.subquery() + ).filter( + Operation.account_id == self.id + ).one() + def balance(self, begin, end): query = db.session.query( - Operation.account_id, db.func.sum( db.case( [(db.func.sign(Operation.value) == -1, @@ -70,8 +74,8 @@ class Account(db.Model): ) ).label("revenues"), db.func.sum(Operation.value).label("balance") - ).group_by( - Operation.account_id + ).filter( + Operation.account_id == self.id, ) if begin: @@ -80,31 +84,7 @@ class Account(db.Model): if end: query = query.filter(Operation.operation_date <= str(end)) - balance_query = query.subquery() - - query = db.session.query( - cls.id, - cls.name, - cls.authorized_overdraft, - db.func.coalesce( - status_query.c.current, 0).label('current'), - db.func.coalesce( - status_query.c.pointed, 0).label('pointed'), - db.func.coalesce( - status_query.c.future, 0).label('future'), - db.func.coalesce( - balance_query.c.expenses, 0).label('expenses'), - db.func.coalesce( - balance_query.c.revenues, 0).label('revenues'), - db.func.coalesce( - balance_query.c.balance, 0).label('balance'), - ).outerjoin( - status_query, status_query.c.account_id == cls.id - ).outerjoin( - balance_query, balance_query.c.account_id == cls.id - ) - - return query.order_by(cls.name) + return query.one() @db.validates('authorized_overdraft') def validate_authorized_overdraft(self, key, authorized_overdraft): diff --git a/accountant/api/views/accounts.py b/accountant/api/views/accounts.py index ce1240a..6011397 100644 --- a/accountant/api/views/accounts.py +++ b/accountant/api/views/accounts.py @@ -18,8 +18,6 @@ import dateutil.parser from flask.ext.restful import Resource, fields, reqparse, marshal_with_field -from sqlalchemy.orm.exc import NoResultFound - from accountant import db from .. import api @@ -36,9 +34,15 @@ account_model = { 'id': fields.Integer(default=None), 'name': fields.String, 'authorized_overdraft': fields.Float, +} + +solds_model = { 'current': fields.Float, 'pointed': fields.Float, 'future': fields.Float, +} + +balance_model = { 'expenses': fields.Float, 'revenues': fields.Float, 'balance': fields.Float, @@ -80,10 +84,8 @@ class AccountListResource(Resource): # Flush session to have id in account. db.session.flush() - # Return account with solds. - return Account.query().filter( - Account.id == account.id - ).one(), 201 + # Return account data. + return account, 201 class AccountResource(Resource): @@ -93,17 +95,13 @@ class AccountResource(Resource): """ Get account. """ - data = date_parser.parse_args() + account = Account.query().get(id) - try: - return Account.query( - **data - ).filter( - Account.id == id - ).one(), 200 - except NoResultFound: + if not account: return None, 404 + return account, 200 + @requires_auth @marshal_with_field(Object(account_model)) def post(self, id): @@ -113,7 +111,7 @@ class AccountResource(Resource): or data.id == id) # Need to get the object to update it. - account = db.session.query(Account).get(id) + account = Account.query().get(id) if not account: return None, 404 @@ -124,16 +122,14 @@ class AccountResource(Resource): db.session.merge(account) - # Return account with solds. - return Account.query().filter( - Account.id == id - ).one(), 200 + # Return account. + return account, 200 @requires_auth @marshal_with_field(Object(account_model)) def delete(self, id): # Need to get the object to update it. - account = db.session.query(Account).get(id) + account = Account.query().get(id) if not account: return None, 404 @@ -147,11 +143,47 @@ api.add_resource(AccountListResource, '/account') api.add_resource(AccountResource, '/account/') +class SoldsResource(Resource): + @requires_auth + @marshal_with_field(Object(solds_model)) + def get(self, id): + """ + Get solds for a specific account and date range. + """ + account = Account.query().get(id) + + if not account: + return None, 404 + + # Note: if we don't pass the code, the result is seen as a tuple and + # causes error on marshalling. + return account.solds(), 200 + + range_parser = reqparse.RequestParser() range_parser.add_argument('begin', type=lambda a: dateutil.parser.parse(a)) range_parser.add_argument('end', type=lambda a: dateutil.parser.parse(a)) +class BalanceResource(Resource): + @requires_auth + @marshal_with_field(Object(balance_model)) + def get(self, id): + """ + Get account balance for a specific date range. + """ + account = Account.query().get(id) + + if not account: + return None, 404 + + data = range_parser.parse_args() + + # Note: if we don't pass the code, the result is seen as a tuple and + # causes error on marshalling. + return account.balance(**data), 200 + + category_model = { 'category': fields.String, 'expenses': fields.Float, @@ -186,5 +218,7 @@ class OHLCResource(Resource): return Operation.get_ohlc_per_day_for_range(id, **data).all() +api.add_resource(SoldsResource, "/account//solds") +api.add_resource(BalanceResource, "/account//balance") api.add_resource(CategoryResource, "/account//category") api.add_resource(OHLCResource, "/account//ohlc") diff --git a/accountant/frontend/static/js/accounts.js b/accountant/frontend/static/js/accounts.js index 066885d..e61ba8c 100644 --- a/accountant/frontend/static/js/accounts.js +++ b/accountant/frontend/static/js/accounts.js @@ -19,11 +19,19 @@ accountantApp .factory("Account", ["$resource", function($resource) { - return $resource( + var Account = $resource( "/api/account/:id", { id: "@id" } ); + + Account.prototype.getSolds = function() { + var Solds = $resource("/api/account/:id/solds", {id: this.id}); + + this.solds = Solds.get(); + }; + + return Account; }]) .controller( diff --git a/accountant/frontend/static/js/operations.js b/accountant/frontend/static/js/operations.js index f0a31be..6aca345 100644 --- a/accountant/frontend/static/js/operations.js +++ b/accountant/frontend/static/js/operations.js @@ -43,13 +43,22 @@ accountantApp ); }]) +.factory("Balance", [ "$resource", "$routeParams", + function($resource, $routeParams) { + return $resource( + "/api/account/:account_id/balance", { + account_id: $routeParams.accountId + } + ); +}]) + /* * Controller for category chart. */ .controller( "CategoryChartController", [ - "$rootScope", "$scope", "$http", "$routeParams", "Category", - function($rootScope, $scope, $http, $routeParams, Category) { + "$rootScope", "$scope", "$http", "Category", "Balance", + function($rootScope, $scope, $http, Category, Balance) { var colors = Highcharts.getOptions().colors; $scope.revenueColor = colors[2]; @@ -150,30 +159,25 @@ accountantApp }; /* - * Get account status. + * Get account balance. */ - $scope.getAccountStatus = function(begin, end) { - $http.get("/api/account/" + $routeParams.accountId, { - params: { - begin: begin.format('YYYY-MM-DD'), - end: end.format('YYYY-MM-DD') - } - }).success(function(account) { - // Emit accountLoadedEvent. - $scope.$emit("accountLoadedEvent", account); - + $scope.getBalance = function(begin, end) { + Balance.get({ + begin: begin.format("YYYY-MM-DD"), + end: end.format("YYYY-MM-DD") + }, function(balance) { // Update pie chart subtitle with Balance. $scope.config.subtitle = { - text: "Balance: " + account.balance + text: "Balance: " + balance.balance }; $scope.config.series[0].data = [{ name: "Revenues", - y: account.revenues, + y: balance.revenues, color: $scope.revenueColor }, { name: "Expenses", - y: -account.expenses, + y: -balance.expenses, color: $scope.expenseColor, }]; }); @@ -182,7 +186,7 @@ accountantApp // Reload categories and account status on range selection. $rootScope.$on("rangeSelectedEvent", function(e, args) { $scope.load(args.begin, args.end); - $scope.getAccountStatus(args.begin, args.end); + $scope.getBalance(args.begin, args.end); }); }]) @@ -329,8 +333,8 @@ accountantApp */ .controller( "OperationController", [ - "$scope", "$rootScope", "$routeParams", "notify", "Operation", - function($scope, $rootScope, $routeParams, notify, Operation) { + "$scope", "$rootScope", "$routeParams", "notify", "Account", "Operation", + function($scope, $rootScope, $routeParams, notify, Account, Operation) { // List of operations. $scope.operations = []; @@ -442,11 +446,8 @@ accountantApp ); }; - /* - * Save account in scope to colorize with authorized overdraft. - */ - $rootScope.$on("accountLoadedEvent", function(e, account) { - $scope.account = account; + $scope.account = Account.get({ + id: $routeParams.accountId }); /* diff --git a/accountant/frontend/static/templates/accounts.html b/accountant/frontend/static/templates/accounts.html index c496a13..3a03e3c 100644 --- a/accountant/frontend/static/templates/accounts.html +++ b/accountant/frontend/static/templates/accounts.html @@ -37,7 +37,7 @@ + ng-repeat="account in accounts" ng-init="account.getSolds()"> - - {{ account.current | currency : "€" }} + + {{ account.solds.current | currency : "€" }} - - {{ account.pointed | currency : "€" }} + + {{ account.solds.pointed | currency : "€" }}