Adventures in Groovy – Part 32: Require Explanations When Data Is Outside Acceptable Ranges

Groovy offers creative ways to force data into acceptable ranges.  But, what happens when there is an acceptable reason for abnormal trends, or drivers?  With Groovy, you can not only add limits to data cells, but you can also allow entry outside of those ranges if the user enters an explanation.  With Groovy, you get the best of both worlds!

Use Case

Requiring data within limits is a huge improvement to your application.  It improves the accuracy and ownership of the budget, and reduces fire-drills at the end of your budget cycle.  In this example, we will require a rate to be between -10 and 30 percent.  If a user enters data outside that range, the form will not be able to be saved.

However, if a user enters a comment, Groovy will then allow the form to be saved.  Is this perfect?  Of course not.  A user can still go in and enter a poor explanation, or just enter a space to bypass the validation rule.  I have always felt that no system can make people be responsible, so we will assume that your users are doing what they are instructed and your leadership will hold them accountable.  To show this functionality, view this short video.

The Code

This validation is actually quite simple.  This follows the same rules as other validations and must be “Run Before Save.”  Use Members on Form and Hide Prompts is also suggested.

// Define cell colors for errors
def BackErrColor = 16755370  
// Iterate through the row with the rate and only the months - exclude quarters and totals
operation.grid.dataCellIterator('ProdRev_2_%_Inp','Jan','Feb','Mar','Apr','May','Jun’,'Jul','Aug','Sep','Oct','Nov','Dec').each { 
  // If data is above .3 or lower than -.1 and does NOT have a cell note, throw the error
  if( (it.data > 0.3 || it.data < -0.1) && !it.hasCellNote() ) {
    it.addValidationError(BackErrColor,"Your Margin has to be between -10% and 30%.  If you want enter something outside that range, an explanation is required", false) 
  }
}

Finishing Up

I really hope this inspires you to be creative about using Groovy to add useful user functionality.  As you can see, you can add awesome functionality relatively easily.  If you have any creative solutions you would like to share, please add a comment.




Adventures in Groovy – Part 31: Benefits of Running Logic in Groovy (Bypass Essbase)

Say what? An application where no calculations are performed with the Essbase calculation engine?

Business logic, currency, and every other calculation can be performed within Groovy outside of Essbase. I don’t know how I feel about this yet, but the thought of building this in calculations on an ASO cube is intriguing. The elimination of consolidating BSO cubes and data movements with Smart Pushes is also interesting. Some of my coworkers have built entire applications this way with great success and speed. I wanted to replicate this on a small scale to identify the pros and cons of this methodology, and here is what I found.

In my adventures, I found that performance was similar. Sometimes it was quicker, sometimes a little slower. I did not do large allocations, but I have done account calculations and currency conversion. I have done it on form save as well as globally. Complexity varies. If you are a Groovy expert, it likely seems less complex. If you are an Essbase developer, it might seem more complicated. Code can be reused by developing functions and placing them in scripts, just like Essbase scripts can be embedded into rules.

One thing I did find appealing for a user is that change history is complete! If you have dabbled with this, you will know that the change history only includes edited data by a user. So, calculated accounts don’t show any history. When the calculations are done in Groovy, the results are stored in a grid, and saved to the database. This mimics a user updating the values and reflects in the change history. This, I think, is pretty cool.

Another benefit is that calculated data in a grid is reflective of the inputs. With prompts, or when data isn’t validated, the form shows the results of the calculations before the data is saved in the form. I have been asked by many if it is possible to show the results real time before save (like an adhoc form with formulas built in Excel). This is now possible if you get creative.

Before we jump into the code, it might make sense to understand what dimensions/members are in the POV.

Code Introduction

The following calculation is replicating what an Essbase calculation is performing.

  1. Performs simple math to calculation revenue and costs – mostly simple multiplication and division
  2. Looks up the currency rates for the Entity selected and applies the exchange rate to the account if it is identified as an account that should be converted (ignores statistical accounts, for example)
  3. Converts the account from ProdRev to the representative account in the P&L plan type.
  4. Submits the calculated results to both the ProdRev BSO and ASO (in a real world situation, you may decide to exclude the BSO cube and connect the form and write the data directly to the ASO cube)

As previously mentioned, there are a couple way to do this. This method runs post save and pulls the necessary data from the cube and created two GridBuilders to submit the calculated accounts back to the databases. It would likely be more productive to pull the data from the grid the user entered it, assuming there were no other data points that were needed to calculate all the accounts. This would reduce the performance by one retrieve. This also connects to the BSO cube, and this may not even be in play if all the calculations were executed in Groovy and the forms connected to the ASO cube. So, I welcome all questions and constructive feedback, but understand this is one way to do this and it will be used to demonstrate the functionality. It doesn’t necessarily represent the best way to architect the plan types.

The data entry form is below. Some of the dimensions are hidden in the POV or Page Header. The columns just include the Period dimension. The rows, and this is important to understand because of the way the code loops, contain the vendors and the accounts.

Explaining the Code

On with the show! The first piece includes a couple functions that will be referenced in loops. I find repetitive logic like this placed in function to be more readable, but it certainly isn’t required.

// This function returns the POV member with or without quotes
// This is one operation I want to look into as it may not be required or may be executed better
def getPovMbr(member, boolean quotes = false) {
  if(quotes == true)
    return '"' + operation.grid.pov.find{it.dimName==member}.essbaseMbrName + '"'
  else
    return operation.grid.pov.find{it.dimName==member}.essbaseMbrName
}
// This function returns a 1 if the value is 0.  The primary use of this is currency
// exchange.  In this example, the planners didn't enter 1 for a USD to USD conversion, 
// so somewhere a 1 needed to be assumed.
def checkZero(double dVal=0) {
  if(dVal == 0) {
    return 1
  }
  else {
    return dVal
  }
}
// This returns a true or false as to whether the account has a UDA identifying that 
// account as one that either does or doesn't get converted.
boolean convertCurrency(def account)
{
  Dimension AccountDim = operation.application.getDimension("Account")
  Member AccountMbr = AccountDim.getMember(account)
  def memberProps = AccountMbr.toMap()
  if(memberProps['UDA'].toString().contains('IgnoreCurrencyConversion'))
    return false
  else
    return true
}

The next piece is setting each POV member in a variable. If you have read some of my other posts, there are better ways to do this. For the sake of simplicity and trying to explain other concepts, I have chosen to do it this way.

// Set each POV to its own variable
String sCompany = operation.grid.getCellWithMembers().getMemberName("Company")
String sVersion = operation.grid.getCellWithMembers().getMemberName("Version")
String sScenario = operation.grid.getCellWithMembers().getMemberName("Scenario")
String sYear = operation.grid.getCellWithMembers().getMemberName("Years")
String sMaterialGroup = operation.grid.getCellWithMembers().getMemberName("Material_Group")
String sChannel = operation.grid.getCellWithMembers().getMemberName("Channel")
String sSource = operation.grid.getCellWithMembers().getMemberName("Source")

At this point, the administrative stuff is out of the way. The first thing this example does functional is to get the currency exchange rates for the month of the Entity selected. This would be a little more complex if it were converting income statement and balance sheet accounts because it would need to distinguish the difference so the correct rate (average or ending) was applied.

// Define the map to hold the appropriate rate.  This will be a map that has an entry for each month, with the value equal to 
// the conversion rate - Jan:.878, Feb:.899, ...
def currencyRates = [:]
// Build currency for the company by retrieving the correct POV from the plan type
Cube lookupCube = operation.application.getCube("ProfRev")
DataGridDefinitionBuilder builder = lookupCube.dataGridDefinitionBuilder()
builder.addPov(['Years', 'Scenario', 'Version','Channel','Material_Group','Source','Vendor','Currency','Account'],[[sYear],[sScenario],[sVersion],['No_Channel'],['No_Material_Group'],['Input'],['No_Vendor_Assigned'],['Local'],['End_C_Rate']])
builder.addColumn(['Period'], [ ['ILvl0Descendants("YearTotal")']])
builder.addRow(['Company'], [ [sCompany] ])
DataGridDefinition gridDefinition = builder.build()

// Load the data grid from the lookupCube to the Map
lookupCube.loadGrid(gridDefinition, false).withCloseable { dataGrid ->
  dataGrid.dataCellIterator().each{ rate ->
    currencyRates.put(rate.getMemberName('Period'),checkZero(rate.data))
  }
}

Now that we have a map of the currency rates, the next piece will create the grids that will submit the calculated results back to the cubes. Remember, in this example, the data is being entered to a plan type that is a BSO cube. This will submit the results back to the BSO cube, but also to the corresponding reporting (ASO) cube. Since this is done post save (and we will assume post Smart Push), the data needs to be update in both places. Is this the best and most efficient way to do this? Probably not. Again, take it for what it is – an education on bypassing the Essbase calculations!

// Get list of vendors that are in the rows
def listVendors = operation.grid.rows.headers*.essbaseMbrName.collect {vendor, account -> return vendor}.unique()

// Create a list of calculated members that need to be in the grid
def listAccounts = ["Regular_Cases","Net_Sales","prodRev_Level_1","Cost_of_Sales_without_Samples","prodRev_Level_2","Depletion_Allowance_Manual_Chargeback"]

//prodRev Grid Setup
Cube prodRevCube = operation.application.getCube("prodRev")
Cube rprodRevCube = operation.application.getCube("rprodRev")

DataGridBuilder prodRevGrid = prodRevCube.dataGridBuilder("MM/DD/YYYY")
DataGridBuilder rprodRevGrid = rprodRevCube.dataGridBuilder("MM/DD/YYYY")

prodRevGrid.addPov(sYear,sScenario,sVersion,sChannel,sMaterialGroup,sSource,sCompany)
rprodRevGrid.addPov(sYear,sScenario,sVersion,sChannel,sMaterialGroup,sSource,sCompany,'MTD')
prodRevGrid.addColumn('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec')
rprodRevGrid.addColumn('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec')

The next piece is replicating what would be in a typical Essbase calculation. This is the business logic and currency conversion.

// Define the currencies to be updated and sent back to the database
def lstCurrency = ["Local","USD"]
// loop through the grid the user entered their updates.  This will loop through each vendor, then loop
// through each account for each vendor
listVendors.each{ vendor ->
  listAccounts.each{ account ->
    // Create two variables to hold the calculated results for local and USD
    def sValues = []
    def sValuesUSD = []
    // Create two variables to hold all the monthly calculated results to be added to the grid to submit
    List addcells = new ArrayList()
    List addcellsUSD = new ArrayList() 
    // Run for 12 months - this would likely need to be dynamic for a form that included 
    // the option for forecast and plan
    ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'].each{cMonth ->
    // Calculate all the accounts
    double setValue
      switch (account) {
         case "Net_Sales":
           setValue = operation.grid.getCellWithMembers("Avg_Price/Case_Inp",cMonth.toString(),vendor.toString()).data * operation.grid.getCellWithMembers("Regular_Cases",cMonth.toString(),vendor.toString()).data
           break
         case "prodRev_Level_1":
           double netSales = operation.grid.getCellWithMembers("Avg_Price/Case_Inp",cMonth.toString(),vendor.toString()).data * operation.grid.getCellWithMembers("Regular_Cases",cMonth.toString(),vendor.toString()).data
           setValue = netSales * operation.grid.getCellWithMembers("prodRev_1_%_Inp",cMonth.toString(),vendor.toString()).data
           break
         case "Cost_of_Sales_without_Samples":
           double netSales = operation.grid.getCellWithMembers("Avg_Price/Case_Inp",cMonth.toString(),vendor.toString()).data * operation.grid.getCellWithMembers("Regular_Cases",cMonth.toString(),vendor.toString()).data
           double prodRev1 = netSales * operation.grid.getCellWithMembers("prodRev_1_%_Inp",cMonth.toString(),vendor.toString()).data
           setValue = netSales - prodRev1
           break
         case "prodRev_Level_2":
           double netSales = operation.grid.getCellWithMembers("Avg_Price/Case_Inp",cMonth.toString(),vendor.toString()).data * operation.grid.getCellWithMembers("Regular_Cases",cMonth.toString(),vendor.toString()).data
           setValue = netSales * operation.grid.getCellWithMembers("prodRev_2_%_Inp",cMonth.toString(),vendor.toString()).data
           break
         case "Depletion_Allowance_Manual_Chargeback":
           double netSales = operation.grid.getCellWithMembers("Avg_Price/Case_Inp",cMonth.toString(),vendor.toString()).data * operation.grid.getCellWithMembers("Regular_Cases",cMonth.toString(),vendor.toString()).data
           double prodRev1 = netSales * operation.grid.getCellWithMembers("prodRev_1_%_Inp",cMonth.toString(),vendor.toString()).data
           double prodRev2 = netSales * operation.grid.getCellWithMembers("prodRev_2_%_Inp",cMonth.toString(),vendor.toString()).data
           setValue = netSales - prodRev1
           break
         default:
           setValue = operation.grid.getCellWithMembers(account.toString(),cMonth.toString(),vendor.toString()).data
           break
      }
      // Update the variables that will be used to create the grid rows
      sValues.add(setValue)
      addcells << setValue
      // Convert Currency if account should be converted
      if(convertCurrency(account) == true){
        sValuesUSD.add(currencyRates[cMonth].toString().toDouble() * setValue)
        addcellsUSD << currencyRates[cMonth].toString().toDouble() * setValue
      }
      else
      {
        sValuesUSD.add(setValue)
        addcellsUSD << setValue
      }
    }
    // After all 12 months are traversed calculated, add local and USD to the grid
    prodRevGrid.addRow([account.toString(),vendor.toString(),'Local'],addcells)
    prodRevGrid.addRow([account.toString(),vendor.toString(),'USD'],addcellsUSD)
    rprodRevGrid.addRow([account.toString(),vendor.toString(),'Local'],addcells)
    rprodRevGrid.addRow([account.toString(),vendor.toString(),'USD'],addcellsUSD)
  }
}

The last piece is to submit the grids. The following will write to the log the number of cells accepted and rejected.

// After all vendors and account have been calculated, submit the grid to the respective database
DataGridBuilder.Status status = new DataGridBuilder.Status()
DataGridBuilder.Status rstatus = new DataGridBuilder.Status()
DataGrid grid = prodRevGrid.build(status)
DataGrid rgrid = rprodRevGrid.build(rstatus)
println("Total number of cells accepted: $status.numAcceptedCells")
println("Total number of cells rejected: $status.numRejectedCells")
println("First 100 rejected cells: $status.cellsRejected")
prodRevCube.saveGrid(grid) 
println("Total number of cells accepted: $rstatus.numAcceptedCells")
println("Total number of cells rejected: $rstatus.numRejectedCells")
println("First 100 rejected cells: $rstatus.cellsRejected")
rprodRevCube.saveGrid(rgrid)

So…

Why Do this?

This would add complexity for those who don’t know Groovy.  It is a completely different thought process. But there are benefits.

  • Elimination of the BSO application creates a simpler model.
  • There is no requirement of data synchronization between the BSO and ASO cube.
  • Users will see cell history for all data that was changed, not just the data changed by a user directly.

That is not to say there aren’t drawbacks, because there are.  I am sure if you are experienced, you are already asking questions about clearing data, large allocations, and several other necessary components.

Is It Worth It?

Honestly, I don’t know yet. This is a complete paradigm shift in our world. The thought of calculating everything in Groovy and not having a BSO plan type is just completely foreign to me. The exploration of this option and documenting here surely proves my interest is peaked! Large allocations seem to be slower than BSO Essbase calculations. Everything else seems to be as quick or quicker. I think in the short term, having both gives us the best of both worlds, even with the additional need to synchronize the data between the two. Long term? Who knows. I never thought I would even be entertaining it, so I am not closing my mind to the possibility.

What do you think?

 




Adventures in Groovy – Part 30: Dynamically Identify Actionable Months

Almost every ePBCS application will require the need to run business logic and execute data movements on specific months based on a scenario selected.  Almost every Data Form will require action to be taken dynamically based on the selection of the Scenario.  Assuming a fiscal year of Jan to Dec and Actuals being final through Jun,

  • processes on Actuals will run on Jan through Jun
  • processes on Budget/Plan will run on Jan through Dec
  • processes on Forecast will run on Jul through Dec

Prior to Groovy, this was done in an Essbase calculation by writing if statements based on the scenario selected in the POV and checking if the month was part of a variable range.  It might have looked something like this.

IF((@ISMBR("Plan") AND @ISMBR(&v_BudYear) AND @ISMBR(&v_BudMths)) OR
  (@ISMBR("Forecast") AND @ISMBR(&v_FcstYr1) AND @ISMBR(&v_FcstMths1)))
 ...
ENDIF

Groovy provides a much more efficient way to accomplish this.  Although it may be different based on specific needs, the example below will assume that Forecast and Actuals can be identified based on a variable that is updated monthly identifying the last month of Actuals.

Setting Up The Variables and Run Time Prompts

This application has a substitution variable named v_CurMonth used to identify the current reporting month of Actuals, or the last month Actuals are final.  This will be used in the Run Time Prompt as a default value in several places and this is required.

A Run Time Prompt is also required.  There is nothing unique about the RTP for this use and the application may likely already have one.  This will be a member Type, be connected to the Period dimension, and have a default value of the substitution variable above.  It would look similar to this.

Finally, a variable in the Groovy business rule must be created.  This variable will be set as an override value with the default value equal to the substitution variable created above.  In a Groovy business rule, a variable is instantiated by adding the following line at the top of the rule.  It looks like a comment, but it acts differently when it starts with RTPS:.

/*RTPS: {RTP_Month}*/

After this is added, the Variables tab of the business rule will show all the variable defined.  Set the variable so that it is hidden and is used as an override value.

The Code

At this point, all of the required variables are setup.  The first step is to define the months.  This can be hard coded since this is not something that will change frequently.  If this is added to a script and included in all the business rules it is needed, changing it would be easy if the months did change since it is in one place and shared throughout.

// Hard coded months 
def months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']

Optionally, this can be set dynamically.  Either option, in my opinion, is acceptable.  The result of the following would produce the exact same value for months.

// Dynamically get the list of months based on the application's fiscal year 
Cube cube = operation.application.getCube("GP") 
Dimension productDim = operation.application.getDimension("Period", cube) 
def months = productDim.getEvaluatedMembers("ILvl0Descendants(YearTotal)", cube)

The next piece of this rule is to get the month from the variable.  This will be the key to identify which months are used in calculations that run on Actuals and Forecast.

// Get the month from the hidden RTP 
def actMonth = rtps.RTP_Month.toString()

The last step is to get the appropriate months based on the Scenario selected.  If Actuals is selected, all the elements in the months list from the first to the index of the month in the RTP are selected.  If Plan is selected, all the months are included.  This can be altered based on needs.  Some companies do mid year plans, for example, and may need to only execute the logic on the last 6 months.  If Forecast is selected, all the elements in the months list AFTER the RTP value to the end of the list are selected.

// Create dynamic list of months based on user selected Scenario
def useMonths = []
if(operation.grid.pov.find{it.dimName =='Scenario'}.essbaseMbrName.toString().toLowerCase().contains('plan')){
  // This will create a list of all months - assuming the the plan is for 12 months
  useMonths = months 
  }
if(operation.grid.pov.find{it.dimName =='Scenario'}.essbaseMbrName.toString().toLowerCase().contains('forecast')){
  // This will create a list all months after the actMonth variable - or the out months
  useMonths = months[months.findIndexOf{it == actMonth}+1..-1] 
  }
else{
  // This will create a list of all actual months
  useMonths = months[0..months.findIndexOf{it == actMonth}] 
  }

// Create delimited list for methods that don't require a List object
def useMonthsString = """\"${useMonths.join('", "')}\""""

Using useMonths and useMonthsString

The hard part is over.  Using the variable is the easy part.  It can be used anywhere the months needs to be change from all months to the identified months.

In a DataMap/SmartPush

operation.grid.getSmartPush("GP_SmartPush").execute(["Period":useMonthsString])

Same concept but overriding the entire POV (something more realistic in a real world example)

// This creates a variable for the povMap that includes all members in the existing POV
// This is used as a possible scenario but is not directly related to the content of this post
def povMap = operation.grid.pov.each{povMbrs["pov$it.dimName"] = "$it.essbaseMbrName"}
// Add the months to a map for the Period dimension
povMap['Period'] = useMonthsString
operation.grid.getSmartPush("GP_SmartPush").execute(povMap)

In a GridBuilder

builder.addColumn(['Period','Currency'], [ useMonths,['Local','USD'] ])

In an Essbase FIX statement

essCalc << """
FIX($useMonthsString)
"""

Finally

This could be added to a script and included in all the Business Rules that require such a variable.  It creates a variable once that can be used in many classes.  It improve maintainability and reduces replicating the same logic for each class.  If you have any suggestions, or have something you would like to share, please post a comment.

 




Adventures in Groovy – Part 29: Troubleshooting Data Movement With GridBuilder

One of the challenges working with grids is validating the results.  As with an Essbase calculation, Smart View reports are developed to validate results.  The same happens when calculations, or data movement, is executed in Groovy via grids.  When the results in Smart View aren’t accurate, where do you go?

Make It Simple

By now you have probably used the grid iterator to iterate through cells to validate data, write values to the log, and check to see if the cells have been edited.  The same can be done with GridBuilders.  All aspects of the grid can be logged.  If done correctly, this can be copied directly from the log and pasted into excel to accomplish 2 things.  One, you have a report in Excel.  Two, you have a Smart View ready ad-hoc report that can be refreshed.

Send The Grid To The Log

Grids can be different so this may be a start for you to construct this validation.  This example has 2 column headers.  The rest should be very close and likely completely reusable.  To break this down, we have a source grid we are pulling data from to create a grid to send to another plan type.  Basically, this loops through the members in the POV and replicates rows for the number of columns in the grid.  This is repatative, but it will provide a retrievable Smart View.

// Loop through the POV to create column headers for each
povmbrs*.essbaseMbrName.each{ POV ->
  // Add a blank column for the row members
  print ','
  // Loop through the columns and repeat the POV member for each of the columns
  colmbrs[0]*.essbaseMbrName.size().times{
    print POV + ','
  } 
  // Print a line return for the next POV member
  println ''
}
//Print a blank column and then each of the column headers for both headers
println ',' + colmbrs[0]*.essbaseMbrName.join(',')
println ',' + colmbrs[1]*.essbaseMbrName.join(',')

At this point, the log will show the column headers.  The following is created while looking through the source grid and produces the row header and the respective data for each of the column headers.

...{
  sValues.add(it.crossDimCell(cMonth.toString(),cCurrency.toString()).data) 
  addcells << it.crossDimCell(cMonth.toString(),cCurrency.toString()).data
}
// After the variables are created with the numeric data to be used when creating the rows, the row is created
finGrid.addRow([acctMap.get(it.getMemberName('Account'))],addcells)
// Print to the log exactly what is being used to create the grid
println "${it.getMemberName('Account')}" + "," + sValues.join(",")

At this point, the entire Smart View is created in the log and can be copied and pasted to Excel.  The log will look something like this.

When pasted into a text editor that doesn’t wrap, it looks a little more palatable!

Create An Excel Report / Smart View Ad-hoc

At this point, you are ready to get this into Excel.  Select the part of the log related to the validation and copy it to Excel. I normally wrap the Groovy code above in a few println statements so it is easier to identify what is related to this validation effort and what isn’t.

println '*************** BEGIN VALIDATION ***************'
// CODE ABOVE
println '*************** END VALIDATION ***************'

If it doesn’t parse by comma automatically, go to the Data ribbon and select the option to convert the selection to Text to Columns and select comma.  This will parse it to what is required.  This may not need to be done depending on a few things, which won’t be discussed here.  The result of the example above looks like this.

Finishing

Now there is an easy viewable report of what is being used to create the grid.  If this data is incorrect, move backwards in the process to the source grid and fix it.  This should provide all the information to do that.  Is the POV correct?  For me, this is normally the issue – I am pulling the wrong POV.  Once the source grid POV is changed, go through the process again and you should see better results.




Adventures in Groovy – Part 28: The DataGridBuilders

Gridbuilders are the link to moving data from BSO to ASO, and ASO to ASO.  Gridbuilders can also be used to save calculated results that are executed directly in Groovy.  If it jumped into your head, yes, you can completely bypass the Essbase calculation engine.  I have a coworker that has done exactly that – everything in Groovy and had all the forms connected directly to ASO!  There is an example of this in my presentation at KScope, Last Minute ODTUG Planning Sourvenirs.  Before we get too far ahead, back to the basics.

Welcome to Grids

Creating grids is the same as creating a Smart View report.  They have POVs, column and row headers, and of course, the data grid.

Gridbuilders come in two forms.

  1. The DataGridDefinitionBuilder is used to read data.
  2. The DataGridBuilder is used to submit data.

The later can use functions like ILvl0Descendants and can suppress missing rows.  Both can return status information about the volume of data the grid retrieves/submits.


The DataGridDefinitionBuilder

The first grid discussed will be the reader grid.  This grid can be used in processes to get currency tables, read data to be used in calculations, or make decisions on the logic to execute.  It can also be used to be the source of data moved to another location or plan type.  Try not to make this too complicated.  It really is exactly the same as a Smart View adhoc retrieve.

The first step is to create a connection to the cube, or plan type, and initiate the GridDefinitionBuilder object.

// Setup the connection to the plan type
Cube cube = operation.application.getCube("Plan1")
//Create the grid definition builder
DataGridDefinitionBuilder builder = cube.dataGridDefinitionBuilder

Now that the object is created and connected to a source, the next step is to setup the grid.  This can be done in any order that makes sense to you because it isn’t used until the grid is built.  Personally, I start with the POV.  If you understand collections, particularly lists, this will seem pretty simple.  If you don’t know what Groovy lists are, read Part 27: Understanding Collections.  Creating the POV requires two parameters.  The first is the list of dimensions.  The second is the list of members in the corresponding dimensions.  The first parameter is one list (a comma delimited list inside brackets).  The second parameter is a list of lists.  I will explain why a little later.  Each sub-list has only one member, but it is still a list.  The format of this is [[member1],[member2]].  So, it is a comma delimited list of embedded lists.  It is important to understand this.  If you are just replicating this example, you are doing yourself an injustice because you will struggle when you start throwing in variables and trying to create efficient ways to create these based off of a POV of the grid the user interacts with.

// Add the POV – Dimensions and a collection of the members in those dimensions
builder.addPov(['Years', 'Scenario', 'Currency', 'Period', 'Version', 'Entity’], [['FY16'], ['Current'], ['Local'], ['BegBalance'], ['BU Version_1'], ['No Entity’]])

The next thing I do is create the columns.  Again, understanding lists is critical to become efficient with these classes and methods.  I will sound like a broken record, but I wish I had somebody tell me this 6 months ago.  The column requires the same two parameters in the same formats.  The first is a list of the dimensions.  The second is a list of the lists of members associated to those dimensions.  The difference from the POV is that multiple members can be added to each dimension.  It might be more clear why the second parameter is a list of lists, now.  If it wasn’t, distinguishing which members related to which dimensions would be a jumbled mess with just a comma delimited list of members.  Just like with Smart View, columns can have multiple headers.

// Add the columns – 2 parameters, collection of dimensions and 
// collection of collections of members in those dimensions
builder.addColumn(['Years', 'Account’],
[['Children(Years)'],['Min Salary','Max Salary','Min Bonus','Max Bonus']])

The rows are identical to the columns, so I won’t repeat the narrative above.

// Add rows no data - in this class as it is retrieving data
builder.addRow(['Grades'], [ ['ILvl0Descendants("Grades")']])

Before we proceed, the addColumn and addRow methods can be replicated multiple times (you know this as segments in developing data forms).

Now that the grid is constructed, execute the build method to create the object with all its properties.

// Build the grid
DataGridDefinition gridDefinition = builder.build()

Finally, load the grid, which is the equivalent of refreshing a Smart View template.

// Load a data grid from the specified grid definition and cube
DataGrid dataGrid = cube.loadGrid(gridDefinition, false)

At this point, the grid acts just like a data form.  All the methods and classes are available just as they are on forms.  These grids can be iterated, just like web forms.

The DataGridBuilder

The DataGridBuilder is the class used to submit data to the database.  The construction and use is similar to the DefinitionBuilder, except the use of functions is not relevant, and rather than loading data to the grid, the data is saved to the database.

// Setup the connection to the plan type
Cube cube = operation.application.getCube("Plan1") 
DataGridBuilder builder = cube.dataGridBuilder("MM/DD/YYYY")

What we are creating below would look like this if it was done in Smart View.

Setting up the POV is similar to above, except this time there is no requirement to pass a list.  Also, the identification of the dimensions is not necessary.  All that is needed is a comma delimited list of members.

// Setup the grid POV, Rows, and Columns
builder.addPov('Salary', 'Current', 'Local', 'BU Version_1’)

Setting up the columns is also slightly different.  The dimensions are again missing and the members are all that is required.  Also, there is a line for each dimension rather than passing it in lists as multiple parameters.  This is exactly the same as setting up a Smart View template.  Each column has to be set.  The example below has 3 columns made up of the year and period dimensions.

builder.addColumn('FY16', 'FY16', 'FY16’)
builder.addColumn('Jan', 'Feb', 'Mar’)

The rows are similar in that there is a slight difference from the DefinitionBuilder, but it does require two parameters.  Each is a list.  The first includes the members of the dimensions in the row headers.  The second are the corresponding values to the column headers above.  The following adds the members from the department and employee, and sets the values for Jan through March of FY16.

// Add rows to the grid
builder.addRow(['Department 1', 'Employee 1'], [30000, 30000, 30000]) 
builder.addRow(['Department 5', 'Employee 2'], [40000, 40000, 40000])
builder.addRow(['Department 1', 'Employee 3'], [30000, 30000, 30000])

Once the grid is laid out, and this can be done before the grid is defined, a status object is created to hold the results of the submission of data.

// Create a status class to hold the results
DataGridBuilder.Status status = new DataGridBuilder.Status()

At this point, the grid can be built.  The difference with this object is that the status object is passed.  That status object can be used to evaluate if records will be submitted and if any will be rejected.

// Build the grid – basically a refresh/retrieve
DataGrid grid = builder.build(status)
println("Total number of cells accepted: status.numAcceptedCells")
println("Total number of cells rejected: status.numRejectedCells")
println("First 100 rejected cells: status.cellsRejected")

Finally, save the grid to submit the data to the database.

// Save the data to the cube
cube.saveGrid(grid)

Finish Up

This all seems pretty simple.  I can’t say it enough.  Spend some time understanding Groovy collections, especially how to manipulate them.  I guarantee the time you spend will be time well spent.  I will be posting another article on how to diagnose issues and troubleshoot grid errors, whether it be grids that won’t return results, or grids that won’t save data.  Hope this was valuable!

 




Adventures in Groovy – Part 27: Understanding Collections

Because so many methods in PBCS require parameters that are maps and lists, it is very important to your productivity and effectiveness to understand, use, and be able to manipulate collections.  Collections include lists, maps, and ranges.  These are based on Java collection types and include a ton of useful methods.  This article will cover lists and maps.

  • Lists are objects that are embedded in brackets and separated by commas, like [Jan, Feb, Mar].
  • Maps are also known as associative arrays and are similar to lists.  Each element is separated by a colon, like  [Revenue:678979, Expense:34387].

Not to jump too far ahead, but these objects aren’t limited to holding strings.  They can hold numeric values, lists inside of lists, maps inside of lists, or really almost any combination of these objects.

Time Well Spent

There is a lot of information in this post.  Before you page down, or read a little and get bored, I highly recommend you invest the time and understand the content.  The feedback from everybody that I gave this information to prior to this post said it made a huge impact and really cleared up a lot of questions they had.

A Little About Lists and Maps And Why Understanding It Is Important

Lists and Maps are used in a number of places that are frequently used in Groovy calculations.  Gridbuilder and DataMap objects are dependent on these objects.  You may be building the POV by embedding strings into what is actually a list without even knowing it. I have done this in previous examples to keep it simple and explainable.

String sYear = '"' + operation.grid.pov.find{it.dimName =='Years'}.essbaseMbrName + '"'
String sScenario = '"' + operation.grid.pov.find{it.dimName =='Scenario'}.essbaseMbrName + '"'
String sCompany = operation.grid.getCellWithMembers().getMemberName("Company")
String sChannel = '"' + operation.grid.pov.find{it.dimName =='Channel'}.essbaseMbrName + '"'
String sMaterialGroup = '"' + operation.grid.pov.find{it.dimName =='Material_Group'}.essbaseMbrName + '"'

operation.application.getDataMap("[name of map]").execute(["Company":sCompany,"Channel":sChannel,"Material_Group":sMaterialGroup,"Vendor":sVendor,"Scenario":sScenario,"Version":"OEP_Working","Years":sYear,"Source":"NBF_Source","Currency":"Local,USD","Account":accountPush],true)

builder.addPov(['Years', 'Scenario', 'Version', 'Company','Channel','Material_Group','Source','Vendor','View'], [sYear.tokenize(), sScenario.tokenize(), ['OEP_Working'], [sCompany],['Tot_Channel'],['Total_Material_Group'],['Tot_Source'],['Tot_Vendor'],['MTD']])

Since the POV and DataMap include lists and maps, building them can also be done by pulling the grid POV, which is a map.   Lists can be extracted from the map.  There are some additional members that are required that are not in the Web Form, so a few dimensions have to be added.

Hopefully the following example will emphasize the importance of understanding these objects and the methods associated to them.  The above is not a ton of work, but replicating this hundreds of times and having the foresight to think about how functions can be reused  highlights why understanding lists, and understanding Groovy, will help you provide solutions that are more manageable to maintain.  The following replicates what was above in a more effective and readable way, in my opinion.  It may seem more complex, but give it a chance.  I think you will find it easier once you understand it, and less work to develop Groovy calculations.

Example Setup

Assume the user changes Units for Jan, Feb, and Mar for NBF1 in the Data Form below.  Net Sales is impacted as it is calculated.  The business rule that runs calculates currency, so that will also have to be included in the DataMap and GridBuilder.

The POV for the form looks like this.

Replicating The DataMap Using A Groovy Map Object

Executing a Data Map uses a Groovy map built from the Data Form POV.  Currency has to be altered.  The edited rows and columns are appended.  For a complete explanation of how to get the edited cells, read Part 3: Acting On Edited Cells.

// Create a map from the data form POV
def povMap = [:]
operation.grid.pov.each{povMap[$it.dimName] = $it.essbaseMbrName}
// Update Currency to include Local AND USD 
povMap['Currency'] = 'Local,USD' 
// Add the vendor and accounts that have been changed 
povMap2.put('Period', sPeriods)
povMap2.put('Account', accountPush)
povMap2.put ('Vendor', sVendor)
// Execute the data map with all the dimensional overrides
operation.application.getDataMap("[name of generic map]").execute(povMap,true)

Let’s walk through the results of the script.  The povMap when created from the Data Form starts with

  • [Version:OEP_Working,
  • Source:NBF_Source,
  • Currency:Local,
  • Company:BILB,
  • Years:FY18,
  • Scenario:OEP_Plan,
  • Material_Group:mI03,
  • Channel:c01]

Once this map is created, there are some edits made.

  1. Currency is changed from Local to Local, USD.
  2. Periods is added as Jan, Feb, Mar.
  3. Account is added as Units, Net_Sales.
  4. Vendors is added as NBF1.

This method requires fewer lines, is much easier to repeat, is easier to read, and can be reused for other functions.

Replicating The GridBuilder POV Using A Groovy Map Object

Assume the data is now going to be replicated to another cube using a GridBuilder.  In the example available in Part 21: Real Time Data Movement (Getting REALLY Groovy), the data is moved to a P&L cube at a summary level and doesn’t include the channel, material group, and vendor dimensions.  A new Groovy map can be created (which we will do here), or we can alter the existing one above.  The source grid would then need to pull data at the total of all these dimensions.  Currency and Period need to be removed as they are in the columns.  Account will be removed and placed in the rows.

def povGridMap = [:]
operation.grid.pov.each{povGridMap[it.dimName] = it.essbaseMbrName}

// Remove dimensions that will be in the rows and columns
povGridMap.remove('Currency')

// Add View dimiension
povGridMap['View'] = 'MTD'

// Change the dimensions that need to pull totals
povGridMap['Channel'] = 'Total_Channel'
povGridMap['Material_Group'] = 'Total_Material_Group' 
povGridMap['Vendor'] = 'Total_Vendor' // Another way to add a new element to the map

builder.addPov(povGridMap.keySet() as List, povGridMap.values().collect{[it]} as List)
// the reason this requires the as List notation is because the addPOV method requires 
// the parameter to be a list type
// From the Java Docs:
// void setPov(java.util.List<java.lang.String> dimensions,
//             java.util.List<java.util.List<java.lang.String>> members)

Let’s walk through the results of the script.  The povMap when created from the Data Form starts with

  • [Version:OEP_Working,
  • Source:NBF_Source,
  • Currency:Local,
  • Company:BILB,
  • Years:FY18,
  • Scenario:OEP_Plan,
  • Material_Group:mI03,
  • Channel:c01]

The edits are then made and the result is

  • [Version:OEP_Working,
  • Source:NBF_Source,
  • Company:BILB,
  • Years:FY18,
  • Scenario:OEP_Plan,
  • Material_Group:Total_Material_Group,
  • Channel:Total_Channel,
  • Vendor:Total_Vendor]

Moving On

Hopefully the difference in the two strategies inspires you to learn a little more about the objects and methods below.

The remainder of this article shows examples that will be extremely useful, and some that will just give you some insight to what is possible.

Lists

Creating And Editing Lists

Lists can be created and referenced a number of ways.  Empty lists can be created.

def emptyList = []

Lists can be created by adding the elements.

Def q1List = [‘Jan’,’Feb’,’Mar’]

Lists can be altered by adding new elements.

def q2List = [‘Apr’]
q2List << (‘May’)
q2List.add(‘Jun’)

or

q2List.addAll(['Apr', 'May', 'Jun'])

They can even be added at specific places.

def q2List = ['Apr']
q2List << ('Jun')
q2List.put(1,'May') // which places it after Apr and before Jun

Elements can be edited based on location.

q2List.set(1,'May') // second item
q2List[-2]'May' // go back 2 from end
q2List[1]'May' / second item

Elements can be removed from lists.

q2List << (‘Jul’)
q2List.remove('Jul')

or

q2List.removeIf{it == 'Jul'}

or

 ['a','b','c','b','b'] - ['b','c'] // result is ['a']
Iterating Lists

Iterating on elements of a list is usually done by using one of the following.

  • each
  • eachWithIndex (same as each but include the index of the element)
['Jan','Feb','Mar'].each { month ->
    println "Item: $month"
}
// With the index of the element
['Jan','Feb','Mar'].eachWithIndex { month, index ->
    println "Item: $month is index $index"
}

The list can be altered while iterating, too.  This doesn’t update the list.  It only produces a new list.

println [1, 2, 3].collect { it * 2 }  //would produce [2, 4, 6]
Manipulating lists
Filtering and searching

There are tons of filtering and searching options.  The following are examples, but it isn’t even close to an exhaustive list.  Hopefully it will inspire some thought as to how you can use these methods.

// find 1st element matching criteria
[1, 2, 3].find { it > 1 } // results in 2

// find all elements matching criteria
[1, 2, 3].findAll { it > 1 } // results in [2, 3]

// find index of 1st element matching criteria
['a', 'b', 'c', 'd', 'e'].findIndexOf {it in ['c', 'e', 'g']} // results in 2

['a', 'b', 'c', 'd', 'c'].indexOf('c') // index returned (2)
['a', 'b', 'c', 'd', 'c'].indexOf('z') // index -1 means value not in list
['a', 'b', 'c', 'd', 'c'].lastIndexOf('c') // 4 is returned

[1, 2, 3].every { it < 5 }               // returns true if all elements match the predicate

![1, 2, 3].every { it < 3 }

[1, 2, 3].any { it > 2 }                 // returns true if any element matches the predicate

![1, 2, 3].any { it > 3 }

[1, 2, 3, 4, 5, 6].sum() == 21                // sum anything with a plus() method
['a', 'b', 'c', 'd', 'e'].sum() // 'abcde'
[['a', 'b'], ['c', 'd']].sum() // ['a', 'b', 'c', 'd']

// an initial value can be provided
[].sum(1000) //  1000

[1, 2, 3].sum(1000) //  1006

[1, 2, 3].join('-') //  '1-2-3'

def list = [9, 4, 2, 10, 5]
list.max() // 10
list.min() // 2

// we can also compare single characters, as anything comparable
assert ['x', 'y', 'a', 'z'].min() // 'a'

// we can use a closure to specify the sorting behavior
def list2 = ['abc', 'z', 'xyzuvw', 'Hello', '321']
list2.max { it.size() } //  'xyzuvw'
list2.min { it.size() } //  'z'

['a','b','c'].contains('a')      // true

[1,3,4].containsAll([1,4])       // true

[1,2,3,3,3,3,4,5].count(3)  // 4
Sorting

Groovy offers a variety of options to sort lists, from using closures to comparators.

[6, 3, 9, 2, 7, 1, 5].sort() // [1, 2, 3, 5, 6, 7, 9]
def list = ['abc', 'z', 'xyzuvw', 'Hello', '321']
list.sort {it.size()} // ['z', 'abc', '321', 'Hello', 'xyzuvw']

def list2 = [7, 4, -6, -1, 11, 2, 3, -9, 5, -13]

Maps

Literals

In Groovy, maps (also known as associative arrays) can be created using the map literal syntax: [:].  Maps are use to create associations between two or more elements.  They can be used to lookup values like currency rates, map accounts from one plan type to another, and a host of other things.  Many of the API methods require maps to be passed.

def map = [name: 'Gromit', likes: 'cheese', id: 1234]
map.get('name')   //  'Gromit'
map.get('id')   // 1234
map['name']   // 'Gromit'
map['id'] == 1234

def emptyMap = [:]
emptyMap.size()   // 0
emptyMap.put("foo", 5)
emptyMap.size()   // 1
emptyMap.get("foo")   // 5
Map Notation

Maps also act like joins so you can use the property notation to get/set items inside the Map as long as the keys are strings which are valid Groovy identifiers:

def map = [name: 'Gromit', likes: 'cheese', id: 1234]
map.name   // 'Gromit'  -    can be used instead of map.get('name')
map.id   // 1234

def emptyMap = [:]
emptyMap.size()   // 0
emptyMap.foo = 5
emptyMap.size()   // 1
emptyMap.foo   // 5
Iterating Maps

Maps makes use of the each and eachWithIndex methods to itarate through maps, just like Lists.

def map = [
        Bob  : 42,
        Alice: 54,
        Max  : 33
]

// Loop through each item
map.each { entry ->
    println "Name: $entry.key Age: $entry.value"
}

// add an index
map.eachWithIndex { entry, i ->
    println "$i - Name: $entry.key Age: $entry.value"
}

// Alternatively you can use key and value directly
map.each { key, value ->
    println "Name: $key Age: $value"
}

// Key, value and i as the index in the map
map.eachWithIndex { key, value, i ->
    println "$i - Name: $key Age: $value"

Manipulating Maps

Adding/Removing Elements

Adding an element to a map can be done either using the put method, the subscript operator or using putAll.

def defaults = [1: 'a', 2: 'b', 3: 'c', 4: 'd']
def overrides = [2: 'z', 5: 'x', 13: 'x']

def result = new LinkedHashMap(defaults)
result.put(15, 't')
result[17] = 'u'
result.putAll(overrides) // [1: 'a', 2: 'z', 3: 'c', 4: 'd', 5: 'x', 13: 'x', 15: 't', 17: 'u']

Removing all the elements of a map can be done by calling the clear method:

def m = [1:'a', 2:'b']
println m.get(1) //  'a'
m.clear()
Filtering and searching

It is useful to search and filter maps, and here are some examples of this functionality.

def people = [
    1: [name:'Bob', age: 32, gender: 'M'],
    2: [name:'Johnny', age: 36, gender: 'M'],
    3: [name:'Claire', age: 21, gender: 'F'],
    4: [name:'Amy', age: 54, gender:'F']
]

def bob = people.find { it.value.name == 'Bob' } // find a single entry
def females = people.findAll { it.value.gender == 'F' }

// both return entries, but you can use collect to retrieve the ages for example
def ageOfBob = bob.value.age
def agesOfFemales = females.collect {
    it.value.age
}
// ageOfBob == 32, agesOfFemales == [21,54]

def agesOfMales = people.findAll { id, person -> person.gender == 'M'}.collect { id, person ->
    person.age
}
// agesOfMales == [32, 36]

// `any` returns true if any entry matches the predicate
people.any { id, person -> person.age == 54 }

Conclusion

There is a lot of information and examples here.  What I hope you walk away with is an understanding of what a list and a map is, most importantly.  Understanding how to use these in the API is really important, and manipulating them will save you a ton of time and unnecessary code.




Adventures in Groovy – Part 26: Is the POV a level 0 member?

One of the more surprisingly useful things that can be done in a Groovy calculation is querying metadata. This was discussed in Part 11: Accessing Metadata Properties, but I recently had a situation where users could load data at parent levels and have it allocated down to the bottom of the hierarchies.  Knowing if users selected a parent member in the hierarchy was critical because the business process was different based on whether it was a parent or a level 0 member.  This can obviously be checked in a business rule, but I had the need to alter several functions in the process if the selection was a parent.  Smart Pushes and data synchronizations, as well as the business rules that were executed, were dependent on the level of the hierarchy selected.  This is possible with Groovy.

The Code Explained

Using a combination of methods, the children of any member can be returned in a list.  That list can be used for all kinds of things.  This focuses on getting the size of the list to identify if it has children.

// Normally getting the POV Selected, but this is hard coded for the example
def povProduct = '"' + "Tents" + '"' 
// Setup a connection to the cube
Cube cube = operation.application.getCube("GP")
// Create a dimension object
Dimension productDim = operation.application.getDimension("Product", cube)
// Create a member list - ILev0 of the member passed
def MemberList = productDim.getEvaluatedMembers("ILvl0Descendants($povProduct)", cube) 

println MemberList
println MemberList.size() == 1 // will print true or false

The MemberList variable now holds a list of all the members (level 0 members below tents), including the member passed in the function.  This will always have at least one item in the list, assuming the member exists in the dimension.  If there is more than 1, we know it has children, and therefore, is not a level 0 member.

if(MemberList.size() == 1)
  {
  //actions taken if the POV is a level 0 selection
  }
  else
  {
  //actions taken if the POV is NOT a level 0 selection
  }

Summary

In applications where top down planning is needed, different logic runs when data is entered at a parent verses when it is entered at a leaf (lev0) member.  This can be handled in an Essbase calculation using @ISLEV0, but with Groovy, the fix statement can be altered so that unnecessary logic isn’t executed and data pushes are limited to only what is impacted.

Do you have an idea of how you might benefit from the results of metadata queries?  Come up with your own and share with the group by commenting below.




Adventures in Groovy – Part 25: Groovy Functions

Building on the previous post and in the spirit of reusing code, this will expand on the concept by walking through the creation and use of functions in Groovy calculations.  Functions have huge value in reducing your Groovy calculations and streamlining development. 

Functions allow access to processes that either do some action(s) or return a value to be used.  There are hundreds of cases I can think of where this is valuable.  Here are a few to get you started.

  1. Getting the form POV, or a dimension in the POV
  2. Creating and executing data maps (automatically using the form POV, for example)
  3. Identifying if the account should be converted to a different currency (headcount will never be converted)
  4. Logging repetative messages to the job console

Share your ideas with the community by posting a comment.  I am sure your thoughts will be valuable to the community!

These functions can be kept in a script that is reusable as discussed in Part 24.  Or, they can be created in individual Groovy calculations if they are unique to a specific calculation.

The Anatomy Of A Function

Functions can be dissected into three pieces.

  1. A name (required)
  2. The parameters (not required)
  3. The logic (required)

The construction is below.

def name(parameter1, parameter2, ...)
{
  logic
}

The name is simple.  This is a reference to the function and the string you use to call it.  I would suggest making it meaningful, but not terribly long.  Typing long names gets annoying after a while!  Also, come up with a consistent case strategy.  I normally capitalize the first letter in all but the first word (getSomethingGood).

The function can have parameters.  Unless it is just doing some generic action, it will need something.  It can be a numeric value, a string, a boolean value, or really any other type of object, like collections.  Parameters are separated by commas and can have default values.

The logic is what is inside the function and the whole purpose to have a function.  Functions can return values, execute some action, or both.

Function Examples

Getting the POV

A lot of functions need members from the POV.  Sometimes surrounding them with quotes causes problems in the PBCS functions so this gives an option to include or exclude them.  This also will return the member as a list for functions that require a list, like data maps.

The second two parameters are not required unless quotes or a list is needed as the return value.

def getPovMbr(dimension, boolean quotes = false, boolean returnAsList = false) {
  if(returnAsList == true)
    // tokenize will split the value based on a delimiter and convert the string to 
    // a collection.  Since this is one value and we just need the one value converted
    // to a collection, I used ### as the delimiter since it will never occur in any
    // dimension name.
    return dimension.tokenize('###')
  else if(quotes == true)
    return '"' + operation.grid.pov.find{it.dimName==dimension}.essbaseMbrName + '"'
  else
    return operation.grid.pov.find{it.dimName==dimension}.essbaseMbrName
}

Assuming the Plan is selected in the POV,

  • getPovMbr(“Scenario”) will return OEP_Plan
  • getPovMbr(“Scenario”,true) will return  “OEP_Plan”
  • getPovMbr(“Scenario”,false,true) will return [OEP_Plan]
Print To The Log

Monitoring processing times of actions inside a calculation is helpful to diagnose issues and manage performance.  This function will accept a message and return the duration from the previous execution of the function.

def startTime = currentTimeMillis()
def procTime = currentTimeMillis()
def elapsed=(currentTimeMillis()-startTime)/1000

def printPerformance(message, boolean printTotalTime = false)
{
  // Gets the elapsed time
  elapsed=(currentTimeMillis()-procTime)/1000
  // Sets the new start time
  procTime = currentTimeMillis()
  // Print to log
  println "****************************************************"
  println "$message = $elapsed secs"
  if (printTotalTime = true)
    println "Total Time = " + (currentTimeMillis()-startTime)/1000 + " secs"
  println "***************************************
}

printPerformance(“The Data Map processed”,true) would write the following message in the log.

****************************************************
The Data Map Processed = .204 secs
Total Time = 1.44 secs
****************************************************

This can obviously be customized to whatever you want to show.  As it is something used often, having a function saves a lot of time, reduces the non-business related code in the rule, and makes reading the business rule more digestible.

Rather than repeat all this,

time elapsed=(currentTimeMillis()-procTime)/1000
procTime = currentTimeMillis()
println “****************************************************”
println “$message = $elapsed secs”
println “Total Time = ” + (currentTimeMillis()-startTime)/1000 + ” secs”
println “***************************************

you simply call the function.

printPerformance(“The Data Map processed”,true)

Convert Account To USD

Groovy calculations don’t need to run Essbase calculations to execute business logic.  The logic can be executed in Groovy.  For a presentation in Orlando, I wanted to prove this.  I replicated a calculation that calculates revenue, gross profit, and margins.  It also needs to produce converted currencies.  As you know, accounts like units, headcount, and rates don’t get converted even if the local currency is something other than USD.  In the example I showed at KScope, a gridbuilder was used and every account needed to be identified as an account that would be converted, or not converted.  The following function returns a true/false for an account based on whether that account has a UDA of IgnoreCurrencyConversion.

boolean convertCurrency(def account)
{
  Dimension AccountDim = operation.application.getDimension("Account")
  Member AccountMbr = AccountDim.getMember(account)
  def memberProps = AccountMbr.toMap()
  if(memberProps['UDA'].toString().contains('IgnoreCurrencyConversion'))
    return false
  else 
    return true
}

convertCurrency(“Units”) would return a false
convertCurrency(“Revenue”) would return a true

In the code, as it is looping through the accounts, it applies the conversion only if it needs to.

if(convertCurrency(account)){
  sValuesUSD.add(currencyRates[cMonth].toString().toDouble() * setValue)
  addcellsUSD << currencyRates[cMonth].toString().toDouble() * setValue
}
else
{
  sValuesUSD.add(setValue)
  addcellsUSD << setValue
}

The full example of this is near the end of the ePBCS Gridbuilder Deep Dive – Last Minute KScope Souvenirs in my Kscope Wrap Up.

Conclusion

There are significant benefits to functions.  As your growth in this space grows, you will likely develop more of these function and reuse them.  Got an idea?  Share it by posting a comment!




Adventures in Groovy – Part 24: Don’t Be Stingy With Reusable Code

Now that you are knee deep in Groovy, help yourself out and reuse common code.  The more you learn, the more efficient you will be and will laugh at some of your initial attempts at your Groovy calculations.  If you are like me, you get excited about all the possibilities and learn as you go.  When you find better ways to do something, or even preferable ways, you end up with an application with inconsistent representations of the same logic.  You are too busy to go back and update all the snippets you have improved, so it stays in a somewhat messy state.

Do yourself a favor and start reusing some of the things you need in every script.  When you start doing more in Groovy calculations, the more you can replicate the better.  Making some of the code business rule agnostic will make you more productive and will create a consistent approach to calculation strategy.  An example of this would be variables for the dimensions in the POV.  Or, maybe a common map used to push data from WFP to the P&L.  Regardless of the use, as soon as you see things that will be duplicated, put them in a script.  Scripts can be embedded in Groovy calculations just like regular Business Rules.

Header Script

This is an example of something that I find useful for every Groovy calculation I am writing.  It accomplished a couple things.

  1. It stores the forecast months so data maps, smart pushes, Essbase calculations, and grid builder functions, can be dynamically execute on all months for a budget and the out months for the forecast based on the scenario selected.
  2. It gets the numeric value for the current month to be used in other localized calculations.
  3. It creates a calendar object to get the current time for logging performance.
  4. The parameters are logged to the job console.
/*RTPS: {RTP_Month}*/

def Month = rtps.RTP_Month.toString().replaceAll("\"", "")
def MonthFix = rtps.RTP_Month.toString()
def months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
def actualMonths = []
def forecastMonths = []

months.eachWithIndex {item, index ->
  if(index > months.findIndexOf{it == Month})
    forecastMonths <<  item 
  else
    actualMonths <<  item 
}

def actualMonthsFix = """\"${actualMonths.join('", "')}\""""
def forecastMonthsFix = """\"${forecastMonths.join('", "')}\""""
def useMonths = []

if(operation.grid.pov.find{it.dimName =='Scenario'}.essbaseMbrName.toString().toLowerCase().contains('forecast'))
  useMonths = forecastMonths
else
  useMonths = months

//Get time for logging perforance
Calendar calendar = Calendar.getInstance()
calendar.setTimeInMillis(currentTimeMillis())

// Get the numeric value for the month (Jan=0)
int monthNumber = Calendar.instance.with {
  time = new Date().parse( "MMM", Month )
  it[ MONTH ]
}

println "The current month number is $monthNumber"
println "The current month is $Month"
println "The current month (Fix) is $MonthFix"
println "The actual months (list) are ${actualMonths}"
println "The actual months (string) are ${actualMonthsFix}"
println "The forecast months (list) are ${forecastMonths}"
println "The forecast months (string) are ${forecastMonthsFix}"

Conversion Table

If the application moves data from more detailed plan types to a consolidated P&L, it likely has conversions.  This is an example of an account map from a Gross Profit plan type to a P&L plan type.  How it is used will be explained in a later submission, but it is used in every calculation that synchronizes data from the GP to the P&L cube.  Therefore, it is a great candidate to have in a shared library (a script) so it can be maintained in one place.

def acctMap = ['Cases':'Units',
               'Sales':'42001',
               'COGS':'50001',
               'Tax':'50015',
               'Chargeback':'56010',
               'Investment Gains':'50010',
               'Writeoffs':'56055',
               'Growth Attributed to New Products':'56300',
               'Samples':'56092'
                ]

Sharing Scripts

Just as scripts can be embedded in regular business rules, the exact same syntax is used in Groovy calculations.  Assume the header script above is called Common Groovy and sits in an application named FinPlan in the GP plan type.  The inclusion of the script into any  Groovy calculation would be done by inserting the following text.

%Script(name:=" Common Groovy ",application:="FinPlan",plantype:="GP")

Conclusion

These are just examples to get you thinking about how you can reduce the headaches down the road when your Groovy calculations need maintained.  It can easily be expanded to include common POV members and other common code.  Be mindful of how global this is as not all POV members are in all forms.  You might find it useful to have a script that is shared everywhere and a few others shared for like calculations or forms.  You aren’t limited to only including one shared script.

If you have a snippet you are using in all your Groovy calculations, share it with the community.  We always like to get feedback and learn from each other.




Adventures in Groovy – Part 23: Disney Style

KScope has concluded, and what a fantastic week it was.  I love the years I get the feedback that I have an abstract selected so I can attend.  This year, I was awarded Oracle Ace, so it was really nice to be nominated and recognized for my contributions to the community. 

I tried to record the sessions and unfortunately, only 1 worked.  All of my presentations were recorded through the event and will be released in the coming months. When they are, I will share the links.  Until them, I hope the visual presentations will inspire you to take a look at Groovy, if I haven’t already pushed you in that direction.

Now that my time has been spent building these decks is over, look forward to more Groovy Adventures!

Top Down and BottomS Up Planning at Breakthru Beverage Group

Why Groovy is Game Changing



ePBCS Gridbuilder Deep Dive – Last Minute KScope Souvenirs