Callouts and Field Validation

Level: Intermediate Module: General Foundation 10 min read Lesson 14 of 47

Overview

  • What you’ll learn: How to implement real-time field-level validation and dynamic behavior using iDempiere callouts, including the IColumnCallout interface, CalloutEngine patterns, and practical code examples.
  • Prerequisites: Lessons 1-12 (Beginner level), basic Java knowledge
  • Estimated reading time: 25 minutes

Introduction

In the previous lesson, you learned how to build sophisticated UIs using the Application Dictionary alone. But there are limits to what declarative configuration can do. When a user changes a field value, you often need to respond in real time: auto-fill related fields, recalculate totals, validate business rules, or show warnings. This is exactly what callouts do.

A callout is a piece of Java code that iDempiere executes immediately when a user changes a field’s value in the UI. Unlike model validators (which fire during the save process), callouts fire instantly on field change, giving the user immediate feedback. This lesson teaches you how to write, register, and debug callouts.

Understanding Callouts

Callouts sit in the UI layer. When a user modifies a field value (by typing, selecting from a dropdown, or any other input), iDempiere checks whether that column has a callout registered. If it does, the callout code executes before the user moves to the next field.

Key Characteristics

  • Triggered on field change — not on save, not on load, but on value change.
  • Synchronous — the UI waits for the callout to complete before the user can continue.
  • Can modify other fields — a callout on field A can set the values of fields B, C, and D.
  • Can return error messages — returning a non-empty string displays an error and reverts the field change.
  • UI-only — callouts do not fire during server-side imports, processes, or API calls. Only direct user interaction triggers them.

The IColumnCallout Interface

The modern way to implement callouts in iDempiere is through the IColumnCallout interface. This is the preferred approach for plugin-based development.

package org.idempiere.callout;

import java.util.Properties;
import org.adempiere.base.IColumnCallout;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;

public class MyCallout implements IColumnCallout {

    @Override
    public String start(Properties ctx, int WindowNo,
                        GridTab mTab, GridField mField,
                        Object value, Object oldValue) {

        // ctx       - Application context (preferences, session info)
        // WindowNo  - Window number (for context variable scoping)
        // mTab      - The tab containing the changed field
        // mField    - The field that was changed
        // value     - The new value
        // oldValue  - The previous value

        // Return "" for success, or an error message string to revert
        return "";
    }
}

Each parameter serves a specific purpose:

  • ctx (Properties) — The application context containing login info, preferences, and session-level variables. Access values with Env.getContext(ctx, WindowNo, "ColumnName").
  • WindowNo (int) — Identifies the window instance. Important when the user has multiple windows open, as context variables are scoped by window number.
  • mTab (GridTab) — Provides access to all fields in the current tab. Use mTab.setValue("ColumnName", newValue) to set other field values.
  • mField (GridField) — The specific field that triggered the callout. Contains metadata like column name, display type, and current value.
  • value (Object) — The new value the user entered. You must cast it to the appropriate type.
  • oldValue (Object) — The value before the change. Useful for comparison logic.

The CalloutEngine Base Class

The CalloutEngine class is the traditional (pre-plugin) approach. It is still widely used in core iDempiere code. You extend CalloutEngine and register individual methods:

package org.compiere.model;

import java.util.Properties;
import org.compiere.model.CalloutEngine;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;

public class CalloutMyTable extends CalloutEngine {

    /**
     * Called when C_BPartner_ID changes.
     * Auto-fills the contact and location fields.
     */
    public String bpartner(Properties ctx, int WindowNo,
                           GridTab mTab, GridField mField,
                           Object value) {

        if (isCalloutActive())  // Prevent recursive callouts
            return "";

        Integer bpartnerId = (Integer) value;
        if (bpartnerId == null || bpartnerId == 0)
            return "";

        // Look up the default contact for this business partner
        int contactId = DB.getSQLValue(null,
            "SELECT AD_User_ID FROM AD_User " +
            "WHERE C_BPartner_ID=? AND IsActive='Y' " +
            "ORDER BY IsDefaultContact DESC, AD_User_ID LIMIT 1",
            bpartnerId);

        if (contactId > 0) {
            mTab.setValue("AD_User_ID", contactId);
        }

        // Look up the default location
        int locationId = DB.getSQLValue(null,
            "SELECT C_BPartner_Location_ID FROM C_BPartner_Location " +
            "WHERE C_BPartner_ID=? AND IsActive='Y' " +
            "ORDER BY IsShipTo DESC, C_BPartner_Location_ID LIMIT 1",
            bpartnerId);

        if (locationId > 0) {
            mTab.setValue("C_BPartner_Location_ID", locationId);
        }

        return "";  // Success
    }
}

The isCalloutActive() Guard

Notice the isCalloutActive() check at the top. This is a critical pattern. When your callout sets a value on another field via mTab.setValue(), that field change can trigger its own callout, which might set another field, and so on — creating infinite recursion. The isCalloutActive() method returns true if a callout is already executing, preventing this chain.

Writing a Complete Callout: A Practical Example

Let us build a callout for a custom Sales Commission table. When the user selects a Product, the callout auto-fills the Commission Percentage from a rate table, and calculates the Commission Amount based on the line total.

package com.mycompany.callout;

import java.math.BigDecimal;
import java.util.Properties;
import org.adempiere.base.IColumnCallout;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;
import org.compiere.util.DB;
import org.compiere.util.Env;

public class CalloutCommission implements IColumnCallout {

    @Override
    public String start(Properties ctx, int WindowNo,
                        GridTab mTab, GridField mField,
                        Object value, Object oldValue) {

        String columnName = mField.getColumnName();

        if ("M_Product_ID".equals(columnName)) {
            return product(ctx, WindowNo, mTab, value);
        }
        if ("LineNetAmt".equals(columnName)) {
            return calculateCommission(mTab);
        }

        return "";
    }

    private String product(Properties ctx, int WindowNo,
                           GridTab mTab, Object value) {

        Integer productId = (Integer) value;
        if (productId == null || productId == 0) {
            mTab.setValue("CommissionPct", Env.ZERO);
            mTab.setValue("CommissionAmt", Env.ZERO);
            return "";
        }

        // Look up the commission rate for this product
        BigDecimal rate = DB.getSQLValueBD(null,
            "SELECT CommissionPct FROM Z_CommissionRate " +
            "WHERE M_Product_ID=? AND IsActive='Y' " +
            "AND AD_Client_ID=?",
            productId,
            Env.getAD_Client_ID(ctx));

        if (rate == null) {
            rate = Env.ZERO;
        }

        mTab.setValue("CommissionPct", rate);

        // Recalculate commission amount
        return calculateCommission(mTab);
    }

    private String calculateCommission(GridTab mTab) {
        BigDecimal lineAmt = (BigDecimal) mTab.getValue("LineNetAmt");
        BigDecimal pct = (BigDecimal) mTab.getValue("CommissionPct");

        if (lineAmt == null) lineAmt = Env.ZERO;
        if (pct == null) pct = Env.ZERO;

        // Commission = LineNetAmt * CommissionPct / 100
        BigDecimal commission = lineAmt.multiply(pct)
            .divide(Env.ONEHUNDRED, 2, BigDecimal.ROUND_HALF_UP);

        mTab.setValue("CommissionAmt", commission);
        return "";
    }
}

Registering Callouts

Method 1: Application Dictionary Registration

For CalloutEngine-style callouts, set the Callout field on the AD_Column record:

-- In AD_Column.Callout field:
org.compiere.model.CalloutMyTable.bpartner

The format is fully.qualified.ClassName.methodName. You can chain multiple callouts by separating them with semicolons:

org.compiere.model.CalloutOrder.bPartner;org.compiere.model.CalloutOrder.bPartnerBill

Method 2: IColumnCalloutFactory (Plugin Approach)

For IColumnCallout implementations, you register a factory as an OSGi service. Create a factory class:

package com.mycompany.callout;

import java.util.ArrayList;
import java.util.List;
import org.adempiere.base.IColumnCallout;
import org.adempiere.base.IColumnCalloutFactory;
import org.compiere.model.GridField;
import org.compiere.model.GridTab;
import org.compiere.model.MColumn;

public class MyCalloutFactory implements IColumnCalloutFactory {

    @Override
    public IColumnCallout[] getColumnCallouts(String tableName,
                                               String columnName) {

        List<IColumnCallout> list = new ArrayList<>();

        if ("Z_Commission".equals(tableName)) {
            if ("M_Product_ID".equals(columnName)
                || "LineNetAmt".equals(columnName)) {
                list.add(new CalloutCommission());
            }
        }

        return list.toArray(new IColumnCallout[0]);
    }
}

Then register it in your plugin’s OSGI-INF component descriptor XML or via annotations:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
    name="com.mycompany.callout.factory">
    <implementation class="com.mycompany.callout.MyCalloutFactory"/>
    <service>
        <provide interface="org.adempiere.base.IColumnCalloutFactory"/>
    </service>
</scr:component>

Callout vs Model Validator: When to Use Which

This is one of the most common questions for iDempiere developers. Here is a clear decision framework:

Criteria Callout Model Validator
When it fires On field change (UI only) On save/delete (all sources)
Triggered by imports No Yes
Triggered by processes No Yes
Triggered by API/web service No Yes
User feedback Immediate (before save) At save time
Best for UX: auto-fill, calculate, hint Data integrity enforcement

Rule of thumb: Use callouts for user experience (auto-fill, calculate, show/hide). Use model validators for data integrity (must always be enforced, regardless of how data enters the system). Often you will use both: a callout for immediate feedback and a model validator as the safety net.

Common Callout Patterns

Pattern 1: Auto-Fill Related Fields

// When user selects a product, fill in UOM and price
public String product(Properties ctx, int WindowNo,
                      GridTab mTab, GridField mField, Object value) {
    Integer productId = (Integer) value;
    if (productId == null || productId == 0) return "";

    MProduct product = MProduct.get(ctx, productId);
    mTab.setValue("C_UOM_ID", product.getC_UOM_ID());
    mTab.setValue("PriceList", product.getPriceList());
    return "";
}

Pattern 2: Calculate Derived Values

// When qty or price changes, recalculate line total
public String amt(Properties ctx, int WindowNo,
                  GridTab mTab, GridField mField, Object value) {
    BigDecimal qty = (BigDecimal) mTab.getValue("QtyOrdered");
    BigDecimal price = (BigDecimal) mTab.getValue("PriceActual");

    if (qty == null) qty = Env.ZERO;
    if (price == null) price = Env.ZERO;

    mTab.setValue("LineNetAmt", qty.multiply(price));
    return "";
}

Pattern 3: Validate and Reject

// Reject negative quantities
public String qty(Properties ctx, int WindowNo,
                  GridTab mTab, GridField mField, Object value) {
    BigDecimal qty = (BigDecimal) value;
    if (qty != null && qty.signum() < 0) {
        return "Quantity cannot be negative";  // Error: reverts field
    }
    return "";
}

Debugging Callouts

When a callout does not behave as expected, use these techniques:

  • Logging: Use CLogger to add debug output: private static final CLogger log = CLogger.getCLogger(MyCallout.class); then log.fine("Callout fired for product: " + value);
  • Eclipse Debugger: Set breakpoints in your callout code and run iDempiere from Eclipse in debug mode. The debugger pauses execution when the callout fires.
  • Check Registration: Verify the callout is registered on the correct column. A common mistake is registering on the wrong table or column name.
  • Check isCalloutActive(): If your callout appears to not fire, another callout might be suppressing it via the active flag.
  • Null Checks: Always handle null values. Users can clear a field, sending null as the new value.

GridTab and GridField Access

The GridTab and GridField objects provide rich access to the current UI state:

// GridTab: Access any field in the current tab
Object val = mTab.getValue("ColumnName");           // Get current value
mTab.setValue("ColumnName", newValue);               // Set a value
int recordId = mTab.getRecord_ID();                  // Current record ID
boolean isNew = mTab.isNew();                        // Is this a new record?

// GridField: Metadata about the changed field
String colName = mField.getColumnName();             // Column name
int displayType = mField.getDisplayType();           // Display type
boolean isMandatory = mField.isMandatory(false);     // Is it mandatory?
String header = mField.getHeader();                  // Field label

Key Takeaways

  • Callouts fire on UI field change, providing immediate feedback to users.
  • The IColumnCallout interface is the modern approach; CalloutEngine is the traditional approach.
  • Always use isCalloutActive() guards in CalloutEngine to prevent infinite recursion.
  • Register callouts via AD_Column.Callout (traditional) or IColumnCalloutFactory (plugin).
  • Use callouts for UX improvements (auto-fill, calculate), use model validators for data integrity enforcement.
  • Callouts are UI-only — they do not fire during imports, processes, or API calls.
  • Return an empty string for success, or an error message to revert the field change.

What’s Next

In Lesson 15, you will learn about Workflow Management — how to configure document processing workflows, build approval chains, and automate business operations using iDempiere’s built-in workflow engine.

You Missed