Skip to Content
Technical Articles
Author's profile photo Robert Zieschang

SAP Customer Checkout Plugin Development – Part V

Back to part I

Back to part II

Back to part III

Back to part IV

Part V – Create a complex UI form, work with the eventbus and create a custom printjob to print

Introduction

Welcome to the last part of my blog series about creating cco plugins. If you are new to this blog series, please use the links above to follow the prerequisties for this part as we will dive directly into the topic.

In this part we will learn how to create complex forms in the quick service ui with different components, we will check for mandatory fields and we will get to know how to work with the event bus. We will save this data from the ui form into cco and we will create a custom print job to print the data on our own print layout with apache FOP.

We will use SAP Customer Checkout 2.0 FP10 for this plugin.

Inject Javascript code and custom CSS

As always we create a maven project in which our base class extends the BasePlugin class provided by SAP Customer Checkout.

We will now inject our javascript and css. See screenshot below.

In the next step please add both files.

We are going to fill these files with code later on.

Create user defined table with DAO and DTO

In the next step, we are using the pattern, we already know from part 3. We are creating a new database table within the cco database and make CRUD operations on this table easily accessible with a DAO layer so that we can use our database records in DTOs.

Please checkout part 3 as I won’t go into detail in this part. The only difference is, that we are using the singleton pattern. This ensures only one instance of our dao class exists while our plugin runs and the dao instance is easily accessible throughout all classes. Also it removes the necessity to instantiate the class everytime again.

Basically we are defining a static property called instance from type AdditionalInfoDao. The only access to this class is through our method getInstance() which checks if instance is already set, sets it if not and returns it. Done.

The access to the methods of our dao class is as simple as this. In our app class (the one which extends ccos BasePlugin class) we override the startup method to setup our database table. See? No AdditionalInfoDao dao = new AdditionalInfoDao() etc.

Our dto class also looks pretty much the same like our LoyaltyFactorDto class from part3.

Creating a user form in the quick service ui

First thing we will do is go to our CCOm and create a quick selection button which will later trigger our form.

Go to POS configuration -> Quick selections and choose your quick selection and add a quickselection button at you desired place.

 

When the button is created, click on the glasses on the left side which shows “display”.

In the next screen click on Edit and add the following js code:

"event": { "eventName": "SHOW_ADD_INFO", "eventPayload" : { "message" : "I am an event payload message" }}

What this code does is, whenever the cashier clicks on this button, it will create an ui event which is pushed into the eventbus of cco. This event later will be handled by our ui coding.

Create ui coding and handle events

Remember we already created and inject a js file we created earlier? Let’s jump to it and create some js code.

So there is a lot going on already, lets get into some details. First of all we create our plugin class, the constructor which will be called by cco gets an instance of the pluginService and the eventBus.

In our init method we are subscribing to this eventBus from cco. In the ‘handleEvent’ scope we are now able to handle our own events but ALSO the events cco pushes in the eventbus.

Before the handling of the events we’re getting various objects from the pluginService like the receiptStore, where the current receipt is stored, the userStore to get the current user and the translationService to create multilanguage ui events.

In the ‘SHOW_ADD_INFO’ event we are checking if there is a receipt in the receiptStore and if not we will show a message box to the cashier. After that we check, if the receipt has an businesspartner set.

If all the checks apply we will send an event to our backend with the customerId we retrieved from the receiptModel.

How to handle these backend events?

Handling plugin events in the backend

I like to have separate classes for handling the backend events. E.g. one class for handling all receipt related events, one class for handling sales item events etc.

For this plugin we will use one class for handling all plugin events. Again we will create a singleton for our event handler.

So in our registerEventHandler method we’re adding a new UIEventChannelListener. CCO will call the handleEvent method for all UIEventChannelListener in their list. You’ll get the eventId and also the payload.

In our case, we retrieve the customerId from the payload and use our AdditionalInfoDao class to look for a database record in our table for this customer id. If we found something, we’re adding the results into an JSON object and use the BroadcasterHolder class to push an event from our backend back to the ui.

The last thing we need to do is to call the registerEventHandler method in our startup Method of our App class.

    @Override
    public void startup() {
        AdditionalInfoDao.getInstance().setupTable();
        BackendEventHandler.getInstance().registerEventHandler();
        super.startup();
    }

Now we have the knowledge to sent events from our backend to the ui and back.

Pretty easy, isn’t it?

Creating the ui form

In the last chapter we sent an event called SHOW_ADD_FORM after we retrieved a database record from our AdditionalInfoDao class. Now lets build a form.

To make things more interesting, we’re not gonna build a plain form. No we’ll build a form with multiple ribbons. And depending on the chosen ribbon we will make input fields visible or invisible.

So jump back to our js coding and add a new case in our switch.

When we get the SHOW_ADD_FORM in our js coding, we need to implement the following things:

  • create all the data models to store the data
  • create all the ui components like labels, inputfields etc. and bind the data models
  • if we got data from the backend (for this customer there is already a record in the database) we need to set the data
  • create the form
  • react on switching between the ribbons
  • check for mandatory fields
  • send the input data back to the backend

Create data models and ui components

So I will break the coding down to this elements. We implement a method called buildElements.

    buildElements() {
        // all our ui components will be bound to a datamodel so that we can easily access the
        // values of the cashiers input
        this.model1 = new cco.InputModel();
        this.modelAmount = new cco.InputModel();
        this.modelDecimal = new cco.InputModel();
        // the option model is like a select box so we need to add options to select.
        this.selectModel = new cco.OptionModel();
        this.selectModel.addOption(new cco.Option('option1', 'First option'));
        this.selectModel.addOption(new cco.Option('option2', 'Second option'));
        this.selectModel.addOption(new cco.Option('option3', 'Third option'));
        this.selectModel.selectFirstOption();

        // we are creating 2 arrays each containing all ui components for each ribbon.
        this.ribbonOneElements = [{
            index: [0, 0],
            hspan: 8,
            vspan: 8,
            content: {
                // this is our first LabelComponent
                component: cco.LabelComponent,
                props: {
                    text: 'Additional Field 1:',
                    class: 'tableCellLayout'
                }
            }
        }, {
            index: [0, 10],
            hspan: 20,
            vspan: 8,
            content: {
                // this is our first InputFieldComponent
                component: cco.InputFieldComponent,
                props: {
                    // bound the datamodel
                    id: 'first',
                    model: this.model1,
                    class: 'tableCellLayout',
                    autoSetPrimaryTarget: true
                }
            }
        }, {
            index: [16, 0],
            hspan: 8,
            vspan: 8,
            content: {
                // this is our second LabelComponent
                component: cco.LabelComponent,
                props: {
                    text: 'Additional Field 2:',
                    class: 'tableCellLayout'
                }
            }
        }, {
            index: [16, 10],
            hspan: 20,
            vspan: 8,
            content: {
                component: cco.InputFieldComponent,
                props: {
                    id: 'second',
                    model: this.modelAmount,
                    class: 'tableCellLayout',
                    numeric: true,
                    autoSetPrimaryTarget: true
                }
            }
        }];

        this.ribbonTwoElements = [{
            index: [0, 0],
            hspan: 8,
            vspan: 8,
            content: {
                component: cco.LabelComponent,
                props: {
                    text: 'Additional Field 3:',
                    class: 'tableCellLayout'
                }
            }
        }, {
            index: [0, 10],
            hspan: 20,
            vspan: 8,
            content: {
                component: cco.InputFieldComponent,
                props: {
                    id: 'third',
                    model: this.modelDecimal,
                    numeric: true,
                    class: 'tableCellLayout',
                    autoSetPrimaryTarget: true
                }
            }
        }, {
            index: [16, 0],
            hspan: 8,
            vspan: 8,
            content: {
                component: cco.LabelComponent,
                props: {
                    text: 'Additional Field 4:',
                    class: 'tableCellLayout'
                }
            }
        }, {
            index: [16, 10],
            hspan: 20,
            vspan: 8,
            content: {
                // For our selection model we use the ui component called SelectComponent
                component: cco.SelectComponent,
                props: {
                    model: this.selectModel,
                    class: 'tableCellLayout'
                }
            }
        }];
    }

Basically we define our data models and create two json arrays one for each ribbon.

Furthermore we create an object of type GenericInputConfiguration. This configuration will hold three elements. Our StatefulBarComponent which is our ribbons buttons. The two other elements are GridLayoutComponents, one for each ribbon. These GridLayoutComponents will hold our elements we defined in the buildElements method.

createRibbonOneConfiguration() {
        // define which text the buttons should show
        let buttons = [{
            content: 'Tab1'
        }, {
            content: 'Tab2'
        }];

        // creating a GenericInputConfiguration containing a GridLayoutComponent
        return new cco.GenericInputConfiguration('RibbonForm', '2', 'Component', null, null, cco.GridLayoutComponent, {
            props: {
                // set this so elements in the gridlayout can overlap. So elements in ribbon 1 and 2 can have the
                // same position.
                overlappingElements: true,
                elements: [{
                    index: [0, 0],
                    hspan: 25,
                    vspan: 15,
                    content: {
                        // this is the ui element we are using for the ribbons.
                        component: cco.StatefulButtonBarComponent,
                        props: {
                            // setting the buttons as prop and also our datamodel.
                            buttons: buttons,
                            class: 'light',
                            populateModel: true,
                            model: this.ribbonSelectModel
                        }
                    }
                }, {
                    index: [30, 0],
                    hspan: 25,
                    vspan: 10,
                    content: {
                        // our father gridlayout will contain another
                        // gridlayout. One per ribbon.
                        component: cco.GridLayoutComponent,
                        props: {
                            static: {
                                elements: this.ribbonOneElements
                            },
                            // setting the dynamic property to change the css class
                            // based on the selection of the ribbon.
                            dynamic: this.dynamicPropsRibbonOne,
                            cols: 100,
                            rows: 100
                        }
                    }
                }, {
                    index: [30, 0],
                    hspan: 25,
                    vspan: 10,
                    content: {
                        component: cco.GridLayoutComponent,
                        props: {
                            static: {
                                elements: this.ribbonTwoElements
                            },
                            dynamic: this.dynamicPropsRibbonTwo,
                            cols: 100,
                            rows: 100
                        }
                    }
                }],
                cols: 100,
                rows: 100
            }
        });
    }

Because we want to make the elements of ribbon two invisible, if the cashier clicks on ribbon one (and vice versa) each GridLayoutComponent will also get dynamicProperties attached which we will define later.

Set values from the backend in the models

Remember the data models we defined earlier and bound to certain ui components? If we got any data from the database, we need to set these values to the data models to have the values shown in our form.

if (Object.keys(event.getPayload()).length !== 0) {
  // set the values we got from the backend to our datamodels
  this.model1.value = event.getPayload().addFld1;
  this.modelAmount.value = event.getPayload().addFld2;
  this.modelDecimal.value = event.getPayload().addFld3;
  this.selectModel.setSelectedOptionKey(event.getPayload().addFld4);
}

Most of values ccos ui data models can be set via the value property, the data model for the selectbox offers a method to set the key of the chosen option.

Create the form

Now that we have all ui components, we also have the configuration of the grid layouts and we set values coming from the database. Let’s create the form.

We will push an event into the cco event bus of type SHOW_GENERIC_INPUT. This event will get the configuration as property and also some other properties like a title.

this.eventBus.push('SHOW_GENERIC_INPUT', {
  configuration: [
    this.createRibbonOneConfiguration(),
  ],
  title: 'Additional Info for customer ' + this.receiptStore.receiptModel.businessPartner.externalID,
  showKeyboardSwitchButton: true,
  keyboardType: 'keyboard',
  widthFactor: 0.8,
  heightFactor: 0.8 
});

React on switching ribbons

Remember the dynamic properties object we set as property of the two grid layout components? These dynamic properties can be used to dynamically switch properties of ui components. E.g. make them bigger, change the text of buttons etc.

// we are also creating something called DynamicProperties so we can hide ui components when not used e.g.
// when the cashier uses ribbon the first ribbon so all elements on the second ribbon will not be visible.
this.dynamicPropsRibbonOne = new cco.DynamicProperties({
  class: this.ribbonSelectModel.getSelected() !== 0 ? 'pluginRibbonHidden' : ''
});

// so we are basically setting a css class when our datamodel behind the ribbons has a certain value.
// the css class can be found in our blogpluginpart5.css
// With the Dynamic Properties you can set all properties of a component. e.g. changing the text, the height, ...
// of a ui component dynamically.
this.dynamicPropsRibbonTwo = new cco.DynamicProperties({
  class: this.ribbonSelectModel.getSelected() !== 1 ? 'pluginRibbonHidden' : ''
});

In our example we dynamically change the css class based on which value our ribbonSelectModel is set.

But this is just for the initialization. Next thing is to add an observer to the ribbonSelectModel. The function within the observer will always be called when the state of the ribbonSelectModel (in our case the selected value) changes. So within this observer, we emit a change to our dynamic properties.

// we are setting an observer to the ribbonSelectModel so we can react
// when the cashier clicks on another ribbon
this.ribbonSelectModel.addObserver({
  modelChanged: (inputModel) => {
    this.dynamicPropsRibbonOne.setAndEmitPropertiesIfChanged({
      class: inputModel.getSelected() !== 0 ? 'pluginRibbonHidden' : ''
    });
    this.dynamicPropsRibbonTwo.setAndEmitPropertiesIfChanged({
      class: inputModel.getSelected() !== 1 ? 'pluginRibbonHidden' : ''
    });
  }
});

Set the css class

Remember the css file we already injected earlier? Remember that we used the dynamicProperties so we can set the class property of our grid layout component? Add the following css class to the css file.

.pluginRibbonHidden {
    display: none;
}

Check for mandatory fields

The check for mandatory fields works with the validate callback. This function will be called when the cashier clicks on the finish (green button) of our form.

 // in the validate function we can implement checks to have mandatory field checks etc.
 cco.GridLayoutComponent.prototype['validate'] = () => {
  if (this.model1.value === '' || this.model1.value === 'MANDATORY') {
    this.eventBus.push('SHOW_MESSAGE_BOX', {
      'title': this.translationStore.getTranslation('ADD_FLD1_MND', this.user.getLanguageCode()),
      'message': this.translationStore.getTranslation('ADD_FLD1_MND', this.user.getLanguageCode())
    });
    return false;
  }
  return true;
};

In our case we are checking if our data model model1 has a value. If not we are showing a simple message box to the cashier, that field1 is mandatory. We are using the translation store to make use of the translation files in cco/translations to have a multilanguage capable plugin.

P.S.: You could also use another set of dynamic properties to change the value of model1 to e.g. MANDATORY to show the cashier which field is mandatory.

Send input data back to the backend

Remember when we pushed the SHOW_GENERIC_INPUT event into the eventbus of cco? We now extend this a little to have a callback after the validation was successful.

this.eventBus.push('SHOW_GENERIC_INPUT', {
  configuration: [
    this.createRibbonOneConfiguration(),
  ],
  title: 'Additional Info for customer ' + this.receiptStore.receiptModel.businessPartner.externalID,
  showKeyboardSwitchButton: true,
  keyboardType: 'keyboard',
  widthFactor: 0.8,
  heightFactor: 0.8,
  callback: (positive) => {
    if (positive) {
      let customerId = this.receiptStore.receiptModel.businessPartner.externalID;
      let addFld1 = this.model1.value;
      let addFld2 = Math.trunc(this.modelAmount.value);
      let addFld3 = this.modelDecimal.value.replace(',', '.');
      let addFld4 = this.selectModel.getSelectedOptionKey();
      this.pluginService.backendPluginEvent('SAVE_ADDITIONAL_CUSTOMER_DATA', {
        'customerId': customerId,
        'addFld1': addFld1,
        'addFld2': addFld2,
        'addFld3': addFld3,
        'addFld4': addFld4
      });
    }
  }
});

And we send an event to our backend and setting all the necessary data from the models to the event payload.

Done. Now let’s have a look at the finished form.

Implement saving ui data

What’s missing? Right! We pushed an event to our backend so now we need to save this data! Go back to the BackendEventHandler class and handle the event.

case "SAVE_ADDITIONAL_CUSTOMER_DATA":
  customerId = payload.getString("customerId");
  String addFld1 = payload.getString("addFld1");
  int addFld2 = payload.getInt("addFld2");
  Double addFld3 = payload.getDouble("addFld3");
  String addFld4 = payload.getString("addFld4");
  logger.info("Got data from ui");

  AdditionalInfoDto newDto = new AdditionalInfoDto(customerId, addFld1, addFld2, BigDecimal.valueOf(addFld3), addFld4);
  AdditionalInfoDao.getInstance().save(newDto);
break;

This is pretty straight forward. Get the data from the payload and save it through our AdditionalInfoDao class.

Done!

Nice!

But we are not done yet!

A custom printjob builder

What do we want to do with our custom printjob builder? We want to print the additional data on a Apache FOP layout if a customer is set in the receipt and the receipt is also printed.

So far so good. What do we need to do?

  • Implement our own printjob builder class extending the BasePrintJobBuilder class from cco
  • Register our own printjob builder
  • Extract the printlayout from the plugin and copy it into the print_templates folder of cco
  • React on the receipt printing to trigger our print job

Implement printjob builder

We extend the BasePrintJobBuilder class from cco. With this done we need to implement some methods. We also use the singleton pattern again to be able to access this class without creating a new instance every time.

public class AdditionalInfoPrintJobBuilder extends BasePrintJobBuilder {

    private static final Logger log = Logger.getLogger(AdditionalInfoPrintJobBuilder.class);

    private static AdditionalInfoPrintJobBuilder instance;

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

    private AdditionalInfoPrintJobBuilder() {
        super();
    }

    @Override
    protected boolean needsMerchantCopy(PrintTemplateEntity printTemplateEntity, Object o, CDBSession cdbSession, boolean b) {
        return false;
    }

    @Override
    protected boolean isTemplateEnabledForSpecificBuilder(PrintTemplateEntity printTemplateEntity, Object o, CDBSession cdbSession, boolean b) {
        return true;
    }

    @Override
    protected void mergeTemplateWithSpecificData(Map<String, Object> map, PrintTemplateEntity printTemplateEntity, Object o, CDBSession cdbSession, boolean b) throws IOException, TemplateException {
        log.fine("Add data to map");
        AdditionalInfoDto additionalInfoDto = (AdditionalInfoDto) o;

        map.put("addInfo", additionalInfoDto);
    }

    @Override
    protected void addSpecificSelectableDataStructure(SelectableDataStructure selectableDataStructure) {
        SelectableDataStructure additionalInfoDataStructure = new SelectableDataStructure("AdditionalInfo", SelectableDataStructure.TYPE_LIST, "AdditionalInfo");
        this.addSubelementToSelectableStructure(additionalInfoDataStructure, "CustomerId", SelectableDataStructure.TYPE_STRING, "CustomerId");
        this.addSubelementToSelectableStructure(additionalInfoDataStructure, "addFld1", SelectableDataStructure.TYPE_STRING, "AddFld1");
        this.addSubelementToSelectableStructure(additionalInfoDataStructure, "addFld2", SelectableDataStructure.TYPE_INT, "AddFld2");
        this.addSubelementToSelectableStructure(additionalInfoDataStructure, "addFld3", SelectableDataStructure.TYPE_DECIMAL, "AddFld3");
        this.addSubelementToSelectableStructure(additionalInfoDataStructure, "addFld4", SelectableDataStructure.TYPE_STRING, "AddFld4");
        selectableDataStructure.addSubelement("AdditionalInfo", additionalInfoDataStructure);
    }

    private void addSubelementToSelectableStructure(SelectableDataStructure parent, String name, String dataType, String descriptionKey) {
        SelectableDataStructure childElement = new SelectableDataStructure(name, dataType, descriptionKey);
        parent.addSubelement(name, childElement);
    }

    @Override
    public String getDescription() {
        return PluginPropertiesConstants.ADDITIONAL_INFO_PRINT_JOB_BUILDER_DESC;
    }

    @Override
    public boolean isResponsibleForDataSource(Object o) {
        return o instanceof AdditionalInfoDto;
    }
}

This should be mostly self-explanatory. The isResponsibleForDataSource method is called first. You can implement checks here e.g. only print if the additionalField2 is more than 200 or if additionalField4 is Option3.

The addSpecificSelectableDataStructure method can be used, to provide information about the printable data for the printtemplate. You can view this structure in ccos backend -> hardware -> print templates.

In the isTemplateEnabledForSpecificBuilder you will also get the printtemplateEntity as well as the data object itself. You could use this method to implement checks based on configuration of the printtemplate. e.g. a certain printer name is set.

Use the mergeTemplateWithSpecificData method to set your data object (in our case the AdditionalInfoDto) to the print data.

Register the printjob builder

After this implementation we need to register this printjob builder in cco. So we are extending our startup method in our App class.

@Override
public void startup() {
  AdditionalInfoDao.getInstance().setupTable();
  BackendEventHandler.getInstance().registerEventHandler();
  addPrintJobBuilder();
  super.startup();
}

The addPrintJobBuilder method is pretty easy:

private void addPrintJobBuilder() {
  PrintJobManager.INSTANCE.addPrintJobBuilders(AdditionalInfoPrintJobBuilder.getInstance());
}

Now we are able to add another print template in ccos backend like this:

Now we need to add a print template which is extracted and copied to the print_templates folder when our plugin starts.

 Extract print template

We are extending our startup method again.

@Override
public void startup() {
  AdditionalInfoDao.getInstance().setupTable();
  BackendEventHandler.getInstance().registerEventHandler();
  addPrintJobBuilder();
  deployPrintTemplate();
  super.startup();
}

Implementing the deployPrinttemplate method:

private void deployPrintTemplate() {
  try {
    String templateFileName = PluginPropertiesConstants.ADDITIONAL_INFO_PRINT_TEMPLATE + ".xsl";
    extractResource("/resources/" + templateFileName, CConfig.getPrintTemplatePath(), false);
  } catch (WrongUsageException ex) {
    log.severe(PluginPropertiesConstants.EXCEPTION_PRINT_TEMPLATE_COULD_NOT_BE_DEPLOYED);
    }
}

public void extractResource(String resName, String path, boolean overwrite) throws WrongUsageException {
  InputStream oIn = this.getClass().getResourceAsStream(resName);
  if (null != oIn) {
    try {
      String outName = resName.substring(resName.lastIndexOf("/"));
      String fileName = path + File.separator + outName;
      File oFl = new File(fileName);
      if (!oFl.exists() || overwrite) {
        OutputStream oOut = new FileOutputStream(oFl);
        IOUtils.copy(oIn, oOut);
        oOut.close();
      }
      oIn.close();
    } catch (IOException oX) {
      throw new WrongUsageException("Error extracting: " + oX.getMessage());
    }
  } else {
    throw new WrongUsageException("Could not find resource '" + resName + "'.");
  }
}

Our plugin will extract the print template from the resources, and copy this to the print_templates folder but it wont overwrite it, if the template is already existing.

React on the receipt print

We are hooking into the printReceipt method of the PrintReceiptPosService class. This method will be triggered with the initial print as well as reprints.

@PluginAt(pluginClass = PrintReceiptPosService.class, method = "printReceipt", where = PluginAt.POSITION.AFTER)
public Object onReceiptPrinted(Object proxy, Object[] args, Object ret, StackTraceElement caller) {
  ReceiptEntity receipt = (ReceiptEntity) args[0];
  if (null != receipt.getBusinessPartner()) {
    AdditionalInfoDto addInfo = AdditionalInfoDao.getInstance().findOne(receipt.getBusinessPartner().getExternalId());
    if (addInfo.getCustomerId() != null) {
      triggerPrint(addInfo, receipt.getCountPrintouts() > 1);
    }
  }
  return ret;
}

private void triggerPrint(AdditionalInfoDto additionalInfoDto, boolean isReprint) {
  try (CDBSession session = CDBSessionFactory.instance.createSession()) {
    List<PrintJob<?>> printJobs = PrintJobManager.INSTANCE.createPrintJobs(additionalInfoDto, session, isReprint);
    PrintEngine.INSTANCE.print(printJobs, false);
  } catch (IOException | WrongUsageException | ParserConfigurationException | SAXException | TemplateException | TransformerException | InterruptedException e) {
    e.printStackTrace();
  }
}

Last thing is the print template itself, but I won’t go into much details, because this would be too much for this part. Just have a look at it in git.

That is basically all. Congratulations!

Plugin in action

The form with data from the database:

Mandatory field check:

Data for a new customer is saved to the database:

And our print template printed after the normal receipt print:

Wrap up

We learned how to inject javascript and css code and we learned how to create rather complex forms in the quick service ui. Furthermore we got to know how to work with the cco eventbus, pushing and handling events in the ui as well as in the backend.

Also we learned how to implement a custom print job builder to create our own print jobs with our own data.

If you have any questions, please do not hesitate to drop me a line.

The code is hosted on gitlab.

https://gitlab.com/ccoplugins/blogpluginpart5

The plugin is licensed under the MIT License.

Assigned Tags

      19 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Joerg Aldinger
      Joerg Aldinger

      Hello Robert Zieschang !

      Excellent stuff as always! We will go through all your code and try to learn as much as possible from it!

      One question already: Is it possible to place additional buttons within the form, for example to have a form that allows you to enter a student number, then a button that sends a REST call to an external system (B1 service layer for example) and have additional fields filled with the response (name, career, etc.)?

      Thanks again!

      Keep safe,

      Joerg.

      Author's profile photo Robert Zieschang
      Robert Zieschang
      Blog Post Author

      Hi Joerg Aldinger

      thank you.

      Yes you can place buttons on your forms. The component is called ButtonComponent. You can add this in the method where I created all the UI components (like labels etc.).

      Config the component like this:

                  {
                      index: [x, y],
                      hspan: hsize,
                      vspan: vsize,
                      content: {
                          component: cco.ButtonComponent,
                          class: 'tableCellRightLayout',
                          props: {
                              content: 'btnTxt'
                              btnPress: () => {
                                  // do stuff when your button is clicked
                              }
                          }
                      }
                  }
      

      Other props for the ButtonComponent can be id, width, height, highlight, undecorated, disabled, contentFontSize, contentFace, class, forcedHighlightBorderWith, subText, focusable, toggleButton, active, loading and dynamicProperties.

      When you want to call an external REST System when you click on a button I would suggest creating a promise inside the btnPress function which can either be rejected or fulfill.
      Also it would be good to set the property loading while the promise is pending so that the cashier has a visual information that something is running in the background and set loading back to false when the promise was fulfilled or rejected.

      This dynamic setting of properties based on the state of some other objects (promise is pending => button.loading shall be true otherwise false) can be done with the dynamic properties I also used in the example for hiding ui components based on the ribbon the cashier clicked.

      Regards
      Robert

      Author's profile photo Asten Labs
      Asten Labs

      Hi Robert Zieschang ,

      How to accomplish same for inserting the details (name, career, etc) in a tablecomponent as new row data?

      Thanks

      AL

      Author's profile photo Ximena Salgado
      Ximena Salgado

      Hello,

      I am running blogpluginpart5, but in this part List<PrintJob<?>> printJobs = PrintJobManager.INSTANCE.createPrintJobs(additionalInfoDto, session, isReprint);
      I think it should output a list, but I get an empty list.
      What can be the error? I'm on FP12 version.

      Thanks for the help.
      Author's profile photo Pablo Aaron Mendieta
      Pablo Aaron Mendieta

      Hi Robert Zieschang,

       

      Is it possible to add custom buttons to standard screens? for example on the sales screen?

       

      Any comment or guidance will be truly appreciated, best regards!

      Author's profile photo Silambu RS
      Silambu RS

      Thanks Robert Zieschang

      Author's profile photo Raúl Campo
      Raúl Campo

      Hi!

      Great Tutorial!

      first sorry for my english!

      so i have a question ...i´m working with scco v2 fp08  (DEMO) and i can´t make the quick selection set up that you describe

      because the Type selector that i only have are :

      Item

      text

      code fucntion

      not appears group

      so...its a problem wit the demo version? or its a problem with FP08 and i need FP10?

      best regards

      and repeat...your tutorial are great

      thanks for all

      best regards

       

       

      Author's profile photo Robert Zieschang
      Robert Zieschang
      Blog Post Author

      Hi Raúl Campo,

       

      I have currently no FP08 available but yes, the plugin was created for FP10. I think the missing selectors were not available in FP08.

       

      Regards

      Robert

      Author's profile photo Raúl Campo
      Raúl Campo

      Thanks!

       

      i will try with fp10 ,, i´m sure it will works ok

      Are there some workaround with fp08 (quick selection) with the options avalaible to manage the event like in fp10?

      best regards!

       

       

       

       

       

      Author's profile photo Raveed Riaz
      Raveed Riaz

      Hi Robert Zieschang ,

      Can we add a grid component and load data through specific filters.

      Author's profile photo PIERRE BROTHIER
      PIERRE BROTHIER

      this blog is really helpful.

      as there's no documentation, it's probably the best resource to start with cco plugin development.

      I wonder if there's a way to generate pdf file from receipt to send by email using existing A4 template. There's some sample about sending mail with receipt info in the body, but nothing about attachments.

      The scenario would be following :

      • cco ask if the the customer want to get the receipt by email
      • if yes the receipt is generated in pdf and send to the specified mail

       

       

      Author's profile photo Robert Zieschang
      Robert Zieschang
      Blog Post Author

      Hi PIERRE BROTHIER,

      not tested it, but what you could do, is implement your own PrintjobBuilder. Within the printjob builder use the FOPProcessor class. It is a singleton.

      The FOPProcessor has a method called

      getPrinterJob(String pathToPrintTemplate, Map<String, Object> root, FileDAO fileDao).

      The return value is a PDDocument (https://pdfbox.apache.org/docs/2.0.2/javadocs/org/apache/pdfbox/pdmodel/PDDocument.html)
      You can use this object to get the generated PDF, attach it to a mail and send it to the recipient.

       

      Regards
      Robert

      Author's profile photo PIERRE BROTHIER
      PIERRE BROTHIER

      Hi Robert,

      thank you, that seems quite simple. I'll try this and make a feedback  !

       

      Author's profile photo PIERRE BROTHIER
      PIERRE BROTHIER

      So i've tried ans it works !

      on the postReceipt event, I map the needed objects, and call the getPrinterJob function with the A4 receipt template.

      Here is the code I've made, if it can help someone.

       @PluginAt(pluginClass = ReceiptPosService.class, method ="postReceipt", where = POSITION.AFTER)
      	  public  void postReceiptAfter(Object porxy, Object[] args,Object ret, StackTraceElement[] ste) throws TransformerException, IOException, TemplateException, FOPException 
      	  {
      		  // cast receipt
      		  ReceiptEntity receipt = (ReceiptEntity) args[0];
      		  
      		  //cast user
      		  UserEntity l_user = (UserEntity) args[3];
      		 
      		  //get  cashDesk info
      		  CashDeskEntity l_entityCd = CashDeskManager.getCurrentCashDesk();
      		 
      		//convert entity object to DTO
      		 CashDeskPrintDTO l_CDeskPrintDTO = ModelConverter.map(l_entityCd);
      		 ReceiptPrintDTO l_print =ModelConverter.mapForPrint(receipt);
      		 UserDTO l_userPrint = ModelConverter.map(l_user);
      		 
      		 //preparing the map for transformation
      		 Map<String,Object> l_newMap = new HashMap<String, Object>();
      	
      		 l_newMap.put("lineFeed", "");
      		 l_newMap.put("isReprint", false);
      		 l_newMap.put("isMerchantCopy", false);
      		 l_newMap.put("maxCharPerLine", -1);
      		 l_newMap.put("outputLocationCode", null);
      		 l_newMap.put("isPrintedFromManagerSystem", false);
      		 l_newMap.put("isDigitalReceipt", false);
      		 l_newMap.put("currentDate", new Date());
      		 
      		 l_newMap.put("cashdesk", l_CDeskPrintDTO);
      		 l_newMap.put("receipt", l_print);
      		 l_newMap.put("user", l_userPrint);
      		 
      	    
      		  //try to format with Sales Receipt A4
      		  PDDocument l_doc = FOPProcessor.INSTANCE.getPrinterJob("SalesReceiptPrintTemplateA4.xsl", l_newMap);
      		  //save the document to file
      		  l_doc.save("receipt.pdf");
      		  
      		  logger.info("POST RECEIPT AFTER");
      		  }

       

      Now i would make it the proper way, with a custom template and my own PrintJobBuilder. THanks !

      Author's profile photo Robert Zieschang
      Robert Zieschang
      Blog Post Author

      Hi PIERRE BROTHIER,

      great! Glad I could help!

       

      Regards

      Robert

      Author's profile photo Ximena Salgado
      Ximena Salgado

      Hello Robert Zieschang!

      I am following this tutorial for customer checkout FP12, I don´t know if this is the reason, but in FP12, I not get the value that I am sending in the class AdditionalInfoDto in the print.

      When I change the library env in my plugin for the version FP09, all function correctly.

      Author's profile photo Isaac Francis
      Isaac Francis

      Hi Robert Zieschang,

      Thanks so much for these incredible material.

      I had a question.

      i can hook the print receipt method when i pay the receipt? and also, can i get data to export to txt file instead get printed?

       

      Thanks in advance

      Isaac

      Author's profile photo Mehdi Selmani
      Mehdi Selmani

      Hi Robert Zieschang,

      Thanks so much for your job.

      I have a question.

      I would like to know the name of tab component ? and how to implement this ? 

      tab%20component

      tab component

       

       

      Thanks in advance

      Mehdi SELMANI.

      Author's profile photo Yousef Aldamisi
      Yousef Aldamisi

      Hi Robert Zieschang I need help

      is there any way i can get the printed data and print it also on the original sales receipt??

      Thanks in advance