Acumatica requirements for development

Hi everybody,

today I want to leave a short note regarding of what is needed, in order to be able to develop for Acumatica ERP.

REQUIREMENTS FOR DEV MACHINE

Display resolution: Minimum 1024 × 768, Typical 1920×1080

Adobe Reader: (to open Acumatica ERP PDF documents) 2019 or later

Microsoft Office: (to view documents exported from Acumatica ERP)

  • MS Office 2019
  • MS Office 2016
  • MS Office 2013
  • MS Office 2010
  • MS Office 2007
  • MS Office 2003 with the Microsoft Office 2007 compatibility pack

IIS

Web Browsers:

  • Microsoft Edge 44 or later
  • Mozilla Firefox 82 or later
  • Apple Safari 12 or later
  • Google Chrome 87 or later

As of June 15, 2022, Microsoft Internet Explorer is no longer supported by any version of Acumatica ERP as the browser has been retired by Microsoft who now recommends Microsoft Edge.

DATABASE REQUIREMENTS

Microsoft SQL Server: 2019, 2017, or 2016

MySQL Community Edition Server: 5.7 and 8.0 64-bit edition

MariaDB: Version 10

Memory: 8 GB RAM

CPU: 2 cores; 2 GHz

Hard Disk Space: For each database, 1 GB available hard disk space. Depending on the number of transactions, additional hard disk space may be required to store large numbers of transactions.

CODE AUTHORING ENVIRONMENTS

To create stand-alone applications with Acumatica ERP or develop customizations and add-on solutions on top of Acumatica ERP, you need one of the integrated development environments (IDEs) listed below.

Operating System

  • Windows 10
  • Windows Server 2019
  • Windows Server 2022

Microsoft Visual Studio with Microsoft Web Developer Tools:

  • 20xx: Community, Professional, and Enterprise editions ( xx stands for version numbers, 09, ..., 19, 22 )
  • (OR) Rider 

Summary

If to sum upp, if you want to develop Acumatica, you'll need Windows, IIS, Database and Visual Studio or Rider

How to remove validation of the Lot/Serial Class field on the Stock Items page

Today I want to share with you the article "How to remove the validation of the Lot/Serial Class field on the Stock Items page".

Recently I had a case where I wanted to change Lot/Serial Class at any time regardless of its use, but out of the box, Acumatica doesn't give this possibility and show an error or warning "Lot/serial class cannot be changed when its tracking method as it is not compatible with the previous class and the item is in use" as it is demonstrated in the image below.

There is a possibility of this warning

The red error comes from validation on FieldVerifying, but the yellow comes from the rule in INItemPlan.inventoryID.InventoryLotSerClassIDRule. Although looks like a warning, it reverts your change so it doesn't let you to change it.

The solution of the Lot/serial class can be changed as it is not a complicated process but can be quite time-consuming.

First of all we need to create a GraphExtension where by overriding the Initialize we provide ability to change the Lot/Serial class.

And we also need to override the FieldVerifiyng event and not call the base method so that this field is not validated.

The full code is here:

[PXCacheName(InventoryItemMaintExtCacheName)]
public class InventoryItemMaintExt : PXGraphExtension<InventoryItemMaint>
{
	private const string InventoryItemMaintExtCacheName = "InventoryItemMaintExt";
	public static bool IsActive() => true;
 
	public override void Initialize()
	{
		base.Initialize();
		Base.MakeRuleWeakeningScopeFor<InventoryItem.lotSerClassID>(RuleWeakenLevel.AllowEdit);
	}
 
	protected virtual void _(Events.FieldVerifying<InventoryItem,
	InventoryItem.lotSerClassID> e, PXFieldVerifying baseMethod)
	{
		//baseMethod?.Invoke(sender, e); 
		//skip the baseMethod so Messages.ItemLotSerClassVerifying is not thrown
	}
}

 And final word, use with caution. Because you may influence plenty of other pages in Acumatica.

 

 

 

How to customize PXDefault attribute of Acumatica

Sometimes you can get request from BA or client to add new field like this: “On the Customers (AR.30.30.00) screen add a checkbox field called Membership. It is a required field and the default value is False. The Membership field should be already there.”

How we usually develop it:

public class PACustomerExt : PXCacheExtension<Customer>
{
	public static bool IsActive() => true;
 
	#region UsrMembership
	[PXDBBool]
	[PXUIField(DisplayName = "Membership", Required = true)]
	[PXDefault(false)]
	public virtual bool? UsrMembership { getset; }
	public abstract class usrMembership : PX.Data.BQL.BqlBool.Field<usrMembership> { }
	#endregion
}

 

And as you know, this logic will work correct for new records in DB, but when you will change existing records in DB, than you will get nullable Exception because of this custom field and PXDefault attribute. “PersistingCheck = PXPersistingCkeck.Nothing” do not help, it excludes the required status of the custom field.

So we can fix this issue and develop custom Default attribute, and we do not need create graph extension and develop additional logic in events.

Here is example of correct logic according the request:

public class PACustomerExt : PXCacheExtension<Customer>
{
	public static bool IsActive() => true;
 
	#region UsrMembership
	[PXDBBool]
	[PXUIField(DisplayName = "Membership", Required = true)]
	[PXDefaultCustom(false)]
	public virtual bool? UsrMembership { getset; }
	public abstract class usrMembership : PX.Data.BQL.BqlBool.Field<usrMembership> { }
	#endregion
}
 
 
[PXAttributeFamily(typeof(PXDefaultAttribute))]
public class PXDefaultCustomAttribute : PXDefaultAttribute
{
	public PXDefaultCustomAttribute(object value) : base(value) { }
 
 
	public override void RowPersisting(PXCache sender, PXRowPersistingEventArgs e)
	{
		var fieldValue = sender.GetValue(e.Row, base.FieldName);
 
		if (fieldValue == null)
			sender.SetValue(e.Row, base.FieldName, false);
	}
}

 

Also you can customize PXDefault attribute with any logic that you need, one more example:

[PXAttributeFamily(typeof(PXDefaultAttribute))]
public class PXDefaultCustomAttribute : PXDefaultAttribute
{
	public PXDefaultCustomAttribute(object value) : base(value) { }
 
	public override void FieldDefaulting(PXCache sender, PXFieldDefaultingEventArgs e)
	{
		base.FieldDefaulting(sender, e); // you can raise base event and logic 
 
		e.NewValue = base._Constant;  // you can setup or check constant value of default attribute
 
		// develop needed logic
	}
 
	public override void RowPersisting(PXCache sender, PXRowPersistingEventArgs e)
	{
		// develop needed logic before value will be persisted to DB
 
		var fieldValue = sender.GetValue(e.Row, base.FieldName);
 
		if (fieldValue == null)
			sender.SetValue(e.Row, base.FieldName, false);
 
		// base.RowPersisting(sender, e);  - you can raise base event if you need, also you can set up PXPersistingCheck on you custom attribute
	}
}

 

How to use LoadOnDemand in PXSmartPanel

First of all I want to say that if you do not use "PXTabItem" in your "PXSmartPanel" you should not set LoadOnDemand(by default this attribute has "False" value).

In our example I will show you the difference between using this attribute for "PXSmartPanel" with "True" or "False" values. For that I prepared a short example:

We will use two TabItems (Stock Items for the first and Non-Stock Items for the second).

Let's create them in GraphExt.

public SelectFrom<InventoryItem>.Where<InventoryItem.stkItem.IsEqual<True>>.View StockItemView;
public SelectFrom<InventoryItem>.Where<InventoryItem.stkItem.IsEqual<False>>.View NonStockItemView;

 Let's add button to GraphExt and to the View. For this example we will use the Sales Orders page and add PXSmartPanel to the View.

public sealed class SoOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
    public static bool IsActive() => true;
 
    public PXSelect<SOLine> MyPanelView;
 
    public PXAction<SOOrder> noteAction;
    [PXUIField(DisplayName = "TestNoteButton", MapViewRights = PXCacheRights.Select, MapEnableRights = PXCacheRights.Update)]
    [PXButton(ImageKey = PX.Web.UI.Sprite.Main.DataEntryF)]
    protected IEnumerable NoteAction(PXAdapter adapter)
    {
        if (Base.Transactions.Current != null &&
            MyPanelView.AskExt() == WebDialogResult.OK)
        {
            //extra stuff here if needed when OK is pushed
        }
 
        return adapter.Get();
    }
 
    public SelectFrom<InventoryItem>.Where<InventoryItem.stkItem.IsEqual<True>>.View StockItemView;
    public SelectFrom<InventoryItem>.Where<InventoryItem.stkItem.IsEqual<False>>.View NonStockItemView;
}

 

ASPX. PXSmartPanel:

<px:PXSmartPanel runat="server" ID="PXSmartPanelNote" DesignView="Hidden" LoadOnDemand="false" CreateOnDemand="false" 
                     CaptionVisible="true" Caption="Order Notes" Key="MyPanelView">
        <px:PXTab ID="PXTab123" runat="server" Height="540px" Style="z-index100;" Width="100%">
            <Items>
                <px:PXTabItem Text="NonStockItem" >
                    <Template>
                        <px:PXGrid runat="server" ID="CstPXGrid4" Width="100%" DataSourceID="ds" SyncPosition="True">
                            <Levels>
                                <px:PXGridLevel DataMember="NonStockItemView">
                                    <Columns>
                                        <px:PXGridColumn DataField="StkItem" Width="60" />
                                        <px:PXGridColumn DataField="InventoryCD" Width="70" />
                                    </Columns>
                                </px:PXGridLevel>
                            </Levels>
                        </px:PXGrid>
                    </Template>
                </px:PXTabItem>
                <px:PXTabItem Text="StockItem">
                    <Template>
                        <px:PXGrid runat="server" ID="CstPXGrid5" Width="100%" SyncPosition="True" DataSourceID="ds">
                            <Levels>
                                <px:PXGridLevel DataMember="StockItemView">
                                    <Columns>
                                        <px:PXGridColumn DataField="StkItem" Width="60" />
                                        <px:PXGridColumn DataField="InventoryCD" Width="70" />
                                    </Columns>
                                </px:PXGridLevel>
                            </Levels>
                        </px:PXGrid>
                    </Template>
                </px:PXTabItem>
            </Items>
        </px:PXTab>
        <px:PXPanel runat="server" ID="PXPanel12" SkinID="Buttons">
            <px:PXButton runat="server" ID="btnMyNoteOk" Text="OK" DialogResult="OK" />
        </px:PXPanel>
    </px:PXSmartPanel>

 ASPX.  Callback:

<CallbackCommands>
    <px:PXDSCallbackCommand CommitChanges="true" Name="NoteAction" Visible="False" DependOnGrid="grid" />
</CallbackCommands> 

ASPX. Action Bar:

<px:PXToolBarButton Text="TestNoteButton" DependOnGrid="grid">
    <AutoCallBack Command="NoteAction" Target="ds" />
</px:PXToolBarButton>

 

 

 

 

 

How to publish customization with custom dll

Hi everybody,

today I want to share with you how to publish Acumatica customization with dll, which gives you errors during publish. For example, recently I've added Accord net library, and during publish got following errors:

Accord.Math.dll Failed to resolve method reference: System.Numerics.Complex& System.Numerics.Complex[0...,0...]::Address(System.Int32,System.Int32) declared in System.Numerics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Accord.Math.dll Failed to resolve method reference: System.Void System.Numerics.Complex[0...,0...]::Set(System.Int32,System.Int32,System.Numerics.Complex) declared in System.Numerics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Accord.Math.dll Failed to resolve method reference: System.Numerics.Complex System.Numerics.Complex[0...,0...]::Get(System.Int32,System.Int32) declared in System.Numerics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

Steps to deal with that are following:

1. Copy into clipboard error messages.

2. Create file cstValidationIgnore.txt

3. Paste content in the step 1 into file cstValidationIgnore.txt in the folder App_Data

4. Include that file into customization:

 

5. Publish.

 

After these steps I was able to get needed features of Accord.Net into Acumatica.

 

ConsentDate has been expirated in Acumatica

Today I will tell you how to fix the import error for CROpportunity "No consent date has been specified".

In order to import CROpportunity you need to create DataProvider and ImportScenario

Data Provider (SM206015)

Import Scenario (SM206025)

Now you have to go to the Import by Scenario (SM206036) screen and do the Import data.

After the successful import, you can see that a new Opportunity was created, but the Contact and Owner fields were not filled in.

This can easily be corrected by making changes to the Import scenario for

  • «Contact» - «ContactID!DisplayName»
  • «Owner» - «OwnerID!DisplayName»

Let's repeat the import on the page Import by Scenario (SM206036)

Import result as an example of one of the records

Good luck with your imports!

 

Good luck with your imports.

 

 

 

 

Display values from 3-rd party API in Acumatica

Good day everyone!

Today I want to share with you one experience with dynamic (virtual) data in Acumatica.

Imagine that you want to have a custom virtual view in Acumatica, and use it to get and update records in the cache.

It can be a lot of different situations such as getting data from a file and putting it into view or getting data from another view, updating the records and so on.

For example, let take a popular screen Sales Order (SO301000).

On updating the Document Details (SOLine) row, I want to have a custom control Availability of selected Item.

What does it look like in the code?

I created a SOOrderEntryExt graph extension and my virtual DAC:

 Snippet

[PXVirtual]

 

public class CustomVirtualDAC : IBqlTable
{
 
    #region InventoryID
    public abstract class inventoryID : PX.Data.BQL.BqlInt.Field<inventoryID> { }
 
    [PXInt(IsKey = true)]
    [PXDefault(PersistingCheck = PXPersistingCheck.Nothing)]
    [PXUIField(DisplayName = "InventoryID")]
    public virtual int? InventoryID
    {
        get;
        set;
    }
    #endregion
 
    #region SiteID
    public abstract class siteID : PX.Data.BQL.BqlInt.Field<siteID> { }
 
    [PXInt(IsKey = true)]
    [PXDefault(PersistingCheck = PXPersistingCheck.Nothing)]
    [PXUIField(DisplayName = "SiteID")]
    public virtual int? SiteID
    {
        get;
        set;
    }
    #endregion
 
    #region LocationID
    public abstract class locationID : PX.Data.BQL.BqlInt.Field<locationID> { }
    [PXInt(IsKey = true)]
    [PXDefault(PersistingCheck = PXPersistingCheck.Nothing)]
    [PXUIField(DisplayName = "LocationID")]
    public virtual int? LocationID
    {
        get;
        set;
    }
    #endregion
 
    #region AvailQty
    public abstract class availQty : PX.Data.BQL.BqlDecimal.Field<availQty> { }
    [PXDecimal(2)]
    [PXDefault(PersistingCheck = PXPersistingCheck.Nothing)]
    [PXUIField(DisplayName = "AvailQty")]
    public virtual decimal? AvailQty { getset; }
    #endregion
}

 After in SOOrderEntryExt I added my view and event:

 

public SelectFrom<CustomVirtualDAC>.View CustomVirtualView;
 
public virtual void _(Events.RowUpdated<SOLine> e)
{
    SOLine oldRow = e.OldRow as SOLine;
    SOLine row = e.Row as SOLine;
    CustomVirtualDAC currVirtualRez = null;
 
    if (row != null)
    {
        currVirtualRez = ReturnVirtualDACRez(row.InventoryID,
            row.SiteID, row.LocationID);
 
        if (row.OrderQty > oldRow.OrderQty)
        {
            //do what needed
            currVirtualRez.AvailQty -= row.OrderQty - oldRow.OrderQty;
            CustomVirtualView.Update(currVirtualRez);
        }
    }
 
}

 And one more method where I can return the needed row or add if it does not exists:

 

public CustomVirtualDAC ReturnVirtualDACRez(intinventoryIDintsiteIDintlocationID)
{
    CustomVirtualDAC currVirtualRez = null;
 
    foreach (CustomVirtualDAC mkinItemStatus in
        CustomVirtualView.Select())
    {
        if (mkinItemStatus.InventoryID == inventoryID && mkinItemStatus.SiteID == siteID && mkinItemStatus.LocationID == locationID)
        {
            currVirtualRez = mkinItemStatus;
        }
    }
 
    if (currVirtualRez == null)
    {
 
        InventorySummaryEnq tempGraph = PXGraph.CreateInstance<InventorySummaryEnq>();
        tempGraph.Filter.Current.InventoryID = inventoryID;
        tempGraph.Filter.Current.SiteID = siteID;
        tempGraph.Filter.Cache.Update(tempGraph.Filter.Current);
        foreach (InventorySummaryEnquiryResult record in tempGraph.ISERecords.Select())
        {
            CustomVirtualDAC newVirtualRez = new CustomVirtualDAC();
 
            newVirtualRez.InventoryID = record.InventoryID;
            newVirtualRez.SiteID = record?.SiteID;
            newVirtualRez.LocationID = record?.LocationID;
            newVirtualRez.AvailQty = record?.QtyAvail;
 
            CustomVirtualView.Insert(newVirtualRez);
        }
 
    }
 
    return currVirtualRez;
}

 

Everything seems ready and should work.But not. I receive error message Incorrect syntax near the keyword 'OPTION':

 

This is because Aсumatiсa is still trying to extract data from a table that does not exist. And none of the attributes such as [PXCopyPasteHiddenView] or [PXVirtualDAC] does not help.

To resolve this problem, you must implement a Dataview delegate when using a Virtual DAC.

So, in your graph extension you must add a dataview delegate to return the records that you need:

protected virtual IEnumerable AvailibilityView()

      {

       //fetch your records

         return your records;

      }

In my specific case, I must implement something like that:

 

public virtual IEnumerable customVirtualView()
{
    List<CustomVirtualDAClistRez = new List<CustomVirtualDAC>();
 
    foreach (CustomVirtualDAC line in CustomVirtualView.Cache.Inserted)
    {
        listRez.Add(line);
    }
 
    foreach (CustomVirtualDAC line in CustomVirtualView.Cache.Updated)
    {
        listRez.Add(line);
    }
    return listRez;
}

 Because I can`t call CustomVirtualView.Select() in the delegate because this will loop the code.

Now it will work.

 

Summary

In case if you need to read data from some 3-rd party source, and display it on the screen, then you have two ways:

1. Create useless table in Acumatica data base

2. Follow technique described in this article. 

 

 

 

 

 

 

 

 

 

 

 

Export from Acumatica to Excel via export scenario

Hi everybody,

today I want to leave a note on how to export from Acumatica to Excel.

To export data to excel, you need to go to the "Data Providers" page, screen SM206015,

then create a provider, fill in such fields as Name, Provider Type (in the case of Excel, select PX.DataSync.ExcelSYProvider), after which you need to add a file for export (where we we will display the data), and add it to Files,

then go to the SCHEMA tab, and fill in the correspondence of the fields into which we will export the data, 

we can click on the FILL SCHEMA OBJECTS / FILL SCHEMA FIELDS button in order to load ready-made fields with the previously loaded tables, or write your own, pay attention, the Active checkbox must be pressed.

After all this, don't forget to save your data.

 

 

Then we go to the "Export Scenarios" page, screen SM207025,

 

A Dac Extension Must Include The Publis Static Isactive Method

Hello everybody,

today  I want to share one line of code for Acuminator for error message:

PX1016 A DAC extension must include the public static IsActive method with the bool return type. Extensions which are constantly active reduce performance. Suppress the error if you need the DAC extension to be constantly active.

In case if you don't want to suppress Acuminator with a comment, you can do something like this inside of your extension:

public static bool IsActive() => true;

Certainly it is not the most elegant way of doing that, as better way could be usage of some attribute for this purpose, or for example use inheritance, but as of now, the smallest amount of code, you can use that line of code which is presented here. 

 

Below goes comment of Sergey Nikomarov:

  I want to notify you that the post has two issues: 1. IsActive method should be declared on graph extensions too. 2. It is completely OK to suppress this alert if your extension should be always active. This is the best practice and it is mentioned in the diagnostic documentation: https://github.com/Acumatica/Acuminator/blob/dev/docs/diagnostics/PX1016.md  > Suppress the error if you need the DAC extension to be constantly active.   In fact, you should never write IsActive() => true; This unnecessarily decreases the performance of your code since the platform will call this check every time.   I just wanted to give you these details although it would be great if you could update the post. I haven't found comments section for posts so I decided to write you here.   Best Regards, Sergey

 

 

 

Purpose Of Rowpersisting Event

 

Hello everybody,

today I want to leave a note on usage of RowPersisting event.

Quite often I see situations, when RowPersisting is used for making additional insertions to database. Also quite often I see cases when some additional inserts being performed to database. 

I want to warn against such an approach. Reason for that is that during RowPersisting event, Acumatica opens transaction scope. Because of that, additional readings from db, or additional persists to db in scope of RowPersisting may lead to performance degradation and even deadlocks. 

Purpose of RowPersisting event is kind of latest resort, in which you can modify your record before putting it to database. And it shouldn't be used for some other purposes. Other purposes of RowPersisting event is validate record before it was putted to database, or cancel commit operation through throwing of an exception.