We have a customer who has requested to have the "Duration" or "End Time" field rounded up to the nearest quarter hour in Service Appointment entry (whichever makes the most sense) Service Appointment Entry screen
With the service appointments there's alot going on. Does anyone have any thoughts on where would be the best way to approach this? There are a lot of events going on that modify the duration and end time and I wouldn't want to have to modify all of them. I'm wondering if its possible to just modify something on the DAC to automatically round the end time up to the quarter hour.
Got Pretty far with modifying the DAC. No errors in the code and as I step through it I can see its rounding the time like I want it to. However the field isnt setting in the screen. Am i missing something silly?
namespace PX.Objects.FS
{
public class FSAppointmentDetServiceExt : PXCacheExtension<PX.Objects.FS.FSAppointmentDetService>
// public class FSAppointmentDetServiceExt : PXCacheExtension<PX.Objects.FS.FSAppointmentDetService>
{
#region ActualDateTimeEnd
[PXDBDateAndTime(UseTimeZone = true, PreserveTime = true, DisplayNameDate = "Actual Date End", DisplayNameTime = "Actual Time End - Nicole")]
[PXUIField(DisplayName = "Actual Date", Visibility = PXUIVisibility.SelectorVisible)]
public virtual DateTime? ActualDateTimeEnd
{
get
{
return this._ActualDateTimeEnd;
}
set
{
this._ActualDateTimeEnd = RoundUp(value, TimeSpan.FromMinutes(15));
}
}
#pragma warning disable PX1026 // Underscores cannot be used in DAC declarations
public DateTime? _ActualDateTimeEnd;
#pragma warning restore PX1026 // Underscores cannot be used in DAC declarations
public static DateTime? RoundUp(DateTime? dt, TimeSpan d)
{
DateTime nt = Convert.ToDateTime(dt);
if (nt != null)
{
return new DateTime((nt.Ticks + d.Ticks - 1) / d.Ticks * d.Ticks, nt.Kind);
}
else
{
return nt;
}
}
#endregion
}
}
I'm wondering if its possible to just modify something on the DAC to
automatically round the end time up to the quarter hour.
Yes this is possible and I would recommend it since it's the easiest (perhaps not best) approach.
Create a DAC extension in CODE section, override (replace) the field dates you need to round. Add a backing field to the property and modify the setter property to round the time.
Example:
[PXDBDateAndTime(UseTimeZone = true, PreserveTime = true, DisplayNameDate = "Actual Date Time Begin", DisplayNameTime = "Actual Start Time")]
[PXUIField(DisplayName = "Actual Date", Visibility = PXUIVisibility.SelectorVisible)]
public virtual DateTime? ActualDateTimeBegin
{
get { return _ActualDateTimeBegin; }
set
{
_ActualDateTimeBegin = RoundTime(value);
}
}
public DateTime? _ActualDateTimeBegin;
public DateTime? RoundTime(DateTime? dateTime)
{
// return the rounded datetime
}
Related
I am building a simple time clock screen. Employee number is entered; using the FieldUpdated event the employee name is filled out, along with the current time (PX.Common.PXTimeZoneInfo.Now) in the Clock Time field. Clicking SAVE puts the data in the table and as you can see it is correct.
Also on the Acumatica screen UPTO this point, the time displayed is correct.
Upon refresh of the screen the time displayed is wrongly shown as 12:00.
My DAC code for the ClockTime looks like:
#region ClockTime
[PXDBDate()]
[PXUIField(DisplayName = "Clock Time")]
public virtual DateTime? ClockTime { get; set; }
public abstract class clockTime : PX.Data.BQL.BqlDateTime.Field<clockTime> { }
#endregion
And my event handler:
protected virtual void _(Events.FieldUpdated<EMPTimeEntries, EMPTimeEntries.employeeID> e)
{
EMPTimeEntries row = e.Row;
if (row.EmployeeID != null)
{
EPEmployee employeeCard = PXSelectorAttribute.Select<EMPTimeEntries.employeeID>(e.Cache, row) as EPEmployee;
row.EmployeeName = employeeCard.AcctName;
row.ClockTime = PX.Common.PXTimeZoneInfo.Now;
}
}
Any thoughts on why this is happening?
Let me know if you need any further information.
I think I have it. I added to my DAC attribute:
[PXDBDate(PreserveTime = true)]
Please advice if there is a more proper way, or if this is the way.
I have 50+ custom paired fields "Inches" and "Centimeters", each enabled and editable. I need to update "Inches" if the user changed the value of "Centimeters" and visa verse. I was able to do this using SetValuePending on one of the paired fields and SetValueExt on the other during the Field Updated Event. My question, is there a way to do this on a higher level without having to do a Field_Updated event for all the 100+ fields. I know that Formulas would create a circular reference so cannot be used. Thanks
Well, you can use one method to handle FieldUpdated events for all fields you need using graph FieldUpdated.AddHandler method in constructor. To get a field name just extend a standard Acumatica FieldUpdated delegate with one additional parameter (name for example) and put it during the FieldUpdated.AddHandler call.
Here is an example with "Invoices and Memos" screen and ARInvoiceEntry graph.
public ARInvoiceEntry()
{
FieldUpdated.AddHandler(typeof(ARTran), typeof(ARTran.inches).Name, (sender, e) => CommonFieldUpdated(sender, e, typeof(ARTran.inches).Name));
FieldUpdated.AddHandler(typeof(ARTran), typeof(ARTran.centimeters).Name, (sender, e) => CommonFieldUpdated(sender, e, typeof(ARTran.centimeters).Name));
...
}
protected virtual void CommonFieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e, string name)
{
// do something
}
Moreover, you can add handlers dynamically using fields collection for example
foreach(var field in Caches[typeof(ARTran)].Fields)
{
// add handler depends on field name
}
You can do it by PXFormula + ExternalValue BQL expression (the same as PXRowUpdatedEventArgs.ExternalCall for example), which will prevent circular reference between pair fields. The idea is to calculate field only when a related field has been changed by the user from UI (ExternalCall = true) and skip calculation when related field updated by the formula (ExternalCall = false).
public class centimetersInInches : PX.Data.BQL.BqlDecimal.Constant<centimetersInInches>
{
public centimetersInInches() : base(2.54m) { }
}
[PXDecimal]
[PXUIField(DisplayName = "Inches")]
[PXUnboundDefault(TypeCode.Decimal, "0.0")]
[PXFormula(typeof(ExternalValue<Div<centimeters, centimetersInInches>>))]
public virtual decimal? Inches { get; set; }
public abstract class inches : PX.Data.BQL.BqlDecimal.Field<inches> { }
[PXDecimal]
[PXUIField(DisplayName = "Centimeters")]
[PXUnboundDefault(TypeCode.Decimal, "0.0")]
[PXFormula(typeof(ExternalValue<Mult<inches, centimetersInInches>>))]
public virtual decimal? Centimeters { get; set; }
public abstract class centimeters : PX.Data.BQL.BqlDecimal.Field<centimeters> { }
And aspx
<px:PXGridColumn DataField="Inches" CommitChanges="True" />
<px:PXGridColumn DataField="Centimeters" CommitChanges="True" />
You can use the RowUpdated event and compare the old row with the new row to detect which field changed. I agree that keeping all logic in a single method is preferable.
public virtual void DAC_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
DAC row = e.Row as DAC;
DAC oldRow = e.OldRow as DAC;
if (row == null || oldRow == null) return;
// Compare old row with new row to determine which field changed
if (row.Inches != oldRow.Inches)
{
// Inches field changed, update CM value
row.CM = row.Inches * INCHES_TO_CM_CONSTANT;
}
// Add more conditions for the other fields
[..]
}
I know that Formulas would create a circular reference so cannot be
used.
Yes I wouldn't recommend it either, you could use the DAC property Setter though.
public string _Inches
public virtual string Inches
{
get
{
return this._Inches;
}
set
{
this._Inches = value;
this.CM = value * INCHES_TO_CM_CONSTANT;
}
}
For all solution (except Formula/DAC attributes) I think a condition to stop recursion should be possible if it's absolutely necessary:
if (this.CM != value * INCHES_TO_CM_CONSTANT)
this.CM = value * INCHES_TO_CM_CONSTANT;
Ideally proper use/avoidance of SetValueExt to control when events are raised (Ext method raises events) would be enough to stop infinite loops.
Unable to setup unit price updating gross profit percent and back.
I have successfully created a new field for the sales order line to display gross profit percent, but now the user wants a way to adjust the percentage and update the unit price as well. All my attempts so far have apparently caused an infinite loop.
Attribute added to [UnitPrice] field in the DAC:
[PXFormula(typeof(Switch<Case<Where<SOLineExt.usrGPPercent, Equal<decimal0>>, decimal0>, Mult<SOLineExt.usrGPPercent, SOLine.curyUnitCost>>))]
[UsrGPPercent] field attributes:
[PXDBDecimal]
[PXUIField(DisplayName="GPPercent", Visible = false)]
[PXFormula(typeof(Switch<Case<Where<SOLine.curyLineAmt, Equal<decimal0>>, decimal0>, Div<SOLineExt.usrTotalProfit, SOLine.curyLineAmt>>))]
[PXDefault(TypeCode.Decimal, "0.0")]
[UsrGPPct] Field attributes:
[PXUIField(DisplayName = "GP %", Enabled = true)]
[PXFormula(typeof(Mult<SOLineExt.usrGPPercent, decimal100>))]
[PXDefault(TypeCode.Decimal, "0.0")]
The above all works perfectly and update the GP% as expected.
Attempt #1, added the following attribute to [UsrGCP] (I realize the math is incomplete, just trying to proof-of-concept at this point).
[PXFormula(typeof(Switch<Case<Where<SOLine.curyLineAmt, Equal<decimal0>>, decimal0>, Div<SOLineExt.usrTotalProfit, SOLine.curyLineAmt>>))]
Attempt #2: FieldUpdated handler:
protected void SOLine_UsrGPPct_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if(InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
var row = (SOLine)e.Row;
PX.Objects.SO.SOLineExt soLineExt = PXCache<SOLine>.GetExtension<PX.Objects.SO.SOLineExt>(row);
if (row.OrderType == "SO")
{
if (soLineExt.UsrGPPct > 0)
{
row.CuryUnitPrice = row.CuryUnitCost + (soLineExt.UsrGPPct * row.CuryUnitCost);
}
}
}
Both methods apparently resulted in an infinite loop (guessing since the debugger was triggered and IIS had to be reset). The goal is to only update once when either field is updated by the user and ignore updates made by the system. Any ideas?
Based on HB_Acumatica's response I updated the code above to:
protected void SOLine_UsrGPPct_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if(InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
var row = (SOLine)e.Row;
PX.Objects.SO.SOLineExt soLineExt = PXCache<SOLine>.GetExtension<PX.Objects.SO.SOLineExt>(row);
if (e.ExternalCall)
{
if (soLineExt.UsrGPPct > 0)
{
if (row.OrderType == "SO")
{
decimal NewUnitPrice;
decimal GPPercent = soLineExt.UsrGPPct ?? 0;
decimal UnitCost = row.CuryUnitCost ?? 0;
NewUnitPrice = UnitCost + ((GPPercent / (decimal)100) * UnitCost);
row.CuryUnitPrice = NewUnitPrice;
row.UnitPrice = NewUnitPrice;
}
}
}
}
This almost works. Nothing happens on the screen, but when saved the unit price does indeed update and save properly. Almost there, is there some sort of update step I'm missing?
This is usually fixed with e.ExternalCall in the field updated event handler to prevent recursion:
protected void SOLine_UsrGPPct_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if (e.ExternalCall)
{
// Make sure it gets in here only once when value change in UI
}
}
ExternalCall Property (PXRowUpdatingEventArgs)
Gets the value indicating, if it equals true, that the update of the
DAC object has been initiated from the UI or through the Web Service
API
Besides that, the other case is when two fields attributes are inter-dependent and need each other to compute their value. You can't have field 1 with a formula that depends on field 2 along with field 1 that also has a formula referencing field 2.
With that kind of setup, when asking for field 1 it will first try to evaluate the dependent field 2 in the field 1 formula and since field 2 also depends on field 1 it will start an infinite recursion loop.
There's no way out of this one that I know, you would have to re-design the DAC fields so none are mutually inter-dependent. Or just leave out the DAC attributes and perform the computation in RowSelected events (this has some disadvantages because it requires graph to work).
EDIT:
About fields values not changing in UI. This might happen when some fields need some events to be triggered when their value change (generally FieldUpdated event).
Instead of assigning values like this which doesn't raise any event:
row.Field = value;
You can use SetValueExt instead which raises events like FieldUpdated:
sender.SetValueExt<DAC.field>(row, value);
Sender needs to be cache of DAC type, in your case it should be otherwise it's:
Base.Caches[typeof(DAC)].SetValueExt<DAC.field>(row, value);
Note that a field that has a PXFormula attribute will get it's value re-computed at some point so you shouldn't manually set the value of such fields.
If you still have such an attribute on UnitPrice field you shouldn't manually update the field value:
[PXFormula(typeof(Switch<Case<Where<SOLineExt.usrGPPercent, Equal<decimal0>>, decimal0>, Mult<SOLineExt.usrGPPercent, SOLine.curyUnitCost>>))]
Maybe all you need to do is force the formula to be refreshed when UsrGPPercent changes. If that's the case, instead of setting UnitPrice value try using RaiseFieldDefaulting method to have the formula recalculated:
object dummy;
sender.RaiseFieldDefaulting<SOLine.unitPrice>(e.Row, out dummy);
Thanks so much to HB_ACUMATICA!
Final code is posted below for anyone trying to do something similar. Also cleaned up.
Two fields added to the DAC (and SalesOrder screen):
UsrGPPercent
Attributes:
[PXDBDecimal()]
[PXUIField(DisplayName = "GP %", Enabled = true, Visible = true)]
[PXFormula(typeof(Mult<Switch<Case<Where<SOLine.curyExtCost, Equal<decimal0>>, decimal0>, Div<SOLineExt.usrTotalProfit, SOLine.curyExtCost>>, decimal100>))]
[PXDefault(TypeCode.Decimal, "0.0")]
Code Extension:
#region Event Handlers
protected void SOLine_UsrGPPercent_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if(InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
var row = (SOLine)e.Row;
PX.Objects.SO.SOLineExt soLineExt = PXCache<SOLine>.GetExtension<PX.Objects.SO.SOLineExt>(row);
if (e.ExternalCall)
{
if (soLineExt.UsrGPPercent > 0)
{
if (row.OrderType == "SO")
{
decimal NewUnitPrice;
decimal GPPercent = soLineExt.UsrGPPercent ?? 0;
decimal UnitCost = row.CuryUnitCost ?? 0;
decimal QtyOrdered = row.OrderQty ?? 0;
NewUnitPrice = (UnitCost + ((GPPercent / (decimal)100) * UnitCost));
soLineExt.UsrTotalProfit = (NewUnitPrice * QtyOrdered) - (UnitCost * QtyOrdered);
row.CuryUnitPrice = NewUnitPrice;
row.UnitPrice = NewUnitPrice;
}
}
}
}
#endregion
UsrTotalProfit
Attributes:
[PXDBCurrency(typeof(SOLine.curyInfoID), typeof(SOLineExt.usrTotalProfit))]
[PXUIField(DisplayName = "Total Profit", Enabled = false)]
[PXFormula(typeof(Sub<SOLine.curyLineAmt, SOLine.curyExtCost>))]
[PXDefault(TypeCode.Decimal, "0.0")]
We have a button "Start" that populates the dateStarted field to DateTime.Now..
When retrieving datetime, it always shows the server's date/time instead of user's local timezone version. How do i make it work like lastModifiedDateTime/CreatedDateTime that whenever we view it it's formatted as user's local timezone. I tried UseTimeZone = true/false but nothing is working
Here is my code that is not working
#region DateStarted
public abstract class dateStarted : PX.Data.IBqlField
{
}
protected DateTime? _DateStarted;
[PXDBDateAndTime(DisplayNameDate = "Date Started", DisplayNameTime = "Time", UseTimeZone = true)]
public virtual DateTime? DateStarted
{
get
{
return this._DateStarted;
}
set
{
this._DateStarted = value;
}
}
#endregion
It sounds like just using PX.Common.PXTimeZoneInfo.Now is enough to do the job.
PX.Common.PXTimeZoneInfo also has UtcNow, UtcToday, and Today if needed
Look up the code of class PXDBDateAndTime in Source Code screen SM204570. For debugging purpose you can copy the whole attribute and rename it to something else like PXDBDateAndTimeDebug:
[PXDBDateAndTimeDebug(DisplayNameDate = "Date Started", DisplayNameTime = "Time", UseTimeZone = true)]
With that approach you can debug SetUseTimeZone and GetTimeZone methods. The time zone used is coming from LocaleInfo.GetTimeZone method and you should debug that too:
public static PXTimeZoneInfo GetTimeZone()
{
if (!PXContext.PXIdentity.IsAnonymous() && PXContext.PXIdentity.TimeZone != null)
{
return PXContext.PXIdentity.TimeZone;
}
return PXTimeZoneInfo.Invariant;
}
The issue here is that from your question we can't tell if there's actually a problem with Acumatica time zone handling or if the user profile time zone is properly configured in your instance or if the result you're expecting is actually a valid ISO conversion as done by the DotNet framework. Debugging step by step should reveal what's going on.
After analyzing how CreatedDateTime and LastModifiedDatetime and other dates behave the same, the problem is in the input. So i created the following code to save the correct datetime with regards to the current user's timezone.
public static class DateTimeHelper
{
public static DateTime? Now()
{
var test = LocaleInfo.GetTimeZone();
PXTimeZoneInfo timezone = LocaleInfo.GetTimeZone();
DateTime dt = DateTime.UtcNow;
dt = PXTimeZoneInfo.ConvertTimeFromUtc(dt, timezone);
return dt;
}
}
and the implementation:
public PXAction<CQLMChecklists> startButton;
[PXUIField(DisplayName = "Start", Visible = true)]
[PXButton()]
public virtual void StartButton()
{
if (Document.Current != null)
{
CQLMChecklists doc = Document.Current;
Actions.PressSave();
CommenceChecklist(DateTimeHelper.Now(), DateTimeHelper.Now().Value, ref doc);
Document.Update(doc);
}
Actions.PressSave();
}
I want to create a time field which displays and allows user to select only Time. I have tried setting it some properties (TimeMode=True, DisplayFormat=hh:mm, EditFormat=hh:mm) but while the format remains as hh:mm but dropdown still has default date in it (see below screenshots).
Please suggest.
I have used some time only datetime fields... here is a quick sample:
On your DAC field, set it to use the PXDBTime attribute type as shown below...
[PXDBTime(DisplayMask = "t", UseTimeZone = false)]
[PXUIField(DisplayName = "My Time")]
public virtual DateTime? MyTime
{
get { return this._MyTime; }
set { this._MyTime = value; }
}
In your page, use the following...
<px:PXDateTimeEdit ID="edMyTime" runat="server" DataField="MyTime" DisplayFormat="t" EditFormat="t" TimeMode="True" SuppressLabel="True"/>