Archive for September, 2009

XRM Unleashed

At the September DCRMUG meeting (www.DynamicsCRMUserGroup.com) Microsoft’s Bob Piskule gave an excellent presentation and demo on the topic of Microsoft’s XRM platform.

For me, one of the most interesting things about the session was Bob’s presentation of the more-or-less official Microsoft positioning statement on XRM. Up until now most of the discussions I’d seen were by “outsiders” such as myself. (For example, here’s an article I wrote).

Here’s a summary of how Microsoft describes XRM:

Microsoft Dynamics XRM is an application platform layer that sits on top of CRM, designed to accelerate the development of relational business applications through flexible dynamic application services.  XRM’s multi-tenancy allows you to build and run as many line of business applications as you need on a single platform.  XRM also leverages other Microsoft technologies as building blocks including:  Office, Outlook, SQL Server, .NET and Windows. 

XRM can be used for vendor, asset, property, employee, program, recruit, grant, contractor, fleet, resource, product, licensing, contract, lifecycle management and more.  The possibilities are endless.  
Here’s Bob’s demo:

Comments (1)

October DCRMUG Meeting: C360 and Dynamics CRM Add-ons

C360 is the leading provider of Dynamics CRM add-ons and value-added applications. At the October meeting of the dynamics CRM User Group (www.dynamicscrmusergroup.com), we’ll have a two-part session featuring some of their most interesting products.

In part 1, C360 representatives will discuss and demonstrate their most popular product, the Productivity Pack. This is a collection of seven useful utilities that many consider must-have add-ons for Dynamics CRM 4.0. For more information on the product, visit this page.

In part 2, Jonathan Lee and yours truly will demonstrate one of our favorite C360 products: BI Analytics.  This product addresses an important feature-gap in the base product, giving mere users the ability to add dashboards and analytics directly within the CRM web client. For more information on the product, visit this page 

Meeting date & time: Thursday, October 29, 3:00 PM Central Time. As always, free and extremely valuable, and visit this page for more information and to register.

Leave a Comment

Dynamics CRM 5 Wish-List

Not that I assume the Dynamics CRM product team hangs on my every blog post, but here’s my wish list of feature enhancements for Dynamics CRM 5.0:

Improvements to Views. You can use Advanced Find in Dynamics CRM 4.0 to find almost any data you need. You can filter on and include columns from entities related to each other using the great RDBMS features of CRM. But once you find the data you want, the actual data view itself isn’t very good. Here are two things they should do to improve views:

  • Ability to create groups and totals/sub-totals. Almost any database application has grouping and totals by group and overall, but not CRM! In SharePoint I can create a custom view on a list or library with all of the grouping and totaling I need; I think the SharePoint implementation makes a fine model for how to implement this. This grouping and totaling would solve another (related) problem with CRM 4 – namely how hard it is to get aggregate data about your entities. I shouldn’t have to export to Excel or create a marketing list just to find out how many Account or Contact records I have, but currently those are my best options unless I want to write some code.
  • Ability to dynamically filter a view. We should be able to create a view, expose it on an entity’s form using an IFRAME, and configure it to receive and filter on data from the current record. For an account, say, I could create a tab called Account Summary and have views (with groups and totals, of course!) of Opportunities, Orders and Invoices, all filtered for the current Account record. This might be a little harder, but it would be cool and would go a long way to countering the often-made and as I understand it correct contention that SalesForce.com has better dashboarding capabilities.

nternet Marketing. If you’ve tried out the Internet Marketing features of CRM Online, the most promising feature (in my opinion) is the sweet landing pages Microsoft gives you tools to create. There’s no code required, the pages look great, Microsoft hosts the pages, and a response goes right into your CRM. Currently, in the on-premise edition of CRM 4, the fact that you have to write a bunch of code to capture information from a web form into your CRM really restricts the number of people who can do it, and it’s a critically important component of most organization’s marketing efforts – or at least it should be! So I think a good way for Microsoft to do this would be to extend the CRM Online landing page functionality to include it with on-premise 5.0, and allow us to link the Landing Pages we create to either our own on-premise ones, or to hosted ones we can create on a Microsoft site. This would really open up Internet Marketing functionality to lots of people who currently can’t do it, and it would be strategic for Microsoft also, since it would gently nudge customers a little further down the SaaS path.

Here are a couple other improvements the Internet Marketing service in CRM Online could stand:

  • Currently data can only be brought into the “Internet Lead” entity… and it’s not really even an entity (e.g., it’s not customizable and not available for Advanced Find or reporting). It would be better if you could import web form data into ANY entity in CRM. Barring that, at least the Internet Lead entity should be a real entity, and you should not have to lose Internet Lead records when you import them as Lead records into Dynamics CRM.
  • Currently only Bing is a supported search engine for integrated keyword searches in marketing campaigns. This should be opened up to include other search engines.

Scheduled and “Batch” Workflows. I love the Dynamics CRM 4.0 workflow implementation. But two things I find myself wishing I could do is schedule a manual workflow, and run a manual workflow against more than 250 records at a time! I’m not exactly sure how this would work, but I can imagine a UI where an administrator could specify a schedule for when a workflow job would be run, and specify a saved view of records it would be run against.

 A KPI entity. The view improvements I discussed previously would be helpful for creating “dashboard”-like functionality, but if you want real dashboard you need key performance indicators. Again, the SharePoint team has a nice model for this. In MOSS 2007, a KPI list is a special kind of list that can get its data from a specific view defined against an underlying SharePoint list. A record in the KPI list contains the goal values, and the measure (the actual data) can be a total, average, or some other aggregate function from the view. Heuristically, some rows in this KPI list might contain data like this:

Source Entity Measure View Goal level 1 Goal level 2 Actual Indicator
Opportunity Actual Revenue Q1 $100,000 $125,000 $137,500   
Opportunity Actual Revenue Q2 $110,000 $135,000 $115,000   

 
 

This kind of BI wouldn’t necessarily compete with Cognos, but not everybody needs that level of detail, and it would be a lot better than nothing!

Better integration with SharePoint, in a specific way. If you’re using CRM to manage customers, and you’re using SharePoint to manage web content, there are lots of times when you’ll want to offer those customers authenticated access to some of that content. But right now you have to jump through hoops to do this cleanly, and the coding effort required to implement what’s known as a “custom authentication provider” (which could essentially allow a user to access SharePoint content while authenticating against a Contact record in CRM) requires a significant development effort. I think this is essentially plumbing code (valuable plumbing, but plumbing nevertheless!) and that Microsoft should at the very least offer it as an add-on or a free download. (Like the CRM 4.0 Data Migration Manager, it wouldn’t be valuable to organizations if they didn’t have CRM and SharePoint.)

Out of the box dashboarding and improved charting. This is partly related to the KPI entity above, but also partly different.

Built-in “Record URL” attribute type. We shouldn’t have to write code to include a record’s URL in the body of an email generated by a workflow.

Ability for Workflows to create and access more record types. Workflows can’t create opportunity product, quote product or invoice product records, for example. There are plenty of examples of this, where you’d really like to create a certain kind of record within a workflow but it isn’t exposed.

 


Comments (13)

Email Record Links from a Dynamics CRM Workflow

It’s easy enough to email another CRM user a link to a record. If you open up a form for almost any record type in Dynamics CRM, you can pull down the Actions menu and select the Send Shortcut command to do this. Dynamics CRM will open up your mail client, and insert a rather complicated looking link which the recipient will be able to click to navigate directly to the record (assuming they have read permissions!)

For example, here’s what it looks like from an opportunity record:

Here’s what it looks like for an account record:

While your specific links will be different than mine (mine are for records contained in my CRM Online database, and the nasty-looking 25-character GUID uniquely identifies CRM records so it better be different!), the general structure will always be the same. I’ll come back to that point a little later, since it will solve a problem for us.

Problem: a Workflow-generated email cannot send a record link

So…while you can use the menu command I just mentioned to manually email a link to a record, you cannot email a link to a record from within a workflow. This might sound somewhat obscure, but it actually comes up a lot, and when you run into it, it just seems like something you should be able to do! Fortunately, there’s a relatively easy fix you can implement, with just a pinch of customization and a smidgeon of script. I’ll show you the solution next, but first, let me illustrate the problem in a little more detail.

Suppose I want a workflow to automatically send an email alert any time something important changes about an opportunity record. Here are a few scenarios to illustrate:

  • If the pipeline stage of an opportunity record changes, send an email alerting the sales manager.
  • If the current date is with three days of an open opportunity’s estimated close date, send a reminder email to the opportunity owner.
  • If an opportunity is closed as “Lost”, send an email alert to the owner’s manager.

You can come up with as many scenarios as there are organizations in the world, but in all of them, it would be nice to include a simple clickable link in the body of the email, so the recipient of the alert can navigate directly to the record in question.

As you know if you’ve read my book on Dynamics CRM workflows, there are many things you can do with Dynamics CRM workflows…but unfortunately, this is not one of them! I’ll illustrate with a simple alert email, sent by a workflow that runs automatically any time specified values of the opportunity entity change.

The following screen shot shows the Set Properties form for the workflow’s Send e-mail action. For demo purposes, the email simply goes to me, and I’ve used Dynamic Values to populate the body of the email with presumably interesting information about the current opportunity.

There is an eponymously titled field you can insert for any entity in Dynamics CRM, in this case, using the “{field name{entity name}}” characteristic of the workflow design environment, it’s the {Opportunity{Opportunity}} you can see in the figure. The problem with that is that it’s only a clickable link to the record if it’s on the Regarding field…and the Regarding field only appears if you happen to be viewing the email activity within Dynamics CRM.

Here’s what an email sent by this workflow looks like in Dynamics CRM (e.g., as a History item associated with the opportunity record):

But if, like most people as you prefer consuming your email in Outlook or a non-CRM email client, you never see that Regarding field. For example, Outlook:

Solution: A Pinch of Customization and a Smidgeon of Script (and URL Addressable Forms)

If you’ve read this far, I’m grateful, and you may have noticed the “Link to record” field that’s appeared in a couple of screen shots. That’s the solution, and it uses a technique called URL Addressable Forms, which simply means that every form in Dynamics CRM can be accessed via a unique URL, consisting of an entity-specific prefix combined with a suffix unique to a specific record. You can use the Send Shortcut command I started out by discussing to see what the prefix is for some common record types:

Common Record Types

Record Type Edit Form Prefix
Account https://imginc.crm.dynamics.com/sfa/accts/edit.aspx
Contact https://imginc.crm.dynamics.com/sfa/conts/edit.aspx
Opportunity https://imginc.crm.dynamics.com/sfa/opps/edit.aspx
Case https://imginc.crm.dynamics.com/cs/cases/edit.aspx
Marketing Campaigns https://imginc.crm.dynamics.com/ma/camps/edit.aspx

 

You can use a custom attribute for any entity you want to include a clickable link for by following these steps:

  1. Customize the entity in question (e.g., Opportunity) by adding a new attribute. I added a custom attribute to the Opportunity entity, called it “Record Link” (it has a corresponding schema name), and gave it the following properties:

     

    The most important is to make it a Type of “nvarchar”, since that’s the only type that has the clickable link format of “URL”. Make sure it’s long enough. I made mine 200, but you can always come back and increase the maximum length (unlike the type and format values, which can’t be changed once the custom attribute is created.)

 

  1. Once the attribute is created, the question is how to put the value into it. This really is just a smidgeon of script code – one line of Jscript you can put in the Opportunity form’s OnSave event:

     

    crmForm.all.img_recordlink.DataValue = ‘https://imginc.crm.dynamics.com/sfa/opps/edit.aspx?id=’+crmForm.ObjectId;

That’s pretty much it. If you implement this you’ll notice that since I wrote this code for the form’s save event, the new “Record Link” field won’t contain anything until a record’s form has been saved at least once after you’ve saved and published theses customizations. Too bad there’s not a built-in Record Link attribute for every Dynamics CRM entity. I guess I should add that to my top X list of new features that should be included in Dynamic CRM 5!

I wrote a book on Dynamics CRM workflows, by the way, and it contains tons of examples like this one and other useful workflows. You can purchase the book on Lulu.com or on Amazon, and if you do, you can also get a (free) subscription to the online version of the book, where you can download the workflows themselves, customizations, and related content.

Here’s a link you can visit to find out more about my book and purchase it:

Comments (12)

September DCRMUG Meeting

XRM Unleashed

At the September meeting of the Dynamics CRM User Group, Microsoft’s redoubtable Bob Piskule will present on an important trend: the emergence of Dynamics CRM as the XRM platform.

Session description: During this presentation you will learn how XRM enables IT and business success by building many applications on a single platform with shared resources and infrastructure. You will also learn how XRM can accelerate the development of relational, line-of-business applications, built on the Microsoft technology building blocks such as Windows, SQL Server, and .NET

This will be a popular session so get registered now before we fill up!

Meeting date is September 24, and we’ll stick to our new and popular meeting time of 3:00 PM CDT. As always, attendance is free, and you can attend in person at Microsoft’s Downers Grove office, or online.

Visit the meeting details page for more information and to register.

Leave a Comment

Synchronize Child and Parent Records – with Code

I posted an earlier article on this topic, based on a question I got from long-time Trick Bag reader Sherry Hale. She wanted to know if you can use an automatic workflow to push changes to an account record down to all of the child contact records. I said,

  1. Unfortunately, you can’t
  2. But you can do it with an On Demand workflow on contacts, if you don’t mind manually selecting all of the contact records and clicking the Run Workflow button
  3. You can also do it with code. I hadn’t done it before but I’d seen articles about it, knew it was out there somewhere, and asked if anybody had any good examples please send them along.

A few hours after posting the article, I received an email from Jim Steger. Jim is a principle with Sonoma Partners and co-author of two of my three favorite books on Dynamics CRM, Working with Dynamics CRM 4.0, and Programming Dynamics CRM 4.0. He reminded me of an example he’d included in his Working with book, and graciously consented to my posting the note along with the code. (I say “reminded” because his was the example I knew was out there somewhere and I’d just got done scouring the Programming book looking for it.)

So with that, below is the note and the code to push changes made to a parent record down to child records, using Accounts and Contacts as an example.

Jim: thank you so much!

Sherry: if you have questions about the code, ask Jim! J

 

*******

Hi Richard-

I wrote an example in my Working with Dynamics CRM 4.0 book that does something very similar in code to the request in your recent blog post. I did have to use a plug-in though and I used a custom attribute on the contact that would determine whether it should be sync’d with the parent.

You can download the code from Microsoft here:

http://www.microsoft.com/mspress/companion/9780735623781/

 

Regards-

Jim

 

 

It is not the simplest of code and might be optimized, but you should get the idea.

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Services.Protocols;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;
 
 
namespace WorkingWithDynamicsCrm4.Plugin
{
  public class AddressSync : IPlugin
  {
    /// <summary>
    /// This sample synchronizes a contacts address with its parent account asynchronously.
    /// </summary>
    public void Execute(IPluginExecutionContext context)
    {
      // Verify we have an entity to work with
      if (context.InputParameters.Properties.Contains("Target") && context.InputParameters.Properties["Target"] is DynamicEntity)
      {
        ICrmService service = context.CreateCrmService(false);
 
        // Obtain the target business entity from the input parmameters.
        DynamicEntity entity = (DynamicEntity)context.InputParameters.Properties["Target"];
 
        // Verify that the entity represents an account.
        if (entity.Name == EntityName.account.ToString())
        {
          Dictionary<string, Property> changedValues = new Dictionary<string, Property>();
          changedValues = SetChangedAddressValues(entity);
 
          // if we have a changed address value, then continue with the update
          if (changedValues.Count > 0)
          {
            Guid accountId = ((Key)entity.Properties["accountid"]).Value;
 
            // retrieve applicable contact ids
            BusinessEntityCollection contactsToUpdate = RetrieveContactsForAddressSync(service, accountId);
 
            // update all contacts found
            UpdateContactAddress(service, contactsToUpdate, changedValues);
          }
        }
      }
    }
 
    /// <summary>
    /// Stores address values.
    /// </summary>
    /// <param name="entity">DynamicEntity object.</param>
    /// <returns></returns>
    private Dictionary<string, Property> SetChangedAddressValues(DynamicEntity entity)
    {
      Dictionary<string, Property> changedValues = new Dictionary<string, Property>();
 
      AddStringPropertyToDictionary(entity, "address1_name", changedValues);
      AddStringPropertyToDictionary(entity, "address1_line1", changedValues);
      AddStringPropertyToDictionary(entity, "address1_line2", changedValues);
      AddStringPropertyToDictionary(entity, "address1_line3", changedValues);
      AddStringPropertyToDictionary(entity, "address1_city", changedValues);
      AddStringPropertyToDictionary(entity, "address1_address1_stateorprovincename", changedValues);
      AddStringPropertyToDictionary(entity, "address1_postalcode", changedValues);
      AddStringPropertyToDictionary(entity, "address1_country", changedValues);
      AddStringPropertyToDictionary(entity, "address1_telephone1", changedValues);
      AddPicklistPropertyToDictionary(entity, "address1_addresstypecode", changedValues);
      AddPicklistPropertyToDictionary(entity, "address1_shippingmethodcode", changedValues);
      AddPicklistPropertyToDictionary(entity, "address1_freighttermscode", changedValues);
 
      return changedValues;
    }
 
    /// <summary>
    /// Updates the contact record's address information based on the new values from the account.
    /// </summary>
    /// <param name="service">CRM service object.</param>
    /// <param name="contactsToUpdate">Collection of contacts to update.</param>
    /// <param name="newAddressValues">Dictionary of address values</param>
    private void UpdateContactAddress(ICrmService service, BusinessEntityCollection contactsToUpdate, Dictionary<string,Property> newAddressValues)
    {
      // Loop through each contact in the collection
      foreach (DynamicEntity retrievedContacts in contactsToUpdate.BusinessEntities)
      {
        try
        {
          DynamicEntity oContact = new DynamicEntity();
          oContact.Name = EntityName.contact.ToString();
 
          KeyProperty contactId = new KeyProperty();
          contactId.Name = "contactid";
          contactId.Value = ((Key)retrievedContacts.Properties["contactid"]);
          oContact.Properties.Add(contactId);
 
          // Loop through address dictionary and add the address values as properties to the contact
          foreach (KeyValuePair<string, Property> prop in newAddressValues)
          {
            oContact.Properties.Add(prop.Value);
          }       
 
          // Update the contact
          TargetUpdateDynamic target = new TargetUpdateDynamic();
          target.Entity = oContact;
 
          UpdateRequest update = new UpdateRequest();
          update.Target = target;
          UpdateResponse response = (UpdateResponse)service.Execute(update);
        }
        catch (SoapException ex)
        {
          throw new Exception(ex.Detail.InnerXml.ToString());
        }
      }
    }
 
    /// <summary>
    /// Returns all contacts from the account valid for address synchronization.
    /// </summary>
    /// <param name="service">CRM service object.</param>
    /// <param name="accountId">The identifier of the account.</param>
    /// <returns></returns>
    private BusinessEntityCollection RetrieveContactsForAddressSync(ICrmService service, Guid accountId)
    {
      // Set up standard query expression
      ColumnSet cols = new ColumnSet();
      cols.AddColumns(new string[] { "contactid" });
 
      QueryByAttribute query = new QueryByAttribute();
      query.EntityName = "contact";
      query.Attributes = new String[] { "parentcustomerid", "sonoma_syncaddresswithparent" };
      query.Values = new Object[] { accountId, true };
      query.ColumnSet = cols;
 
      try
      {
        // Retrieve the values from CRM as a dynamic entity
        RetrieveMultipleRequest request = new RetrieveMultipleRequest();
        request.ReturnDynamicEntities = true;
        request.Query = query;
 
        RetrieveMultipleResponse matchingContacts = (RetrieveMultipleResponse)service.Execute(request);
        return matchingContacts.BusinessEntityCollection;
      }
      catch (SoapException ex)
      {
        throw new Exception(ex.Detail.InnerText);
      }
    }
 
    /// <summary>
    /// Helper method to add string properties to the dictionary
    /// </summary>
    /// <param name="entity">DynamicEntity to evaluate.</param>
    /// <param name="attribute">Attribute name.</param>
    /// <param name="newValue">Attribut value.</param>
    private void AddStringPropertyToDictionary(DynamicEntity entity, string attribute, Dictionary<string, Property> newValue)
    {
      if (entity.Properties.Contains(attribute))
      {
        StringProperty prop = new StringProperty();
        prop.Name = attribute;
        prop.Value = entity.Properties[attribute].ToString();
        newValue[attribute] = prop;
      }
    }
 
    /// <summary>
    /// Helper method to add picklist properties to the dictionary
    /// </summary>
    /// <param name="entity">DynamicEntity to evaluate.</param>
    /// <param name="attribute">Attribute name.</param>
    /// <param name="newValue">Attribut value.</param>
    private void AddPicklistPropertyToDictionary(DynamicEntity entity, string attribute, Dictionary<string, Property> newValue)
    {
      if (entity.Properties.Contains(attribute))
      {
        Picklist picklist = new Picklist();
        picklist.name = attribute;
        picklist.Value = ((Picklist)entity.Properties[attribute]).Value;
 
        PicklistProperty prop = new PicklistProperty();
        prop.Name = attribute;
        prop.Value = picklist;
        newValue[attribute] = prop;
      }
    }
  }
}

Comments (5)

Synchronize Child and Parent Records – with Workflows

Sherry Hale, a long-time  Trick Bag reader, recently emailed me the following question:

…can you make a workflow that when an account address is updated, the contacts that share that address are also updated?  It seems simple, but I can’t seem to pull in contact entity information on an account related workflow, nor can I start a contact child workflow from an account workflow.

Unfortunately, I had to answer her that she was correct: it’s not as simple as it seems it would be! An automatic workflow written against the Account entity doesn’t know anything about child records (like contacts, as in this example). In this sense, workflows are similar to Dynamics CRM Advanced Find: child records know lots about parent records, but parents don’t know anything about children.

You certainly can do that with code, and as soon as I find a good example of that kind of code out there I’ll post an update with a link to it. [If you know of any good examples of that or have written code to that effect yourself, please let me know and I'll give you ample credit!]

Updating Child Records from a Parent … with an On Demand Workflow

But, notice I said above that you can’t do it with an automatic workflow on the parent entity (in this case, account, but this is a more general point and will work the same way for anywhere in CRM where you’ve got a 1:N relationship). If you can live with an “on demand” workflow written for the child entity, it’s actually quite straightforward.  

Here’s how you do it:

  1. Create a new workflow for Contact, call it something descriptive like “Update Contact from Parent Account”.
  2. Uncheck the default “Record is created” checkbox in the “Options for Automatic…” section, and make it available to run on demand:


  3. Pull down the Add Step menu and give it a single Update Record action, on Contact, as I show in the above figure, then click Set Properties, and use Dynamic Values in the Form Assistant to update the fields on the child record(remember, the workflow will be run against any selected contacts, in this example!) with any values you want from the parent record (you can see the “Parent Customer (Account)” entity selected in the “Related Entities” list below, and also reflected in the values I’m using to update the fields on the contact record:


Save and Close, publish, and you’re ready to go.

If the workflow is published as an On Demand workflow for Contact, you’ll see it on any contact grid, including the so-called “Contact Associated View” for accounts. In an example like the following figure shows, I’ve clicked the Run Workflow button on the toolbar (which you won’t see unless you’ve got one of the on demand workflows published for Contact), and I’m about ready to change 119 contact records with updated address information from their parent account record.

So, Sherry, it might not be automatic…but it’s a lot better than doing them one at a time.

One More Thing…

You might be thinking: big deal: I can do that with a bulk edit. (select all the contact records you want and select Edit from the More Actions menu, and any updates you make to the fields there will be applied to all selected records). While it’s true that sometimes this will work, there are two limitations (at least) to the bulk edit approach that will still work in the workflow approach:

  1. Plenty of fields can’t be edited with a bulk edit (e.g., certain lookup fields, such as Parent Customer). Any field can be updated with the workflow approach I showed here.
  2. Another advantage of the workflow update approach is that you can apply logic in a workflow. For example, suppose you only want to update certain of those child records with the parent record values, and other ones you didn’t. If there are criteria determining which ones get the update and which ones don’t, you can put that into a “Check Condition” in a workflow, whereas “bulk edit” really is a bulk edit, and gets applied to every record no matter what.

I wrote a book on Dynamics CRM workflows, by the way, and it contains tons of examples like this one and other useful workflows. You can purchase the book on Lulu.com or on Amazon, and if you do, you can also get a (free) subscription to the online version of the book, where you can download the workflows themselves, customizations, and related content.

Here’s a link you can visit to find out more about my book and purchase it:Support independent publishing: Buy this book on Lulu.></a></p>
				</div>
			
				<p class= Comments (2)