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:
Traditional Callouts - Extend CalloutEngine, use method signatures
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:
<? 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