Lab Info
Lesson: Product Configuration (Custom Form Embedded into a Quote)
Target Audience: Certified Configuration Engineer
Estimated Time to complete: TBD
Requirements & Solution Author: Petr Rys
Solution Developer & Lab Author: Marcin Łuczakuczak
User Story / Requirements
Our Customer (Interior design studio) is selling doors to their end-customers. (B2B or B2C)
Some Products are defined as standard skus, but our customer can also manufacture non-standard Products via specification.
Create an embedded Custom Form on Quote to serve as "Custom Product Configurator" for the sales person for non-standard doors.
The sales agent has a Quote, which allows to add Products (skus) into the Product Master a use these Products right away as line items of this Quote.
Product Master contains standard doors as well as accessories and, doors already manufactured before.
Learning Outcomes
At the end of the Lab, you should be able to:
Create Standalone Custom Form, Custom Form Type
Utilize Input Matrix as Custom Form input
Understand Custom Forms Workflow
Provided resources
Logics:
eCFO_ProductFilter,
eCFO_ProductConfigurator,
QuoteProductConfigurator,
QuoteProductFilter_QuoteHeader_Logic,
QuoteProductConfiguratorHeaderLogic
Company Parameters:
Material Adjustment
Color Adjustment
Door Size Adjustment
Product Master Table that categorizes basic products into specific Product Groups. Each product has a unique identifier starting with the ‘DR’ prefix, and a label starting with the ‘Basic’ prefix.
'Product Base Values' Product Extension Table contains each product's base values.
Acceptance Criteria
The user can create a quote of the appropriate type.
The new quote has an extra tab with a form containing all the necessary fields.
The form enables setting up the basic product downloaded with the Product Master Table.
Once the recalculation is complete, the form will show the newly calculated Base Value for the selected products. The result of the calculation should be a new product that includes Doorframes and Locks&handles. Please note that the desired outcome should include all possible combinations of the two pairs of products.
Users must select 'Create new Products' to make table changes permanent.
If a product with the same configuration does not exist, it is added to the Product Master Table. The calculated Base Value is also added to the Product Extensions Table.
The user should be informed what products have been added to the tables.
Sample solution
Step-by-step solution
Develop the logic and test in Studio
Go to the Pricefx Studio and create the following logics.
a) sCFO_ProductDiscountDataMgmt
Pricefx -> Create Calculation Logic
Nature: Custom form header (customFormHeader)
Name: eCFO_ProductConfigurator
Open the logic.json file in the newly created logic and add a label property to the configuration.
{ "elements" : [ ], "formulaNature" : "customFormHeader", "status" : "ACTIVE", "label" : "Product Configurator", "uniqueName" : "eCFO_ProductConfigurator", "validAfter" : "2020-01-01" }
Now, let's deploy the logic onto the partition
Go to Administration → Logics → Header Logics and select the logic type 'Custom Form' to check if the newly created logic is visible on the list.
Access Administration → Custom Form Types from your partition. Create a new form type by clicking on the [+ New Form Type] button located at the top-right corner of the screen. Uncheck Embedded option and fill in the form:
Specify the values for the parameters listed below.
Name | eCFO_ProductConfigurator |
Label | Product Configurator |
Header Logic | Product Configurator |
Embedded | true |
Form type | Custom Form, Quote |
Access Quoting → Quote Types from your partition. Create a new quote type by clicking on the [+ Add Quote Type] button located at the top-right corner of the screen. Specify the values for the parameters listed below.
Name | ProductConfigurator |
---|---|
Label | Product Configurator |
Pricing Logic | QuoteProductConfigurator |
Header Logic | QuoteProductConfiguratorHeaderLogic |
Configuration | { "name": "default", "tabs": [ { "name": "header", "translationKey": "sfdc_quotes_tabs_header", "type": "header" }, { "name": "Product Configurator", "translationKey": "Product Configurator", "typeReference": "eCFO_ProductConfigurator", "type": "customForms" }, { "name": "items", "parameters": { "defaultPlacement": "right", "flexibleLayout": true }, "translationKey": "sfdc_quotes_tabs_items", "type": "items" }, { "name": "attachments", "translationKey": "sfdc_quotes_tabs_attachments", "type": "attachments" }, { "name": "workflow", "translationKey": "sfdc_quotes_tabs_workflow", "type": "workflow" }, { "name": "workflow-history", "translationKey": "sfdc_quotes_tabs_workflowHistory", "type": "workflowHistory" }, { "name": "messages", "translationKey": "sfdc_entity_tabs_messages", "type": "header" } ] } |
QuoteProductConfigurator and QuoteProductConfiguratorHeaderLogic are in your partition. QuoteProductConfigurator calculates the total Invoice price based on the base price and quantity, while QuoteProductConfiguratorHeaderLogic displays the calculation results in a Quote Header.
Access Quoting → Quotes from your partition. Create a new quote by clicking on the [+ NewQuote ] button located at the top-right corner of the screen. Choose the type that was created in the previous step. The new form should contain and additional tab called Product Configurator .
TODO: Work in progress - START
Pricefx Studio - Create Custom Form Type
TODO: Work in progress - END
Form creation
Our objective is to design a form allowing users to configure the product according to their preferences. The form will consist of two multiple-selection lists and two tables, where users can input new parameters to customize the basic version of the product.
Requirements for Calculation result table:
Columns | Table consists of three columns, each with the appropriate data type.
|
Generated | The new product id should consist of two parts, separated by a dash. The first part is prefix DOORSET, and the second part is a randomly generated string of 10 characters. To meet this condition, you can utilize the example: DOORSET-y0QNSxCuYj |
Generated | The new label should be created by removing the "Basic" prefix from the product label and separating parameter names with a dash character for each product. Each pair of these two values should be connected using the "/" sign. examples:
|
| The calculation of the base price involves adding the parameter coefficients multiplied by the base price to the base price of the basic product. The total base price should be calculated by adding the base price of both products together. |
Please find a list of form fields along with their respective requirements below.
Field | Requirments |
---|---|
Doorframes | Product group entry with the following parameters. Ensure that only products with the prefix 'Basic' are included in the selection list. To meet this condition, you can utilize the "Filter.custom" method.
|
Locks&handles | Product group entry with the following parameters. Ensure that only products with the prefix 'Basic' are included in the selection list. To meet this condition, you can utilize the "Filter.custom" method.
|
Doorframes Configurator Table | Input matrix with the the following parameters:
|
Locks and Handles Configurator Table | Input matrix with the the following parameters:
|
Create new Products | Boolean user entry with the following parameters.
|
Open eCFO_ProductConfigurator logic.json file.
Add new elements to your logic:
Logic element | |
---|---|
| In this element, you can add methods and fields to create the logic of the form. |
| This component will be in charge of generating a view for a form. |
| This section will display the result of the calculations in a table format. The table will have columns such as ‘Product Id’, ‘Label’, and ‘Base Price’. |
| This code is responsible for saving the configured product to two tables: Product Master and Product Extensions. |
Task | Code example |
---|---|
Create inputMatrix | api.inputBuilderFactory() .createInputMatrix("LocksAndHandlesMatrix") .setLabel("Locks and Handles Configurator Table") .setHideAddButton(true) .setHideRemoveButton(false) .setColumnValueOptions(['Colour': colors, 'Material': materials]) .setColumns(['Product Id', 'Label', 'Base Price', 'Material', 'Colour']) .setReadOnlyColumns(['Product Id', 'Label', 'Base Price']) .addOrUpdateInput(customFormProcessor, 'ROOT') |
Take names from company parameters teble. | api.findLookupTableValues('ColorAdjustment')['name'] |
Add group input entry | api.inputBuilderFactory() .createProductGroupEntry('productDoorframes') .setLabel("Doorframes") .setFilterFormulaName("eCFO_ProductFilter") .setFilterFormulaParam("Doorframes") .addOrUpdateInput(customFormProcessor, 'ROOT') Please note the following instructions: You should create the logic "eCFO_ProductFilter" (line 4) responsible for filtering products based on their product group. This filter logic should return a filter object that meets two specific conditions. Firstly, it should only include products from Nature: Product input filter (productInputfilter) Name: eCFO_ProductFilter |
Updating Products table | def addProduct(def product) { def label = product["Label"] def sku = product["Product Id"] def updateProductMap = [ "sku" : sku, "label" : label, "attribute1": "DoorSet", "unitOfMeasure": "EA", "currency": "EUR", "attribute2": "BU-DoorSet", "attribute3": "UNDEFINED", "attribute4": "default", "attribute5": "default", "attribute6": "Introduction", ] api.add("P", updateProductMap) } |
Updating Products Base Values table | /** * Adds a new product's base value via an API call. * * @param product The product whose base value is to be added. This should * be a map containing at least a "Label", a "Product Id" * and a "Base Price". * Example: * ['Label': 'Doorframes-DR-0013-1981 x 686-Wood-yellow/Locks&handles-DR-0029-Wood-gray', * 'Product Id': 'DOORSET-y0QNSxCuYj', * 'Base Price': '99.99'] */ def addProductBaseValue(def product) { def label = product["Label"] def sku = product["Product Id"] def basePrice = product["Base Price"] def updateValueMap = [ "name" : "ProductBaseValues", "sku" : sku, "attribute1": label, "attribute2": basePrice ] api.add("PX3", updateValueMap) } |
This task can be accomplished in numerous ways. See below for an example of how to do it, or try to solve the task independently without the use of suggestions.
Logic element | |
---|---|
| Add the following code: //Output.groovy methods /** * This method is used to create a label configuration based on the provided parameters. * * @param label The initial label. * @param doorSize The size of the door. If it's available, it will be appended to the label after a dash. * @param material The material of the door. If it's not empty/null, it will be appended to the label after a dash. * @param colour The colour of the door. If defined, it will be appended to the label after a dash. * @return A String that represents the new label. If the original label starts with 'Basic-', * it is replaced. Dash-separated size, material, and colour are appended if available. */ def labelConfigurator(def label, def doorSize, def material, def colour) { //Your code: check the requirements for calculation result table } /** * Calculates the price of a door based on various parameters. * * @param basePrice The base price of the door. It must be a valid number. * @param doorSize The size of the door. If not provided, it defaults to "default". * @param material The material of the door. If not provided, it defaults to "default". * @param colour The colour of the door. If not provided, it defaults to "default". * * @return Returns the total price as a BigDecimal. * The price is calculated as the base price multiplied by the total increase percent which includes the material, * color and door size price increments. */ def calculateDoorPrice(basePrice, doorSize, material, colour) { //Your code: check the requirements for calculation result table } /** * This function generates a new product Id in a specific format. * The format is "DOORSET-" followed by a UUID of length 10 characters. * The UUID is generated using an external API. * * @return A New formatted Product ID string */ def generateNewProductId(def productId) { //Your code: check the requirements for calculation result table } // SaveData.groovy methods /** * This method creates information messages about products and their base values. * Then, it uses this data to set an alert message through the `api`. * * @param addedProducts - A list of products represented as Maps where each Map should contain a "LABEL" key. The values of "LABEL" are used to construct a part of the information message. * @param addedBaseValue - A list of product base values represented as Maps where each Map should contain "LABEL" and "BASE_PRICE" keys. The values of these keys are used to construct a part of the information message. * * Note: Any changes or operations executed inside this method does not affect original list of maps passed as `addedProducts` and `addedBaseValue`. */ def addInfoMessage(addedProducts, addedBaseValue) { addedProducts = addedProducts.collect({ it[LABEL] }).join("<br>") addedBaseValue = addedBaseValue.collect({ it[LABEL]+", Base Price: "+it[BASE_PRICE] }).join("<br>") String productInfo = "<br>Following products has been added to Product Table: <br>$addedProducts <br>" String valuesInfo = "<br>Following products has been added to Product Base Values Table: <br>$addedBaseValue<br>" String info = "" if (!addedBaseValue.isEmpty()) { info += valuesInfo } if (!addedProducts.isEmpty()) { info += productInfo } api.setAlertMessage(info) } It's important to note that the methods calculateDoorPrice and labelConfigurator are designed to calculate values for individual products, such as Doorframe or Lock&Handle. |
| // 1. If the form being processed is in the post phase, // immediately return and skip the rest of the code. // 2. Add a product group entry for doorframes, // filtered by 'Doorframes' to the form. // 3. Add a product group entry for locks and handles, // filtered by 'Locks&handles' to the form. // 4. Get the names from the 'Material', // 'Color' and 'DoorSizes' look-up tables. // 5. Create configurator tables for doorframes and locks/handles. // 6. Get Products data from "productDoorframes" and "productLocksAndHandles" // 7. If there are any Doorframes products, populate // the DoorframesMatrix of the form with relevant data. // Otherwise, set the matrix to empty. // 8. If there are any LocksAndHandles products, populate /// the LocksAndHandlesMatrix of the form with relevant data. // Otherwise, set the matrix to empty. // 9. Create a boolean entry in the form processor to hold // the state of whether changes should be saved. |
| // 1. If the form processor is in pre-phase, // exit from the function // 2. Get the current item from the API // 3.Find the 'LocksAndHandlesMatrix' values // from the currentItem's inputs. // Assign these values to the LocksAndHandles collection. // 4.Find the 'DoorframesMatrix' values from the currentItem's inputs. // Assign these values to the doorframes collection. // 5. Use nested loops to add new door product to the newDoors collection def newDoors = [] // Iterate over the doorframes collection. doorframes.each { doorframe -> // Configure and assign a label to the doorframe item by using its properties. // Calculate price for the doorframe item by using its properties. // Use calculateDoorPrice and labelConfigurator methods // Iterate over the lockAndHandles collection. lockAndHandles.each { // Configure and assign a label to the lockAndHandle item by using its properties. // Calculate price for the lockAndHandle item by using its properties. // Use calculateDoorPrice and labelConfigurator methods // Concatenate doorframeLabel and lockHandleLabel separated by a slash.(newLabel) // Calculate the total price by adding doorframePrice and lockHandlePrice.(newPrice) // Create a row for the new door product with its ID, label, and base price. def singleRow = [ 'Product Id': Utils.generateNewProductId(), 'Label' : newLabel, 'Base Price': newPrice ] // Add the new door product to the newDoors collection. newDoors.add(singleRow) } } // 6. Generate a new matrix for the new products // 7. Add the generated newDoors to de newProducts matrix // 8. Add or update the output of the processed form by // establishing a new "discountMatrix" result // 9. return the generated rows |
| // 1. If the customFormProcessor is in the pre-phase, // end the function immediately // 2. get the current item from the api // 3. find 'saveChangesBoolean' from currentItem's inputs, // which determines whether changes should be saved // 4. If changes need to be saved, proceed if (save) { // 5. get the output from the out.Output // 7. create empty lists for addedBaseValue // and addedProducts // 8. for each product in output output.each { product -> // check if the product exists in Product Master table // check if product base value exists in Product Extensions table // if the product does not exist in Product Master table // a) add the product // b) add the product to the list of added products // if product base value does not exist in Product Extensions table // a) add product base value // b) add the product to the list of added base value } // add information message about the added products and base values Utils.addInfoMessage(addedProducts, addedBaseValue) } |
DEBUGGING with Studio Editor
Create a New Product Discount Manager
Open logic.json in eCFO_ProductConfigurator.
Open Studio Editor.
Click on the Inputs tab.
Set Context to CUSTOMFORM if needed
Set the CustomForm to the previously created form. If you can't find it, use the Reload option at the end of the dropdown list.
To check the output, please click on the button and go to the Result tab. Please refer to the following output as an example of what you should expect to see.
To track the variables that you need, you can use the api.trace method.
DEBUGGING with LOGS
To debug your application, you can read the logs of your partition. This will help you identify and fix any issues in your application.
Go to Administration → Logs → Logs. A new tab containing partition logs will open in your browser. The display may vary based on your patition preferences.
To add logging, you should open the logic and place api.logInfo or api.logWarn methods in the appropriate location.
def currentItem = api.currentItem() api.logInfo("##TEST##", currentItem)
Deploy your logic to your designated partition.
Open the previously created form and recalculate values. Check the log for expected output.
Resources for Further Learning
References
Custom Forms Configuration How-To(s)