Another process has updated the record, your changes will be lost

Hello everybody,

Today a want to share with you couple approaches that can help you to fix the famous Acumatica’s error:

“Another process has updated the {Table} record. Your changes will be lost”

You can get this error when you open two the same screens with the same record from DB and then modify data on both screens and try to Save.

Or you may have custom logic that runs some functionality in PXLongOperation (that is, run in multi-threading mode) and one record from DB can be modified by different threads. Then you will get this error during persisting.

Or maybe you need to create and to release several documents while code modifies the same record or uses same inherited table. For example, you create and release invoice, and then create and release payment, both of them update the ARRegister table.

 

Generally speaking, we get this error when someone (something) tries to store in the DB a record that was modified earlier and the record now has another stage (i.e., another value). Means there is a conflict between user’s copy of data and actually stored in database.

In what way may it come? As you may know Acumatica persists rows one by one (in most cases). And before persisting to DB, Acumatica keeps data and changes in a graph’s specified cache. So, when a record is opened in several screens, the record will be kept in different caches. Thus, it might be that several different screens might have different versions of same record with different changes. That may cause concurrent updates, when a database needs to understand changes’ priority.

To handle concurrent updates Acumatica uses the special Timestamp data field. In the DAC this field has PXDBTimestamp attribute. In database this column has name TStamp with timestamp data type.

Acumatica handles the Timestamp field automatically and checks the record version every time the record is modified. When a record is updated, the Timestamp field is increased, in this way every next version of a record will have bigger Timestamp value than pervious.

Then, each time a graph needs to persist a record or changes to the DB, Acumatica verifies the version of the record to ensure that the record has not been modified by another process. And if Timestamp value in the cache differs against Timestamp value in the DB, you’ll get the error we are talking about here.

Yes, there might be many honest reasons for this mismatching. Like, another user or another code has really done changes the same time. Or record has been created with mistake, and Acumatica may not find corresponding record in DB.

 

We propose you three approaches that can help fix concurrent update error and to allow running code logic till the end.

You may find two first approaches in source code of Acumatica Framework; Acumatica often uses them.

  1. To use SelectTimeStamp() graph’s method

When you invoke this methos, a graph selects and sets the TimeStamp field with new value and then you can persist data to the DB.

Here one of Acumatica’s examples — the Persist method from the VendorMaint graph:

		public override void Persist()
		{
			using (PXTransactionScope ts = new PXTransactionScope())
			{
				bool persisted = false;
				try
				{
					BAccountRestrictionHelper.Persist();
                    persisted = (base.Persist(typeof(Vendor), PXDBOperation.Update) > 0);
				}
				catch
				{
					Caches[typeof(Vendor)].Persisted(true);
					throw;
				}
				base.Persist();
				if (persisted)
				{
					base.SelectTimeStamp();
				}
				ts.Complete();
			}
		}
And one more example:
private void PrepareForPOCreate(List<SOLine> listSO)
{
    foreach (SOLine item in listSO)
    {
        item.Qty = item.GetExtension<APSOLineExt>().UsrMasterSOQty;
 
        var inventoryItem = UpdatePOOrderExtension.CheckDefVendor(this.Base, item.InventoryID);
 
        if (inventoryItem == null)
        {
            Base.Caches<SOLine>().SetValueExt<SOLine.pOCreate>(item, false);
            Base.Transactions.Cache.Update(item);
        }
    }
 
    Base.SelectTimeStamp();
    Base.Save.Press();
}

 2. To use PXTimeStampScope.SetRecordComesFirst() method

Sometimes first approach doesn’t help and the error still occurs.

This is because, by default, Acumatica checks TimeStamp only when key fields have been modified.

Calling to the SetRecordComesFirst() method activates RecordComesFirst flag and hereupon Acumatica checks TimeStamp value for all changes in cache.

Here is an example how to create and release Payment:

private static ARPayment CreateAndReleasePayment(ARInvoice arInvoice)
{
    ARPaymentEntry paymentEntry = PXGraph.CreateInstance<ARPaymentEntry>();
 
    var arAdjust = SelectFrom<ARAdjust>.Where<ARAdjust.adjdRefNbr.IsEqual<@P.AsString>>.View.Select(paymentEntry, arInvoice.RefNbr)?.TopFirst;
 
    if (arAdjust?.AdjgRefNbr == null)
    {
        paymentEntry.CreatePayment(arInvoice, null, arInvoice.DocDate, arInvoice.FinPeriodID, false);
 
        paymentEntry.Document.Current.ExtRefNbr = arInvoice.DocDesc;
        paymentEntry.Document.Current.BranchID = arInvoice.BranchID;
        paymentEntry.Document.UpdateCurrent();
        paymentEntry.Save.Press();
    }
    else
    {
        paymentEntry.Document.Current = SelectFrom<ARPayment>.Where<ARPayment.refNbr.IsEqual<@P.AsString>>.View.Select(paymentEntry, arAdjust.AdjgRefNbr)?.TopFirst;
    }
 
    ARPayment arPayment = paymentEntry.Document.Current;
 
    paymentEntry.Clear();
 
    ARRegister doc = arPayment;
    List<ARRegister> list = new List<ARRegister>() { doc };
 
    using (new PXTimeStampScope(null))
    {
        PXTimeStampScope.SetRecordComesFirst(typeof(ARRegister), true);
 
        try
        {
            ARDocumentRelease.ReleaseDoc(list, false);
        }
        catch (PXException e)
        {
            PXTrace.WriteError(e.Message);
        }
    }
 
    return arPayment;
}

Usage by Acumatica team of this method in ARInvoiceEntry:

public void ReleaseProcess(List<ARRegister> list)
{
    PXTimeStampScope.SetRecordComesFirst(typeof(ARInvoice), true);
 
    ARDocumentRelease.ReleaseDoc(list, falsenull, (ab) => { });
}

 3. To use PXTimeStampScope.DuplicatePersisted() method

This approach is the most difficult, but fixes the error when ordinary approaches don’t work.

There are no examples in Acumatica Framework source code. The next examples are my samples to override persist methods.

public class ARDocumentReleaseExt : PXGraphExtension<ARReleaseProcess>
{
    public static bool IsActive() => true;
 
    [PXOverride]
    public virtual void Persist(Action baseMethod)
    {
        var updRegisters = Base.ARDocument.Cache.Updated.Cast<ARRegister>().ToList();
        baseMethod?.Invoke();
 
        var lazyTempGraph = new Lazy<PXGraph>(() => PXGraph.CreateInstance<PXGraph>());
 
        foreach (ARRegister updRegister in updRegisters)
        {
            PXTimeStampScope.DuplicatePersisted(lazyTempGraph.Value.Caches[typeof(ARPayment)], updRegister, typeof(ARRegister));
            PXTimeStampScope.DuplicatePersisted(lazyTempGraph.Value.Caches[typeof(ARInvoice)], updRegister, typeof(ARRegister));
        }
    }
}
And second example:
public class ARPaymentEntryExt : PXGraphExtension<ARPaymentEntry>
{
    public static bool IsActive() => true;
 
    [PXOverride]
    public virtual void Persist(Action baseMethod)
    {
        var updInvoices = Base.ARInvoice_DocType_RefNbr.Cache.Updated.Cast<ARInvoice>().ToList();
        var updPayments = Base.Document.Cache.Updated.Cast<ARPayment>().ToList();
 
        baseMethod?.Invoke();
 
        var lazyTempGraph = new Lazy<PXGraph>(() => PXGraph.CreateInstance<PXGraph>());
        foreach (ARInvoice updInvoice in updInvoices)
        {
            PXTimeStampScope.DuplicatePersisted(lazyTempGraph.Value.Caches[typeof(ARRegister)], updInvoice, typeof(ARInvoice));
            PXTimeStampScope.DuplicatePersisted(lazyTempGraph.Value.Caches[typeof(ARPayment)], updInvoice, typeof(ARInvoice));
        }
        foreach (ARPayment updPayment in updPayments)
        {
            PXTimeStampScope.DuplicatePersisted(lazyTempGraph.Value.Caches[typeof(ARRegister)], updPayment, typeof(ARPayment));
        }
    }
}

 

In the second example described the approach usage in case when you need to persists dependent documents.

In both examples is also shown lazy graph initialization. This is not necessary to use it.
I use it for performance need, when I had long and heavy processing logic on my custom processing screen.

 Summary

Now you have needed knowledge and strategies on how to fix the "Another process has updated the {Table} record. Your changes will be lost" error in Acumatica, which occurs when multiple users try to modify the same record in the database. The error can be resolved by using the Timestamp field, which Acumatica uses to handle concurrent updates. The article proposes three approaches to fixing the error, including using the SelectTimeStamp() method, calling the SetRecordComesFirst() method, and using the DuplicatePersisted() method. The article provides code examples for each approach and explains how they can be used to fix the error.


 

Acumatica: SMS Provider, Twilio SMS provider, send SMS in action

Hello everybody,

Today I want to share one approach, how to send SMS message from custom action in Acumatica.

Acumatica has several sms providers in SalesDemo data base, it depends from Acumatica’s version, so, we will use Twilio provider. On SMS Provider screen you can find authorization parameters from each provider, as on screen shot from (22r1 build):

First you need to add two references from Acumatica’s Bin folder to your project in VS:

PX.SmsProvider.Core.dll

PX.SmsProvider.UI.dll

 

Then create graph extension for any screen that you need and develop next logic in custom button “Send SMS”.

Also, you need to use Dependency Injection (ASP.net) and IReadOnlyDictionary interface and define field in graph extension with ISmsProvider type.

The main logic consists from next steps: prepare list of settings (List<ISmsProviderSetting>), create SmsProvider fabric (using dependency injection), load setting to fabric, prepare SMS message and send it in async mode. Also, you can add PXLongOperation feature in action, it doesn’t have conflicts with async method.

Source code example here:

public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
    public static bool IsActive() => true;
 
 
    [InjectDependency]
    internal IReadOnlyDictionary<string, ISmsProviderFactory> ProviderFactories { get; set; }
    private ISmsProvider _currentProvider;
 
 
    public PXAction<SOOrder> SendSMS;
    [PXButton()]
    [PXUIField(DisplayName = "Send SMS", MapEnableRights = PXCacheRights.Select, MapViewRights = PXCacheRights.Select)]
    protected virtual void sendSMS()
    {
        SendSMSNotification(this.Base, "+380990123456", "Test SMS message from Acumatica server");
    }
 
    public virtual void SendSMSNotification(PXGraph graph, string phone, string smsMessage)
    {
        string parsePhoneTo = phone.Trim(' ', '(', ')', '-');
 
        var selectProviderAuth = SelectFrom<SmsPluginParameter>.InnerJoin<SmsPlugin>.On<SmsPluginParameter.pluginName.IsEqual<SmsPlugin.name>>.
            Where<SmsPlugin.isDefault.IsEqual<@P.AsBool>>.View.Select(graph, true);
 
        string ACCOUNT_SID = string.Empty;
        string SECRET = string.Empty;
        string FROM_PHONE_NBR = string.Empty;
 
        var setting = new List<TGSmsProviderSettings>();
 
        foreach (SmsPluginParameter item in selectProviderAuth)
        {
            if (item.PluginTypeName != "PX.SmsProvider.Twilio.TwilioVoiceProvider")
                throw new PXException("No preferences for Twilio provider!");
 
            switch (item.Name)
            {
                case nameof(ACCOUNT_SID):
                    {
                        ACCOUNT_SID = item.Value;
                        setting.Add(new TGSmsProviderSettings()
                        {
                            Name = nameof(ACCOUNT_SID),
                            Value = ACCOUNT_SID,
                        });
                        break;
                    }
                case nameof(SECRET):
                    {
                        SECRET = item.Value;
                        setting.Add(new TGSmsProviderSettings()
                        {
                            Name = nameof(SECRET),
                            Value = SECRET,
                        });
                        break;
                    }
                case nameof(FROM_PHONE_NBR):
                    {
                        FROM_PHONE_NBR = item.Value;
                        setting.Add(new TGSmsProviderSettings()
                        {
                            Name = nameof(FROM_PHONE_NBR),
                            Value = FROM_PHONE_NBR,
                        });
                        break;
                    }
            }
        }
 
        if (string.IsNullOrEmpty(ACCOUNT_SID) || string.IsNullOrEmpty(FROM_PHONE_NBR) || string.IsNullOrEmpty(SECRET))
            throw new PXException("No preferences for Twilio provider!");
 
        SendSmSMessage(setting, parsePhoneTo, smsMessage);
    }
 
    private void SendSmSMessage(List<TGSmsProviderSettings> settings, string phone, string body)
    {
        if (this._currentProvider != null) return;
 
        this._currentProvider = this.ProviderFactories["PX.SmsProvider.Twilio.TwilioVoiceProvider"].Create();
        this._currentProvider.LoadSettings(settings);
 
        var messageRequest = new SendMessageRequest()
        {
            RecepientPhoneNbr = phone,
            RecepientSMSMessage = body
        };
 
        try
        {
            _currentProvider.SendMessageAsync(messageRequest, CancellationToken.None).Wait();
        }
        catch (AggregateException ex)
        {
            string str = string.Join(";", ex.InnerExceptions.Select(x => x.Message));
            throw new PXException(str);
        }
    }
}
 
 
public class TGSmsProviderSettings : ISmsProviderSetting
{
    public string Name { get; set; }
    public string Description { get; set; }
    public string Value { get; set; }
}

Summary

With provided code and Twilio you can send sms messages to your USA based customers. Similar activites may be done for other customers, but out of the box Acumatica allows to use Twilio.

How to open windows desktop applications from Acumatica

Imagine you are working on Acumatica customization that needs to integrate with an existing desktop application. How can you launch the desktop application from the web-based app? It might seem impossible at first, but on Windows, it's actually quite simple. The key is to use Custom Protocol Handlers. All you need to do is install a new custom protocol and tell Windows which application should handle it. For example, let's say you have a desktop application that performs sales analysis based on the stock item when it is launched. You can create a new custom protocol called " ItemAnalyzer://" and whenever a URL with this protocol is entered into the browser, the desktop application will be launched and the text after the protocol will be treated as a parameter.

 

It's important to note that when using protocol handlers, the protocol name itself will be included as part of the argument passed to the desktop application. This may require some additional processing to remove the protocol name (such as the "GetStringBetweenDelimiters" function on line 17). For example, if you run the desktop application with an argument, you might get something like the following:

class Program
{
    static void Main(string[] args)
    {
        string inventoryCD = GetStringBetweenDelimiters(args[0]);
 
        Console.WriteLine($"Processing...: {inventoryCD}");
        // ...
        // do something with inventoryCD
        // ...
 
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
 
    static string GetStringBetweenDelimiters(string input)
    {
        int firstIndex = input.IndexOf("://") + 3;
        int lastIndex = input.IndexOf('/', firstIndex);
        return input.Substring(firstIndex, lastIndex - firstIndex);
    }
 
    static void RegProtocol()
    {
        var key = Registry.ClassesRoot.CreateSubKey("ItemAnalyzer");
        
        key.SetValue("", "URL:ItemAnalyzer Protocol");
        key.SetValue("URL Protocol", "");
        
        var subKey = key.CreateSubKey(@"shell\open\command");
        var execPath = Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location),
                                    System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
 
        subKey.SetValue("", $"{execPath} %1");
        subKey.Close();
        key.Close();
    }
}

To make the magic happen, we need to register the custom protocol handler in the Windows registry. This can be done manually or automatically. First, let's do it manually. To do this, open the Windows registry as a system administrator (type "Regedit" in the start menu or run it as a command). Then, follow these steps:

 

  1. Under HKEY_CLASSES_ROOT, create a new key with the same name as the protocol (in this case, " ItemAnalyzer ").
  2. Inside the new key, add a default new string value with no name (just "Default") and set its content to "URL:protocol_name Protocol" (in this case, "URL: ItemAnalyzer Protocol").

3. Add a new string with the name "URL Protocol" and no content.

4. Under the " ItemAnalyzer" key, add the following keys hierarchically: shell\open\command

5. Inside the "command" key, add a new string with an empty name (just "Default") and set its value to the location of the executable followed by %1, which represents the argument to pass to the executable.

After completing these steps, if you open the run window and type "ItemAnalyzer:// " and press enter, the application will be launched. You can also do this from the browser, and the browser will prompt you for confirmation before launching the application.

Now, let's proceed to the implementation of an action within Acumatica that will initiate the opening of a desktop application when activated. Specifically, we want to create an action on the Sales Order screen that, will execute a specified program to run our desktop application and pass the InventoryCD as an argument.

namespace AcuStockItemAnalizer
{
    public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
    {
        public static bool IsActive() => true;
 
        #region Action
        public PXAction<SOOrder> RunStockItemAnalyzer;
        
        [PXUIField(DisplayName = "Run Item Analyzer")]
        [PXButton(CommitChanges = true)]
        protected virtual IEnumerable runStockItemAnalyzer(PXAdapter adapter)
        {
            var tranRow = Base.Transactions.Current;
            if (tranRow != null)
            {
                var inventoryItem = PXSelectorAttribute.Select<SOLine.inventoryID>(Base.Caches[typeof(SOLine)], tranRow, tranRow?.InventoryID) as InventoryItem;
                if (inventoryItem != null)
                {
                    string urlProtocol = string.Format($"ItemAnalyzer://{inventoryItem.InventoryCD}");
                    throw new PXRedirectToUrlException(urlProtocol, null);
                }
            }
            
            return adapter.Get();
        }
        #endregion
    }
}
As demonstrated in the example, activating the action initiates the opening of a desktop application, with the InventoryCD being passed as a parameter.


To automate the process of registering a custom protocol handler, you can change the registry during the application installation or have the application do it automatically. One way to do this in C# is to use the code provided.

Another option is to create a .REG file. This is a plain text file with a .REG extension that contains registry entries, and it can be used to add or modify registry entries automatically when opened. When you double-click a .REG file, it will be imported into the registry, and the registry entries it contains will be added or modified. This can be a convenient way to automatically register custom protocol handlers without manually editing the registry.

 

Windows Registry Editor Version 5.00

 

[HKEY_CLASSES_ROOT\ItemAnalyzer]

@="URL: ItemAnalyzer Protocol"

"URL Protocol"=""

 

[HKEY_CLASSES_ROOT\ItemAnalyzer\shell]

 

[HKEY_CLASSES_ROOT\ItemAnalyzer\shell\open]

 

[HKEY_CLASSES_ROOT\ItemAnalyzer\shell\open\command]

@="\"C:\\TestApplication\\StockItemAnalizer.exe\" \"%1\""

 

How to inject delegate for PXFilteredProcessingJoin and similar classes

Hi,

want to share code, which was written in the context of this question on https://community.acumatica.com. Question is how to modify request and use filtering with help of In or IsIn operator of Acumatica framework. 

After plenty of trial and error, here is the code, with which I've come:

public class INReplenishmentFilterExt : PXCacheExtension<PX.Objects.IN.INReplenishmentFilter>
{
    #region UsrWarehouse
    [PXSelector(typeof(INSite.siteCD), typeof(INSite.siteCD), typeof(INSite.descr), ValidateValue = false, DescriptionField = typeof(INSite.siteCD))]
    [PXUIField(DisplayName = "Warehouse")]
 
    public virtual string UsrWarehouse { get; set; }
    public abstract class usrWarehouse : PX.Data.BQL.BqlString.Field<usrWarehouse> { }
    #endregion
}
 
public class INReplenishmentCreate_Extension : PXGraphExtension<PX.Objects.IN.INReplenishmentCreate>
{
    public override void Initialize()
    {
        base.Initialize();
        BqlCommand cmd =
            new SelectFrom<INReplenishmentItem>();
        var f1 = new PXSelectDelegate(
            () =>
            {
                return records1(Base);
            });
        Base.Views["Records"] = new PXView(Base, false, cmd, f1);
    }
 
    public virtual IEnumerable records1(PXGraph graph)
    {
        var cr = Base.Filter.Current;
        if (cr != null)
        {
            var ext = cr.GetExtension<INReplenishmentFilterExt>();
 
            var objs = ext.UsrWarehouse.Split(';').ToList().Select(a => a.Trim()).ToArray<String>();
            var listResults = new List<INReplenishmentItem>();
 
            var warehouses = SelectFrom<INSite>.Where<INSite.siteCD.IsIn<@P.AsString>>.View.Select(graph, new[]{ objs}).ToList(100);
 
            var wsIds = warehouses.Select(a => a.GetItem<INSite>().SiteID).ToList();
 
            return SelectFrom<INReplenishmentItem>.Where<INReplenishmentItem.siteID.IsIn<@P.AsInt>>.View.Select(graph, wsIds.ToArray());
        }
        else
        {
            return Base.Records.Select(Base);
        }
    }
}

Want to highlight usage of PXSelectDelegate. With it's usage, you can inject any kind of business logic into your graph extension.

Summary

 

 As Gabriel Michaud once pointed, with great power comes great responsibility. Use this code with carefullness, as you may introduce bugs, especially if couple more packages are running along with yours.

PXAggregateAttribute usage for saving of your development time

Hi everybody,

today I want to share one of the insights from code and code, which was conducted by Stéphane Bélanger, and which seems useful, also may be controversial. But still, you may like it. So let me introduce or re-introduce PXAggregateAttribute .

 

If to sum up, purpose of PXAggregateAttribute, is merging of couple of attributes into single one. Consider following situation. You need to have selector Active customer over multiple places in Acumatica: at Purchase orders form, and Sales order form and at Shipment form. And difference between them will be zero, or close to that. Wouldn't that be nice, to declare this attribute in one place, and then to re-use it everywhere else? Of course yes. And for that purpose, Acumatica introduced attribute PXAggregateAttribute. 

Below goes code sample, of how that can be used, and re-used:

[PXInt()]
[PXUIField(DisplayName = "Active Customer")]
[PXDefault()]
[PXSelector(typeof(Search<BAccountR.bAccountID,
    Where<BAccountR.status, Equal<BAccount.status.active>>>))]
public class ActiveCustomer : PXAggregateAttribute {}
 
 
public class SOShipmentExt : PXCacheExtension<SOShipment>
{
    public abstract class activeCustomer : PX.Data.BQL.BqlInt.Field<activeCustomer> { }
 
    // Acuminator disable once PX1030 PXDefaultIncorrectUse [For demonstration purposes that will be sufficient]
    [ActiveCustomer]
    public Int32? ActiveCustomer { get; set; }
}
 
public class POOrderExt : PXCacheExtension<POOrder>
{
    public abstract class activeCustomer : PX.Data.BQL.BqlInt.Field<activeCustomer> { }
 
    // Acuminator disable once PX1030 PXDefaultIncorrectUse [For demonstration purposes that will be sufficient]
    [ActiveCustomer]
    public Int32? ActiveCustomer { get; set; }
}
 
public class SOOrderExt : PXCacheExtension<SOOrder>
{
    public abstract class activeCustomer : PX.Data.BQL.BqlInt.Field<activeCustomer> { }
 
    // Acuminator disable once PX1030 PXDefaultIncorrectUse [For demonstration purposes that will be sufficient]
    [ActiveCustomer]
    public Int32? ActiveCustomer { get; set; }
}
 As you can see, above our class ActiveCustomer, we've declared bundle of attributes, and then everywhere else, we've used them, but instead of duplication of code, we've re-used them as single line.

How To Avoid Copy Paste With Help Of Attributes In Acumatica

 

Hello everybody,

today I want to leave a short note on how to avoid Copy/paste with help of custom attributes. 

Imagine following scenario. You have some set of duplicated code, which you need to apply at FieldSelecting . One of the ways of achieving this can be creation of some class and method within this class, which will handle that functionality, and then just copy/paste creation of the instance of the class at any place, where you need to have that business logic applied.

But you can use another way. You can use custom attributes. You can create your attribute, and then use that attribute over all places, where you may have a need for calling your business logic. In order to work that properly you'll need to inherit your Attribute from PXEventSubscriberAttribute, IPXFieldSelectingSubscriber . For example you can accomplish it like this:

 

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Parameter | AttributeTargets.Method)]
public class SomeAttribute : PXEventSubscriberAttributeIPXFieldSelectingSubscriber
{
    protected Type _TargetField;
    protected Type _DacType;
    
 
    public SomeAttribute(Type dacType, Type targetField)
    {
        _DacType = dacType;
        _TargetField = targetField;
    }
 
    public virtual void FieldSelecting(PXCache sender, PXFieldSelectingEventArgs e)
    {
var graph = sender.Graph;
//and some other logic
}

Also this can be applied to the field like this:

 public class YourDacClass

{

    [Some(typeof(DacClass), typeof(DacClass.someField))]
    [PXUIField(DisplayName = "Value")]
    public virtual string SomeField { getset; }

}

 

and that's it. Now you can have move your duplication logic to attribute SomeAttribute, and enjoy smaller amount of code.

Why Code Changes From Dll Are Not Shown On The Form In Acumatica

 

today I want to share with you interesting use case which stolen one night of sleep from me, as well as from one of my collegues. 

We had very trivial case. Or at least we thought it is trivial. In Acumatica we had something like this:

#region UsrCardTheme
[PXDBString(256)]
[PXUIField(DisplayName="Card Theme")]
 
public virtual string UsrCardTheme { getset; }
public abstract class usrCardTheme : PX.Data.BQL.BqlString.Field<usrCardTheme> { }
#endregion

For some reason we weren't able to update it in database, so we've made changes to it like this in order to check if we will see it in UI:

#region UsrCardTheme
[PXDBString(256)]
[PXUIField(DisplayName="Card Theme2")]
 
public virtual string UsrCardTheme { getset; }
public abstract class usrCardTheme : PX.Data.BQL.BqlString.Field<usrCardTheme> { }
#endregion

went to the page, open it, and .... label of column remained the same. We've did our best in order to find what is the problem, what's wrong with the syntax, removed completely, restarted IIS, thrown away garbage, nothing worked. 

Until we've found stackoverflow advice to take a look at folder App_RuntimeCode:

and only after we've cleared up folder App_RuntimeCode we've noticed dream of all night, 2 in the end!

How To Get User Friendly Error Message Out Of Webexception

 

Hello everybody,

today I want to leave a short notice on how to get user friendly text of WebException. Imagine that you need to know which part of data in your json is missing. How can you quickly figure out which part? For one of my projects I wanted to get easy way of saying user which field is potentially missing. In order to achieve that I've used following fragment:

catch (WebException ex)
{
  string errorMessage = string.Empty;
  if (ex.Response != null)
  {
      using (var errorResponse = (HttpWebResponseex.Response)
      {
          using (var reader = new StreamReader(errorResponse.GetResponseStream()))
          {
              errorMessage = reader.ReadToEnd();
          }
      }
  }

After that I've just used to throw exception with adding additional details over Data property of Exception class.

 

How To Call Non Public Method Of Acumatica

 

Hello everybody,

today I want to share with you how it's possible to call some methods of Acumatica, which are not public, and which you don't want to copy/paste completely into your source code. In that case reflection will save you. Consider calling of InsertSOAdjustments method of graph SOOrderEntry below.

MethodInfo invokeSOAdjustment = typeof(SOOrderEntry).GetMethod(
    "InsertSOAdjustments"BindingFlags.Instance | BindingFlags.NonPublic, Type.DefaultBinder,
    new[] { typeof(SOOrder), typeof(ARPaymentEntry), typeof(ARPayment) }, null);
 
invokeSOAdjustment.Invoke(Base, new object[] { orderdocgraphpayment });

Also I want to give you a word of warning, that such approach potentially will not be certified, and another way of usage will be the one below:

In extension of SOOrderEntry create lines like those:

[PXOverride]
public void InsertSOAdjustments(SOOrder orderARPaymentEntry docgraphARPayment payment,
    Action<SOOrderARPaymentEntryARPaymentbaseAction)
{
    baseAction(orderdocgraphpayment);
}

and then just call InsertSOAdjustments method whenever you'll have a need for this.

Summary

Because Acumatica is written with C# which is very powerful language which gives you a lot of features you can easily achieve a lot of thigs, also be careful with usage of reflection. Somtime even more then Acumatica team anticipated themselves. 

Template For Usage Of PXLineNbr Attribute In Acumatica

 

Hello everybody,

today I want to leave a note on how to use PXLineNbr attribute in Acumatica pages. Recently myself and one of my team members struggled a bit with a question on how to add it properly. Below goes a bit changed worked tempalte:

public class PrimaryTable : IBqlTable
{
    #region LineId
    public abstract class lineId : PX.Data.BQL.BqlInt.Field<lineId> { }
 
    [PXDBInt]
    [PXDefault(0)]
    public virtual int? LineId { getset; }
    #endregion
 
    #region LineCntr
    public abstract class lineCntr : PX.Data.BQL.BqlInt.Field<lineCntr> { }
 
    [PXDBInt]
    [PXDefault(0)]
    public virtual int? LineCntr { getset; }
    #endregion
}
 
public class DetailsTable : IBqlTable
{
    #region LineNbr
    public abstract class lineNbr : IBqlField { }
 
    [PXDBInt(IsKey = true)]
    [PXDefault]
    [PXLineNbr(typeof(PrimaryTable.lineCntr))]
    [PXParent(typeof(SelectFrom<PrimaryTable>.Where<PrimaryTable.lineId.IsEqual<PrimaryTable.lineId
        .FromCurrent>>))]
    public virtual int? LineNbr { getset; }
    #endregion
}

Summary

As you see, nothing special in this code. Add PXParent, PXLineNbr and enjoy with line counter feature.