With the growth of the web, there has also been a boom of software companies that provide access to their applications through web APIs and even offer microservices for very specific functions that help build complex and more powerful applications.
Why is this interesting? Well, this rise of APIs has created an expanding ecosystem of services and tools businesses can integrate into their own processes. If their systems allow it, a company can focus on its core operation while benefiting from functionality that would take too long to develop or maintain to be cost effective.
I like to think Acumatica has cloud in its DNA and it really shows when we work on integrations: it’s much easier to work with applications and services that provide a web API. If we need to work from within an external application, Acumatica allows us to easily expose data via OData and business objects through SOAP and REST web services. On the other hand, if we need to consume a service from within Acumatica, its Customization Platform provides tools to embed the new features into existing parts of the ERP.
A Practical Example
Last February, we participated in the first Acumatica Hackathon held during the Acumatica Summit in San Diego. Our team decided to work on a project that showed the power of “connecting clouds” by adding voice and SMS notifications to Acumatica by using Twilio.
Twilio is a cloud communications platform that provides web APIs to add messaging, voice, and video in web and mobile applications. The module and its source code are available in github: Acumatica-Twilio.
This project serves as a good example of how quickly it is to build a self contained module that adds specific functionality to Acumatica, so let’s do a step-by-step review of how this module was built.
Project Design
The first thing we needed to do is create a new customization project so the new functionality can be deployed and even shared for publication in other instances. The following diagram shows an overview of all the module parts:
Using Twilio’s API
In order to send SMS and Voice notifications from Acumatica using Twilio, we needed to consume its web API.
Luckily, there is a Twilio C#/.Net helper library which handles all requests and can be referenced directly.
So we added a reference to the library to our project using NuGet:
Install-Package Twilio
We then built a class to wrap the library and expose two simple methods that allowed us to either send an SMS message or start a call:
private TwilioRestClient _client;
public string Origin { get; set; }
public TwilioNotification(string sid, string token)
{
_client = new TwilioRestClient(sid, token);
}
public void SendSMS(string number, string message)
{
var msg = _client.SendMessage(Origin, number, message);
if (msg.RestException != null)
{
throw new PXException(msg.RestException.Message);
}
}
public void SendCall(string number, string message)
{
var call = _client.InitiateOutboundCall(Origin, number, MessageUrl(message));
if (call.RestException != null)
{
throw new PXException(call.RestException.Message);
}
}
If this looks simple, it’s because it is! For the SMS notifications, we just provide an origin and destination number along with the desired text message and Twilio does the rest.
Voice notifications need a little more of preparation. For voice, Twilio offers a simple text-to-speech service that we can use to read messages as audio. This service requires us to provide a callback URL where their engine looks for instructions of what to do when the call is connected.
We handled this by using one of the utility services offered by Twilio called “twimlets”. These micro apps generate Twilio instructions on the fly from the information they get on the URL query string.
By using the simple message twimlet, we can just build an URL with our message as part of the query string and the twimlet will build the required code for Twilio to say it.
private static string TwimletBase = "http://twimlets.com/message";
...
public static string MessageUrl(params string[] messages)
{
var messageCollection = new NameValueCollection();
for (int i = 0; i < messages.Length; i++)
{
messageCollection.Add("Message[" + i + "]", messages[i]);
}
return TwimletBase + messageCollection.ToQueryString();
}
Setup Screen
In order to create an instance of the client library, we needed to provide the credentials of a Twilio account, so we programmed a Setup screen to enable any user to enter their own.
It’s always a good practice to use the PXRSACryptString attribute provided by Acumatica to encrypt the credentials in the new database table.
#region TwilioAccountSid
public abstract class twilioAccountSid : IBqlField { }
[PXRSACryptString(255)]
[PXDefault]
[PXUIField(DisplayName = "Twilio Account Sid")]
public string TwilioAccountSid { get; set; }
#endregion
#region TwilioAuthToken
public abstract class twilioAuthToken : IBqlField { }
[PXRSACryptString(255)]
[PXDefault]
[PXUIField(DisplayName = "Twilio Auth Token")]
public string TwilioAuthToken { get; set; }
#endregion
Twilio requires sending the original phone number for voice notifications, so we also added a field to store this number so it is always sent in the call request.
Finally, we also added the option of selecting the notification templates used for voice and SMS notifications.
#region SMSNotificationID
public abstract class sMSNotificationID : IBqlField { }
[PXDBInt]
[PXDefault]
[PXSelector(typeof(Notification.notificationID),
DescriptionField = typeof(Notification.name), ValidateValue = true)]
[PXUIField(DisplayName = "Notification ID")]
public int? SMSNotificationID { get; set; }
#endregion
#region CallNotificationID
public abstract class callNotificationID : IBqlField { }
[PXDBInt]
[PXDefault]
[PXSelector(typeof(Notification.notificationID),
DescriptionField = typeof(Notification.name), ValidateValue = true)]
[PXUIField(DisplayName = "Call Notification ID")]
public int? CallNotificationID { get; set; }
#endregion
Once we defined the Setup DAC, we created a simple Graph for it and the related Page as TW101000 and added it to the Site Map under Management -> Configure -> Twilio Integration.
Adding Actions
Now comes the interesting part: adding the action to trigger a notification. The Acumatica Customization Platform allows us to extend existing business logic by using the PXGraphExtension class.
For our project, we decided to send notifications for invoices with due balance, but this behavior can easily be added to other business objects.
First, we created an extension of the ARInvoiceMaint graph and added a private helper function to handle sending the notifications. Depending on its type, the function prepares the content from the NotificationID
configured in the setup graph and calls our Twilio class to send the message.
public void SendTwilioNotification(ARInvoiceEntry invGraph, string notificationType, bool isMassProcess = false)
{
ARInvoiceEntryPXExt invGraphExt = invGraph.GetExtension<ARInvoiceEntryPXExt>();
//Raise error if Twilio Integration Setup not configured
if (invGraphExt.TwilioSetupInfo.Current == null)
throw new PXException(Messages.TwilioAccountNotSetup);
//Get current billing contact
if (invGraph.Billing_Contact.Current == null)
invGraph.Billing_Contact.Current = invGraph.Billing_Contact.Select();
ARContact contact = invGraph.Billing_Contact.Current;
if (contact == null || String.IsNullOrEmpty(contact.Phone1))
throw new PXException(Messages.InvoiceBillingContactNotExists);
int? iNotificationID = (notificationType == TwilioNotificationType.SMS) ?
invGraphExt.TwilioSetupInfo.Current?.SMSNotificationID :
invGraphExt.TwilioSetupInfo.Current?.CallNotificationID;
//Raise notification appropriate error message
if (notificationType == TwilioNotificationType.SMS)
{
if (iNotificationID == null)
throw new PXException(Messages.SMSNotificationIDNotSpecified);
}
else if (notificationType == TwilioNotificationType.OutBoundCall)
{
if (iNotificationID == null)
throw new PXException(Messages.CallNotificationIDNotSpecified);
}
else
throw new PXException(Messages.UNSpecifiedTwilioNotificationType);
//Get the notification
PX.SM.Notification notification = PXSelect<PX.SM.Notification,
Where<PX.SM.Notification.notificationID, Equal<Required<PX.SM.Notification.notificationID>>>>.
Select(invGraph, invGraphExt.TwilioSetupInfo.Current.SMSNotificationID);
if (notification == null)
throw new PXException(Messages.NotificationNotFound);
//Process the datafields and get Subject and Notification Body ready.
string subjectNotification = String.Format("{0} - {1}", (notificationType == TwilioNotificationType.SMS) ?
Messages.SMSSubjectPrefix : Messages.CallSubjectPrefix,
PX.Data.Wiki.Parser.PXTemplateContentParser.
Instance.Process(notification.Subject, invGraph,
invGraph.Document.Current.GetType(), null));
string bodyNotification = PX.Data.Wiki.Parser.PXTemplateContentParser.
Instance.Process(notification.Body, invGraph,
invGraph.Document.Current.GetType(), null);
//Create Twilio Notification
var twilio = new TwilioNotification(invGraphExt.TwilioSetupInfo.Current.TwilioAccountSid,
invGraphExt.TwilioSetupInfo.Current.TwilioAuthToken)
{
Origin = invGraphExt.TwilioSetupInfo.Current.TwilioFromPhoneNumber
};
if (notificationType == TwilioNotificationType.SMS)
{
twilio.SendSMS(contact.Phone1, PX.Data.Search.SearchService.Html2PlainText(bodyNotification));
}
else if (notificationType == TwilioNotificationType.OutBoundCall)
{
twilio.SendCall(contact.Phone1, PX.Data.Search.SearchService.Html2PlainText(bodyNotification));
}
else
throw new PXException(Messages.UNSpecifiedTwilioNotificationType);
//Create Activity in Acumatica
CreateTwilioNotificationActivity(invGraph, subjectNotification, bodyNotification);
}
The last line of the function was added later to add the notification to the document activity as a nice add-on. Please review the full code to see how this was done.
After this, we added the actions to trigger each notification type. This is done by using the [PXProcessButton]
attribute to each method. We wrapped the SendTwilioNotification
in a PXLongOperation
to send the notifications asynchronously and avoid blocking the user interface.
#region Actions
public PXAction<ARInvoice> SendSMSNotification;
[PXUIField(DisplayName = "Send SMS Notification", MapEnableRights = PXCacheRights.Insert, MapViewRights = PXCacheRights.Select)]
[PXProcessButton]
public virtual IEnumerable sendSMSNotification(PXAdapter adapter)
{
ARInvoice invoice = PXCache<ARInvoice>.CreateCopy(Base.Document.Current);
PXLongOperation.StartOperation(Base, delegate
{
ARInvoiceEntry invGraph = PXGraph.CreateInstance<ARInvoiceEntry>();
invGraph.Document.Current = invoice;
ARInvoiceEntryPXExt invGraphExt = invGraph.GetExtension<ARInvoiceEntryPXExt>();
invGraphExt.SendTwilioNotification(invGraph, TwilioNotificationType.SMS);
});
return adapter.Get();
}
public PXAction<ARInvoice> SendCallNotification;
[PXUIField(DisplayName = "Send Call Notification", MapEnableRights = PXCacheRights.Insert, MapViewRights = PXCacheRights.Select)]
[PXButton(SpecialType = PXSpecialButtonType.Cancel)]
public virtual IEnumerable sendCallNotification(PXAdapter adapter)
{
ARInvoice invoice = PXCache<ARInvoice>.CreateCopy(Base.Document.Current);
PXLongOperation.StartOperation(Base, delegate
{
ARInvoiceEntry invGraph = PXGraph.CreateInstance<ARInvoiceEntry>();
invGraph.Document.Current = invoice;
ARInvoiceEntryPXExt invGraphExt = invGraph.GetExtension<ARInvoiceEntryPXExt>();
invGraphExt.SendTwilioNotification(invGraph, TwilioNotificationType.OutBoundCall);
});
return adapter.Get();
}
#endregion
Finally we included rules to control when to enable the actions in the ARInvoice_RowSelected event and we added the actions to the Actions menu.
public override void Initialize()
{
TwilioNotificationSetup setup = TwilioSetupInfo.Current;
Base.action.AddMenuAction(SendSMSNotification);
Base.action.AddMenuAction(SendCallNotification);
}
Processing screen
As the last part of the module, we built a simple processing screen. The processing screen was build as a new graph that shows all invoices with pending balance.
A custom DAC added as a processing filter allows the user to select the action (SMS or voice notification) and to filter documents by date, workgroup or owner.
The process delegate for each selected document just calls the SendTwilioNotification
method with the selected action.
Conclusion
As we have seen, we were able to add very useful functionality to our ERP system in a few hours. By leveraging the capacities of Acumatica xRP and its Customization Framework we can offer powerful features to our customers in a shorter time by connecting to existing SaaS applications available right now.
Editors Note
The coding of the above integration was accomplished in only 4 hours! Take a moment to take that in to really appreciate, not only the power or the Acumatica xRP Platform – but also the skill and creativity of Marco and their team. You can see clearly, a very powerful combination.
For more information about Syntegh, please visit their website and Twitter feed.