From 23b3cf5ee028006c2cc257d1f6ff9d4c963866cf Mon Sep 17 00:00:00 2001 From: Jonathan Niles Date: Mon, 11 Dec 2017 17:17:35 +0100 Subject: [PATCH] feat(account statement): grid beginning balance This commit adds the beginning balance to the Account Statement grid for the period shown on the grid. To do so, I had to add a new function to the accounts service to search for the first date in a generic period selection. Closes #1762. --- client/src/i18n/en/form.json | 5 +- client/src/i18n/fr/form.json | 3 + .../account_statement.ctrl.js | 34 ++++-- .../account_statement/grid.footer.html | 5 +- .../src/modules/accounts/accounts.service.js | 30 +++-- .../general-ledger/general-ledger.service.js | 2 - server/config/routes.js | 1 + server/controllers/finance/accounts/extra.js | 14 +-- server/controllers/finance/accounts/index.js | 104 ++++++++++++++++-- 9 files changed, 162 insertions(+), 36 deletions(-) diff --git a/client/src/i18n/en/form.json b/client/src/i18n/en/form.json index 985e8395ec..9a505004e3 100644 --- a/client/src/i18n/en/form.json +++ b/client/src/i18n/en/form.json @@ -171,7 +171,7 @@ "VIEW_BEGINNING_BALANCE": "View Beginning Balance", "VIEW_CLOSING_BALANCE": "View Closing Balance" }, - "LABELS": { + "LABELS" : { "ABBREVIATION": "Abbreviation", "ACCOUNT": "Account", "ACCOUNT_EXTRACT": "Account Details", @@ -200,6 +200,8 @@ "MONTHLY_CONSUMPTION": "Monthly Consumption", "BALANCE": "Balance", "BALANCE_INTERMEDIATE" : "Intermediate Balance", + "BEGINNING_VIEW_BALANCE" : "Beginning View Balance", + "ENDING_VIEW_BALANCE" : "Ending View Balance", "BALANCE_FINAL" : "Final Balance", "BALANCE_SECTION": "Balance Sheet Section", "BANK": "Bank", @@ -256,6 +258,7 @@ "DATE_REGISTRATION": "Registration Date", "DATE_TO": "End", "DAYS": "Days", + "DIFFERENCE" : "Difference", "DEACTIVATE": "Deactivate", "DEBIT": "Debit", "DEBTOR_GROUP": "Debtor Group", diff --git a/client/src/i18n/fr/form.json b/client/src/i18n/fr/form.json index ee6c58f369..5e001ec55a 100644 --- a/client/src/i18n/fr/form.json +++ b/client/src/i18n/fr/form.json @@ -206,6 +206,8 @@ "BALANCE_SECTION": "Section du Bilan", "BANK": "Banque", "BANK_ACCOUNT": "Compte Bancaire", + "BEGINNING_VIEW_BALANCE" : "Solde Début du Grid", + "ENDING_VIEW_BALANCE" : "Solde Finale du Grid", "BASIC_SALARY": "Salaire de base", "BILLING_DATE": "Date Facturation", "INVOICING_FEES": "Les frais de facturation appliquée", @@ -259,6 +261,7 @@ "DATE_TO": "Fin", "DAYS": "Jours", "DEACTIVATE": "Désactiver", + "DIFFERENCE" : "Différence", "DEBIT": "Débit", "DEBTOR_GROUP": "Groupe Débiteur", "DEBTOR_GROUP_FORM": "Formulaire d'enregistrement de groupe débiteur", diff --git a/client/src/modules/account_statement/account_statement.ctrl.js b/client/src/modules/account_statement/account_statement.ctrl.js index fa0c54deb1..57d46b926b 100644 --- a/client/src/modules/account_statement/account_statement.ctrl.js +++ b/client/src/modules/account_statement/account_statement.ctrl.js @@ -6,7 +6,7 @@ AccountStatementController.$inject = [ 'GridSortingService', 'GridFilteringService', 'GridColumnService', 'SessionService', 'bhConstants', 'uiGridConstants', 'AccountStatementService', 'ModalService', 'LanguageService', 'GridExportService', - 'TransactionService', 'GridStateService', '$state', + 'TransactionService', 'GridStateService', '$state', 'AccountService', ]; /** @@ -19,7 +19,7 @@ AccountStatementController.$inject = [ function AccountStatementController( GeneralLedger, Notify, Journal, Sorting, Filtering, Columns, Session, bhConstants, uiGridConstants, AccountStatement, Modal, Languages, - GridExport, Transactions, GridState, $state + GridExport, Transactions, GridState, $state, Accounts ) { // global variables var vm = this; @@ -292,27 +292,47 @@ function AccountStatementController( vm.gridOptions.gridFooterTemplate = null; vm.gridOptions.showGridFooter = false; - vm.balances = {}; + vm.balances = { + opening : 0, + debits : 0, + credits : 0, + difference : 0, + }; + + // load the opening balance for the range + Accounts.getOpeningBalanceForPeriod(options.account_id, options) + .then(function (balances) { + vm.balances.opening = balances.balance; + vm.balances.ending = vm.balances.opening + vm.balances.difference; + }) + .catch(Notify.handleError); GeneralLedger.read(null, options) .then(function (data) { + var balances; + vm.gridOptions.data = data; // compute the difference between the debits and credits // TODO(@jniles) - is there a way to get this from ui grid's aggregation? - vm.balances = data.reduce(computeTransactionBalances, { + balances = data.reduce(computeTransactionBalances, { debits : 0, credits : 0, - balance : 0, + difference : 0, }); + vm.balances.debits = balances.debits; + vm.balances.credits = balances.credits; + vm.balances.difference = balances.difference; + vm.balances.ending = vm.balances.opening + balances.difference; + vm.gridOptions.showGridFooter = true; vm.gridOptions.gridFooterTemplate = '/modules/account_statement/grid.footer.html'; // @TODO investigate why footer totals aren't updated automatically on data change vm.gridApi.core.notifyDataChange(uiGridConstants.dataChange.ALL); }) - .catch(handleError) + .catch(Notify.handleError) .finally(toggleLoadingIndicator); } @@ -320,7 +340,7 @@ function AccountStatementController( function computeTransactionBalances(aggregates, row) { aggregates.debits += row.debit_equiv; aggregates.credits += row.credit_equiv; - aggregates.balance += (row.debit_equiv - row.credit_equiv); + aggregates.difference += (row.debit_equiv - row.credit_equiv); return aggregates; } diff --git a/client/src/modules/account_statement/grid.footer.html b/client/src/modules/account_statement/grid.footer.html index 94dfb41de6..9c23d9cd8c 100644 --- a/client/src/modules/account_statement/grid.footer.html +++ b/client/src/modules/account_statement/grid.footer.html @@ -1,5 +1,8 @@
({{grid.rows.length}} FORM.INFO.ROWS) + + FORM.LABELS.BEGINNING_VIEW_BALANCE: {{ grid.appScope.balances.opening | currency:grid.appScope.enterprise.currency_id }} + FORM.LABELS.DEBIT: {{ grid.appScope.balances.debits | currency:grid.appScope.enterprise.currency_id }} @@ -7,6 +10,6 @@ FORM.LABELS.CREDIT: {{ grid.appScope.balances.credits | currency:grid.appScope.enterprise.currency_id }} - FORM.LABELS.BALANCE: {{ grid.appScope.balances.balance | currency:grid.appScope.enterprise.currency_id }} + FORM.LABELS.DIFFERENCE: {{ grid.appScope.balances.difference | currency:grid.appScope.enterprise.currency_id }}
diff --git a/client/src/modules/accounts/accounts.service.js b/client/src/modules/accounts/accounts.service.js index ca9381b2d3..82303e64e2 100644 --- a/client/src/modules/accounts/accounts.service.js +++ b/client/src/modules/accounts/accounts.service.js @@ -2,7 +2,7 @@ angular.module('bhima.services') .service('AccountService', AccountService); AccountService.$inject = [ - 'PrototypeApiService', '$http', 'util', 'bhConstants', + 'PrototypeApiService', 'bhConstants', ]; /** @@ -10,7 +10,7 @@ AccountService.$inject = [ * * A service wrapper for the /accounts HTTP endpoint. */ -function AccountService(Api, $http, util, bhConstants) { +function AccountService(Api, bhConstants) { var baseUrl = '/accounts/'; var service = new Api(baseUrl); @@ -18,12 +18,27 @@ function AccountService(Api, $http, util, bhConstants) { service.label = label; service.getBalance = getBalance; + service.getOpeningBalanceForPeriod = getOpeningBalanceForPeriod; service.getChildren = getChildren; service.filterTitleAccounts = filterTitleAccounts; service.flatten = flatten; service.order = order; + /** + * @method getOpeningBalance + * + * + * @description + * This method exists to get the opening balance for parameters like those + * used to load a date range. + */ + function getOpeningBalanceForPeriod(id, options) { + var url = service.url.concat(id, '/openingBalance'); + return service.$http.get(url, { params : options }) + .then(service.util.unwrapHttpResponse); + } + /** * The read() method loads data from the api endpoint. If an id is provided, * the $http promise is resolved with a single JSON object, otherwise an array @@ -35,9 +50,7 @@ function AccountService(Api, $http, util, bhConstants) { * an array of JSONs. */ function read(id, options) { - var url = baseUrl.concat(id || ''); - return $http.get(url, { params : options }) - .then(util.unwrapHttpResponse) + return Api.read.call(this, id, options) .then(handleAccounts); } @@ -61,8 +74,8 @@ function AccountService(Api, $http, util, bhConstants) { function getBalance(accountId, opt) { var url = baseUrl.concat(accountId, '/balance'); - return $http.get(url, opt) - .then(util.unwrapHttpResponse); + return service.$http.get(url, opt) + .then(service.util.unwrapHttpResponse); } function filterTitleAccounts(accounts) { @@ -117,7 +130,7 @@ function AccountService(Api, $http, util, bhConstants) { */ function flatten(_tree, _depth) { var tree = _tree || []; - var depth = isNaN(_depth) ? -1 : _depth; + var depth = Number.isNaN(_depth) ? -1 : _depth; depth += 1; function handleTreeLevel(array, node) { @@ -140,7 +153,6 @@ function AccountService(Api, $http, util, bhConstants) { * @returns {Array} - the properly ordered list of account objects */ function order(accounts) { - // NOTE // we assume the root node is 0 var ROOT_NODE = 0; diff --git a/client/src/modules/general-ledger/general-ledger.service.js b/client/src/modules/general-ledger/general-ledger.service.js index 3b69f1d691..8d3c03742a 100644 --- a/client/src/modules/general-ledger/general-ledger.service.js +++ b/client/src/modules/general-ledger/general-ledger.service.js @@ -42,7 +42,5 @@ function GeneralLedgerService(Api, $httpParamSerializer, Languages) { return $httpParamSerializer(options); } - return service; } - diff --git a/server/config/routes.js b/server/config/routes.js index b3163179da..00c84fd881 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -154,6 +154,7 @@ exports.configure = function configure(app) { app.get('/accounts', accounts.list); app.get('/accounts/:id', accounts.detail); app.get('/accounts/:id/balance', accounts.getBalance); + app.get('/accounts/:id/openingBalance', accounts.getOpeningBalanceForPeriod); app.post('/accounts', accounts.create); app.put('/accounts/:id', accounts.update); app.delete('/accounts/:id', accounts.remove); diff --git a/server/controllers/finance/accounts/extra.js b/server/controllers/finance/accounts/extra.js index bca8a93d27..6105cc49a6 100644 --- a/server/controllers/finance/accounts/extra.js +++ b/server/controllers/finance/accounts/extra.js @@ -95,12 +95,12 @@ function getPeriodAccountBalanceUntilDate(accountId, date, fiscalYearId) { function getComputedAccountBalanceUntilDate(accountId, date, periodId, includeMaxDate) { const dateOperator = includeMaxDate ? '<=' : '<'; const sql = ` - SELECT + SELECT IFNULL(SUM(debit), 0) as debit, IFNULL(SUM(credit), 0) as credit, - IFNULL(SUM(debit_equiv - credit_equiv), 0) AS balance - FROM + IFNULL(SUM(debit_equiv - credit_equiv), 0) AS balance + FROM general_ledger - WHERE + WHERE account_id = ? AND DATE(trans_date) ${dateOperator} DATE(?) AND period_id = ?; @@ -129,11 +129,11 @@ function getOpeningBalanceForDate(accountId, date, includeMaxDate = true) { return getFiscalYearForDate(date) - // 1. sum period totals up to the current required period + // 1. Sum period totals up to the current required period .then(fiscalYearId => getPeriodAccountBalanceUntilDate(accountId, date, fiscalYearId)) - // 2. fetch the current dates period + // 2. Fetch the current dates period .then((previousPeriodClosing) => { balance += previousPeriodClosing.balance; credit += previousPeriodClosing.credit; @@ -142,7 +142,7 @@ function getOpeningBalanceForDate(accountId, date, includeMaxDate = true) { return getPeriodForDate(date); }) - // 3. calculate the sum of all general ledger transaction against this account + // 3. Calculate the sum of all general ledger transaction against this account // for the current period up to the current date .then(periodId => getComputedAccountBalanceUntilDate(accountId, date, periodId, includeMaxDate)) diff --git a/server/controllers/finance/accounts/index.js b/server/controllers/finance/accounts/index.js index 960837ab14..2a82662b5d 100644 --- a/server/controllers/finance/accounts/index.js +++ b/server/controllers/finance/accounts/index.js @@ -15,18 +15,24 @@ * @todo - move away from calling lookup() before action. This is an * unnecessary database request. * - * @requires db - * @requires NotFound + * @requires q + * @requires lib/db + * @requires lib/errors/NotFound + * @requires lib/errors/BadRequest * @requires accounts/types * @requires accounts/categories + * @requires lib/periods + * @requires accounts */ +const q = require('q'); const db = require('../../../lib/db'); -const NotFound = require('../../../lib/errors/NotFound'); -const BadRequest = require('../../../lib/errors/BadRequest'); +const { NotFound, BadRequest } = require('../../../lib/errors'); const types = require('./types'); const categories = require('./categories'); - +const Periods = require('../../../lib/period'); +const AccountExtras = require('./extra.js'); +const debug = require('debug')('accounts'); /** * @method create @@ -60,7 +66,7 @@ function create(req, res, next) { * PUT /accounts/:id */ function update(req, res, next) { - const id = req.params.id; + const { id } = req.params; const data = req.body; const sql = 'UPDATE account SET ? WHERE id = ?'; @@ -137,10 +143,12 @@ function list(req, res, next) { // convert locked to a number if it exists if (req.query.locked) { locked = Number(req.query.locked); + } else { + locked = 0; } // if locked is a number, filter on it - if (!isNaN(locked)) { + if (!Number.isNaN(locked)) { sql += ` WHERE a.locked = ${locked}`; } @@ -183,7 +191,7 @@ function detail(req, res, next) { * GET /accounts/:id/balance */ function getBalance(req, res, next) { - const id = req.params.id; + const { id } = req.params; let optional = ''; const params = [id]; @@ -226,6 +234,83 @@ function getBalance(req, res, next) { .done(); } +/** + * @function getFirstDateOfFirstFiscalYear + * + * @description + * returns the start date of the very first fiscal year for the provided + * enterprise. + * + * @TODO - move this to the fiscal controller with other AccountExtra functions. + */ +function getFirstDateOfFirstFiscalYear(enterpriseId) { + const sql = ` + SELECT start_date FROM fiscal_year + WHERE enterprise_id = ? + ORDER BY DATE(start_date) + LIMIT 1; + `; + + return db.one(sql, enterpriseId); +} + + +/** + * @function getOpeningBalanceForPeriod + * + * @description + * Computes the opening balance for an account based on the default period range + * provided by default filters. This is useful for registries. If you know + * what the date key is, it is better to call getOpeningBalanceForDate() from + * the AccountExtras directly with the account id and date. + */ +function getOpeningBalanceForPeriod(req, res, next) { + const period = new Periods(req.query.client_timestamp); + const targetPeriod = period.lookupPeriod(req.query.period); + const accountId = req.params.id; + + debug( + '#getOpeningBalanceForPeriod() finding opening balance for account %s on period %s', + accountId, + req.query.period + ); + + let promise = q(); + + switch (targetPeriod) { + case period.periods.allTime: + debug('#getOpeningBalanceForPeriod() all time period detected. Using first fiscal year start date.'); + promise = promise + .then(() => getFirstDateOfFirstFiscalYear(req.session.enterprise.id)) + .then(fiscal => fiscal.start_date); + break; + + case period.periods.custom: + debug('#getOpeningBalanceForPeriod() custom period detected. Using custom_period_start key.'); + promise = promise + .then(() => new Date(req.query.custom_period_start)); + break; + + default: + debug('#getOpeningBalanceForPeriod() %s period detected. Using computed start date.', req.query.period); + promise = promise + .then(() => targetPeriod.limit.start()); + break; + } + + promise + .then(date => { + debug(`#getOpeningBalanceForPeriod() computed ${date} for start date.`); + return AccountExtras.getOpeningBalanceForDate(accountId, new Date(date)); + }) + .then(balances => { + debug('#getOpeningBalanceForPeriod() computed %j balances for account id %s.', balances, accountId); + res.status(200).json(balances); + }) + .catch(next) + .done(); +} + /** * @method lookupAccount * @@ -307,7 +392,7 @@ function getChildren(accounts, parentId) { * @param {number} depth A depth */ function flatten(tree, depth) { - let currentDepth = isNaN(depth) ? -1 : depth; + let currentDepth = Number.isNaN(depth) ? -1 : depth; currentDepth += 1; return tree.reduce((array, node) => { @@ -327,4 +412,5 @@ exports.lookupAccount = lookupAccount; exports.processAccountDepth = processAccountDepth; exports.list = list; exports.remove = remove; +exports.getOpeningBalanceForPeriod = getOpeningBalanceForPeriod; exports.categories = categories;