After you’ve spent any length of time working with the out of the box sales process within Dynamics 365 Sales Professional / Enterprise, you get used to some of the behavioural quirks that can commonly cause you challenges during an implementation. A prime candidate for consideration here concerns the various types of tables used by the application. When we consider these in detail, such as the Opportunity, Quote and Order table, many of the assumptions that we make about how things work are instantly proven wrong, as these tables tend to operate by their own set of rules. For example, the ability to easily map across attributes between the different product line table types (Opportunity Product, Quote Product etc.) becomes a challenge; we must instead revert to using custom code instead to satisfy this requirement. Little things like this can exasperate both new and long-time users of the application and can often be quite frustrating when explaining to organisations using the software. 😅 Notwithstanding these gripes, I still do genuinely believe the Dynamics 365 Sales platform provides a solid base for organisations to leverage out of the box functionality, with the ability to customise further, as needed. And, when we start to bring into the equation more advanced capabilities, via things such as custom pricing plug-ins, you begin to move to an entirely new level when it comes to what we can do with this application.
A particularly annoying behaviour quirk I dealt with recently involved Quotes marked as Won in the system. The organisation I was working with needed to, on occasions, modify details of a Won Quote after the fact. For example, if the salesperson included the wrong item or the customer changed their order after confirming it. As we can observe in the below screenshot, we have no option on the ribbon to reactivate the Quote once Won in the application:
The only “out of the box” way of dealing with this was to create and re-input all details into a new Quote; not a viable and time-effective solution. Fortunately, there is a faster way we can achieve the objective by working through the following outline steps:
- First, we need to change the Status & Status Reason of the Quote back to Draft & In Progress.
- After we have made modifications to the Quote, it needs to be set as Active again.
- Once Active, the Quote then must be closed as Won. Microsoft wraps the logic for this within the WinQuote action, which we can call via the SDK or against the Microsoft Dataverse Web API.
These steps should be achievable via either a classic workflow or a Power Automate cloud flow automation, which would typically be our first preference for a requirement like this. However, in our case, we wanted to call these steps as part of a Custom API definition, which could then be called via a JavaScript action on a Ribbon button, similar to how I’ve described previously on the blog. And, most crucially, we needed all steps to be completed synchronously. So, given these requirements, we had to look at a solution leveraging C# instead.
With all of this in mind, let’s jump into the reason why you’re probably reading this post. 😉 To open a Won Quote using C#, we would need to write and execute the following code:
//TODO: Implement code to generate your IOrganizationService reference. For plug-ins, this would look something like this:
//IOrganizationServiceFactory serviceFactory = serviceProvider.GetService<IOrganizationServiceFactory>();
//IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
Guid quoteID = new Guid("9cc66ad5-2506-4394-854d-f60c35185d96");
Entity quote = new Entity("quote", quoteID);
quote["statecode"] = new OptionSetValue(0); // Draft
quote["statuscode"] = new OptionSetValue(1); // In Progress
service.Update(quote);
Then, once we’re ready to return the Quote to its previous state, we’d then run the following code to re-close the Quote. Note in particular, at this stage, we need to execute two separate actions and ensure they are completed successfully:
//TODO: Implement code to generate your IOrganizationService reference. For plug-ins, this would look something like this:
//IOrganizationServiceFactory serviceFactory = serviceProvider.GetService<IOrganizationServiceFactory>();
//IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
// We execute everything in a transaction, so we can rollback cleanly on failure
ExecuteTransactionRequest transactionRequest = new ExecuteTransactionRequest()
{
Requests = new OrganizationRequestCollection(),
ReturnResponses = true,
};
ExecuteTransactionResponse transactionResponse;
Guid quoteID = new Guid("9cc66ad5-2506-4394-854d-f60c35185d96");
// First, we need to Activate the Quote ...
Entity quote = new Entity("quote", quoteID);
quote["statecode"] = new OptionSetValue(1); // Active
quote["statuscode"] = new OptionSetValue(2); // In Progress
UpdateRequest updateRequest = new UpdateRequest()
{
Target = quote,
};
transactionRequest.Requests.Add(updateRequest);
// ...with this done, now we can re-close the Quote as Won
Entity quoteClose = new Entity("quoteclose");
quoteClose["subject"] = "Quote Close" + DateTime.Now.ToString();
quoteClose["quoteid"] = quote.ToEntityReference();
WinQuoteRequest winQuoteRequest = new WinQuoteRequest()
{
QuoteClose = quoteClose,
Status = new OptionSetValue(-1),
};
transactionRequest.Requests.Add(winQuoteRequest);
transactionResponse = (ExecuteTransactionResponse)service.Execute(transactionRequest);
tracer.Trace($"Quote ID {quoteID} closed as won successfully!");
As stated earlier, if there wasn’t a need to execute these actions synchronously, then leveraging Power Automate cloud flows would be something I’d actively encourage instead, as both the ability to update and call action steps are supported via this tool. So before you act too gung-ho with the above, validate your approach against the requirements you are working against.
Regardless of the precise approach you take, it is a minor point of frustration that these types of steps are not natively supported within Dynamics 365 Sales. I understand why the system behaves like this, and there are arguable benefits to having a Quote locked after it’s been approved. However, I imagine the ability to make quick edits to a Won Quote is a common requirement across many different organisations, and expecting users to have to go through raising an entirely new Quote is both impractical and ludicrous in equal measure. It’s good to know that the underlying platform supports us in building workarounds such as this so that we are not left entirely adrift with a system that must fit around the business instead of the other way around.