Zoomed Image

Per-Product Scoping

Software Asset Management Guide
Customizing the Calculation

Per-Product Scoping

Some publishers license per organisational unit — Adobe traditionally licenses by department, some Oracle products by location. The default engine treats department as a soft preference via affinity scoring. This page explains the scoping pattern for promoting a dimension to a hard requirement for specific products only.

The Problem

The default rule:

Affinity Consumption.DepartmentID = License.DepartmentID, 3000

is global. It applies to every product. A naive change to:

Requirement Consumption.DepartmentID = License.DepartmentID

would also apply to every product, excluding every license that does not have a department set. That is too blunt.

You need: "for products marked as licensed-by-department, enforce the requirement; for everything else, behave normally."

The Pattern

The pattern uses calculated fields with a sentinel value. When the product is flagged, both sides resolve to the real DepartmentID and the requirement is enforced. When the product is not flagged, both sides collapse to -1 and the requirement is trivially satisfied (because -1 = -1 always).

Step 1: Add a Flag to the Catalog

Add a field on Software Catalog entries called LicenseByDepartment (integer, defaults to 0). Set it to 1 for products that must be department-bound.

This is a custom spec field — see the Configuration Guide: Custom Spec Fields for how to add it. Add it to the form so administrators can tick it on individual catalog entries.

Step 2: Surface the Flag in the Data Sources

Both feeder queries need to surface the flag, aliased exactly as LicenseByDepartment on both sides:

  • Calculate Software License Entitlements — join from license to its product (via Asset.SoftwareCatalogEntry) and select LicenseByDepartment
  • Calculate Software License Consumptions — join from st.ProductID to the product and select LicenseByDepartment

Use an outer join or ISNULL(..., 0) so the flag is always present on every row. If a license or consumption is missing the flag entirely, the engine throws "Missing required field in affinity rules."

Step 3: Add the Scoping Rules

In the Configure dialog, add three rules:

Set Consumption.DeptScope = IIF(Consumption.LicenseByDepartment = 1, Consumption.DepartmentID, -1)
Set License.DeptScope     = IIF(License.LicenseByDepartment     = 1, License.DepartmentID,     -1)
Requirement Consumption.DeptScope = License.DeptScope

The first two are calculated fields; the third is a requirement.

How It Works

For a flagged product:

  • Consumption.DeptScope = the consumption's DepartmentID
  • License.DeptScope = the license's DepartmentID
  • Requirement: must be equal → license excluded if departments differ

For an unflagged product:

  • Consumption.DeptScope = -1
  • License.DeptScope = -1
  • Requirement: -1 = -1 → always satisfied → no-op

The default Affinity Consumption.DepartmentID = License.DepartmentID, 3000 keeps running for unflagged products as before — the scoping rule layers on top of the affinity rule rather than replacing it.

Generalising to Other Dimensions

The same pattern applies to location, cost centre, or any other dimension. The naming convention should be consistent: LicenseByDepartment / DeptScope, LicenseByLocation / LocScope, LicenseByCostCentre / CCScope.

For a more elaborate version that supports both Require and Prefer modes selectable per product, see the "configurable section-by" pattern in BusClassNET04/sam-affinity-rules.md (developer-facing skills file). It uses two integer columns on the catalog entry — SectionBy (which dimension) and SectionMode (require vs prefer) — and twelve rule lines that cover the full matrix.

Important Constraints

Constraint Detail
Use integer flags Strings (e.g., IIF(Manufacturer = "Adobe", ...)) misbehave silently because the expression evaluator does not quote string literals reliably. Use IIF(LicenseByDepartment = 1, ...).
Sentinel choice matters Use -1 (or another value that cannot legitimately occur in the column), not 0 — some records genuinely have DepartmentID = 0.
Column must exist on every row The data source queries must always return the flag. Use outer joins or ISNULL — a missing column throws.
Comparator is = only The scoping pattern works with equality. The within comparator is wired only for the three real hierarchy fields (DepartmentID, LocationID, CostCentreID) and does not work on synthetic scope fields like DeptScope. See Hierarchy Fields.

Hierarchy Limitation

Because the scope field is synthetic, you cannot use within on it. A license in "IT Division" will not cover consumptions in "IT Support Dept" under this pattern — only exact department matches work.

For most per-product scoping policies, this is what you want anyway (the whole point is to prevent cross-tree bleed). If you genuinely need hierarchy-aware scoping, the workaround is to expand the source data: emit one row per ancestor at query time so equality matching can pick up the parent. This increases cardinality but needs no engine change.

Testing the Scope

After saving the rules:

  1. Pick a flagged product (e.g., set LicenseByDepartment = 1 on Adobe Creative Cloud).
  2. Confirm at least one license and one consumption exist with mismatched departments.
  3. Recalculate from the current period.
  4. Open the Licensing Position for that product. The mismatched consumptions should now be in deficit (no allocation), even if licenses with capacity exist in other departments.
  5. Pick an unflagged product and confirm allocation behaves normally.

If the flagged product allocates anyway, the most likely cause is the data source not surfacing LicenseByDepartment correctly. Check both feeder queries return the column.

Why Not Just Add a Per-Product Requirement Syntax

The engine processes one product at a time, but the rules block is global to the calculation — not parameterized per product. There is no Requirement ... WHERE Product = 'Adobe' form. The scoping pattern is the supported way to express per-product policies in a global rule block.

This is a deliberate design choice: it keeps the rule evaluator simple and predictable, while pushing the per-product variability into the data (the flag column) where it belongs.