How to use IKVM and Java in Acumatica along with jar

Hello everybody,

today want to share on how you can use IKVM in Acumatica.

Imagine following scenario. You need some business logic, but that business logic is implemented in some java library, and not in C#. 

How to deal with that?

Recently I have found solution: IKVM. IKVM allows you to create dll, which you can use inside of C#, but in scope of Acumatica you may need to download one of the IKVM builds from here

I used 8.2.3 as one of the recent builds.

Step 1. Use ikvmc

For creation of dll, you may need ikvmc tool. It may look simple usage. Navigate to ikvmc, and run something like 

ikvmc -target:library -out:jackcess-4.0.5.dll jackcess-4.0.5.jar commons-lang3-3.10.jar poi-4.1.1.jar commons-logging-1.2.jar junit-4.13.1.jar

2. Versioning of IKVMC

Quite puzzling may be which exact builds to use for creation. That can be found through command 

jar xf jackcess-4.0.5.jar

and looking on pom.xml, section dependencies, and there you'll find which jar files you need to download.

3. Create class library for working with created dll.

4. In created graph/graph extension use code like below:

List<string[]> rowsClosure = new List<string[]>();
 
            try
            {
                Thread thr = new Thread(
                    t =>
                    {
                        string[] assemblyPaths = new string[]
                        {
                            @"D:\SourceCode\IKVMAccess\IKVMAccess\AccessReader.Lib\bin\Debug\IKVM.Java.dll",
                            @"D:\SourceCode\IKVMAccess\IKVMAccess\AccessReader.Lib\bin\Debug\IKVM.Runtime.dll",
                            @"D:\SourceCode\IKVMAccess\IKVMAccess\AccessReader.Lib\bin\Debug\IKVM.Runtime.JNI.dll",
                            @"D:\SourceCode\IKVMAccess\IKVMAccess\AccessReader.Lib\bin\Debug\jackcess-4.0.5.dll", // Ensure this is the .NET assembly generated by IKVMC
                        };
 
 
                        // Load each assembly
                        foreach (string path in assemblyPaths)
                        {
                            Assembly.UnsafeLoadFrom(path);
                        }
 
                        // The path to the DLL
                        string dllPath = @"D:\Source\IKVMAccess\IKVMAccess\AccessReader.Lib\bin\Debug\AccessReader.Lib.dll";
 
                        // Load the assembly
                        Assembly assembly = Assembly.UnsafeLoadFrom(dllPath);
 
                        // Get the type of the class you want to create
                        Type type = assembly.GetType("AccessReader.Lib.Reader");
 
                        // Create an instance of the class
                        object instance = Activator.CreateInstance(type);
 
                        // Prepare the parameters for the method call
                        string accessFilePath = @"c:\Vision\delay.mdb";
                        string tableName = "staff";
 
                        // Get the MethodInfo object for the method you want to call
                        MethodInfo methodInfo = type.GetMethod("ReadAllRows");
 
                        // Call the method
                        // Note: we're assuming the method is public and non-static
                        object result = methodInfo.Invoke(instance, new object[] { accessFilePath, tableName });
 
                        // passing output from executed thread to calling thread
                        lock (lockthis)
                        {
                            rowsClosure = (List<string[]>)result;
                        }
                    }
 
                    );
                thr.Start();
                thr.Join();
            }
            catch (Exception e)
            {
                PXTrace.WriteError(e);
            }

 Couple of explanations on the provided code. You can't load dll inside of your main thread, as it will not work

 

 

 

 

 

 

Four types of Security types in Acumatica

One crucial feature that ensures data security and compliance within Acumatica is Row-Level Security (RLS).
Acumatica ERP supports a variety of scenarios for configuring the visibility of objects in the system. In the most common scenarios, you can create restriction groups. Acumatica ERP provides four basic types of restriction groups—A, A Inverse, B and B Inverse.

Let me show you with an example: I create two simple groups on the SM201030 screen. I name the first one "Some Items", Entity Type = "Inventory Item". Make sure the Active checkbox is checked. I select a couple of items: AACOMPUT01, AALEGO500, AAMACHINE1, AAPOWERAID. Select Group Type “A”.

Name of second is “Acomp350wind". Select the item "AACOMPUT01", which we have in the previous group and AM350WINDO".

Now let's go to the Restriction Groups by User page (SM201035). Select the user "admin", add our groups to him and save.

We do the same for the user "mendenhall", but select one group "Some Items".

Go to the "Stock Items" page (IN2025PL). Below are examples with different types of groups and results.


Differences between them:
Type "A" (Direct):

  • Direct access: Makes entities included in a group visible to users who are also included in that group.
  • Restricted access: Other users who do not belong to this group cannot view these entities.
  • Adding users: If a particular entity belongs to more than one Type A group, a user must be added to at least one of those groups to see that entity.

The admin user has access to all stock items.

The mendenhall user has access to 4 items from the "Some Items" group (AACOMPUT01, AALEGO500, AAMACHINE1, AAPOWERAID). Please note that it has access to AACOMPUT01 that is also in another group A type.

Other users do not have access to our stock items

Type "A Inverse":

  • Reverse access: Hides entities included in a group from users who are also included in that group.
  • Access by users outside the group: Users outside the group can view and use the entities.
  • Adding users: If a particular entity belongs to more than one A Inverse group, a user must be added to each of these groups, if necessary, to avoid being able to see that entity.

Change the type of each of our groups to A Inverse

The user admin does not have access to stock items from all groups.

The user mendenhall does not have access to all items in the "Some Items" group except AACOMPUT01, which is in another group. To prevent access to the item, you need to add all groups where it exists. In our case, there is another group "Acomp350wind" with AACOMPUT01.

Other users have access to all items.

 

Type "B" (Direct):

  • Direct access: Makes entities included in a group visible to users who are also included in that group.
  • Restricted access: Other users who do not belong to this group cannot view these entities.
  • Adding users: If a particular entity belongs to more than one type B group, a user must be added to each of these groups to see the entity.

Change the type of each of our groups to B.
The admin user has access to all stock items.

The user mendenhall has access to 3 items from the group "Some items" (AALEGO500, AAMACHINE1, AAPOWERAID). Note that he does not have access to AACOMPUT01 which is in the "Some Items" group. In order to have access to it, you need to add to this user all groups where AACOMPUT01 is. In our case, AACOMPUT01 is still in the "Acomp350wind" group, which is why we don't see it here.

Other users do not have access to our stock items

Type "B Inverse":

  • Reverse access: Hides entities included in a group from users who are also included in that group.
  • Access by users outside the group: Users outside the group can view and use the entities.
  • Adding users: If a particular entity belongs to more than one type B Inverse group, a user must be added to at least one of those groups to be able to see that entity.

Change the type of each of our groups to B Inverse.
The user admin does not have access to stock items from all groups.

The user mendenhall has access to only 1 item AM350WINDO.

Other users have access to all items.

The entry form cannot be automated. The view doesn't exist

Hello. Today I want to tell you about one possible issue during upgrading from for example 21R1 to 23R2 through adding pages into Custom Files in the Customization package.

After upgrading when I opened the screen which was added to Custom Files in the Customization package I faced the issue above. When I started discovering this issue I found that Acumatica could rename some of the views from version to version. And if you are using some components that are not in the Add Controls tab for screen editing better to not add this page file to Custom Files. And even if you leave this file in the package and will try to change the view name in ASPX manually it will not help you. You will face only new issues. To fix this issue, first of all update your site. Then delete these pages from Custom Files and try to add your components through editing ASPX in the package. Then press Generate Script and it would help you. After these steps you could publish customization. And i recommend you not to add pages to Custom Files to not facing such issues.

 

Acumatica TreeView: Understanding and Customizing in Acumatica ERP

Hello everyone! Today, I'd like to share my knowledge about the TreeView control in Acumatica, focusing on what it is, how to create a custom TreeView, and adding additional fields to an existing one.

  1. What is TreeView in Acumatica ERP?

In Acumatica, a TreeView is a crucial user interface element utilized to showcase hierarchical data in a tree-like structure. It empowers users to navigate through a hierarchy of records or data in a visually organized manner. Each node in the tree signifies a record or a category, with child nodes nested under their parent nodes, forming a hierarchical structure.

Acumatica employs numerous screens with these controls:

2. How to Create a Custom TreeView?

  1. Add a table with NoteID and ParentID values:

b. Add DAC class:

[Serializable]
[PXCacheName("CustomTreeView")]
public class CustomTreeView : IBqlTable
{
    public new abstract class noteID : BqlType<IBqlGuid, Guid>.Field<noteID>
    {
    }
 
    [PXDBGuid(false, IsKey = true)]
    [PXUIField(Visibility = PXUIVisibility.SelectorVisible)]
    public virtual Guid? NoteID
    {
        getset;
    }
 
    public new abstract class parentUID : BqlType<IBqlGuid, Guid>.Field<parentUID>
    {
    }
 
    [PXDBGuid(false)]
    [PXUIField(DisplayName = "Parent Folder")]
    public virtual Guid? ParentUID
    {
        getset;
    }
 
    public new abstract class name : BqlType<IBqlString, string>.Field<name>
    {
    }
 
    [PXDefault]
    [PXDBString(InputMask = "", IsUnicode = true)]
    [PXUIField(DisplayName = "ID", Visibility = PXUIVisibility.SelectorVisible)]
    public virtual string Name
    {
        getset;
    }
 
    public abstract class title : BqlType<IBqlString, string>.Field<title>
    {
    }
 
    [PXDefault(PersistingCheck = PXPersistingCheck.Nothing)]
    [PXString(InputMask = "", IsUnicode = true)]
    [PXUIField(DisplayName = "Name", Visibility = PXUIVisibility.SelectorVisible)]
    public virtual string Title
    {
        getset;
    }
 
    public abstract class pageID : BqlType<IBqlGuid, Guid>.Field<pageID>
    {
    }
 
    [PXGuid]
    [PXUIField(Visibility = PXUIVisibility.SelectorVisible)]
    public virtual Guid? PageID
    {
        getset;
    }
}
 

 c. Add a view in your Graph for selecting these values from the database and logic:

public SelectFrom<CustomTreeView>.View.ReadOnly Articles;
 
protected virtual IEnumerable articles(string PageID)
{
    Guid parentID = GUID.CreateGuid(PageID) ?? Guid.Empty;
 
    PXResultset<CustomTreeViewcustomTrees = null;
    if (PageID != null)
    {
        customTrees = SelectFrom<CustomTreeView>.Where<CustomTreeView.parentUID.IsEqual<BqlPlaceholder.P.AsGuid>>.View.ReadOnly.Select(Base, parentID);
    }
    else
    {
        customTrees = SelectFrom<CustomTreeView>.Where<CustomTreeView.parentUID.IsNull>.View.ReadOnly.Select(Base);
    }
 
    foreach (CustomTreeView childNode in customTrees)
    {
 
        yield return new CustomTreeView
        {
            PageID = childNode.NoteID,
            Title = childNode.Name,
            ParentUID = parentID,
        };
    }
}

 d. Add a field for a view to save the selected GUID in the database:

public abstract class getLinkTemplate : BqlType<IBqlGuid, Guid>.Field<getLinkTemplate>
{
}
 
[PXDBGuid(false)]
[PXUIField(DisplayName = "Template for External Links")]
public virtual Guid? GetLinkTemplate { getset; }

 e. Add a control in your .aspx page:

or unformatted text:

<px:PXTreeSelector runat="server" ID="edCustomTreeView" TreeDataSourceID="ds" TreeDataMember="Articles" InitialExpandLevel="0" PopulateOnDemand="True" ShowRootNode="False" CommitChanges="True" DataField="GetLinkTemplate">

                                                                             <DataBindings>

                                                                                                <px:PXTreeItemBinding TextField="Title" ValueField="PageID" />

                                                                         </DataBindings>

                                          </px:PXTreeSelector>

And that’s all. After all these steps it should be visible on Acumatica UI:

As you can see, it is quite simple and can be useful in various business cases.

  1. How to Add Additional Fields to the Existing One?

Suppose you added a custom tab for the PM304500 screen and need to add your custom DACs and fields to the existing TreeView:

How to do that?

You just need add [PXViewName ()] attribute to your view:

[PXViewName("Chemical Usage")]
public SelectFrom<ChemicalUsage>.Where<ChemicalUsage.contractCD.IsEqual<PMQuote.quoteProjectCD.FromCurrent>>.
    View ChemicalUsage;

 

An additional way to select data fields from any table and show them in a TreeView is to create a Generic Inquiry and add all tables that you need:

That’s all. Good luck with this experience.

Acumatica: redirection to screens from grid or how to enable hyper-link for grid fields

Hello developers,

Today I want to share with you the way how to enable hyper-link functionality from grids to another screen.

To enable a redirection to screen from the hyper-link in the grid, first we must be sure at two points:

  1. Field in grid has [PXSelector] attribute from foreign DAC table
  2. Foreign table (DAC) has [PXPrimaryGraph] attribute

If both steps are ready, then we go to the Customization Editor screen and add a needed field to Levels, like on screenshot:

Then we need enable “AllowEdit=True” attribute on field, publish customization and check results:

Results from the Sales Order screen, a hyper link with redirection function from grid to Sales Person screen:

One more example with custom screen:

First step, it is to develop the custom DAC with [PXPrimaryGraph()] attribute.

Example of source code of custom DAC and graph:

[Serializable]
[PXCacheName("AP TariffHTS Code DAC")]
[PXPrimaryGraph(typeof(APTariffHTSCodeEntry))]
public class APTariffHTSCode : AuditSystemFields, IBqlTable
{
    #region HSTariffCode
    [PXDBString(30, IsKey = true, InputMask = "9999.99.9999", IsUnicode = true)]
    [PXUIField(DisplayName = "Tariff Code", Visibility = PXUIVisibility.SelectorVisible)]
    public virtual string HSTariffCode { getset; }
    public abstract class hSTariffCode : BqlString.Field<hSTariffCode> { }
    #endregion
 
    #region  HSTariffCodeDescr
    [PXDBString(255, IsUnicode = true)]
    [PXUIField(DisplayName = "Tariff Code Description", Visibility = PXUIVisibility.SelectorVisible)]
    public virtual string HSTariffCodeDescr { getset; }
    public abstract class hSTariffCodeDescr : BqlString.Field<hSTariffCodeDescr> { }
    #endregion
 
    #region NoteID
    [PXNote()]
    public virtual Guid? NoteID { getset; }
    public abstract class noteID : PX.Data.BQL.BqlGuid.Field<noteID> { }
    #endregion
}
 
public class APTariffHTSCodeEntry : PXGraph<APTariffHTSCodeEntry, APTariffHTSCode>
{
    [PXFilterable]
    public SelectFrom<APTariffHTSCode>.View TariffHTSCodeView;
}

 

One more example with custom screen:

First step, it is to develop the custom DAC with [PXPrimaryGraph()] attribute.

Example of source code of custom DAC and graph:

[Serializable]
[PXCacheName("Vendor Duty DAC")]
public class INVendorDuty : AuditSystemFields, IBqlTable
{
    #region InventoryID
    [PXDBInt(IsKey = true)]
    [PXParent(typeof(SelectFrom<InventoryItem>.Where<InventoryItem.inventoryID.IsEqual<inventoryID.FromCurrent>>))]
    [PXDBDefault(typeof(InventoryItem.inventoryID))]
    [PXUIField(DisplayName = "Inventory ID", Visible = false)]
    public virtual int? InventoryID { getset; }
    public abstract class inventoryID : PX.Data.BQL.BqlInt.Field<inventoryID> { }
    #endregion
 
    #region VendorID
    [VendorNonEmployeeActive(IsKey = true, DisplayName = "Vendor ID",
        Visibility = PXUIVisibility.SelectorVisible, DescriptionField = typeof(Vendor.acctName), Filterable = true)]
    [PXDefault]
    public virtual int? VendorID { getset; }
    public abstract class vendorID : PX.Data.BQL.BqlInt.Field<vendorID> { }
    #endregion
 
    #region HSTariffCode
    [PXDBString(30, IsUnicode = true)]
    [PXUIField(DisplayName = "Tariff Code")]
    [PXSelector(typeof(SearchFor<APTariffHTSCode.hSTariffCode>))]
    [PXDefault(typeof(InventoryItem.hSTariffCode))]
    public virtual string HSTariffCode { getset; }
    public abstract class hSTariffCode : PX.Data.BQL.BqlString.Field<hSTariffCode> { }
    #endregion
 
    #region CountryID
    [PXDBString(100)]
    [PXUIField(DisplayName = "Country")]
    [Country]
    [PXDefault(typeof(SearchFor<Address.countryID>.Where<Address.bAccountID.IsEqual<vendorID.FromCurrent>>), PersistingCheck = PXPersistingCheck.Nothing)]
    public virtual string CountryID { getset; }
    public abstract class countryID : PX.Data.BQL.BqlString.Field<countryID> { }
    #endregion
 
    #region DutyPct
    [PXUIField(DisplayName = "Duty, %")]
    [PXDBDecimal(2, MinValue = 0, MaxValue = 100)]
    [PXDefault(TypeCode.Decimal, "0.000000", PersistingCheck = PXPersistingCheck.Nothing)]
    public decimal? DutyPct { getset; }
    public abstract class dutyPct : PX.Data.BQL.BqlDecimal.Field<dutyPct> { }
    #endregion
 
    #region EffectiveDate
    [PXDBDate()]
    [PXDefault(typeof(AccessInfo.businessDate), PersistingCheck = PXPersistingCheck.Nothing)]
    [PXUIField(DisplayName = "Effective Date")]
    public virtual DateTime? EffectiveDate { getset; }
    public abstract class effectiveDate : PX.Data.BQL.BqlDateTime.Field<effectiveDate> { }
    #endregion
 
    #region NoteID
    [PXNote()]
    public virtual Guid? NoteID { getset; }
    public abstract class noteID : PX.Data.BQL.BqlGuid.Field<noteID> { }
    #endregion
}

 

Then we customize another screen (in my case it is Stock Item) and we are going to have redirection to our custom screen (Tariff / HTS Code). So we develop new DAC with field (public string HSTariffCode) that has [PXSelector()] attribute. This DAC we put as a grid to the Stock Item screen on new tab.

Here is example of custom DAC with Parent-Child connection to Stock Item screen:

[Serializable]
[PXCacheName("Vendor Duty DAC")]
public class INVendorDuty : AuditSystemFields, IBqlTable
{
    #region InventoryID
    [PXDBInt(IsKey = true)]
    [PXParent(typeof(SelectFrom<InventoryItem>.Where<InventoryItem.inventoryID.IsEqual<inventoryID.FromCurrent>>))]
    [PXDBDefault(typeof(InventoryItem.inventoryID))]
    [PXUIField(DisplayName = "Inventory ID", Visible = false)]
    public virtual int? InventoryID { getset; }
    public abstract class inventoryID : PX.Data.BQL.BqlInt.Field<inventoryID> { }
    #endregion
 
    #region VendorID
    [VendorNonEmployeeActive(IsKey = true, DisplayName = "Vendor ID",
         Visibility = PXUIVisibility.SelectorVisible, DescriptionField = typeof(Vendor.acctName), Filterable = true)]
    [PXDefault]
    public virtual int? VendorID { getset; }
    public abstract class vendorID : PX.Data.BQL.BqlInt.Field<vendorID> { }
    #endregion
 
    #region HSTariffCode
    [PXDBString(30, IsUnicode = true)]
    [PXUIField(DisplayName = "Tariff Code")]
    [PXSelector(typeof(SearchFor<APTariffHTSCode.hSTariffCode>))]
    [PXDefault(typeof(InventoryItem.hSTariffCode))]
    public virtual string HSTariffCode { getset; }
    public abstract class hSTariffCode : PX.Data.BQL.BqlString.Field<hSTariffCode> { }
    #endregion
 
    #region CountryID
    [PXDBString(100)]
    [PXUIField(DisplayName = "Country")]
    [Country]
    [PXDefault(typeof(SearchFor<Address.countryID>.Where<Address.bAccountID.IsEqual<vendorID.FromCurrent>>), PersistingCheck = PXPersistingCheck.Nothing)]
    public virtual string CountryID { getset; }
    public abstract class countryID : PX.Data.BQL.BqlString.Field<countryID> { }
    #endregion
 
    #region DutyPct
    [PXUIField(DisplayName = "Duty, %")]
    [PXDBDecimal(2, MinValue = 0, MaxValue = 100)]
    [PXDefault(TypeCode.Decimal, "0.000000", PersistingCheck = PXPersistingCheck.Nothing)]
    public decimal? DutyPct { getset; }
    public abstract class dutyPct : PX.Data.BQL.BqlDecimal.Field<dutyPct> { }
    #endregion
 
    #region EffectiveDate
    [PXDBDate()]
    [PXDefault(typeof(AccessInfo.businessDate), PersistingCheck = PXPersistingCheck.Nothing)]
    [PXUIField(DisplayName = "Effective Date")]
    public virtual DateTime? EffectiveDate { getset; }
    public abstract class effectiveDate : PX.Data.BQL.BqlDateTime.Field<effectiveDate> { }
    #endregion
 
    #region NoteID
    [PXNote()]
    public virtual Guid? NoteID { getset; }
    public abstract class noteID : PX.Data.BQL.BqlGuid.Field<noteID> { }
    #endregion
}

After, we must replicate all steps in Customization Editor for Levels that I describe above (add field “HSTariffCode“ to Levels, enable AllowEdit attribute on it).

Here results, new Tab with grid on Stock Item screen and redirection hyper-link to our custom screen:

If you need enable redirection from a field on the Form to another screen, this approach is described by Acumatica team in T210-220 courses.

Wish you enjoy customizing Acumatica!

 

 

How to turn find out duration of SQL Query execution time

Hi everybody,

Want to share two important commands for MS SQL:

SET STATISTICS IO ON

SET STATISTICS TIME ON

After you turn it on, you may see following in the output window:

with this ouptut window you may see how long it took actually to execute some query, and if added indexes improved or didn't execution time.

What is important to pay attention to is Logical reads. Each logical read is equal to reading a chunk of 8Kb

Find the custom fields in Acumatica using ScreenUtils class

We'll guide you on how to show all custom fields on a screen, even if they are not associated with your package. You can use the PX.Api.ScreenUtils class to locate any field. This class contains a method called GetScreenInfo() that returns a Content. Inside the Content object, there's a field named Containers, which you can use to find the required fields.

When creating custom fields, it is necessary to add the prefix 'usr'. So, we can use it to find them.

Let’s imagine we need to show all custom fields in the SOShipments screen and show their DisplayName and FieldName using the action called FieldSeeker.

As an example, we have added a field with “NextSODetails” DisplayName and “UsrSODetails” FieldName. So, by clicking on the FieldSeeker button, we should see those details about the custom field.

 

Here is how you can get the Container field:

    // Acuminator disable once PX1016 ExtensionDoesNotDeclareIsActiveMethod extension should be constantly active
    public class SOShipmentEntryExt : PXGraphExtension<SOShipmentEntry>
    {
        #region Actions
        public PXAction<SOShipment> FieldSeeker;
        [PXUIField(Visible = true, DisplayName = "Field Seeker")]
        [PXButton(CommitChanges = true)]
        public virtual IEnumerable fieldSeeker(PXAdapter adapter)
        {
            string customFields = string.Empty;
            foreach (Container container in ScreenUtils.GetScreenInfo(Base.Accessinfo.ScreenID, truetrue)?.Containers?
                         .Where(x => x.Fields?.Any(f => f.FieldName.StartsWith("Usr")) == true))
            {
                foreach (Field customField in container.Fields.Where(f => f.FieldName.StartsWith("Usr")))
                {
                    customFields += "<b>DisplayName:</b> " + customField.Name +
                                    "<br><b>FieldName:</b> " + customField.FieldName + "<br><br>";
                }
            }
            Base.Document.Ask(customFields, MessageButtons.OK);
‚Äč
            return adapter.Get();
        }
        #endregion
    }

 

In this code, we are using the foreach loop to locate all the Containers in the Containers array and check if there is any Container including any field with the ‘Usr’ prefix. Since the Containers field is an array, it can have multiple containers, so we need to use another loop to retrieve all the records in each container. Therefore, we have a nested loop within the main loop to achieve this.

In the nested foreach loop, we look for every field that starts with 'Usr' within the Container. Once we find them, we assign their Name and FieldName values to an empty string variable that was created earlier.

As we want to show them on the pop-up, we call the Ask method from the graph view and give the customFields variable to that method as a message parameter.

Here is the result:

Summary

ScreenUtils class may be used for findinging customly added fields in Acumatica, and allows you to build flexible applications, which will interop with other customizations.

 

 

How to call Persist of PXgraph without triggering the code written in that graph

Sometimes there is a need to call the PXGraph.Persist method from a graph extension but without triggering the code written in the base graph itself. I had this issue with using the ARSalesPriceMaint graph, so will show all the examples on that graph extension.

The problem is, it’s not possible to just call it, as it is not reachable from the GraphExtension level.

There was a solution by using reflection. Something like this:

public void Persist(Intercompany.PersistDelegate baseMethod)
{
    //base.Persist();
    MethodInfo fooA = typeof(PXGraph).GetMethod("Persist",
        BindingFlags.Public | BindingFlags.Instance, nullnew Type[] { }, null);
    DynamicMethod baseBasePersist = new DynamicMethod("foo_A",
        nullnew[] { typeof(PXGraph) }, typeof(PXGraph));
    ILGenerator il = baseBasePersist.GetILGenerator();
    il.Emit(OpCodes.Ldarg, 0);
    il.EmitCall(OpCodes.Call, fooA, null);
    il.Emit(OpCodes.Ret);
    baseBasePersist.Invoke(nullnew object[] { Base });
 
}

 

The problem with this solution is that first of all, any error messages thrown from the base Persist method will not appear on the screen instead of them only the “Exception has been thrown by the target of an invocation” appears. The second and the bigger issue is – all the caches will remain not cleared. It will cause some unexpected behavior, when the already deleted wrong record won’t allow to save the screen.

Since for the PXGraph.Persist we don't need specific graph logic, instead of calling the PXGraph from ARSalesPriceMaint base graph, we can create a completely empty new custom graph, then call it from our graph extension while passing all the caches and views. Additionally, we can even add some validations to the persist at the graph level.

This is the code example:

internal class ARSalesPriceMaintPersistHelper : PXGraph<ARSalesPriceMaintPersistHelper>
{
    public override void Persist()
    {
        MyValidation();
        base.Persist();
    }
 
    protected virtual void MyValidation()
    {
        PXTrace.WriteInformation("Overriden Validation is called");
    }
}
 
public class ext : PXGraphExtension<ARSalesPriceMaint>
{
    public delegate void PersistDelegate();
    [PXOverride]
    public void Persist(PersistDelegate baseMethod)
    {
        ARSalesPriceMaintPersistHelper ph = PXGraph.CreateInstance<ARSalesPriceMaintPersistHelper>();
        ph.Views = Base.Views;
        ph.Caches = Base.Caches;
        ph.Persist();
        Base.Cancel.Press();
    }
}

 The Graph was made internal to try to avoid its appearance in different graph selector in Acumatica, but it can also be just public.

 Summary

In addressing the challenge of invoking PXGraph.Persist in Acumatica from a graph extension, there is a nuanced solution beyond the initial reflection method. The initial approach, while innovative, led to issues with error messaging and cache management. Solution involves creating a new, custom graph specifically for handling the Persist method. This method ensures functional integrity and enables custom validations, effectively bypassing the limitations of the reflection-based approach. The article concludes with a practical code example, highlighting the new graph's internal designation to restrict its visibility in Acumatica.

 

Localized reports in Acumatica

One of the recent additions to the new Acumatica 24R1 version is the ability to retrieve localized reports through REST API. This article provides a manual how specifically one can achieve it.

 

We start of creating an endpoint for reports. For this we navigate to the Web Service Endpoints (SM207060) screen. In this article we will create a new endpoint, but it also works with extended endpoints.

To create an endpoint click a “+” sign at the top toolbar. On the screen that opens, we specify the endpoint name and endpoint version and save the changes.

 

Now we need to create an entity. We can achieve it by clicking “Insert” button on the endpoint view at the left side of the screen

A pop-up window shows up, where we specify the name of an entity and the screen id, which it is going to be originated from. For this example, we will create an API endpoint for retrieving Invoice/Memo (SO643000) reports in pdf. Thus we populate the fields with the following values:

Click “OK”. Now, as we created the entity, we can manipulate parameters that are going to be specified in the HTTP request later, by selecting the “Fields” tab, if we need that. For this example the default settings are good enough, so we will leave them as they are.

Now let’s head in to Postman and test our endpoint.

 

First, we have to log into Acumatica. For this we send a POST request to this address:

{{baseUrl}}/entity/auth/login

Where baseUrl is the url to an Acumatica instance. In my case it is http://localhost/demo

In request body we specify username and password as follows:

An expected response is 204 No Content

 

Now we need to retrieve a link to a report. To do this, we send a POST request to this url:

 

{{baseUrl}}/entity/{{endpointName}}/{{endpointVersion}}/

{{entityName}}

 

Where {{endpointName}} is the name of an endpoint (in our case “Report”), {{endpointVersion}} is the version we specified for this endpoint (in our case 23.200.001), and {{entityName}} is the name of an entity we are trying to retrieve (in our case InvoiceMemo)

In headers we need to specify the following keys:

And then in the request body we specify the parameters values that we have set up at the “Fields” tab at the Web Service Endpoint screen.

We send a report and expected response is 202 Accepted. However, what we need in the response is the value of the Location header

This link specifies where we can retrieve our actual PDF report from. For this we copy the value and send a GET request to this address:

 

{{host}}/{{Location}}

 

Where {{host}} is the host of an Acumatica instance. In response we get our report .

Now let’s modify this request to retrieve a localized report.

 

For this we first have to navigate to the System Locales (SM200550) screen, create a new locale and check the “Active” checkbox.

Then we navigate to the Translation Dictionaries (SM200540) screen, search for a field and set a translation to it. Let’s take the “Unit Price” fiend and translate it and save the changes.

Now, going back to our Postman requests. In the request to get a report location, we specify yet another header called Accept-Language and assign it the value of the locale name as follows.

After that we repeat the same steps, copy the location and send a GET request to get an actual PDF report and in the end report generates with the translation we added.

 

OData Version 4.0 Changes in Acumatica 24R1

Hello everybody,

Today, I want to share with you a few differences between the previous version of OData and the new version, ODataV4. But before we dive in, let's set up our Postman environment to test it together. The first step is to create environment variables in Postman.

After creating these variables, go to the Authorization tab, choose the 'Basic Auth' authorization method, and set the corresponding variables for username and password.

A request for $metadata returns an additional attribute, Scale="Variable", for Edm.Decimal types, as shown in the following example. <Property Name="ControlAmount" Type="Edm.Decimal" Nullable="false" Scale="Variable"/>

When a $metadata request is made with the $format=json parameter specified, it now returns an error. In earlier versions, this parameter was ignored and XML was provided instead

If the response code is 404 Not Found, the response body will be empty.

If a request is made using an unsupported HTTP method (like POST, PUT, or DELETE), it will now return a 405 Method Not Allowed response instead of the previous 404 Not Found.

Now, a request that includes an invalid OData-Version header, like 6.0, will return a 400 Bad Request response instead of the earlier 404 Not Found.

"When a request for full metadata is made, as shown in the example below, the value of @odata.id differs from what was returned in previous versions.

The updated value now incorporates the full path to the service, like '@odata.id': '' http://localhost/Acumatica24R1Pre/odatav4/SOSetup"

In earlier versions, when a client made a $value request (like the example below) without specifying the Accept-Charset header, the UTF-8 BOM (byte order mark) was included in the response. However, in the current version, the UTF-8 BOM is no longer added.