Skip to main content

Breaking Acumatica best practices on graph extensions - do we all do it?

  • December 16, 2024
  • 5 replies
  • 171 views

Joe Schmucker
Captain II
Forum|alt.badge.img+3

Hi fellow developers.  I’ve been doing Acumatica dev since 2019.  I am a “lone ranger”, which means I don’t have fellow associates to run things by when I don’t know the best way (or any way) to do a particular “thing”.  Thank God for this forum.  I’d have quit a long time ago without it.  

However, I just finished a project that involved working with Attributes on the Projects screen.  I have custom fields on the screen that are Key Performance Indicators and they are calculations based on 3 sources: one of my own custom fields on the PMProject DAC, a field in the Summary tab (PMProjectRevenueTotal DAC), and Attributes in the Attributes tab.

For this one, I have to wait until ALL data has been loaded into the Views.  Due to the really unusual way that Attributes are loaded, I cannot use the “appropriate” events to trigger my calculations.  Based on my skill set, I can find only one way to do it and it is primarily done in the RowSelected handler, not for the primary DAC on the extension, but the PMProjectRevenueTotal DAC in the summary tab because the RowSelected in the PMProject DAC is loaded before the PMProjectRevenueTotal DAC and it seems to know nothing about that DAC.  The PMProjectRevenueTotal DAC is loaded AFTER the PMProject DAC, so I am doing the work there.

My solution feels extremely clunky and I am violating a couple of the Acumatica best practices in order to get it to work.  

I host a web site on behalf of a fortune five company that I wrote.  It is an ASP.Net site in VB.Net (don’t ask me how I started out on VB instead of C#...it wasn’t my fault).  On each page load, there is a “Pre-Prender” event where I can make any final changes to the page.  I would give up a kidney to have such an event in Acumatica.  In my opinion, there are things you need to do on a graph extension that you simply cannot do the proper way.  When I create my own custom graphs, I can follow the best practices because it is MY code.

I am curious and I want to know if any of you are ever in positions where you simply do what you have to do on graph extensions, even if you are breaking rules.  I am hoping other have break the rules.  It would make me feel less like a bad programmer in Acumatica.

5 replies

Dmitrii Naumov
Acumatica Moderator
Forum|alt.badge.img+7
  • Acumatica Moderator
  • December 16, 2024

Hi ​@Joe Schmucker,

I am not sure I understood why your requirements dictate you to wait for the data to load in all other views.

The way it works in Acumatica is that the data loads in a view when requested (either from UI or from another process in the code). If you need to get some data from a view you use View.Select() to retrieve the data. Could you please clarify how you access the data? 


Joe Schmucker
Captain II
Forum|alt.badge.img+3
  • Author
  • Captain II
  • December 16, 2024

Hi ​@Dmitrii Naumov 

I am performing calculations using Attributes, custom fields in the header and a field from the Summary tab.  I need access to ALL of those fields at the same time.  They are not all available at the same time depending on which event handler/DAC you are using.  As such, I had to use a rowselected handler to do them.

I didn’t really want to get into the gory details in this post, but rather, just wanted to see if other developers face issues where sometimes you have to break the rules.

Most of my trouble in this one project is because it pulls information from the Attributes which are not handled like “everything else”.

I was hoping other developers faced similar issues and I wouldn’t feel like an incompetent programmer.

 


Dmitrii Naumov
Acumatica Moderator
Forum|alt.badge.img+7
  • Acumatica Moderator
  • December 16, 2024

They are not all available at the same time depending on which event handler/DAC you are using.

@Joe Schmucker I still don’t see why that is the case to be honest. If you need something from some view, you can just select from it, can’t you?

 

if other developers face issues where sometimes you have to break the rules.

Sure, that happens from time to time.


Joe Schmucker
Captain II
Forum|alt.badge.img+3
  • Author
  • Captain II
  • December 16, 2024

@Dmitrii Naumov It is complicated to try to explain. I’m in the process of re-working the project.  I delivered a working project to the customer and I did it the way I had to do it to get them a project as it was time sensitive.

Since you are prying… 😉 Here is the method I am using to do the calculations, just so you can see the complexities.

I originally was doing these calcs in the FieldUpdated handler of the CSAnswers DAC, but it got HORRIBLY complicated.  The inter dependencies are crazy complicated and it required a LOT of duplication of code within the handler, so I put them in one method that I can call. 

This is the majority of the code I ended up with.

public bool kpiHandledPMProjectRevenueTotal = false;

protected void PMProjectRevenueTotal_RowSelected(PXCache cache, PXRowSelectedEventArgs e, PXRowSelected InvokeBaseHandler)
        {
            if (InvokeBaseHandler != null)
                InvokeBaseHandler(cache, e);
            var row = (PMProjectRevenueTotal)e.Row;
            if (row == null) return;

            if (Base.ProjectRevenueTotals.Cache.GetStatus(e.Row) == PXEntryStatus.Inserted) return;

            if (kpiHandledPMProjectRevenueTotal == true) return;

            DoKPI();
            kpiHandledPMProjectRevenueTotal = true;
}
protected void DoKPI()
{
//EXTENSIONS
//PRJCNI0033 Install: Days to Install
//PRJUNI0004 Closeout: Hrs/kW
//PRJUNI0005 Closeout: Hrs/Module
//PRJUNI0006 Sales: Price/W
//PRJUNI0007 Sales: Price/Module
//PRJUNI0019 Project: System Size(DC)

//ATTRIBUTES
//PRJUNI0095 Install: Construction Start Date DATETIME
//PRJUNI0096 Install: Construction Complete Date DATETIME
//PRJUNI0100 Install: Actual Install Hours -- INT
//PRJUNI0082 Procurement: Modue Wattage -- int
//PRJUNI0083 Procurement: Modue Count -- int

//FORMULAS
//PRJCNI0033 PRJUNI0096 - PRJUNI0095
//PRJUNI0004 PRJUNI0100 / PRJUNI0019
//PRJUNI0019 PRJUNI0082 * PRJUNI0083
//PRJUNI0006 Total value / (PRJUNI0019* 1000)
//PRJUNI0005 PRJUNI0100 / PRJUNI0083
//PRJUNI0007 Total value / PRJUNI0083

var pmProject = Base.Project.Current;
if (pmProject == null) return;

var pmProjectRevenueTotal = Base.ProjectRevenueTotals.Current;

GSContractExt ext = pmProject.GetExtension<GSContractExt>();
if (ext == null) return;

CSAnswers pr100 = SelectFrom<CSAnswers>.Where<CSAnswers.refNoteID.IsEqual<@P.AsGuid>
.And<CSAnswers.attributeID.IsEqual<@P.AsString>>>
.View.Select(Base, pmProject.NoteID, "PRJUNI0100");

CSAnswers pr82 = SelectFrom<CSAnswers>.Where<CSAnswers.refNoteID.IsEqual<@P.AsGuid>
.And<CSAnswers.attributeID.IsEqual<@P.AsString>>>
.View.Select(Base, pmProject.NoteID, "PRJUNI0082");

CSAnswers pr83 = SelectFrom<CSAnswers>.Where<CSAnswers.refNoteID.IsEqual<@P.AsGuid>
.And<CSAnswers.attributeID.IsEqual<@P.AsString>>>
.View.Select(Base, pmProject.NoteID, "PRJUNI0083");

CSAnswers pr95 = SelectFrom<CSAnswers>.Where<CSAnswers.refNoteID.IsEqual<@P.AsGuid>
.And<CSAnswers.attributeID.IsEqual<@P.AsString>>>
.View.Select(Base, pmProject.NoteID, "PRJUNI0095");

CSAnswers pr96 = SelectFrom<CSAnswers>.Where<CSAnswers.refNoteID.IsEqual<@P.AsGuid>
.And<CSAnswers.attributeID.IsEqual<@P.AsString>>>
.View.Select(Base, pmProject.NoteID, "PRJUNI0096");

bool pr100IsInt = false;
int pr100Result = 0;

bool pr82IsInt = false;
int pr82Result = 0;

bool pr83IsInt = false;
int pr83Result = 0;

bool pr95IsDate = false;
DateTime pr95Result = System.DateTime.Today;

bool pr96IsDate = false;
DateTime pr96Result = System.DateTime.Today;

if (pr100 != null)
{
pr100IsInt = int.TryParse(pr100.Value, out pr100Result);
}

if (pr82 != null)
{
pr82IsInt = int.TryParse(pr82.Value, out pr82Result);
}

if (pr83 != null)
{
pr83IsInt = int.TryParse(pr83.Value, out pr83Result);
}

if (pr95 != null)
{
pr95IsDate = DateTime.TryParse(pr95.Value, out pr95Result);
}

if (pr96 != null)
{
pr96IsDate = DateTime.TryParse(pr96.Value, out pr96Result);
}

//PRJCNI0033 PRJUNI0095 - PRJUNI0096
if (pr95IsDate && pr96IsDate)
{
TimeSpan difference = pr96Result - pr95Result;
double days = difference.TotalDays;

int daysResult;
bool gotDays = int.TryParse(days.ToString(), out daysResult);
if (gotDays)
{
ext.UsrPRJCNI0033 = daysResult + 1;
}
}

//ORDER IS IMPORTANT!!!!

//PRJUNI0019 PRJUNI0082 * PRJUNI0083
if (pr82IsInt && pr83IsInt)
{
ext.UsrPRJUNI0019 = pr82Result * pr83Result;
}

//PRJUNI0004 PRJUNI0100 / PRJUNI0019
if (ext.UsrPRJUNI0019 != null & ext.UsrPRJUNI0019 > 0 && pr100IsInt)
{
ext.UsrPRJUNI0004 = pr100Result / ext.UsrPRJUNI0019;
}

//PRJUNI0006 Total value / (PRJUNI0019* 1000)
if (ext.UsrPRJUNI0019 != null && ext.UsrPRJUNI0019 > 0 && pmProjectRevenueTotal != null)
{
if (pmProject.IncludeCO == true)
{
ext.UsrPRJUNI0006 = pmProjectRevenueTotal.CuryRevisedAmount / (ext.UsrPRJUNI0019 * 1000);
}
else
{
ext.UsrPRJUNI0006 = pmProjectRevenueTotal.CuryAmount / (ext.UsrPRJUNI0019 * 1000);
}
}

//PRJUNI0005 PRJUNI0100 / PRJUNI0083
if (pr83IsInt && pr83Result > 0 && pr100IsInt)
{
//Base.Project.Cache.SetValue<GSContractExt.usrPRJUNI0005>(pmProject, pr100Result / pr83Result);
ext.UsrPRJUNI0005 = pr100Result / pr83Result;
}

//PRJUNI0007 Total value / PRJUNI0083
if (pr83IsInt && pr83Result > 0 && pmProjectRevenueTotal != null)
{
if (pmProject.IncludeCO == true)
{
ext.UsrPRJUNI0007 = pmProjectRevenueTotal.CuryRevisedAmount / pr83Result;
}
else
{
ext.UsrPRJUNI0007 = pmProjectRevenueTotal.CuryAmount / pr83Result;
}
}
}

 


Joe Schmucker
Captain II
Forum|alt.badge.img+3
  • Author
  • Captain II
  • December 16, 2024

I just made a change and I think this no longer breaks the rules.  I am not sure why I didn’t do it this way to begin with, but after a full day, I think I was just tired and was stuck in a mental rut.

I removed the RowSelected handler and I fire my calcs from this one.

        protected void CSAnswers_Value_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e)
        {
            DoKPI();
        }

It seems to be working OK.  Originally, I tried to embody all that logic into the CSAnswers_Value_FieldUpdated handler and it was just a mess.  You had to check which field was updated and try to determine which calculations needed to be done.