Background
As a beginner with C# and Acumatica, I found myself limited by my ability to think beyond the basic concepts about the design of a Data Access Class (DAC). I knew that I had to live with a segment in an inventory location established by our sysadmin to support an important business case. While this resolved our business concern, it made it problematic for me to extract the nature of each location with a simple DAC as you might create through some automation within the Customization Project screen. Ok, by “as a beginner”, I mean… that was yesterday.
While I have seen a lot of great examples in Acumatica source code to accomplish things I have not yet been able to understand, one thing stood out in a lot of source code that kept picking at my thoughts. I love the simplicity of the {get; set;} syntax on a field definition, but Acumatica often explodes that out into seemingly useless “long hand” syntax. For example, from PX.Objects.IN.INLocation, notice the definition of LocationID:
#region LocationID
public abstract class locationID : PX.Data.BQL.BqlInt.Field<locationID> { }
protected Int32? _LocationID;
[PXDBForeignIdentity(typeof(INCostSite))]
[PXReferentialIntegrityCheck]
public virtual Int32? LocationID
{
get
{
return this._LocationID;
}
set
{
this._LocationID = value;
}
}
#endregion
My preference is to make it much shorter with the modern shorthand of:
#region LocationID
public abstract class locationID : PX.Data.BQL.BqlInt.Field<locationID> { }
[PXDBForeignIdentity(typeof(INCostSite))]
[PXReferentialIntegrityCheck]
public virtual Int32? LocationID { get; set; }
#endregion
So much shorter and easier to read, in my opinion. However, while I have stared at that long format for months, I never really thought about how I can leverage its power. You see, my shorthand version, more formally known as “Auto-Implemented properties” was implemented in C# version 3.0. Before that, the properties for get (read property) and set (write property) were explicitly defined. The practice inherently reminds us that the read and write properties can be customized to control the data going into and coming out of those fields, and even if you can set or retrieve values through that field. Reflecting on that simple idea brings me to an almost obvious (now) solution to an annoying problem.
A Problem to Solve
My problem, from the business case, is that we stored an element in a defined segment of the INLocation.LocationCD. For reporting purposes, it would be nice to extract that segment and provide a field that presents the meaning of that segment in a more report-friendly format without a report writer having to translate the value in every report. After all, what if you add another valid code for the segment to use? Are you going to simply update every report that was written? I assure you that I’m not that eager to do so.
For our example, let’s assume the first segment of the LocationCD field is a single character with an important meaning. Is this location in the storeroom (Warehouse) designed for a Palletized, Boxed, Bagged, Loose, or Mixed-Use storage of materials? For this example, our segment values will be:
- P -> Palletized
- B -> Boxed
- G -> Bagged
- L -> Loose in a Bin
- M -> Mixed Use
For good measure, we should create a catch-all value in case someone uses a value that we don’t know yet. We will call it Undefined and arbitrarily use the code X, but you can use anything that is not considered a valid segment value for your use case.
Creating the Solution
To simplify maintenance of our list for usability and to define the related description for reporting, let’s make a class for the ListAttribute first. Notice the use of “Messages.Storage…” for the descriptive values. Be sure to define those variables wherever you maintain your language files. Also, I don’t really define a list directly in my base namespace, so define this list as your normally would.
namespace BLOG
{
public static class StorageTypes
{
public class ListAttribute : PXStringListAttribute
{
public ListAttribute() : base(
new[]
{
Pair(Palletized, Messages.StoragePalletized),
Pair(Boxed, Messages.StorageBoxed),
Pair(Bagged, Messages.StorageBagged),
Pair(Loose, Messages.StorageLoose),
Pair(Mixed, Messages.StorageMixed),
Pair(Undefined, Messages.StorageUndefined),
})
{ }
}
public const string Palletized = "P";
public const string Boxed = "B";
public const string Bagged = "G";
public const string Loose = "L";
public const string Mixed = "M";
public const string Undefined = "X";
}
}
The next step is to add a field to the DAC Extension for INLocation. If you are not using INLocation, you may want to establish access to the LocationCD value so that you can extract the segment from the field. In that case, you might want to add a field using PXDBScalar to bring the LocationCD value to your virtual fingertips.
#region LocationCD
[PXString]
[PXDBScalar(typeof(Search<INLocation.locationCD, Where<INLocation.locationID, Equal<MyCurrentDACName.locationID>>>))]
public String LocationCD { get; set; }
public abstract class locationCD : PX.Data.BQL.BqlString.Field<locationCD> { }
#endregion
WARNING: If you are extending INLocation, do not add LocationCD to the DAC Extension. You will want to access the field from the BASE of the DAC Extension. (see below the StorageType field below)
Once you have access to the descriptive LocationCD value, it’s time to work some magic using what we learned above by adding this to our custom DAC or to our DAC extention. Here we will put some logic into the get (read) parameter and make the field read-only by excluding the set (write) parameter.
#region StorageType
[PXString]
[PXUIField(DisplayName = "Storage Type")]
[StorageTypes.List]
public String StorageType
{
get
{
if (LocationCD == null || LocationCD.Length == 0) return StorageTypes.Undefined;
switch (LocationCD.Substring(0, 1))
{
case "P":
return StorageTypes.Palletized;
case "B":
return StorageTypes.Boxed;
case "G":
return StorageTypes.Bagged;
case "L":
return StorageTypes.Loose;
case "M":
return StorageTypes.Mixed;
default:
return StorageTypes.Undefined;
}
}
}
public abstract class storageType : PX.Data.BQL.BqlString.Field<storageType> { }
#endregion
Note: If this is added to a DAC Extention on INLocation, you already have LocationCD in the Base DAC. Simply change references above from LocationCD to Base.LocationCD for this code to work.
That’s it! As data is retreived into your DAC, the StorageType field will evaluate the LocationCD value. If it is null or empty, the value will return as Undefined (as per whatever you put in your StorageUndefined variable in the language file. Otherwise, the first character will be evaluated to return the related value. In case the 1st character does not match any known Storage Type, the value will return as Undefined.
Now when you compile your code, you should have a new field called Storage Type that will display the meaningful description of the 1 character 1st segment of your LocationCD value. With this element isolated, pivot tables can be built on the storage type.