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:
- Transfer consolidated FY27 Planning Working data for Operations US to the Strategic Modeling Forecast.
- Transfer Strategic Modeling Forecast data for Operations US East for 2027 and 2028 to Planning Target for FY27 and FY28.
- Transfer Planning Working data for four regional entities to their corresponding Strategic Modeling Forecasts.
- 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.
- On the Home page, click Data.
- Scroll to the bottom of the list and click the Strategic Modeling folder to view Strategic Modeling forms.
- Click Enter Data Form Working to open the form.

- In the Entity dropdown, select Operations US East.

- For Operations US East, enter the following values in the FY27 Dec and FY28 Dec columns:
Account FY27 Dec FY28 Dec OFS_Product Revenue 4000043000OFS_Services Revenue 60006500OFS_Material 1600017000OFS_Salaries 40004700OFS_Selling Expense 15001800
- Click Save to save the updated Planning data for Operations US East.

- In the Information dialog, click OK to close the message confirming that the data has been saved.

- In the Entity dropdown, select Operations US West.

- For Operations US West, enter the following values in the FY27 Dec and FY28 Dec columns:
Account FY27 Dec FY28 Dec OFS_Product Revenue 3300036000OFS_Services Revenue 50005500OFS_Material 1350014500OFS_Salaries 35004200OFS_Selling Expense 12001500
- Click Save to save the updated Planning data for Operations US West.

- 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.
- At the bottom of the page, select the Data tab to return to the list of forms.
- Click Enter Data Form Target to open the form.

- Verify that Operations US West and FY27 are selected.
- In the Jan column, clear the value for OFS_Product Revenue.
- 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.

Note:
Releasing the cursor applies the cleared value to all writable cells in the selected range. Read-only calculated cells are not modified.
- Click Save to save the cleared Target data.

- 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.
- Repeat the preceding clearing procedure for the following entity and year combinations:
Entity Year Operations US West FY28 Operations US East FY27 Operations US East FY28 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.

- 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.
Selecting a Navigation Flow in Planning
Activate the existing SM navigation flow to access the integration cards.
- At the top of the page, click
(Home) to return to the Planning Home page. - Click Strategic Modeling and review the navigation cards displayed by the default navigation flow.

- Click Strategic Modeling again to close the card. Click Tools, and then click Navigation Flows.
- Verify that the Default navigation flow is Active and the SM navigation flow is Inactive.

- In the Navigation Flow list, click Inactive in the Active column for the SM navigation flow.
- Verify that the SM navigation flow displays Active and the Default navigation flow displays Inactive.

- Return to the Planning Home page.
- In the upper-right corner, click the user icon to open the Settings and Actions menu, then select Reload Navigation Flow.

- Click Strategic Modeling to verify that the new navigation cards are displayed.

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 US | Model: Operations US |
| Scenario: OEP_Strategic Planning | Scenario/Version: Forecast |
| Market: US Market | |
| Plan Element: OFS_Direct Input | |
| Currency: USD | |
| Product: Smart Phone 6 in | |
| Year: FY27 | Period: 2027 |
| Period: YearTotal | |
| Version: OEP_Working | |
| Source Account | Target Account |
| OFS_Product Revenue | v1000:010 — Product Sales |
| OFS_Services Revenue | v1000:020 — Service Revenue |
| OFS_Support Revenue + OFS_Other Revenue | v1000:030 — Other Revenues |
| OFS_Adjustments to Revenue | v1020 — Discounts & Returns |
| OFS_Total Cost Of Sales | v1040 — 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
Click
(Navigator), then, under Create and Manage, select Rules to open Calculation Manager.
On the System View tab, expand EPM Cloud, then HP4, then OEP_FS. Right-click Rules and select New.

For Name, enter
SM - Sync Bottom-Up Plan with Strategic Forecast, then click OK.
In the Rule Editor, change the Designer option to Edit Script and set Script Type to Groovy Script.

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
// 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)
// 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)
// 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())
}
// 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')
// 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')
// 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')
Identify the Planning Cube and Forecast YearThe 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.
Define the Source and Destination AccountsThe 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.
Retrieve and Validate Planning Source ValuesThe 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.
Map Planning Values to Strategic Modeling AccountsThe script prepares five destination values, combining Support Revenue and Other Revenue. It prints the prepared amounts in underlying USD to the execution log.
Build and Validate the Destination GridThe 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.
Save Data and Report CompletionAfter validation, the script saves the grid, reports accepted and rejected cells, and reminds learners to verify the five persisted values independently in Strategic Modeling.
Click
(Save) to save the rule. At the information message, click OK.
Click
(Validate and Deploy) to validate and deploy the rule. At the information message, click OK.
Adding the Rule to the Testing Form
Leave the Calculation Manager window open and return to the Planning browser window.
From the Home page, click Strategic Modeling, then click Sync Bottom Up Plan.

Click Actions, then select Edit. A new dynamic tab opens to edit the data form.

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

Click Save. At the information message, click OK.

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
Click Actions, then select Rules & Smart Push.

Under Rules, select SM - Sync Bottom-Up Plan with Strategic Forecast to launch the rule.

When the rule completes, click OK to dismiss the information message.

At the bottom of the page, select the Strategic Plan tab. In the
2027column, verify that the data was pushed to the expected accounts. For example,OFS_Product Revenuewas pushed tov1000:010Product Sales, which displays1.328million USD.
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 East | Entity: Operations US East |
| Scenario/Version: Forecast | Scenario: OEP_Strategic Planning |
| Years: 2027, 2028 | Years: FY27, FY28 |
| Time: Annual | Periods: 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 Source | Planning Target Account |
|---|---|
| v1000:010 Product Sales | OFS_Product Revenue |
| v1000:020 Service Revenues | OFS_Services Revenue |
| v1000:030 Other Revenues | OFS_Support Revenue; OFS_Other Revenue |
| v1020 Discounts & Returns | OFS_Discounts; OFS_Returns |
| v1040 Cost of Goods Sold | OFS_Labor Expense; OFS_Material; OFS_Overhead; OFS_Freight; OFS_Other Cost of sales |
| v1080:010 Salary Expense | OFS_Salaries |
| v1080:020 Selling Expense | OFS_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
Return to the Calculation Manager window and select the System View tab.
Under OEP_FS, right-click Rules, then select New.

For Name, enter
SM - Adjust Targets in Bottom-Up Plan, then click OK.
In the Rule Editor, change the Designer option to Edit Script and set Script Type to Groovy Script.

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. */
// 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
]
]
// 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()
}
// 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]
)
}
}
}
// 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")
// 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
)
}
// 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")
Define the Account Mappings and Allocation Weights — The script identifies seven source accounts and 13 Planning Target accounts. The sourceIndexarray maps target accounts to their sources. Year-specific baseline weights determine the proportional allocation; they are not the values written directly to Planning.
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.
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.
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.
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.
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.Click
(Save) to save the rule. At the information message, click OK.
Click
(Validate and Deploy) to validate and deploy the rule. At the information message, click OK.
Adding the Rule to the Testing Form
Leave Calculation Manager open and return to the Planning browser window.
At the top of the page, select the Push Targets tab.

At the bottom of the page, select the Push Targets tab to open the data form.

Click Actions, then select Edit. A new dynamic tab opens to edit the data form.

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

Click Save. At the information message, click OK.

At the bottom of the page, close the Edit Push Targets One Entity tab to return to the data form.
Testing the Groovy Rule
Click Actions, then select Rules & Smart Push.

Under Rules, select SM - Adjust Targets in Bottom-Up Plan to launch the rule.

When the rule completes, click OK to dismiss the information message.

Verify that the rule populated the Planning Target values for Operations US East in FY27 and FY28. For example, OFS_Product Revenue displays
35000for FY27 and38000for FY28.
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.
| Dimension | Planning Source | Strategic Modeling Target |
|---|---|---|
| Entity / Model | Operations US East, West, North, South | Corresponding regional models |
| Scenario | Strategic Planning | Forecast |
| Version | Working | — |
| Years | FY27, FY28 | 2027, 2028 |
| Period | YearTotal | Annual forecast values |
| Accounts | Nine Planning source accounts | Seven Strategic Modeling destination accounts |
The rule maps the Planning accounts to the following Strategic Modeling accounts.
| Planning Source Account | Strategic Modeling Destination Account |
|---|---|
| OFS_Product Revenue | v1000:010 Product Sales |
| OFS_Services Revenue | v1000:020 Service Revenues |
| OFS_Support Revenue + OFS_Other Revenue | v1000:030 Other Revenues |
| OFS_Discounts + OFS_Returns | v1020 Discounts & Returns |
| OFS_Total Cost Of Sales | v1040 Cost of Goods Sold |
| OFS_Salaries | v1080:010 Salary Expense |
| OFS_Selling Expense | v1080: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
Leave the Planning browser window open and return to Calculation Manager. Select the System View tab.
Under OEP_FS, right-click Rules, then select New.

For Name, enter
SM - Push Bottom-Up Plans for Multiple Entities, then click OK.
In the Rule Editor, change the Designer option to Edit Script and set Script Type to Groovy Script.

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. */
// [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()]
// [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())
// [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)
// [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.
// [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)
// [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)
// [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')
// [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 {
// [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 ===')
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_PlanStartYrsubstitution variable and derives the following Planning year. The rule converts both fiscal years to the four-digit years used by Strategic Modeling.
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.
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.
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.
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.
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.
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.
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.
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.On the toolbar, click
(Save) to save the script. Click OK to dismiss the save confirmation.
Click
(Validate and Deploy). Click OK at the confirmation message.
Adding the Rule to the Testing Form
Leave Calculation Manager open and return to the Planning browser window.
In the top navigation, select the Push Bottom Up Plan Multi tab.

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

In the Edit Form page, select the Business Rules tab.
Under Business Rules, select SM - Push Bottom-Up Plans for Multiple Entities, then click Add to move it to Selected Business Rules.

Click
(Save) to save the form.
Click OK to confirm the changes.
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
In the Push Bottom Up Plan Multiple Entities form, click Actions, then select Rules & Smart Push.

Select SM - Push Bottom-Up Plans for Multiple Entities to execute the Groovy rule.

When the information message confirms that the rule completed successfully, click OK.

At the bottom of the page, select the US Operations East tab to open the Operations US East model.

Review the seven highlighted Forecast account values for 2027 and 2028 in the Operations US East model.
Repeat the verification for the US Operations West, US Operations North, and US Operations South models.



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.
| Dimension | Strategic Modeling Source | Planning Target |
|---|---|---|
| Model / Entity | Operations US East, West, North, South | Corresponding regional entities |
| Scenario | Forecast | Strategic Planning |
| Version | — | Target |
| Years | 2027, 2028 | FY27, FY28 |
| Period | Annual forecast values | Jan–Dec |
| Accounts | Seven Strategic Modeling source accounts | 13 writable Planning target accounts |
The rule maps the Strategic Modeling accounts to the following Planning accounts.
| Strategic Modeling Source Account | Planning Target Account |
|---|---|
| v1000:010 Product Sales | OFS_Product Revenue |
| v1000:020 Service Revenues | OFS_Services Revenue |
| v1000:030 Other Revenues | OFS_Support Revenue, OFS_Other Revenue |
| v1020 Discounts & Returns | OFS_Discounts, OFS_Returns |
| v1040 Cost of Goods Sold | OFS_Labor Expense, OFS_Material, OFS_Overhead, OFS_Freight, OFS_Other Cost of sales |
| v1080:010 Salary Expense | OFS_Salaries |
| v1080:020 Selling Expense | OFS_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
Leave the Planning browser window open and return to Calculation Manager. Select the System View tab.
Under OEP_FS, right-click Rules, then select New.

For Name, enter
SM - Push Multiple Strategic Models to Multiple Planning Entities, then click OK.
In the Rule Editor, change the Designer option to Edit Script and set Script Type to Groovy Script.

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']
// [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.
// [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]
]
]
]
// [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.
// [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
}
// [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.
// [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]
// [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)
// [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.
// [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.
// [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 {
// [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.')
Defining the Source and Target Years The rule retrieves the starting Planning year from the OEP_PlanStartYrsubstitution 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 isFY27, the rule reads Forecast 2027 and 2028 and updates Planning Target FY27 and FY28.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.On the toolbar, click
(Save) to save the script. Click OK to dismiss the save confirmation.
Click
(Validate and Deploy). Click OK at the confirmation message.
Adding the Rule to the Testing Form
Leave Calculation Manager open and return to the Planning browser window.
In the top navigation, select the Push Targets Multi Entity tab.

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

In the Push Targets Multiple Entities form, click Actions, then select Edit.

In the Edit Form page, select the Business Rules tab.
Under Business Rules, select SM - Push Multiple Strategic Models to Multiple Planning Entities, then click Add to move it to Selected Business Rules.

Click
(Save) to save the form.
Click OK to confirm the changes.
At the bottom of the page, close the Edit Push Targets Multiple Entities tab to return to the data form.
Testing the Groovy Rule
In the Push Targets Multiple Entities form, click Actions, then select Rules & Smart Push.

Select SM - Push Multiple Strategic Models to Multiple Planning Entities to execute the Groovy rule.

When the information message confirms that the rule completed successfully, click OK.

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.

Related Links
- Planning Tutorials — Select Implementing Groovy from the area-of-interest list to explore additional Groovy tutorials.
- Java API Reference for Groovy Rules — Explore the Groovy APIs available for developing Groovy rules and templates.
- Working with Strategic Modeling in Smart View — Explore Strategic Modeling models, forecasts, and related functionality.
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.
Integrating Data Between Planning and Strategic Models Using Groovy
F87765-02
November 2026
Copyright © 2026, Oracle and/or its affiliates.