A formula in Acumatica is a DAC field that is calculated based on the values of other object fields.
To calculate a formula, Aсumatiсa framework provides a set of various operations and functions (such as arithmetical, logical, and comparison operations and string processing functions; see List of Built-In Common Formulas). In addition to the field values, a formula can use various constants provided by both the core of Acumatica and the application solutions. Moreover, a formula can obtain values for the calculation not only from the current record but also from other sources (see Formula Context and Its Modifiers).
The beauty of the formulas is that they will automatically recalculate the value at the right time:
FieldDefaulting
event handler of formula field)FieldUpdated
event handler of each dependent field)RowSelecting
event handler)RowPersisted
event handler)Recalculation of a formula field value on the update of a dependent field raises a FieldUpdated
event for formula field. This allows you to make a chain of dependent formulas (see Direct and Mediated Circular References in Formulas).
Application developers can write their own application-side formulas.
A formula can be used in three main modes:
There is another auxiliary mode, unbound formulas, that is very similar to mixed mode, but the calculated values of the formula are not assigned to the formula field. The aggregated value is calculated immediately and assigned to the field of the parent object. See Usage of Unbound Formulas for more information.
PXFormulaAttribute
Properties and Constructor ParametersThe formula functionality is implemented by PXFormulaAttribute
. The constructor of PXFormulaAttribute has the following signatures:
public PXFormulaAttribute(Type formulaType) { // ... }
The single parameter formulaType
is a type of formula expression to calculate the field value from other fields of the same data record. This parameter must meet one of the following conditions:
public PXFormulaAttribute(Type formulaType, Type aggregateType) { // ... }
The first parameter, formulaType
, is the same as in the first constructor. The second parameter, aggregateType
, is a type of aggregation formula to calculate the parent data record field from the child data record fields. An aggregation function can be used, such as SumCalc, CountCalc, MinCalc, and MaxCalc. Application developers can create their own aggregation formulas.
An aggregate formula type must be a generic type and must implement IBqlAggregateCalculator
interface. The first generic parameter of the aggregate formula type must implement the IBqlField
interface and must have the field type of the parent object.
public virtual bool Persistent { get; set; }
The PXFormulaAttribute.Persistent
property indicates whether the attribute recalculates the formula after changes are saved to the database. You may need recalculation if the fields the formula depends on are updated on the RowPersisting
event. By default, the property equals false
.
In most cases, formulas are used for direct calculation of the value of the formula field from other fields of the same data record.
The simplest example of formula usage:
[PXDBDate] [PXFormula(typeof(FADetails.receiptDate))] [PXDefault] [PXUIField(DisplayName = Messages.PlacedInServiceDate)] public virtual DateTime? DepreciateFromDate { get; set; }
In this example, the value of the ReceiptDate field is assigned to the DepreciateFromDate field on the insertion of a new record and on the update of the ReceiptDate field.
A slightly more complex example:
[PXCurrency(typeof(APPayment.curyInfoID), typeof(APPayment.unappliedBal))] [PXUIField(DisplayName = "Unapplied Balance", Visibility = PXUIVisibility.Visible, Enabled = false)] [PXFormula(typeof(Sub<APPayment.curyDocBal, APPayment.curyApplAmt>))] public virtual Decimal? CuryUnappliedBal { get; set; }
Here, the unapplied balance of the document is calculated as the difference between the balance of the document and the applied amount.
Example of multiple choice with a default value:
[PXUIField(DisplayName = "Class Icon", IsReadOnly = true)] [PXImage] [PXFormula(typeof(Switch< Case<Where<EPActivity.classID, Equal<CRActivityClass.task>>, EPActivity.classIcon.task, Case<Where<EPActivity.classID, Equal<CRActivityClass.events>>, EPActivity.classIcon.events, Case<Where<EPActivity.classID, Equal<CRActivityClass.email>, And<EPActivity.isIncome, NotEqual<True>>>, EPActivity.classIcon.email, Case<Where<EPActivity.classID, Equal<CRActivityClass.email>, And<EPActivity.isIncome, Equal<True>>>, EPActivity.classIcon.emailResponse, Case<Where<EPActivity.classID, Equal<CRActivityClass.history>>, EPActivity.classIcon.history>>>>>, Selector<Current2<EPActivity.type>, EPActivityType.imageUrl>>))] public virtual string ClassIcon { get; set; }
The order of fields in the DAC is important to correct formula calculation. All source fields (from which the formula is calculated) including other formulas must be defined in the DAC before the formula field. Otherwise, the field can be calculated incorrectly or can cause a runtime error.
By default, the context of the formula calculation is restricted by the current object (record) of the class containing the formula declaration. It is also allowed to use constants (descendants of the Constant<>
class).
A formula that uses the fields of its object only:
public partial class Contract : IBqlTable, IAttributeSupport { //... [PXDecimal(4)] [PXDefault(TypeCode.Decimal, "0.0", PersistingCheck = PXPersistingCheck.Nothing)] [PXFormula(typeof(Add<Contract.pendingRecurring, Add<Contract.pendingRenewal, Contract.pendingSetup>>))] [PXUIField(DisplayName = "Total Pending", Enabled=false)] public virtual decimal? TotalPending { get; set; } //... }
However, it is possible to obtain input values for the formula calculation from other sources:
PXSelectorAttribute
.PXParentAttribute
.The formula supports the following context modifiers.
Current<TRecord.field>
and Current2<TRecord.field>
Fetches the field value from the record stored in the Current
property of the TRecord cache.
If the cache's Current
property or the field itself contains null:
Example:
[PXFormula(typeof(Switch< Case<Where< ARAdjust.adjgDocType, Equal<Current<ARPayment.docType>>, And<ARAdjust.adjgRefNbr, Equal<Current<ARPayment.refNbr>>>>, ARAdjust.classIcon.outgoing>, ARAdjust.classIcon.incoming>))] protected virtual void ARAdjust_ClassIcon_CacheAttached(PXCache sender)
Parent<TParent.field>
Fetches the field value from the parent data record as defined by PXParentAttribute residing on the current DAC.
public class INTran : IBqlTable { [PXParent(typeof(Select< INRegister, Where< INRegister.docType, Equal<Current<INTran.docType>>, And<INRegister.refNbr,Equal<Current<INTran.refNbr>>>>>))] public virtual String RefNbr { ... } [PXFormula(typeof(Parent<INRegister.origModule>))] public virtual String OrigModule { ... } }
IsTableEmpty<TRecord>
Returns true
if the DB table corresponding to the specified DAC contains no records, false
otherwise.
public class APRegister : IBqlTable { [PXFormula(typeof(Switch< Case<Where< IsTableEmpty<APSetupApproval>, Equal<True>>, True, Case<Where< APRegister.requestApproval, Equal<True>>, False>>, True>))] public virtual bool? DontApprove { get; set; } }
Selector<KeyField, ForeignOperand>
public class APVendorPrice : IBqlTable { // Note: inventory attribute is an // aggregate containing a PXSelectorAttribute // inside, which is also valid for Selector<>. // - [Inventory(DisplayName = "Inventory ID")] public virtual int? InventoryID [PXFormula(typeof(Selector< APVendorPrice.inventoryID, InventoryItem.purchaseUnit>))] public virtual string UOM { get; set; } }
If the formula field is an unbound field marked with one of the PXFieldAttribute
descendants (such as PXIntAttribute
or PXStringAttribute
), then its calculation is additionally triggered during RowSelecting
event.
TBD
TBD
TBD
TBD
The PXSelectorAttribute attribute (also referred to as the selector), while vital and frequently used, has however two major drawbacks:
"<object_name> cannot be found in the system"
if no items are found to satisfy the selector condition.The PXRestrictorAttribute
(also referred to as the restrictor) can be used to solve these problems.
PXRestrictorAttribute
does not work alone; it should always be paired with a PXSelectorAttribute
. Using the restrictor without the selector will have no effect.
The restrictor finds the selector on the same field, injecting into it an additional condition and the corresponding error message. The restrictor condition is appended to the selector condition via a boolean AND, and an appropriate error message is generated if the referenced object violates the restrictor constraint. Also, if the referenced object has changed and no longer meets the restrictor condition, no error messages are produced when you change any other fields of the referencing object.
General usage:
[PXDBInt] [PXSelector(typeof(Search<FAClass.assetID, Where<FAClass.recordType, Equal<FARecordType.classType>>>), typeof(FAClass.assetCD), typeof(FAClass.assetTypeID), typeof(FAClass.description), typeof(FAClass.usefulLife), SubstituteKey = typeof(FAClass.assetCD), DescriptionField = typeof(FAClass.description), CacheGlobal = true)] [PXRestrictor(typeof(Where<FAClass.active, Equal<True>>), Messages.InactiveFAClass, typeof(FAClass.assetCD))] [PXUIField(DisplayName = "Asset Class", Visibility = PXUIVisibility.Visible)] public virtual int? ClassID { get; set; }
Multiple restrictors can be used with one selector attribute. In this case, all additional restrictor conditions are applied in a non-determined order. Once any condition is violated, the appropriate error message is generated.
The Where<>
condition of the selector itself is applied after all restrictor conditions.
[PXDefault] // An aggregate attribute containing the selector inside. // - [ContractTemplate(Required = true)] [PXRestrictor(typeof(Where<ContractTemplate.status, Equal<Contract.status.active>>), Messages.TemplateIsNotActivated, typeof(ContractTemplate.contractCD))] [PXRestrictor(typeof(Where<ContractTemplate.effectiveFrom, LessEqual<Current<AccessInfo.businessDate>>, Or<ContractTemplate.effectiveFrom, IsNull>>), Messages.TemplateIsNotStarted)] [PXRestrictor(typeof(Where<ContractTemplate.discontinueAfter, GreaterEqual<Current<AccessInfo.businessDate>>, Or<ContractTemplate.discontinueAfter, IsNull>>), Messages.TemplateIsExpired)] public virtual int? TemplateID { get; set; }
The constructor of PXRestrictorAttribute takes three parameters:
IBqlWhere
interface.PX.Objects.GL.Messages
).IBqlField
interface. The values of the fields will be used for error message formatting.Also, there are several options that specify the restrictor behavior.
The ReplaceInherited
property indicates whether the current restrictor should override the inherited restrictors. If this property is set to true, then all inherited restrictors (placed on any aggregate attributes or base attribute) will be replaced.
Replacing inherited restrictors:
[CustomerActive(Visibility = PXUIVisibility.SelectorVisible, Filterable = true, TabOrder = 2)] [PXRestrictor(typeof(Where<Customer.status, Equal<CR.BAccount.status.active>, Or<Customer.status, Equal<CR.BAccount.status.oneTime>, Or<Customer.status, Equal<CR.BAccount.status.hold>, Or<Customer.status, Equal<CR.BAccount.status.creditHold>>>>>), Messages.CustomerIsInStatus, typeof(Customer.status), ReplaceInherited = true)] // Replaced all restrictors from CustomerActiveAttribute [PXUIField(DisplayName = "Customer")] [PXDefault()] public override int? CustomerID { get; set; }
Please note that we do not advise that you use the ReplaceInherited
property in application code when reasonable alternatives exist. This property is primarily intended to be used in customizations.
CacheGlobal
supports global dictionary functionality in the same way as in PXSelectorAttribute
.
When restrictors and a selector are used together, the latter should not contain the IBqlWhere
clause. Ideally, all conditions should be moved into restrictors. This approach provides more user-friendly error messages and eliminates unnecessary retroactive errors.
An ideal example:
[PXDBString(5, IsFixed = true, IsUnicode = false)] [PXUIField(DisplayName = "Type", Required = true)] [PXSelector(typeof(EPActivityType.type), DescriptionField = typeof(EPActivityType.description))] [PXRestrictor(typeof(Where<EPActivityType.active, Equal<True>>), Messages.InactiveActivityType, typeof(EPActivityType.type))] [PXRestrictor(typeof(Where<EPActivityType.isInternal, Equal<True>>), Messages.ExternalActivityType, typeof(EPActivityType.type))] public virtual string Type { get; set; }
Possible retroactive errors:
[PXDBInt] [PXUIField(DisplayName = "Contract")] [PXSelector(typeof(Search2<Contract.contractID, LeftJoin<ContractBillingSchedule, On<Contract.contractID, Equal<ContractBillingSchedule.contractID>>>, Where<Contract.isTemplate, NotEqual<True>, And<Contract.baseType, Equal<Contract.ContractBaseType>, And<Where<Current<CRCase.customerID>, IsNull, Or2<Where<Contract.customerID, Equal<Current<CRCase.customerID>>, And<Current<CRCase.locationID>, IsNull>>, Or2<Where<ContractBillingSchedule.accountID, Equal<Current<CRCase.customerID>>, And<Current<CRCase.locationID>, IsNull>>, Or2<Where<Contract.customerID, Equal<Current<CRCase.customerID>>, And<Contract.locationID, Equal<Current<CRCase.locationID>>>>, Or<Where<ContractBillingSchedule.accountID, Equal<Current<CRCase.customerID>>, And<ContractBillingSchedule.locationID, Equal<Current<CRCase.locationID>>>>>>>>>>>>, OrderBy<Desc<Contract.contractCD>>>), DescriptionField = typeof(Contract.description), SubstituteKey = typeof(Contract.contractCD), Filterable = true)] [PXRestrictor(typeof(Where<Contract.status, Equal<Contract.status.active>>), Messages.ContractIsNotActive)] [PXRestrictor(typeof(Where<Current<AccessInfo.businessDate>, LessEqual<Contract.graceDate>, Or<Contract.expireDate, IsNull>>), Messages.ContractExpired)] [PXRestrictor(typeof(Where<Current<AccessInfo.businessDate>, GreaterEqual<Contract.startDate>>), Messages.ContractActivationDateInFuture, typeof(Contract.startDate))] [PXFormula(typeof(Default<CRCase.customerID>))] [PXDefault(PersistingCheck = PXPersistingCheck.Nothing)] public virtual int? ContractID { get; set; }