Skip to content

Commit

Permalink
Ignore hours spent on WRs with unapproved quotes.
Browse files Browse the repository at this point in the history
This new behaviour is enabled by the "free_presales" config option.
This change also includes significant refactoring of get_sla_hours.js,
fixing some of the lingering crapness in composite API calls.
  • Loading branch information
jlabusch committed Mar 29, 2018
1 parent 390bd62 commit 1fa2631
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 131 deletions.
3 changes: 2 additions & 1 deletion api/config/default.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"listen_port": 80,
"wrms_details": {
"user_id": 4089,
"exclude_orgs": "37,1098"
"exclude_orgs": "37,1098",
"free_presales": true
}
},
"icinga_uri": "icinga.example.com",
Expand Down
1 change: 1 addition & 0 deletions api/lib/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function cache_key(name, context){
function cache_put(key, val){
var now = new Date().getTime();
cache[key] = {ts: now, val: JSON.parse(JSON.stringify(val))};
util.log_debug(__filename, 'CACHE PUT [' + key + ']', DEBUG);
}

function cache_get(key, limit){
Expand Down
217 changes: 145 additions & 72 deletions api/lib/get_sla_hours.js
Original file line number Diff line number Diff line change
@@ -1,97 +1,170 @@
var query = require('./query'),
http = require('http'),
config= require('config'),
cache = require('./cache'),
qf = require('./quote_funcs'),
util = require('./util');

const HOURS = 60*60*1000,
DEBUG = false;

function build_sql(ctx){
// TODO: use the string_agg() trick to avoid multiple rows for multiple tags.
// See get_quotes.js.
return `SELECT r.request_id,r.brief,r.invoice_to,SUM(ts.work_quantity) AS hours,otag.tag_description as tag
FROM request r
JOIN request_timesheet ts ON r.request_id=ts.request_id
LEFT JOIN request_tag rtag ON r.request_id=rtag.request_id
LEFT JOIN organisation_tag otag ON otag.tag_id=rtag.tag_id
JOIN usr u ON u.user_no=r.requester_id
WHERE u.org_code=${ctx.org}
AND r.system_id IN (${ctx.sys.join(',')})
AND ts.work_on >= '${ctx.period + '-01'}'
AND ts.work_on < '${util.next_period(ctx) + '-01'}'
AND ts.work_units='hours'
GROUP BY r.request_id,otag.tag_description`;
}

function populate_timesheets(data, ctx){
let ts = {},
warranty_wrs = {};

data.rows.forEach(row => {
// Don't add hours if we find a Warranty tag.
if (row.tag === 'Warranty'){
warranty_wrs[row.request_id] = true;
delete ts[row.request_id];
}else if (!warranty_wrs[row.request_id]){
ts[row.request_id] = row;
ts[row.request_id].total = util.calculate_timesheet_hours(row.hours, row.invoice_to, ctx);
}
});

util.log_debug(__filename, JSON.stringify(ts, null, 2), DEBUG);
return ts;
}

function process(pending_quotes, approved_quotes, ts, budget, ctx, next){
let sla = 0,
add = 0;

if (!Array.isArray(pending_quotes.rows)){
util.log(__filename, "pending_quotes.rows is not an array: " + JSON.stringify(pending_quotes, null, 2));
next({error: "Couldn't calculate SLA breakdown"});
return;
}
if (config.get('server.wrms_details').free_presales){
// Delete timesheets if there has ever been any kind of quote,
// even if it's unapproved or for a different month.
pending_quotes.rows.forEach(row => {
delete ts[row.request_id];
util.log_debug(__filename, 'deleting timesheets for ' + row.request_id + ', it has a pending quote', DEBUG);
});
}

if (!Array.isArray(approved_quotes.rows)){
util.log(__filename, "approved_quotes.rows is not an array: " + JSON.stringify(approved_quotes, null, 2));
next({error: "Couldn't calculate SLA breakdown"});
return;
}
// Delete more timesheets as above, and also tally up SLA/Additional quote totals
approved_quotes.rows.forEach(row => {
if (qf.is_sla_quote_for_this_period(row, ctx)){
sla += qf.convert_quote_amount(row);
}else if (qf.is_additional_quote_for_this_period(row, ctx)){
add += qf.convert_quote_amount(row);
}
delete ts[row.request_id];
util.log_debug(__filename, 'deleting timesheets for ' + row.request_id + ', it has an approved quote', DEBUG);
});
util.log_debug(__filename, JSON.stringify(ts, null, 2), DEBUG);

cache.put(cache.key('sla_hours_ts',ctx), ts);

// Sum up total unquoted hours
let t = Object.keys(ts).reduce((acc, val) => {
return acc + ts[val].total;
}, 0);
util.log_debug(__filename, 'sum of unquoted SLA hours: ' + t, DEBUG);

let res = {
budget: budget,
result: [
['SLA quotes', sla],
['SLA unquoted', t],
['Additional quotes', add]
]
};

cache.put(cache.key('sla_hours_api',ctx), res);
next(res);
}

function all(){
return function(){
return true;
}
}

function int_req_handler(key, next){
return function(){
let d = cache.get(key);
util.log_debug(__filename, 'IR handler "' + key + '" cache get: ' + JSON.stringify(d, null, 2), DEBUG);
if (d){
next(d);
}else{
next({error: "Couldn't calculate SLA breakdown"});
}
}
}

module.exports = query.prepare({
label: 'sla_hours',
cache_key_base: 'sla_hours',
sql: function(ctx){
// TODO: use the string_agg() trick to avoid multiple rows for multiple tags. See get_quotes.js.
return `SELECT r.request_id,r.brief,r.invoice_to,SUM(ts.work_quantity) AS hours,otag.tag_description as tag
FROM request r
JOIN request_timesheet ts ON r.request_id=ts.request_id
LEFT JOIN request_tag rtag ON r.request_id=rtag.request_id
LEFT JOIN organisation_tag otag ON otag.tag_id=rtag.tag_id
JOIN usr u ON u.user_no=r.requester_id
WHERE u.org_code=${ctx.org}
AND r.system_id IN (${ctx.sys.join(',')})
AND ts.work_on >= '${ctx.period + '-01'}'
AND ts.work_on < '${util.next_period(ctx) + '-01'}'
AND ts.work_units='hours'
GROUP BY r.request_id,otag.tag_description`;
},
sql: build_sql,
process_data: function(data, ctx, next){
let ts = {};
if (data && data.rows && data.rows.length > 0){
let warranty_wrs = {};
data.rows.forEach(row => {
// Don't add hours if we find a Warranty tag.
// It sucks that the cache is a low-level DB cache, that means
// get_sla_unquoted.js has to repeat all of the Warranty
// pruning logic.
if (row.tag === 'Warranty'){
warranty_wrs[row.request_id] = true;
delete ts[row.request_id];
}else if (!warranty_wrs[row.request_id]){
ts[row.request_id] = util.calculate_timesheet_hours(row.hours, row.invoice_to, ctx);
}
});
util.log_debug(__filename, JSON.stringify(ts, null, 2), DEBUG);
if (data && Array.isArray(data.rows)){
ts = populate_timesheets(data, ctx);
}

let budget = 0;
if (util.get_org(ctx) && util.get_org(ctx).budget_hours){
budget = util.get_org(ctx).budget_hours;
}
cache.wait(cache.key('approved_quotes',ctx))
.then((aq) => {
let sla = 0,
add = 0;
aq.rows.forEach(row => {
if (qf.is_sla_quote_for_this_period(row, ctx)){
sla += qf.convert_quote_amount(row);
}else if (qf.is_additional_quote_for_this_period(row, ctx)){
add += qf.convert_quote_amount(row);
}
// Delete timesheets if there has ever been any kind of quote,
// even if it's for a different month.
delete ts[row.request_id];
util.log_debug(__filename, 'deleting timesheets for ' + row.request_id + ', it has a quote', DEBUG);
});
util.log_debug(__filename, JSON.stringify(ts, null, 2), DEBUG);
let t = Object.keys(ts).reduce((acc, val) => {
return acc + ts[val];
}, 0);
util.log_debug(__filename, 'sum of unquoted SLA hours: ' + t, DEBUG);
let res = {
budget: budget,
result: [
['SLA quotes', sla],
['SLA unquoted', t],
['Additional quotes', add]
]
};
cache.put(cache.key('sla_hours_api',ctx), res);
next(res);
})
util.log_debug(__filename, 'budget => ' + budget, DEBUG);

let send_internal_request = require('./internal_request');

cache.wait(cache.key('approved_quotes', ctx))
.then(get_pending_quotes)
.use_last_known_good(true)
.timeout(() => {
let res = {
budget: 0,
result: [
['SLA quotes', 0],
['SLA unquoted', 0],
['Additional quotes', 0]
]
};
cache.put(cache.key('sla_hours_api',ctx), res);
next(res);
send_internal_request(
require('./get_quotes')(all),
ctx,
int_req_handler(cache.key('approved_quotes', ctx), get_pending_quotes)
);
})
.limit(17);
.limit(2);

function get_pending_quotes(aq){
let calculate_result = function(pq){
process(pq, aq, ts, budget, ctx, next);
}

cache.wait(cache.key('pending_quotes', ctx))
.then(calculate_result)
.use_last_known_good(true)
.timeout(() => {
send_internal_request(
require('./get_pending_quotes')(all),
ctx,
int_req_handler(cache.key('pending_quotes', ctx), calculate_result)
);
})
.limit(2);
}
},
use_last_known_good: true
});
Expand Down
57 changes: 19 additions & 38 deletions api/lib/get_sla_unquoted.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,27 @@ var query = require('./query'),
const DEBUG = false;

module.exports = function(req, res, next, ctx){
function process_results(tsdata, qdata){
let r = {result: []};
if (tsdata && tsdata.rows && tsdata.rows.length > 0 && qdata && Array.isArray(qdata.rows)){
let wrs_with_time = {},
warranty_wrs = {};
tsdata.rows.forEach(row => {
// This duplicates logic in get_sla_hours.js for pretty lame reasons. (It's 3am.)
if (row.tag === 'Warranty'){
warranty_wrs[row.request_id] = true;
delete wrs_with_time[row.request_id];
}else if (!warranty_wrs[row.request_id]){
wrs_with_time[row.request_id] = row;
}
});
qdata.rows.forEach(row => {
util.log_debug(__filename, 'delete wrs_with_time[' + row.request_id + ']');
delete wrs_with_time[row.request_id];
});
r.result = Object.keys(wrs_with_time).sort().map(key => {
let row = wrs_with_time[key];
return [{
wr: row.request_id + ': ' + row.brief,
result: util.calculate_timesheet_hours(row.hours, row.invoice_to, ctx)
}];
});
}else{
r.result.push({wr: "None", result: 0});
}
res.json(r);
next && next(false);
}
cache.wait(cache.key('sla_hours_ts', ctx))
.then((ts) => {
let r = {result: [{wr: "None", result: 0}]},
arr = Object.keys(ts);

cache.wait(cache.key('sla_hours', ctx))
.then((tsdata) => {
cache.wait(cache.key('approved_quotes', ctx))
.then((qdata) => { process_results(tsdata, qdata) })
.timeout(() => { query.error(res, next)(new Error('sla_unquoted: quote cache timed out')); })
.limit(20);
if (arr.length > 0){
r.result = arr.sort().map(key => {
let row = ts[key];
return [{
wr: row.request_id + ': ' + row.brief,
result: row.total
}];
});
}

res.json(r);
next && next(false);
})
.timeout(() => {
query.error(res, next)(new Error('sla_unquoted: sla_hours_ts cache timed out'));
})
.timeout(() => { query.error(res, next)(new Error('sla_unquoted: timesheet cache timed out')); })
.limit(20);
}

Expand Down
26 changes: 26 additions & 0 deletions api/lib/internal_request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
var util = require('./util');

let empty_fn = function(){};

const DEBUG = false;

// A shim for http.Response that makes it easier for us to
// build composite APIs like sla_hours, which relies
// on approved_quotes and pending_quotes under the hood.
function Response(handler){
this.handler = handler || empty_fn;
}

Response.prototype.charSet = empty_fn;

Response.prototype.json = function(j){
util.log_debug(__filename, JSON.stringify(j, null, 2), DEBUG);
this.handler(j);
}

module.exports = function(fn, ctx, next){
util.log_debug(__filename, JSON.stringify(ctx, null, 2), DEBUG);
let res = new Response(next);
fn({}, res, empty_fn, ctx);
}

5 changes: 5 additions & 0 deletions frontend/static/dash.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ function draw_custom_charts(){
return;
}

if (data.error){
console.log('sla_hours: ' + data.error);
return;
}

var o = JSON.parse(JSON.stringify(std_gchart_options));
o.orientation = 'vertical';
o.chartArea = {height: 200, left: '25%', width: '75%' };
Expand Down
Loading

0 comments on commit 1fa2631

Please sign in to comment.