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.

 

 

 

 

 

 

Lightweight Persist To Database

 

Hello everybody,

today I want to describe following use case. Quite often it is needed to persist to database one or another DAC class, which is filled by some data. 

As usually I see people do this via hard coding of DAC class inside of the Graph. But today I want to share with you a way of persisting DAC class without hardcoding it as a view. 

In order to accomplish this, you can use following graph:

public class ImportEntitiesInsertion : PXGraph<ImportEntitiesInsertion>
{
    public string AddView(Type dacType)
    {
        var viewName = "_DYNAMIC_" + dacType.GetLongName();
        if (!this.Views.ContainsKey(viewName))
        {
            var command = BqlCommand.CreateInstance(typeof(Select<>), dacType);
            var newView = new PXView(thistrue, command);
            Views.Add(viewName, newView);
            Views.Caches.Add(dacType);
        }
        return viewName;
    }
}

 

After that, in some other place of the code, you can use this graph like this:

 

var graphForInsertion = PXGraph.CreateInstance<ImportEntitiesInsertion>();
var dacType = typeof(SOOrder); 
var viewName = graphForInsertion.AddView(dacType);
 
for (int i = 0; i < 10; i++)
{
    var newOrd = new SOOrder();
    graphForInsertion.Views[viewName].Cache.Insert(newOrd);
}
graphForInsertion.Persist();

 

 What I especially like about this approach, is that records will be persisted initially in the cache, and only after you'll call Persist, all bunch of records will be persisted to database.

 

 

How To Override Properly Creatematrixitems

 

Hello everybody,

today I want to leave a short snippet on how to override methods in CreateMatrixItemsImpl graph extension. Below goes code snippet you can use for this purpose:

 

public class CreateMatrixItemsImplExt : PXGraphExtension<CreateMatrixItems.CreateMatrixItemsImpl, CreateMatrixItems>
{
    public override void Initialize()
    {
        base.Initialize();
    }
}

With help of this code fragment you can override and customize a bit more Matrix management of Acumatica.