diff --git a/client/src/i18n/en/report.json b/client/src/i18n/en/report.json index d79448c2ae..cbb1b43091 100644 --- a/client/src/i18n/en/report.json +++ b/client/src/i18n/en/report.json @@ -68,7 +68,10 @@ "PROFIT_AND_LOSS_BY_MONTH": "Profit and Loss Statement By Month", "PROFIT_AND_LOSS_BY_YEAR": "Profit and Loss Statement By Fiscal Year", "INCOME_REPORT": "Income Report", - "MONTHLY_BALANCE": "Monthly Balance", + "MONTHLY_BALANCE": { + "DESCRIPTION": "This report is used to analyze the balance of accounts on a monthly basis. This analysis shows the total debits and credits as well as the total balances for each account.", + "TITLE": "Monthly Balance" + }, "OPEN_DEBTORS": { "DESCRIPTION": "This report shows the debtors that have a standing debt with the institution, and gives some control over the sort order. For advanced analysis, the report can add in the last invoice date and last payment date, as well as limit the scope of the search by a date.", "TITLE": "Debtors with Unpaid Debts", diff --git a/client/src/i18n/en/tree.json b/client/src/i18n/en/tree.json index 693943b850..d107415d3b 100644 --- a/client/src/i18n/en/tree.json +++ b/client/src/i18n/en/tree.json @@ -67,6 +67,7 @@ "IPR_TAX_CONFIGURATION" : "IPR Tax Configuration", "JOURNAL_VOUCHER" : "Journal Voucher", "LOCATION" : "Location Management", + "MONTHLY_BALANCE": "Monthly Balance", "MULTI_PAYROLL" : "Multiple Payroll", "OHADA_BILAN":"[OHADA] Bilan", "OHADA_PROFIT_LOSS" : "[OHADA] Profit and Loss", diff --git a/client/src/i18n/fr/report.json b/client/src/i18n/fr/report.json index 246d5b2db6..0620acf0c3 100644 --- a/client/src/i18n/fr/report.json +++ b/client/src/i18n/fr/report.json @@ -62,7 +62,10 @@ "PROFIT_AND_LOSS_BY_MONTH": "Rapport des Produit et des Charges par Mois", "PROFIT_AND_LOSS_BY_YEAR": "Rapport des Produit et des Charges par Années", "INCOME_REPORT" : "Rapport des Recettes", - "MONTHLY_BALANCE" : "Balance Mensuelle", + "MONTHLY_BALANCE": { + "DESCRIPTION": "Ce rapport permet de faire l'analyse mensuel de la balance des comptes, cette analyse permet de visualiser le total des debits et des des crédits ainsi que le totals des soldes pour chaque comptes", + "TITLE": "Analyse mensuel de la Balance" + }, "OPEN_DEBTORS": { "TITLE" : "Débiteurs endettés", "TREE" : "Dettes des Débiteurs", diff --git a/client/src/i18n/fr/tree.json b/client/src/i18n/fr/tree.json index 80da5d88bc..e19248ceda 100644 --- a/client/src/i18n/fr/tree.json +++ b/client/src/i18n/fr/tree.json @@ -67,6 +67,7 @@ "IPR_TAX_CONFIGURATION": "Configuration de la taxe IPR", "JOURNAL_VOUCHER":"Ajout Transactions", "LOCATION":"Localisations", + "MONTHLY_BALANCE": "Analyse mensuel de la Balance", "MULTI_PAYROLL" : "Payroll Multiple", "OFFDAYS_MANAGEMENT" : "Gestion des jours fériés", "OHADA_BILAN":"[OHADA] Bilan", diff --git a/client/src/modules/reports/generate/monthlyBalance/monthlyBalance.config.js b/client/src/modules/reports/generate/monthlyBalance/monthlyBalance.config.js new file mode 100644 index 0000000000..fc7098bc73 --- /dev/null +++ b/client/src/modules/reports/generate/monthlyBalance/monthlyBalance.config.js @@ -0,0 +1,87 @@ +angular.module('bhima.controllers') + .controller('monthlyBalanceController', MonthlyBalanceController); + +MonthlyBalanceController.$inject = [ + '$sce', 'NotifyService', 'BaseReportService', 'AppCache', 'reportData', '$state', 'AccountService', +]; + +function MonthlyBalanceController($sce, Notify, SavedReports, AppCache, reportData, $state, Accounts) { + const vm = this; + const cache = new AppCache('monthlyBalance'); + const reportUrl = 'reports/finance/monthly_balance'; + + vm.previewGenerated = false; + vm.reportDetails = {}; + + Accounts.read() + .then(elements => { + // bind the accounts to the controller + const accounts = Accounts.order(elements); + vm.accounts = accounts; + }); + + vm.onSelectFiscalYear = (fiscalYear) => { + vm.reportDetails.fiscal_id = fiscalYear.id; + }; + + vm.onSelectPeriod = (period) => { + vm.reportDetails.period_id = period.id; + vm.reportDetails.periodLabel = period.hrLabel; + }; + + vm.clearPreview = function clearPreview() { + vm.previewGenerated = false; + vm.previewResult = null; + }; + + vm.preview = function preview(form) { + if (form.$invalid) { + Notify.danger('FORM.ERRORS.RECORD_ERROR'); + return 0; + } + + if (vm.account) { + vm.reportDetails.accountNumber = vm.account.number; + vm.reportDetails.accountLabel = vm.account.label; + vm.reportDetails.accountId = vm.account.id; + } + + if (vm.reportDetails.allAccount) { + vm.reportDetails.accountNumber = null; + vm.reportDetails.accountLabel = null; + vm.reportDetails.accountId = null; + } + + // update cached configuration + cache.reportDetails = angular.copy(vm.reportDetails); + + return SavedReports.requestPreview(reportUrl, reportData.id, angular.copy(vm.reportDetails)) + .then(result => { + vm.previewGenerated = true; + vm.previewResult = $sce.trustAsHtml(result); + }) + .catch(Notify.handleError); + }; + + vm.requestSaveAs = function requestSaveAs() { + const options = { + url : reportUrl, + report : reportData, + reportOptions : angular.copy(vm.reportDetails), + }; + + return SavedReports.saveAsModal(options) + .then(() => { + $state.go('reportsBase.reportsArchive', { key : options.report.report_key }); + }) + .catch(Notify.handleError); + }; + + checkCachedConfiguration(); + + function checkCachedConfiguration() { + if (cache.reportDetails) { + vm.reportDetails = angular.copy(cache.reportDetails); + } + } +} diff --git a/client/src/modules/reports/generate/monthlyBalance/monthlyBalance.html b/client/src/modules/reports/generate/monthlyBalance/monthlyBalance.html new file mode 100644 index 0000000000..5074f420f0 --- /dev/null +++ b/client/src/modules/reports/generate/monthlyBalance/monthlyBalance.html @@ -0,0 +1,102 @@ + + + +
+
+
+

REPORT.MONTHLY_BALANCE.TITLE

+

REPORT.MONTHLY_BALANCE.DESCRIPTION

+
+
+ +
+
+
+
+ REPORT.UTIL.OPTIONS +
+
+
+ + + + + + + +
+
+ +
+
+ +
+
+
+
+
+ +
+
+ + + + + {{$select.selected.number}} {{$select.selected.label}} + + + + + + + +
+
+
+
+
+ + + REPORT.UTIL.PREVIEW + +
+
+
+
+
+
\ No newline at end of file diff --git a/client/src/modules/reports/reports.routes.js b/client/src/modules/reports/reports.routes.js index 6d95f7c7b1..56f2144cf8 100644 --- a/client/src/modules/reports/reports.routes.js +++ b/client/src/modules/reports/reports.routes.js @@ -34,6 +34,7 @@ angular.module('bhima.routes') 'breakEvenFeeCenter', 'indicatorsReport', 'visit_report', + 'monthlyBalance', ]; $stateProvider diff --git a/server/config/routes.js b/server/config/routes.js index 0b9583f857..85d962ca50 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -399,6 +399,7 @@ exports.configure = function configure(app) { app.get('/reports/finance/income_expense_by_year', financeReports.income_expense_by_year.document); app.get('/reports/finance/cash_report', financeReports.cashReport.document); app.get('/reports/finance/balance', financeReports.balance.document); + app.get('/reports/finance/monthly_balance', financeReports.monthlyBalance.document); app.get('/reports/finance/account_report', financeReports.reportAccounts.document); app.get('/reports/finance/account_report_multiple', financeReports.reportAccountsMultiple.document); app.get('/reports/finance/journal', financeReports.journal.postingReport); diff --git a/server/controllers/finance/reports/index.js b/server/controllers/finance/reports/index.js index 066108f1fe..5c408f83ad 100644 --- a/server/controllers/finance/reports/index.js +++ b/server/controllers/finance/reports/index.js @@ -35,3 +35,4 @@ exports.annualClientsReport = require('./debtors/annual-clients-report').annualC exports.breakEven = require('./break_even'); exports.breakEvenFeeCenter = require('./break_even_fee_center'); exports.operating = require('./operating'); +exports.monthlyBalance = require('./monthlyBalance'); diff --git a/server/controllers/finance/reports/monthlyBalance/index.js b/server/controllers/finance/reports/monthlyBalance/index.js new file mode 100644 index 0000000000..68ee211925 --- /dev/null +++ b/server/controllers/finance/reports/monthlyBalance/index.js @@ -0,0 +1,176 @@ + +const q = require('q'); +const _ = require('lodash'); +const db = require('../../../../lib/db'); +const util = require('../../../../lib/util'); +const Tree = require('../../../../lib/Tree'); +const ReportManager = require('../../../../lib/ReportManager'); + +const fiscal = require('../../fiscal'); + +const TEMPLATE = './server/controllers/finance/reports/monthlyBalance/report.handlebars'; + +exports.document = document; +exports.formatData = formatData; +exports.reporting = reporting; + +const DECIMAL_PRECISION = 2; // ex: 12.4567 => 12.46 + +/** + * @description this function helps to get html document of the report in server side + * so that we can use it with others modules on the server side + * @param {*} options the report options + * @param {*} session the session + */ +function reporting(opts, session) { + const params = opts; + params.currency_id = 2; + params.allAccount = parseInt(params.allAccount, 10); + const accountNumber = params.allAccount ? `` : params.accountNumber; + const accountLabel = params.allAccount ? `` : params.accountLabel; + + let docReport; + const options = _.extend(opts, { + filename : 'FORM.LABELS.MONTHLY_BALANCE', + csvKey : 'rows', + user : session.user, + }); + + try { + docReport = new ReportManager(TEMPLATE, session, options); + } catch (e) { + throw e; + } + + let queries; + let range; + + const periods = { + periodFrom : params.period_id, + periodTo : params.period_id, + }; + + return fiscal.getDateRangeFromPeriods(periods).then(dateRange => { + range = dateRange; + const sqlParams = [ + params.fiscal_id, + params.period_id, + ]; + + let filterByAccount; + if (accountNumber) { + filterByAccount = selectAccountParent(accountNumber); + } else { + filterByAccount = ''; + } + + const sql = ` + SELECT ac.id, ac.number, ac.label, ac.parent, s.debit, s.credit, s.amount, s.type_id + FROM account as ac LEFT JOIN ( + SELECT pt.debit, pt.credit, SUM(pt.debit - pt.credit) as amount, pt.account_id, act.id as type_id + FROM period_total as pt + JOIN account as a ON a.id = pt.account_id + JOIN account_type as act ON act.id = a.type_id + JOIN period as p ON p.id = pt.period_id + JOIN fiscal_year as fy ON fy.id = p.fiscal_year_id + WHERE fy.id = ? AND pt.period_id = ? + GROUP BY pt.account_id + )s ON ac.id = s.account_id + WHERE ac.locked = 0 ${filterByAccount} + ORDER BY ac.number; + `; + + const sqlSum = ` + SELECT p.id, p.start_date, SUM(pt.debit) AS debit, SUM(pt.credit) AS credit + FROM period_total AS pt + JOIN fiscal_year AS f ON f.id = pt.fiscal_year_id + JOIN period AS p ON p.id = pt.period_id + WHERE pt.fiscal_year_id = ? AND pt.period_id = ? + `; + + queries = [ + db.exec(sql, sqlParams), + db.one(sqlSum, sqlParams), + ]; + + return q.all(queries); + }) + .spread((exploitation, totalExploitation) => { + const context = { + exploitation : prepareTree(exploitation, 'amount', 'debit', 'credit'), + totalExploitation, + dateFrom : range.dateFrom, + dateTo : range.dateTo, + periodLabel : params.periodLabel, + currencyId : params.currency_id, + accountLabel, + accountNumber, + allAccount : params.allAccount, + }; + + formatData(context.exploitation, context.totalExploitation, DECIMAL_PRECISION); + + return docReport.render(context); + }); +} + +function document(req, res, next) { + reporting(req.query, req.session) + .then((result) => { + res.set(result.headers).send(result.report); + }) + .catch(next) + .done(); +} + +function selectAccountParent(account) { + let sqlFilter = ``; + const accountArray = account.split(''); + + if (accountArray.length) { + let accountFilter = ``; + if (accountArray.length > 1) { + for (let i = 0; i < accountArray.length; i++) { + accountFilter += `${accountArray[i]}`; + const conditionOr = (i < (accountArray.length - 1)) ? `OR` : ``; + sqlFilter += `ac.number = '${accountFilter}' ${conditionOr} `; + } + sqlFilter = `OR (${sqlFilter})`; + } + sqlFilter = ` AND (ac.number LIKE '${account}%' ${sqlFilter})`; + } + + return sqlFilter; +} + +// create the tree structure, filter by property and sum nodes' summableProp +function prepareTree(data, amount, debit, credit) { + const tree = new Tree(data); + + try { + tree.walk(Tree.common.sumOnProperty(amount), false); + tree.walk(Tree.common.sumOnProperty(debit), false); + tree.walk(Tree.common.sumOnProperty(credit), false); + tree.walk(Tree.common.computeNodeDepth); + return tree.toArray(); + } catch (error) { + return []; + } + +} + +// set the percentage of each amoun's row, +// round amounts +function formatData(result, total, decimalPrecision) { + const _total = (total === 0) ? 1 : total; + return result.forEach(row => { + + row.title = (row.depth < 3); + + if (row.title) { + row.percent = util.roundDecimal(Math.abs((row.amount / _total) * 100), decimalPrecision); + } + + row.amount = util.roundDecimal(row.amount, decimalPrecision); + }); +} diff --git a/server/controllers/finance/reports/monthlyBalance/report.handlebars b/server/controllers/finance/reports/monthlyBalance/report.handlebars new file mode 100644 index 0000000000..1659c671b8 --- /dev/null +++ b/server/controllers/finance/reports/monthlyBalance/report.handlebars @@ -0,0 +1,58 @@ +{{> head title="{{translate 'REPORT.MONTHLY_BALANCE.TITLE' }}" }} + +
+ {{> header}} + + +
+
+ +

{{translate 'REPORT.MONTHLY_BALANCE.TITLE'}}

+

{{periodLabel}}

+ {{#if this.accountNumber}} +

{{this.accountNumber}}: {{this.accountLabel}}

+ {{/if}} + + + + + + + +
+ + + + + + + + + + {{#each this.exploitation}} + {{#if amount}} + + + + + + + + {{/if}} + {{/each}} + + {{#if allAccount}} + + + + + + + {{/if}} +
{{translate 'FORM.LABELS.ACCOUNT_NUMBER'}}{{translate 'FORM.LABELS.LABEL'}}{{translate 'FORM.LABELS.DEBIT'}}{{translate 'FORM.LABELS.CREDIT'}}{{translate 'FORM.LABELS.AMOUNT'}}
{{number}} + {{label}} + {{debcred debit ../currencyId}}{{debcred credit ../currencyId}}{{debcred amount ../currencyId}}
{{translate 'FORM.LABELS.TOTAL'}}{{debcred totalExploitation.debit ./currencyId}}{{debcred this.totalExploitation.credit ./currencyId}}
+
+
+
+
\ No newline at end of file diff --git a/server/models/bhima.sql b/server/models/bhima.sql index afecfac422..dee0da1699 100644 --- a/server/models/bhima.sql +++ b/server/models/bhima.sql @@ -127,7 +127,8 @@ INSERT INTO unit VALUES (240, '[Stock] Stock Entry Report','TREE.STOCK_ENTRY_REPORT','Stock Entry Report', 144,'/modules/reports/generated/stock_entry','/reports/stock_entry'), (241, 'Entity Folder', 'ENTITY.MANAGEMENT', 'Entity Folder', 0, '/modules/entities', '/ENTITY_FOLDER'), (242, 'Entity Management','ENTITY.MANAGEMENT','',241,'/modules/entities','/entities'), - (243, 'Entity Group', 'ENTITY.GROUP.TITLE', 'Entity Group', 241, '/modules/entity_group', '/entity_group'); + (243, 'Entity Group', 'ENTITY.GROUP.TITLE', 'Entity Group', 241, '/modules/entity_group', '/entity_group'), + (244, 'Monthly Balance', 'TREE.MONTHLY_BALANCE', 'Monthly Balance', 144, '/modules/reports/monthlyBalance', '/reports/monthlyBalance'); -- Reserved system account type INSERT INTO `account_category` VALUES @@ -178,7 +179,8 @@ INSERT INTO `report` (`id`, `report_key`, `title_key`) VALUES (30, 'breakEvenFeeCenter', 'TREE.BREAK_EVEN_FEE_CENTER_REPORT'), (31, 'indicatorsReport', 'TREE.INDICATORS_REPORT'), (32, 'visit_report', 'PATIENT_RECORDS.REPORT.VISITS'), - (33, 'stock_entry', 'REPORT.STOCK.ENTRY_REPORT'); + (33, 'stock_entry', 'REPORT.STOCK.ENTRY_REPORT'), + (34, 'monthlyBalance', 'REPORT.MONTHLY_BALANCE.TITLE'); -- Supported Languages INSERT INTO `language` VALUES diff --git a/server/models/migrations/next/migrations.sql b/server/models/migrations/next/migrations.sql index d53f71b853..162fc95ef1 100644 --- a/server/models/migrations/next/migrations.sql +++ b/server/models/migrations/next/migrations.sql @@ -336,3 +336,15 @@ INSERT INTO unit VALUES INSERT INTO `report` (`id`, `report_key`, `title_key`) VALUES (33, 'stock_entry', 'REPORT.STOCK.ENTRY_REPORT'); + +/* + @author:lomamech + @date: 2019-07-20 + @description: This report allows for monthly analysis of accounts +*/ +INSERT INTO unit VALUES + (244, 'Monthly Balance', 'TREE.MONTHLY_BALANCE', 'Monthly Balance', 144, '/modules/reports/monthlyBalance', '/reports/monthlyBalance'); + +INSERT INTO `report` (`id`, `report_key`, `title_key`) VALUES + (34, 'monthlyBalance', 'REPORT.MONTHLY_BALANCE.TITLE'); +