How to handle passing values within Quote's header logics, configurators and line item logic

Introduction

This article summarizes possible solutions for different use cases of passing on values within Quote’s different calculation logics. Some of the use cases can be found here on confluence in articles, comments or in teams chat. We tried to implement all the use cases for our internal PoC’s and we encountered several obstacles while doing so.

We tried to describe possible solutions, some obstacles or wrong ways how to do it. Please feel free to comment the solutions, add wider explanations, or if you know better way how to do some of the use cases or feel strong about some use case that we should not try to implement it at all, please share the knowledge in comments.

Calculation logic architecture prerequisities:

How different logics are executed from the Quote perspective:

Syntax Check

This dry run is used to discover inputs in the line item. Most of other api calls are mocked. Be aware that syntax check dry run is executed only once in case of adding a new line item.

Other logics executable from quote

There are two other logics, which can be executed from header logics:

  • Configurator Logic

    • logic is executed after clicking on the Open Button of Configurator

      After clicking on save button of Pop up Configurator, new Configurator value is saved to Quote object. (quote object is what you get by calling quoteProcessor.getQuoteView()). But you cannot work with the values until Quote is recalculated. Why? All logic knows only the old quote object since all other calculation logics finished before Configurator logic was executed. You must press recalculate button or set automatic recalculation, please read the article how to recalculate a quote automatically.

  • Template Logic

    • logic is executed after clicking to download PDF button.

    • from template logic whole quote object is accessible through api.getCurrentItem().

Summary of passing values between different types of logics

Use case

From

To

Solution

Use case

From

To

Solution

1

Quote Header Logic Configurator

Quote Header Logic

Read Configurator value field.

2

Quote Header Logic

Quote Header Logic Configurator

Pass through Configurator value field, read using HIDDEN field inside configurator

3

Quote Header Logic

Line Item Logic

Hidden Input (Pre-Phase)

4

Line Item Logic

Header Logic

Read from getQuoteView()

(Post-Phase)

5

Quote Header Logic

Publishing Template Logic

Hidden Input or api.currentItem (provides quoteView)

6

Quote Header Configurator

Line Item Logic

Hidden Input

7

Line Item Logic

Quote Header Logic Configurator

Read from getQuoteView()

(Post-Phase), pass to Configurator trough value filed.

8

Quote Header Logic Configurator

Line Item Logic Configurator

Not possible, please see details below, including workaround.

9

Line Item Logic Configurator

Quote Header Configurator

Read from getQuoteView()

(Post-Phase), pass to Configurator trough value filed.

10

Line Item Logic

Line Item Logic Configurator

Not possible, please see details below, including workaround.

Use case 1: Quote Header Logic Configurator → Quote Header Logic

Step 1: Create Configurator button in header logic

1 2 3 4 5 6 7 8 9 10 11 if (quoteProcessor.isPrePhase()) { quoteProcessor.addOrUpdateInput( "ROOT", [ "name" : "Configurator", "label": "Configurator", "url" : "ConfiguratorLogic", "type" : "CONFIGURATOR" ] ) }

Step 2: Define generic logic (name it e.g. “ConfiguratorLogic”). It contains input matrix in our case.

1 2 3 4 5 6 7 8 9 10 def ce = api.createConfiguratorEntry() def p = ce.createParameter(InputType.INPUTMATRIX, "QuoteFeatures") def cols = ["Name", "Description"] def types = ["Text", "Text"] p.addParameterConfigEntry("columns", cols) p.addParameterConfigEntry("columnType", types) return ce

Step 3: In header logic, access all the values from configurator using following code snippet:

1 2 3 4 if(quoteProcessor.isPrePhase()) { def quote = quoteProcessor.getQuoteView() def quoteFeatures = quote?.inputs?.find { it.name == "Configurator" }?.value }

Where Configurator is a name of configurator element (created in step 1). What you will get is a following HashMap:

{QuoteFeatures=[{Name=firstLineName, firstLineDescr, selected=false}]

Use case 2: Quote Header Logic → Quote Header Logic Configurator

Step 1: Create header input field.

Suppose I have following header logic stringUserEntry field which I want to pass to the configurator:

1 2 3 4 5 6 7 8 9 10 if (quoteProcessor.isPrePhase()) { quoteProcessor.addOrUpdateInput("ROOT", [ "name" : "ValueToBePassed", "label" : "ValueToBePassed", "type" : "STRINGUSERENTRY", "required": false ] ) }

Step 2: Passing header value to configurator.

The problem is that Configurator and Header Logic do not share global space. So I cannot simply use api.global. Instead of using global I need to pass this value to configurator “value” property.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (quoteProcessor.isPrePhase()) { def passedValue = quoteProcessor.getQuoteView().inputs?.find { it.name == "ValueToBePassed" }?.value quoteProcessor.addOrUpdateInput( "ROOT", [ "name" : "Configurator", "label": "Configurator", "url" : "ConfiguratorLogic", "type" : "CONFIGURATOR", "value": passedValue ] ) }

Step 3: Merging values

Problem with solution from step 2 is that logic always overrides the configurator value after recalculation. Configurator value field is used for storing configurator object. It looks like this (QuoteFeature is inputMatrix object):

{QuoteFeatures=[{Name=firstLineName, firstLineDescr, selected=false}]

Therefore I need to read configurator values first and then create value map where I put backed up configurator’s values together with values I need to pass to the configurator. My goal is this:

{QuoteFeatures=[{Name=firstLineName, firstLineDescr, selected=false}], PassedValue=asdfasfd}

Code snippet:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (quoteProcessor.isPrePhase()) { def quote = quoteProcessor.getQuoteView() def passedValue = quote.inputs?.find { it.name == "ValueToBePassed" }?.value def configuratorValueBackup = quote.inputs?.find { it.name == "Configurator" }?.value ?: [:] configuratorValueBackup.put("PassedValue", passedValue) quoteProcessor.addOrUpdateInput( "ROOT", [ "name" : "Configurator", "label": "Configurator", "url" : "ConfiguratorLogic", "type" : "CONFIGURATOR", "value": configuratorValueBackup ] ) }

Step 4: How to read passed value in configurator logic

Now the final step: To be able to read value inside the configuration logic which has been passed from header logic I need to use HIDDEN input. The code bellow has better description capabilities:

1 2 def confEntryPassedValue = api.createConfiguratorEntry(InputType.HIDDEN, "PassedValue") def passedValue = confEntryPassedValue.getFirstInput()?.getValue()

The QuoteFeatures object is automatically mapped to input matrix so the passed values are displayed in input matrix and I can use PassedValue for whenever purpose I need (f.e. conditional field showing)

Use case 3: Quote Header Logic -> Line Item logic

Solution for this use case is easy: api.global

In header logic:

1 2 3 api.retainGlobal = true api.global.variableTest = "value from header2"

Quote Line Logic:

1 2 3 def value = api.global.variableTest api.logInfo("LineItemLogic value:" + value);

 

Instead of api.retainGlobal = true you can set set flag “api.retainGlobal defaults to TRUE” in configuration of partition (Admin->Configuration->General Settings) to true.

Use case 4: Quote Line Item Logic → Quote Header Logic

This can be useful e.g. if you want to calculate summary on header level or display summary chart on Custom Quote Header.

This is very easy, as it is just reading quote structure. This needs to be done in Quote Post Phase (= after line item calculation).

In header logic, you can read the values from line items this way:

1 2 3 4 5 6 7 8 9 10 if(quoteProcessor.isPostPhase()){ def lineItems = quoteProcessor.getQuoteView().lineItems lineItems.each{ lineItem -> def sku= lineItem.sku def outputItems = lineItem.outputs def value = outputItems.find{ it.resultName == "LineItemValue"}?.result api.trace("result", "sku: " +sku + " value: "+ value); } }

Use case 5: Quote Header Logic → Publishing (Preprocessing) Logic

If you are using publishing templates for quotes, you have to pass data to preprocessing (publishing) logic. Simply call api.currentItem from preprocessing logic to get full quote view.

1 def quote = api.currentItem().

Note: data will be available in preprocessing logic after the quote was saved, not before.

Use case 6: Quote Header Configurator -> Line Item Logic

From configurator logic you don’t have direct access to quote object. This means you have to pass values from header logic to line item logic.

To accomplish this behavior you need to combine following use cases:

Use case 1: Quote Header Logic Configurator → Quote Header Logic.

and then

Use case 3: Quote Header Logic -> Line Item logic

Without priceEntityAfterSave set up to true the recalculate button has to be pressed after saving values in Configurator.

Use case 7: Line Item Logic - Quote Header Configurator Logic

To read values from line item logic we can use logic from Use case 4: Quote Line Item Logic → Quote Header Logic

Then at PostPhase in header logic we pass those values through “value” property inside configurator. Use case 2: Quote Header Logic → Quote Header Logic Configurator.

Thanks to postPhase we can pass those values within one recalculation transaction.

This how would look like final code:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 if (quoteProcessor.isPostPhase()) { def quote = quoteProcessor.getQuoteView() def lineItems = quote?.lineItems def sum = 0 lineItems.each { lineItem -> def outputItems = lineItem.outputs def value = outputItems.find { it.resultName == "LineItemValue" }?.result sum = sum + value } def passedSumValue = sum def mergedValue = quote.inputs?.find { it.name == "Configurator" }?.value ?: [:] mergedValue.put("PassedValue", passedSumValue) quoteProcessor.addOrUpdateInput( "ROOT", [ "name" : "Configurator", "label": "Configurator", "url" : "ConfiguratorLogic", "type" : "CONFIGURATOR", "value": mergedValue ] ) }

Now when you click on configurator button, you will have values from line items available in that configurator.

Use case 8: Quote Header Logic Configurator → Line Item Logic Configurator

This may be useful if you want for example to set generic parameters in quote header configurator and then have pre-filled configurator on each line item, to edit the values for each line item separately.

It is possible to pass data from Quote Header Logic Configurator to Line Logic, but it is not possible to pass data from Quote Header Logic Configurator to Configurator defined in Line Item Logic.

Why? Let’s see the example of how you pass the values from Line Item Logic to Line Item Configurator.

1 2 3 4 5 api.configurator("Configurator A","ConfiguratorFormula") def confA = api.getParameter("Configurator A") if(confA != null && confA.getValue() == null) { confA.setValue(["ConfiguratorType":"A"]) }

This works fine as you use static (hard-coded) values. Data are not loaded dynamically. If you want to load data dynamically from quote header configurator, code is like this in line item logic (non-working demo!).

1 2 3 4 5 6 7 8 9 10 //NOTE: this is NON WORKING code - it is here just for exlanation what this approach it not working def conf = api.configurator("test", "ConfiguratorLogic"); //line item configurator def headerValues = api.input("valueFromHeaderConfigurator") //loading values from hidden input created in header logic valueToSet = headerValues?.QuoteFeatures?.Name?.getAt(0) //get one of the values def confA = api.getParameter("test") //getting reference for line item configurator def valueToSet = ["QuoteFeatures":["Name":valueToSet, "selected":"false"]] //data structure for input matrix to set confA?.setValue(valueToSet) //setting value to configurator

But this is not not working, because:

  1. Line Item Configurator “test” is created during syntax check.

  2. During syntax check, api.input returns only mock data, not real data.

  3. During syntax check (and only during syntax check), api.getParameter(“test”) return contex parameter of the “test” configurator. During recalculation (when syntax check is not running), api.getParameter(“test”) returns null

  4. ConfA?.setValue in syntax check:

    1. has context parameter of configurator

    2. does not have value loaded from api.input

  5. ConfA?.setvalue when quote is recalculated:

    1. does not have context parameter of configurator

    2. has value loaded from api.input, but it cannot be set, as confA is null.

 

As a result:

  • you are able to set static value via confA.setValue() as value are known at syntax check.

  • you are not able to set dynamically loaded data from header configurator because in syntax check api.input does not return expected value and during recalculation, api.getParameters(“test”) does not return context parameter of the configurator.

In this case quote recalculation will not help, as during recalculation, there is no syntax check. So let’s talk about workaround.

Workaround - Create Line Item Configurator on Quote Header

Create a configurator for line items from header logic and pass it to line items via addOrUpdate function.

Example:

Configurator on header level, defined in header logic. From this configurator, we load data to our line item configurator.

1 2 3 4 5 6 7 8 9 10 11 if (quoteProcessor.isPrePhase()) { quoteProcessor.addOrUpdateInput( "ROOT", [ "name" : "Configurator", "label": "Configurator", "url" : "ConfiguratorLogic", "type" : "CONFIGURATOR" ] ) }

Line item configurator defined in the same header logic:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if(quoteProcessor.isPostPhase()) { for (lineItemMap in quoteProcessor.getQuoteView().lineItems) { if (lineItemMap.folder) continue //skip folders def quote = quoteProcessor.getQuoteView() def configuratorValues = quote.inputs?.find { it.name == "Configurator" }?.value ?: [:] def valuesToSet = configuratorValues; quoteProcessor.addOrUpdateInput(lineItemMap.lineId, ["name" : "LinteItemConfigurator", "label" : "LinteItemConfigurator", "type" : InputType.CONFIGURATOR, "url" : "ConfiguratorLogic", "readOnly": false, "value" : valuesToSet] ) } }

In this case, line item configurator will always have the same values as quote header configurator. Any change in line item configurator will be overwritten on quote recalculation. If you want to keep changes done on item level, you have to merge it with header value, before you set them via addOrUpdateInput.

Use case 9: Quote Line Item Configurator → Quote Header Configurator

The principles are same as in Use case 7: Line Item Logic -> Quote Header Configurator Logic

Only change here is when you are iterating over lineItems you need to look inside configurator value field and collect needed values.

This needs to be done in quote header logic.

1 2 3 4 5 6 7 8 def quote = quoteProcessor.getQuoteView() def lineItems = quote?.lineItems lineItems.each { lineItem -> def inputItems = lineItem.inputs def value = inputItems.find { it.name == "ConfiguratorName" }?.value sum = sum + value?.configuratorFieldValue? : 0 }

Use case 10: Line Item Logic → Line Item Logic Configurator

It is possible to add default values to line item configurator from line item logic using following code:

1 2 3 4 5 api.configurator("Configurator A","ConfiguratorFormula") def confA = api.getParameter("Configurator A") if(confA != null && confA.getValue() == null) { confA.setValue(["ConfiguratorType":"A"]) }

So when you need pass values only once as an initialization step, it can be done using code above. If you would like to pass different values each time on recalculate, you must change the approach.

Problem here is function api.getParameter("Configurator A"). It returns Configurator object only in case that configurator was not open before and has no value in value property. Same is with f.e. stringuserentry, while is empty you can get userEntry object by calling api.getParameter. After you fill it in, no object is returned to you.

Workaround / Solution

To dynamically fill in configurators in line calculation logic, you have to update it using header logic. Basically follow the instructions in Use case 4: Quote Line Item Logic → Quote Header Logic and update configurator values while iterating.

Possible design troubles

“Add new line item” architecture awareness

Some of the use cases above are implemented with header logic updating line items.

Example:

Header logic configurator has three string inputs. I need to see those input values at line item level to work with them. I can do this in the quote header prephase logic. I will iterate over lineItems and passing them some HIDDEN fields.

So far so good.

What happen when I add new item? Only the item logic syntax check and item logic is executed. When quote header prephase did the iteration over all items, this new line was not there yet.

Conclusion:

It means that I will get those passed values on new line item only after clicking on Recalculate button. This could be UX issue.