Skip to content

Commit

Permalink
feat(cashflow): implement cashflow service report
Browse files Browse the repository at this point in the history
This commit implements the server-side of the cashflow by service
report.  It is rather naive at the moment, but provides enough
information to begin a daily audit.

Features:
 1. Only the services that were affected are displayed.
 2. The totals are given at the bottom of every single table
 3. The cash payment identifiers and patient names are listed.
 4. Limited by date range.

Next steps:
 1. Implement the client-side with validation.
  • Loading branch information
Jonathan Niles authored and sfount committed Feb 1, 2017
1 parent 9379f04 commit 0fdad75
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 11 deletions.
1 change: 1 addition & 0 deletions server/config/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ exports.configure = function configure(app) {
app.get('/reports/finance/vouchers/:uuid', financeReports.vouchers.receipt);
app.get('/reports/finance/accounts/chart', financeReports.accounts.chart);
app.get('/reports/finance/cashflow', financeReports.cashflow.document);
app.get('/reports/finance/cashflow/services', financeReports.cashflow.byService);
app.get('/reports/finance/financialPatient/:uuid', financeReports.patient);
app.get('/reports/finance/income_expense', financeReports.incomeExpense.document);
app.get('/reports/finance/balance', financeReports.balance.document);
Expand Down
138 changes: 131 additions & 7 deletions server/controllers/finance/reports/cashflow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,18 @@ const util = require('../../../../lib/util');
const ReportManager = require('../../../../lib/ReportManager');
const BadRequest = require('../../../../lib/errors/BadRequest');

const identifiers = require('../../../../config/identifiers');

const TEMPLATE = './server/controllers/finance/reports/cashflow/report.handlebars';
const TEMPLATE_BY_SERVICE = './server/controllers/finance/reports/cashflow/reportByService.handlebars';

// expose to the API
exports.report = report;
exports.weeklyReport = weeklyReport;
exports.document = document;

exports.byService = reportByService;

/**
* @function report
* @desc This function is responsible of generating the cashflow data for the report
Expand Down Expand Up @@ -94,7 +99,8 @@ function queryIncomeExpense (params, dateFrom, dateTo) {
params.dateFrom = dateFrom;
params.dateTo = dateTo;
}
var requette = `

let requette = `
SELECT BUID(t.uuid) AS uuid, t.trans_id, t.trans_date, a.number, a.label,
SUM(t.debit_equiv) AS debit_equiv, SUM(t.credit_equiv) AS credit_equiv,
t.debit, t.credit, t.currency_id, t.description, t.comment,
Expand Down Expand Up @@ -273,8 +279,8 @@ function closingBalance(accountId, periodStart) {
*/
function getFiscalYear(date) {
var query =
`SELECT fy.id, fy.previous_fiscal_year_id FROM fiscal_year fy
JOIN period p ON p.fiscal_year_id = fy.id
`SELECT fy.id, fy.previous_fiscal_year_id FROM fiscal_year fy
JOIN period p ON p.fiscal_year_id = fy.id
WHERE ? BETWEEN p.start_date AND p.end_date`;
return db.exec(query, [date]);
}
Expand All @@ -286,10 +292,10 @@ function getFiscalYear(date) {
*/
function getPeriods(dateFrom, dateTo) {
var query =
`SELECT id, number, start_date, end_date
FROM period WHERE (DATE(start_date) >= DATE(?) AND DATE(end_date) <= DATE(?))
OR (DATE(?) BETWEEN DATE(start_date) AND DATE(end_date))
OR (DATE(?) BETWEEN DATE(start_date) AND DATE(end_date));`;
`SELECT id, number, start_date, end_date
FROM period WHERE (DATE(start_date) >= DATE(?) AND DATE(end_date) <= DATE(?))
OR (DATE(?) BETWEEN DATE(start_date) AND DATE(end_date))
OR (DATE(?) BETWEEN DATE(start_date) AND DATE(end_date));`;
return db.exec(query, [dateFrom, dateTo, dateFrom, dateTo]);
}

Expand Down Expand Up @@ -564,3 +570,121 @@ function document(req, res, next) {
}
}
}


// the ID of a voucher for cash return
const CASH_RETURN_ID = 8;

/**
* This function creates a cashflow report by service, reporting the realized income
* for the hospital services.
*
* @todo - factor in cash reversals.
* @todo - factor in posting journal balances
*/
function reportByService(req, res, next) {

const dateFrom = new Date(req.query.dateFrom);
const dateTo = new Date(req.query.dateTo);

let report;

const options = _.clone(req.query);
_.extend(options, { orientation : 'landscape' });

try {
report = new ReportManager(TEMPLATE_BY_SERVICE, req.session, options);
} catch (e) {
return next(e);
}

const data = {};

// get the cash flow data
const cashflowByServiceSql = `
SELECT BUID(cash.uuid) AS uuid, CONCAT_WS('.', '${identifiers.CASH_PAYMENT.key}', project.abbr, cash.reference) AS reference,
cash.date, cash.amount AS cashAmount, invoice.cost AS invoiceAmount, cash.currency_id, service.id AS service_id,
patient.display_name, service.name
FROM cash JOIN cash_item ON cash.uuid = cash_item.cash_uuid
JOIN invoice ON cash_item.invoice_uuid = invoice.uuid
JOIN project ON cash.project_id = project.id
JOIN patient ON patient.debtor_uuid = cash.debtor_uuid
JOIN service ON invoice.service_id = service.id
WHERE cash.is_caution = 0
AND cash.date >= DATE(?) AND cash.date <= DATE(?)
AND cash.uuid NOT IN
(SELECT DISTINCT voucher.reference_uuid FROM voucher WHERE voucher.type_id = ${CASH_RETURN_ID})
ORDER BY cash.date;
`;

// get all service names in alphabetical order
const serviceSql = `
SELECT DISTINCT service.name FROM service WHERE service.id IN (?) ORDER BY name;
`;

// get the totals of the captured records
const serviceAggregationSql = `
SELECT service.name, SUM(cash.amount) AS totalCashIncome, SUM(invoice.cost) AS totalAcruelIncome
FROM cash JOIN cash_item ON cash.uuid = cash_item.cash_uuid
JOIN invoice ON cash_item.invoice_uuid = invoice.uuid
JOIN service ON invoice.service_id = service.id
WHERE cash.uuid IN (?)
GROUP BY service.name
ORDER BY service.name;
`;

db.exec(cashflowByServiceSql, [dateFrom, dateTo])
.then(rows => {
data.rows = rows;

// get a list of unique service ids
let serviceIds = rows
.map(row => row.service_id)
.filter((id, index, array) => array.indexOf(id) === index);

// execute the service SQL
return db.exec(serviceSql, [serviceIds]);
})
.then(services => {

let rows = data.rows;
let uuids = rows.map(row => db.bid(row.uuid));
delete data.rows;

// map services to their service names
data.services = services.map(service => service.name);

let xAxis = data.services.length;

// file the matrix with nulls except the correct columns
let matrix = rows.map(row => {

// fill with each service + two lines for cash payment identifier and patient name
let line = _.fill(Array(xAxis + 2), null);

// each line has the cash payment reference and then the patient name
line[0] = row.reference;
line[1] = row.display_name;

// get the index of the service name and fill in the correct cell in the matrix
const idx = data.services.indexOf(row.name) + 2;
line[idx] = row.cashAmount;
return line;
});

// bind to the view
data.matrix = matrix;

// query the aggregates
return db.exec(serviceAggregationSql, [uuids]);
})
.then(aggregates => {
data.aggregates = aggregates;
return report.render(data);
})
.then(result => {
res.set(result.headers).send(result.report);
})
.catch(next)
.done();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{{> head title="CASHFLOW.BY_SERVICE" }}

<body>

{{> header}}

<!-- body -->
<div class="row">
<div class="col-xs-12">

<!-- page title -->
<h2 class="text-center text-capitalize">
{{translate 'CASHFLOW.BY_SERVICE'}}
</h2>

<table class="table table-bordered table-condensed" style="font-size:0.75em;">
<thead>
<tr>
<th>{{translate 'FORM.LABELS.REFERENCE'}}</th>
<th>{{translate 'TABLE.COLUMNS.NAME' }}</th>
{{#each services as |service| }}
<th>{{ service }}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each matrix as | row |}}
<tr>
{{#each row as |value |}}
<td {{#unless @first}}class="text-right"{{/unless}}>
{{#if value}}
{{value}}
{{/if}}
</td>
{{/each}}
</tr>
{{/each}}
</tbody>

<tfoot>
<tr>
<th colspan="2">{{translate "TABLE.COLUMNS.TOTAL" }}</th>
{{#each aggregates as | aggregate |}}
<th class="text-right">{{currency aggregate.totalCashIncome ../metadata.enterprise.currency_id }}</th>
{{/each}}
</tr>
</tfoot>
</table>
</div>
</div>
</body>
7 changes: 3 additions & 4 deletions server/lib/template/helpers/finance.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

const accountingjs = require('accounting-js');

const USD_FMT = {
precision: 2
};
const USD_FMT = { precision: 2 };

const FC_FMT = {
symbol: 'FC',
Expand All @@ -22,6 +20,8 @@ function currency(value, currencyId) {
return accountingjs.formatMoney(value || 0, fmt);
}

const INDENTATION_STEP = 40;

/**
* @function indentAccount
* @description indent with 40px accounts based on the account depth for the chart of accounts
Expand All @@ -30,7 +30,6 @@ function currency(value, currencyId) {
*/
function indentAccount(depth) {
// indentation step is fixed arbitrary to 40 (40px)
const INDENTATION_STEP = 40;
let number = Number(depth);
return number ? number * INDENTATION_STEP : 0;
}
Expand Down

0 comments on commit 0fdad75

Please sign in to comment.