Skip to content

Commit

Permalink
feat(reports): bare-bones aged debtor report
Browse files Browse the repository at this point in the history
This commit adds the SQL and handlebars template for a bare-bones aged
debtor report.  It is very plain.  The next steps are:

 1. Make a report partial with the enterprise header information.  This
 should be readily available to be used in any report.
 2. Make a report partials with a "no data" message for tables that do
 not have any data.
 3. Move styles into a central reports.css location.
  • Loading branch information
Jonathan Niles committed Aug 17, 2016
1 parent 6f34210 commit 9d77b39
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 67 deletions.
9 changes: 8 additions & 1 deletion client/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,14 @@
"REPORT" : {
"SINCE": "Since",
"PRODUCED_BY" : "Produced by",
"PRODUCED_ON" : "Produced on"
"PRODUCED_ON" : "Produced on",
"AGED_DEBTORS" : {
"TITLE": "Aged Debtors",
"THIRTY_DAYS" : "Less Than 30 Days",
"SIXTY_DAYS" : "Less Than 60 Days",
"NINETY_DAYS" : "Less Than 90 Days",
"OVER_NINETY_DAYS" : "Over 90 Days"
}
},
"SECTION_BILAN": {
"ADD_SECTION_BILAN" : "Add a bilan Section",
Expand Down
2 changes: 2 additions & 0 deletions server/config/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,12 @@ exports.configure = function configure(app) {

// reports API: Invoices (receipts)
app.get('/reports/invoices/:uuid', financeReports.receipts.invoices);

app.get('/reports/patient/registrations', medicalReports.patientRegistrations);
app.get('/reports/patient/:uuid', medicalReports.receipts.patients);
app.get('/reports/patients/:uuid/checkins', medicalReports.patientCheckins);
app.get('/reports/purchases/:uuid', inventoryReports.receipts.purchases);
app.get('/reports/finance/aged_debtor', financeReports.agedDebtor);

// patient group routes
app.get('/patients/groups', patientGroups.list);
Expand Down
103 changes: 103 additions & 0 deletions server/controllers/finance/reports/agedDebtor.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<head>
<title>{{translate "REPORT.AGED_DEBTORS.TITLE"}}</title>
<link href="{{absolutePath}}/css/bhima-bootstrap.css" rel="stylesheet">
<link href="{{absolutePath}}/vendor/components-font-awesome/css/font-awesome.css" rel="stylesheet">
<meta charset="utf-8" />
<style>
.table-report {
font-size: 0.8em;
}
.table-report > thead > th {
font-size: 1.2em;
}
.table-report > tbody > tr > td,
.table-report > tbody > tr > th,
.table-report > tfoot > tr > td,
.table-report > tfoot > tr > th,
.table-report > thead > tr > th {
padding : 2px;
}
.table-report {
margin-bottom : 0;
}
.table-report > tbody > tr > td,
.table-report > tbody > tr > th {
border: 1px solid #ddd;
}
.table-report > tbody > tr:last-child > td,
.table-report > tbody > tr:last-child > th {
border-bottom:none;
}
.table-report > thead > tr > th {
border-bottom : 1px solid #000;
}
.table-report > tfoot > tr > th {
border-top: 1px solid #000;
}
.clear-borders {
border: none !important;
}
</style>
</head>
<body>
<main class="container">
<h4 class="text-center">{{translate "REPORT.AGED_DEBTORS.TITLE"}}</h4>

<!-- margin is the cell size -->
<section style="margin-right: 120px;">
<table class="table table-condensed table-report">
<thead>
<tr class="text-capitalize">
<th class="text-center">{{translate "TABLE.COLUMNS.NAME"}}</th>
<th class="text-center" style="width:15%">{{translate "FORM.LABELS.ACCOUNT_NUMBER"}}</th>
<th class="text-center">{{translate "REPORT.AGED_DEBTORS.THIRTY_DAYS"}}</th>
<th class="text-center">{{translate "REPORT.AGED_DEBTORS.SIXTY_DAYS"}}</th>
<th class="text-center">{{translate "REPORT.AGED_DEBTORS.NINETY_DAYS"}}</th>
<th class="text-center">{{translate "REPORT.AGED_DEBTORS.OVER_NINETY_DAYS"}}</th>
</tr>
</thead>
<tbody>

<!-- print a row for each debtor -->
{{#each debtors}}
<tr>
<th>{{this.name}}</th>
<td class="text-left">{{this.number}}</td>
<td class="text-right">{{currency this.thirty ../metadata.currency_id}}</td>
<td class="text-right">{{currency this.sixty ../metadata.currency_id}}</td>
<td class="text-right">{{currency this.ninety ../metadata.currency_id}}</td>
<td class="text-right">{{currency this.excess ../metadata.currency_id}}</td>
</tr>
{{else}}
<tr>
<td colspan="6" class="text-center clear-borders">
<i class="fa fa-warning"></i> {{translate "TABLE.COLUMNS.EMPTY"}}
</td>
</tr>
{{/each}}
</tbody>
{{#if aggregates}}
<tfoot>
<tr>
<th colspan="2">{{translate "TABLE.COLUMNS.TOTAL"}}</th>
<th class="text-right">{{currency aggregates.thirty metadata.currency_id}}</th>
<th class="text-right">{{currency aggregates.sixty metadata.currency_id}}</th>
<th class="text-right">{{currency aggregates.ninety metadata.currency_id}}</th>
<th class="text-right">{{currency aggregates.excess metadata.currency_id}}</th>
</tr>
</tfoot>
{{/if}}
</table>
</section>
</main>
</body>
117 changes: 117 additions & 0 deletions server/controllers/finance/reports/agedDebtor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* @overview agedDebtor
*
* @description
* This report displays the amounts owed by debtor groups broken down by age of
* their debt. The report highlights clients who have long overdue debts, so
* that the administration can send out a recovery service to try and recover
* the owed debt.
*
* The typical age categories are 0-30 days, 30-60 days, 60-90 days, and > 90
* days.
*
* As usual, the reports are created with a handlebars template and shipped to
* the client as either JSON, HTML, or PDF, depending on the renderer specified
* in the HTTP query string.
*/

'use strict';

const _ = require('lodash');
const path = require('path');

const db = require('../../../lib/db');
const BadRequest = require('../../../lib/errors/BadRequest');

// group supported renderers
const renderers = {
'json': require('../../../lib/renderers/json'),
'html': require('../../../lib/renderers/html'),
'pdf': require('../../../lib/renderers/pdf'),
};

// default rendering parameters
const defaults = {
pageSize: 'A4',
renderer: 'pdf'
};

// path to the template to render
const template = path.normalize('./server/controllers/finance/reports/agedDebtor.handlebars');

/**
* @method agedDebtorReport
*
* @description
* The HTTP interface which actually creates the report.
*/
module.exports = function agedDebtorReport(req, res, next) {

const qs = req.query;

// choose the renderer
const renderer = renderers[qs.renderer || defaults.renderer];
if (_.isUndefined(renderer)) {
return next(new BadRequest(`The application does not support rendering ${qs.renderer}.`));
}

// data to be passed to the report
const data = {
metadata : {
timestamp : new Date(),
currency_id : req.session.enterprise.currency_id
}
};

// make sure the language is set appropriately
const context = { lang : qs.lang };
_.defaults(context, defaults);

// makes the
const havingNonZeroValues = ' HAVING thirty + sixty + ninety + excess > 0 ';
const includeZeroes = Boolean(Number(qs.zeroes));

// selects into columns of 30, 60, 90, and >90
const debtorSql = `
SELECT BUID(dg.uuid) AS id, dg.name, a.number,
SUM(IF(DATEDIFF(CURRENT_TIMESTAMP(), gl.trans_date) BETWEEN 0 AND 30, gl.debit_equiv - gl.credit_equiv, 0)) AS thirty,
SUM(IF(DATEDIFF(CURRENT_TIMESTAMP(), gl.trans_date) BETWEEN 30 AND 60, gl.debit_equiv - gl.credit_equiv, 0)) AS sixty,
SUM(IF(DATEDIFF(CURRENT_TIMESTAMP(), gl.trans_date) BETWEEN 60 AND 90, gl.debit_equiv - gl.credit_equiv, 0)) AS ninety,
SUM(IF(DATEDIFF(CURRENT_TIMESTAMP(), gl.trans_date) > 90, gl.debit_equiv - gl.credit_equiv, 0)) AS excess
FROM debtor_group AS dg JOIN debtor AS d ON dg.uuid = d.group_uuid
LEFT JOIN general_ledger AS gl ON gl.entity_uuid = d.uuid
JOIN account AS a ON a.id = dg.account_id
GROUP BY dg.uuid
${includeZeroes ? '' : havingNonZeroValues}
ORDER BY dg.name;
`;

// aggregates the data above as totals into columns of 30, 60, 90, and >90
const aggregateSql = `
SELECT
SUM(IF(DATEDIFF(CURRENT_TIMESTAMP(), gl.trans_date) BETWEEN 0 AND 30, gl.debit_equiv - gl.credit_equiv, 0)) AS thirty,
SUM(IF(DATEDIFF(CURRENT_TIMESTAMP(), gl.trans_date) BETWEEN 30 AND 60, gl.debit_equiv - gl.credit_equiv, 0)) AS sixty,
SUM(IF(DATEDIFF(CURRENT_TIMESTAMP(), gl.trans_date) BETWEEN 60 AND 90, gl.debit_equiv - gl.credit_equiv, 0)) AS ninety,
SUM(IF(DATEDIFF(CURRENT_TIMESTAMP(), gl.trans_date) > 90, gl.debit_equiv - gl.credit_equiv, 0)) AS excess
FROM debtor_group AS dg JOIN debtor AS d ON dg.uuid = d.group_uuid
LEFT JOIN general_ledger AS gl ON gl.entity_uuid = d.uuid
${includeZeroes ? '' : havingNonZeroValues}
`;

// fire the SQL for the report
db.exec(debtorSql)
.then(function (debtors) {
data.debtors = debtors;
return db.exec(aggregateSql);
})
.then(aggregates => {
data.aggregates = aggregates[0];
return renderer.render(data, template, context);
})
.then(result => {
res.set(renderer.headers).send(result);
})
.catch(next)
.done();

};
3 changes: 2 additions & 1 deletion server/controllers/finance/reports/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ module.exports = {

/** @todo - make this voucher receipt be exposed in this method */
voucher: require('./voucher.receipt')
}
},
agedDebtor: require('./agedDebtor')
};
2 changes: 0 additions & 2 deletions server/controllers/finance/reports/voucher.report.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@
{{/each}}
</tbody>
</table>

</div>
</div>

</div>
2 changes: 1 addition & 1 deletion server/controllers/medical/reports/checkins.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function build(req, res, next) {
// choose the renderer
const renderer = renderers[qs.renderer || defaults.renderer];
if (_.isUndefined(renderer)) {
throw new BadRequest(`The application does not support rendering ${qs.renderer}.`);
return next(new BadRequest(`The application does not support rendering ${qs.renderer}.`));
}

// delete from the query string
Expand Down
1 change: 0 additions & 1 deletion server/lib/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ function multiply(a, b) {
function currency(value, currencyKey) {
/**@todo this should be driven by currencyKey if provided */
var formatExpression = formatDollar;

return numeral(value).format(formatExpression);
}

Expand Down
67 changes: 6 additions & 61 deletions server/models/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ CREATE TABLE `cash` (
KEY `debtor_uuid` (`debtor_uuid`),
KEY `user_id` (`user_id`),
KEY `cashbox_id` (`cashbox_id`),
INDEX `debtor_uuid` (`debtor_uuid`),
FOREIGN KEY (`project_id`) REFERENCES `project` (`id`),
FOREIGN KEY (`currency_id`) REFERENCES `currency` (`id`),
FOREIGN KEY (`debtor_uuid`) REFERENCES `debtor` (`uuid`),
Expand Down Expand Up @@ -1330,6 +1331,11 @@ CREATE TABLE `posting_journal` (
KEY `user_id` (`user_id`),
KEY `cc_id` (`cc_id`),
KEY `pc_id` (`pc_id`),
INDEX `trans_date` (`trans_date`),
INDEX `record_uuid` (`record_uuid`),
INDEX `reference_uuid` (`record_uuid`),
INDEX `entity_uuid` (`entity_uuid`),
INDEX `account_id` (`account_id`),
FOREIGN KEY (`fiscal_year_id`) REFERENCES `fiscal_year` (`id`),
FOREIGN KEY (`period_id`) REFERENCES `period` (`id`),
FOREIGN KEY (`origin_id`) REFERENCES `transaction_type` (`id`) ON UPDATE CASCADE,
Expand All @@ -1340,67 +1346,6 @@ CREATE TABLE `posting_journal` (
FOREIGN KEY (`pc_id`) REFERENCES `profit_center` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `primary_cash`;

CREATE TABLE `primary_cash` (
`reference` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`uuid` BINARY(16) NOT NULL,
`project_id` smallint(5) unsigned NOT NULL,
`type` char(1) NOT NULL,
`date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`deb_cred_uuid` BINARY(16) DEFAULT NULL,
`deb_cred_type` varchar(1) DEFAULT NULL,
`currency_id` tinyint(3) unsigned NOT NULL,
`account_id` int(10) unsigned NOT NULL,
`cost` decimal(19,4) unsigned NOT NULL DEFAULT 0.0,
`user_id` smallint(5) unsigned NOT NULL,
`description` text,
`cash_box_id` mediumint(8) unsigned NOT NULL,
`origin_id` tinyint(3) unsigned NOT NULL,
PRIMARY KEY (`uuid`),
UNIQUE KEY `primary_cash_1` (`project_id`, `reference`),
KEY `project_id` (`project_id`),
KEY `reference` (`reference`),
KEY `currency_id` (`currency_id`),
KEY `user_id` (`user_id`),
KEY `cash_box_id` (`cash_box_id`),
KEY `account_id` (`account_id`),
KEY `origin_id` (`origin_id`),
FOREIGN KEY (`project_id`) REFERENCES `project` (`id`),
FOREIGN KEY (`currency_id`) REFERENCES `currency` (`id`),
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
FOREIGN KEY (`cash_box_id`) REFERENCES `cash_box` (`id`),
FOREIGN KEY (`account_id`) REFERENCES `account` (`id`),
FOREIGN KEY (`origin_id`) REFERENCES `primary_cash_module` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TRIGGER primary_cash_reference BEFORE INSERT ON primary_cash
FOR EACH ROW SET NEW.reference = (SELECT IFNULL(MAX(reference) + 1, 1) FROM primary_cash WHERE primary_cash.project_id = new.project_id);

DROP TABLE IF EXISTS `primary_cash_item`;

CREATE TABLE `primary_cash_item` (
`uuid` varBINARY(16) NOT NULL,
`primary_cash_uuid` varBINARY(16) NOT NULL,
`debit` decimal(19,4) unsigned NOT NULL DEFAULT 0.0,
`credit` decimal(19,4) unsigned NOT NULL DEFAULT 0.0,
`inv_po_id` varBINARY(16) DEFAULT NULL,
`document_uuid` varBINARY(16) DEFAULT NULL,
PRIMARY KEY (`uuid`),
KEY `primary_cash_uuid` (`primary_cash_uuid`),
FOREIGN KEY (`primary_cash_uuid`) REFERENCES `primary_cash` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `primary_cash_module`;

CREATE TABLE `primary_cash_module` (
`id` tinyint(3) unsigned NOT NULL AUTO_INCREMENT,
`text` varchar(45) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `primary_cash_module_1` (`text`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `profit_center`;

CREATE TABLE `profit_center` (
Expand Down

0 comments on commit 9d77b39

Please sign in to comment.