Parse SubAccount to get individual segments - acumatica

I would like to access the individual segments of a subaccount programmatically. Assuming I have a particular sub account set as ABC-123, I would like to be able to access ABC and 123 separately in code so that I can implement a particular business requirement.
I know that SubAccounts are saved in the Sub table as one string example ABC123. The sub account fields which link to this table would then link based on the ID (integer - PK of the Sub table). I can of course read from this table and then split accordingly (by taking the first 3 characters and the second 3 characters). However, I would like this to be dynamic so that the customization will work for different clients and clients may have different lengths for the segment. Therefore I cannot hard-code the value 3. I can make use of the SegmentValues table to retrieve the lengths of each Segment accordingly.
However, since Acumatica is already somehow carrying out this parsing (example in the UI), is there an API where Acumatica handles this logic and can provide the Sub-account as an array of strings. I tried to look into the SubAccountAttribute, PXDimensionSelectorAttribute, and SubAccountProvider but could not find anything which delivers this functionality.
Does Acumatica provide a way to split the sub-account into an array of strings or should I do this manually by identifying lengths from the Segment Values?

I believe some of the logic used to separate the segment are in the protected Definition class. The separated segments are in the Dimensions collections of the Definition class. You can access it in attributes that derive from PXDimensionAttribute class but since Definition class is protected you can't access it in a graph because PXGraph/PXGraphExtension don't derive from it.
Not much can be extracted from Dimension because most properties are protected:
You can roll your own by reading the segments of the segmented key:
Here is an example that write the segment values of the transactions subaccount in the trace for the Invoices and Memos screen:
using PX.Data;
using PX.Objects.AR;
using PX.Objects.CS;
using PX.Objects.GL;
namespace PX.Objects.SO
{
public class ARInvoiceEntry_Extension : PXGraphExtension<ARInvoiceEntry>
{
public void ARTran_RowSelected(PXCache cache, PXRowSelectedEventArgs e)
{
ARTran tran = e.Row as ARTran;
if (tran != null && tran.SubID.HasValue)
{
Sub sub = SubAccountAttribute.GetSubaccount(Base, tran.SubID.Value);
if (sub != null && sub.SubCD != null)
{
short segmentStartIndex = 0;
foreach (Segment segment in PXSelect<Segment,
Where<Segment.dimensionID, Equal<Required<Segment.dimensionID>>>,
OrderBy<Asc<Segment.segmentID>>>.Select(Base, "SUBACCOUNT"))
{
if (segment.SegmentID.HasValue && segment.Length.HasValue)
{
PXTrace.WriteInformation(string.Format("Segment {0}: {1}",
segment.SegmentID,
sub.SubCD.Substring(segmentStartIndex, segment.Length.Value)));
segmentStartIndex += segment.Length.Value;
}
}
}
}
}
}
}
Trace results:

Related

JOOQ: fetchGroups() always returns list with only one element

I'm new to JOOQ and currently fail to map a joined query to Map<K, List<V>>: the list always only contains one element.
Here's my code:
DSL.using(...)
.select(ORDER.fields())
.select(ORDER_ITEM_ARTICLE.fields())
.from(ORDER)
.leftOuterJoin(ORDER_ITEM_ARTICLE).on(ORDER.ID.eq(ORDER_ITEM_ARTICLE.ORDER_ID))
// to Map<InOutOrder, List<OrderItemArticle>>
.fetchGroups(
r -> r.into(ORDER).into(InOutOrder.class),
r -> r.into(ORDER_ITEM_ARTICLE).into(OrderItemArticle.class)
)
// map to InOutOrder
.entrySet().stream().map( e -> {
// e.getValue() always returns list with only 1 element?!
e.getKey().articles = e.getValue();
return e.getKey();
})
.collect(Collectors.toList())
;
Say I have 1 row in ORDER and 2 corresponding rows in ORDER_ITEM_ARTICLE. Running the SQL returned by .getSQL() (after .fetchGroups()), returns me 2 rows as expected, so I assumed the fetchGroups() call will populate my list with two entries as well?!
What am I missing?
Thanks!
Update:
As requested, the InOutOrder class:
public class InOutOrder extends Order {
public List<OrderItemArticle> articles;
public List<OrderItemOther> others;
public List<OrderItemCost> costs;
public List<OrderContact> contacts;
public List<EmailJob> emailJobs;
}
So this is just an extension of the JOOQ POJO class and is used for JSON communication with the API clients...
fetchGroups() simply puts objects in a LinkedHashMap. You have to adhere to the usual Map contract, which means implementing equals() and hashCode(). Without it, each object you're creating (or which jOOQ is creating for you) will use identity comparison, so you get every "value" only once in the result.

Hyperlink to page with smart search filter prepopulated

I have a smart search page that shows all the products on the page with some smart search filters that narrows down the products on some criterias (Let's say for example Filter1 has Option1, Option2 and Option3).
What I am trying to accomplish is to have a link on a seperate page that links to the product page, but when the user clicks on that link some of the search filters gets set (For example Filter1 would have Option2 selected).
I'm not sure if that is possible with out of the box solution, but with simple tweaks inside SearchFilter.ascx.cs, you can make a workaround. File is placed under CMSWebParts/SmartSearch/SearchFilter.ascx.cs. You should change method 'GetSelectedItems' to take a look into query string for filter value (see snippet bellow):
/// <summary>
/// Gets selected items.
/// </summary>
/// <param name="control">Control</param>
/// <param name="ids">Id's of selected values separated by semicolon</param>
private string GetSelectedItems(ListControl control, out string ids)
{
ids = "";
string selected = "";
//CUSTOM: retrive value for query string
var customFilter = QueryHelper.GetString("customFilter", "");
// loop through all items
for (int i = 0; i != control.Items.Count; i++)
{
//CUSTOM: ----START-----
if (!RequestHelper.IsPostBack())
{
if (!string.IsNullOrEmpty(customFilter))
{
if (control.Items[i].Text.Equals(customFilter, StringComparison.InvariantCultureIgnoreCase))
{
control.Items[i].Selected = true;
}
}
}
//CUSTOM: ----END-----
if (control.Items[i].Selected)
{
selected = SearchSyntaxHelper.AddSearchCondition(selected, control.Items[i].Value);
ids += ValidationHelper.GetString(i, "") + ";";
}
}
if (String.IsNullOrEmpty(selected) && (control.SelectedItem != null))
{
selected = control.SelectedItem.Value;
ids = control.SelectedIndex.ToString();
}
return selected;
}
And your hyperlink will look like this: /Search-result?searchtext=test&searchmode=anyword&customfilter=coffee
With this modifications, you can send only one value in filter, but if you need more then one value, you can send them and customize it however suits you best. Also, you can send filter name (in case that you have multiple filters) and then add check in method above.
I will recommend you not to modify kentico files. Instead of that, clone default filter web part and make modifications there, because withing next upgrade of project, you will lose your changes. I checked this in Kentico 11.
For Smart Search Filters:
if turn off auto-post back option -then web part control ID should become a query string parameter that you can use.
This above will form something like:
/Smart-search-filter.aspx?searchtext=abc&searchmode=anyword&wf=2;&ws=0;&wa=0
P.S. I suggest you to take a look at the corporate site example: look the smart search filter web part: /Examples/Web-parts/Full-text-search/Smart-search/Smart-search-filter. It is working example you can use it as starting point.

SSIS Script Component Source - Read from SharePoint 365 List via Odata

I apologize if my question is not adequately described. I am a .Net / C# / SSIS newbie. Also apologize if already answered, I've tried searching here and Google for a few hours without luck.
Background: I need to pull data rows from a SharePoint 365 list and unpivot certain columns into a format ready for import to a SQL Server table. I realize that SSIS has an Odata Source and built-in Unpivot component, and I've successfully used those for proof of concept.
However, I believe I need a custom script component because the nummber of columns to unpivot from the source SharePoint list is variable. Each month or so, a new column will be added (it relates to a financial forecasting "tool" in SharePoint, and the latest forecasted month changes). My understanding is that source columns must be defined in SSIS at design time, so if my source columns are changing, the only way I can think to address this without manually changing the SSIS data flow each month is to programatically combine the Odata source and unpivot functions into a custom script component.
I understand, or can figure out, the unpivot logic. The part I am struggling with is how to actually connect to and expose a given list and it's data rows / columns as lists that I can loop through and perform my mapping to the output columns.
My "starting point" for requested guidance is this:
1) Created and successfully connected to the SharePoint site in question using standard SSIS Odata Connection Manager.
2) Created standard "Script Component", type = source, on the visual designer.
3) From script component properties, associated the Odata connection manager with the name of "myConnection".
4) Need help -> within the script component, open a connection to a specific list, read it's contents, and perform unpivot logic.
For illustration purposes, assume the source is a SharePoint list with two "fixed" string columns titled Study and Site, and a variable number of columns with names matching month-end dates (e.g. 9/30/2016, 10/31/2016, etc.) that contain integer values. I want to map the study and site source columns to destination columns of the same name and unpivot the month columns where column name is mapped to ProjectionMonth and the integer value is mapped to ProjectionValue.
Here's the basic algorithm I have in mind (I realize this isn't compilable - that's where I need your help!):
using System;
using System.Data;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Runtime.Wrapper;
using System.Data.SqlClient;
[Microsoft.SqlServer.Dts.Pipeline.SSISScriptComponentEntryPointAttribute]
public class ScriptMain : UserComponent
{
IDTSConnectionManager100 connMgr;
SqlConnection sqlConn; // from MSDN tutorial, but I don't know how to adapt to Odata/SharePoint 365 list
SqlDataReader sqlReader; // not sure how to adapt
public override void AcquireConnections(object Transaction)
{
connMgr = this.Connections.myConnection;
sqlConn = (SqlConnection)connMgr.AcquireConnection(null); // This is from MSDN tutorial, but I don't know how to adapt to Odata
}
public override void PreExecute()
{
//Not sure how to adapt to odata to read specific SharePoint list
SqlCommand cmd = new SqlCommand("SELECT * FROM <some sharepoint list>", sqlConn);
sqlReader = cmd.ExecuteReader();
}
public override void PostExecute()
{
sqlReader.Close(); // Not sure how to adapt.
}
public override void CreateNewOutputRows()
{
string myStudy;
string mySite;
string myProjectionMonth;
string myProjectionValue;
// This is a rough representation of the logic needed.
// I realize that the actual code to access column values / names depends on the class(es) I need to use, but not sure what those classes are / how to access
foreach (myListRow in sqlConn.rows)
{
myStudy = myListRow.Columns["Study"].value;
mySite = myListRow.Columns["Site"].value;
foreach (myColumn in myListRow.Columns)
if (DateTime.TryParse(myColumn.Name, out dateValue))
{
myProjectionMonth = myColumn.Name;
myProjectionValue = myColumn.Value;
Output0Buffer.AddRow();
Output0Buffer.Study = myStudy;
Output0Buffer.Site = mySite;
Output0Buffer.ProjectionMonth = myProjectionMonth;
Output0Buffer.ProjectionValue = myProjectionValue;
}
}
}
}
Edit: as an example, assume the source SharePoint list has the following:
Study Site 9/30/2016 10/31/2016
123 ABC 5 10
I want to the script component to connect to the list, read it's contents, and return the following unpivoted data set for eventual load into SQL Server:
Study Site ProjectionMonth ProjectionValue
123 ABC 9/30/2016 5
123 ABC 10/31/2016 10
So this is probably not an ideal way of doing it, and it doesn't leverage the standard SSIS Odata Connection Manager that I wanted... but it does technically get the job done and it's good enough for me, for now.
Would be interested on any suggested feedback / improvements / etc., if you have any.
#region Namespaces
using System;
using Microsoft.SharePoint.Client;
using System.Security;
using System.Collections.Generic;
#endregion
[Microsoft.SqlServer.Dts.Pipeline.SSISScriptComponentEntryPointAttribute]
public class ScriptMain : UserComponent
{
public override void CreateNewOutputRows()
{
// Connect to SharePoint
ClientContext context = new ClientContext("https://<redacted>.sharepoint.com/Development");
SecureString passWord = new SecureString();
foreach (char c in Variables.sharepointPassword.ToCharArray()) passWord.AppendChar(c);
context.Credentials = new SharePointOnlineCredentials("<redacted>#<redacted>.onmicrosoft.com", passWord);
// Define the target list
List EnrollmentList = context.Web.Lists.GetByTitle("Enrollment Projections");
// Find all fields in the target list
FieldCollection myFields = EnrollmentList.Fields;
context.Load(myFields);
context.ExecuteQuery();
// Load all rows from the target list
CamlQuery query = CamlQuery.CreateAllItemsQuery(100);
ListItemCollection items = EnrollmentList.GetItems(query);
context.Load(items);
context.ExecuteQuery();
//Store valid date fields
List<Field> myDateFields = new List<Field>();
foreach (Field tempField in myFields)
{
DateTime tempDate;
if (DateTime.TryParse(tempField.Title, out tempDate))
{
myDateFields.Add(tempField);
}
}
string myStudy;
string mySite;
string myMonth;
string myValue;
foreach (ListItem listItem in items)
{
myStudy = listItem["Study"].ToString();
mySite = listItem["Site"].ToString();
foreach (Field tempField in myDateFields)
{
myMonth = tempField.Title;
myValue = listItem[tempField.InternalName.ToString()].ToString();
Output0Buffer.AddRow();
Output0Buffer.Study = myStudy;
Output0Buffer.Site = mySite;
Output0Buffer.ProjectedMonth = myMonth;
Output0Buffer.ProjectedValue = Convert.ToInt32(myValue);
}
}
}
}

Obtaining FluentValidation max string length rules and their max values

We want to implement a character counter in our Javascript data entry form, so the user gets immediate keystroke feedback as to how many characters he has typed and how many he has left (something like "25/100", indicating current string length is 25 and 100 is the max allowed).
To do this, I would like to write a service that returns a list of dto property names and their max allowed lengths.
{Name='SmallComment', MaxLength=128}
{Name='BigComment', MaxLength=512}
The best way I can think of to do this would be to create an instance of the validator for that dto and iterate through it to pull out the .Length(min,max) rules. I had other ideas as well, like storing the max lengths in an attribute, but this would require rewriting all the validators to set up the rules based on the attributes.
Whatever solution is best, the goal is to store the max length for each property in a single place, so that changing that length affects the validation rule and the service data passed down to the javascript client.
If you want to maintain a single source of reference for both client/server I would take a metadata approach and provide a Service that returns the max lengths to the client for all types, something like:
public class ValidationMetadataServices : Service
{
public object Any(GetFieldMaxLengths request)
{
return new GetFieldMaxLengthsResponse {
Type1 = GetFieldMaxLengths<Type1>(),
Type2 = GetFieldMaxLengths<Type2>(),
Type3 = GetFieldMaxLengths<Type3>(),
};
}
static Dictionary<string,int> GetFieldMaxLengths<T>()
{
var to = new Dictionary<string,int>();
typeof(T).GetPublicProperties()
.Where(p => p.FirstAttribute<StringLengthAttribute>() != null)
.Each(p => to[p.PropertyName] =
p.FirstAttribute<StringLengthAttribute>().MaximumLength);
return to;
}
}
But FluentValidation uses Static properties so that would require manually specifying a rule for each property that validates against the length from the property metadata attribute.

ARM - non continuous subaccount

According to the list of new/updated features in acumatica it mentions the following: "Specify multiple, non-continuous ranges in the data source for accounts, subaccounts,and branches in the general ledger engine."
However from what I'm seeing, this doesn't seem to apply to subaccounts.
For example in my situation, the first segment of the subaccount defines the "region".
When defining my Unit sets what I need to do is the following:
Central Region - (subacct segment AB and DE)
- Sub1 (AB)
- Sub2 (DE)
North Region (subacct segment BC)
In the Data Source, I can only specify a single "Start" and "End" subaccount which uses all in between.
Is it possible to have the "Central Region" be a summary of all the child nodes OR can I specify i want ONLY subaccts AB, DE for "Central Region"
In FRx you can do this where the "Folder" or "Parent" is a summary of the children items underneath it.
It is indeed possible, but by default the user interface will not allow you to enter the string in such a way, due to the segmented nature of the subaccount field. You can, however, customize the field so that it no longer behaves as a segmented editor. To do that, you need to create a PXCacheExtension on RMDataSourceGL which will override the StartSub/EndSub attributes. The class looks like that:
using System;
using PX.Data;
using PX.Objects.GL;
using PX.CS;
namespace PX.Objects.CS
{
[Serializable]
public partial class RMDataSourceGL : PXCacheExtension<RMDataSource>
{
[PXDBString(255, IsUnicode = true)]
[PXUIField(DisplayName = "Start Sub.")]
public virtual string StartSub { get; set; }
[PXDBString(255, IsUnicode = true)]
[PXUIField(DisplayName = "End Sub.")]
public virtual string EndSub { get; set; }
}
}
Once the customization is in place, you can put non-contiguous ranges in the StartSub/EndSub fields. Please note that the segmented key separator (usually it's '-') should not be part of the ranges. For example, assuming you want to include all subaccounts between US-00-00-00-000 and US-99-99-99-999 AND all the CA- accounts, you would put the following in the StartSub field: US?????????,CA?????????. You could also express it differently: US000000000-US999999999,CA000000000-CA999999999. The syntax is documented in the 5.0 release notes, and is the same for accounts.
As a secondary Comment for those who might be reading this, I also found that it is possible to make a Parent folder summarize the children. For Example:
CE - Central Region
AB - Sub1
DE - Sub2
Instead of specifying a Data Source for "CE", specify a Value of
=#AB+#DE

Resources