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