Protect and Control Sales Order Margins with Business Rules

Following on from the excellent blog written by my colleague Harshad Mutha, in which he describes a method to control the low margin of sales orders using workflow capabilities, I wanted to follow on and offer a different spin on this issue, that several businesses face.

To be more specific, this blog will differ, in that we will focus on controlling the margin at the sales order item level and we will leverage the alternate price determination functionalities to determine the margin and throw an error message if the margin is too low. This will still allow you to save the document but will be considered incomplete (hence no transfer of requirements). This method is also different in that we enforce a company policy – thus requiring the person entering the sales order to do what is needed (eg reduce discounts..) to keep the margin within acceptable minimums.

As a summary, what we will cover in this blog:

  • Custom logic: We will leverage a pricing BADI, in which we will code an alternative calculation of a condition-amount determination, that we will subsequently use in our sales pricing procedure.
  • Manage your solution / CBC: We will need to make some configuration settings.
    • Register our BADI as an alternative condition calculation amount
    • Create a ‘Sales Margin’ pricing condition.
    • Amend our pricing procedure to reference the above pricing condition and alternative calculation

Lastly, whilst we will start off by hard coding the minimum sales order margin, this is not very flexible. I am a firm believer that business rules should be the responsibility of the business users to own and maintain. So, in line with this thought, we will show you how you can leverage a Custom Business Object (CBO) to house these business rules.

So let’s get going.

It might sound odd to start here but that is what will do and I’m sure the rationale for that will become evident in the subsequent steps.

Go to the App “Custom Logic”, and make sure the tab “Custom Logic” is selected.

Custom%20Logic

Custom Logic

Then click “+” to add your own logic. When you add an extension, you need to indicate the appropriate business context and BAdI. In this case, we are, as shown below, interested in :

  • The Business Context of “Pricing
  • The BAdI with description: Define Alternative Calculation of Condition Amount in Document Item
  • Enter an Implementation Description of your choice

BAdI%20Context

BAdI Context

The next thing you will want to do is add your own code, by creating a draft. Make sure you read the documentation and you can leverage the example code as a starting point.

I am below adding the entire code I used (not production ready!).

* Mandatory step for all implementations:
* Move the importing parameters to the changing parameters. MOVE-CORRESPONDING item_amounts TO item_result_amounts. MOVE-CORRESPONDING item_attributes TO item_result_attributes. MOVE-CORRESPONDING item_quantities TO item_result_quantities. MOVE-CORRESPONDING prcg_element_attributes TO prcg_element_result_amounts. * the below two lines are our code. We are essentially comparing the item cost with the net * amount to construct the margin. One line updates the amount, and the other the ratevalue TRY. prcg_element_result_amounts-conditionamount = ( ( 1 - ( item_amounts-costamount / item_amounts-netamount ) ) * 100 ) . prcg_element_result_amounts-conditionratevalue = ( ( 1 - ( item_amounts-costamount / item_amounts-netamount ) ) * 100 ) . * If the margin is less than 30% - here we have hard code the margin
* If the margin does fall below, we issue a status message to the user IF prcg_element_result_amounts-conditionratevalue < 30.
* it is possible to raise a message if an issue is identified pricing_message = 'Check the Margin of your Order Item'.
* indicate a pricing error if required item_result_attributes-pricinghaserror = abap_true. ENDIF. CATCH cx_sy_conversion_error cx_sy_arithmetic_error INTO DATA(lx_conversion). prcg_element_result_amounts-conditioninactivereason = 'X'. prcg_element_result_amounts-conditionamount = 0.
* You should use the statement "RAISE EXCEPTION" only for critical business errors or programming runtime errors. RAISE EXCEPTION TYPE cx_ble_runtime_error EXPORTING previous = lx_conversion. ENDTRY.

Save your draft and test your logic. If it works as expected, then go to the “filter” tab and add a formula. I.e give a name to your logic, as shown below.

Provide%20a%20name%20to%20your%20logic

Provide a name to your logic

Now make a note of your formula name (as you will need it later) and publish your logic!

Depending on your system, go to Manage your Solution or CBC.

Custom Adaptation for Pricing

Go to the SSCUI “Set Alternative Calculation of Condition Amount” (Id 102430). Here we will need to reference our custom logic. To do so, proceed as in the two screenshots below.

Make sure you give a routine number that is not in the SAP namespace (here I used 3000000). Make a note of your chosen number as we will need it later.

Register%20your%20formula

Register your formula

Then go to the detail screen level and here reference the name you had given the formula in the BAdI.

Reference%20your%20BAdI%20Formula%20name

Reference your BAdI Formula name

Add a Pricing Condition

This step is not mandatory, but I wanted to be able to show a % amount for the margin to the sales person entering the sales order rather than the defaulted currency and amount, which could be confusing to the user.

For this, still in configuration go to the SSCUI “Set Condition Types for Pricing” (Id 101120). There is nothing out of the ordinary for this pricing condition, except that it has a calculation type A – Percentage.

Addition%20of%20a%20%25%20based%20price%20condition

Addition of a % based price condition

Adapt the pricing procedure

Before we leave the configuration side, we need to amend the pricing procedure used by our document and add to it our pricing condition as well as our alternate condition-amount determination routine, as shown below. Note that we have referenced our routine (3000000) with our pricing condition – ie our logic will supply the calculated value to the pricing condition.

Amendment%20of%20your%20pricing%20procedure

Amendment of your pricing procedure

Now that we have made the required settings, let’s create a sales order to see how things pan out.

As you can see below, the sales person used his discretionary right to give the customer a discount of 20% (Condition YK07). Unfortunately, this has had the effect of pushing the margin of this sales order item down to 14.926%, which is below the minimum of 30%.  This results in the Status Message “Check the Margin of your Order‘ being advertised.

Order%20item%20below%20acceptable%20margin

Order item below the minimum acceptable margin

The user can still save their order, but it will be considered incomplete.

Document%20incompletion%20flag

Document incompletion notification

As I said early on in this blog, I believe that business rules should be owned and maintained by the business, and what I have shown above, whilst it might do the trick is not flexible, nor scaleable and would require the involvement of a programmer to change your code whenever your business rules changed.

To introduce this idea of business rules, we are introducing a new object to the setup: A Custom Business Object (CBO). This custom object will be used to define the variables that make up the business rules and then an auto-generated UI will be used by the users to enter these rules. For example, you could have rules that are influenced by the sales organisation, the document type, the customer, the product, a promotion, etc…you might also have scenarios where you have free goods (100% discount) where a margin = 0 has to be accepted.

In the example below, we have created a CBO to illustrate how this could work. To keep it simple the only variable we will use is the sales organisation. To that effect, our CBO has been defined with the following fields. The important fields are the sales organisation and the margin. I have also highlighted the technical name of the CBO, as a CDS that we will need later will be generated when we publish our CBO.

Fields%20in%20our%20CBO.

Fields in our CBO

Once we have published our CBO and activated the check box to generate a UI, we can enter the business rule entries.

As you can see we have different margins, depending on the sales organisation of the order item.

Business%20rules%20data

Business rules data

Lastly, we need to revisit our custom logic and replace the hard-coded margin, and instead, read it from our CBO.

Our new logic, now looks like this.

* We are now declaring a new variable ZMARGIN DATA : lv_text TYPE String, ZMARGIN TYPE p LENGTH 4 DECIMALS 2. * Mandatory step for all implementations:
* Move the importing parameters to the changing parameters. MOVE-CORRESPONDING item_amounts TO item_result_amounts. MOVE-CORRESPONDING item_attributes TO item_result_attributes. MOVE-CORRESPONDING item_quantities TO item_result_quantities. MOVE-CORRESPONDING prcg_element_attributes TO prcg_element_result_amounts. * the logic to determine the margin does not change TRY. prcg_element_result_amounts-conditionamount = ( ( 1 - ( item_amounts-costamount / item_amounts-netamount ) ) * 100 ) . prcg_element_result_amounts-conditionratevalue = ( ( 1 - ( item_amounts-costamount / item_amounts-netamount ) ) * 100 ) . * Here out logic changes, we are now querying our custom business object YY1_MARGINALLOWANCERULES
* to obtain the margin amount where the sales organisation of the order item = the CBO entry
* we then put the found result in the variable ZMARGIN
* the CBO and CDS have the same name --> YY1_MARGINALLOWANCERULES SELECT SINGLE margin FROM YY1_MARGINALLOWANCERULES into @ZMARGIN Where salesorganisation = @item_attributes-salesorganization. * The below also changes as we are now comparing the margin to our variable ZMARGIN IF prcg_element_result_amounts-conditionratevalue < ZMARGIN.
* it is possible to raise a message if an issue is identified pricing_message = 'Check the Margin of your Order'.
* indicate a pricing error if required item_result_attributes-pricinghaserror = abap_true. ENDIF. CATCH cx_sy_conversion_error cx_sy_arithmetic_error INTO DATA(lx_conversion). prcg_element_result_amounts-conditioninactivereason = 'X'. prcg_element_result_amounts-conditionamount = 0.
* You should use the statement "RAISE EXCEPTION" only for critical business errors or programming runtime errors. RAISE EXCEPTION TYPE cx_ble_runtime_error EXPORTING previous = lx_conversion. ENDTRY.

Please note that you could also introduce the use of a CBO in the scenario demonstrated by Harshad in his blog.

Whilst this blog could not be used as is, it has hopefully given you some ideas on how you could implement such a margin control mechanism in your sales orders. It has also hopefully demonstrated the flexibility of SAP S/4HANA Cloud, and maybe you will come up with a different method to tackle this issue – if you do please share your knowledge with the community!

Don’t forget to transport (export/import) your extensibility objects!

Feel free to leave your feedback or questions in the comment section below.