Versions Compared

Key

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

Business use case:

You want to create a promotional campaign, in which your customers, when they buy a specified product, receive a free bonus product. In practice, this means that after adding a certain product to a quote, another product is added automatically with a zero price tag.

...

The solution will consist of two parts. First, we define the promotion in PromotionManger Agreements & Promotions and in the second part, we will handle the promotion in the quote.

...

Agreements & Promotions

First of all we need to create a logic for the promotion. The logic will enable us, when creating a new contractdocument, to determine which products will be promoted, to which customer groups it will apply and set the priority in case that we create more promotions.

Its conditions will be stored in Price Records, and for this reason, the PR column names must be the same as the names of the elements in the promotion calculation logic. We need to store the following information: ContractTermType, PromotionalProduct, PromotionPriority (change the names of PR columns accordingly). In this example we use the following names: PromotionPriority-attribute1, ContractTermType-attribute5, PromotionalProduct-attribute6.

In Configuration In Administration > Calculation Logic > Contracts Logics > Agreements & Promotions, we create the promotion logic, which will consist of five elements.

...

And finally the fifth element saves the information about ContractTermType Condition Type to the Price Records:

Paste code macro
languagegroovy
titleContractTermType.groovy
def ctt = api.isSyntaxCheck() ? [:] : api.currentItem().contractTermType
return ctt

When we have all the information about our promotion safely stored in Price Records, we can use it in quotes.

...

Quoting

To achieve the functionality described at the beggining of beginning of this article, we need to transport information from the quote header logic (that adds the bonus item) to the line logic. We will use HIDDEN type inputs to do that (see also How to Create Quote Line Items from Header Logic).

First, we will create a quote header logic:

Paste code macro
languagegroovy
api.logInfo("Quote Header Logic", "************ START ************")
api.logInfo("IsPrePhase", quoteProcessor.isPrePhase())

if (quoteProcessor.isPrePhase()) {
  for (lineItemMap in quoteProcessor.getQuoteView().lineItems) {
    if (lineItemMap.folder) continue

    def yId = shallPromoXforYBeApplied(lineItemMap)

    if (yId!=null) {
      def isPromoAlreadyApplied = isPromoXforYAlreadyApplied(lineItemMap)

      if (!isPromoAlreadyApplied) {
        def quoteQuantity = getInputValueForLine(lineItemMap.lineId, "Quote_Quantity")
        list = []
        list.add([name: "XforY", type: InputType.HIDDEN, "value": lineItemMap.lineId])
        list.add([name: "Requested_Price", label: "Requested_Price", type: InputType.USERENTRY, "value": 0])
        list.add(["name": "Quote_Quantity", "label": "Quote_Quantity", "type": InputType.USERENTRY, "readOnly" : true, "value": quoteQuantity ])
        addedLineItem = quoteProcessor.addLineItem(lineItemMap.parentId, yId, list)

        setPromoXforYAlreadyApplied(lineItemMap)

      }
    }
  }


  for (lineItemMap in quoteProcessor.getQuoteView().lineItems) {
    if (lineItemMap.folder) continue

    isItY = getInputValueForLine(lineItemMap.lineId, "XforY")

    if (isItY != null) {

     if(!checkIfXExist(isItY)){
       quoteProcessor.deleteItem(lineItemMap.lineId)
     }
      setRequestedPriceReadOnly(lineItemMap.lineId)
      yQuantity = getInputValueForLine(lineItemMap.lineId, "Quote_Quantity")
      xQuantity = getInputValueForLine(isItY, "Quote_Quantity")

      api.logInfo("yQuantity", yQuantity)
      api.logInfo("xQuantity", xQuantity)

      setYQuantity(lineItemMap.lineId, xQuantity)
    }
  }


}

/**
 * after changing the quantity of productX and choosing 'Recalculate Changes' or 'Recalculate' , the quantity of product Y should be also changed,
 * in PrePhase only "Quote_Quantity" input is being changed by setYQuantity, output "Quantity" remains without any changes, after
 * invoking setYOutputQuantity in postPhase it is finally changed
 * */

if (quoteProcessor.isPostPhase()) {
  api.logInfo("IsPrePhase", "PostPhase!")
  api.logInfo("IsPrePhase", quoteProcessor.getQuoteView().lineItems)
  def qq = api.getParameter("Quote_Quantity")
  api.logInfo("IsPrePhase qq", qq)
  for (lineItemMap in quoteProcessor.getQuoteView().lineItems) {
    if (lineItemMap.folder) continue

    isItY = getInputValueForLine(lineItemMap.lineId, "xforY")
    api.logInfo("isItY", isItY)

    if (isItY != null) {

      yInPutQuantity = getInputValueForLine(lineItemMap.lineId, "Quote_Quantity")
      yOutPutQuantity = getOutputValueForLine(lineItemMap.lineId, "Quantity")

      api.logInfo("yInPutQuantity", yInPutQuantity)
      api.logInfo("yOutPutQuantity", yOutPutQuantity)

      if (yInPutQuantity != yOutPutQuantity) {
        setYOutputQuantity(lineItemMap.lineId, yInPutQuantity)
      }

    }
  }
}

api.logInfo("Test Quote Header Logic", "************ END ************")

// *************************  FUNCTiONS  ******************************

def shallPromoXforYBeApplied(lineItem) {
  // x should be read from PriceRecords
  def targetDate = api.targetDate()
  def sku = lineItem?.sku
  def customerId = quoteProcessor.getHelper().getRoot().getInputByName("Customer")?.value


  if(customerId!=null){
    def filtersPromotion = [
        api.productToRelatedObjectsFilter("PR", sku),
        api.customerToRelatedObjectsFilter("PR", customerId),
        Filter.lessOrEqual("validAfter", targetDate),
        Filter.or(Filter.greaterOrEqual("expiryDate", targetDate), Filter.isNull("expiryDate"))
    ]

    priceRecordRows = api.find("PR", 0, 1, "attribute1", *filtersPromotion, Filter.like("attribute5", "XforY"))

    def yId =  priceRecordRows[0]?.attribute6

    return yId
  }


  return null
}

/**
 *  returns given input from the quote header. We need to read it this way, because api.input() works only on the line items.
 *  */

def getInputValue(inputName) {
  return quoteProcessor.getQuoteView()?.inputs?.find { it.name == inputName }?.value
}

def getInputValueForLine(lineId, inputName) {
  inputs = quoteProcessor.getQuoteView()?.lineItems?.find { it.lineId == lineId }?.inputs

  for (input in inputs) {
    if (inputName.equals(input.name)) return input.value
  }
  return null
}

def getOutputValueForLine(lineId, outputName) {
  outputs = quoteProcessor.getQuoteView()?.lineItems?.find { it.lineId == lineId }?.outputs

  for (output in outputs) {
    if (outputName.equals(output.resultName)) return output.result
  }
  return null
}

def setPromoXforYAlreadyApplied(lineItem) {
  def rootPromoAlreadyAppliedMap = api.isSyntaxCheck() ? [:] : (getInputValue("ROOT_IsPromoXforYAlreadyAppliedMap") ?: [:])
  rootPromoAlreadyAppliedMap[lineItem.lineId] = "true"
  quoteProcessor.addOrUpdateInput(["name" : "ROOT_IsPromoXforYAlreadyAppliedMap",
                                   //"label":"ROOT_IsPromoXforYAlreadyAppliedMap",
                                   "type" : InputType.HIDDEN,
                                   "value": rootPromoAlreadyAppliedMap]
  )
  api.logInfo("rootPromoAlreadyAppliedMap", rootPromoAlreadyAppliedMap)
}

/**
 * returns TRUE, if the special Promo Line (X for Y) was already added for this line
 */
def isPromoXforYAlreadyApplied(lineItem) {
  def rootPromoAlreadyAppliedMap = api.isSyntaxCheck() ? [:] : getInputValue("ROOT_IsPromoXforYAlreadyAppliedMap")
  promoAlreadyApplied = (rootPromoAlreadyAppliedMap?.get(lineItem.lineId) == "true")

  return promoAlreadyApplied
}

def setYQuantity(lineId, newQuantity) {
  quoteProcessor.addOrUpdateInput(lineId, ["name": "Quote_Quantity", "label": "Quote_Quantity", "value": newQuantity, "readOnly" : true])
  quoteProcessor.addOrUpdateOutput(lineId, ["resultName": "Quantity", "label": "Quantity", "result": newQuantity])
}

def setYOutputQuantity(lineId, newQuantity) {
  quoteProcessor.addOrUpdateOutput(lineId, ["resultName": "Quantity", "label": "Quantity", "result": newQuantity])
}

def setRequestedPriceReadOnly(lineId){
  quoteProcessor.addOrUpdateInput(lineId, ["name": "Requested_Price", "label": "Requested_Price", "value": 0, "readOnly" : true])
}

def checkIfXExist(isItY){
  quoteProcessor.getQuoteView().lineItems
  xExist = quoteProcessor.getQuoteView()?.lineItems?.find { it.lineId == isItY }
  if(xExist!= null) return true
  return false
}

Then we need to create a logic for the quote line items in Configuration Administration > Calculation Logic Logics > Generic Logic. This logic will have three elements.

...

Paste code macro
languagegroovy
//here we can read the value of XforY input, for now it is Id of the X line
def inputXforY = api.input("XforY")
api.logInfo("inputXforY", inputXforY)
return inputXforY

Everytime Every time you add product X to your quote, the header and line logics will be executed in the following order:

...