NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name ATO Model Helper // @version 0.20 // @description Helper tool to assist in ATO models NPI and EOL process in CPQ // @author Obada Kadri // @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 // @grant GM_addStyle // @license MIT // ==/UserScript== let nextPosition = {}; let fromHwAtoConfigModel = {}; let fromHwSMHierarchy = {}; let fromExtCfgDetails = {}; const colorDict = {}; const PRODUCT_FAMILY = 'SWCPQ'; const PF_SEGMENT = 'hwato'; const HW_SYSTEM_TYPE = "HW ATO Config" const tileColors = [ 'echoSignBackground', 'greenBackground', 'pinkBackground', 'blueBackground', 'orangeBackground', 'burgundyBackground', 'darkblueBackground', 'darkBurgundyBackground', 'grayBackground', ]; const productLines = [ { variable: "networkingProducts", genericModel: 'networkingProductsGeneric', product_line_label: "Networking Products", icon: "quoteCrossProductServices.png", smhCode: "SMHW100100", color: 0, }, { variable: "engineeredSystems", genericModel: 'engineeredSystemsGeneric', product_line_label: "Oracle Engineered Systems", icon: "quoteCrossProductServices.png", smhCode: "SMHW100031", color: 0, }, { variable: "sPARCServers", genericModel: 'sPARCServersGeneric', product_line_label: "Sparc Servers", icon: "quoteCrossProductServices.png", smhCode: "SMHW100001", color: 0, }, { variable: "tapeStorage", genericModel: 'tapeStorageGeneric', product_line_label: "Tape Storage", icon: "quoteCrossProductServices.png", smhCode: "SMHW100061", color: 0, }, { variable: "cGBUProducts", genericModel: 'cGBUGeneric', product_line_label: "CGBU Products", icon: "quoteCrossProductServices.png", smhCode: "SMHW100082", color: 0, }, { variable: "fujitsuSPARCServers", genericModel: 'fujitsuSPARCServersGeneric', product_line_label: "Fujitsu SPARC Servers", icon: "quoteCrossProductServices.png", smhCode: "SMHW100130", color: 0, }, { variable: "nASStorage", genericModel: 'nASStorageGeneric', product_line_label: "NAS Storage", icon: "quoteCrossProductServices.png", smhCode: "SMHW100051", color: 0, }, { variable: "netraSystems", genericModel: 'netraSystemsGeneric', product_line_label: "Netra Systems", icon: "quoteCrossProductServices.png", smhCode: "SMHW100079", color: 0, }, { variable: "x86Servers", genericModel: 'x86ServersGeneric', product_line_label: "x86 Servers", icon: "quoteCrossProductServices.png", smhCode: "SMHW100131", color: 0, }, { variable: "internalCommercial", genericModel: 'internalCommercialGeneric', product_line_label: "Oracle\nEng Systems Exadata Upgrades", icon: "quoteCrossProductServices.png", smhCode: "SMHW100291", color: 0, }, { variable: "sANStorage", genericModel: 'sANStorageGeneric', product_line_label: 'SAN Storage', icon: "quoteCrossProductServices.png", smhCode: "", color: 0, }, { variable: "oracleCloudAtCustomer", genericModel: 'oracleCloudAtCustomerGeneric', product_line_label: 'Oracle Cloud at Customer', icon: "quoteCrossProductServices.png", smhCode: "SMHW100323", color: 0, }, { variable: "genericF", genericModel: 'generic', product_line_label: 'CHANGE_ME', icon: "quoteCrossProductServices.png", }, ]; const capitalizeFirstLetter = (string) => { return string.charAt(0).toUpperCase() + string.slice(1); }; // Datatable API const getApiTableName = (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 (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 = (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, (data) => { if (data.count && data.hasMore) { let nextOffset = offset + 1000; allData = allData.concat(data.items); getTableData(tableName, query, nextOffset, allData, dTableData); } else { dTableData.resolve(allData.concat(data.items)); } }).fail(() => { dTableData.reject(); }); return dTableData.promise(); }; const getTableInfo = async (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; }; const sanitizeCell = (desc) => { let itemDesc; if (desc) { if (isNaN(desc)) { itemDesc = (desc + '').replace(/(\r\n|\n|\r|\s+|\t| )/gm,' '); itemDesc = itemDesc.replace(/"/g, '""'); itemDesc = itemDesc.replace(/ +(?= )/g,''); if (itemDesc.indexOf('"') >= 0 || itemDesc.indexOf(',') >= 0) { itemDesc = '"' + itemDesc + '"'; } } else { itemDesc = desc; } } else { itemDesc = ''; } return itemDesc; }; const dataToCsv = (tableMetaData, tableData, includeUpdate = true) => { let output = '_start meta data' + tableMetaData.columns.map(col => '').join(',') + '\n'; output += (includeUpdate ? '_update_action,' : '') + tableMetaData.columns.map(col => col.title).join(',') + '\n'; output += (includeUpdate ? 'String,' : '') + tableMetaData.columns.map(col => capitalizeFirstLetter(col.type)).join(',') + '\n'; output += (includeUpdate ? ',' : '') + tableMetaData.columns.map(col => sanitizeCell(col.description)).join(',') + '\n'; output += '_end meta data' + tableMetaData.columns.map(col => '').join(',') + '\n'; output += tableData.map(row => (includeUpdate ? 'modify,' : '') + tableMetaData.columns.map(col => sanitizeCell(row[col.title.toLowerCase()]) ).join(',')).join('\n'); return output + '\n'; }; const render = (parentId, title, body, erase = false) => { if (erase) $(`#${parentId}`).empty(); let renderEl = $('<div></div>'); // Title renderEl.append(`<div class="section-title"><strong>${title}</strong></div>`); // Content renderEl.append(body); $(`#${parentId}`).append(renderEl); }; const renderAtoSelectOperation = async () => { if (!fromHwAtoConfigModel.isValid) { fromHwAtoConfigModel = await getTableInfo("hwATOconfigModel"); console.info('fromHwAtoConfigModel', fromHwAtoConfigModel); fromHwSMHierarchy = await getTableInfo("hwSMHierarchy"); console.info('fromHwSMHierarchy', fromHwSMHierarchy); } const content = ` <div class="list-group"> <a href="#" class="list-group-item list-group-item-action list-group-item-success operation-type" data-action="add">Add a Model</a> <a href="#" class="list-group-item list-group-item-action list-group-item-danger operation-type" data-action="remove">Remove a Model</a> </div> `; render("ato-helper-body", `Select Operation`, content, true); $(".operation-type").on("click", (e) => { const selectedAction = $(e.currentTarget).data('action'); if (selectedAction == 'add') { npiCollectRequiredInfo(); } else if (selectedAction == 'remove') { eolCollectRequiredInfo(); } }); }; const setColorDict = () => { const sortedL1Arr = fromHwSMHierarchy.rawData .filter(obj => !obj.smh_code_parent) .sort((obj1, obj2) => obj1.smh_desc_child > obj2.smh_desc_child ? 1 : obj1.smh_desc_child < obj2.smh_desc_child ? -1 : 0); sortedL1Arr.forEach(l1 => { const plObj = productLines.find(pl => pl.smhCode?.toLowerCase() == l1.smh_code_child.toLowerCase()); if (plObj) { let colorIdx = plObj.color; colorDict[l1.smh_code_child] = {color: tileColors[colorIdx], desc: l1.smh_desc_child, level: 1}; const sortedL2Arr = fromHwSMHierarchy.rawData .filter(obj => obj.smh_code_parent == l1.smh_code_child && obj.smh_code_intermediat) // Only children of L1 that have intermediate level .filter((obj, index, arr) => arr.map(mapObj => mapObj.smh_code_intermediat).indexOf(obj.smh_code_intermediat) === index) // Remove duplicate intermediate levels .sort((obj1, obj2) => obj1.smh_code_intermediat > obj2.smh_code_intermediat ? 1 : obj1.smh_code_intermediat < obj2.smh_code_intermediat ? -1 : 0); console.log(l1.smh_desc_child, sortedL2Arr); sortedL2Arr.forEach(l2 => { colorDict[l2.smh_code_intermediat] = {color: tileColors[colorIdx], level: 2, parent: l2.smh_code_parent}; colorIdx = (colorIdx + 1) % tileColors.length; }); } else { console.warn(`Could not find a matching product line for ${l1.smh_desc_child}`); } }); }; const npiCollectRequiredInfo = () => { const content = ` <form> <div class="form-group"> <label for="part-numbers">Part Number(s)</label> <input type="text" class="form-control ato-info-input required" id="part-numbers" placeholder="Example: 7123456, 7123457, 7123458"> </div> <div class="form-group"> <div class="form-check"> <input class="form-check-input ato-info-input" type="checkbox" value="" id="new-table-mode"> <label class="form-check-label" for="new-table-mode"> New Table Mode (Ignore existing data) </label> </div> </div> <button id="submit-ato-info" class="btn btn-primary" disabled>Submit</button> </form> `; render("ato-helper-body", "New ATO Model", content, true); $("#start-over").removeClass('d-none'); $(".ato-info-input").on("change keyup", (e) => { const emptyInputs = $(".ato-info-input.required").filter((i, e) => $(e).val().trim() === ""); if (emptyInputs.length === 0) { $('#submit-ato-info').prop('disabled', false); } }); $("#submit-ato-info").on("click", (e) => { e.preventDefault(); $("#submit-ato-info").addClass('d-none'); // Removes submit button $(".ato-info-input").prop('disabled', true); // Disable inputs $("#new-table-mode").prop('disabled', true); // Disable inputs const partNumbers = $("#part-numbers").val(); const newTableMode = $("#new-table-mode").prop('checked'); npiCalculateChanges(partNumbers, newTableMode); }); }; const getHwAtoEntry = (partNumber, fromItemMaster, fromHwPartAttributes, fromPartToSMH, newTableMode) => { const hwAtoEntry = {}; let productLine = 'UNKNOWN'; let hwSmhHierarchyRecord, plObj; const hwAtoFields = fromHwAtoConfigModel.metaData.columns.map(col => col.title.toLowerCase()); const itemMasterRecord = fromItemMaster.rawData.find(record => record.partNumber === partNumber); const hwPaRecord = fromHwPartAttributes.rawData.find(record => record.part_number === partNumber); const partToSmhRecord = fromPartToSMH.rawData.find(record => record.part_number === partNumber); if (partToSmhRecord) { hwSmhHierarchyRecord = fromHwSMHierarchy.rawData.find(record => record.smh_code_child === partToSmhRecord.smh_code_child); plObj = productLines.find(pl => pl.smhCode?.toLowerCase() == hwSmhHierarchyRecord.smh_code_parent.toLowerCase()) || {}; productLine = plObj?.variable; const lastPlPosition = fromHwAtoConfigModel.rawData .filter(cm => cm.product_line === productLine) .map(cm => cm.hw_config_position) .sort((n1, n2) => n1 > n2 ? 1 : n1 < n2 ? -1 : 0).pop() || 0; nextPosition[productLine] = nextPosition[productLine] || ( newTableMode ? 1 : lastPlPosition + 1 ); } else { console.warn(`Could not find product line for ${partNumber}`); nextPosition[productLine] = 'UNKNOWN'; } hwAtoFields.forEach(field => { switch(field) { case 'configuration': hwAtoEntry[field] = partNumber; break; case 'config_name': case 'alt_display_name': hwAtoEntry[field] = itemMasterRecord.description; break; case 'product_line': case 'product_link': hwAtoEntry[field] = productLine; break; case 'product_line_label': case 'icon': hwAtoEntry[field] = plObj ? plObj[field] : 'UNKNOWN'; break; case 'color': hwAtoEntry[field] = hwSmhHierarchyRecord ? (hwSmhHierarchyRecord.smh_code_intermediat ? colorDict[hwSmhHierarchyRecord.smh_code_intermediat]?.color : colorDict[hwSmhHierarchyRecord.smh_code_parent]?.color) : 'UNKNOWN' break; case 'product_family': hwAtoEntry[field] = PRODUCT_FAMILY; break; case 'pf_segment': hwAtoEntry[field] = PF_SEGMENT; break; case 'hw_system_type': hwAtoEntry[field] = HW_SYSTEM_TYPE; break; case 'segplinemodelvarname': hwAtoEntry[field] = plObj ? `${PF_SEGMENT}:${productLine}:${plObj.genericModel}` : 'UNKNOWN'; break; case 'hw_config_position': hwAtoEntry[field] = nextPosition[productLine]++; break; case 'is_active': hwAtoEntry[field] = 'Y'; break; case 'is_redundant': hwAtoEntry[field] = 'N'; break; case 'model_type': hwAtoEntry[field] = hwPaRecord.support_exc_cat === 'SE1027' ? 'CLM' : ''; break; case 'is_restricted': hwAtoEntry[field] = itemMasterRecord._part_custom_field12.toLowerCase().indexOf('active') >= 0 || hwPaRecord.support_exc_cat === 'SE1027' ? 'N' : 'Y'; break; case 'inventory_item_id': hwAtoEntry[field] = itemMasterRecord._part_custom_field1; break; default: hwAtoEntry[field] = 'UNKNOWN' console.warn(`Unknown field "${field}"`); } }); return hwAtoEntry; }; const npiCalculateChanges = async (partNumbers, newTableMode) => { const partNumArr = partNumbers.split(',').map(pn => pn.trim()); let fromHwPartAttributes = await getTableInfo("hwPartAttributes", `{$or:[${partNumArr.map(pn => `{part_number:{$eq: '${pn}'}}`).join(',')}]}`); let fromItemMaster = await getTableInfo("_parts", `{$or:[${partNumArr.map(pn => `{partNumber:{$eq: '${pn}'}}`).join(',')}]}`); let fromPartToSMH = await getTableInfo("partToSMH", `{$or:[${partNumArr.map(pn => `{part_number:{$eq: '${pn}'}}`).join(',')}]}`); console.info('fromHwPartAttributes',fromHwPartAttributes); console.info('fromItemMaster', fromItemMaster); console.info('fromPartToSMH',fromPartToSMH); if (!newTableMode && fromHwAtoConfigModel.rawData.some(cm => partNumArr.indexOf(cm.configuration) >= 0)) { render("ato-helper-body", "A Model Already exists in hwATOconfigModel"); } else if (fromHwPartAttributes.rawData.length === partNumArr.length && fromItemMaster.rawData.length === partNumArr.length) { // Construct the hwPartAttributes row setColorDict(); console.log('colorDict', colorDict); const hwAtoEntries = partNumArr.map(pn => getHwAtoEntry(pn, fromItemMaster, fromHwPartAttributes, fromPartToSMH, newTableMode)); render( "ato-helper-body", "Next Steps:", '<table class="table table-striped table-sm table-limited-width"><tr>' + Object .keys(hwAtoEntries[0]) .map(key => `<th>${key}</th>${hwAtoEntries.map(e => `<td class='${e[key] == 'UNKNOWN' ? 'danger' : ''}'>${e[key]}</td>`).join('\n')}` ).join('</tr>\n<tr>') + '</tr></table><br>\n' + `<ol class="steps"><li><a class="btn btn-outline-info btn-sm" href="data:text/csv;charset=utf-8,${encodeURI(dataToCsv(fromHwAtoConfigModel.metaData, hwAtoEntries, false))}" target="_blank" download="${fromHwAtoConfigModel.name}.csv">Download CSV</a> : Import and deploy</li>\n` + `<li><a class="btn btn-outline-info btn-sm" href="https://confluence.oraclecorp.com/confluence/x/LdIJRg" target="_blank">Regenerate JSON</a></li></ol>` ); } else { render("ato-helper-body", "Some Parts are not available in either Item Master, hwPartAttributes, or both"); console.log('parts not in Item Master', partNumArr.filter(pn => !fromItemMaster.rawData.some(d => d.partNumber === pn))); console.log('parts not in hwPartAttributes', partNumArr.filter(pn => !fromHwPartAttributes.rawData.some(d => d.part_number === pn))); } }; const eolCollectRequiredInfo = () => { const content = ` <form> <div class="form-group"> <label for="partNumber">Part Number</label> <input type="text" class="form-control ato-info-input required" id="part-number"> </div> <button id="submit-ato-info" class="btn btn-primary" disabled>Submit</button> </form> `; render("ato-helper-body", "Remove ATO Model", content, true); $("#start-over").removeClass('d-none'); $(".ato-info-input").on("change keyup", (e) => { const emptyInputs = $(".ato-info-input.required").filter((i, e) => $(e).val().trim() === ""); if (emptyInputs.length === 0) { $('#submit-ato-info').prop('disabled', false); } }); $("#submit-ato-info").on("click", (e) => { e.preventDefault(); $("#submit-ato-info").addClass('d-none'); // Removes submit button $(".ato-info-input").prop('disabled', true); // Disable inputs const partNumber = $("#part-number").val(); eolCalculateChanges(partNumber); }); }; const eolCalculateChanges = async (partNumber) => { render( "ato-helper-body", "JET UI Steps (hwATOconfigModel):", `<ol class="steps"><li>Look for entry where <strong>configuration = ${partNumber}</strong> and change <strong>is_active to N</strong>, then deploy</li>\n` + `<li><a class="btn btn-outline-info btn-sm" href="https://confluence.oraclecorp.com/confluence/x/LdIJRg" target="_blank">Regenerate JSON</a></li></ol>` ); }; const showAtoHelper = () => { "use strict"; var modalHtml = ` <!-- Modal --> <div class="modal fade" id="atoHelperModal" role="dialog"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content d-none"> <div class="modal-header"> <h5 class="modal-title">CPQ ATO Model Helper</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-helper-body">Examining CPQ Instance...</div> </div> <div class="modal-footer"> <button type="button" class="btn btn-danger" id="start-over">Start Over</button> <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("ATO Helper"); showPerfStatsBtn.appendChild(text); showPerfStatsBtn.id = "btn-btn-ato-helper"; showPerfStatsBtn.className = "btn-ato-helper"; showPerfStatsBtn.dataset.toggle = "modal"; showPerfStatsBtn.dataset.target = "#atoHelperModal"; //--- Add nodes to page okToolBar.append(showPerfStatsBtn); $("body").append(modalHtml); $("#btn-btn-ato-helper, #start-over").on("click", () => { $(".modal-content").removeClass("d-none"); renderAtoSelectOperation(); $("#start-over").addClass('d-none'); }); }; // Don't run on login page let 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">' ); showAtoHelper(); }; // 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-helper { color: #fff; background-color: #94AFAF; border-color: #94AFAF; } .section-title { margin: 2em 0 1em; } ol.steps { border: 1px solid #aaa; border-radius: 10px; box-shadow: 0 0 20px #aaa; padding-left: 2em; } ol.steps li { margin: 0.7em; } body { margin: auto; } table.table-limited-width { display: block; overflow-x: auto; font-size: 10px; } .danger { color: red; font-weight: bold; } `; GM_addStyle(bm_css_src);