CRM and CX Blogs by Members
Find insights on SAP customer relationship management and customer experience products in blog posts from community members. Post your own perspective today!
cancel
Showing results for 
Search instead for 
Did you mean: 
R_Zieschang
Contributor
5,267
Back to part I

Back to part II

Back to part III

Part IV - Gather data in CCO UI and sent this data to Business One


Introduction


Welcome to the fourth part of this blog series. If you are new to this blog series, please use the links above to follow the prerequisites for this part as we will dive directly into this topic.

 

In this part we will create a plugin, which will gather data directly from the cashier in the SAP Customer Checkout UI. We will use the retail UI for this part. The use case is to have an input field for the customer age for all casual customers. For this we will learn how to inject javascript into the salescreen, create html code, react on the receipt posting to send the data to our plugin via REST POST.
We will also create a user defined table to save the data from the frontend with the receipt key as reference.

When SAP Customer Checkout sends the receipts to the B1i we will use a plugin exit to expand the data with our customer age.

On the b1i site we will use the b1i sample extension scenarios provided at the partner edge but I will also include it in the git repository. The last thing we need to do is adding an UDF field in SAP Business One.

To summarize:

  • Inject javascript code to create an input field

  • create backend logic to receive the input data

  • create a user-defined table to save the data

  • intercept the receipt post to b1i to add the input data

  • import b1i sample extensions

  • add UDF in SAP Business One


Inject Javascript code


As always we create a maven project in which our base class extends the BasePlugin class provided by SAP Customer Checkout. If you need some more details you may read Part I again where I described this in much more detail.

Set Id, Name and Version of your plugin as shown in the screenshot.



To inject javascript code, we will use the annotation @JSInject which is provided by SAP Customer Checkout. This annotation needs to have the property targetScreen set. This determines in which UI screen our javascript will be injected. For our purpose it is the screen "sales".

You can determine the screen like this: start CCO and navigate to the screen you would like to inject your code. Look at the url in your browser:





So "sales" for the first example and "cashdeskclosing" for the second example.

Next thing is, we going to look for a possible place where we can add our input field. This would be the perfect place to place our input field.



Press F12 to open the google chrome developer tools. Click on this litte icon.



With this feature we can select certain parts of the html site to examine it's structure.



The corresponding div part is the customer area. Let's examine what is the structure of this div and determine the right position for our own input area.



The div customer area is split into the customerInfoContainer div and the salesDetailsMain div. So we will place our input are after the customerInfoContainer.

Implement the following method to inject javascript code:
@JSInject(targetScreen = "sales")
public InputStream[] injectJS() {
return new InputStream[] {this.getClass().getResourceAsStream("/resources/salesInject.js")};
}

You may have noticed, that this method returns a stream of a resource from our project which is actually a javascript file. This means we need to create this javascript file.



Now we will add the javascript code. The code is split into three parts I will roughly explain.
// Add the input field
$(document).ready(function () {
$('.customerInfoContainer').after($('<div style="position: relative; right: 0; top: 0; background-color: #FFF; padding: 5px; border: 1px solid #000; width: 25%; height: 68%; font-size: small; margin-left: 5px; margin-right: 5px; margin-top: 5px; float:left" id="customerAgeDiv">Age of Customer: <input type="number" id="customerAge"></div>'));
});
// monkey path the original execute method.
var origMethod = BasicReceiptActionCommand.prototype.execute;


// add our own code and call the original method we monkey patched.
BasicReceiptActionCommand.prototype.execute = function (a, b, c) {
if (a.entity === "createReceipt") {
var receiptKey = $('#receiptEntityKey').val();
var requestBody = '{' + "\"customerAge\" : " + $("#customerAge").val() + ' }';
$.ajax({
type: "POST",
url: "PluginServlet?action=customerAge&receiptId="+encodeURIComponent(receiptKey),
data: requestBody,
contentType: "application/json",
dataType: "json",
success: function(result) {
if (result && result["status"] == 1) {
$("#customerAge").val(0);
}
}
});
}
origMethod.call(this, a, b, c);
};

In the first part we add our own div, which will hold the input field. With the jquery function .after we will determine where our div will be placed. This will be the customerInfoContainer we determined earlier.

The second part is something called "monkey patching". Most of the actions happening on the receipt will be run through the execute function of the BasicReceiptActionCommand. You can have a look at the javascript code with the developer tools of google chrome.

Our code does roughly the following. Save the original execute in a variable we call origMethod.

The third part is implementing our own execute method which is then called by SAP Customer Checkout. In our method we can implement our custom code. After that we call the original SAP Customer Checkout execute function we saved in the variable origMethod earlier.

In our custom code we first check, if SAP Customer Checkout wants to create the receipt.
if (a.entity === "createReceipt") {

We also need to determine the current receiptkey and the entered customer age. The element where we get the entered value is #customerAge which is the id of our input fied (see <input type="number" id="customerAge">).
var receiptKey = $('#receiptEntityKey').val();
var requestBody = '{' + "\"customerAge\" : " + $("#customerAge").val() + ' }';

As we want to send some data to our plugin we will send the data via POST. So we need to send the data as JSON in the http request body.

With the jquery function "ajax" we now send both the receipt key as URL parameter and the customer age in the JSON body. Please also consult the official jquery doc for further information.
        $.ajax({
type: "POST",
url: "PluginServlet?action=customerAge&receiptId="+encodeURIComponent(receiptKey),
data: requestBody,
contentType: "application/json",
dataType: "json",
success: function(result) {
if (result && result["status"] == 1) {
$("#customerAge").val(0);
}
}
});

If our request was sucessfull, we check if our plugin answers with a status == 1 so that we can reset our input field in the UI. We will learn a bit later how to handle a request from the UI and how to send a response from our plugin.

After our POST request we will call the original function we stored earlier in our own variable.
origMethod.call(this, a, b, c);

Now we can build our plugin and we can see our newly created div element with our input field.


Create backend logic to receive the input data


The next thing we need to implement is some logic to handle the POST request we implemented on javascript side. For this we can use the plugin exit "PluginServlet.callback.post". There is also a servlet for get requests which is called "PluginServlet.callback.get". For our use-case we stick to post.

In the objects array we will get a request object and also a response object. The response object we can use to send data back to the ui. These objects are from the type HttpServletRequest and HttpServletResponse.
	@ListenToExit(exitName="PluginServlet.callback.post")
public void pluginServletPost(Object caller, Object[] args) {

HttpServletRequest request = (HttpServletRequest)args[0];
HttpServletResponse response = (HttpServletResponse)args[1];

RequestDelegate.getInstance().handleRequest(request, response);
}

The RequestDelegate is a class we will implement now. I like to handle all the request and response stuff in a seperate class to keep my main plugin class nice and clean.

The request delegate is implemented as a singleton to make sure, that only one instance will ever handle the requests and we will not run into any concurrent problems.
/**
* @author Robert Zieschang
*/
public class RequestDelegate {


private static final Logger logger = Logger.getLogger(RequestDelegate.class);

private CustomerAgeDao customerAgeDao;

private static RequestDelegate instance = new RequestDelegate();
public static synchronized RequestDelegate getInstance() {
if (instance == null) {
instance = new RequestDelegate();
}
return instance;
}

private RequestDelegate() {
this.customerAgeDao = new CustomerAgeDao();
}

public void handleRequest(HttpServletRequest request, HttpServletResponse response) {
String urlMethod = request.getMethod();
String action = request.getParameter("action");
JSONObject responseObj = new JSONObject();

if ("POST".equals(urlMethod)) {
JSONObject requestBody = this.getRequestBody(request);
switch(action) {
case "customerAge":
String receiptKey = request.getParameter("receiptId");
customerAgeDao.save(new CustomerAgeDto(receiptKey, requestBody.getInt("customerAge")));
responseObj.put("status", 1);
break;
}
} else if ("GET".equals(urlMethod)) {

}

if(!responseObj.isEmpty()) {
try (OutputStreamWriter osw = new OutputStreamWriter(response.getOutputStream())) {
osw.write(responseObj.toString());
} catch (IOException e) {
logger.severe("Error while Processing Request");
}
}
}

private JSONObject getRequestBody(HttpServletRequest request) {

StringBuilder sb = new StringBuilder();
try {
BufferedReader br = request.getReader();
String line = null;

while ((line = br.readLine()) != null) {
sb.append(line);
}

} catch (IOException e) {
logger.severe("Error while parsing JSON Request body");
}
return JSONObject.fromObject(sb.toString());
}
}

So what happens here is pretty straight forward. We have a private constructor. We access the instance of the request delegate via the getInstance() method which returns the instance or if not existing, creates a new one.

The handleRequest method is where we extract the url method (POST or GET) and also the action (see salesInject.js action=customerAge). This is used to distinguish between calls from the UI. You can use this to extend or implement further actions or request types.
        if ("POST".equals(urlMethod)) {
JSONObject requestBody = this.getRequestBody(request);
switch(action) {
case "customerAge":
String receiptKey = request.getParameter("receiptId");
customerAgeDao.save(new CustomerAgeDto(receiptKey, requestBody.getInt("customerAge")));
responseObj.put("status", 1);
break;
}
} else if ("GET".equals(urlMethod)) {

}

If the action was customerAge we also get the url parameter receiptId to save in our customerAgeDAO the receiptId and the customerAge the cashier set.

When everything worked fine, we set the status 1 in our respone object. Remember the success part in our salesInject.js?
            success: function(result) {
if (result && result["status"] == 1) {
$("#customerAge").val(0);
}
}

For production use you may implement some further validation.

Create a user-defined table to save the data


In the RequestDelegate class you may have noticed the CustomerAgeDao and the CustomerAgeDto. The DAO is the data-access-object (our user defined table including all methods for accessing the data) and the DTO is the data-transfer-object we use for transfer the data through our plugin.

As we already learned how to do this, I will not go into the details. Have a look into part III if you want to know more.
public class CustomerAgeDao {

private static final Logger logger = Logger.getLogger(CustomerAgeDao.class);

private static final String TABLE_NAME = "HOK_CUSTOMERAGE";

private static final String QUERY_CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
+ "RECEIPTKEY varchar(100) not null PRIMARY KEY,"
+ "CUSTOMERAGE INT not null)";

private static final String QUERY_INSERT_ROW = "INSERT INTO " + TABLE_NAME + " VALUES(?,?)";

private static final String QUERY_UPDATE_ROW = "UPDATE " + TABLE_NAME
+ " SET RECEIPTKEY = ?2 WHERE CUSTOMERAGE = ?1";


// query for find all
private static final String QUERY_FIND_ALL = "SELECT RECEIPTKEY, CUSTOMERAGE FROM " + TABLE_NAME;

// query for find one
private static final String QUERY_FIND_ONE = "SELECT RECEIPTKEY, CUSTOMERAGE FROM " + TABLE_NAME
+ " where RECEIPTKEY = ?";

// query to drop one
private static final String QUERY_DROP_ONE = "DELETE FROM " + TABLE_NAME + " WHERE RECEIPTKEY = ?";


public void setupTable() {
CDBSession session = CDBSessionFactory.instance.createSession();
try {
session.beginTransaction();
EntityManager em = session.getEM();

Query q = em.createNativeQuery(QUERY_CREATE_TABLE);
q.executeUpdate();

session.commitTransaction();
logger.info("Created table " + TABLE_NAME);

} catch (Exception e) {
session.rollbackDBSession();
logger.info("Error or table " + TABLE_NAME + " already existing");
} finally {
session.closeDBSession();
}
}

private void save(CustomerAgeDto customerAge, boolean isAlreadyInDB) {

CDBSession session = CDBSessionFactory.instance.createSession();
String query = isAlreadyInDB ? QUERY_UPDATE_ROW : QUERY_INSERT_ROW;

try {
session.beginTransaction();
EntityManager em = session.getEM();

Query q = em.createNativeQuery(query);
q.setParameter(1, customerAge.getReceiptKey());
q.setParameter(2, customerAge.getCustomerAge());

q.executeUpdate();
session.commitTransaction();

} catch (Exception e) {
session.rollbackDBSession();
logger.info("Could not create CustomerAge");
logger.info(e.getLocalizedMessage());
} finally {
session.closeDBSession();
}
}

public List<CustomerAgeDto> findAll() {
CDBSession session = CDBSessionFactory.instance.createSession();
ArrayList<CustomerAgeDto> resultList = new ArrayList<>();

try {
session.beginTransaction();
EntityManager em = session.getEM();

Query q = em.createNativeQuery(QUERY_FIND_ALL);
@SuppressWarnings("unchecked")
List<Object[]> results = q.getResultList();

if (results.isEmpty()) {
// return an empty list rather than null
return resultList;
}

for (Object[] resultRow : results) {
resultList.add(new CustomerAgeDto((String) resultRow[0], (Integer) resultRow[1]));
}

} catch (Exception e) {
logger.info("Error while getting results from table " + TABLE_NAME);

} finally {
session.closeDBSession();

}

return resultList;
}

public CustomerAgeDto findOne(String inReceiptKey) {
CDBSession session = CDBSessionFactory.instance.createSession();
CustomerAgeDto customerAge = new CustomerAgeDto();

try {

EntityManager em = session.getEM();
Query q = em.createNativeQuery(QUERY_FIND_ONE);
q.setParameter(1, inReceiptKey);

@SuppressWarnings("unchecked")
List<Object[]> results = q.getResultList();

if (results.isEmpty()) {
// return empty dto
return customerAge;
}

customerAge.setReceiptKey((String) results.get(0)[0]);
customerAge.setCustomerAge((Integer) results.get(0)[1]);

} catch (Exception e) {
logger.info("Error while getting " + inReceiptKey + " from table " + TABLE_NAME);

} finally {
session.closeDBSession();
}

return customerAge;
}


public void dropOne(String inReceiptKey) {
CDBSession session = CDBSessionFactory.instance.createSession();

try {
session.beginTransaction();
EntityManager em = session.getEM();
Query q = em.createNativeQuery(QUERY_DROP_ONE);
q.setParameter(1, inReceiptKey);
q.executeUpdate();
session.commitTransaction();

} catch (Exception ex) {

} finally {
session.closeDBSession();
}
}


public void dropAll() {
List<CustomerAgeDto> list = this.findAll();

for (CustomerAgeDto entry : list) {
this.dropOne(entry.getReceiptKey());
}
}


public void save(CustomerAgeDto customerAge) {
CustomerAgeDto customerAgeInDb = this.findOne(customerAge.getReceiptKey());
boolean isAlreadyInDb = customerAge.getReceiptKey().equals(customerAgeInDb.getReceiptKey());
// check if entity is already in database, so that we update rather than insert.
this.save(customerAge, isAlreadyInDb);

}

public class CustomerAgeDto {

private String receiptKey;

private Integer customerAge;

public CustomerAgeDto() {

}

public CustomerAgeDto(String inReceipKey, Integer inCustomerAge) {
this.receiptKey = inReceipKey;
this.customerAge = inCustomerAge;
}

public String getReceiptKey() {
return receiptKey;
}

public void setReceiptKey(String receiptKey) {
this.receiptKey = receiptKey;
}

public Integer getCustomerAge() {
return customerAge;
}

public void setCustomerAge(Integer customerAge) {
this.customerAge = customerAge;
}
}

 

Intercept the receipt post to b1i to add the input data


Lets wrap up. We created an input field in the UI. We sent the data to our plugin via the plugin servlet and we saved the data in a user defined table. Now we are implementing a method to intercept the receipt which is going to be sent to our B1i server.
	@ListenToExit(exitName="BusinessOneServiceWrapper.beforePostInvoiceRequest")
public void enlargeB1iMessage(Object caller, Object[] args) {

ReceiptDTO receiptDto = (ReceiptDTO) args[0];
PostInvoiceType request = (PostInvoiceType) args[1];
CustomerAgeDto customerAge = this.customerAgeDao.findOne(receiptDto.getKey());
if (null != customerAge.getReceiptKey()) {
GenericValues.KeyValPair keyValPair = new GenericValues.KeyValPair();
keyValPair.setKey("customerAge");
keyValPair.setValue(customerAge.getCustomerAge().toString());

if (null == request.getGenericValues()) {
request.getSale().getDocuments().setGenericValues(new GenericValues());
}

request.getSale().getDocuments().getGenericValues().getKeyValPair().add(keyValPair);
args[1] = request;
}
}

So lets examine, what we are doing here. We listen to the exit BusinessOneServiceWrapper.beforePostInvoiceRequest. In the array of objects we get the receiptDTO and the request. We use our findOne method of the customerAgeDao to find the record in our user-defined table.

If we found a record we create a key-value pair object. The key is customerAge and the value the age the cashier set for this receipt. When we have set key and value we are adding our KeyValPair object to the document.

When everything is set and done we just adding the the modified request in the array.

Now when we are opening the SAPCustomerCheckout monitor (normally http://localhost:8080/SAPCustomerCheckoutB1i), search for a receipt which was created with our plugin and open the incoming message.





There it is! Our KeyValPair we set in our plugin!

Import b1i sample extensions


Now we are going to use a scenario which is provided by SAP as an example for extending the standard SAP Customer Checkout scenarios.

You can download these package in the partner edge. I also included them into the git repository.

Click this link. Now you can download the package here:



After you have download this package open your B1i (normally: http://localhost:8080/B1iXcellerator) and click on the scenarios tab. Now choose the import link.



Now go to the setup and choose the scenario sap.CustomerCheckout and click on Data Mgt.



Now we choose the IncludeList.



Now we get all the possible extension points presented where we can extend the standard behaviour of the SAP Customer Checkout B1i scenarios.



Note whenever you intend to use the extension points. Consume the payload you get from the scenario and return the same structure. Otherwise the whole scenario may stop working.

The sap.CCO.ReceiptUDFV2 scenario step takes all the KeyValPairs in the documents section and set U_[key] = [value].
So you can add whatever KeyValPair you want and the combination of the standard SAP Customer Checkout scenarios with this example will save this into SAP Business One.

In the next chapter we will see, what we are going to achieve with this.

Add UDF in SAP Business One


In the last chapter we will prepare our Business One to receive our customer age. Lets add a user-defined field. As I explained before we set the key customerAge. So name your UDF customerAge. The key of the keyValPair will always be the name of your UDF in Business One.



When everything works you will now have the customer age from your CCO which was set by the cashier in your SAP Business One!


Wrap up


We learned how to inject javascript code into the sales screen and how to determine which targetName to choose. Also we know now how to send data from the UI to a backend part of our plugin and to handle this data and how to response to the UI.

Furthermore we are now able to expand and modify the data which is sent from SAP Customer Checkout to B1i and how to use the extension points provided by the SAP Customer Checkout B1i scenarios.

That's all for this part. If you have any questions, please do not hesitate to drop me a line.

The code is, as always, hosted on github.

https://gitlab.com/ccoplugins/blogpluginpart4

 

The plugin is licensed under the MIT License.
20 Comments
kvbalakumar
Active Contributor
Wow.... Excellent and so detailed! Thanks buddy for the blog post.

Regards

Bala
cameron_fischer2
Explorer
0 Kudos
Thanks for the useful info. Do you have any suggestions on how to extend the dialog screens? I want to add information on the customer create/update. The jsp loads these js files after the plugin files so the regular jquery functions cannot find the files, I've had to get my plugins to wait for the window ready event and then trigger my jquery functions.
R_Zieschang
Contributor
0 Kudos
Hi cameron.fischer2 , sorry for the late reply. I was on vacation.

I assume you are going to use retail UI?

 

Regards

Robert
former_member609283
Participant
0 Kudos
Hi Robert,

Thanks for the blog it's really helpful.

But in the B1i integration part after importing extension package the file "sap.CCO.ReceiptUDFV2" doesn't shows.Kindly guide me in this regard.
R_Zieschang
Contributor
0 Kudos
Hi idrees.mohsin,

which b1i integration framework did you use? I built this scenario with version 1.0.

Are there any error messages while importing?

Regards

Robert

 
former_member609283
Participant
0 Kudos
Hi Robert,

I am importing the same extension file you have mentioned above "sap.CCO.Extention_V3.0.0".
former_member609283
Participant
Hi Robert,

Thank you sooo much...i did this and it works well now i am working on user defined field part, will let you know if there is any further query. 😉
R_Zieschang
Contributor
0 Kudos
Hi idrees.mohsin ,

glad that it worked so far. May I ask what was the problem? So that other users can benefit from this and won't step into this trap, also?

 

Regards

Robert
former_member609283
Participant
0 Kudos
Hi Robert,

I hope you are doing well.I read your blog part four and tried it,now it's working well.Thank you very much for that blog it was really helpful.

I just want to know that how many plugin files we can add in Customer Checkout plugin tab at a time.Actually i have to create 7 to 8 additional fields on the CCO header but i won't be able to arrange all 8 fields on header section.Can i add all 8 fields in one plugin file or i have to create it separately. Can you please guide me in this regard.
former_member609283
Participant
0 Kudos
Hi Robert,

Hope you are doing well.

I have created 7 fields in CCO and all went well but right now i am facing an issue data in these fields is not properly fetched into SAP fields.My Integration Framework  Version is 1.22.12 and i am using the same extension file "“sap.CCO.Extention_V3.0.0“ that you give in the gitlab link. Is it compatible with my B1if Version or i have to use other extension scenarios. Kindly guide me in this regard.:)
txemapar
Participant
0 Kudos
Hi Robert, first of all, congratulate you great work and detail in your blogs related to SAP Customer Checkout.

 

I am testing the functional possibilities of Plugins development in CCO and I would like to know how I can add a button (in the same place in the example) and capture the event click on Sales.

 

Thanks!.

 

Txema
n007praveen71
Participant
0 Kudos
Hello Bro, I'm back hope you remember me.

I have tried replicating a jar file with salesInject.js as its resource and pasted it in my POSplugin--> AP folder, when i run the application it is created in the plugin tab in the setting but i can't seems to find any customer age field box in my sales screen .

I know I'm definitely missing something here pls help me create a textbox thank you.

 
R_Zieschang
Contributor
0 Kudos
Hi n007praveen71 ,

can you please open the google chrome devtools (by pressing F12).

Navigate to the tab "Sources".

Look if there is somewhere a file named something like PluginUIJSServlet... Click on this file. This file should have your javascript code as content.

 

Regards

Robert
n007praveen71
Participant
0 Kudos

i did as you said brother but i couldn’t find the specified file or my coding. pls help me  out thanks

 

R_Zieschang
Contributor
0 Kudos
 

Then something went wrong while injecting.

Things you can do:

  • Please double check for correct spelling of the js filename.

  • Doublecheck if the file is located in the resources folder.

  • Open your plugin jar with winrar and check if there is the resources folder on top level and in this folder your js file

n007praveen71
Participant
0 Kudos
Sorry for the Late reply, and Thanks bro . I managed to find that above mentioned folder in inspect element , but its empty. Am i missing a path ? or doing wrong?
n007praveen71
Participant
0 Kudos
Hello there bro, im back again!. i have tried everything and finally figured my js file had some wrong coding so i copy pasted what you have here and now i can see the age of customer field.

but now  i want that text field inside the customer container  so that it can show that field middle of the screen

can we do that?
xsalgadog
Participant
0 Kudos
good afternoon,
To carry out this same process from the in the Kiosk Mode ( Quick service) and obtain the value entered in the text field to the backend?
R_Zieschang
Contributor
0 Kudos
Dear xsalgadog

in quick service ui you do not have the full flexibility to add new input fields whereever you like etc. but you could add e.g. a quick selection button to trigger a input form:

look at my answer here on how to open a custom input field:
https://answers.sap.com/questions/12901101/example-for-a-quick-service-plugin-cco.html?childToView=1...

Alternatively you could intercept the booking of the receipt, trigger the input field from above, update the receipt with your data and let cco proceed the booking of the receipt.

 

hth

Robert
bkaremba57
Member
0 Kudos
Hi Robert,

Thanks once again for another brilliant tutorial. I was able to use it to get the weight from a scale connected to the POS machine and update the UI!

Brian
Labels in this area