Skip to content

Commit

Permalink
feature(Budget Report)
Browse files Browse the repository at this point in the history
- Implementation of the budget report, allowing the selection of the fiscal year and the number of previous years for result analysis.
- Retrieving the values for the periods, excluding period 0, the closing period 13, as well as hidden and blocked accounts.
- Correction of the restriction during the compilation of securities accounts for non-budgeted accounts.

closes Third-Culture-Software#7683
  • Loading branch information
lomamech committed Sep 17, 2024
1 parent c74aa67 commit 6ddd727
Show file tree
Hide file tree
Showing 16 changed files with 677 additions and 15 deletions.
17 changes: 17 additions & 0 deletions client/src/i18n/en/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@
"GRAND_TOTAL": "Grand Total",
"MEDICATION_COSTS" : "Medication Costs"
},
"BUDGET_REPORT":{
"TITLE" : "Budget Report",
"COMPLETION_RATE" : "Completion rate",
"DEFAULT_SETTING": "Default Setting",
"DESCRIPTION" : "The Budget Management Report is a comprehensive tool designed to provide detailed insights into the financial planning and allocation processes.",
"DISPLAY_ALL_ACCOUNTS" : "Display all accounts",
"EXPENSES": "Expenses",
"HIDE_TITLE_ACCOUNT": "Hide Title Account",
"HIDE_UNUSED_ACCOUNTS": "Hide Unused Accounts or Accounts with Zero Values",
"MAX_5_YEAR": "Please note that the budget analysis period is limited to a maximum of 5 years.",
"PERCENTAGE_VARIATION_COMPARED": "Percentage Variation Compared to the Budget",
"REALIZATION" : "Realization",
"REVENUS": "Revenus",
"SET_NUMBER_YEAR": "Set the Number of Years for Analysis",
"SHOW_ONLY_TITLE_ACCOUNT": "Display Only the Title Account",
"VARIATION_IN_AMOUNT": "Variation in Amount"
},
"BY_ASC": "By Ascending Order",
"BY_DESC": "By Descending Order",
"BALANCE": "Balance Report",
Expand Down
1 change: 1 addition & 0 deletions client/src/i18n/en/tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"BREAK_EVEN_REFERENCE":"Break-even references",
"BREAK_EVEN_REPORT": "Break-even Report",
"BUDGET": "Budget Management",
"BUDGET_REPORT": "Budget Report",
"CASHBOX_MANAGEMENT" : "Cashbox Management",
"CASHFLOW_BY_SERVICE" : "Cashflow by Service",
"CASHFLOW" : "Statement of Cash Flows",
Expand Down
4 changes: 2 additions & 2 deletions client/src/i18n/fr/budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"BUDGET_YTD_COLUMN": "Budget",
"BUDGET_YTD_SUBTITLE": "(CDA)",
"BUDGET_YTD_TOOLTIP": "Budget cumul de l'année (CDA)",
"EXPENSES_TOTAL": "Dépenses totales",
"EXPENSES_TOTAL": "Total des Dépenses",
"DEVIATION_YTD": "Déviation % (CDA)",
"DEVIATION_YTD_TOOLTIP": "Déviation % (CDA) = 100*(Réels_CDA/budget_CDA)",
"DIFFERENCE_YTD": "Différence (CDA)",
Expand All @@ -26,7 +26,7 @@
"IMPORT_BUDGET_ERROR_LOCKED_ACCOUNT": "ERREUR : L'importation du budget a échoué ! Les données d'importation du budget comprennent un compte bloqué. Veuillez le supprimer et réessayer :",
"IMPORT_BUDGET_ERROR_NEGATIVE_BUDGET_VALUE": "ERREUR : L'importation du budget a échoué ! La valeur du budget est négative.",
"IMPORT_BUDGET_WARN_TITLE_BUDGET_IGNORED": "ATTENTION : Les valeurs budgétaires des comptes titres sont ignorées.",
"INCOME_TOTAL": "Revenu totales",
"INCOME_TOTAL": "Total des Revenus",
"TOTAL_SUMMARY": "Résumé des totaux (réel - budget)",
"ACTUALS": {
"JANUARY": "Réalisations de janvier",
Expand Down
17 changes: 17 additions & 0 deletions client/src/i18n/fr/report.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@
"GRAND_TOTAL": "Total général",
"MEDICATION_COSTS" : "Coûts des médicaments"
},
"BUDGET_REPORT":{
"TITLE" : "Rapport Budgetaire",
"COMPLETION_RATE" : "Taux de réalisation",
"DEFAULT_SETTING": "Paramètre par défaut",
"DESCRIPTION" : "Le rapport de gestion budgétaire est un outil complet conçu pour fournir des informations détaillées sur les processus de planification et d'allocation financières.",
"DISPLAY_ALL_ACCOUNTS" : "Afficher tous les comptes",
"EXPENSES": "Dépenses",
"HIDE_TITLE_ACCOUNT": "Cacher le compte de titre",
"HIDE_UNUSED_ACCOUNTS": "Cacher le compte non utilisé ou dont les valeurs sont égales à zéro",
"MAX_5_YEAR": "Veuillez noter que la période d'analyse budgétaire est limitée à un maximum de 5 années.",
"PERCENTAGE_VARIATION_COMPARED": "Variation en pourcentage par rapport au budget",
"REALIZATION" : "Réalisation",
"REVENUS": "Revenus",
"SET_NUMBER_YEAR": "Définir le Nombre d'années pour l'Analyse",
"SHOW_ONLY_TITLE_ACCOUNT": "Afficher uniquement le compte de titre",
"VARIATION_IN_AMOUNT": "Variation en chiffre"
},
"BY_ASC": "Par ordre croissant",
"BY_DESC": "Par ordre décroissant",
"BALANCE": "Rapport de la Balance",
Expand Down
1 change: 1 addition & 0 deletions client/src/i18n/fr/tree.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"BREAK_EVEN_REFERENCE":"Références du seuil de rentabilité",
"BREAK_EVEN_REPORT": "Rapport du seuil de rentabilité",
"BUDGET": "Gestion budgétaire",
"BUDGET_REPORT": "Rapport Budgétaire",
"CASHBOX_MANAGEMENT":"Caisses et Banques",
"CASHFLOW": "Flux de Trésorerie",
"CASHFLOW_BY_SERVICE": "Journal de Ventilation",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
angular.module('bhima.controllers')
.controller('budget_reportController', BudgetReportController);

BudgetReportController.$inject = [
'$sce', 'NotifyService', 'BaseReportService', 'AppCache', 'reportData', '$state', 'SessionService',
];

function BudgetReportController($sce, Notify, SavedReports, AppCache, reportData, $state, Session) {
const vm = this;
const cache = new AppCache('configure_budget_report');
const reportUrl = 'reports/finance/budget_report';

vm.reportDetails = {
currency_id : Session.enterprise.currency_id,
set_number_year : 1,
filter : 'default',
};

vm.previewGenerated = false;
checkCachedConfiguration();

vm.onSelectFiscalYear = (fiscalYear) => {
vm.reportDetails.fiscal_id = fiscalYear.id;
};

vm.onSelectCurrency = (currency) => {
vm.reportDetails.currency_id = currency.id;
};

vm.onSelectCronReport = report => {
vm.reportDetails = angular.copy(report);
};

vm.numberYears = [
{ id : 1 }, { id : 2 }, { id : 3 }, { id : 4 }, { id : 5 },
];

vm.preview = function preview(form) {
if (form.$invalid) { return 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.clearPreview = function clearPreview() {
vm.previewGenerated = false;
vm.previewResult = null;
};

vm.clear = (value) => {
delete vm.reportDetails[value];
};

vm.filterBudget = [
{ value : 'default', label : 'REPORT.BUDGET_REPORT.DISPLAY_ALL_ACCOUNTS' },
{ value : 'hide_title', label : 'REPORT.BUDGET_REPORT.HIDE_TITLE_ACCOUNT' },
{ value : 'show_title', label : 'REPORT.BUDGET_REPORT.SHOW_ONLY_TITLE_ACCOUNT' },
];

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);
};

function checkCachedConfiguration() {
if (cache.reportDetails) {
vm.reportDetails = angular.copy(cache.reportDetails);
}
vm.reportDetails.type = 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<bh-report-preview
ng-if="ReportConfigCtrl.previewGenerated"
source-document="ReportConfigCtrl.previewResult"
on-clear-callback="ReportConfigCtrl.clearPreview()"
on-save-callback="ReportConfigCtrl.requestSaveAs()">
</bh-report-preview>

<div ng-show="!ReportConfigCtrl.previewGenerated">
<div class="row">
<div class="col-md-12">
<h3 class="text-capitalize" translate>REPORT.BUDGET_REPORT.TITLE</h3>
<p class="text-info" translate>REPORT.BUDGET_REPORT.DESCRIPTION</p>
</div>
</div>

<div class="row" style="margin-top : 10px">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<span translate>REPORT.UTIL.OPTIONS</span>
</div>

<div class="panel-body">
<form name="ConfigForm" bh-submit="ReportConfigCtrl.preview(ConfigForm)" novalidate>
<bh-fiscal-year-select
fiscal-id="ReportConfigCtrl.reportDetails.fiscal_id"
on-select-fiscal-callback="ReportConfigCtrl.onSelectFiscalYear(fiscalYear)"
required="true">
</bh-fiscal-year-select>

<div class="form-group" ng-class="{'has-error' : ConfigForm.set_number_year.$invalid && ConfigForm.$submitted}">
<label class="control-label" translate>REPORT.BUDGET_REPORT.SET_NUMBER_YEAR</label>
<bh-clear on-clear="ReportConfigCtrl.clear('id')"></bh-clear>
<ui-select
name="set_number_year"
ng-model="ReportConfigCtrl.reportDetails.set_number_year">

<ui-select-match placeholder="{{ 'REPORT.BUDGET_REPORT.MAX_5_YEAR' | translate }}">
<span>{{$select.selected.id}}</span>
</ui-select-match>

<ui-select-choices repeat="year.id as year in ReportConfigCtrl.numberYears | filter:{id: $select.search}">
<strong ng-bind-html="year.id | highlight:$select.search"></strong>
</ui-select-choices>
</ui-select>
<div class="help-block" ng-messages="ConfigForm.id.$error" ng-show="ConfigForm.$submitted">
<div ng-messages-include="modules/templates/messages.tmpl.html"></div>
</div>
</div>

<div class="form-group" ng-class="{ 'has-error' : ConfigForm.$submitted && ConfigForm.budgetFilter.$invalid }">
<div ng-repeat="budgetFilter in ReportConfigCtrl.filterBudget" class="radio">
<label>
<input
name="filter"
type="radio"
ng-model="ReportConfigCtrl.reportDetails.filter"
ng-value="budgetFilter.value"
data-report-format-option="{{ budgetFilter.value }}"
required>
<span translate>{{budgetFilter.label}}</span>
</label>
</div>

<div class="help-block" ng-messages="ConfigForm.filter.$error" ng-show="ConfigForm.$submitted">
<div ng-messages-include="modules/templates/messages.tmpl.html"></div>
</div>
</div>

<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="ReportConfigCtrl.reportDetails.hide_unused" ng-true-value="1" ng-false-value="0">
<span translate>REPORT.BUDGET_REPORT.HIDE_UNUSED_ACCOUNTS</span>
</label>
</div>
</div>

<bh-loading-button loading-state="ConfigForm.$loading">
<span translate>REPORT.UTIL.PREVIEW</span>
</bh-loading-button>
</form>
</div>
</div>
</div>

<div class="col-md-6">
<bh-cron-email-report
report-key="operating"
report-form="ConfigForm"
report-details="ReportConfigCtrl.reportDetails"
on-select-report="ReportConfigCtrl.onSelectCronReport(report)">
</bh-cron-email-report>
</div>
</div>
</div>
1 change: 1 addition & 0 deletions client/src/modules/reports/reports.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ angular.module('bhima.routes')
'system_usage_stat',
'unpaid_invoice_payments',
'visit_report',
'budget_report',
];

function resolveReportData($stateParams, SavedReports) {
Expand Down
1 change: 1 addition & 0 deletions server/config/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ exports.configure = function configure(app) {

app.get('/reports/finance/analysis_auxiliary_cashboxes', financeReports.analysisAuxiliaryCashboxes.report);
app.get('/reports/finance/configurable_analysis_report', financeReports.configurableAnalysisReport.report);
app.get('/reports/finance/budget_report', financeReports.budget_analytical.report);

// visits reports
app.get('/reports/visits', medicalReports.visitsReports.document);
Expand Down
30 changes: 22 additions & 8 deletions server/controllers/finance/budget/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ function buildBudgetData(fiscalYearId) {
let accounts;
let periodActuals;

// Get basic info on all relevant accounts
// Get basic info on all relevant accounts excluding hidden and blocked accounts.
const accountsSql = `
SELECT
a.id, a.number, a.label, a.locked, a.type_id,
a.parent, a.locked, a.hidden
FROM account AS a
WHERE a.type_id in (${allowedTypes}) AND a.locked = 0;
WHERE a.type_id in (${allowedTypes}) AND a.locked = 0 AND a.hidden = 0;
`;

// First get the basic account and FY budget data (if available)
Expand All @@ -105,9 +105,10 @@ function buildBudgetData(fiscalYearId) {
JOIN period AS p ON p.id = b.period_id
WHERE p.number = 0 and p.fiscal_year_id = ?
) AS bdata ON bdata.account_id = a.id
WHERE a.type_id in (${INCOME}, ${EXPENSE}) AND a.locked = 0;
WHERE a.type_id in (${INCOME}, ${EXPENSE}) AND a.locked = 0 AND a.hidden = 0;
`;

// Retrieving the values for the periods, excluding period 0 and the closing period 13
const actualsSql = `
SELECT
a.id,
Expand All @@ -116,17 +117,21 @@ function buildBudgetData(fiscalYearId) {
FROM period_total pt
JOIN account AS a ON a.id = pt.account_id
JOIN account_type AS at ON at.id = a.type_id
JOIN period AS p ON p.id = pt.period_id
WHERE pt.fiscal_year_id = ? AND a.type_id in (${INCOME}, ${EXPENSE}) AND a.locked = 0
AND a.hidden = 0 AND (p.number <> 0 AND p.number <> 13)
GROUP BY a.id;
`;

// Retrieving the values for the periods, excluding period 0 and the closing period 13
const periodActualsSql = `
SELECT a.id, pt.debit, pt.credit, p.number AS periodNum
FROM period_total pt
JOIN period AS p ON p.id = pt.period_id
JOIN account AS a ON a.id = pt.account_id
JOIN account_type AS at ON at.id = a.type_id
WHERE pt.fiscal_year_id = ? AND a.type_id in (${INCOME}, ${EXPENSE}) AND a.locked = 0;
WHERE pt.fiscal_year_id = ? AND a.type_id in (${INCOME}, ${EXPENSE}) AND a.locked = 0
AND a.hidden = 0 AND (p.number <> 0 AND p.number <> 13);
`;

const months = constants.periods.filter(elt => elt.periodNum !== 0);
Expand Down Expand Up @@ -269,8 +274,17 @@ function getBudgetData(req, res, next) {
function sortAccounts(origAccounts, allAccounts) {

// first separate the types of accounts
const expenses = origAccounts.filter(item => item.type_id === EXPENSE).sort((a, b) => a.number - b.number);
const incomes = origAccounts.filter(item => item.type_id === INCOME).sort((a, b) => a.number - b.number);
const expenses = origAccounts.filter(item => item.type_id === EXPENSE);
const incomes = origAccounts.filter(item => item.type_id === INCOME);

// Improvement of the function for sorting account numbers by treating account numbers as strings.
expenses.sort((a, b) => {
return String(a.number).localeCompare(String(b.number));
});

incomes.sort((a, b) => {
return String(a.number).localeCompare(String(b.number));
});

// Construct the list of periods (leave out the FY total period)
const periods = constants.periods.filter(elt => elt.periodNum !== 0);
Expand Down Expand Up @@ -492,7 +506,7 @@ function computeTitleAccountTotals(budgetAccts, allAccounts) {
const childrenIDs = getChildrenAccounts(allAccounts, acct.id);
childrenIDs.forEach(childId => {
const bdAcct = budgetAccts.find(item => item.id === childId);
if (bdAcct && bdAcct.budget) {
if (bdAcct) {
if (bdAcct.type_id === INCOME) {
acct.isIncomeTitle = true;
} else if (bdAcct.type_id === EXPENSE) {
Expand Down Expand Up @@ -701,7 +715,7 @@ function computeTitleAccountPeriodTotals(budgetAccts, allAccounts) {
const childrenIDs = getChildrenAccounts(allAccounts, acct.id);
childrenIDs.forEach(childId => {
const bdAcct = budgetAccts.find(item => item.id === childId);
if (bdAcct && bdAcct.budget) {
if (bdAcct) {
if ((bdAcct.type_id === INCOME) || (bdAcct.type_id === EXPENSE)) {
acct.actuals += bdAcct.actuals ? bdAcct.actuals : 0;
acct.credit += bdAcct.credit ? bdAcct.credit : 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<!-- data -->
<table class="table table-condensed table-bordered table-report">
<thead>
<tr style="background-color:#ddd;">
<tr style="background-color:#A0A0A0;">
<th class="text-left">{{translate 'TABLE.COLUMNS.ACCOUNT'}}</th>
<th class="text-left">{{translate 'TABLE.COLUMNS.LABEL'}}</th>
<th class="text-left">{{translate 'TABLE.COLUMNS.TYPE'}}</th>
Expand Down
Loading

0 comments on commit 6ddd727

Please sign in to comment.