Dynamics 365 – Restricted Sales entities

Back in August/ September 2019 when Microsoft announced the license changes to Dynamics 365 and the Power Platform as part of the wave 2 2019 release Microsoft also announced changes to the list of restricted entities that require a full Dynamics 365 license.

Annoyingly 5 months later that list of restricted entities has still not been updated which is fine until a customer asks, “Can we use the product entity outside of Dynamics” and you disappear off to answer the question only to discover there is no answer.

So last week we decided to do an experiment and see if it is possible to identify the entities that exist in Dynamics 365 but do not exist in freshly created Common Data Service / Power Platform application environment.

This post is the first of a number of posts listing Dynamics 365 entities that don’t exist in non Dynamics 365 / standard Power Platform environments.

For convenience, we are going to display things on a Solution by Solution basis. So here are the Sales related Entities in the msdynce_Sales solution.

D365 Sales Entities not in available in PowerApps

Entity NameEntity System NameNotes
Competitor Competitor?
Competitor AddressCompetitorAddressHelper Entity
Competitor ProductCompetitorProductHelper Entity
Competitor Sales LiteratureCompetitorSalesLiteratureHelper Entity
Contact InvoicesContactInvoicesHidden
Contact OrdersContactOrdersHidden
Contact QuotesContactQuotesHidden
Customer Opportunity RoleCustomerOpportunityRoleHelper Entity
DiscountDiscount ?
Discount TypeDiscount TypeHelper Entity
InvoiceInvoiceProbably Restricted
Invoice DetailInvoiceDetailProbably Restricted
LeadLeadProbably Restricted
Lead CompetitorsLeadCompetitorsHelper Entity
Lead ProductLeadProductProbably Restricted (join on two entities that are both probably restricted)
Opportunity OpportunityProbably Restricted
Opportunity CloseOpportunity Closetied to opportunity
Opportunity CompetitorsOpportunity Competitorstied to opportunity entity
Opportunity ProductOpportunityProduct Probably Restricted (there is lot of business logic behind here)
Order CloseOrderClosetied to Sales Order entity
Product Price LevelProductPriceLevelProbably restricted (join on two entities that are both probably restricted)
Product Sales LiteratureProductSalesLiterature?
QuoteQuoteProbably restricted
Quote CloseQuoteClosetied to Quote entity
Quote DetailQuoteDetailProbably restricted (there is lot of business logic behind here)
Sales LiteratureSalesLiterature??
Sales Literature ItemSalesLiteratureItem??
Sales OrderSalesOrderProbably Restricted
Sales Order DetailsSalesOrderDetailProbably Restricted (there is lot of business logic behind here)

So that’s the Sales side of things – where there are a number of likely entities due to the business logic that occurs behind them.

Next I will move on to the product side of things before looking at Marketing, then customer service and finally some other interesting bits and pieces.

And yes there should be a sales pitch here but I will leave that until everything else is set up and ready to go.

Reducing WebAPI calls by using LocalStorage

When Microsoft introduced the WebAPI call limits for Dynamics 365 and the Power Platform back in October last year we started to look for approaches that allow you to reduce the number of calls we made to the webAPI services within the Common Data Service.

Microsoft have actually done a lot of work in this area by using etags within their results and ensuring their client side api (XRM.Webapi utilises it to minimise the number of api calls required) which works wonders on a record level but doesn’t help when you need to run queries to gather results instead of retrieving individual records.

And our Licence management software (License Power) needs to run queries as the standard question is “does this environment / instance have a vaild license for this solution called XYZ?” Which means that every time we needed to answer that question we would need to run a query and that would eat into the 5,000 – 20,000 requests a user is allowed every day.

To fix that the only solution we had to find a way that we could avoid running retrieveMultiple requests and replace them with standard retrieve requests so we disappeared to google and found a suitable solution and we found it by using localStorage within the browser.

Now I could spend a lot of time talking about localStorage and it’s strengths and weaknesses – but that is for another day. What is worth saying is that Dynamics makes full use of it to reduce download times after first installation and you should never store anything secure within it. However that isn’t an issue here as I don’t want to store anything secure I just need to know that the record (license) I want to check exists and what the GUID of the record is.

Using LocalStorage

So as you might have guessed from reading the previous paragraph by using local storage we can store the results of a previous query and use it as the basis of the subsequent query. Which means that in our case, after the initial retrieveMultiple request with the returned GUID stored in localStorage – I can access the GUID from local storage as required and use the caching within Xrm.WebApi.retrieve to retrieve the record in when required at no cost due to Xrm.WebApi’s caching functionality.

So without further ado the sample code.

	var item= localStorage.getItem(license);
	if (item===null || !item.id ){
		// we need to get the record from the system
		Xrm.WebApi.retrieveMultipleRecords("hdn_license", "?$select=hdn_licensexml,hdn_expirydate,hdn_licenseid&$filter=hdn_product eq '"+license+"'").then(
			function success(result) {
				if (result.entities.length>0){
					let cache={
						id: result.entities[0].hdn_licenseid,
						etag: result.entities[0]["@odata.etag"]
					localStorage.setItem(license, JSON.stringify(cache) );
				else {
					// something has gone wrong here.
			function(error) {  
				console.log("Error: " + error.message);  
	else {
	// this is the retrieve version
		Xrm.WebApi.retrieveRecord("hdn_license", item, "?$select=hdn_licensexml,hdn_expirydate,hdn_licenseid").then(
			function success(result) {
				//ready to call the next stage	
				if (storedrecord.etag===result["@odata.etag"]){}
				else {
					let cache={
						id: result.hdn_licenseid,
						etag: result["@odata.etag"]
					localStorage.setItem(license, JSON.stringify(cache) );
					// you may wish to do other things here if say child records are cached against this parent record
			function(error) {  
				// invalidate the cache as somethings gone wrong
				console.log("Error: " + error.message);

And finally, a quick explanation:-

The code checks for a stored item in the localStorage of the browser.
If no stored record exists, the code calls retrievemultiple (API cost involved) retrieves the record and caches the information for the next time it’s required.
If a local stored record does exist, we use the information contained within it, to retrieve the record using the stored GUID via a retrieve request. This should be at no cost as between the browser and the logic built into Xrm.Webapi and the Power Platform as a whole, the record will be retrieved from a cache instead of requiring a database call.