Offering Summary
Introduction
Sales structure is a hierarchical representation of the product configuration. It consists of different sections which can be hidden based on UI selections. The structure is represented by an offering summary tree which consists of nodes (Offering Summary section).
Quotation's offering summary is generated by the services defined in the configuration
and adapter
keys under the product line configuration.
The Adapter takes the input and forms the configuration object required by the configurator.
The Configurator uses the configuration object to build the finished offering summary
Building the Offering Summary
To get an instance to the Offering Summary tree, you must ask the Offering Summary manager to build it. The tree is revision specific and is generated if:
- The revision is the latest in the quotation and the revision is not valid (meaning some user data has changed)
- The user wants to refresh the summary
- The summary has never been built
You can get the Offering Summary manager with the service id klaro_quotation.offering_summary
:
<?php
$offeringSummaryManager = $this->get('klaro_quotation.offering_summary');
To get the tree for a any revision, call OfferinSummaryManager::generate()
method:
<?php
$offeringSummaryManager = $this->get('klaro_quotation.offering_summary');
$quotation = $quotationFacade->getQuotation(1);
$revision = $quotationFacade->getQuotationRevision($quotation, 1);
$summaryTree = $offeringSummaryManager->generate($revision);
The summary is constructed roughly in these phases:
- Create new Offering Summary tree and get the structure rules from the revision. This can be modified or added by the application in the
OfferingSummaryEvents::SUMMARY_STRUCTURE_CREATE
event. This is where the application can add visitors to the tree. - Add sections according to the rules.
- Run default visitors on the tree structure.
- Run tree-specific visitors on the structure
- Finalize summary the summary. If quotation is in order state, the price can be overridden by the user which is calculated here.
- Export the summary and store to revision.
The following events are available during the structure creation process (see OfferingSummaryEvents
class).
Event | Description |
---|---|
SUMMARY_STRUCTURE_CREATE | Triggered when the sales structure is being created. You can set the structure by setting the structure key on the event data. By default, this the value stored in the revision when it was created and read from configuration. |
SUMMARY_STRUCTURE_READY | Triggered when the sales structure has been generated and the all the visitors have run. |
Below is the activity diagram about what happens when the summary tree is requested.
Section Structure
The idea is that the structure defines "slots" into which product items can be inserted based on user selections. Each configuration section consists of a key (which acts as the identifier for that section), a title and optional subsections.
[sectionId]:
title: [...]
sections:
[subSectionId1]:
title: [...]
sections:
[antoherSection]:
title: [...]
...
[subSectionIdN]:
title: [...]
Each section can have multiple subsections, and they in turn can have more subsections, etc. Besides title
and sections
, each section can have options to repeat that section or to hide it.
Options for each section:
Option | Type | Optional | Description |
---|---|---|---|
code | string | Yes | Section identifier. If empty, the array key is used. |
title | string/condition | No | Section title. By default just a string but use evaluateTitle option to evaluate the title as a condition string (useful for dynamic titles). |
evaluateTitle | boolean | Yes | If true, the title string is evaluated as a condition. |
sections | array | No | List of subsections |
condition | condition | Yes | Condition string, if true section is shown - otherwise hidden. If condition is not present, section is always shown. |
repeat | condition | Yes | Repeat the current section for the amount returned by the condition statement. |
Example:
title: Plant
sections:
line:
title: '"Line" ~ " " ~ (this.subId + 1)
evaluateTitle: true
repeat: 'models.site.NumberOfLines'
sections:
feedbox:
title: FB (Feedbox)
condition: 'models["lines/line"][flotationLine.subId].FB === "yes"'
sections:
feedboxLining:
title: Feedbox Lining
In the condition statements, following objects are available:
Object | Description |
---|---|
model | The current phase model |
models | Model provider, use models.ModelName.Field to access fields from other models. If a phase is repeatable, then you have to use models.get("ModelName", index) to get the correct sub model. By default, the first sub model is always returned. |
quotation | The current quotation, use like quotation.QuotationType == "FP" |
revision | The current revision |
user | The current user, use like user.FullName |
this | The current section. This is not available in repeat expression (see "parent" below). If a phase is repeatable and you need current subId number (for example in condition option), you can use subId without this . Eg. models["phaseId"][subId].ModuleIncluded == 1 |
parent | Reference to the section parent. In the repeat expression, this refers to the section under which the repeatable section is being created. This is because the repeatable value has to be defined before the item is created for N amount of times, we cannot reference the item itself (ie. it refers to the de facto parent of the repeatable item). |
To access sections in the hierarchy, use parent
to access the section's parent or parent.parent("[parentSection]")
where [parentSection]
refer to the section keys defined in the hierarchy. To illustrate using the above example code, in section feedboxLining
we could write in the condition this.parent("line")
to access the line
section object.
The following section properties can be used in conditions (by writing [section].Property
). Section subId
should be used for getting the correct model for repeatable sections, eg. models["RepeatablePhaseModel"][section.subId]
.
Property | Description |
---|---|
ItemCode | Section Item code |
Title | Section title |
Level | Section nesting level with 0 being the main level. |
SubId | Running number for repeatable sections, 0 for the first section. |
Offering Summary Visitors
Visitors provide a way for the application access the offering summary tree structure. The offering summary tree implements the Visitor Design pattern to allow for others to visit the summary structure, make changes to it or gather some information about it. This also enables to add sections and items that are not defined in sales structure configuration.
Visitors must implement the Klaro\QuotationBundle\Api\OfferingSummaryVisitorInterface
. The visitors can be either added to the offering summary manager or to the created tree. The difference is that the ones attached to the manager are always run first and the tree visitors only for those trees that they have been attached to.
NOTE: Visitors are only run once when the tree is being built! So if nothing has changed in the configuration, there is no need to rebuild the tree - and therefore no need to run the visitors.
To add a default visitor, add a service with the tag klaro_quotation.offering_summary_visitor
. To add a visitor the offering summary tree, catch the OfferingSummaryEvents::SUMMARY_STRUCTURE_CREATE
event and attach a new visitor to the tree:
<?php
namespace OSC\FlotationBundle\Subscriber;
class QuotationEventSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return [
OfferingSummaryEvents::SUMMARY_STRUCTURE_CREATE => 'onSummaryStructureCreate'
];
}
public function onSummaryStructureCreate(OfferingSummaryEvent $event) {
$summaryTree = $event->getSummary();
$visitor = ... // Get / create new visitor
$summary->addVisitor(visitor);
}
}
To just run a visitor on the tree structure, you can also just create the visitor, call OfferingSummaryTree::acceptVisitor()
if you don't want the visitor to be visible to other code using the summary. This can be done for example in the OfferingSummaryEvents::SUMMARY_STRUCTURE_READY
event or whenever you have access to the offering summary tree.
<?php
$summaryTree = $offeringSummaryManager->generate($revision);
$summaryTree->acceptVisitor(new MyVisitor());
NOTE: For every visitor, the visit()
method is called but only for the root node. To access elements under the root section, you can do it by recursively calling acceptVisitor()
on the section's subsections. The parameter is the current offering summary section being visited.
Also the method setContext()
is called before to the set the reference to the current offering summary tree. You can use it store the current revision and initialize the context.
<?php
class MyVisitor implements OfferingSummaryVisitorInterface {
protected $revision;
public function setContext(OfferingSummaryTree $context) {
$this->revision = $context->getQuotationRevision();
}
public function visit(OfferingSummaryItem $section) {
// do some processing...
// Process sub-levels.
if($section->hasSections()) {
foreach($section->getSections() as $subSection) {
$subSection->acceptVisitor($this);
}
}
}
}
Managing sections
Sections can be created by calling the OfferingSummaryItem::addSections()
or OfferingSummaryItem::addSection()
. The parameter is an array of config values as described above in "Section Structure".
<?php
$root = $summaryTree->getRoot();
// Add one section
$main = $root->addSection([
'code' => 'root',
'title' => 'Main section'
]);
// Add two sections under main
$main->addSections([
'first' => [
'title' => 'First Subsection'
],
'second' => [
'title' => 'Second Subsection'
]
]);
This will produce the following hierarchy:
- Main section
- First Subsection
- Second Subsection
For each section, you calculate the totals of sales and cost price and the margin based on these. The values are summed from the product items that are added under the sections (see below).
Creating product items
Product items (or conf items) are product rows that are placed under the offering summary sections. Items have an identifying item code, (an external and an internal) title, quantity, as well as cost and sales prices.
To add a conf item to a section, use OfferingSummaryItem::addItem()
. The item must implement the interface in Klaro\QuotationBundle\Api\ConfItemInterface
.
To create a conf item from scratch, call OfferingSummaryItem::createConfItem()
:
<?php
$confItem = $section->createConfItem()
->setItemCode('ABC-123')
->setInternalTitle('Item name for internal use')
->setExternalTitle('Item name for external use')
->setCostPrice(100)
->setSalesPrice(200);
Product item code visitor
By default, the summary will be visited by the product item visitor that will go through the structure and add product items to it according to the given rules (see Product Items). So instead of manually adding product items the sections, you can define rules for how to create or copy them automatically from a database.
From this visitor, you can query which product codes were added and their numbers. The implementation can be found in Klaro\QuotationBundle\Library\SaleItemSummaryItemCodeVisitor
.
For more information about product items and the rules see the Product Items page.
Factors
You can apply factors to each section that influence the product item prices. Factors are shown in summary when you hover over the price (requires a permission).
To add a factor, call OfferingSummaryItem::addFactor()
to apply a factor to section or OfferingSummaryItem::addFactor()
to apply to the whole tree:
<?php
// Apply 10% increase to cost & sales price in one section called "Risk and Contingency".
$main = $summaryTree->getSection('main');
$main->addFactor('CostPrice', 1.1, 'Risk and Contingency');
$main->addFactor('SalesPrice', 1.1, 'Risk and Contingency');
// Apply user discount of 5% to the whole tree's sales price.
$summaryTree->addFactor('SalesPrice', 0.95, 'User Discount');
Sales Tax
Sales tax is added to product items with ConfItemInterface::setSalesTaxPercentage()
. One item may have only one sales tax percentage value applied but within the whole summary tree there may be multiple percentages defined.
<?php
// Apply 20% sales tax to "main" section items.
$main = $summaryTree->getSection('main');
if($main->hasItems()) {
foreach($main->getItems() as $confItem) {
$confItem->setSalesTaxPercentage(20);
}
}
You can get the list of sales tax total with ConfItemInterface::getSalesTaxTotals()
. This will return the list of the percentage and the total value of that tax in the tree sum. Total tax value is the sum the values.
<?php
// Get totals when 10% tax is applied to items under "Services" and 20% for "Components".
$totals = $summaryTree->getSalesTaxTotals();
// Result => [
// 10 => 1234,
// 20 => 5678
// ]
$taxes = $summaryTree->getTotalSalesTax(); // 1234 + 5678 = 6912
Totals
Totals are calculated based on the product items under each section. To get a section total, call any of the methods below. Calling the methods for the root node is the same calling it for the whole offering summary tree.
Method | Description |
---|---|
getCostPrice() | Get cost price sum of items in the section |
getTotalCostPrice() | As above + subsections' cost price sums |
getSalesPrice() | Get sales price sum of items in the section |
getTotalSalesPrice() | As above + subsections' sales price sums |
getTotalSalesPriceWithTax() | As above + tax |
getSalesTaxTotals() | List of percentages and taxes in section + subsections |
getTotalSalesTax() | Sum of the tax values above |
getAggregateSum() | Get grouped sum of column values. |
Aggregate sum example:
<?php
// Get product quantities grouped by item code:
$quantities = $summaryTree->getAggregateSum('ItemCode', 'Quantity');
// Result:
// $quantities = [
// 'ItemCode1' => 4,
// 'ItemCode2' => 1,
// ...
// ]
Metadata
Sometimes it will be useful to store metadata in the summary structure which is not shown anywhere. For sections, you can add that with OfferingSummaryItem::setMetaData()
and access it later with OfferingSummaryItem::getMetaData()
. For example:
<?php
// Add metadata for section to hide it from printout.
$main = $summaryTree->getSection('main');
$main->setMetaData('ShowInDocuments', false);
// when printing:
if($section->getMetadata('ShowInDocuments') === true) {
// print section data
}
Product Items
See Product Items for managing sections items.