Oracle Sales Cloud provides a comprehensive set of customization tools and configuration options to implement customer-specific business cases. In this article I would like to put a spot light on an imaginary situation
Such an extension can be easily implemented via Custom UI, Sales Cloud Application Composer and Sales Cloud REST interfaces. For custom UI multiple options exist such as using Oracle JET, or Ionic/HTML5 with AngularJS. My teammate Angelo Santagata has published recently a great article describing the interface between Oracle Sales Cloud and Oracle JET. Please refer to the article when you want learn more about Mobile UI creation via Oracle JET. In this article I’ll show a generic approach how a custom user interface built in HTML and Javascript could look like. The focus for this article will also be on Sales Cloud Child Objects and addressing them via REST interfaces.
Before we start implementing such a solution our customer must be clear about the functional requirements:
In a next step the new extension objects must be setup in AppComposer in Sales Cloud. Eventually a new page will be created that allows the creation and maintenance of the admired competitive information. So far no coding had to be done as all of these steps work on a declarative base.
These new objects will be addressable by REST API's and the creation of a custom UI follows as a technical implementation step. A base decision has to be made about the technical implementation:
The best way to add Account related custom information can be done by creation of a Child Object in App Composer. In the beginning we must make a decision to create the data structures for this extension as Child Object or a Standalone Object. Standalone Objects are new objects that exist autonomously as new data entities in OSC, while Child Objects have a fixed relation to an existing Standard Object like Accounts, Addresses, Contacts, Sales Orders or others. In our specific use-case the definition of a Child Object under Accounts makes most sense as the additional information is tied to specific accounts and exist in that specific context. Standalone Objects might cause more overhead for creation of a relationship to existing Accounts. So there wouldn't be a benefit to use them, while Child Objects fulfill exactly the requirements as needed in this business case.
Picture below shows the solution once a Child Object extension for storing competitive intelligence information has been set up. An additional icon represents a custom page to enter and maintain the competitive information for a specific Account. No coding is required as being explained further down in this article. Creation of such an extension is a pure declarative activity and data structures will resist lifecycle maintenance operations.
Once the Child Objects have been created and user clicked on custom icon above, a new page will be opened to enter all known information about competitor products installed at this Account. Least number of information is mandatory like Competitor Name and a Remark with some qualifying information. A Service Engineer onsite might be under time pressure or have no access to other sources of competitive intelligence like pricing. We want him to enter the information he is capable to gather – sales employees can enrich the information at a later stage. Once a field in Child Object structure is registered as mandatory it will be also a required field in REST structure. For a better flexibility it is sufficient to make just those fields mandatory that are really qualifying an information.
The starting point for creation of a Child Object is the Objects menu in AppComposer as shown in screenshot below. Choose Standard Objects and Accounts to create a custom structure for competitive intelligence being related to a customer record.
In our case we provide information for field definitions and access management as we must our service engineers able to enter information.
The new Child Objects exist will run in a user specific sandbox, but can also published to the global sandbox once this solution will be made final. Why is the usage of sandboxes crucial for the development of extensions like this? As the name says a sandbox is an isolated runtime environment for every user subscribing to it. A user can subscribe to any sandbox, but not use more than one single sandbox at a time. Using sandboxes will decrease the risk of harming the entire system: if issues appear as a result of customizations and extensions they will run only in an isolated environment and not harm other business processes, UI's or affect other functionalities. Only when a solution in a sandbox has been accurately tested and certified for a more common usage it should be published and made available globally as part of a careful maintenance activity. Its important to mention that all steps for registration of Child Objects and usage of their REST API's are tied to a specific sandbox and only those users have a benefit of their usage who have applied to the specific sandbox where they have been created.
As shown in screenshot below we’ve created a record name containing the competitors name for our custom record.
For the record structure we’ve chosen the following fields
We should bear in mind that our service engineers are not usually the audience working with core customer data. For a perfect fitting access management we should consider to create a special role for our service engineers and give them exactly the grant to manage competitive information they need.
Once done, we’re good to collect the competitive information per account. This is available yet as shown in screenshot above. Means the members of sales team can start entering/maintaining the information. However the Sales Cloud UI might provide too many interactions for service engineers. For that audience we will create a more simplified UI to create information via the REST interface of our Child Object.
The REST structure of Sales Cloud objects is documented here. For a full introduction to Sales Cloud REST APIs refer to Arvind’s blog!
By using the URL https://<mysalescloud.oraclecloud.com>/crmCommonApi/resources/latest/accounts/describe we will retrieve information about the structure for Account REST API as shown below in Google Chrome’s extension Postman.
While the information above is probably known we might wonder how to address the custom Child Object for Competitive Information via REST. When editing the Child Objects in AppComposer we find the internal name set for the ChildCollection as “CompetitorCollection_c “ – this name is usually derived from Display Name of Child Object (“Competitor”) concatenated with “Collection_c”. The child structure in REST uses the same name concatenated with some context information: "OrganizationDEO_CompetitorCollection_c” as shown in screenshot below.
Where do these context information come from? As a hint you might want to check the main page for our Child Object in AppComposer. There you will find “OrganizationProfile” as a parent object. Means the Child Objects are all linked to Organization Profile as additional information for this Standard Object. This link is fixed and cannot be changed to another node in the REST structure hierarchy.
As shown on top we received the entire Account structure in REST by using the “describe” qualifier as part of the URL. Our Child object description is embedded in that huge output, so that we have to search for it. Once found it looks like this as a declaration:
... { "rel": "child", "href": "https://<mysalescloud.oraclecloud.com>:443/crmCommonApi/resources/11.1.11/accounts/{id}/child/OrganizationDEO_CompetitorCollection_c", "name": "OrganizationDEO_CompetitorCollection_c", "kind": "collection", "cardinality": { "value": "1 to *", "sourceAttributes": "OrganizationProfileId", "destinationAttributes": "OrganizationProfile_Id_c" } }, ...
Further down in REST structure we find more details about our child object – below shown for the Child Object structure and the field definition for “ProductNameInstalled” as a sample:
... }, "children": { ... "OrganizationDEO_CompetitorCollection_c": { "discrColumnType": false, "title": "Competitor", "titlePlural": "Competitors", "attributes": [ { "name": "Id", "type": "integer", "updatable": false, "mandatory": true, "queryable": true, "allowChanges": "never", "precision": 32, "hasDefaultValueExpression": true, "title": "Record ID", "properties": { "fnd:FND_AUDIT_ATTR_ENABLED": "false" } }, { "name": "RowType", "type": "string", }, ... { "name": "ProductNameInstalled_c", "type": "string", "updatable": true, "mandatory": false, "queryable": true, "allowChanges": "always", "precision": 80, "title": "Product Name Installed", "maxLength": "80", "properties": { "protectionObjectTitle": "Competitor", "fnd:OSN_ENABLED_ATTR": "true", "TOOLTIP": "Name of competitors product installed at our clients side", "protectionKey": "Competitor_c.ProductNameInstalled_c", "DISPLAYWIDTH": "50", "description": "Name of competitors product installed at our clients side", "protectionState": "TOKENIZED", "AttributeType": "Text", "ExtnCustom": "Y" } }, ...
As the definition shows the information and fields for every specific Child Object are related to an {id} in the hierarchy. In our case for the Standard Object “Accounts” the unique key “PartyNumber” of parent object represents this ID. Means: without knowing the value of “PartyNumber” for a specific Account we can’t address the attached Child Object.
With the knowledge above about REST structures and addressing we will show in a sample how to retrieve and enter data for competitive information via REST.
One choice to evaluate the data would be Postman as a Google Chrome extension. As described in the standard Oracle Docs we can add parameters to our REST call to add some filter conditions. The screenshot below shows a sample where we look for a customer called “Willis Towers”. In the JSON result we find the value for PartyNumber (2nd item below): 34014. Now we got the information we need to access our child objects!
For those people who prefer a command line call to retrieve the data they could use curl with the following parameters:
curl -u <user>:<passwd> -H "Content-Type:application/json" \ -H "Accept: application/json" -k -X GET \ https://<mysalescloud.oraclecloud.com>/crmCommonApi/resources/latest/accounts?onlyData\&limit=200\&q=OrganizationName="Willis%20Towers"
The resulting JSON will look like this:
{ "items" : [ { "PartyId" : 300000007548087, "PartyNumber" : "34014", "SourceSystem" : null, "SourceSystemReferenceValue" : null, "OrganizationName" : "Willis Towers", "UniqueNameSuffix" : null, "PartyUniqueName" : "Willis Towers", "Type" : "ZCA_PROSPECT", "OwnerPartyId" : 300000006885963, "OwnerPartyNumber" : "31005", "OwnerEmailAddress" : "john.doe@foo.com", "OwnerName" : "John Doe", ... }
Coming back to our sample for the service engineers having a custom application to collect competitive information we can follow the following algorithm as a sample:
As shown in the Postman screenshot below we filter the PartyNumber and PartyUniqueName in our call. The name value will be shown in custom app later while the number will be used to address the Child Object.
The call via curl would look like this:
curl -u <user>:<passwd> -H "Content-Type:application/json" -H "Accept: application/json" -k X GET \ https://<mysalescloud.oraclecloud.com>/crmCommonApi/resources/latest/acounts?onlyData\&limit=200\&q=OwnerName="Ulrich%20Janke"\&fields=PartyNumber,PartyUniqueName
Maybe worth to mention that we get an overview about number of records found, any limits for number or records in result set as defined in the REST call and information that more records exist:
{ "items" : [ { "PartyId" : 300000007548087, "PartyNumber" : "34014", ... } ], "count" : 2, "hasMore" : false, "limit" : 200, "offset" : 0, "links" : [ { "rel" : "self", "href" : "https://<mysalescloud.oraclecloud.com>/:443/crmCommonApi/resources/11.1.11/accounts", "name" : "accounts", "kind" : "collection" } ] ...
It’s a business decision, but also a question of usability whether a long list of customers would make sense to be shown in custom UI. Apparently it would make rather sense to restrict it to a number less than 200 and to use any other filter condition. Solution details are not part of the scope in this article, but maybe a SR number or something similar would be better choice.
Once our service engineer has opened the custom app and entered the competitive intelligence data, the final action is to trigger a REST call to update the values of according Child Object by pressing the Submit button.
For this action two things are important to mention (also explained in detail in Oracle Docs):
Screenshot below shows a sample REST call in Postman:
With curl the same REST call would look like this:
curl -u <user>:<passwd> -H "Content-Type: application/vnd.oracle.adf.resourceitem+json " \ -H "Accept: application/json" -k X PATCH \ -d '{\ "RecordName":"Bad & Expensive",\ "CompetitorName_c":"Deal late in 2014",\ "ProductNameInstalled_c":"UJ/890-7896/ABC-890",\ "NumberOfItemsInstalled_c":2, \ "ProductInstallationDate_c":"2014-11-01",\ "ProductExpirationDate_c":"2019-11-01"\ }' \ https://<mysalescloud.oraclecloud.com>/crmCommonApi/resources/latest/accounts/{partyID}/child/OrganizationDEO_CompetitorCollection_c
Once the call was executed successfully on server he will send back the complete new child record containing the competitive information.
Finally the screenshot below shows a sample UI as written in plain HTML and Javascript.
As mentioned in the beginning any JS framework including Oracle JET in combination with Oracle Mobile Cloud or Java Cloud Service would make sense to host such a custom app. The reason to provide a generic code sample is based on the consideration that we focus in this article about the logical structure to handle the access to Child Objects. A plain HTML sample is more compact and might be easier to read or to be used in an own test application other than some framework specific solution.
Below you can find the sample implementation in HTML:
<!DOCTYPE html> <html> <head> <title>Oracle Sales Cloud - Rest Access for Child Objects</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> form { width: 40em; } #h2 { font-weight: bold; font-size: 150%; width: 90%; margin-left: 30%; margin-bottom: 5%; height: 10%; float: left; } input, label.input { float: left; width: 40%; } select, select.option { width: 45%; margin-left: 30%; } select { margin: 0 0 1em .2em; padding: .2em .5em; background-color: #aaeee0; border: 1px solid #e7c157; font-weight: bold; } input { margin: 0 0 1em .2em; padding: .2em .5em; background-color: #fffbf0; border: 1px solid #e7c157; } label.input { text-align: right; line-height: 1.5; font-weight: bold; } label.input::after { content: ": "; } button { float: right; width: 30%; } </style> </head> <body> <main> <form id="competitiveInfo"> <label id="h2" form="compInfo">Competitive Information</label> <label class="input" for="customerList">Customer</label> <select id="customerList" name="customerList"></select> <label class="input" for="compInfo">Competitor Name</label> <input type="text" id="compName" maxlength="100" required> <label class="input" for="compInfo">Additional Competitor Info</label> <input type="text" id="compInfo" maxlength="100"> <label class="input" for="prodName">Product Name/Type</label> <input type="text" id="prodName" maxlength="100" required> <label class="input" for="numItems">Number of items</label> <input type="number" id="numItems" min="1" max="100" required> <label class="input" for="installDate">Installation Date</label> <input type="date" id="installDate"> <label class="input" for="expireDate">Expiration Date</label> <input type="date" id="expireDate"> <button type="submit" id="mySubmit">Submit</button> <button type="reset" id="myReset">Clear</button> </form> </main> <script> var handleSubmit = document.getElementById("mySubmit"); var getCustData = null; var sendCustData = null; handleSubmit.addEventListener ('click', doSubmit); document.addEventListener("DOMContentLoaded", doPastDocLoad); function doPastDocLoad() { var custDataURL = "https://<mysalescloud.oraclecloud.com>/crmCommonApi/resources/latest/accounts?onlyData&limit=200&q=OwnerName=John%20Doe&fields=PartyNumber,PartyUniqueName"; getCustData = new XMLHttpRequest(); getCustData.onreadystatechange = processCustDataRequest; getCustData.open( "GET", custDataURL, true ); getCustData.setRequestHeader("content-type", "application/json"); getCustData.setRequestHeader("accept", "application/json"); getCustData.setRequestHeader("Allow-Control-Allow-Origin", "*" ); getCustData.send( null ); } function processCustDataRequest() { if ( getCustData.readyState === XMLHttpRequest.DONE && getCustData.status === 200 ) { var myCusts = getCustData.responseText; var custData = JSON.parse(myCusts); if( custData.hasMore ) alert("More than " + custData.limit + " customers existing! Just showing first " + custData.count + " records ..."); var custDataOpts = ''; if( custData.count === 0 ) { custDataOpts = '<option value=0>NO CUSTOMER ASSIGNED</option>'; document.getElementById('customerList').innerHTML = custDataOpts; document.getElementById('mySubmit').setAttribute("disabled", "true"); } else { for (var i = 0; i < custData.count; i++) { custDataOpts += '<option value="'+ custData.items[i].PartyNumber + '">' + custData.items[i].PartyUniqueName + '</option>'; } document.getElementById('customerList').innerHTML = custDataOpts; } } } function doSubmit() { var custList = document.getElementById("customerList"); var partyID = custList.options[custList.selectedIndex].value; var compName = document.getElementById("compName").value; var compInfo = document.getElementById("compInfo").value; var prodName = document.getElementById("prodName").value; var numItems = document.getElementById("numItems").value; var installDate = document.getElementById("installDate").value; var expireDate = document.getElementById("expireDate").value; if ( compInfo === '') compInfo = compName; var patchBodyString = '{ "RecordName": "' + compName + '",' + '"CompetitorName_c": "' + compInfo + '",' + '"ProductNameInstalled_c": "' + prodName + '", ' + '"NumberOfItemsInstalled_c": ' + numItems; if (installDate.toString() !== '') patchBodyString = patchBodyString + ', "ProductInstallationDate_c": "' + installDate + '"'; if (expireDate.toString() !== '') patchBodyString = patchBodyString + ', "ProductExpirationDate_c": "' + expireDate + '"'; patchBodyString = patchBodyString + ' }'; var patchBody = JSON.parse(patchBodyString); try { sendCustData = new XMLHttpRequest(); sendCustData.onreadystatechange = procCompUpdReq; var updCustDataURL = "https://<mysalescloud.oraclecloud.com/crmCommonApi/resources/latest/accounts/" + partyID + "/child/OrganizationDEO_CompetitorCollection_c"; sendCustData.open( "PATCH", updCustDataURL, true ); sendCustData.setRequestHeader("Content-Type", "application/vnd.oracle.adf.resourceitem+json"); sendCustData.setRequestHeader("Allow-Control-Allow-Origin", "*" ); sendCustData.send( patchBody ); } catch ( e ) { if ( e instanceof TypeError ) console.log("TypeError occured!"); if ( e instanceof SecurityError ) console.log("SecurityError occured!"); if ( e instanceof InvalidAccessError) console.log("InvalidAccessError occured!"); console.log(e.message); console.log(e.name); console.log(e.fileName); console.log(e.lineNumber); console.log(e.columnNumber); console.log(e.stack); alert(“Failure: “ + e.message ); } } function procCompUpdReq() { if ( sendCustData.readyState === XMLHttpRequest.DONE ) { var respText = sendCustData.responseText; var respData = JSON.parse(respText); if (sendCustData.status !== 200) { alert("Failed! Status= " + sendCustData.status + " readyState=" + sendCustData.readyState); } else { alert("Competitive Information successfully updated!"); } } } </script> </body> </html>
In this article I didn’t handle some topics as they were already mentioned in Angelo’s blog post about Oracle JET and Sales Cloud:
Next Post