Notes: Adding custom fields to entity maps between Quotes, Orders and Invoices (solution aware)

4 minute read

Finally, it’s the weekend and I have some time to focus on an issue which bothered our team for a few months. As always we wanted to do it the right way so it will be fast, reusable, continuous integration compatible and without spawning unnecessary workflows and plugin instances.

The Problem

Sales solution makes it easy to go through the lead to cash process. One of the out-of-box helpers is copying data between entities. Example:

  1. Add products to opportunity
  2. Create a quote by clicking on a little plus sign in the top right corner of product list. The quote gets relevant fields prepopulated using data from opportunity including products lines
  3. Edit quote and create an order from it using a ribbon button.
  4. Order also gets all the data from the quote.
  5. And the same goes for invoices.

This works well until we want to customize this behavior or do it for other entities. Unfortunately CRM server (the platform side) still has a lot of hidden hacks. Now with Common Data Model we want to reuse entities and processes wherever possible.

Use Cases

There are few legitimate reasons for adding custom fields to sales line entities. We have these:

  • Customer Facing Product Name
    • Specify different text for product names in the actual quotes and invoices.
  • Date of delivery
    • Have delivery dates specified for each line item.

Background

As you may already know you can map some fields which have 1:N relationship. https://docs.microsoft.com/en-us/powerapps/maker/common-data-service/map-entity-fields Let’s have a look how we use this in code. Once you have maps in place you can create a new record with InitializeFrom and it will have values prepopulated according to the map. https://docs.microsoft.com/en-us/dynamics365/customer-engagement/web-api/initializefrom Here is a sample how it works:

  1. Create the InitializeFromRequest
  2. Set Contact as the Target
  3. Set
  4. the reference of parent Account as the EntityMoniker which you want to copy the data from
  5. Execute the InitializeFromRequest
  6. Read the object with copied data from Account from InitializeFromResponse
  7. Make other modifications
  8. Create the record
InitializeFromRequest request = new InitializeFromRequest();

request.TargetEntityName = "contact";

request.EntityMoniker = new EntityReference("account", idOfAccount);

InitializeFromResponse response = (InitializeFromResponse)organizationService.Execute(request);

if (response.Entity != null)
{
Entity newContactRecord = response.Entity;

newContactRecord.Attributes.Add("firstname", "Tomas");

organizationService.Create(newContactRecord);
}

  The internal Sales solution uses this too. When you hit Create Order button they do this. But you don’t see their EntityMaps in solution editor because there is no direct relationship between mapped entities. Field mappings aren’t actually defined within the entity relationships, but they are exposed in the relationship user interface.

Solution

So can’t edit mappings for these entities using UI editor for relationships because there is no direct relationship between the records but the entity map exists in the database (entitymapbase table). When you google a bit you’ll find a way to get an ID of the entity mapping table using an organization service endpoint:

/XRMServices/2011/OrganizationData.svc/EntityMapSet

  (This can be done with Web API endpoint too: /api/data/v9.0/entitymaps?$filter=contains(sourceentityname,%27quotedetail%27)) If you try to list this EntityMapSet resource, you probably won’t find it because it’s hidden in some instances (I’ve seen both cases). But you can filter mapping tables by specifying source and destination in a request query and it works:

/XRMServices/2011/OrganizationData.svc/EntityMapSet?$select=EntityMapId&$filter=SourceEntityName%20eq%20%27salesorderdetail%27%20and%20TargetEntityName%20eq%20%27invoicedetail%27



   EntityMapSet
   https://ABC.crm4.dynamics.com/XRMServices/2011/OrganizationData.svc/EntityMapSet
   2018-08-25T15:32:50Z
   
   
      https://ABC.crm4.dynamics.com/XRMServices/2011/OrganizationData.svc/EntityMapSet(guid'6825c078-5459-e811-a84a-000d3ab6b1ed')
      
      <updated>2018-08-25T15:32:50Z</updated>
      <author>
         <name />
      </author>
      <link rel="edit" title="EntityMap" href="EntityMapSet(guid'6825c078-5459-e811-a84a-000d3ab6b1ed')" />
      <category term="Microsoft.Crm.Sdk.Data.Services.EntityMap" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
      <content type="application/xml">
         <m:properties>
            <d:EntityMapId m:type="Edm.Guid">6825c078-5459-e811-a84a-000d3ab6b1ed</d:EntityMapId>
         </m:properties>
      </content>
   </entry>
</feed>

</pre>

<p>Look for the EntityMapId element and find the map ID. It is different in every environment since the metadata definition does not contain any identifier. In my case it is <strong>6825c078-5459-e811-a84a-000d3ab6b1ed.</strong> Now I need to open a designer directly using this URL with my Guid:</p>

<blockquote>
  <p>/tools/systemcustomization/relationships/mappings/mappingList.aspx?mappingId=%7b<span style="color: #ff0000;"><strong>6825c078-5459-e811-a84a-000d3ab6b1ed</strong></span>%7d</p>
</blockquote>

<p>Here you can add your desired mapping using New button: <img src="/uploads/2018/08/chrome_2018-08-25_22-03-15.png" alt="" /> <img src="/uploads/2018/08/chrome_2018-08-25_22-04-58.png" alt="" /></p>

<h1 id="thats-not-everything-include-it-in-a-solution">That’s not everything.. Include it in a solution!</h1>

<p><span style="text-decoration: underline;"><span style="color: #ff0000;"><strong>You should never ever ever make unmanaged modifications to the Default Solution in your production environment!!</strong></span></span> Yeah but you have another problem because the EntityMap does not get exported with your solution in your Dev environment and therefore you are not able to move it to production. There is just no way to include it using UI.</p>

<h2 id="import---the-easy-part">Import - the easy part</h2>

<p>If you are able to construct the mapping during your build process or you have your solutions in source control, you can inject it to customizations.xml right away. 😊 <img src="/uploads/2018/08/Code_2018-08-25_22-35-38.png" alt="" /></p>

<div>

<pre class="lang:default highlight:0 decode:true"><?xml version="1.0" encoding="UTF-8"?>
<EntityMaps>
   <EntityMap>
      <EntitySource>quotedetail</EntitySource>
      <EntityTarget>salesorderdetail</EntityTarget>
      <AttributeMaps>
         <AttributeMap>
            <AttributeSource>tntg_productcustomername</AttributeSource>
            <AttributeTarget>tntg_productcustomername</AttributeTarget>
         </AttributeMap>
      </AttributeMaps>
   </EntityMap>
</EntityMaps></pre>

After importing this CRM server will append the Attribute map to the existing EntityMap and you are good to go: ![](/uploads/2018/08/chrome_2018-08-25_22-33-57.png) Great!

## Export - where it gets dirty

Take this part more as notes from my research than a guide. After the initial euphoria immediately came disappointment. I tried to export the solution to check whether it contains the necessary mapping information. It did not. ![](/uploads/2018/08/Code_2018-08-26_02-21-13.png) Out of desperation I've started searching the solution export logic in assemblies from Dynamics CRM installation.</div>

<p>Here is the code responsible for determining whether an EntityMap gets exported.</p>

<pre class="lang:c# decode:true">private bool DoesEntityMapNeedToBeSkipped(Hashtable entitiesTable, EntityMappingStruct mapping)
{
    if (entitiesTable != null)
    {
        if (!entitiesTable.Contains(mapping.EntitySource.EntityInfo.LogicalName) && !entitiesTable.Contains(mapping.EntityTarget.EntityInfo.LogicalName))
        {
            return true;
        }
        if (mapping.EntitySource.IsCustomEntity && !entitiesTable.Contains(mapping.EntitySource.EntityInfo.LogicalName))
        {
            return true;
        }
        if (mapping.EntityTarget.IsCustomEntity && !entitiesTable.Contains(mapping.EntityTarget.EntityInfo.LogicalName))
        {
            return true;
        }
    }
    return false;
}</pre>

<p>This indicates that the map will be skipped if:</p>

<ul>
  <li>Both Target and Source entities are not included in the solution</li>
  <li>One of the entities is custom and is not included in the solution</li>
</ul>

<p>I have also found this: <a href="https://stackoverflow.com/a/41273138">https://stackoverflow.com/a/41273138</a></p>

<blockquote>
  <p>I have also seen where you have to have both the relationship, and both fields defined the mapping in the solution in order for the mappings to be exported… So if I have Entity A that has a Mapping to B, for fields A.1 to B.1 and A.2 to B.2, I have to make sure that the relationship, and fields A.1, A.2, B.1 and B.2 have been added to the solution as well, or else they don’t get exported. After some further testing, in order for Lookup Attributes to be included in the Export of a Mapping, the Target Attribute field <strong>MUST BE</strong> included in the solution!</p>
</blockquote>

<p>So I guess maps with both out-of-box entities are automatically skipped.</p>

<div>BTW this is how individual attribute map looks like:</div>

<blockquote>
  <p>/api/data/v9.0/attributemaps?$filter=_entitymapid_value%20eq%2013180255-43fd-e711-80f9-00155d036800</p>
</blockquote>

<p><img src="/uploads/2018/08/chrome_2018-08-25_23-45-01.png" alt="" /></p>

        
      </section>

      <footer class="page__meta">
        
        
  


  
  
  

  <p class="page__taxonomy">
    <strong><i class="fas fa-fw fa-tags" aria-hidden="true"></i> Tags: </strong>
    <span itemprop="keywords">
    
      
      
      <a href="/tags/#cds" class="page__taxonomy-item" rel="tag">CDS</a><span class="sep">, </span>
    
      
      
      <a href="/tags/#hack" class="page__taxonomy-item" rel="tag">Hack</a><span class="sep">, </span>
    
      
      
      <a href="/tags/#internal" class="page__taxonomy-item" rel="tag">Internal</a><span class="sep">, </span>
    
      
      
      <a href="/tags/#mapping" class="page__taxonomy-item" rel="tag">Mapping</a><span class="sep">, </span>
    
      
      
      <a href="/tags/#plugin" class="page__taxonomy-item" rel="tag">Plugin</a><span class="sep">, </span>
    
      
      
      <a href="/tags/#solution" class="page__taxonomy-item" rel="tag">Solution</a>
    
    </span>
  </p>




  


  
  
  

  <p class="page__taxonomy">
    <strong><i class="fas fa-fw fa-folder-open" aria-hidden="true"></i> Categories: </strong>
    <span itemprop="keywords">
    
      
      
      <a href="/categories/#dynamics-365-cds-powerapps" class="page__taxonomy-item" rel="tag">Dynamics 365 / CDS / PowerApps</a>
    
    </span>
  </p>


        
          <p class="page__date"><strong><i class="fas fa-fw fa-calendar-alt" aria-hidden="true"></i> Updated:</strong> <time datetime="2018-10-28T13:17:58+00:00">October 28, 2018</time></p>
        
      </footer>

      <section class="page__share">
  

  <a href="https://twitter.com/intent/tweet?text=Notes%3A+Adding+custom+fields+to+entity+maps+between+Quotes%2C+Orders+and+Invoices+%28solution+aware%29%20https%3A%2F%2Fblog.thenetw.org%2F2018%2F10%2F28%2Fnotes-adding-custom-fields-to-mapping-between-quotes-orders-and-invoices-solution-aware%2F" class="btn btn--twitter" onclick="window.open(this.href, 'window', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0'); return false;" title="Share on Twitter"><i class="fab fa-fw fa-twitter" aria-hidden="true"></i><span> Twitter</span></a>

  <a href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fblog.thenetw.org%2F2018%2F10%2F28%2Fnotes-adding-custom-fields-to-mapping-between-quotes-orders-and-invoices-solution-aware%2F" class="btn btn--facebook" onclick="window.open(this.href, 'window', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0'); return false;" title="Share on Facebook"><i class="fab fa-fw fa-facebook" aria-hidden="true"></i><span> Facebook</span></a>

  <a href="https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fblog.thenetw.org%2F2018%2F10%2F28%2Fnotes-adding-custom-fields-to-mapping-between-quotes-orders-and-invoices-solution-aware%2F" class="btn btn--linkedin" onclick="window.open(this.href, 'window', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0'); return false;" title="Share on LinkedIn"><i class="fab fa-fw fa-linkedin" aria-hidden="true"></i><span> LinkedIn</span></a>
</section>


      
  <nav class="pagination">
    
      <a href="/2018/10/03/azure-ad-connect-group-based-licensing-and-proxy-addresses/" class="pagination--pager" title="Azure AD Connect, group-based licensing and proxy addresses
">Previous</a>
    
    
      <a href="/2018/11/10/logic-apps-foreach-and-variables/" class="pagination--pager" title="Logic Apps foreach and variables
">Next</a>
    
  </nav>

    </div>

    
      <div class="page__comments">
  
  
      <section id="static-comments">
        
          <!-- Start static comments -->
          <div class="js-comments">
            
          </div>
          <!-- End static comments -->

          <!-- Start new comment form -->
          <div class="page__comments-form">
            <h4 class="page__comments-title">Leave a Comment</h4>
            <p class="small">Your email address will not be published. Required fields are marked <span class="required">*</span></p>
            <form id="new_comment" class="page__comments-form js-form form" method="post" action="https://api.staticman.net/v3/entry/github/networg/networg.github.io/master/comments">
              <div class="form__spinner">
                <i class="fas fa-spinner fa-spin fa-3x fa-fw"></i>
                <span class="sr-only">Loading...</span>
              </div>

              <div class="form-group">
                <label for="comment-form-message">Comment <small class="required">*</small></label>
                <textarea type="text" rows="3" id="comment-form-message" name="fields[message]" tabindex="1"></textarea>
                <div class="small help-block"><a href="https://daringfireball.net/projects/markdown/">Markdown is supported.</a></div>
              </div>
              <div class="form-group">
                <label for="comment-form-name">Name <small class="required">*</small></label>
                <input type="text" id="comment-form-name" name="fields[name]" tabindex="2" />
              </div>
              <div class="form-group">
                <label for="comment-form-email">Email address <small class="required">*</small></label>
                <input type="email" id="comment-form-email" name="fields[email]" tabindex="3" />
              </div>
              <div class="form-group">
                <label for="comment-form-url">Website (optional)</label>
                <input type="url" id="comment-form-url" name="fields[url]" tabindex="4"/>
              </div>
              <div class="form-group hidden" style="display: none;">
                <input type="hidden" name="options[slug]" value="notes-adding-custom-fields-to-mapping-between-quotes-orders-and-invoices-solution-aware">
                <label for="comment-form-location">Not used. Leave blank if you are a human.</label>
                <input type="text" id="comment-form-location" name="fields[hidden]" autocomplete="off"/>
                <input type="hidden" name="options[reCaptcha][siteKey]" value="6LfU-9UUAAAAAKoZxSz_xhPHYtXlMtpiPSHxN9uy">
                <input type="hidden" name="options[reCaptcha][secret]" value="L5/nbdGnElGJ5Lc66Os/t3Jf2QeD8czRcVqoDO/p4H9JoCewTQVfsFBHLKd0GD+pCiduF6dnjA3v8t4vfNjtmqgMP4ChfiwVD4vqFEsEl6AZq3hdzGh22Bk9y8KFA57ZGzvzWjLSOi1aFR5OvpMJ4wXEd0O3zgqE/y4aAiUTsHw=">
              </div>
              <!-- Start comment form alert messaging -->
              <p class="hidden js-notice">
                <strong class="js-notice-text"></strong>
              </p>
              <!-- End comment form alert messaging -->
              
                <div class="form-group">
                  <div class="g-recaptcha" data-sitekey="6LfU-9UUAAAAAKoZxSz_xhPHYtXlMtpiPSHxN9uy"></div>
                </div>
              
              <div class="form-group">
                <button type="submit" id="comment-form-submit" tabindex="5" class="btn btn--primary btn--large">Submit Comment</button>
              </div>
            </form>
          </div>
          <!-- End new comment form -->
          <script async src="https://www.google.com/recaptcha/api.js"></script>
        
      </section>
    
</div>

    
  </article>

  
  
    <div class="page__related">
      <h4 class="page__related-title">You May Also Enjoy</h4>
      <div class="grid__wrapper">
        
          



<div class="grid__item">
  <article class="archive__item" itemscope itemtype="https://schema.org/CreativeWork">
    
    <h2 class="archive__item-title" itemprop="headline">
      
        <a href="/2020/02/29/logic-apps-polling-trigger/" rel="permalink">Support Logic Apps Polling Trigger in your .NET Core Web API
</a>
      
    </h2>
    
      <p class="page__meta"><i class="far fa-clock" aria-hidden="true"></i> 




  less than 1 minute read

</p>
    
    <p class="archive__item-excerpt" itemprop="description">Are you familiar with Logic Apps? Have you ever written your custom connector and wondered how to support Polling Triggers in your API? The way is not exactl...</p>
  </article>
</div>

        
          



<div class="grid__item">
  <article class="archive__item" itemscope itemtype="https://schema.org/CreativeWork">
    
    <h2 class="archive__item-title" itemprop="headline">
      
        <a href="/2020/01/29/recover-e-mails-from-user-account-permanently-deleted-in-azure-active-directory-office-365/" rel="permalink">Recover e-mails from user account permanently deleted in Azure Active Directory (Office 365)
</a>
      
    </h2>
    
      <p class="page__meta"><i class="far fa-clock" aria-hidden="true"></i> 




  1 minute read

</p>
    
    <p class="archive__item-excerpt" itemprop="description">Picture this: You need to solve a ticket, where a user needs to be recovered after being deleted. Not a big deal, just access the deleted users from the Offi...</p>
  </article>
</div>

        
          



<div class="grid__item">
  <article class="archive__item" itemscope itemtype="https://schema.org/CreativeWork">
    
    <h2 class="archive__item-title" itemprop="headline">
      
        <a href="/2019/12/02/office-365-academic-licenses/" rel="permalink">Office 365 Academic Licenses
</a>
      
    </h2>
    
      <p class="page__meta"><i class="far fa-clock" aria-hidden="true"></i> 




  1 minute read

</p>
    
    <p class="archive__item-excerpt" itemprop="description">If you are an administrator for some academic institution and you would like to start using Office 365 you have probably hit a rock of lack of information an...</p>
  </article>
</div>

        
          



<div class="grid__item">
  <article class="archive__item" itemscope itemtype="https://schema.org/CreativeWork">
    
    <h2 class="archive__item-title" itemprop="headline">
      
        <a href="/2019/10/31/remote-server-returned-550-5-7-708-service-unavailable-access-denied-traffic-not-accepted-from-this-ip/" rel="permalink">Troubleshooting 550 5.7.708 error from EXO
</a>
      
    </h2>
    
      <p class="page__meta"><i class="far fa-clock" aria-hidden="true"></i> 




  less than 1 minute read

</p>
    
    <p class="archive__item-excerpt" itemprop="description">From time to time, when you start Office 365 trial with Exchange Online license, after some time you happen to be cut off from email communication and you ge...</p>
  </article>
</div>

        
      </div>
    </div>
  
  
</div>

    </div>

    
      <div class="search-content">
        <div class="search-content__inner-wrap"><form class="search-content__form" onkeydown="return event.key != 'Enter';">
    <label class="sr-only" for="search">
      Enter your search term...
    </label>
    <input type="search" id="search" class="search-input" tabindex="-1" placeholder="Enter your search term..." />
  </form>
  <div id="results" class="results"></div></div>

      </div>
    

    <div id="footer" class="page__footer">
      <footer>
        <!-- start custom footer snippets -->

<!-- end custom footer snippets -->
        <div class="page__footer-follow">
    <ul class="social-icons">
        

        
        
        
        <li><a href="https://fb.me/thenetworg" rel="nofollow noopener noreferrer"><i
                    class="fab fa-fw fa-facebook" aria-hidden="true"></i> Facebook</a></li>
        
        
        
        <li><a href="https://twitter.com/thenetworg" rel="nofollow noopener noreferrer"><i
                    class="fab fa-fw fa-twitter-square" aria-hidden="true"></i> Twitter</a></li>
        
        
        
        <li><a href="https://github.com/networg" rel="nofollow noopener noreferrer"><i
                    class="fab fa-fw fa-github" aria-hidden="true"></i> GitHub</a></li>
        
        
        
        <li><a href="https://instagram.com/thenetworg" rel="nofollow noopener noreferrer"><i
                    class="fab fa-fw fa-instagram" aria-hidden="true"></i> Instagram</a></li>
        
        
        

        <li><a
                href="/feed/"><i
                    class="fas fa-fw fa-rss-square" aria-hidden="true"></i>
                Feed</a></li>
    </ul>
</div>

<div class="page__footer-copyright">© 2020 NETWORG | Blog.
    Powered by <a href="https://jekyllrb.com"
        rel="nofollow">Jekyll</a> & <a href="https://mademistakes.com/work/minimal-mistakes-jekyll-theme/"
        rel="nofollow">Minimal Mistakes</a>. Hosted with <a href="https://pages.github.com">GitHub Pages</a>.</div>
      </footer>
    </div>

    
  <script src="/assets/js/main.min.js"></script>
  <script src="https://kit.fontawesome.com/4eee35f757.js"></script>




<script src="/assets/js/lunr/lunr.min.js"></script>
<script src="/assets/js/lunr/lunr-store.js"></script>
<script src="/assets/js/lunr/lunr-en.js"></script>




  <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-73142323-4"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'UA-73142323-4', { 'anonymize_ip': false});
</script>






    
  <script>
    (function ($) {
    $('#new_comment').submit(function () {
      var form = this;

      $(form).addClass('disabled');
      $('#comment-form-submit').html('<i class="fas fa-spinner fa-spin fa-fw"></i> Loading...');

      $.ajax({
        type: $(this).attr('method'),
        url: $(this).attr('action'),
        data: $(this).serialize(),
        contentType: 'application/x-www-form-urlencoded',
        success: function (data) {
          $('#comment-form-submit').html('Submitted');
          $('.page__comments-form .js-notice').removeClass('notice--danger');
          $('.page__comments-form .js-notice').addClass('notice--success');
          showAlert('Thanks for your comment! It will show on the site once it has been approved.');
        },
        error: function (err) {
          console.log(err);
          $('#comment-form-submit').html('Submit Comment');
          $('.page__comments-form .js-notice').removeClass('notice--success');
          $('.page__comments-form .js-notice').addClass('notice--danger');
          showAlert('Sorry, there was an error with your submission. Please make sure all required fields have been completed and try again.');
          $(form).removeClass('disabled');
        }
      });

      return false;
    });

    function showAlert(message) {
      $('.page__comments-form .js-notice').removeClass('hidden');
      $('.page__comments-form .js-notice-text').html(message);
    }
  })(jQuery);
  </script>


  





  </body>
</html>