A sales report built on Dynamics 365 finance and operations data can look complete and still be missing most of the business. For Supply Chain Analytics teams at Aimpex Resources Group LLC, this is a problem worth catching early, because the numbers add up but only for one legal entity.

What does the problem look like?

A common report: a Power BI report pulls Dynamics 365 data, but the dataAreaId column shows only one company. Adding cross-company=true seems not to help when the feed uses Implementation="2.0".

In this version, the option sits on the service root and the entity is picked in a navigation step. In reports like this, the option did not carry through:

let Source = OData.Feed("https://yourenv.operations.dynamics.com/data/?cross-company=true", null, [Implementation = "2.0"]), Items = Source{[Name = "InventTableBiEntities", Signature = "table"]}[Data] in Items 

This version calls the entity directly, with the option on that URL:

let Items = OData.Feed("https://yourenv.operations.dynamics.com/data/InventTableBiEntities?cross-company=true", null, [Implementation = "2.0"]), WithKey = Table.AddColumn(Items, "CompanyItemKey", each [dataAreaId] & "-" & [ItemId], type text) in WithKey 

Why does OData show one company by default?

Microsoft documents this on the OData page for finance and operations apps: "By default, OData returns only data that belongs to the user's default company." To see more, "specify the ?cross-company=true query option. This option returns data from all companies that the user has access to."

Two parts of that sentence matter:

  • Default company. The filter comes from the user or service account behind the call, not from the report.
  • Companies the user has access to. cross-company=true does not bypass security. If the service account cannot see a legal entity, that data still will not come back.

The cross-company behavior article explains where the filter comes from. Each data entity has a PrimaryCompanyContext property. When it is set, the entity gets a dataAreaId field that ties each row to a company. When it is empty, the entity behaves like a shared table. Not every standard entity is built for cross-company reads.

How do you ask for every company, or one other company?

Put the option on the entity URL, not on the /data/ root. Microsoft's two examples use the Fleet Management sample entity:

https://[baseURI]/data/FleetCustomers?cross-company=true https://[baseURI]/data/FleetCustomers?$filter=dataAreaId eq 'usrt'&cross-company=true 

The first returns all companies the user can see. The second reads one company that is not the user's default. In your own system, swap in your entity name and company ID. The list of public entities is at [Your organization's root URL]/data, and field names are in /data/$metadata.

How do you pull it from Python?

A scheduled extract for a data warehouse usually runs as an app, not a person. The service endpoints overview says OData supports OAuth 2.0 with "Service to service calls using client credentials (shared secret or certificate)." The app needs two setups: an app registration in Microsoft Entra ID, and a record under System administration > Setup > Microsoft Entra applications that maps the client ID to a service account. Microsoft suggests a dedicated service account "that has the correct permissions" rather than Admin. That service account decides which companies cross-company=true can return.

This script gets a token with MSAL (Microsoft's sign-in library for Python), reads every page, and counts rows per company:

import os import time from collections import Counter import msal import requests TENANT_ID = os.environ["FO_TENANT_ID"] CLIENT_ID = os.environ["FO_CLIENT_ID"] CLIENT_SECRET = os.environ["FO_CLIENT_SECRET"] BASE_URL = os.environ["FO_BASE_URL"] # for example https://yourenv.operations.dynamics.com (no trailing slash) def get_token(): app = msal.ConfidentialClientApplication( CLIENT_ID, authority=f"https://login.microsoftonline.com/{TENANT_ID}", client_credential=CLIENT_SECRET, ) result = app.acquire_token_for_client(scopes=[f"{BASE_URL}/.default"]) if "access_token" not in result: raise RuntimeError(result.get("error_description", "token request failed")) return result["access_token"] def get_all(entity, params, token, max_retries=5): url = f"{BASE_URL}/data/{entity}" headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"} rows, retries = [], 0 while url: resp = requests.get(url, headers=headers, params=params, timeout=120) if resp.status_code == 429 and retries < max_retries: retries += 1 wait = resp.headers.get("Retry-After", "30") time.sleep(int(wait) if wait.isdigit() else 30) continue resp.raise_for_status() retries = 0 body = resp.json() rows.extend(body["value"]) url = body.get("@odata.nextLink") params = None # the next link already carries the query options return rows if __name__ == "__main__": token = get_token() customers = get_all( "CustomersV3", { "cross-company": "true", "$select": "dataAreaId,CustomerAccount,CustomerGroupId", }, token, ) print(Counter(row["dataAreaId"] for row in customers)) 

What each part of the script does:

  • Paging. The OData page says the service "supports serving driven paging with a maximum page size of 10,000." Large entities come back in pages, so the loop follows @odata.nextLink until it is gone. The next link already holds the query options, so params is cleared after the first call.
  • Throttling. The service protection API limits page says the service returns "429 Too Many Requests" when limits are hit, and suggests a 429 handler "that uses Retry-After logic." The loop waits and retries a few times before it gives up.
  • Field names. CustomersV3 and its fields are only an example. Check /data/$metadata for the entity you need.
  • Secrets. The client secret comes from an environment variable. Keep it out of the script and out of source control.

How do you know the fix worked?

The last line prints a count per dataAreaId. Before the fix, it shows one company. After it, it should show every legal entity the service account can see. If a company is still missing, check these three things:

  • The service account's access to that legal entity.
  • How the entity's PrimaryCompanyContext is set, and whether that entity supports cross-company reads at all.
  • Whether the option sits on the entity URL, not the /data/ root, as shown above.

For reporting, keep dataAreaId in every key you build. The CompanyItemKey column above does this, because the same item number can appear in more than one company.

Sources

From its base in Lincoln, NE, Aimpex Resources Group LLC (founded in 2025) delivers ERP Architecture, Supply Chain Analytics and IT Staffing Solutions. Its ERP work spans SAP S/4HANA, Oracle Cloud ERP and Microsoft Dynamics, which helps supply chain reports cover every legal entity, not just the default one. To see how Aimpex supports Dynamics 365 data and reporting work, visit aimpexresources.com.