Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

In this section you will learn more about Customs Forms.

...

Estimated Time to complete: TBD

Requirements & Solution Author: Petr Rys

...

Product Discount Data Management Custom Form allows users to perform these tasks:

  • Change Data → select a table and change values within the table without adding new rows.

  • Add new Product Group → add a new group, assign it to a product family, and assign all the required discounts for different levels. It is important that the discount for each additional level is greater than or equal to the discount for the previous level; otherwise, the validation process will fail.

  • Add a product family → reassign product groups to reflect a new product family.

  • Add new discount level to existing product family → create new discount levels for an existing product family. The levels must be continuous, starting from level 1 at 0 Min Revenue. Each additional level must have a minimum revenue greater than or equal to the previous level. Users can assign new discounts for these new levels, ensuring that the discount for each additional level is greater than or equal to the discount for the previous level; otherwise, the validation will fail.

...

  • AbortOnInputGenerationExecution

Code Block
languagegroovyjava
if (api.isInputGenerationExecution()) {
    api.abortCalculation()
}
  • BuildStep

Code Block
languagegroovyjava
workflow.addWatcherStep("Sales Manager")
        .withUserWatchers("admin")
        .withReasons("Sales manager needs to approve this form")

workflow.addApprovalStep("Sales Manager")
        .withPostStepLogic("customform_WorkflowPostStep")
        .withApprovers("admin")
        .withReasons("Sales manager needs to approve this form")

...

  1. Navigate to the sCFO_ProductDiscountDataMgmn logic and open the Configurator.groovy file. Once you have accessed the file, add the following code snippet to it.

Code Block
languagegroovyjava
if (customFormProcessor.isPrePhase()) {
    
    //1. Create two variables named 'configuratorName' and 'cfoFormIdKey'. Assign values to them from the Constconfig file.
    //String configuratorName = 
    //String cfoFormIdKey = 

    //2. Assign value to currentCfoForm. Use api.currentItem()
    //def currentCfoForm = 
    
    //3. Get id from current Item. Remember that current item can be null.
    //Long cfoFormId = 

    //4. Get inputs from currentCfoFrom.
    //5. Find input with a name that matches configuratorName
    //6. Take value  
    //7. If the above operation returns null, return an empty map [:]. You can use elvis operator '?:'
    //If you want to prevent NullPointerException, it is recommended to use the safe navigation operator '?'. This will ensure that your code doesn't throw an exception if a null value is encountered.
    //Map configuratorValueBackup = 
    
    //8. Add current form id to the configuratorValueBackup. Use cfoFormIdKey as a key and cfoFormId as value.
    //configuratorValueBackup.put()

    //9. update the input "ROOT" with a map that provides the name, label, url, type and value.
    customFormProcessor.addOrUpdateInput("ROOT",
            [name : configuratorName,
             label: configuratorName,
             url  : configuratorName,
             type : InputType.INLINECONFIGURATOR,
             value: configuratorValueBackup]
    )

}

...

In this step, we will create the configuratorEntry object and add a new hidden input with our custom form id to the configuratorEntry before returning it.

Code Block
languagegroovyjava
import net.pricefx.server.dto.calculation.ConfiguratorEntry

// Create a key for the CFO form ID from specified library
//String cfoFormIdKey = 

// Create a new ConfiguratorEntry using the API
//ConfiguratorEntry configuratorEntry = 

// Create a HiddenEntry InputBuilder with the CFO form ID key and build a context parameter for it
//def param = 

// Add the newly created parameter to the ConfiguratorEntry. Use createParameter method.

// Return the modified ConfiguratorEntry

...

In this step, we will add a header and a drop-down list with options for selecting individual forms to the configurator.

Code Block
languagegroovyjava
// Initialize a constant for main paragraph message
//String PARAGRAPH_MESSAGE_MAIN = 

// Get value of the first input from the ConfiguratorEntry, only if it exists
Long formId = 

// Get the product discount configurator utility script from the product discount library
Script productDiscountConfigurator = 

// Instantiate the main entry of the product discount configurator. 
// Use getMainConfiguratorEntry method with formId as a parameter
ConfiguratorEntry mainConfiguratorEntry = 

// Set the paragraph header for the main configurator entry. Use the constant initialized earlier

// Return the main configurator entry

...

In this step, we retrieve the option selected by the user and pass it as a parameter to the method that changes the form depending on the selected option.

Code Block
languagegroovyjava
import net.pricefx.server.dto.calculation.ConfiguratorEntry

// Get value of the first input from the OtionInput.groovy, only if it exists
String selectedType = 
// Use configuratorSwitch method. Pass selectedType as a parameter.

/**
 * This method is used to select and configure different types of ConfiguratorEntries.
 * It also loads the correct messages for each type.
 *
 * @param selectedType The type of ConfiguratorEntry to be used. This could be 'CHANGE_DATA', 'NEW_FAMILY', or 'NEW_GROUP'.
 * @return A ConfiguratorEntry instance configured based on the selectedType.
 * If the getterMethod or message for the selectedType do not exist, the method will return null.
 */
ConfiguratorEntry configuratorSwitch(String selectedType) {
    Script CONFIG_UTILS = libs.sCFO_ProductDiscountLib.CfoConfiguratorUtils
    Script CONST_CONFIG = libs.sCFO_ProductDiscountLib.ConstConfig

    def getterMap = [
            (CONST_CONFIG.CHANGE_DATA) : CONFIG_UTILS.&getChangeDataConfiguratorEntry,
            (CONST_CONFIG.NEW_FAMILY)  : CONFIG_UTILS.&getAddFamilyConfiguratorEntry,
            (CONST_CONFIG.NEW_GROUP)   : CONFIG_UTILS.&getAddGroupConfiguratorEntry,
    ]

    def messageMap = [
            (CONST_CONFIG.CHANGE_DATA) : CONST_CONFIG.CFO_CONFIGURATOR_CONFIG.PARAGRAPH_MESSAGE_CHANGE,
            (CONST_CONFIG.NEW_FAMILY)  : CONST_CONFIG.CFO_CONFIGURATOR_CONFIG.PARAGRAPH_MESSAGE_ADD_FAMILY,
            (CONST_CONFIG.NEW_GROUP)   : CONST_CONFIG.CFO_CONFIGURATOR_CONFIG.PARAGRAPH_MESSAGE_ADD_GROUP,
    ]

    def getterMethod = getterMap[selectedType]
    def message = messageMap[selectedType]

    if (getterMethod && message) {
        return configureEntries(getterMethod, message)
    }
}

/**
 * This method applies configuration to entries.
 *
 * @param getterMethod Method reference for getting configurator entry.
 * @param message The message to be set as paragraph header in configurator entry.
 * @return ConfiguratorEntry The modified configurator entry with new paragraph header.
 */
ConfiguratorEntry configureEntries(def getterMethod, String message) {
    ConfiguratorEntry configuratorEntry = getterMethod.call()
    configuratorEntry.setMessage(message)
    return configuratorEntry
}

EXPECTED RESULT

...

DEBUGINGDEBUGGING

  • Open logic.json in sCFO_ProductDiscountHeader_Configurator.

  • Open Studio Editor.

  • Click on the Inputs tab.

  • Set the Context to CONFIGURATOR.

  • Test Logic

  • Click on the Results tab and check output

...

Go to ChangeDataInputs.groovy and look at the configuratorSwitch method. Implement methods for each option chosen by the user to create one of three forms. Each method returns the ConfiguratorEntry object.

Code Block
languagegroovyjava
/* Fragment of configuratorSwitch method */
def getterMap = [
            (CONST_CONFIG.CHANGE_DATA) : CONFIG_UTILS.&getChangeDataConfiguratorEntry,
            (CONST_CONFIG.NEW_FAMILY)  : CONFIG_UTILS.&getAddFamilyConfiguratorEntry,
            (CONST_CONFIG.NEW_GROUP)   : CONFIG_UTILS.&getAddGroupConfiguratorEntry,
    ]
  • getChangeDataConfiguratorEntry()

Code Block
languagegroovyjava
ConfiguratorEntry getChangeDataConfiguratorEntry() {
    ConfiguratorEntry changeDataSection = api.createConfiguratorEntry()
    //TODO: create changeDataSection
    //1. Get config map from libraray (CFO_CONFIGURATOR_CONFIG)
    //2. Create option input - name: "ppTableInput", label: "Select table", required: true. 
    //   Get the value from CFO_CONFIGURATOR_CONFIG by OPTIONS.TABLE_OPTIONS key and
    //   pass it as options parameter.
    //3. Get value from ppTableInput 
    //4. Display a specific group of fields based on the option that is chosen.
    //   Check required inputs in follow sections:
    //   - For Discount Level Definition option
    //   - For Product Family Mapping option
    //   - For Discount option
    return changeDataSection
}

...

  • getAddFamilyConfiguratorEntry()

Code Block
languagegroovyjava
ConfiguratorEntry getAddFamilyConfiguratorEntry() {
    ConfiguratorEntry productFamilySection = api.createConfiguratorEntry()
    //TODO: create productFamilySection
    //1. Create two inputs:
    //   a) InputType.STRINGUSERENTRY, name: "productFamilyInput", label: "New Product Family", required: true
    //   b) InputType.BOOLEAN, name: "assignToGroup", label: "Assign new Product Family to an already existing Product Group."
    //2. Get input value from "assignToGroup". If value is true:
    //   a) Find unique family names from the ProductFamilyMapping company parameters table.
    //   b) Create option input - name: "assignProductGroup", label: "Assign to", pass values from step a)
    //3. Create a table. Use result from step 1a as a option for product family option.
    return productFamilySection
}

...

  • getAddGroupConfiguratorEntry()

Code Block
languagegroovyjava
ConfiguratorEntry getAddGroupConfiguratorEntry() {
    ConfiguratorEntry productGroupSection = api.createConfiguratorEntry()
    //TODO: create productGroupSection
    //1. Find unique family names from the ProductFamilyMapping company parameters table. 
    //   You can use the API function "findLookupTableValues".
    //2. Create two inputs:
    //   a) InputType.STRINGUSERENTRY, name: "productGroupInput", label: "New Product Group", required: true 
    //   b) InputType.Option, name: "Assign Product Family", label: "assignProductFamily", required: true
    //      Pass the values from first step as a setValuesOptions() parameter
    //3. Get input value from productGroupInput
    //4. Create a table. Use result from step 3 as a option for product group option.      
    return productGroupSection
}

...

  1. Open the Calculation.groovy file, which is located in the sCFO_ProductDiscountDataMgmt logic. Once it's open, please add the following code to the file.

Code Block
languagegroovyjava
if (customFormProcessor.isPostPhase()) {
    // choosenOption is a value from managementOptionsInput
    if (chosenOption == "Add new Product Group") {
        //TODO: Part-1
    } else if (chosenOption == "Add new Product Family") {
        //TODO: Part-2
    } else if (chosenOption == "Change data") {
        //TODO: Part-3
        if (ppTableInput == "Product Family Mapping") {
            //TODO: Part-4
            if (chosenTable == "Product Family") {
            //TODO: Part-5    
            } else if (chosenTable == "Product Group") {
            //TODO: Part-6  
            } 
            //TODO: Part-7
        } else if (ppTableInput == "Discount Level Definition") {
            //TODO: Part-8  
        } else if (ppTableInput == "Discount") {
            //TODO: Part-9
        }
    }
    
}

...

Task

Sample Code

Create a table

Code Block
languagegroovyjava
ResultMatrix newTable = api.newMatrix('Column_1', 'Column_2',)
newTable.setColumnFormat('Column_1', FieldFormatType.TEXT)
newTable.setColumnFormat('Column_2', FieldFormatType.NUMERIC)

Get data from company parameter table

Code Block
languagegroovyjava
def getDiscount() {
    def id = api.findLookupTable("Discount").id
    def filters = [ Filter.equal('lookupTable.id', id) ]
    def discountTable = api.find('MLTV2', 0, 0, null, *filters)

    def rows = discountTable.collect {
        [
                productGroup  : it.key1,
                discountLevel : it.key2,
                targetDiscount: it.attribute1,
                maxDiscount   : it.attribute2,
                typedId       : it.typedId]
    }
    return rows
}

How to edit cell style in table

Code Block
languagegroovy
def cell_1 = newTable.styledCell(example_value, "black", "yellow", "bold")
def cell_2 = newTable.styledCell(example_value, "black", "yellow", "bold")
newTable.addRow(cell_1, cell_2)

How to add new table to the customFormProcessor.

Code Block
languagegroovyjava
customFormProcessor.addOrUpdateOutput(
        [
                "resultName" : "new_name",
                "resultLabel": "new_label",
                "resultType" : "MATRIX",
                "result"     : newTable,
        ]
)

...

We need to revisit the logic that controls our Workflow. Our logic consists of two steps. Take a look at BuildStep.groovy

Code Block
languagegroovyjava
workflow.addWatcherStep("Sales Manager")
        .withUserWatchers("admin")
        .withReasons("Sales manager needs to approve this form")

workflow.addApprovalStep("Sales Manager")
        .withPostStepLogic("customform_WorkflowPostStep")
        .withApprovers("admin")
        .withReasons("Sales manager needs to approve this form")

...

This task can be approached in various ways. You can either attempt to preserve the data or use the provided solution below.

Code Block
languagegroovyjava
if (workflowHistory.steps.findAll { it.approvalStep }.every { it.approved } || workflowHistory.activeStep?.isEmpty()) {
    //part-1
    if (optionInput == "Change data") {
        if (changeGroupOrFamily == "Product Group") {
        //part-2
        } else if (changeGroupOrFamily == "Product Family") {
        //part-3
        } else if (ppTableInput == "Discount") {
        //part-4
        } else if (ppTableInput == "Discount Level Definition") {
        //part-5
        }
    } else {
        //part-6
        if (optionInput == "Add new Product Family") {
        //part-7
        } else if (optionInput == "Add new Product Group") {
        //part-8
        }
    }
}

...

Task

Code samples

Get user inputs from current item assign it to inputs variable

Code Block
languagejava
api.currentItem()?.inputs?.get(0)?.value

Get individual field values from user inputs:

Code Block
languagejava
inputs?.get("managementOptionsInput")

Get "Discount" lookup table Id assign it to the discountID variable.

Code Block
def discountID = api.findLookupTable("Discount").id

Create filters:

  • 'ProductGroup' equal oldName

  • 'lookupTable.id' equal discountID

Code Block
languagejava
def discountFilters = [
        Filter.equal("ProductGroup", oldName),
        Filter.equal('lookupTable.id', discountID)
]

Update "ProductFamilyMapping" lookup table. To obtain accurate data, make use of filters.

Code Block
languagejava
api.find('LTV', 0, *discountFilters).collect {
    def entry = [
            lookupTableName: "ProductFamilyMapping",
            id             : it.id,
            valueType      : it.valueType,
            typedId        : it.typedId,
            name           : newName,
            value          : it.value
    ]
    api.addOrUpdate("LTV", entry)
}

Use discountEntries and update "Discount" lookup table

Code Block
languagejava
discountEntries.each {
    api.addOrUpdate("MLTV2", [
            lookupTableName: "Discount",
            key1           : it["Product Group"].value,
            key2           : it["Discount Level"].value,
            attribute1     : it["Target Discount %"].value,
            attribute2     : it["Max Discount %"].value
    ])
}

Get "DiscountLevelDefinitionMatrix" table outputs from current item

Code Block
languagegroovyjava
api.currentItem()?.outputs?.find { it.resultName == "DiscountLevelDefinitionMatrix" }

If output is null get an empty list

Code Block
languagegroovyjava
discountLevelDefinitionTable = discountLevelDefinitionTable ? discountLevelDefinitionTable : []

Filter the rows in their respective tables based on the values of Map.

Code Block
languagegroovyjava
def discountLevelDefinitionEntries = discountLevelDefinitionTable.result.entries.findAll { it['Product Family'] instanceof Map }

...