Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/idempiere/idempiere/llms.txt

Use this file to discover all available pages before exploring further.

Callouts are field-level triggers that execute when a field value changes. They’re used to calculate values, validate input, set defaults, and enforce business rules in real-time.

What Are Callouts?

A callout is a Java method that executes when:
  • A user changes a field value in the UI
  • A field receives focus (optional)
  • Related fields need updating
Common use cases:
  • Calculate line amounts when quantity or price changes
  • Auto-populate related fields (e.g., set price when product is selected)
  • Validate business rules (e.g., check credit limit)
  • Set default values based on context

Callout Architecture

iDempiere supports two callout styles:
  1. Traditional Callouts - Extend CalloutEngine, use method signatures
  2. Annotation-based Callouts - Implement IColumnCallout, use annotations (modern approach)
We’ll focus on the traditional approach as it’s more commonly used in the codebase.

Creating a Callout

Basic Callout Structure

Here’s a real example from the iDempiere source:
org.compiere.model.CalloutOrder (excerpt from ~/workspace/source/org.adempiere.base.callout/)
package org.compiere.model;

import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;
import java.util.logging.Level;

import org.compiere.util.CLogger;
import org.compiere.util.DB;
import org.compiere.util.Env;

/**
 * Order Callouts
 * Field-level triggers for sales orders
 */
public class CalloutOrder extends CalloutEngine {
    
    /**
     * Order Header Change - DocType
     * Sets invoice/delivery/payment rules based on document type
     * 
     * @param ctx      Context
     * @param WindowNo current Window No
     * @param mTab     Model Tab
     * @param mField   Model Field
     * @param value    The new value
     * @return Error message or ""
     */
    public String docType(Properties ctx, int WindowNo, GridTab mTab, 
                         GridField mField, Object value) {
        Integer C_DocType_ID = (Integer)value;
        if (C_DocType_ID == null || C_DocType_ID.intValue() == 0)
            return "";
        
        String sql = "SELECT d.DocSubTypeSO, d.HasCharges, " +
                     "d.IsDocNoControlled, s.AD_Sequence_ID, d.IsSOTrx " +
                     "FROM C_DocType d " +
                     "LEFT OUTER JOIN AD_Sequence s ON (d.DocNoSequence_ID=s.AD_Sequence_ID) " +
                     "WHERE C_DocType_ID=?";
        
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = DB.prepareStatement(sql, null);
            pstmt.setInt(1, C_DocType_ID.intValue());
            rs = pstmt.executeQuery();
            
            if (rs.next()) {
                // Set context variables
                String docSubTypeSO = rs.getString("DocSubTypeSO");
                Env.setContext(ctx, WindowNo, "DocSubTypeSO", 
                              docSubTypeSO == null ? "" : docSubTypeSO);
                
                Env.setContext(ctx, WindowNo, "HasCharges", 
                              rs.getString("HasCharges"));
                
                // Set document number if controlled
                if ("Y".equals(rs.getString("IsDocNoControlled"))) {
                    int AD_Sequence_ID = rs.getInt("AD_Sequence_ID");
                    mTab.setValue("DocumentNo", 
                                 MSequence.getPreliminaryNo(mTab, AD_Sequence_ID));
                }
            }
        } catch (Exception e) {
            log.log(Level.SEVERE, sql, e);
            return e.getLocalizedMessage();
        } finally {
            DB.close(rs, pstmt);
        }
        
        return "";
    }
}

Callout Method Signature

All callout methods must follow this signature:
public String methodName(Properties ctx, int WindowNo, GridTab mTab, 
                        GridField mField, Object value)
Parameters:
  • ctx - Application context containing session variables
  • WindowNo - Window instance number
  • mTab - Tab model (access to all fields and data)
  • mField - The field that triggered the callout
  • value - The new value of the field
Return:
  • Empty string "" for success
  • Error message string to display to user and prevent save

Real-World Example: Invoice Callout

From org.compiere.model.CalloutInvoice:
org.compiere.model.CalloutInvoice (excerpt)
package org.compiere.model;

import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;
import java.util.logging.Level;

import org.compiere.util.DB;
import org.compiere.util.Env;

/**
 * Invoice Callouts
 */
public class CalloutInvoice extends CalloutEngine {
    
    /**
     * Invoice Header - Business Partner
     * Sets price list, location, user, payment terms when BP selected
     */
    public String bPartner(Properties ctx, int WindowNo, GridTab mTab, 
                          GridField mField, Object value) {
        Integer C_BPartner_ID = (Integer)value;
        if (C_BPartner_ID == null || C_BPartner_ID.intValue() == 0)
            return "";
        
        String sql = "SELECT p.AD_Language, p.C_PaymentTerm_ID, " +
            "COALESCE(p.M_PriceList_ID, g.M_PriceList_ID) AS M_PriceList_ID, " +
            "p.PaymentRule, p.POReference, p.SO_Description, " +
            "p.IsDiscountPrinted, p.SO_CreditLimit, " +
            "p.SO_CreditLimit - p.SO_CreditUsed AS CreditAvailable, " +
            "(SELECT MAX(lbill.C_BPartner_Location_ID) " +
            " FROM C_BPartner_Location lbill " +
            " WHERE p.C_BPartner_ID=lbill.C_BPartner_ID " +
            " AND lbill.IsBillTo='Y' AND lbill.IsActive='Y') AS C_BPartner_Location_ID " +
            "FROM C_BPartner p " +
            "INNER JOIN C_BP_Group g ON (p.C_BP_Group_ID=g.C_BP_Group_ID) " +
            "WHERE p.C_BPartner_ID=? AND p.IsActive='Y'";
        
        boolean IsSOTrx = Env.getContext(ctx, WindowNo, "IsSOTrx").equals("Y");
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        
        try {
            pstmt = DB.prepareStatement(sql, null);
            pstmt.setInt(1, C_BPartner_ID.intValue());
            rs = pstmt.executeQuery();
            
            if (rs.next()) {
                // Set price list
                Integer priceList_ID = rs.getInt("M_PriceList_ID");
                if (!rs.wasNull())
                    mTab.setValue("M_PriceList_ID", priceList_ID);
                
                // Set payment term
                Integer paymentTerm_ID = rs.getInt("C_PaymentTerm_ID");
                if (!rs.wasNull())
                    mTab.setValue("C_PaymentTerm_ID", paymentTerm_ID);
                
                // Set payment rule
                String paymentRule = rs.getString("PaymentRule");
                if (paymentRule != null)
                    mTab.setValue("PaymentRule", paymentRule);
                
                // Set location
                Integer location_ID = rs.getInt("C_BPartner_Location_ID");
                if (!rs.wasNull())
                    mTab.setValue("C_BPartner_Location_ID", location_ID);
                
                // Check credit limit for sales transactions
                if (IsSOTrx) {
                    BigDecimal creditLimit = rs.getBigDecimal("SO_CreditLimit");
                    BigDecimal creditAvailable = rs.getBigDecimal("CreditAvailable");
                    
                    if (creditLimit != null && creditLimit.signum() > 0 &&
                        creditAvailable != null && creditAvailable.signum() < 0) {
                        // Warning: over credit limit
                        mTab.fireDataStatusEEvent("CreditLimitOver", 
                                                  "Credit Limit exceeded", false);
                    }
                }
            }
        } catch (Exception e) {
            log.log(Level.SEVERE, sql, e);
            return e.getLocalizedMessage();
        } finally {
            DB.close(rs, pstmt);
        }
        
        return "";
    }
}

Working with Tab Values

Getting Field Values

// Get value as Object
Object value = mTab.getValue("FieldName");

// Get typed values
Integer id = (Integer) mTab.getValue("C_BPartner_ID");
BigDecimal amount = (BigDecimal) mTab.getValue("GrandTotal");
String docNo = (String) mTab.getValue("DocumentNo");
Boolean isActive = (Boolean) mTab.getValue("IsActive");

// Check if null
if (mTab.getValue("C_BPartner_ID") == null) {
    // Handle null
}

Setting Field Values

// Set field value
mTab.setValue("FieldName", value);

// Examples
mTab.setValue("C_PaymentTerm_ID", 105);
mTab.setValue("GrandTotal", new BigDecimal("1000.00"));
mTab.setValue("Description", "Auto-populated");
mTab.setValue("IsApproved", Boolean.TRUE);

Working with Context

// Set context variable (available in display logic)
Env.setContext(ctx, WindowNo, "HasCharges", "Y");

// Get context variable
String isSOTrx = Env.getContext(ctx, WindowNo, "IsSOTrx");

// Get global context (session variables)
int AD_Client_ID = Env.getContextAsInt(ctx, "#AD_Client_ID");
int AD_Org_ID = Env.getContextAsInt(ctx, WindowNo, "AD_Org_ID");
String dateFormat = Env.getContext(ctx, "#DateFormat");

Registering Callouts

Method 1: Application Dictionary

Register in the AD_Column table:
UPDATE AD_Column
SET Callout = 'org.compiere.model.CalloutOrder.docType'
WHERE AD_Column_ID = 2161;  -- C_DocType_ID column
Format: ClassName.methodName

Method 2: Plugin Extension Point

For plugins, use the org.adempiere.base.Callout extension point. Create plugin.xml:
plugin.xml
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
    <extension 
        id="com.yourcompany.customCallout" 
        point="org.adempiere.base.Callout">
        <callout 
            class="com.yourcompany.plugin.CustomCallout"
            tableName="C_Order"
            columnName="C_BPartner_ID">
        </callout>
    </extension>
</plugin>
Key attributes:
  • class - Full class name implementing IColumnCallout
  • tableName - Table name (case-sensitive)
  • columnName - Column name (case-sensitive)
  • priority - Optional, higher value = higher priority

Method 3: Annotation-Based (Modern)

Implement IColumnCallout interface:
com.yourcompany.plugin.ModernCallout
package com.yourcompany.plugin;

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

public class ModernCallout implements IColumnCallout {
    
    @Override
    public String start(Properties ctx, int WindowNo, GridTab mTab,
                       GridField mField, Object value, Object oldValue) {
        // Your callout logic here
        return "";
    }
}

Common Patterns

Pattern 1: Calculate Line Total

public String qty(Properties ctx, int WindowNo, GridTab mTab,
                 GridField mField, Object value) {
    BigDecimal qty = (BigDecimal) value;
    if (qty == null) qty = BigDecimal.ZERO;
    
    BigDecimal price = (BigDecimal) mTab.getValue("PriceActual");
    if (price == null) price = BigDecimal.ZERO;
    
    BigDecimal lineNet = qty.multiply(price);
    mTab.setValue("LineNetAmt", lineNet);
    
    return "";
}

Pattern 2: Validate Before Setting

public String creditLimit(Properties ctx, int WindowNo, GridTab mTab,
                         GridField mField, Object value) {
    BigDecimal grandTotal = (BigDecimal) mTab.getValue("GrandTotal");
    if (grandTotal == null) return "";
    
    Integer bpID = (Integer) mTab.getValue("C_BPartner_ID");
    if (bpID == null) return "";
    
    MBPartner bp = new MBPartner(ctx, bpID, null);
    BigDecimal creditLimit = bp.getSO_CreditLimit();
    BigDecimal creditUsed = bp.getSO_CreditUsed();
    BigDecimal creditAvailable = creditLimit.subtract(creditUsed);
    
    if (grandTotal.compareTo(creditAvailable) > 0) {
        return "Order total exceeds available credit: " + creditAvailable;
    }
    
    return "";
}

Pattern 3: Conditional Field Population

public String product(Properties ctx, int WindowNo, GridTab mTab,
                     GridField mField, Object value) {
    Integer M_Product_ID = (Integer) value;
    if (M_Product_ID == null || M_Product_ID <= 0) return "";
    
    MProduct product = MProduct.get(ctx, M_Product_ID);
    
    // Set description
    mTab.setValue("Description", product.getName());
    
    // Set UOM
    mTab.setValue("C_UOM_ID", product.getC_UOM_ID());
    
    // Set price from price list
    Integer M_PriceList_ID = (Integer) mTab.getValue("M_PriceList_ID");
    if (M_PriceList_ID != null) {
        MProductPrice pp = MProductPrice.get(ctx, M_PriceList_ID, 
                                            M_Product_ID, null);
        if (pp != null) {
            mTab.setValue("PriceList", pp.getPriceList());
            mTab.setValue("PriceActual", pp.getPriceStd());
        }
    }
    
    return "";
}

Callout Best Practices

Callouts execute in the UI thread. Avoid slow database queries or complex calculations. Use indexes and optimize SQL.
Always check for null before using values:
Integer id = (Integer) mTab.getValue("C_BPartner_ID");
if (id == null || id.intValue() == 0) return "";
Always close database resources in finally blocks:
finally {
    DB.close(rs, pstmt);
    rs = null; 
    pstmt = null;
}
Don’t create circular dependencies where Callout A sets Field B which triggers Callout B which sets Field A.
Return descriptive error messages to help users understand what went wrong:
return "Credit limit exceeded. Available: " + creditAvailable;

Debugging Callouts

Enable debug logging in log4j2.xml:
<Logger name="org.compiere.model" level="DEBUG"/>
Add logging to your callout:
import java.util.logging.Level;

public String myCallout(Properties ctx, int WindowNo, GridTab mTab,
                       GridField mField, Object value) {
    log.info("Callout triggered: " + mField.getColumnName() + " = " + value);
    
    try {
        // Your logic
    } catch (Exception e) {
        log.log(Level.SEVERE, "Callout error", e);
        return e.getLocalizedMessage();
    }
    
    return "";
}
Use the iDempiere System Configurator window to enable SQL logging and see exactly what queries your callout executes.

Next Steps

Processes

Create batch processes and reports

Model Validators

Handle document lifecycle events