NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name CPQ ATO Model Punch-out Validator
// @version 0.011
// @description Validates ATO Punch outs on the classic CPQ homepage and JetUI tiles
// @author Obada Kadri
// @license MIT
// @match *://*.bigmachines.com/commerce/display_company_profile.jsp*
// @require https://code.jquery.com/jquery-3.4.1.min.js
// @require https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js
// @require https://cdn.jsdelivr.net/npm/alasql@0.4
// @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js
// @grant GM_addStyle
// ==/UserScript==
let collapseIndex = 0;
function renderResultCategory(parentId, categoryDesc, data, columns) {
const card = $('<div class="wrap-collabsible"></div>');
// Collapsible Header
card.append(`
<input id="collapsible-${collapseIndex}" class="toggle" type="checkbox">
<label for="collapsible-${collapseIndex}" class="lbl-toggle">${categoryDesc} <span class="badge badge-pill badge-dark">${data.length}</span></label>
`);
// Collapsible Content
card.append(`
<div class="collapsible-content">
<div class="content-inner">
<table class="table table-striped">
<tr>${
data.length
? columns.map(label => `<th>${label}</th>`).join("\n")
: ""
}</tr>
${data
.map(row => {
const cols = columns.map(
col => `<td>${row[col] ? row[col] : ""}</td>`
).join("\n");
return `<tr>${cols}</tr>`;
})
.join("\n")}
</table>
</div>
</div>`);
$(`#${parentId}`).append(card);
collapseIndex++;
}
async function validatePunchOuts() {
// Datatable API
const getApiTableName = function(tableName) {
let result = tableName;
if (tableName === "_parts") {
result = "parts";
} else if (tableName.length) {
result = "custom" + tableName[0].toUpperCase() + tableName.slice(1);
}
return result;
};
const getTableMetaData = async function(tableName) {
const metaData = {};
const apiTableName = getApiTableName(tableName);
const base_url = `${location.protocol}//${location.hostname}`;
const url = `${base_url}/rest/v8/metadata-catalog/${apiTableName}`;
const data = await fetch(url)
.then(r => r.json(), console.log)
.catch(console.log);
metaData.columns = Object.keys(data.definitions[apiTableName].properties)
.filter(c => c !== "id")
.map(col => data.definitions[apiTableName].properties[col]);
return metaData;
};
const getTableData = function(tableName, query, offset, allData, deferred) {
const dTableData = Object.is(deferred, undefined)
? new $.Deferred()
: deferred;
offset = offset || 0;
query = query || "";
const apiTableName = getApiTableName(tableName);
const url = `/rest/v8/${apiTableName}?q=${query}&totalResults=true&offset=${offset}`;
allData = allData || [];
$.get(url, function(data) {
if (data.hasMore) {
const nextOffset = offset + 1000;
allData = allData.concat(data.items);
getTableData(tableName, query, nextOffset, allData, dTableData);
} else {
dTableData.resolve(allData.concat(data.items));
}
}).fail(function() {
dTableData.reject();
});
return dTableData.promise();
};
const getTableInfo = async function(tableName, query) {
const tableInfo = {};
tableInfo.isValid = false;
if (tableName) {
tableInfo.isValid = true;
tableInfo.name = tableName;
tableInfo.metaData = await getTableMetaData(tableName);
tableInfo.rawData = await getTableData(tableName, query);
}
return tableInfo;
};
// Item Type is either "ATO" or "PTO HW MODEL": {$or:[{"_part_custom_field4":{$eq:"ATO"}},{"_part_custom_field4":{$eq:"PTO HW MODEL"}}
const fromItemMaster = await getTableInfo(
"_parts",
'{$and:[{$or:[{"_part_custom_field4":{$eq:"ATO"}},{"_part_custom_field4":{$eq:"PTO HW MODEL"}}]},{"_part_custom_field22":{$regex:".*HWCPQ.*"}}]}'
);
// hwPartAttributes
const fromHwPartAttributes = await getTableInfo("hwPartAttributes");
// hwATOconfigModel
const fromHwAtoConfigModel = await getTableInfo("hwATOconfigModel");
// partToSMH
const fromPartToSmh = await getTableInfo('partToSMH');
// partToSMH
const fromWwSMHierarchy = await getTableInfo('hwSMHierarchy');
let results;
results = alasql(
`SELECT * FROM ? AS l INNER JOIN ? AS r ON l.partNumber = r.part_number`,
[fromItemMaster.rawData, fromHwPartAttributes.rawData]
);
results = alasql(
`SELECT * FROM ? AS l FULL OUTER JOIN ? AS r ON l.partNumber = r.configuration`,
[results, fromHwAtoConfigModel.rawData]
);
results = alasql(
`SELECT * FROM ? AS l LEFT JOIN ? AS r ON l.partNumber = r.part_number`,
[results, fromPartToSmh.rawData]
);
results = alasql(
`SELECT * FROM ? AS l LEFT JOIN ? AS r ON l.smh_code_child = r.smh_code_child`,
[results, fromWwSMHierarchy.rawData]
);
results = alasql(
`SELECT l.*, r.smh_desc_child as smh_desc_parent FROM ? AS l LEFT JOIN ? AS r ON l.smh_code_parent = r.smh_code_child`,
[results, fromWwSMHierarchy.rawData]
);
console.log("Result", results);
const activeModels = results.filter(
(r, i) =>
r.partNumber &&
r._part_custom_field12.match(/active|restricted/i) &&
(r.is_active && r.is_active == "Y") &&
results.map(mapObj => mapObj.partNumber).indexOf(r.partNumber) === i // Removes duplicates due to multi SMH
).sort((m1,m2) => {
return m1.product_line < m2.product_line ? -1 :
m1.product_line > m2.product_line ? 1 :
m1.hw_config_position < m2.hw_config_position ? -1 :
m1.hw_config_position > m2.hw_config_position ? 1 : 0;
});
const inItemMasterNotJet = results.filter(
(r, i) =>
r.partNumber &&
r._part_custom_field12.match(/active|restricted/i) &&
(!r.is_active || r.is_active == "N") &&
results.map(mapObj => mapObj.partNumber).indexOf(r.partNumber) === i // Removes duplicates due to multi SMH
);
const inJetNotItemMaster = results.filter(
(r, i) =>
(!r.partNumber || !r._part_custom_field12.match(/active|restricted/i)) &&
r.is_active && r.is_active == "Y" &&
results.map(mapObj => mapObj.partNumber).indexOf(r.partNumber) === i // Removes duplicates due to multi SMH
);
$("#ato-punch-outs-validation-results").empty();
renderResultCategory(
"ato-punch-outs-validation-results",
"Active Models",
activeModels,
["partNumber", "description", "_part_custom_field4", "_part_custom_field12", "last_order_date", "smh_code_child", "smh_desc_child", "smh_code_intermediat", "smh_code_parent", "smh_desc_parent", "product_line_label", "hw_config_position"]
);
renderResultCategory(
"ato-punch-outs-validation-results",
"Records active\/restricted in Item Master but not active in Jet UI",
// Filter out models that expired based on LOD
inItemMasterNotJet.filter(r => r.last_order_date == null || moment().isSameOrBefore(moment(r.last_order_date, "DD-MMM-YY").format("YYYY-MM-DD"))),
["partNumber", "description", "_part_custom_field4", "_part_custom_field12", "last_order_date", "smh_code_child", "smh_desc_child", "smh_code_intermediat", "smh_code_parent", "smh_desc_parent"]
);
renderResultCategory(
"ato-punch-outs-validation-results",
"Records active in Jet UI but not active\/restricted in Item Master",
inJetNotItemMaster,
["configuration", "product_line", "hw_config_position", "smh_code_child", "smh_desc_child", "smh_code_intermediat", "smh_code_parent", "smh_desc_parent"]
);
}
function showValidator() {
"use strict";
var modalHtml = `
<!-- Modal -->
<div class="modal fade" id="atoModelPunchoutModal" tabindex="-1" role="dialog" aria-labelledby="atoModelPunchoutModalLabel">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content d-none">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">CPQ ATO Model Punch-out Validator</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div id="ato-punch-outs-validation-results" class="accordion">Validating...</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
`;
var okToolBar = document.querySelector("ul.ok-tool-bar");
if (!okToolBar) {
okToolBar = document.createElement("ul");
okToolBar.className = "ok-tool-bar";
$("body").append(okToolBar);
}
var showPerfStatsBtn = document.createElement("li");
var text = document.createTextNode("Validate ATO Punch-outs");
showPerfStatsBtn.appendChild(text);
showPerfStatsBtn.id = "btn-btn-ato-validator";
showPerfStatsBtn.className = "btn-ato-validator";
showPerfStatsBtn.dataset.toggle = "modal";
showPerfStatsBtn.dataset.target = "#atoModelPunchoutModal";
//--- Add nodes to page
okToolBar.append(showPerfStatsBtn);
$("body").append(modalHtml);
$("#btn-btn-ato-validator").on("click", function() {
$(".modal-content").removeClass("d-none");
validatePunchOuts();
});
}
// Don't run on login page
const notLoginPage = !document.querySelector("#login-form .login-links");
if (notLoginPage) {
$("head").append(
'<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">'
);
showValidator();
}
// Styling Stuff
var bm_css_src = `
ul.ok-tool-bar {
position: fixed;
bottom: 0;
right: 10%;
margin: 0;
}
ul.ok-tool-bar li {
display: inline-block;
padding: 3px 6px;
margin: 0 3px;
font-size: 11px;
white-space: nowrap;
vertical-align: middle;
-ms-touch-action: manipulation;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: none;
border: 1px solid transparent;
border-radius: 5px 5px 0 0;
}
li.btn-ato-validator {
color: #fff;
background-color: #A64123;
border-color: #A64123;
}
body {
margin: auto;
}
#atoModelPunchoutModal .modal-xl {
margin: 0;
width: 100%;
max-width:100%;
}
.wrap-collabsible {
margin-bottom: 1.2rem 0;
}
input.toggle[type='checkbox'] {
display: none;
}
.lbl-toggle {
display: block;
padding: 5px 10px;
color: #333;
background: #ddd;
cursor: pointer;
transition: all 0.25s ease-out;
}
.lbl-toggle:hover {
color: #7C5A0B;
}
.lbl-toggle::before {
content: ' ';
display: inline-block;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 5px solid currentColor;
vertical-align: middle;
margin-right: .7rem;
transform: translateY(-2px);
transition: transform .2s ease-out;
}
.toggle:checked + .lbl-toggle::before {
transform: rotate(90deg) translateX(-3px);
}
.collapsible-content {
max-height: 0px;
overflow: hidden;
transition: max-height .25s ease-in-out;
}
.toggle:checked + .lbl-toggle + .collapsible-content {
max-height: none;
}
.toggle:checked + .lbl-toggle {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.collapsible-content .content-inner {
padding: .5rem 1rem;
}
`;
// eslint-disable-next-line no-undef
GM_addStyle(bm_css_src);