Introduction

In this tutorial, you integrate financial data between Oracle Cloud EPM Planning and Strategic Modeling using Groovy business rules.

You'll work with an application containing a Planning financial cube and five models. Using four Groovy rules, you'll transfer financial data from Planning to Strategic Modeling and from Strategic Modeling back to Planning.

You'll perform the following exercises:

  1. Transfer consolidated FY27 Planning Working data for Operations US to the Strategic Modeling Forecast.
  2. Transfer Strategic Modeling Forecast data for Operations US East for 2027 and 2028 to Planning Target for FY27 and FY28.
  3. Transfer Planning Working data for four regional entities to their corresponding Strategic Modeling Forecasts.
  4. Transfer Strategic Modeling Forecast data for four regional models to their corresponding Planning Target entities.

These exercises build on one another. Complete the sections sequentially.

Background

Strategic Modeling, available as a module in Planning, is a feature-rich financial forecasting and modeling solution. With Strategic Modeling, you can model long-range planning, treasury and funding options, and mergers and acquisitions, among other things.

Prerequisites

Cloud EPM Hands-on Tutorials may require you to import a snapshot into a Cloud EPM Enterprise Service instance. A snapshot cannot be imported over an existing application or business process. Before importing the snapshot, use a separate Cloud EPM Enterprise Service instance or remove the existing application or business process from the instance you plan to use. Importing the snapshot does not automatically replace or restore existing content.

Before starting this tutorial, you must:

  • Have Service Administrator access to a Cloud EPM Enterprise Service instance.
  • Upload and import this snapshot into your instance. This snapshot requires the Planning business process. If you have already imported the snapshot for another Groovy tutorial, you can continue using that application without importing the snapshot again.

Note:

If you run into migration errors importing the snapshot, re-run the migration excluding the HSS-Shared Services component, as well as the Security and User Preferences artifacts in the Core component. For more information on uploading and importing snapshots, refer to the Administering Migration for Oracle Enterprise Performance Management Cloud documentation.

Tip:

The scripts you need for this tutorial are linked as text files within each section.

Understanding Strategic Modeling

The primary artifact in Strategic Modeling is a model. Models have the following dimensions:

  • Model (Entity)
  • Version
  • Measure
  • Business Case / Scenario
  • Currency
  • Time
  • Account
  • Custom Dimensions

Because the dimensions in Strategic Modeling and Planning differ, you must understand how they align when mapping data between the two applications.

Model (Entity)

The Model dimension in Strategic Modeling is hierarchical. Each model represents a single entity, such as a company, business unit, or project. A model stores a complete set of data and metadata for each saved version.

Version

The Version dimension is flat. It enables you to specify a particular saved version of model data.

Measure

The Measure dimension is flat and contains two members:

  • Output: Represents calculated results in Strategic Modeling. Output values are commonly mapped between Strategic Modeling and Planning.
  • Input: Represents input values. These values are generally less useful for mapping to Planning because users can change the forecast methods that transform input values into calculated results.

Business Case / Scenario

The Business Case / Scenario dimension is flat. Business cases and scenarios are closely related in Strategic Modeling. Data is stored in scenarios, and a mapping uses either a business case or a scenario, depending on the intended operation.

Currency

The Currency dimension is flat. Each model operates with a particular currency.

Time

The Time dimension is hierarchical. Unlike Planning, Strategic Modeling uses a single continuous Time dimension, with years directly beneath its root. Each year can contain different levels of detail, including halves, quarters, months, and weeks.

Account

The Account dimension is hierarchical. Each account represents an accounting concept, such as Sales or Cost of Goods Sold. Accounts generally represent financial line items. For example, an Output value can exist for Sales across time periods and scenarios within a model. You can extend the Account dimension by creating subaccounts beneath the existing account structure, typically for accounts that accept input.

Custom Dimensions

Strategic Modeling supports hierarchical custom dimensions that can represent additional business concepts, such as products or regions.

Applying These Concepts in This Tutorial

In the following exercises, you'll use Groovy business rules to map financial data between Planning and Strategic Modeling.

The supplied application includes an Operations US parent model and four regional models. You'll work with Planning Working and Target versions and Strategic Modeling Forecast scenarios.

The exercises use Planning fiscal years FY27 and FY28 and the corresponding Strategic Modeling years 2027 and 2028.

The models are configured to store financial data at the yearly level, while Planning stores input data at the monthly level. When transferring data from Planning to Strategic Modeling, the Groovy rules use annual totals. When transferring data from Strategic Modeling to Planning, the rules distribute annual values across the corresponding months.

Although Strategic Modeling displays financial values in millions, the integration scripts use underlying USD values without additional scaling.

Understanding how models, versions, scenarios, accounts, and time periods align will help you follow the mappings used in each Groovy rule.

Understanding the Strategic Models and Planning Application

You'll integrate data between the OEP_FS Planning cube and the Operations US models. The four regional Planning entities—Operations US East, West, North, and South—correspond to the four regional models. The Operations US parent model is a separate integration destination.

The integration mappings differ by exercise. Each rule section will identify its source and destination intersections.

Setting Up for Testing

Entering Test Planning Data

Enter the approved FY27/FY28 December Working values for East and West. YearTotal is calculated from the monthly data; do not enter values into YearTotal.

  1. On the Home page, click Data.
  2. Scroll to the bottom of the list and click the Strategic Modeling folder to view Strategic Modeling forms.
  3. Click Enter Data Form Working to open the form.
    Data entry form showing Operations US and FY27/FY28 Working columns.
  4. In the Entity dropdown, select Operations US East.
    Operations US East initial Working values with missing December FY27 expense entries.
  5. For Operations US East, enter the following values in the FY27 Dec and FY28 Dec columns:
    AccountFY27 DecFY28 Dec
    OFS_Product Revenue4000043000
    OFS_Services Revenue60006500
    OFS_Material1600017000
    OFS_Salaries40004700
    OFS_Selling Expense15001800
    Operations US East Working form showing edited December FY27 and FY28 input cells.
  6. Click Save to save the updated Planning data for Operations US East.
    Operations US East Working data after Save, showing updated annual totals.
  7. In the Information dialog, click OK to close the message confirming that the data has been saved.
    Information dialog confirming the data has been saved.
  8. In the Entity dropdown, select Operations US West.
    Operations US West seeded Working values before learner entry.
  9. For Operations US West, enter the following values in the FY27 Dec and FY28 Dec columns:
    AccountFY27 DecFY28 Dec
    OFS_Product Revenue3300036000
    OFS_Services Revenue50005500
    OFS_Material1350014500
    OFS_Salaries35004200
    OFS_Selling Expense12001500

    Operations US West with ten intended December edits, Services Revenue updated and Support Revenue unchanged.
  10. Click Save to save the updated Planning data for Operations US West.
    Saved Operations US West Working form with updated December and YearTotal values for FY27 and FY28
  11. In the Information dialog, click OK. The edited-cell highlighting clears after saving.

Clearing Planning Data

Clear Target input data for East and West in both FY27 and FY28. Preserve North and South Target values.

  1. At the bottom of the page, select the Data tab to return to the list of forms.
  2. Click Enter Data Form Target to open the form.
    Target form showing populated Operations US West FY27 monthly data.
  3. Verify that Operations US West and FY27 are selected.
  4. In the Jan column, clear the value for OFS_Product Revenue.
  5. Select the handle at the corner of the cleared cell and drag the selection area across the grid through the Dec column and down through OFS_Selling Expense.
    Selected Target input range after releasing the drag handle, with writable cells blank.

    Note:

    Releasing the cursor applies the cleared value to all writable cells in the selected range. Read-only calculated cells are not modified.
    Selection range spanning January through December and all Target account rows.
  6. Click Save to save the cleared Target data.
    Operations US West FY27 Target form after Save, showing blank monthly data and calculated totals.
  7. In the Information dialog, click OK to close the message confirming that the data has been saved.

    Note:

    After saving, the calculated accounts and YearTotal also display blank values. You do not need to clear these cells directly.
    Save confirmation dialog for the Target clearing operation.
  8. Repeat the preceding clearing procedure for the following entity and year combinations:
    EntityYear
    Operations US WestFY28
    Operations US EastFY27
    Operations US EastFY28

    For each combination, select the entity and year from the corresponding dropdowns, clear the monthly data, click Save, and then click OK in the Information dialog.

    Operations US East FY28 Target form after Save, with cleared monthly data and totals.
  9. In the Entity dropdown, select Operations US North and review the existing Target data for FY27 and FY28. Repeat for Operations US South.

    Note:

    The Target data for North and South is already populated in the application snapshot. Leave these values unchanged. Only East and West Target data is cleared in this tutorial. Later, you will use a Groovy rule to transfer Strategic Modeling forecasts into Target for all four regions.
    Operations US North FY27 Target form showing seeded monthly and annual values.

Selecting a Navigation Flow in Planning

Activate the existing SM navigation flow to access the integration cards.

  1. At the top of the page, click Home (Home) to return to the Planning Home page.
  2. Click Strategic Modeling and review the navigation cards displayed by the default navigation flow.
    Default navigation flow showing Models, Scenario Rollups, and Templates cards.
  3. Click Strategic Modeling again to close the card. Click Tools, and then click Navigation Flows.
  4. Verify that the Default navigation flow is Active and the SM navigation flow is Inactive.
    Navigation Flow list with Default active and SM inactive.
  5. In the Navigation Flow list, click Inactive in the Active column for the SM navigation flow.
  6. Verify that the SM navigation flow displays Active and the Default navigation flow displays Inactive.
    Navigation Flow list showing SM active and Default inactive.
  7. Return to the Planning Home page.
  8. In the upper-right corner, click the user icon to open the Settings and Actions menu, then select Reload Navigation Flow.
    Settings and Actions menu with Reload Navigation Flow available.
  9. Click Strategic Modeling to verify that the new navigation cards are displayed.
    SM navigation flow showing Models, Scenario Rollups, Templates, Sync Bottom Up Plan, Push Targets, Push Bottom Up Plan Multi, and Push Targets Multi Entity.

Syncing Bottom-Up Planning Data with Strategic Forecast

In this section, you create a Groovy rule to transfer consolidated Planning data from the Operations US entity to the Forecast version of the Operations US model in Strategic Modeling. After deploying the rule, you associate it with a form and run the rule to verify the transferred data.

Understanding the Source and Target Intersections

The Groovy rule retrieves consolidated FY27 Working data from the Operations US entity in Planning and transfers it to the Forecast version of the Operations US model in Strategic Modeling. The following table identifies the source and target intersections used by the rule.

Planning (Source)Strategic Modeling (Target)
Entity: Operations USModel: Operations US
Scenario: OEP_Strategic PlanningScenario/Version: Forecast
Market: US Market
Plan Element: OFS_Direct Input
Currency: USD
Product: Smart Phone 6 in
Year: FY27Period: 2027
Period: YearTotal
Version: OEP_Working
Source AccountTarget Account
OFS_Product Revenuev1000:010 — Product Sales
OFS_Services Revenuev1000:020 — Service Revenue
OFS_Support Revenue + OFS_Other Revenuev1000:030 — Other Revenues
OFS_Adjustments to Revenuev1020 — Discounts & Returns
OFS_Total Cost Of Salesv1040 — Cost of Goods Sold

Note:

Planning and Strategic Modeling use underlying USD values for this integration. Although Strategic Modeling may display values in millions, the Groovy rule transfers the underlying amounts without applying a scaling factor.

Creating the Groovy Rule

  1. Click Navigator (Navigator), then, under Create and Manage, select Rules to open Calculation Manager.

    Navigator menu showing Rules under Create and Manage
  2. On the System View tab, expand EPM Cloud, then HP4, then OEP_FS. Right-click Rules and select New.

    Calculation Manager System View with Rules context menu and New option
  3. For Name, enter SM - Sync Bottom-Up Plan with Strategic Forecast, then click OK.

    New Rule dialog with rule name, application, cube and OK button
  4. In the Rule Editor, change the Designer option to Edit Script and set Script Type to Groovy Script.

    Rule editor showing Edit Script and Groovy Script selections
  5. Copy this script and paste it into the editor.

    /*
     * Sync Bottom-Up Plan with Strategic Forecast
     *
     * Transfers consolidated Operations US Planning Working
     * data for the plan start year to the Forecast version
     * of the Operations US Strategic Modeling model.
     *
     * Six Planning source accounts map to five Strategic
     * Modeling destination accounts.
     *
     * Financial values use underlying USD.
     */
    
    import java.math.RoundingMode
    
    Code section 1// 1. Identify the Planning cube and forecast year
    
    Cube cube = operation.application.getCube('OEP_FS')
    
    String planningYear =
        cube.getSubstitutionVariableValue('OEP_PlanStartYr')
    
    if (planningYear == null ||
        !(planningYear ==~ /FY[0-9]{2}/)) {
        throw new IllegalStateException(
            'Invalid OEP_PlanStartYr: ' + planningYear)
    }
    
    String strategicYear = '20' + planningYear.substring(2)
    
    Code section 2// 2. Define the source and destination accounts
    
    String[] sourceAccounts = [
        'OFS_Product Revenue',
        'OFS_Services Revenue',
        'OFS_Support Revenue',
        'OFS_Adjustments to Revenue',
        'OFS_Total Cost Of Sales',
        'OFS_Other Revenue'
    ]
    
    String[] destinationAccounts = [
        'v1000:010',
        'v1000:020',
        'v1000:030',
        'v1020',
        'v1040'
    ]
    
    println('SM - Sync Bottom-Up Plan with Strategic Forecast START: Planning ' + planningYear +
        ' to Strategic Modeling ' + strategicYear)
    
    Code section 3// 3. Retrieve and validate Planning source values
    
    DataGridDefinitionBuilder definitionBuilder =
        cube.dataGridDefinitionBuilder()
    
    definitionBuilder.addPov(
        ['Years','Scenario','Currency','Version',
         'Entity','Market','Plan Element','Product'],
        [[planningYear],['OEP_Strategic Planning'],
         ['USD'],['OEP_Working'],['Operations US'],
         ['US Market'],['OFS_Direct Input'],
         ['Smart Phone 6 in']]
    )
    
    List<String> periodMembers = ['YearTotal']
    definitionBuilder.addColumn(
        ['Period'], [periodMembers]
    )
    
    definitionBuilder.addRow(
        ['Account'], [sourceAccounts.toList()]
    )
    
    Map<String, BigDecimal> sources = [:]
    Set<String> seen = new HashSet<String>()
    
    cube.loadGrid(
        definitionBuilder.build(), false
    ).withCloseable { DataGrid grid ->
    
        grid.dataCellIterator().each { DataGrid.DataCell cell ->
            String account = cell.accountName.toString()
    
            if (!sourceAccounts.toList().contains(account)) {
                throw new IllegalStateException(
                    'Unexpected Planning account: ' + account)
            }
    
            if (!seen.add(account)) {
                throw new IllegalStateException(
                    'Duplicate Planning account: ' + account)
            }
    
            if (cell.missing) {
                throw new IllegalStateException(
                    'Missing Planning value: ' + account)
            }
    
            Object raw = cell.data
            if (!(raw instanceof Number)) {
                throw new IllegalStateException(
                    'Non-numeric Planning value: ' + account)
            }
    
            BigDecimal amount =
                new BigDecimal(raw.toString()).setScale(
                    2, RoundingMode.UNNECESSARY)
    
            sources.put(account, amount)
    
            println('SOURCE ' + account +
                ' USD=' + amount.toPlainString())
        }
    }
    
    if (seen.size() != 6 || sources.size() != 6) {
        throw new IllegalStateException(
            'Expected six unique Planning sources; found ' +
            sources.size())
    }
    
    Code section 4// 4. Map Planning values to Strategic Modeling accounts
    
    BigDecimal[] destinations = [
        sources.get('OFS_Product Revenue'),
        sources.get('OFS_Services Revenue'),
        sources.get('OFS_Support Revenue').add(
            sources.get('OFS_Other Revenue')),
        sources.get('OFS_Adjustments to Revenue'),
        sources.get('OFS_Total Cost Of Sales')
    ]
    
    for (int i = 0; i < destinationAccounts.length; i++) {
        println('PREPARED ' + destinationAccounts[i] +
            ' USD=' + destinations[i].toPlainString())
    }
    
    println('ALL SIX PLANNING SOURCES VERIFIED')
    println('ALL FIVE SM DESTINATIONS PREPARED')
    
    Code section 5// 5. Build and validate the destination grid
    
    operation.application.getStrategicModel(
        'Operations US'
    ).withCloseable { StrategicModel smModel ->
    
        DataGridBuilder builder = smModel.dataGridBuilder()
    
        builder.addPov('Forecast')
        builder.addColumn(strategicYear)
    
        for (int i = 0; i < destinationAccounts.length; i++) {
            builder.addRow(
                [destinationAccounts[i]],
                [destinations[i].doubleValue()]
            )
        }
    
        DataGridBuilder.Status status =
            new DataGridBuilder.Status()
    
        builder.build(status).withCloseable { DataGrid targetGrid ->
    
            println('GRID size=' + targetGrid.size() +
                ' accepted=' + status.numAcceptedCells +
                ' rejected=' + status.numRejectedCells)
    
            if (targetGrid.size() != 5 ||
                status.numAcceptedCells != 5 ||
                status.numRejectedCells != 0) {
    
                println('REJECTION DETAILS: ' +
                    status.cellsRejected)
    
                throw new IllegalStateException(
                    'the rule destination validation failed; ' +
                    'ZERO SAVES ATTEMPTED')
            }
    
            println('ALL FIVE DESTINATION CELLS VALIDATED')
    Code section 6        // 6. Save data and report completion
    
            smModel.saveGrid(targetGrid)
    
            println('SM - Sync Bottom-Up Plan with Strategic Forecast SAVE COMPLETED')
            println('Accepted cells: ' +
                status.numAcceptedCells)
            println('Rejected cells: ' +
                status.numRejectedCells)
            println('Verify all five persisted values ' +
                'independently in Strategic Modeling.')
        }
    }
    
    println('SM - Sync Bottom-Up Plan with Strategic Forecast END')
    
    

    Code section 1 Identify the Planning Cube and Forecast Year

    The script accesses the OEP_FS Planning cube and retrieves the OEP_PlanStartYr substitution variable. It verifies the FYnn format and converts FY27 to the Strategic Modeling year 2027.

    Code section 2 Define the Source and Destination Accounts

    The script identifies six Planning source accounts and five Strategic Modeling destination accounts. Support Revenue and Other Revenue are combined later. A starting message identifies the years being processed.

    Code section 3 Retrieve and Validate Planning Source Values

    The script builds a Planning grid at the specified dimensional intersections, with YearTotal as the period. It requires six unique, nonmissing, numeric source values and checks that each amount has no more than two decimal places without rounding.

    Code section 4 Map Planning Values to Strategic Modeling Accounts

    The script prepares five destination values, combining Support Revenue and Other Revenue. It prints the prepared amounts in underlying USD to the execution log.

    Code section 5 Build and Validate the Destination Grid

    The script builds the Operations US Forecast grid for the derived year and checks that it has five cells, five accepted values, and zero rejected values before saving.

    Code section 6 Save Data and Report Completion

    After validation, the script saves the grid, reports accepted and rejected cells, and reminds learners to verify the five persisted values independently in Strategic Modeling.

  6. Click Save (Save) to save the rule. At the information message, click OK.

    Information message confirming the Groovy rule was saved
  7. Click Validate and Deploy (Validate and Deploy) to validate and deploy the rule. At the information message, click OK.

    Deployment successful confirmation with OK button

Adding the Rule to the Testing Form

  1. Leave the Calculation Manager window open and return to the Planning browser window.

  2. From the Home page, click Strategic Modeling, then click Sync Bottom Up Plan.

    Push Bottom Up Plan One Entity form showing FY27 Working YearTotal data
  3. Click Actions, then select Edit. A new dynamic tab opens to edit the data form.

    Actions menu showing Edit option
  4. Select the Business Rules tab.

  5. Add SM - Sync Bottom-Up Plan with Strategic Forecast to the Selected Business Rules list.

    Business Rules tab with integration rule in Selected Business Rules
  6. Click Save. At the information message, click OK.

    Information dialog confirming the form was saved
  7. At the bottom of the page, close the Edit Push Bottom Up Plan One Entity tab to return to the data form.

Testing the Groovy Rule

  1. Click Actions, then select Rules & Smart Push.

    Actions menu showing Rules and Smart Push option
  2. Under Rules, select SM - Sync Bottom-Up Plan with Strategic Forecast to launch the rule.

    Rules menu listing the Strategic Modeling integration rule
  3. When the rule completes, click OK to dismiss the information message.

    Information message confirming successful rule execution
  4. At the bottom of the page, select the Strategic Plan tab. In the 2027 column, verify that the data was pushed to the expected accounts. For example, OFS_Product Revenue was pushed to v1000:010 Product Sales, which displays 1.328 million USD.

    Operations US Forecast 2027 showing five highlighted destination values

Adjusting Targets in a Bottom-Up Plan

In this section, you create a Groovy rule to transfer Forecast data for 2027 and 2028 from the Operations US East model to the Target version of the Operations US East entity in Planning for FY27 and FY28. After deploying the rule, you associate it with a form and run the rule to verify the transferred data.

Understanding the Source and Target Intersections

The Groovy rule retrieves Forecast data for 2027 and 2028 from the Operations US East model and transfers it to the Target version of the Operations US East entity in Planning for FY27 and FY28. The rule allocates annual values to Planning accounts and distributes the resulting amounts across January through December. The following tables identify the source and target intersections and account mappings used by the rule.

Strategic Modeling (Source)Planning (Target)
Model: Operations US EastEntity: Operations US East
Scenario/Version: ForecastScenario: OEP_Strategic Planning
Years: 2027, 2028Years: FY27, FY28
Time: AnnualPeriods: Jan–Dec
Version: OEP_Target
Cube: OEP_FS
Market: US Market
Plan Element: OFS_Direct Input
Currency: USD
Product: Smart Phone 6 in
Strategic Modeling SourcePlanning Target Account
v1000:010 Product SalesOFS_Product Revenue
v1000:020 Service RevenuesOFS_Services Revenue
v1000:030 Other RevenuesOFS_Support Revenue; OFS_Other Revenue
v1020 Discounts & ReturnsOFS_Discounts; OFS_Returns
v1040 Cost of Goods SoldOFS_Labor Expense; OFS_Material; OFS_Overhead; OFS_Freight; OFS_Other Cost of sales
v1080:010 Salary ExpenseOFS_Salaries
v1080:020 Selling ExpenseOFS_Selling Expense

Note:

Strategic Modeling stores annual values in underlying USD, while Planning Target data is entered by month. The Groovy rule allocates annual source amounts among mapped Planning accounts using approved year-specific baseline weights, then distributes each account’s annual allocation across 12 months. It reconciles allocations to USD cents without applying a million-unit scaling factor.

Creating the Groovy Rule

  1. Return to the Calculation Manager window and select the System View tab.

  2. Under OEP_FS, right-click Rules, then select New.

    System View showing New on the Rules context menu
  3. For Name, enter SM - Adjust Targets in Bottom-Up Plan, then click OK.

    New Rule dialog with approved rule name
  4. In the Rule Editor, change the Designer option to Edit Script and set Script Type to Groovy Script.

    Script editor set to Edit Script and Groovy Script
  5. Copy this script and paste it into the editor.

    /*
     * Adjust Targets in Bottom-Up Plan
     *
     * Transfers Forecast data for 2027 and 2028 from
     * the Operations US East Strategic Modeling model
     * to the Target version of the Operations US East
     * entity in Planning for FY27 and FY28.
     *
     * Seven Strategic Modeling source accounts map to
     * 13 Planning input accounts. Annual source amounts
     * are allocated using approved year-specific weights
     * and distributed across January through December.
     *
     * Allocations are reconciled to USD cents.
     * Calculated accounts are excluded.
     */
    
    Code section 1// 1. Define the account mappings and allocation weights
    
    String[] sourceAccounts = [
        'v1000:010',
        'v1000:020',
        'v1000:030',
        'v1020',
        'v1040',
        'v1080:010',
        'v1080:020'
    ]
    
    String[] targetAccounts = [
        'OFS_Product Revenue',
        'OFS_Services Revenue',
        'OFS_Support Revenue',
        'OFS_Other Revenue',
        'OFS_Discounts',
        'OFS_Returns',
        'OFS_Labor Expense',
        'OFS_Material',
        'OFS_Overhead',
        'OFS_Freight',
        'OFS_Other Cost of sales',
        'OFS_Salaries',
        'OFS_Selling Expense'
    ]
    
    int[] sourceIndex = [
        0, 1, 2, 2, 3, 3,
        4, 4, 4, 4, 4, 5, 6
    ]
    
    String[] years = ['2027', '2028']
    String[] planningYears = ['FY27', 'FY28']
    
    String[] months = [
        'Jan', 'Feb', 'Mar', 'Apr',
        'May', 'Jun', 'Jul', 'Aug',
        'Sep', 'Oct', 'Nov', 'Dec'
    ]
    
    // Year-specific baseline weights.
    // Use List<List<Long>> for Planning Groovy type checking.
    
    List<List<Long>> yearWeights = [
        [
            35000L, 5000L, 3000L, 2000L,
            1500L, 500L,
            2000L, 14000L, 2000L, 1000L, 3000L,
            4000L, 1500L
        ],
        [
            38000L, 5500L, 3200L, 2100L,
            1600L, 500L,
            2200L, 15000L, 2100L, 1100L, 3200L,
            4200L, 1600L
        ]
    ]
    
    Code section 2// 2. Allocate amounts using the largest-remainder method
    
    def allocateLargestRemainder = {
        long lrSignedCents, List<Long> lrWeights ->
    
        if (lrSignedCents == Long.MIN_VALUE ||
            lrWeights == null ||
            lrWeights.isEmpty() ||
            lrWeights.any { it < 0L }) {
    
            throw new IllegalArgumentException(
                "Invalid allocation input"
            )
        }
    
        BigDecimal lrDenominator = BigDecimal.ZERO
    
        lrWeights.each { lrWeight ->
            lrDenominator = lrDenominator.add(
                new BigDecimal(lrWeight)
            )
        }
    
        if (lrDenominator.signum() == 0) {
            throw new IllegalArgumentException(
                "Zero baseline denominator"
            )
        }
    
        BigDecimal lrMagnitude =
            new BigDecimal(lrSignedCents).abs()
    
        long[] lrResults = new long[lrWeights.size()]
    
        BigDecimal[] lrFractions =
            new BigDecimal[lrWeights.size()]
    
        long lrAllocated = 0L
    
        for (int lrI = 0; lrI < lrWeights.size(); lrI++) {
    
            BigDecimal lrNumerator =
                lrMagnitude.multiply(
                    new BigDecimal(lrWeights[lrI])
                )
    
            lrResults[lrI] = lrNumerator
                .divideToIntegralValue(lrDenominator)
                .longValueExact()
    
            lrFractions[lrI] =
                lrNumerator.remainder(lrDenominator)
    
            lrAllocated = Math.addExact(
                lrAllocated, lrResults[lrI]
            )
        }
    
        long lrRemaining =
            lrMagnitude.longValueExact() - lrAllocated
    
        List<Integer> lrOrder =
            (0..<lrWeights.size()).toList()
    
        lrOrder.sort { lrA, lrB ->
    
            int lrComparison =
                lrFractions[lrB].compareTo(
                    lrFractions[lrA]
                )
    
            lrComparison != 0 ?
                lrComparison : lrA.compareTo(lrB)
        }
    
        if (lrRemaining < 0L ||
            lrRemaining > lrOrder.size()) {
    
            throw new IllegalStateException(
                "Invalid allocation remainder"
            )
        }
    
        for (int lrR = 0; lrR < lrRemaining; lrR++) {
    
            int lrIndex = lrOrder[lrR]
    
            lrResults[lrIndex] = Math.addExact(
                lrResults[lrIndex], 1L
            )
        }
    
        long lrSign = lrSignedCents < 0L ? -1L : 1L
    
        for (int lrI = 0; lrI < lrResults.length; lrI++) {
            lrResults[lrI] *= lrSign
        }
    
        return lrResults.toList()
    }
    
    Code section 3// 3. Retrieve Strategic Modeling Forecast data
    
    BigDecimal[][] sourceUSD = new BigDecimal[2][7]
    boolean[][] seen = new boolean[2][7]
    
    println("SM - Adjust Targets in Bottom-Up Plan START")
    
    operation.application.getStrategicModel(
        "Operations US East"
    ).withCloseable { smModel ->
    
        DataGridDefinitionBuilder builder =
            smModel.dataGridDefinitionBuilder()
    
        builder.addPov(['Scenario'], [ ['Forecast'] ])
    
        builder.addColumn(
            ['Time'], [ ['2027', '2028'] ]
        )
    
        builder.addRow(['Account'], [ [
            'v1000:010',
            'v1000:020',
            'v1000:030',
            'v1020',
            'v1040',
            'v1080:010',
            'v1080:020'
        ] ])
    
        DataGridDefinition definition = builder.build()
    
        smModel.loadGrid(definition).withCloseable { smGrid ->
    
            smGrid.dataCellIterator().each { cell ->
    
                String account =
                    cell.memberNames[0].toString()
    
                String year = cell.periodName.toString()
    
                int s =
                    sourceAccounts.toList().indexOf(account)
    
                int y = years.toList().indexOf(year)
    
                if (s < 0 || y < 0 ||
                    seen[y][s] || cell.missing) {
    
                    throw new IllegalStateException(
                        "Invalid source cell: " +
                        cell.memberNames
                    )
                }
    
                seen[y][s] = true
    
                sourceUSD[y][s] =
                    new BigDecimal(cell.data.toString())
    
                println(
                    "SOURCE " + year + " " + account +
                    " USD=" + sourceUSD[y][s]
                )
            }
        }
    }
    
    Code section 4// 4. Allocate and reconcile the annual values
    
    long[][][] monthlyCents = new long[2][13][12]
    
    int totalAllocations = 0
    
    for (int y = 0; y < 2; y++) {
    
        println("=== ALLOCATING " + planningYears[y] + " ===")
    
        long[] sourceCents = new long[7]
    
        for (int s = 0; s < 7; s++) {
    
            if (!seen[y][s]) {
                throw new IllegalStateException(
                    "Missing source: " +
                    years[y] + " " + sourceAccounts[s]
                )
            }
    
            sourceCents[s] = sourceUSD[y][s]
                .multiply(new BigDecimal("100"))
                .setScale(
                    0,
                    java.math.RoundingMode.HALF_UP
                )
                .longValueExact()
        }
    
        long[] annualCents = new long[13]
    
        for (int s = 0; s < 7; s++) {
    
            List<Long> groupWeights = []
            List<Integer> groupIndices = []
    
            for (int i = 0; i < 13; i++) {
    
                if (sourceIndex[i] == s) {
                    groupWeights.add(yearWeights[y][i])
                    groupIndices.add(i)
                }
            }
    
            List<Long> allocations =
                allocateLargestRemainder(
                    sourceCents[s], groupWeights
                )
    
            long groupCheck = 0L
    
            for (int g = 0; g < groupIndices.size(); g++) {
    
                int index = groupIndices[g]
    
                annualCents[index] = allocations[g]
    
                groupCheck = Math.addExact(
                    groupCheck, allocations[g]
                )
            }
    
            if (groupCheck != sourceCents[s]) {
                throw new IllegalStateException(
                    "Group reconciliation failed: " +
                    planningYears[y] + " " +
                    sourceAccounts[s]
                )
            }
    
            println(
                "GROUP PASS: " + planningYears[y] +
                " " + sourceAccounts[s]
            )
        }
    
        for (int i = 0; i < 13; i++) {
    
            long amount = annualCents[i]
    
            if (amount == Long.MIN_VALUE) {
                throw new IllegalStateException(
                    "Unsupported annual amount"
                )
            }
    
            long magnitude = Math.abs(amount)
            long base = Math.floorDiv(magnitude, 12L)
            long remainder = magnitude % 12L
            long sign = amount < 0L ? -1L : 1L
    
            long monthlyCheck = 0L
    
            for (int m = 0; m < 12; m++) {
    
                long extra = m < remainder ? 1L : 0L
    
                monthlyCents[y][i][m] =
                    sign * (base + extra)
    
                monthlyCheck = Math.addExact(
                    monthlyCheck,
                    monthlyCents[y][i][m]
                )
    
                totalAllocations++
            }
    
            if (monthlyCheck != amount) {
                throw new IllegalStateException(
                    "Monthly reconciliation failed: " +
                    planningYears[y] + " " +
                    targetAccounts[i]
                )
            }
    
            println(
                "ACCOUNT PASS: " + planningYears[y] +
                " " + targetAccounts[i]
            )
        }
    
        println(
            "YEAR PASS: " + planningYears[y] +
            " 156 allocations reconciled"
        )
    }
    
    if (totalAllocations != 312) {
        throw new IllegalStateException(
            "Expected 312 allocations, found " +
            totalAllocations
        )
    }
    
    println("ALL 312 ALLOCATIONS RECONCILED")
    
    Code section 5// 5. Build the Planning Target grids
    
    Cube cube = operation.application.getCube("OEP_FS")
    
    DataGridBuilder fy27Builder =
        cube.dataGridBuilder("MM/DD/YYYY")
    
    DataGridBuilder fy28Builder =
        cube.dataGridBuilder("MM/DD/YYYY")
    
    fy27Builder.addPov(
        'FY27',
        'OEP_Strategic Planning',
        'USD',
        'Target',
        'Operations US East',
        'OFS_Direct Input',
        'Smart Phone 6 in',
        'US Market'
    )
    
    fy28Builder.addPov(
        'FY28',
        'OEP_Strategic Planning',
        'USD',
        'Target',
        'Operations US East',
        'OFS_Direct Input',
        'Smart Phone 6 in',
        'US Market'
    )
    
    fy27Builder.addColumn(months as String[])
    fy28Builder.addColumn(months as String[])
    
    for (int i = 0; i < 13; i++) {
    
        List<Object> fy27Values = []
        List<Object> fy28Values = []
    
        for (int m = 0; m < 12; m++) {
    
            BigDecimal fy27USD =
                new BigDecimal(monthlyCents[0][i][m])
                    .movePointLeft(2)
    
            BigDecimal fy28USD =
                new BigDecimal(monthlyCents[1][i][m])
                    .movePointLeft(2)
    
            fy27Values.add(fy27USD.doubleValue())
            fy28Values.add(fy28USD.doubleValue())
        }
    
        fy27Builder.addRow(
            [targetAccounts[i]], fy27Values
        )
    
        fy28Builder.addRow(
            [targetAccounts[i]], fy28Values
        )
    
        println(
            "GRID ROW: " + targetAccounts[i] +
            " FY27=" + fy27Values +
            " FY28=" + fy28Values
        )
    }
    
    Code section 6// 6. Validate and save the Planning Target data
    
    DataGridBuilder.Status fy27Status =
        new DataGridBuilder.Status()
    
    DataGridBuilder.Status fy28Status =
        new DataGridBuilder.Status()
    
    fy27Builder.build(fy27Status).withCloseable { fy27Grid ->
    
        fy28Builder.build(fy28Status).withCloseable { fy28Grid ->
    
            println("FY27 grid size: " + fy27Grid.size())
            println("FY27 accepted: " +
                fy27Status.numAcceptedCells)
            println("FY27 rejected: " +
                fy27Status.numRejectedCells)
            println("FY27 rejection details: " +
                fy27Status.cellsRejected)
    
            println("FY28 grid size: " + fy28Grid.size())
            println("FY28 accepted: " +
                fy28Status.numAcceptedCells)
            println("FY28 rejected: " +
                fy28Status.numRejectedCells)
            println("FY28 rejection details: " +
                fy28Status.cellsRejected)
    
            if (fy27Grid.size() != 156 ||
                fy28Grid.size() != 156 ||
                fy27Status.numAcceptedCells != 156 ||
                fy28Status.numAcceptedCells != 156 ||
                fy27Status.numRejectedCells != 0 ||
                fy28Status.numRejectedCells != 0) {
    
                throw new IllegalStateException(
                    "Combined grid validation failed. " +
                    "No Planning data saved."
                )
            }
    
            println("BOTH PRE-WRITE VALIDATIONS PASSED")
    
            // Both saves are required for this test.
            // They are separate operations, not a
            // single atomic transaction.
    
            cube.saveGrid(fy27Grid)
            println("FY27 PLANNING SAVE COMPLETED")
    
            cube.saveGrid(fy28Grid)
            println("FY28 PLANNING SAVE COMPLETED")
    
            println("=== COMBINED PLANNING SAVE COMPLETED ===")
            println("Entity: Operations US East")
            println("Scenario: OEP_Strategic Planning")
            println("Version: Target")
            println("Years: FY27 and FY28")
            println("Months: Jan through Dec")
            println("Accounts per year: 13")
            println("Total cells submitted: 312")
        }
    }
    
    println("SM - Adjust Targets in Bottom-Up Plan END")
    

    Code section 1 Define the Account Mappings and Allocation Weights — The script identifies seven source accounts and 13 Planning Target accounts. The sourceIndex array maps target accounts to their sources. Year-specific baseline weights determine the proportional allocation; they are not the values written directly to Planning.

    Code section 2 Allocate Amounts Using the Largest-Remainder Method — The script allocates whole USD cents according to baseline weights, assigns remaining cents to the largest fractional remainders, and uses account order to break ties. It supports negative amounts and validates the inputs.

    Code section 3 Retrieve Strategic Modeling Forecast Data — The script opens the Operations US East model and reads seven Forecast accounts for 2027 and 2028. It checks expected account and year intersections, duplicates, and missing values, then logs underlying USD amounts.

    Code section 4 Allocate and Reconcile the Annual Values — The script allocates each annual source amount among mapped Planning accounts and then across January–December. It checks account-group and monthly reconciliation and confirms 312 monthly allocations.

    Code section 5 Build the Planning Target Grids — The script accesses the OEP_FS Planning cube and builds separate FY27 and FY28 Target grids, each with 13 account rows and January–December columns. It converts cents to USD and logs prepared row values.

    Code section 6 Validate and Save the Planning Target Data — The script checks that both grids have 156 accepted cells and zero rejected cells before saving either year. It saves FY27 and FY28 separately, reports completion, and closes the grids. The two saves are not an atomic transaction.

  6. Click Save (Save) to save the rule. At the information message, click OK.

    Information dialog confirming the rule was saved
  7. Click Validate and Deploy (Validate and Deploy) to validate and deploy the rule. At the information message, click OK.

    Deployment successful confirmation

Adding the Rule to the Testing Form

  1. Leave Calculation Manager open and return to the Planning browser window.

  2. At the top of the page, select the Push Targets tab.

    Operations US East model Forecast 2027 and 2028
  3. At the bottom of the page, select the Push Targets tab to open the data form.

    Push Targets One Entity form without Depreciation
  4. Click Actions, then select Edit. A new dynamic tab opens to edit the data form.

    Actions menu with Edit option
  5. Select the Business Rules tab.

  6. Add SM - Adjust Targets in Bottom-Up Plan to the Selected Business Rules list.

    Business Rules tab with approved rule selected
  7. Click Save. At the information message, click OK.

    Form saved information message
  8. At the bottom of the page, close the Edit Push Targets One Entity tab to return to the data form.

Testing the Groovy Rule

  1. Click Actions, then select Rules & Smart Push.

    Actions menu with Rules and Smart Push option
  2. Under Rules, select SM - Adjust Targets in Bottom-Up Plan to launch the rule.

    Rules menu showing Adjust Targets in Bottom-Up Plan
  3. When the rule completes, click OK to dismiss the information message.

    Successful Adjust Targets rule execution message
  4. Verify that the rule populated the Planning Target values for Operations US East in FY27 and FY28. For example, OFS_Product Revenue displays 35000 for FY27 and 38000 for FY28.

    Push Targets One Entity showing highlighted FY27 and FY28 December values

Pushing Bottom-Up Planning Data to Strategic Forecast for Multiple Entities

In this section, you create a Groovy rule to push bottom-up Planning data from four regional entities for FY27 and FY28 to the corresponding Strategic Modeling models for Forecast 2027 and 2028. You then deploy the rule, associate it with the Strategic Modeling form, and execute it to verify that the Planning data is reflected in the models.

Understanding the Source and Target Intersections

The Groovy rule retrieves bottom-up Planning data from four regional entities in the OEP_FS cube and pushes the data to the corresponding Strategic Modeling models. The rule processes FY27 and FY28, updating the Forecast scenarios for 2027 and 2028.

DimensionPlanning SourceStrategic Modeling Target
Entity / ModelOperations US East, West, North, SouthCorresponding regional models
ScenarioStrategic PlanningForecast
VersionWorking—
YearsFY27, FY282027, 2028
PeriodYearTotalAnnual forecast values
AccountsNine Planning source accountsSeven Strategic Modeling destination accounts

The rule maps the Planning accounts to the following Strategic Modeling accounts.

Planning Source AccountStrategic Modeling Destination Account
OFS_Product Revenuev1000:010 Product Sales
OFS_Services Revenuev1000:020 Service Revenues
OFS_Support Revenue + OFS_Other Revenuev1000:030 Other Revenues
OFS_Discounts + OFS_Returnsv1020 Discounts & Returns
OFS_Total Cost Of Salesv1040 Cost of Goods Sold
OFS_Salariesv1080:010 Salary Expense
OFS_Selling Expensev1080:020 Selling Expense

The rule reads nine Planning source accounts and maps them to seven Strategic Modeling destination accounts for each entity and year combination. This results in 72 Planning source values and 56 Strategic Modeling destination values.

Note:

Planning stores financial values in underlying USD, while Strategic Modeling displays financial values in millions. The Groovy rule transfers the underlying USD amounts without applying a scaling factor. For example, a Planning value of 1,000,000 USD appears as 1.000 million in Strategic Modeling.

Creating the Groovy Rule

  1. Leave the Planning browser window open and return to Calculation Manager. Select the System View tab.

  2. Under OEP_FS, right-click Rules, then select New.

    Rules context menu showing New
  3. For Name, enter SM - Push Bottom-Up Plans for Multiple Entities, then click OK.

    New Rule dialog showing the rule name
  4. In the Rule Editor, change the Designer option to Edit Script and set Script Type to Groovy Script.

    Rule editor configured for Groovy Script
  5. Copy this script and paste it into the editor.

    /*
     * Push Bottom-Up Plans for Multiple Entities
     *
     * Transfers annual Planning data from the Working version
     * of four regional entities in the Strategic Planning scenario
     * to the Forecast scenario of their corresponding
     * Strategic Modeling models.
     *
     * Uses the OEP_PlanStartYr substitution variable to identify
     * two consecutive Planning fiscal years and derive the
     * corresponding four-digit Strategic Modeling forecast years.
     *
     * Nine Planning source accounts map to seven Strategic
     * Modeling destination accounts for each entity and year.
     * Financial values use underlying USD without scaling.
     *
     * The rule validates all destination grids before saving.
     * Each model is saved separately; the four saves are not atomic.
     */
    
    Code section 1// [1] Define regional entities and derive years from the Planning substitution variable.
    String[] entities = [
        'Operations US East', 'Operations US West',
        'Operations US North', 'Operations US South'
    ]
    Cube cube = operation.application.getCube('OEP_FS')
    String planningStartYear = cube.getSubstitutionVariableValue('OEP_PlanStartYr')
    if (planningStartYear == null || !(planningStartYear ==~ /FY[0-9]{2}/)) {
        throw new IllegalStateException('Invalid OEP_PlanStartYr: ' + planningStartYear)
    }
    int startYear = 2000 + Integer.parseInt(planningStartYear.substring(2))
    String[] planningYears = [
        planningStartYear,
        'FY' + String.format('%02d', (startYear + 1) % 100)
    ]
    String[] smYears = [startYear.toString(), (startYear + 1).toString()]
    Code section 2// [2] Define the nine Planning source and seven destination accounts.
    String[] planningAccounts = [
        'OFS_Product Revenue',
        'OFS_Services Revenue',
        'OFS_Support Revenue',
        'OFS_Other Revenue',
        'OFS_Discounts',
        'OFS_Total Cost Of Sales',
        'OFS_Returns',
        'OFS_Salaries',
        'OFS_Selling Expense'
    ]
    String[] smAccounts = [
        'v1000:010', 'v1000:020', 'v1000:030',
        'v1020', 'v1040', 'v1080:010', 'v1080:020'
    ]
    
    // Source values come from the current Planning Working YearTotal data.
    println('=== SM - Push Bottom-Up Plans for Multiple Entities ===')
    println('Planning years: ' + planningYears.toList() +
        '; Strategic Modeling years: ' + smYears.toList())
    
    Code section 3// [3] Prepare the source read and destination value storage.
    BigDecimal[][][] destinationUSD = new BigDecimal[4][2][7]
    int sourceCount = 0
    int destinationCount = 0
    
    // PHASE 1: Read all Planning source cells and calculate destinations.
    // No SM grids are built or saved in this phase.
    for (int e = 0; e < entities.length; e++) {
        String entity = entities[e]
        DataGridDefinitionBuilder definitionBuilder =
            cube.dataGridDefinitionBuilder()
    
        List<String> povDimensions = [
            'Period', 'Scenario', 'Currency', 'Version',
            'Entity', 'Market', 'Plan Element', 'Product'
        ]
        List<List<String>> povMembers = [
            ['YearTotal'], ['OEP_Strategic Planning'], ['USD'],
            ['OEP_Working'], [entity], ['US Market'],
            ['OFS_Direct Input'], ['Smart Phone 6 in']
        ]
        List<String> yearMembers = planningYears.toList()
        List<String> accountMembers = planningAccounts.toList()
        definitionBuilder.addPov(povDimensions, povMembers)
        definitionBuilder.addColumn(['Years'], [yearMembers])
        definitionBuilder.addRow(['Account'], [accountMembers])
    
        BigDecimal[][] sources = new BigDecimal[9][2]
        boolean[][] seen = new boolean[9][2]
        DataGridDefinition definition = definitionBuilder.build()
    
        cube.loadGrid(definition, false).withCloseable { planningGrid ->
            planningGrid.dataCellIterator().each { cell ->
                String account = cell.accountName.toString()
                String year = cell.yearName.toString()
                int a = planningAccounts.toList().indexOf(account)
                int y = planningYears.toList().indexOf(year)
                Code section 4// [4] Validate Planning cells and prepare mapped financial values.
                if (a < 0 || y < 0 || seen[a][y]) {
                    throw new IllegalStateException(
                        'Unexpected or duplicate Planning cell: ' +
                        entity + ' ' + year + ' ' + account)
                }
                if (cell.missing) {
                    throw new IllegalStateException(
                        'Missing Planning cell: ' +
                        entity + ' ' + year + ' ' + account)
                }
                seen[a][y] = true
                sources[a][y] = new BigDecimal(cell.data.toString())
                sourceCount++
            }
        }
    
        for (int y = 0; y < 2; y++) {
            for (int a = 0; a < 9; a++) {
                if (!seen[a][y]) {
                    throw new IllegalStateException(
                        'Source not returned: ' + entity + ' ' +
                        planningYears[y] + ' ' + planningAccounts[a])
                }
            }
    
            // Combine the nine Planning sources into seven destinations.
            BigDecimal[] mapped = [
                sources[0][y],
                sources[1][y],
                sources[2][y].add(sources[3][y]),
                sources[4][y].add(sources[6][y]),
                sources[5][y],
                sources[7][y],
                sources[8][y]
            ]
    
            for (int a = 0; a < 7; a++) {
                // Reject non-cent-precision inputs instead of silently
                // rounding a financial source amount.
                BigDecimal value = mapped[a].setScale(
                    2, java.math.RoundingMode.UNNECESSARY)
                destinationUSD[e][y][a] = value
                destinationCount++
                println('PREPARED ' + entity + ' ' + smYears[y] +
                    ' Forecast ' + smAccounts[a] +
                    ' USD=' + value.toPlainString())
            }
        }
    }
    
    if (sourceCount != 72 || destinationCount != 56) {
        throw new IllegalStateException(
            'Unexpected source/destination counts: ' +
            sourceCount + '/' + destinationCount)
    }
    println('ALL 72 PLANNING SOURCES VERIFIED')
    println('ALL 56 SM DESTINATIONS PREPARED')
    
    // PHASE 2: Build and validate all four SM grids before any save.
    // Keep all models and grids open until every build has passed.
    // Close in reverse order even if validation or saving fails.
    Code section 5// [5] Open and prepare the regional Strategic Modeling models.
    List<StrategicModel> smModels = []
    List<DataGrid> smGrids = []
    List<DataGridBuilder.Status> smStatuses = []
    try {
        for (int e = 0; e < entities.length; e++) {
            String entity = entities[e]
            StrategicModel smModel = operation.application.getStrategicModel(entity)
            smModels.add(smModel)
    
            Code section 6// [6] Populate the Forecast grid for both years.
            DataGridBuilder builder = smModel.dataGridBuilder()
            builder.addPov('Forecast')
            builder.addColumn(smYears as String[])
    
            for (int a = 0; a < 7; a++) {
                List<Object> amounts = []
                for (int y = 0; y < 2; y++) {
                    amounts.add(destinationUSD[e][y][a].doubleValue())
                }
                builder.addRow([smAccounts[a]], amounts)
            }
    
            DataGridBuilder.Status status = new DataGridBuilder.Status()
            DataGrid smGrid = builder.build(status)
            smGrids.add(smGrid)
            smStatuses.add(status)
    
            println('GRID ' + entity +
                ' size=' + smGrid.size() +
                ' accepted=' + status.numAcceptedCells +
                ' rejected=' + status.numRejectedCells +
                ' rejectionDetails=' + status.cellsRejected)
    
            Code section 7// [7] Validate the destination grid before any model is saved.
            if (smGrid.size() != 14 ||
                status.numAcceptedCells != 14 ||
                status.numRejectedCells != 0) {
                throw new IllegalStateException(
                    'Destination grid validation failed for ' + entity +
                    '; NO SM GRIDS SAVED')
            }
        }
    
        if (smGrids.size() != 4) {
            throw new IllegalStateException(
                'Not all four destination grids were built')
        }
        println('ALL FOUR DESTINATION GRIDS VALIDATED; BEGINNING SAVES')
        println('WARNING: Four saves are independent, NOT atomic')
    
        Code section 8// [8] Save the four models separately after all validations pass.
        // PHASE 3: Save only after every grid has passed validation.
        // A failure stops subsequent saves; previous saves persist.
        for (int e = 0; e < entities.length; e++) {
            println('SAVING ' + entities[e])
            smModels[e].saveGrid(smGrids[e])
            println('SAVE COMPLETED ' + entities[e])
        }
    
        println('ALL FOUR SM SAVES COMPLETED')
        println('TOTAL DESTINATION CELLS SUBMITTED: 56')
    } finally {
        Code section 9// [9] Close grids and models even when execution fails.
        for (int i = smGrids.size() - 1; i >= 0; i--) {
            smGrids[i].close()
        }
        for (int i = smModels.size() - 1; i >= 0; i--) {
            smModels[i].close()
        }
    }
    println('=== SM - Push Bottom-Up Plans for Multiple Entities FINISHED ===')
    

    Code section 1 Defining the Source and Target Configuration The rule identifies four regional Planning entities and their corresponding Strategic Modeling models. It retrieves the starting Planning year from the OEP_PlanStartYr substitution variable and derives the following Planning year. The rule converts both fiscal years to the four-digit years used by Strategic Modeling.

    Code section 2 Defining the Account Mappings The rule defines nine Planning source accounts and seven Strategic Modeling destination accounts. Some Planning accounts are combined before being transferred to a single destination account.

    Code section 3 Retrieving Planning Data The rule accesses the OEP_FS cube and retrieves YearTotal data from the Strategic Planning scenario and Working version. It reads the nine source accounts for both years across all four entities.

    Code section 4 Validating and Preparing Source Values The rule checks for missing, duplicate, or unexpected source cells. It combines the appropriate account values, verifies cent precision, and prepares 56 destination values in underlying USD.

    Code section 5 Preparing the Strategic Modeling Models The rule opens each regional Strategic Modeling model and prepares a data grid for its Forecast scenario. All four models are prepared before the rule begins saving changes.

    Code section 6 Populating the Destination Grids For each model, the rule assigns the seven mapped account values to Forecast 2027 and 2028. Each destination grid contains 14 values.

    Code section 7 Validating the Destination Data The rule verifies that each destination grid contains 14 accepted values and no rejected values. If any grid fails validation, the rule stops before saving any model.

    Code section 8 Saving the Strategic Modeling Models After all four grids pass validation, the rule saves each model separately. These saves are not atomic: if a later save fails, previously saved models are not automatically rolled back.

    Code section 9 Closing Resources and Reporting Results The rule closes the destination grids and models, including when an error occurs. Diagnostic messages report the save results and the total number of destination cells submitted.

  6. On the toolbar, click Save (Save) to save the script. Click OK to dismiss the save confirmation.

    Confirmation that the Groovy rule was saved
  7. Click Validate and Deploy (Validate and Deploy). Click OK at the confirmation message.

    Validate and Deploy confirmation

Adding the Rule to the Testing Form

  1. Leave Calculation Manager open and return to the Planning browser window.

  2. In the top navigation, select the Push Bottom Up Plan Multi tab.

    Push Bottom Up Plan Multi navigation and form
  3. In the Push Bottom Up Plan Multiple Entities form, click Actions, then select Edit.

    Actions menu showing Edit
  4. In the Edit Form page, select the Business Rules tab.

  5. Under Business Rules, select SM - Push Bottom-Up Plans for Multiple Entities, then click Add to move it to Selected Business Rules.

    Rule 3 selected in the form Business Rules tab
  6. Click Save (Save) to save the form.

    Form save confirmation
  7. Click OK to confirm the changes.

  8. At the bottom of the page, close the Edit Push Bottom Up Plan Multiple Entities tab to return to the data form.

Testing the Groovy Rule

  1. In the Push Bottom Up Plan Multiple Entities form, click Actions, then select Rules & Smart Push.

    Actions menu showing Rules and Smart Push
  2. Select SM - Push Bottom-Up Plans for Multiple Entities to execute the Groovy rule.

    Rule 3 selected in Rules and Smart Push menu
  3. When the information message confirms that the rule completed successfully, click OK.

    Successful Rule 3 execution message
  4. At the bottom of the page, select the US Operations East tab to open the Operations US East model.

    Operations US East Forecast 2027 and 2028 highlighted values
  5. Review the seven highlighted Forecast account values for 2027 and 2028 in the Operations US East model.

  6. Repeat the verification for the US Operations West, US Operations North, and US Operations South models.

    Operations US West Forecast values for both years
    Operations US North Forecast values for both years
    Operations US South Forecast values for both years

Pushing Multiple Strategic Models to Multiple Planning Entities

In this section, you create a Groovy rule to push forecast data from four regional Strategic Modeling models for 2027 and 2028 to the corresponding Planning Target entities for FY27 and FY28. You then deploy the rule, associate it with the Planning form, and execute it to verify that the forecast data is reflected in the Planning targets.

Understanding the Source and Target Intersections

The Groovy rule retrieves annual Forecast data from four regional Strategic Modeling models and pushes the values to the corresponding Planning Target entities in the OEP_FS cube. The rule processes Forecast 2027 and 2028 in the regional strategic models, allocating the annual values across the 12 months of FY27 and FY28 in the corresponding Planning entities.

DimensionStrategic Modeling SourcePlanning Target
Model / EntityOperations US East, West, North, SouthCorresponding regional entities
ScenarioForecastStrategic Planning
Version—Target
Years2027, 2028FY27, FY28
PeriodAnnual forecast valuesJan–Dec
AccountsSeven Strategic Modeling source accounts13 writable Planning target accounts

The rule maps the Strategic Modeling accounts to the following Planning accounts.

Strategic Modeling Source AccountPlanning Target Account
v1000:010 Product SalesOFS_Product Revenue
v1000:020 Service RevenuesOFS_Services Revenue
v1000:030 Other RevenuesOFS_Support Revenue, OFS_Other Revenue
v1020 Discounts & ReturnsOFS_Discounts, OFS_Returns
v1040 Cost of Goods SoldOFS_Labor Expense, OFS_Material, OFS_Overhead, OFS_Freight, OFS_Other Cost of sales
v1080:010 Salary ExpenseOFS_Salaries
v1080:020 Selling ExpenseOFS_Selling Expense

The rule reads seven Strategic Modeling accounts for each model and year combination and allocates their annual values to 13 writable Planning accounts. Across four entities, two years, and 12 months, the rule updates 1,248 monthly Planning Target cells.

Note:

Strategic Modeling displays financial values in millions, but the Groovy rule retrieves the underlying USD amounts without applying a scaling factor. The rule uses approved regional and yearly Planning baseline weights to distribute values among target accounts, then allocates each account’s annual amount across the 12 months. A largest-remainder calculation reconciles rounding differences so the monthly values total the allocated annual amount to the cent.

Creating the Groovy Rule

  1. Leave the Planning browser window open and return to Calculation Manager. Select the System View tab.

  2. Under OEP_FS, right-click Rules, then select New.

    Rules context menu showing New
  3. For Name, enter SM - Push Multiple Strategic Models to Multiple Planning Entities, then click OK.

    New Rule dialog showing the rule name
  4. In the Rule Editor, change the Designer option to Edit Script and set Script Type to Groovy Script.

    Rule editor configured for Groovy Script
  5. Copy this script and paste it into the editor.

    /*
     * Pushing Multiple Strategic Models to Multiple Planning Entities
     *
     * Reads annual Forecast values from four regional Strategic Modeling
     * models for two years, then allocates those values to 13 writable
     * monthly Planning Target accounts in each corresponding entity.
     *
     * The OEP_PlanStartYr substitution variable determines the first
     * Planning fiscal year; the rule derives the next fiscal year and
     * corresponding four-digit Strategic Modeling forecast years.
     *
     * Regional/year-specific allocation weights are approved for FY27
     * and FY28 only. Unsupported year pairs stop before any writes.
     * The largest-remainder method preserves cent-level totals.
     *
     * All eight destination grids must pass validation before saving.
     * The eight Planning saves are independent, not atomic.
     */
    import java.math.RoundingMode
    
    def entities = ['Operations US East','Operations US West','Operations US North','Operations US South']
    Code section 1// [1] Derive source and target years from the Planning substitution variable.
    Cube cube = operation.application.getCube('OEP_FS')
    String planningStartYear = cube.getSubstitutionVariableValue('OEP_PlanStartYr')
    if (planningStartYear == null || !(planningStartYear ==~ /FY[0-9]{2}/)) {
        throw new IllegalStateException('Invalid OEP_PlanStartYr: ' + planningStartYear)
    }
    int startYear = 2000 + Integer.parseInt(planningStartYear.substring(2))
    String nextPlanningYear = 'FY' + String.format('%02d', (startYear + 1) % 100)
    List<List<String>> yearPairs = [
        [startYear.toString(), planningStartYear],
        [(startYear + 1).toString(), nextPlanningYear]
    ]
    def smAccounts = ['v1000:010','v1000:020','v1000:030','v1020','v1040','v1080:010','v1080:020']
    def months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
    
    // Regional FY27/FY28 approved Planning monthly baselines: weights only, not source assertions.
    Code section 2// [2] Define the approved regional/yearly allocation weights.
    def weights = [
     'Operations US East': [
      'FY27': [
       'v1000:010':['OFS_Product Revenue':35000],
       'v1000:020':['OFS_Services Revenue':5000],
       'v1000:030':['OFS_Support Revenue':3000,'OFS_Other Revenue':2000],
       'v1020':['OFS_Discounts':1500,'OFS_Returns':500],
       'v1040':['OFS_Labor Expense':2000,'OFS_Material':14000,'OFS_Overhead':2000,'OFS_Freight':1000,'OFS_Other Cost of sales':3000],
       'v1080:010':['OFS_Salaries':4000],
       'v1080:020':['OFS_Selling Expense':1500]
      ],
      'FY28': [
       'v1000:010':['OFS_Product Revenue':38000],
       'v1000:020':['OFS_Services Revenue':5500],
       'v1000:030':['OFS_Support Revenue':3200,'OFS_Other Revenue':2100],
       'v1020':['OFS_Discounts':1600,'OFS_Returns':500],
       'v1040':['OFS_Labor Expense':2200,'OFS_Material':15000,'OFS_Overhead':2100,'OFS_Freight':1100,'OFS_Other Cost of sales':3200],
       'v1080:010':['OFS_Salaries':4200],
       'v1080:020':['OFS_Selling Expense':1600]
      ]
     ],
     'Operations US West': [
      'FY27': [
       'v1000:010':['OFS_Product Revenue':30000],
       'v1000:020':['OFS_Services Revenue':4000],
       'v1000:030':['OFS_Support Revenue':2500,'OFS_Other Revenue':1500],
       'v1020':['OFS_Discounts':1300,'OFS_Returns':500],
       'v1040':['OFS_Labor Expense':1800,'OFS_Material':12000,'OFS_Overhead':1500,'OFS_Freight':900,'OFS_Other Cost of sales':2800],
       'v1080:010':['OFS_Salaries':3500],
       'v1080:020':['OFS_Selling Expense':1200]
      ],
      'FY28': [
       'v1000:010':['OFS_Product Revenue':33000],
       'v1000:020':['OFS_Services Revenue':4500],
       'v1000:030':['OFS_Support Revenue':2700,'OFS_Other Revenue':1600],
       'v1020':['OFS_Discounts':1400,'OFS_Returns':500],
       'v1040':['OFS_Labor Expense':2000,'OFS_Material':13000,'OFS_Overhead':1600,'OFS_Freight':1000,'OFS_Other Cost of sales':3000],
       'v1080:010':['OFS_Salaries':3700],
       'v1080:020':['OFS_Selling Expense':1300]
      ]
     ],
     'Operations US North': [
      'FY27': [
       'v1000:010':['OFS_Product Revenue':25000],
       'v1000:020':['OFS_Services Revenue':3000],
       'v1000:030':['OFS_Support Revenue':2000,'OFS_Other Revenue':1000],
       'v1020':['OFS_Discounts':1100,'OFS_Returns':400],
       'v1040':['OFS_Labor Expense':1500,'OFS_Material':10000,'OFS_Overhead':1500,'OFS_Freight':800,'OFS_Other Cost of sales':2200],
       'v1080:010':['OFS_Salaries':3000],
       'v1080:020':['OFS_Selling Expense':1000]
      ],
      'FY28': [
       'v1000:010':['OFS_Product Revenue':27000],
       'v1000:020':['OFS_Services Revenue':3300],
       'v1000:030':['OFS_Support Revenue':2200,'OFS_Other Revenue':1100],
       'v1020':['OFS_Discounts':1200,'OFS_Returns':400],
       'v1040':['OFS_Labor Expense':1600,'OFS_Material':11000,'OFS_Overhead':1600,'OFS_Freight':900,'OFS_Other Cost of sales':2400],
       'v1080:010':['OFS_Salaries':3200],
       'v1080:020':['OFS_Selling Expense':1100]
      ]
     ],
     'Operations US South': [
      'FY27': [
       'v1000:010':['OFS_Product Revenue':20000],
       'v1000:020':['OFS_Services Revenue':2000],
       'v1000:030':['OFS_Support Revenue':1500,'OFS_Other Revenue':1000],
       'v1020':['OFS_Discounts':900,'OFS_Returns':300],
       'v1040':['OFS_Labor Expense':1200,'OFS_Material':8000,'OFS_Overhead':1200,'OFS_Freight':600,'OFS_Other Cost of sales':2000],
       'v1080:010':['OFS_Salaries':2500],
       'v1080:020':['OFS_Selling Expense':800]
      ],
      'FY28': [
       'v1000:010':['OFS_Product Revenue':22000],
       'v1000:020':['OFS_Services Revenue':2200],
       'v1000:030':['OFS_Support Revenue':1600,'OFS_Other Revenue':1100],
       'v1020':['OFS_Discounts':1000,'OFS_Returns':300],
       'v1040':['OFS_Labor Expense':1300,'OFS_Material':8800,'OFS_Overhead':1300,'OFS_Freight':700,'OFS_Other Cost of sales':2100],
       'v1080:010':['OFS_Salaries':2700],
       'v1080:020':['OFS_Selling Expense':900]
      ]
     ]
    ]
    
    Code section 3// [3] Reject unsupported years or incomplete allocation configurations before reading data.
    for (String entity : entities) {
        for (List<String> pair : yearPairs) {
            String fiscalYear = pair[1]
            if (!weights.containsKey(entity) || !weights[entity].containsKey(fiscalYear)) {
                throw new IllegalStateException('No approved allocation weights for ' +
                    entity + '/' + fiscalYear + '; no data has been written')
            }
            for (String account : smAccounts) {
                def splits = weights[entity][fiscalYear][account]
                if (splits == null || splits.isEmpty()) {
                    throw new IllegalStateException('Missing approved allocation weights for ' +
                        entity + '/' + fiscalYear + '/' + account + '; no data has been written')
                }
            }
        }
    }
    
    // Integer cents: largest remainder; ties go to original destination order.
    // Works with negative amounts by allocating magnitude then restoring sign.
    Code section 4// [4] Allocate amounts using integer cents and largest remainders.
    def allocate = { long totalCents, List<Long> weightValues ->
        if (weightValues.isEmpty()) throw new IllegalArgumentException('Empty allocation weights')
        long denominator = 0L
        for (Long weight : weightValues) {
            if (weight.longValue() < 0L) throw new IllegalArgumentException('Negative allocation weight')
            denominator += weight.longValue()
        }
        if (denominator <= 0L) throw new IllegalArgumentException('Zero allocation denominator')
        if (totalCents == Long.MIN_VALUE) throw new IllegalArgumentException('Amount out of range')
        long magnitude = Math.abs(totalCents)
        int count = weightValues.size()
        long[] amounts = new long[count]
        BigInteger[] remainders = new BigInteger[count]
        BigInteger divisor = BigInteger.valueOf(denominator)
        long assigned = 0L
        for (int i = 0; i < count; i++) {
            BigInteger numerator = BigInteger.valueOf(magnitude).multiply(BigInteger.valueOf(weightValues.get(i).longValue()))
            amounts[i] = numerator.divide(divisor).longValueExact()
            remainders[i] = numerator.remainder(divisor)
            assigned += amounts[i]
        }
        long leftover = magnitude - assigned
        // Select the highest remainder, using the lowest index to break ties.
        // Avoid untyped map sorting under Oracle Groovy static type checking.
        boolean[] selected = new boolean[count]
        for (int j = 0; j < (int) leftover; j++) {
            int best = -1
            for (int i = 0; i < count; i++) {
                if (!selected[i] && (best < 0 || remainders[i].compareTo(remainders[best]) > 0)) {
                    best = i
                }
            }
            if (best < 0) throw new IllegalStateException('Allocation remainder selection failed')
            amounts[best] = amounts[best] + 1L
            selected[best] = true
        }
        List<Long> result = []
        for (int i = 0; i < count; i++) {
            result.add(totalCents < 0L ? -amounts[i] : amounts[i])
        }
        return result
    }
    
    Code section 5// [5] Convert source amounts to cents and destination amounts to USD.
    def toCents = { Object raw ->
        if (!(raw instanceof Number)) throw new IllegalStateException('Non-numeric SM value: ' + raw)
        BigDecimal amount = new BigDecimal(raw.toString())
        return amount.movePointRight(2).setScale(0,RoundingMode.HALF_UP).longValueExact()
    }
    def toDollars = { long cents -> BigDecimal.valueOf(cents,2) }
    
    
    // This is an executing integration rule, not a read-only diagnostic.
    // Preserve a recovery snapshot before execution. All eight grids are validated
    // before the first save, but saves are not atomic.
    
    String[] targetAccounts = [
        'OFS_Product Revenue','OFS_Services Revenue','OFS_Support Revenue',
        'OFS_Other Revenue','OFS_Discounts','OFS_Returns','OFS_Labor Expense',
        'OFS_Material','OFS_Overhead','OFS_Freight','OFS_Other Cost of sales',
        'OFS_Salaries','OFS_Selling Expense'
    ]
    List<DataGridBuilder> builders = []
    List<String> gridNames = []
    int expectedIntersections = 0
    int processed = 0
    
    // PHASE 1: Read all SM sources, calculate allocations, construct eight builders.
    // No Planning save operations occur in this phase.
    Code section 6// [6] Read and validate Strategic Modeling Forecast source cells.
    entities.each { String entity ->
        operation.application.getStrategicModel(entity).withCloseable { smModel ->
            DataGridDefinitionBuilder definition = smModel.dataGridDefinitionBuilder()
            String forecastScenario = 'Forecast'
            definition.addPov(['Scenario'], [[forecastScenario]])
            definition.addColumn(['Time'], [yearPairs.collect { it[0] }])
            definition.addRow(['Account'], [smAccounts])
            smModel.loadGrid(definition.build()).withCloseable { smGrid ->
                def source = [:]
                smGrid.dataCellIterator().each { cell ->
                    String account = cell.accountName
                    String year = cell.periodName
                    if (!smAccounts.contains(account) || !(year in yearPairs.collect { it[0] }))
                        throw new IllegalStateException('Unexpected SM intersection ' + entity + ': ' + account + '/' + year)
                    String key = account + '|' + year
                    if (source.containsKey(key))
                        throw new IllegalStateException('Duplicate SM source ' + entity + ': ' + key)
                    if (cell.missing)
                        throw new IllegalStateException('Missing SM source ' + entity + ': ' + key)
                    source[key] = toCents(cell.data)
                }
                yearPairs.each { pair ->
                    String smYear = pair[0]
                    String fy = pair[1]
                    Code section 7// [7] Allocate annual source values to target accounts and months.
                    def monthly = [:]
                    smAccounts.each { String account ->
                        String key = account + '|' + smYear
                        if (!source.containsKey(key))
                            throw new IllegalStateException('Missing source ' + entity + ': ' + key)
                        def splits = weights[entity][fy][account]
                        if (splits == null)
                            throw new IllegalStateException('Missing weights ' + entity + '/' + fy + '/' + account)
                        def targets = splits.keySet().toList()
                        def allocated = allocate(source[key] as long,
                            splits.values().collect { (it as Number).longValue() })
                        targets.eachWithIndex { String target, int i ->
                            if (monthly.containsKey(target))
                                throw new IllegalStateException('Duplicate target ' + target)
                            monthly[target] = allocate(allocated[i] as long, (1..12).collect { 1L })
                        }
                        if (allocated.sum(0L) != source[key])
                            throw new IllegalStateException('Group mismatch ' + entity + '/' + fy + '/' + account)
                    }
                    if (monthly.size() != 13)
                        throw new IllegalStateException('Expected 13 target accounts for ' + entity + '/' + fy)
    
                    Code section 8// [8] Prepare a Planning Target grid for each entity and year.
                    DataGridBuilder builder = cube.dataGridBuilder('MM/DD/YYYY')
                    builder.addPov(fy,'OEP_Strategic Planning','USD','Target',
                        entity,'OFS_Direct Input','Smart Phone 6 in','US Market')
                    builder.addColumn(months as String[])
                    long groupTotalCents = 0L
                    int groupCells = 0
                    for (String target : targetAccounts) {
                        if (!monthly.containsKey(target))
                            throw new IllegalStateException('Missing destination ' + entity + '/' + fy + '/' + target)
                        List<Long> values = (List<Long>) monthly.get(target)
                        if (values.size() != 12)
                            throw new IllegalStateException('Expected 12 months ' + target)
                        List<Object> rowValues = []
                        long annualCents = 0L
                        for (int m = 0; m < 12; m++) {
                            long cents = values.get(m).longValue()
                            annualCents = Math.addExact(annualCents, cents)
                            rowValues.add(toDollars(cents).doubleValue())
                            groupCells++
                        }
                        groupTotalCents = Math.addExact(groupTotalCents, annualCents)
                        builder.addRow([target], rowValues)
                    }
                    long sourceTotalCents = 0L
                    for (String account : smAccounts) {
                        sourceTotalCents = Math.addExact(sourceTotalCents,
                            ((Number) source.get(account + '|' + smYear)).longValue())
                    }
                    if (groupTotalCents != sourceTotalCents || groupCells != 156)
                        throw new IllegalStateException('Allocation mismatch ' + entity + '/' + fy)
                    builders.add(builder)
                    gridNames.add(entity + '/' + fy)
                    expectedIntersections += groupCells
                    processed++
                    println('PREPARED ' + entity + '/' + fy + ' sourceUSD=' +
                        toDollars(sourceTotalCents).toString() + ' cells=156')
                }
            }
        }
    }
    if (processed != 8 || expectedIntersections != 1248 || builders.size() != 8)
        throw new IllegalStateException('Unexpected coverage: groups=' + processed +
            ' cells=' + expectedIntersections)
    
    // PHASE 2: Build ALL eight Planning grids and reject the whole test if any
    // builder fails. No save happens until every grid has passed validation.
    Code section 9// [9] Validate all eight Planning destination grids before saving.
    List<DataGrid> builtGrids = []
    boolean allValidated = false
    try {
        for (int g = 0; g < builders.size(); g++) {
            DataGridBuilder.Status status = new DataGridBuilder.Status()
            DataGrid grid = builders.get(g).build(status)
            builtGrids.add(grid)
            println('GRID ' + gridNames.get(g) + ' size=' + grid.size() +
                ' accepted=' + status.numAcceptedCells +
                ' rejected=' + status.numRejectedCells)
            if (grid.size() != 156 ||
                status.numAcceptedCells != 156 ||
                status.numRejectedCells != 0) {
                println('REJECTED ' + gridNames.get(g) + ' details=' + status.cellsRejected)
                throw new IllegalStateException('Grid validation failed; ZERO saves attempted')
            }
        }
        if (builtGrids.size() != 8)
            throw new IllegalStateException('Eight validated grids required')
        allValidated = true
        println('ALL EIGHT PRE-SAVE GRID VALIDATIONS PASSED: 1248 cells')
    
        // Non-atomic saves. A failure can leave earlier grids committed.
        // STOP on error; do not blindly rerun. Restore from recovery snapshot
        // if required, then independently read back all 1248 cells.
        Code section 10// [10] Save each validated grid separately; saves are not atomic.
        int completedSaves = 0
        for (int g = 0; g < builtGrids.size(); g++) {
            println('SAVE START ' + gridNames.get(g))
            cube.saveGrid(builtGrids.get(g))
            completedSaves++
            println('SAVE COMPLETED ' + gridNames.get(g))
        }
        println('SM - Push Multiple Strategic Models to Multiple Planning Entities SAVE PHASE COMPLETE: ' + completedSaves +
            '/8 grids submitted; independent readback still REQUIRED.')
    } finally {
        Code section 11// [11] Close all destination grids and report completion.
        for (DataGrid grid : builtGrids) {
            grid.close()
        }
    }
    println('SM - Push Multiple Strategic Models to Multiple Planning Entities FINISHED. AllValidated=' + allValidated +
        '; independent readback still REQUIRED.')
    

    Code section 1 Defining the Source and Target Years The rule retrieves the starting Planning year from the OEP_PlanStartYr substitution variable and derives the following fiscal year. It converts both fiscal years into the four-digit years used by Strategic Modeling. For example, when the variable is FY27, the rule reads Forecast 2027 and 2028 and updates Planning Target FY27 and FY28.

    Code section 2 Defining Regional Allocation Weights The rule defines approved allocation weights for each regional entity and fiscal year. These weights determine how annual Strategic Modeling amounts are distributed among the corresponding writable Planning accounts. The weights represent relative proportions, not fixed target amounts.

    Code section 3 Validating the Allocation Configuration Before reading source data or writing Planning values, the rule checks that approved allocation weights exist for every required entity, year, and source account. If the substitution variable identifies unsupported years or a required configuration is missing, the rule stops without writing data.

    Code section 4 Allocating Values Using Largest Remainders The rule allocates amounts using integer cents to avoid rounding discrepancies. It first calculates each destination’s proportional share, then distributes any remaining cents to destinations with the largest fractional remainders. This ensures that the allocated amounts reconcile to the original total.

    Code section 5 Converting Financial Values The rule converts Strategic Modeling source amounts into integer cents for allocation and converts the calculated amounts back to USD for Planning. Strategic Modeling values are retrieved in underlying USD, so no million-unit scaling is applied.

    Code section 6 Retrieving Strategic Modeling Forecast Data The rule opens each of the four regional Strategic Modeling models and retrieves the seven required Forecast accounts for 2027 and 2028. It checks for missing, duplicate, or unexpected source intersections before preparing the Planning allocations.

    Code section 7 Allocating Annual Values to Accounts and Months For each model and year, the rule uses the approved weights to distribute annual Forecast amounts among 13 writable Planning accounts. It then distributes each account’s annual allocation across January through December, reconciling any remaining cents so the monthly amounts equal the annual allocation.

    Code section 8 Preparing Planning Target Grids The rule creates a Planning data grid for each regional entity and fiscal year. Each grid contains 13 writable accounts across 12 months in the Strategic Planning scenario and Target version, resulting in 156 destination cells per grid.

    Code section 9 Validating All Destination Grids The rule validates all eight Planning destination grids before saving any changes. It checks that each grid contains the expected 156 cells and that no destination values were rejected. If validation fails, the rule stops before the save phase.

    Code section 10 Saving the Planning Target Data After all eight grids pass validation, the rule saves each entity-year grid separately. The eight saves are not atomic, so a failure during a later save does not automatically undo previously saved values.

    Code section 11 Closing Resources and Reporting Results The rule closes the Planning data grids after processing, including when an error occurs. Diagnostic messages report the completed saves and execution status, helping confirm whether the integration finished successfully.

  6. On the toolbar, click Save (Save) to save the script. Click OK to dismiss the save confirmation.

    Confirmation that the Groovy rule was saved
  7. Click Validate and Deploy (Validate and Deploy). Click OK at the confirmation message.

    Validate and Deploy confirmation

Adding the Rule to the Testing Form

  1. Leave Calculation Manager open and return to the Planning browser window.

  2. In the top navigation, select the Push Targets Multi Entity tab.

    Top Push Targets Multi Entity tab selected
  3. At the bottom of the page, select the Push Targets Multi Entity tab to open the Planning data form.

    Bottom Push Targets Multi Entity tab and YearTotal form without Depreciation
  4. In the Push Targets Multiple Entities form, click Actions, then select Edit.

    Actions menu showing Edit
  5. In the Edit Form page, select the Business Rules tab.

  6. Under Business Rules, select SM - Push Multiple Strategic Models to Multiple Planning Entities, then click Add to move it to Selected Business Rules.

    Rule 4 selected in the form Business Rules tab
  7. Click Save (Save) to save the form.

    Form save confirmation
  8. Click OK to confirm the changes.

  9. At the bottom of the page, close the Edit Push Targets Multiple Entities tab to return to the data form.

Testing the Groovy Rule

  1. In the Push Targets Multiple Entities form, click Actions, then select Rules & Smart Push.

    Actions menu showing Rules and Smart Push
  2. Select SM - Push Multiple Strategic Models to Multiple Planning Entities to execute the Groovy rule.

    Rule 4 selected in Rules and Smart Push menu
  3. When the information message confirms that the rule completed successfully, click OK.

    Successful Rule 4 execution message
  4. Review the automatically refreshed Push Targets Multiple Entities form to verify the YearTotal values for FY27 and FY28 across the four regional entities. Review the transferred revenue, cost, and operating expense values, including the calculated Adjustments to Revenue and Total Cost Of Sales amounts.

    Planning Target YearTotal values for FY27 and FY28 highlighted across four entities

More Learning Resources

Explore other labs on docs.oracle.com/learn or access more free learning content on the Oracle Learning YouTube channel. Additionally, visit education.oracle.com/learning-explorer to become an Oracle Learning Explorer.

For product documentation, visit Oracle Help Center.