Lab Info
Lesson | Agreements & Promotions |
Category / Topic / Section | Pricefx Core / Agreements & Promotions / CFG 2 |
Target Audience | Certified Configuration Engineer |
Estimated Time to complete | 1:00 |
Learning Outcomes
At the end of the Lab, you should be able to:
lookup contract’s Price Records from within another module
calculate the Promotion Contract impact on Quote calculation
Pre-requisites
In order to be able to complete this laboratory exercise, please complete the prerequisites before you proceed.
Labs
In order to implement the Quote which reads the Price Records, we expect you to already have some Price Records created from the previous 2 Labs.
This lab expects, that you already have in your partition a solution of the preceding labs:
Agreements & Promotions Engineering - Lab 1a
Agreements & Promotions Engineering - Lab 1b
If you need to work on this lab without finishing the preceding ones first, you can deploy the solutions of the preceding labs provided in a ZIP file attached to the course.
User Story / Requirements
As a Sales Manager, I want the Invoice Price (on the Quote Line Item) to be influenced by the Promotion Contracts, so that I can negotiate correct Invoice Price with the customer and correctly apply the corresponding promotion contracts.
Details
The Quote line item’s Invoice Price can be impacted by several different types of the Promotion Contracts.
In our example,they are Promotion Discount and Volume Discount.
the discounts in this lab are additive - i.e. on the Quote the discount percents will be totaled before they are applied.
If more Condition Types apply to the Quote line item, all of them should be used.
There’s a chance, that Sales/Marketing managers negotiated overlapping contracts of the same type.
In such case, only the contract with highest discount will be applied.
Besides the actual impact on the Invoice Price, the user would also like to see the ID of the contract which is used for the promotion discount.
if possible, the ID should be displayed as a link, so that the user can directly click to see the contract detail.
Acceptance Criteria
On the Quote Line Item, the user can see (additionally to the provided implementation):
for Promotion Discount
Promotion Discount percentage - value found on the Promotion Contract (stored in Price Records) matching to the quote line item’s Product and Customer selection.
if more contracts of the Promotion Discount type are found for the line item, use the one with highest percentage value.
Promotion Discount contract ID - which matching contract is used. This number should be displayed as a link leading the user to the detail page of the particular contract.
for Volume Discount
Volume Discount percentage - value found on the Promotion Contract (stored in Price Records) matching to the quote line item’s Product and Customer selection and also matching to the Quantity entered by the used on the line item.
if more contracts of the Volume Discount type are found for the line item, use the one with highest percentage value.
Volume Discount contract ID - which matching contract is used. This number should be displayed as a link leading the user to the detail page of the particular contract.
Invoice Price
the Invoice Price is calculated from the List price in the following way:
ListPrice * (1.0 - VolumeDiscount% - PromotionDiscount% )
the discounts in this lab are additive - i.e. on the Quote the discount percents will be totaled before they are applied.
In the consumer space though, many times the discounts are multiplicative (i.e. get an additional discount), in such case the Invoice Price would be
ListPrice * (1.0 - VolumeDiscount%) * (1.0 - PromotionDiscount%)
Implementation Steps
If you already have some previous knowledge about implementation/configuration of the PromotionManager module, we encourage you to try to implement it first on your own based on the information provided in the User Story and Acceptance Criteria.
Step: Create basic Quote Line Item Logic, the starting point
create a logic
in Studio, create new Calculation Logic, named "QuotePromotion"
the logic has Default Nature
add Element "CustomerId", with Display mode Never and with code:
return api.customer("customerId")
add Element "ProductId",with Display mode Never and with code:
return api.product("sku")
add Element "Quantity",with Display mode Never and with code:
//api.userEntry("Quantity") final String INPUT_NAME = "Quantity" if (api.isInputGenerationExecution()) { api.inputBuilderFactory().createUserEntry(INPUT_NAME).getInput() } else { return input[INPUT_NAME] }
add Element "AbortOnInputGeneration", with Display mode Never and with code:
if (api.isInputGenerationExecution()) { api.abortCalculation() }
add Element "ListPrice", with code:
// return fictious number for the sake of the training exercise return 21.3
this value should be displayed
this value is formatted as Money (EUR)
Test the Logic
test with empty parameters, if all ok
test with all parameters entered, if all ok
Deploy the logic to the partition
create a Quote Type
in Unity, navigate to Quoting Quote Types
click on Add Quote Type and add new Quote Type
Name: "QuotePromotion"
Pricing Logic: "QuotePromotion"
in Studio, fetch the new Quote Type QuotePromotion to your project, so you can later put it to Git
Step: Implement reading Price Records with promotion conditions
Reading of the Price Record is done using either api.find()
or api.stream()
, but the key thing is the filtering, because on the Quote line you have a single SKU and single CustomerId, where as on the the contract conditions are defined mostly on a set of products and set of customers.
So you must use the special Filter functions to implement the search.
As you will need to build the filters several times for different types of contracts, you will implement the filters as a function.
add new Element "FilterLib", with Display mode Never, with code:
List<Filter> buildFilters(String conditionType, String productId, String customerId) { def filters = [ Filter.equal("ContractTermType", conditionType), Filter.equal("status", "ACTIVE"), /* select only Price Records valid at the Effective Date of the Quote */ Filter.lessOrEqual("validAfter", api.targetDate()), Filter.greaterOrEqual("expiryDate", api.targetDate()), ] if (productId) { filters << Filter.or( api.productToRelatedObjectsFilter("PR", productId), Filter.isNull("productGroup") ) } if (customerId) { filters << Filter.or( api.customerToRelatedObjectsFilter("PR", customerId), Filter.isNull("customerGroup") ) } return filters }
search only for the specific Condition Type. Remember, this is your customized column of Price Record, and the value gets there from each Contract Line.this is the special function, which builds a filter unique for search of Price Records table by specific SKU, even though the Price Record is defined for a set of products.this is additional check (must be there), because the productGroup can be null (some contracts allows it), and
api.productToRelatedObjectsFilter()
does not cover that situationthis is the special function, which builds a filter unique for search of Price Records table by specific CustomerId, even though the Price Record is defined for a set of customers.this is additional check (must be there), because customerGroup can be null (some contracts allows it), andapi.customerToRelatedObjectsFilter()
does not cover that situationnotice, that there’s an OR between the next 2 filters, it important for correct working
Step: Implement finding the right contract
Remember, there’s a chance, that you will have at the same date more contracts (of the same type) effective. In such case, in our training requirement, the Sales wants to use the highest discounts.
Using the Promotion Discount:
add Element "PromotionDiscountContract", with Display mode _Never, with code:
final String FIELD_PROMOTION_DISCOUNT = "PromotionDiscount" def filters = FilterLib.buildFilters("PromotionDiscount", out.ProductId, out.CustomerId) def highestPromotionDiscount = 0.0 def contract = [:] def iter = api.stream("PR", null, ["sourceId", FIELD_PROMOTION_DISCOUNT], *filters) iter.each { pr -> def discountPct = pr[FIELD_PROMOTION_DISCOUNT] as BigDecimal if (discountPct > highestPromotionDiscount) { highestPromotionDiscount = discountPct contract.highestPromotionDiscount = highestPromotionDiscount contract.selectedContractId = pr.sourceId } } iter.close() return contract
build the Price Record filters only for the specific type of Condition types linesmore Price Records could be available - need to review all of them.In our case, based on the business decision, the higher discount will be used.
add Element "PromotionDiscountPct", with code:
return out.PromotionDiscountContract?.highestPromotionDiscount
this element’s result should be visible
the value should be formatted as Percentage
add Element "PromotionDiscountContractId", with code:
def contractId = out.PromotionDiscountContract?.selectedContractId if (contractId) { return api.getBaseURL() + "/app/#/contracts/detail/${contractId}/contractDetail" }
Note, that this solution will work only in Unity UI. In Classic UI, you would have to solve the link in a different way.
this value should be visible
this value must be formatted as Link
Verify, that the logic works as expected
Using the Volume Discount:
add Element "VolumeDiscountContract", with Display mode _Never, with code:
final String FIELD_VOLUME_DISCOUNT = "VolumeDiscount" def filters = FilterLib.buildFilters("VolumeDiscount", out.ProductId, out.CustomerId) // more PRs could be available - need to solve conflict. In our case, the higher discount wins def highestVolumeDiscount = 0.0 def contract = [:] def iter = api.stream("PR", null, ["sourceId", FIELD_VOLUME_DISCOUNT], *filters) iter.each { pr -> def volumeDiscountTiers = api.jsonDecode(pr[FIELD_VOLUME_DISCOUNT]) .collect { [(it.key as BigDecimal), (it.value as BigDecimal)] } volumeDiscountTiers.sort { -(it[0]) } def discountPct = volumeDiscountTiers.find { out.Quantity >= it[0] }?.getAt(1) if (discountPct > highestVolumeDiscount) { highestVolumeDiscount = discountPct contract.highestVolumeDiscount = highestVolumeDiscount contract.selectedContractId = pr.sourceId } } iter.close() return contract
build the Price Record filters only for the specific type of Condition types linesmore Price Records could be available - need to review all of them.remember, the volume tiers thresholds are stored in a map encoded in JSON format. Review your Price Records created from the Volume Discount contracts to recall the values.In our case, based on the business decision, the higher discount will be used.
add Element "VolumeDiscountPct", with code:
return out.VolumeDiscountContract?.highestVolumeDiscount
this element’s result should be visible
the value should be formatted as Percentage
add Element "VolumeDiscountContractId", with code:
def contractId = out.VolumeDiscountContract?.selectedContractId if (contractId) { return api.getBaseURL() + "/app/#/contracts/detail/${contractId}/contractDetail" }
Note, that this solution will work only in Unity UI. In Classic UI, you would have to solve the link in a different way.
this value should be visible
this value must be formatted as Link
Verify, that the logic works as expected
Invoice Price evaluation - finally, you have to evaluate the Invoice Price, because it is impacted by both types of the contracts.
add Element "InvoicePrice", with code:
if (out.ListPrice == null) { api.addWarning("Cannot calculate Invoice Price, "+ "because List Price is not available.") return } def promotionDiscount = out.PromotionDiscountPct ?: 0.0 def volumeDiscount = out.VolumeDiscountPct ?: 0.0 return out.ListPrice * (1.0 - promotionDiscount - volumeDiscount)
the value must be visible
it should be formatted as Money (EUR)
Verify, that the logic works as expected
Deploy your Quote Logic to the partition
Step: Verify the Quote Logic
We invite you now not only to verify the functionality, but mainly to experiment with your system - try different combinations of new Promotion Contracts and then experiment with different Quote settings, and review how the contract conditions are applied.
Pay attention while testing of this use case, because it is sensitive to data available in Price Records table and Effective date of your Quote
So before you start testing, review:
Price Records available and their
Condition Types in column ConditionType
validity range, in columns ValidAfter
values of the discounts in columns PromotionDiscount and VolumeDiscount
Quote
the Effective date of the Quote must fit into the range of your Price Record contract lines, otherwise the code will not find any
If you are missing PriceRecords needed for testing, remember, you can quickly go to PromotionManager, create new contracts and submit them for approval.
Let’s expect to have PriceRecords like following:
in Unity, navigate to Quoting Quotes
Click on New Quote and select the Quote Type QuotePromotion
set Effective Date to be within the range of your Price Records
select a Customer, which fits into customer groups used by your Price Records
move to Items tab
add a product, which fits the sets of products in your Price Records
enter Quantity so that some VolumeDiscount contract could effect the line
Use Recalculate to ensure, that the Quantity si used for Contract lookup
Verify, if correct values are caclulated
Verify, that the links work ok
References
Developer Documentation
Documentation (Classic)
Older Training Materials
Accelerators (non-public)