I'm trying to use the Acumatica API (Version 4.20.2231) for the Lead screen to convert a Lead to a Business Account. I was a bit worried about this even before I tried it because when you use the "Convert to Business Account" Action in Acumatica, it pops up a small dialog box. Here's what it looks like in Acumatica after choosing Actions > Convert to Business Account:
With the API, the first arrangement of commands I tried was failing to convert the Lead and not producing any kind of error. Eventually, I found a sequence of commands that produced an error that references dialog boxes, so that makes me think I'm on the right track. Maybe I just don't know how to manipulate the dialog box using the Commands. Does anyone know where I'm going wrong? Here is my code:
Public Function ConvertLeadToCustomer(ByVal leadID As String, ByVal firstName As String, ByVal lastName As String, ByRef companyName As String) As String
Dim CR301000 As CR301000Content = m_context.CR301000GetSchema()
m_context.CR301000Clear()
' converting a lead requires that there is a value for company, so create one if it is blank
If companyName = "" Then
companyName = lastName & ", " & firstName
End If
' create key field
Dim leadKeyValue As Value = New Value With {.LinkedCommand = CR301000.LeadSummary.LeadID, .Value = leadID}
' create company field, since its required
Dim companyValue As Value = New Value With {.LinkedCommand = CR301000.DetailsSummary.CompanyName, .Value = companyName, .Commit = True}
Dim updateLeadCommands As Command() = {leadKeyValue, CR301000.Actions.ActionConvertToBAccount, companyValue, CR301000.Actions.Save}
Dim updateLeadResult As CR301000Content() = m_context.CR301000Submit(updateLeadCommands)
' TO DO: search Business Accounts by name to find new Business Account ID
Dim newBAID As String = ""
Return newBAID
End Function
And here is the error returned when CR301000Submit is called:
System.Web.Services.Protocols.SoapException: Server was unable to process request. ---> PX.Data.PXDialogRequiredException
at PX.Data.DialogManager.a(String A_0, PXGraph A_1, String A_2, Object A_3, String A_4, String A_5, MessageButtons A_6, MessageIcon A_7, Boolean A_8, InitializePanel A_9)
at PX.Data.DialogManager.AskExt(PXView view, String key, InitializePanel initializeHandler, Boolean repaintControls)
at PX.Data.PXView.AskExt(InitializePanel initializeHandler, Boolean refreshRequired)
at PX.Objects.CR.LeadMaint.ConvertToBAccount(PXAdapter adapter)
at PX.Data.PXAction`1.a(PXAdapter A_0)
at PX.Data.PXAction`1.<Press>d__c.MoveNext()
at PX.Data.PXAction`1.<Press>d__c.MoveNext()
at PX.Data.PXAction`1.<Press>d__c.MoveNext()
at PX.Data.PXAction`1.<Press>d__c.MoveNext()
at PX.Api.SyImportProcessor.SyStep.CommitChanges(Object itemToBypass, PXFilterRow[] targetConditions)
at PX.Api.SyImportProcessor.ExportTableHelper.ExportTable()
at PX.Api.ScreenUtils.Submit(String screenId, Command[] commands, SchemaMode schemaMode, PXGraph graph)
at PX.Api.Services.ScreenService.Submit(String id, IEnumerable`1 commands, SchemaMode schemaMode)
at PX.Api.Soap.Screen.ScreenGate.Submit(Command[] commands)
--- End of inner exception stack trace ---
Your current question
Your error occurs because the action ConvertToBAccount is calling a Pop-Up Dialog and is expecting an answer:
if (AccountInfo.AskExt((graph, view) => graph.Views[view].Cache.Clear(), true) != WebDialogResult.OK) return contacts;
The way to tell Acumatica of it answer is to send the value "OK" before even calling the action. Depending on your configuration you might also want populate the field in this pop-up:
Dim commandsConvert As Command() =
{
New Value With {.Value = leadID, .LinkedCommand = CR301000.LeadSummary.LeadID, .Commit = True},
New Value With {.Value = "OK", .LinkedCommand = CR301000.NewAccountServicesSettings.ServiceCommands.DialogAnswer, .Commit = True}, 'This is how close the pop-up. We fill the field from the pop-up after this line
New Value With {.Value = newCompanyCD, .LinkedCommand = CR301000.NewAccountServicesSettings.BAccountID}, 'With autonumbering On, no need for this line.
New Value With {.Value = newCompanyName, .LinkedCommand = CR301000.NewAccountServicesSettings.AccountName}, 'The default value will be taken from DetailsSummary.CompanyName
CR301000.Actions.ActionConvertToBAccount
}
m_context.CR301000Submit(commandsConvert)
Your future question
Converting from a Lead to a BAccount is a 2-step process where you are redirected to your newly created BAccount and where you need to save it. It will not be converted as long as you don't save it.
This is generally a pretty straight forward process or you simply submit Save to the page where you were redirected (CR303000):
'Once the Process is completed, We want to save the new record.
'If we want to edit some information on the new Business Account
'this is the right place to do it.
Dim newBAID As String = String.Empty
Dim commandsBAccount As Command() =
{
CR303000.Actions.Save,
CR303000.AccountSummary.BusinessAccount
}
Dim newBAccountContent As CR303000Content() = m_context.CR303000Submit(commandsBAccount)
If newBAccountContent.Length > 0 Then
newBAID = newBAccountContent(0).AccountSummary.BusinessAccount.Value
End If
As long as you keep the same cookie container, the UserState should be aware that you are currently on a dirty CR303000 with cached information. If you use a custom web service endpoint that included both CR301000 and CR303000 there is nothing to deal with.
Unfortunatly, in this scenario it doesn't work.
It seems that the PXRedirectRequiredException is raised from within a PXLongOperation (read Thread) and that the Web-Service doesn't pick the dirty state of BAccount. The only solution I can find right now is to customize the action ConvertToBAccount to remove threading:
public class LeadMaintExt : PXGraphExtension<LeadMaint>
{
[PXUIField(DisplayName = Messages.ConvertToBAccount, MapEnableRights = PXCacheRights.Update, MapViewRights = PXCacheRights.Select)]
[PXButton(ImageKey = PX.Web.UI.Sprite.Main.Process)]
public virtual IEnumerable ConvertToBAccount(PXAdapter adapter)
{
List<Contact> contacts = new List<Contact>(adapter.Get().Cast<Contact>());
foreach (Contact lead in contacts)
{
if (Base.AccountInfo.AskExt((graph, view) => graph.Views[view].Cache.Clear(), true) != WebDialogResult.OK) return contacts;
bool empty_required = !Base.AccountInfo.VerifyRequired();
BAccount existing = PXSelect<BAccount, Where<BAccount.acctCD, Equal<Required<BAccount.acctCD>>>>.SelectSingleBound(Base, null, Base.AccountInfo.Current.BAccountID);
if (existing != null)
{
Base.AccountInfo.Cache.RaiseExceptionHandling<LeadMaint.AccountsFilter.bAccountID>(Base.AccountInfo.Current, Base.AccountInfo.Current.BAccountID, new PXSetPropertyException(Messages.BAccountAlreadyExists, Base.AccountInfo.Current.BAccountID));
return contacts;
}
if (empty_required) return contacts;
Base.Save.Press();
//PXLongOperation.StartOperation(this, () => ConvertToAccount(lead, AccountInfo.Current));
LeadMaint.ConvertToAccount(lead, Base.AccountInfo.Current);
}
return contacts;
}
}
I am looking for a better way to address the situation and I'll edit my answer when I find it.
The commands specified should be for the dialog not the summary.
The required fields and the dialog answer should be provided before calling the action.
Below is the sample used for create Receipt action in receipts screen.
Note: C# code
commands = new Command[]
{
new Value
{
Value = CreatedOn.HasValue ? ((DateTime)CreatedOn.Value).ToLongDateString() : "",
LinkedCommand = SOSchema.SpecifyShipmentParameters.ShipmentDate,
Commit = true
},
new Value
{
Value = "OK",
LinkedCommand = SOSchema.SpecifyShipmentParameters.ServiceCommands.DialogAnswer,
Commit = true
},
SOSchema.Actions.ActionCreateReceipt
};
context.SO301000Submit(commands);
Hope this helps.
Related
I have an activity which receives an intent with a putExtra from other activity.
And I want to create a SharedPreference each time the activity receives the putExtra value, in this case a String so I can show all the Strings stored and show in a TextView without loosing the previous String shown.
tvTextView = (TextView)findViewById(R.id.tvTextView);
Bundle extras = getIntent().getExtras();
linearLayout = (LinearLayout)findViewById(R.id.linearLayout);
if (extras != null) {
newNote = extras.getString("Note");
Button noteButton = new Button(this);
noteButton.setText(newNote);
linearLayout.addView(noteButton);
// and get whatever type user account id is
SharedPreferences prefs = getSharedPreferences("MisPreferencias",getApplication().MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("newNote", note);
editor.commit();
String note = prefs.getString("newNote", "Welcome");
tvDiario.setText(note);
This is my code but it only saves the last string I want to show a string and then when I get a different String from intent show it but keep showing the previous, as a story. I need to edit the SharedPreference with different values each time the activity receive the putExtra data.
I don't think you fully understand SharedPreferences; it's a key-value data storage. By calling:
editor.putString("newNote", note);
you always override your saved value under the newNote key by new value note. editor.putString does not append your new data to your stored data, it rewrites the data.
The solution: you need to get your stored data first, append new data to those stored data, and store the updated data. Try:
newNote = extras.getString("Note");
...
SharedPreferences prefs = getSharedPreferences("MisPreferencias", getApplication().MODE_PRIVATE);
String storedNotes = prefs.getString("notes", "");
SharedPreferences.Editor editor = prefs.edit();
editor.putString("notes", storedNotes + newNote + "\n");
editor.commit();
String notes = prefs.getString("notes", "");
tvDiario.setText(notes);
I exchanged your newNote key by notes to better describe what is actually stored. I also recommend to read on documentation of Editor.putString and SharedPreferences.getString, I feel like you don't have a clear idea of the interface yet.
Our Business Accounts in Acumatica have 13 custom Attributes for our main Business Account Class. I've been able to save values to the Attributes successfully, based on Acumatica's example "Adding Records to the Business Accounts and Opportunities Forms". But I have not been able to figure out how to retrieve the values with an Export.
First, I tried using a format similar to how the field was specified when saving them.
Public Function GetCustomerAttributes(ByVal customerID As String) As String()()
Dim customer As CR303000Content = m_context.CR303000GetSchema()
m_context.CR303000Clear()
Dim idFilter As Filter = New Filter()
idFilter.Field = customer.AccountSummary.BusinessAccount
idFilter.Condition = FilterCondition.Equals
idFilter.Value = customerID
' SIMILAR TO EXAMPLE FOR SAVING
Dim awdField As Field = New Field()
awdField.ObjectName = customer.Attributes.Attribute.ObjectName
awdField.FieldName = "AWD Number"
Dim searchfilters() As Filter = {idFilter}
Dim searchCommands() As Command = {awdField}
Dim searchResult As String()() = m_context.CR303000Export(searchCommands, searchfilters, 0, False, False)
Return searchResult
End Function
I thought this would return one result with the value for our attribute named "AWD Number". Instead, it returned 13 results, one for each attribute, and the value of each one was blank. I changed the FieldName to customer.Attributes.Attribute.FieldName and then it started returning the name of each attribute. So I thought if I added another field for the value, then I might get the name and value in separate results, like this:
Public Function GetCustomerAttributes(ByVal customerID As String) As String()()
Dim customer As CR303000Content = m_context.CR303000GetSchema()
m_context.CR303000Clear()
Dim idFilter As Filter = New Filter()
idFilter.Field = customer.AccountSummary.BusinessAccount
idFilter.Condition = FilterCondition.Equals
idFilter.Value = customerID
Dim awdField As Field = New Field()
awdField.ObjectName = customer.Attributes.Attribute.ObjectName
awdField.FieldName = customer.Attributes.Attribute.FieldName
Dim awdValue As Field = New Field()
awdValue.ObjectName = customer.Attributes.Attribute.ObjectName
awdValue.FieldName = customer.Attributes.Attribute.Value
Dim searchfilters() As Filter = {idFilter}
Dim searchCommands() As Command = {awdField, awdValue}
Dim searchResult As String()() = m_context.CR303000Export(searchCommands, searchfilters, 0, False, False)
Return searchResult
End Function
I did get a 2-item array back for each of the 13 results, but the value in the second field was still blank.
Does anyone know how I can get the values? I don't really care if I have to get them one at a time, but I'd prefer to get them all at once with their names or codes so that I don't have to rely on the indices always staying the same. Below are images of the debugger running on my second example and view in Acumatica. Thanks!
Your first attempt is correct, however you're not using the right object name and field name. The system will dynamically add fields to the primary object (view) of the screen, in this case the object name represented by customer.AccountSummary.BusinessAccount.ObjectName variable (I suggest you use the debugger to see what this value equals too - good learning exercise).
The attribute field name will use the same naming convention as used in How To Retrieve An Attribute Field In StockItems In Acumatica API?. The naming convention is _Attributes. The attribute ID is not the attribute name; I don't see your configuration but I doubt in your case the Attribute ID is "AWD Number". To summarize, the code will look like:
Dim awdField As Field = New Field()
awdField.ObjectName = customer.AccountSummary.BusinessAccount.ObjectName
awdField.FieldName = "AWDNumber_Attributes"
In your example, by putting the Attributes.Attribute.ObjectName object, the system will iterate through all values inside this table, and then return for every row the fields you want. I'm not exactly sure why you're not seeing all the attribute values in this case, but I think you should be fine with the example above.
I have a problem with creating a page programmatically with a page template. The creation of the page is working, but it is not applying the page template to the page. I have searched and found always the same code snippets. There is also no error.
ThemeDisplay themeDisplay = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY);
long userId = themeDisplay.getUserId();
long groupId = themeDisplay.getScopeGroupId();
boolean privateLayout = false;
long parentLayoutId = 0;
String name = "Testpage";
String title = null;
String description = null;
String type = LayoutConstants.TYPE_PORTLET;
boolean hidden = false;
String friendlyURL = "/testpage";
ServiceContext serviceContext = new ServiceContext();
serviceContext.setAddGroupPermissions(true);
serviceContext.setAddGuestPermissions(true);
Group group = themeDisplay.getScopeGroup();
serviceContext.setScopeGroupId(group.getGroupId());
serviceContext.setUserId(themeDisplay.getUserId());
Layout layout = LayoutLocalServiceUtil.addLayout(userId, groupId, privateLayout, parentLayoutId, name, title, description, type, hidden, friendlyURL, serviceContext);
List<LayoutSetPrototype> lsps = LayoutSetPrototypeServiceUtil.search(themeDisplay.getCompanyId(), Boolean.TRUE, null);
for(LayoutSetPrototype lsp : lsps) {
System.out.println("->" + lsp.getName() + " - " + lsp.getLayoutSetPrototypeId());
if (HtmlUtil.escape(lsp.getName()).contains("kDSSMP")){
System.out.println("hit");
LayoutTypePortlet ltp = (LayoutTypePortlet) layout.getLayoutType();
ltp.setLayoutTemplateId(userId, lsp.getUuid(), false);
LayoutLocalServiceUtil.updateLayout(layout.getGroupId(), layout.isPrivateLayout(),layout.getLayoutId(), layout.getTypeSettings());
addResources(layout, PortletKeys.DOCKBAR);
}
}
Basic question (please correct if I'm in wrong): "hit" is printed and template is not applied?
Just a background question: do you know that LR uses Layout for referring a page and template to indicate a page template? (I think yes)
(If my assertion about "hit" is correct) I don't try your code... but at first look it seems correct.
I have a similar snippet of code (for LR 6.1) in which I wrote:
layoutTypePortlet.setLayoutTemplateId(user.getUserId(), layoutTemplate);
LayoutLocalServiceUtil.updateLayout(layout);
The only differences seem to be on the setLayoutTemplateId (but false is correct to don't check for permissions) and on LayoutLocalServiceUtil.updateLayout.
On the last point Javadoc says:
updateLayout(Layout layout): Updates the layout in the database or adds it if it does not yet exist.
and
updateLayout(long groupId, boolean privateLayout, long layoutId, String typeSettings): Updates the layout replacing its type settings.
The last note is about lsp.getUuid() using... are you sure it is correct?
In javadoc it is declared as "newLayoutTemplateId" and not UUID.
Use LayoutPrototype instead of LayoutSetPrototype. This is Page Template vs. Site Template.
I have a plugin where i am creating a new case and I want to send an email out as it is created including its ticketnumber. I have attempted just to call this in the plugin but it is coming back saying that it is not present in the dictionary. I know this field is populated using CRM's own autonumbering so what i'm guessing is happening is that my plugin is firing and creating the case but then i'm trying to use this field before the autonumber has completed.
So is there a way that i can get my plugin to "wait" until this field is available and then use it?
Thanks
EDIT: Code below:
string emailBody = entity.Attributes["description"].ToString();
int bodyLength = emailBody.Length;
int textStart = emailBody.IndexOf(">") + 1;
int newLength = bodyLength - (textStart + 7);
string description = emailBody.Substring(textStart, newLength);
//create complaint
Entity complaint = new Entity("incident");
complaint["description"] = description;
complaint["ts_case_type"] = 717750001;
complaint["ts_registration_datetime"] = DateTime.Now;
complaint["ownerid"] = Owner;
complaint["customerid"] = Organisation;
Service.Create(complaint);
As a side I would suggest sending the email with a workflow if possible, it will be far easier to maintain in the long run and quicker to implement in the short.
In any case to answer your question, from what you have here you need to update your code to retrieve the ticketnumber once you have created the incident. You can do this with a Retrieve message.
For example:
//Create the complaint
Entity complaint = new Entity("incident");
//This is the information that is being sent to the server,
//it will not be updated by CRM with any additional information post creation
complaint["description"] = description;
complaint["ts_case_type"] = 717750001;
complaint["ts_registration_datetime"] = DateTime.Now;
complaint["ownerid"] = Owner;
complaint["customerid"] = Organisation;
//Capture the id of the complaint, we will need this in a moment
Guid complaintId = Service.Create(complaint);
//complaint["ticketnumber"] <-- The server does not populate this information in your object
//Retrieve the ticketnumber from the incident we just created
Entity complaintRetrieved = service.Retrieve("incident", complaintId, new ColumnSet("ticketnumber"));
//Get the ticketnumber
String ticketNumber = (String)complaintRetrieved.Attributes["ticketnumber"];
Like James said in comment, if you just want to send email with some case properties, it is best to do that with workflow (on case create).
In your plugin, ID is generated, and you can get it with:
entity.Attributes["ticketnumber"]
i'm implementing a sand box solution where is should have more than one column a unique key, i have to use the item adding event receiver but how to get the current adding item field values to know if this item is occurred within the list.
thanks
Create UniqueID column and make it unique.
Create an event receiver as follows:
public override void ItemAdding(SPItemEventProperties properties)
{
string Name = properties.AfterProperties["Name"].ToString();
string Title = properties.AfterProperties["Title"].ToString();
StringBuilder StringBuilder = new StringBuilder(Name);
StringBuilder.Append("-");
StringBuilder.Append(Title);
properties.AfterProperties["UniqueID0"] = StringBuilder.ToString();
base.ItemAdding(properties);
}