Acumatica ยท Customization

DAC Extension on a View, Not a Table in Acumatica

Most DAC extensions target a real table-backed DAC - SOOrder , ARInvoice , something with rows that actually live in SQL Server.

John Kihiu12 min read

Most DAC extensions target a real table-backed DAC - SOOrder, ARInvoice, something with rows that actually live in SQL Server. Less commonly, and less well understood, you need to extend a DAC that's really a projection or a view: a Generic Inquiry-backed result type, a report DAC, or a PXProjection composed from a join across several real tables. The rules are mostly the same, but the exceptions are exactly the ones that cause confusing compile errors or silent no-ops if you don't know they're coming.

A view-backed DAC has no column of its own to persist into

A PXProjection<T> DAC (used heavily under Generic Inquiries and some report screens) maps its fields to expressions over a join, not to columns of a single table. Extending it with a normal PXCacheExtension<T> field that uses PXDBString or PXDBDecimal - attributes that assume a real, persistable column - either fails at schema sync or, worse, appears to work in the designer and then throws at runtime the first time the framework tries to actually persist a change to that field:

C#
// Wrong: PXDBString implies a persistable column on a projection DAC
// that has no backing table of its own - this is a trap, not a feature
public sealed class OpenOrderSummaryExt : PXCacheExtension<OpenOrderSummary>
{
    [PXDBString(20)]   // will not persist correctly against a projection
    public string UsrNote { get; set; }
}

// Correct: unbound field, computed or set in-memory only, never
// expected to round-trip through SQL against the projection's own storage
public sealed class OpenOrderSummaryExt : PXCacheExtension<OpenOrderSummary>
{
    [PXString(20)]
    [PXUIField(DisplayName = "Note", Enabled = true)]
    public string UsrNote { get; set; }
    public abstract class usrNote : PX.Data.BQL.BqlString.Field<usrNote> { }
}

If you genuinely need a persisted value tied to a row that a view represents, the pattern is to store it on a real side table keyed by whatever the view's natural key is, and populate the unbound extension field from that side table in RowSelected - not to pretend the projection itself can take writes it was never built to hold.

Extend the underlying table when you can, extend the view only when you must

A frequent mistake: a developer needs a new field visible on a Generic-Inquiry-backed screen, and reaches straight for extending the GI's projection DAC, when the actual right move is extending the real underlying table DAC (via a normal PXCacheExtension) and then adding that field to the GI's field list through the GI designer. Extending the underlying table gives you a real, persistable, BQL-queryable field usable everywhere, not just on the one screen built from that particular projection. Extend the projection DAC directly only when the field is genuinely specific to that one view's presentation - a computed display value that has no meaning outside that particular join, like a formatted concatenation of three other fields solely for one report layout.

Ask "does this field mean anything outside this GI" before choosing where to add it

If the answer is yes - a business user would reasonably want it on the base entity screen, another report, or the REST API someday - put it on the real table via a normal DAC extension. If the answer is genuinely no, it exists only for this one inquiry's display, extending the projection DAC directly is fine and avoids polluting the base table's schema with a field nobody else will ever use.

Event handlers on a view-backed DAC fire the same way, with one caveat

RowSelected, FieldUpdated, and friends fire normally on a PXProjection DAC's rows, same as any table-backed one - this part is not different. The caveat is RowPersisting and RowPersisted: since there's no real save target for the view's own fields, persisting logic you write there needs to actually target the underlying real DAC (via its own PXSelect/graph reference) rather than assuming the projection's row itself is what gets written to SQL. Writing validation logic in RowPersisting on a pure read-only projection DAC that's never actually part of a save operation on that screen is dead code that will never fire, which is a subtle enough mistake that I've seen it shipped and only noticed months later when someone asked why a "validation rule" was never actually blocking anything.

Wrapping up

Extending a view-backed or projection DAC works, but only with unbound attributes (PXString, not PXDBString) since there's no real column behind the field to persist into - and RowPersisting logic on a pure read-only projection is often dead code because the screen never actually saves that DAC's rows. Prefer extending the real underlying table when the field has any meaning beyond one inquiry's display, and reserve direct projection extension for genuinely view-specific, presentation-only fields. Getting this backwards produces customizations that look correct in the designer and fail exactly at the moment someone expects them to save.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.