...
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.
Requirements:
After choosing a customer and adding product
X toX to the quote, product Y should be added to the quote automatically.
After changing the quantity of product X and clicking 'Recalculate Changes' or 'Recalculate', the quantity of product Y should change accordingly and the user input should be read-only.
The 'Requested Price' input should be set to 0 and read-only for product Y.
After deleting a quote line containing product X and clicking 'Recalculate Changes' or 'Recalculate', the line with product Y should be deleted.
Solution:
The solution will consist of two parts. First, we define the promotion in Agreements & Promotions and in the second part, we will handle the promotion in the quote.
...
The first element creates a user input where the promoted product is selected:
ProductGroup.groovy
paste-code-macro | ||||
---|---|---|---|---|
| ||||
def pg =api.productGroupEntry() if(pg!=null && !"sku".equals(pg?.productFieldName)){ api.redAlert("Only one product is permitted!") } return pg?.productFieldValue |
The second element creates a user input for selecting the free promotional product:
...
PromotionalProduct.groovy
Code Block | ||||
---|---|---|---|---|
| ||||
def pg =api.productGroupEntry("Promotional Product") if(pg!=null && !"sku".equals(pg?.productFieldName)){ api.redAlert("Only one product is permitted!") } return pg?.productFieldValue |
The third element creates a user input for Customer Group:
...
CustomerGroup.groovy
Code Block | ||||
---|---|---|---|---|
| ||||
def cg =api.customerGroupEntry() return cg |
The fourth element creates a user input for assigning a priority level to the promotion:
...
PromotionPriority.groovy
Code Block | ||||
---|---|---|---|---|
| ||||
def priority = api.option("priority", [ "1", "2", "3" ]) def prio = api.getParameter("priority") // retrieve the context parameter with the same name as the input if (prio != null && prio.getValue() == null) { prio.setLabel("priority") // set the displayed label prio.setRequired(true) // set the mandatory hint prio.setReadOnly(false) // set the read only flag } return priority |
And finally the fifth element saves the information about Condition Type to the Price Records:
ContractTermType.groovy
paste-code-macro | ||||
---|---|---|---|---|
| ||||
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 quotesQuotes.
Quoting
To achieve the functionality described at the 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 | ||
---|---|---|
| ||
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 Administration > Logics > Generic Logic. This logic will have three elements.
The first element creates a user input, where the user indicates the desired quantity of the product:
paste-code-macro | ||
---|---|---|
| ||
def quantity = api.userEntry("Quote_Quantity") return quantity |
The second element creates a user input, where the user indicates the requested price:
paste-code-macro | ||
---|---|---|
| ||
def requestedPrice = api.userEntry("Requested_Price") return requestedPrice |
In the third element, we just want you to show, that in the line logic we can read a value of the input which has been added in Header Logic.
paste-code-macro | ||
---|---|---|
| ||
//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
|
Every time you add product X to your quote, the header and line logics will be executed in the following order:
Header logic in the pre-phase.
Line logic for new product (X).
Line logic for the new, added by the header logic, product (Y).
Header logic in the post-phase.
For your convenience, all the code samples are downloadable below:
View file | ||||
---|---|---|---|---|
|
View file | ||||
---|---|---|---|---|
|
View file | ||||
---|---|---|---|---|
|